diff --git a/.mergify.yml b/.mergify.yml new file mode 100644 index 0000000000..b0e11e0e6d --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,13 @@ +pull_request_rules: + - name: Automatic merge on CI success and review + conditions: + - status-success=Codacy/PR Quality Review + - status-success=Semantic Pull Request + - status-success=continuous-integration/travis-ci/pr + - status-success=security/snyk - package.json (frappe) + - status-success=security/snyk - requirements.txt (frappe) + - label!=don't-merge + - "#approved-reviews-by>=1" + actions: + merge: + method: merge diff --git a/.travis.yml b/.travis.yml index 0dbd382702..df66db88a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,43 +2,88 @@ language: python dist: trusty sudo: required -python: - - 2.7 - - 3.6 - -env: - - DB=mariadb - - DB=postgres - - TEST_TYPE=ui - -services: - - mysql - addons: - postgresql: "9.5" hosts: - test_site - - test_site_postgres - - test_site_ui + mariadb: 10.3 + postgresql: 9.5 + +git: + depth: 1 + +cache: + - pip + - npm + - yarn matrix: - exclude: - - python: 2.7 - env: DB=postgres - - python: 2.7 - env: TEST_TYPE=ui + include: + - name: "Python 3.6 MariaDB" + python: 3.6 + env: DB=mariadb TYPE=server + script: bench --site test_site run-tests --coverage + + - name: "Python 3.6 PostgreSQL" + python: 3.6 + env: DB=postgres TYPE=server + script: bench --site test_site run-tests --coverage + + - name: "Cypress" + python: 3.6 + env: DB=mariadb TYPE=ui + before_script: bench --site test_site execute frappe.utils.install.complete_setup_wizard + script: bench --site test_site run-ui-tests frappe --headless + + - name: "Python 2.7 MariaDB" + python: 2.7 + env: DB=mariadb TYPE=server + script: bench --site test_site run-tests --coverage install: - - $TRAVIS_BUILD_DIR/.travis/install.sh + - cd ~ + - source ./.nvm/nvm.sh + - nvm install v8.10.0 + + - git clone https://github.com/frappe/bench --depth 1 + - pip install -e ./bench + + - bench init frappe-bench --skip-assets --python $(which python) --frappe-path $TRAVIS_BUILD_DIR + + - mkdir ~/frappe-bench/sites/test_site + - cp $TRAVIS_BUILD_DIR/.travis/$DB.json ~/frappe-bench/sites/test_site/site_config.json + + - mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'" + - mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + + - mysql -u root -e "CREATE DATABASE test_frappe" + - mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'" + - mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'" + + - mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'" + - mysql -u root -e "FLUSH PRIVILEGES" + + - psql -c "CREATE DATABASE test_frappe" -U postgres + - psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres + + - 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 + - sudo chmod o+x /usr/local/bin/wkhtmltopdf + + - cd ./frappe-bench + + - sed -i 's/watch:/# watch:/g' Procfile + - sed -i 's/schedule:/# schedule:/g' Procfile + + - 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 + + - if [ $TYPE == "ui" ]; then bench setup requirements --node; fi -before_script: - - cd ~/frappe-bench - - sed -i 's/9000/9001/g' sites/common_site_config.json - bench start & - - sleep 10 - -script: - - $TRAVIS_BUILD_DIR/.travis/run-tests.sh + - bench --site test_site reinstall --yes + - bench build --app frappe after_script: - - coveralls -b apps/frappe -d ../../sites/.coverage \ No newline at end of file + - pip install python-coveralls + - coveralls -b apps/frappe -d ../../sites/.coverage diff --git a/.travis/install.sh b/.travis/install.sh deleted file mode 100755 index a457939b22..0000000000 --- a/.travis/install.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -set -e - -sudo rm /etc/apt/sources.list.d/mongodb*.list -sudo rm /etc/apt/sources.list.d/docker.list -sudo apt-get install hhvm && rm -rf /home/travis/.kiex/ -sudo apt-get purge -y mysql-common mysql-server mysql-client -source ~/.nvm/nvm.sh -nvm install v8.10.0 - -pip install python-coveralls - -wget https://raw.githubusercontent.com/frappe/bench/master/playbooks/install.py - -sudo python install.py --develop --user travis --without-bench-setup -sudo pip install -e ~/bench - -rm $TRAVIS_BUILD_DIR/.git/shallow -cd ~/ && bench init frappe-bench --python $(which python) --frappe-path $TRAVIS_BUILD_DIR -if [[ $DB == 'mariadb' ]]; then - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ -elif [[ $TEST_TYPE == 'ui' ]]; then - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site_ui ~/frappe-bench/sites/ -elif [[ $DB == 'postgres' ]]; then - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site_postgres ~/frappe-bench/sites/ -fi diff --git a/.travis/mariadb.json b/.travis/mariadb.json new file mode 100644 index 0000000000..550ad94769 --- /dev/null +++ b/.travis/mariadb.json @@ -0,0 +1,14 @@ +{ + "db_host": "localhost", + "db_name": "test_frappe", + "db_password": "test_frappe", + "db_type": "mariadb", + "auto_email_id": "test@example.com", + "mail_server": "smtp.example.com", + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "root_login": "root", + "root_password": "travis", + "host_name": "http://test_site:8000" +} diff --git a/.travis/postgres.json b/.travis/postgres.json new file mode 100644 index 0000000000..619dd91f10 --- /dev/null +++ b/.travis/postgres.json @@ -0,0 +1,14 @@ +{ + "db_host": "localhost", + "db_name": "test_frappe", + "db_password": "test_frappe", + "db_type": "postgres", + "auto_email_id": "test@example.com", + "mail_server": "smtp.example.com", + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "root_login": "postgres", + "root_password": "travis", + "host_name": "http://test_site:8000" +} diff --git a/.travis/run-tests.sh b/.travis/run-tests.sh deleted file mode 100755 index 177e191088..0000000000 --- a/.travis/run-tests.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -set -e - -setup_mariadb_env() { - mysql -u root -ptravis -e "create database $1" - mysql -u root -ptravis -e "USE mysql; CREATE USER '$1'@'localhost' IDENTIFIED BY '$1'; FLUSH PRIVILEGES; " - mysql -u root -ptravis -e "USE mysql; GRANT ALL PRIVILEGES ON \`$1\`.* TO '$1'@'localhost';" -} - -if [[ $DB == 'mariadb' ]]; then - setup_mariadb_env 'test_frappe' - bench --site test_site reinstall --yes - bench --site test_site scheduler disable - bench --site test_site run-tests --coverage - -elif [[ $TEST_TYPE == 'ui' ]]; then - setup_mariadb_env 'test_site_ui' - bench --site test_site_ui reinstall --yes - bench --site test_site_ui execute frappe.utils.install.complete_setup_wizard - bench --site test_site_ui scheduler disable - cd apps/frappe && yarn && yarn cypress:run - -elif [[ $DB == 'postgres' ]]; then - psql -c "CREATE DATABASE test_frappe;" -U postgres - psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe';" -U postgres - bench --site test_site_postgres reinstall --yes - bench --site test_site_postgres scheduler disable - bench --site test_site_postgres run-tests --coverage -fi diff --git a/cypress.json b/cypress.json index bd93405273..7d853271b9 100644 --- a/cypress.json +++ b/cypress.json @@ -1,4 +1,5 @@ { "baseUrl": "http://test_site_ui:8000", - "projectId": "92odwv" + "projectId": "92odwv", + "adminPassword": "admin" } diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js index 18bfde520c..eafb5b338e 100644 --- a/cypress/integration/awesome_bar.js +++ b/cypress/integration/awesome_bar.js @@ -1,7 +1,7 @@ context('Awesome Bar', () => { before(() => { cy.visit('/login'); - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); }); @@ -10,8 +10,9 @@ context('Awesome Bar', () => { }); it('navigates to doctype list', () => { - cy.get('#navbar-search') - .type('todo{downarrow}{enter}', { delay: 100 }); + cy.get('#navbar-search').type('todo', { delay: 200 }); + cy.get('#navbar-search + ul').should('be.visible'); + cy.get('#navbar-search').type('{downarrow}{enter}', { delay: 100 }); cy.get('h1').should('contain', 'To Do'); @@ -20,7 +21,7 @@ context('Awesome Bar', () => { it('find text in doctype list', () => { cy.get('#navbar-search') - .type('test in todo{downarrow}{enter}', { delay: 100 }); + .type('test in todo{downarrow}{enter}', { delay: 200 }); cy.get('h1').should('contain', 'To Do'); @@ -31,14 +32,14 @@ context('Awesome Bar', () => { it('navigates to new form', () => { cy.get('#navbar-search') - .type('new blog post{downarrow}{enter}', { delay: 100 }); + .type('new blog post{downarrow}{enter}', { delay: 200 }); cy.get('.title-text:visible').should('have.text', 'New Blog Post 1'); }); it('calculates math expressions', () => { cy.get('#navbar-search') - .type('55 + 32{downarrow}{enter}', { delay: 100 }); + .type('55 + 32{downarrow}{enter}', { delay: 200 }); cy.get('.modal-title').should('contain', 'Result'); cy.get('.msgprint').should('contain', '55 + 32 = 87'); diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js new file mode 100644 index 0000000000..0524905cce --- /dev/null +++ b/cypress/integration/control_link.js @@ -0,0 +1,75 @@ +context('Control Link', () => { + beforeEach(() => { + cy.login(); + cy.visit('/desk'); + cy.create_records({ + doctype: 'ToDo', + description: 'this is a test todo for link' + }).as('todos'); + }); + + function get_dialog_with_link() { + return cy.dialog({ + title: 'Link', + fields: [ + { + 'label': 'Select ToDo', + 'fieldname': 'link', + 'fieldtype': 'Link', + 'options': 'ToDo' + } + ] + }); + } + + it('should set the valid value', () => { + get_dialog_with_link().as('dialog'); + + cy.server(); + cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link'); + + cy.get('.frappe-control[data-fieldname=link] input') + .focus() + .type('todo for li') + .type('n', { delay: 600 }) + .type('k', { delay: 700 }); + cy.wait('@search_link'); + cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible'); + cy.get('.frappe-control[data-fieldname=link] input').type('{downarrow}{enter}', { delay: 100 }); + cy.get('.frappe-control[data-fieldname=link] input').blur(); + cy.get('@dialog').then(dialog => { + cy.get('@todos').then(todos => { + let value = dialog.get_value('link'); + expect(value).to.eq(todos[0]); + }); + }); + }); + + it.only('should unset invalid value', () => { + get_dialog_with_link().as('dialog'); + + cy.server(); + cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link'); + + cy.get('.frappe-control[data-fieldname=link] input') + .type('invalid value', { delay: 100 }) + .blur(); + cy.wait('@validate_link'); + cy.get('.frappe-control[data-fieldname=link] input').should('have.value', ''); + }); + + it('should route to form on arrow click', () => { + get_dialog_with_link().as('dialog'); + + cy.server(); + cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link'); + + cy.get('@todos').then(todos => { + cy.get('.frappe-control[data-fieldname=link] input').type(todos[0]).blur(); + cy.wait('@validate_link'); + cy.get('.frappe-control[data-fieldname=link] input').focus(); + cy.get('.frappe-control[data-fieldname=link] .link-btn').click(); + cy.location('hash').should('eq', `#Form/ToDo/${todos[0]}`); + }); + }); +}); diff --git a/cypress/integration/control_rating.js b/cypress/integration/control_rating.js index a367a82273..5fa479b888 100644 --- a/cypress/integration/control_rating.js +++ b/cypress/integration/control_rating.js @@ -1,14 +1,21 @@ -context('Rating Control', () => { - beforeEach(() => { - cy.login('Administrator', 'qwe'); +context('Control Rating', () => { + before(() => { + cy.login(); + cy.visit('/desk'); }); + function get_dialog_with_rating() { + return cy.dialog({ + title: 'Rating', + fields: [{ + 'fieldname': 'rate', + 'fieldtype': 'Rating', + }] + }); + } + it('click on the star rating to record value', () => { - cy.visit('/desk'); - cy.dialog('Rating', [{ - 'fieldname': 'rate', - 'fieldtype': 'Rating', - }]).as('dialog'); + get_dialog_with_rating().as('dialog'); cy.get('div.rating') .children('i.fa') @@ -18,15 +25,13 @@ context('Rating Control', () => { cy.get('@dialog').then(dialog => { var value = dialog.get_value('rate'); expect(value).to.equal(1); + dialog.hide(); }); }); it('hover on the star', () => { - cy.visit('/desk'); - cy.dialog('Rating', [{ - 'fieldname': 'rate', - 'fieldtype': 'Rating', - }]); + get_dialog_with_rating(); + cy.get('div.rating') .children('i.fa') .first() diff --git a/cypress/integration/file_uploader.js b/cypress/integration/file_uploader.js index b58e0d49a8..787644b596 100644 --- a/cypress/integration/file_uploader.js +++ b/cypress/integration/file_uploader.js @@ -1,6 +1,6 @@ context('FileUploader', () => { before(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); }); diff --git a/cypress/integration/form.js b/cypress/integration/form.js index 9bab715d5b..b7ddd6ecb7 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -1,6 +1,6 @@ context('Form', () => { before(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); }); diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js index f4deb66c99..52c6483ab4 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -1,9 +1,9 @@ context('List View', () => { before(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); cy.window().its('frappe').then(frappe => { - frappe.call("frappe.tests.test_utils.setup_workflow"); + frappe.call("frappe.tests.ui_test_helpers.setup_workflow"); }); cy.clear_cache(); }); diff --git a/cypress/integration/list_view_settings.js b/cypress/integration/list_view_settings.js index 59921b162c..84131386f6 100644 --- a/cypress/integration/list_view_settings.js +++ b/cypress/integration/list_view_settings.js @@ -1,12 +1,12 @@ context('List View Settings', () => { beforeEach(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); }); it('Default settings', () => { cy.visit('/desk#List/DocType/List'); cy.get('.list-count').should('contain', "20 of"); - cy.get('.sidebar-stat').should('contain', "No Tags"); + cy.get('.sidebar-stat').should('contain', "Tags"); }); it('disable count and sidebar stats then verify', () => { cy.visit('/desk#List/DocType/List'); @@ -14,13 +14,13 @@ context('List View Settings', () => { cy.get('button').contains('Menu').click(); cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click(); cy.get('.modal-dialog').should('contain', 'Settings'); - + cy.get('input[data-fieldname="disable_count"]').check({force: true}); cy.get('input[data-fieldname="disable_sidebar_stats"]').check({force: true}); cy.get('button').filter(':visible').contains('Save').click(); - + cy.reload(); - + cy.get('.list-count').should('be.empty'); cy.get('.list-sidebar .sidebar-stat').should('not.exist'); diff --git a/cypress/integration/login.js b/cypress/integration/login.js index 4da1f39229..3f13130b58 100644 --- a/cypress/integration/login.js +++ b/cypress/integration/login.js @@ -23,7 +23,7 @@ context('Login', () => { it('logs in using correct credentials', () => { cy.get('#login_email').type('Administrator'); - cy.get('#login_password').type('qwe'); + cy.get('#login_password').type(Cypress.config('adminPassword')); cy.get('.btn-login').click(); cy.location('pathname').should('eq', '/desk'); diff --git a/cypress/integration/query_report.js b/cypress/integration/query_report.js index 04bc1a6fd4..8aa6279887 100644 --- a/cypress/integration/query_report.js +++ b/cypress/integration/query_report.js @@ -1,6 +1,6 @@ context('Form', () => { before(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); }); diff --git a/cypress/integration/recorder.js b/cypress/integration/recorder.js index f037005dbb..e8b55a9d12 100644 --- a/cypress/integration/recorder.js +++ b/cypress/integration/recorder.js @@ -1,6 +1,6 @@ context('Recorder', () => { before(() => { - cy.login('Administrator', 'qwe'); + cy.login(); }); it('Navigate to Recorder', () => { diff --git a/cypress/integration/relative_filters.js b/cypress/integration/relative_filters.js index c071ce0355..27f594f96e 100644 --- a/cypress/integration/relative_filters.js +++ b/cypress/integration/relative_filters.js @@ -1,13 +1,13 @@ context('Relative Timeframe', () => { beforeEach(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); }); before(() => { - cy.login('Administrator', 'qwe'); + cy.login(); cy.visit('/desk'); cy.window().its('frappe').then(frappe => { - frappe.call("frappe.tests.test_utils.create_todo_records"); + frappe.call("frappe.tests.ui_test_helpers.create_todo_records"); }); }); it('set relative filter for Previous and check list', () => { diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js index cca9a6eb46..e75baf05f1 100644 --- a/cypress/integration/table_multiselect.js +++ b/cypress/integration/table_multiselect.js @@ -1,6 +1,6 @@ context('Table MultiSelect', () => { beforeEach(() => { - cy.login('Administrator', 'qwe'); + cy.login(); }); let name = 'table multiselect' + Math.random().toString().slice(2, 8); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 850898d4c5..84d896dbb0 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -25,6 +25,12 @@ import 'cypress-file-upload'; // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); Cypress.Commands.add('login', (email, password) => { + if (!email) { + email = 'Administrator'; + } + if (!password) { + password = Cypress.config('adminPassword'); + } cy.request({ url: '/api/method/login', method: 'POST', @@ -35,6 +41,29 @@ Cypress.Commands.add('login', (email, password) => { }); }); +Cypress.Commands.add('call', (method, args) => { + return cy.window().its('frappe.csrf_token').then(csrf_token => { + return cy.request({ + url: `/api/method/${method}`, + method: 'POST', + body: args, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }).then(res => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add('create_records', (doc) => { + return cy.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc }) + .then(r => r.message); +}); + Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => { let selector = `.form-control[data-fieldname="${fieldname}"]`; @@ -72,15 +101,9 @@ Cypress.Commands.add('clear_cache', () => { }); }); -Cypress.Commands.add('dialog', (title, fields) => { - cy.window().then(win => { - var d = new win.frappe.ui.Dialog({ - title: title, - fields: fields, - primary_action: function(){ - d.hide(); - } - }); +Cypress.Commands.add('dialog', (opts) => { + return cy.window().then(win => { + var d = new win.frappe.ui.Dialog(opts); d.show(); return d; }); diff --git a/frappe/__init__.py b/frappe/__init__.py index d4bade75a8..c19295a95c 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -17,7 +17,7 @@ from faker import Faker from .exceptions import * from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader) -# Hamless for Python 3 +# Harmless for Python 3 # For Python 2 set default encoding to utf-8 if sys.version[0] == '2': reload(sys) diff --git a/frappe/desk/doctype/auto_repeat/__init__.py b/frappe/automation/doctype/auto_repeat/__init__.py similarity index 100% rename from frappe/desk/doctype/auto_repeat/__init__.py rename to frappe/automation/doctype/auto_repeat/__init__.py diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js new file mode 100644 index 0000000000..a11de1d881 --- /dev/null +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -0,0 +1,104 @@ +// Copyright (c) 2018, Frappe Technologies and contributors +// For license information, please see license.txt +frappe.provide("frappe.auto_repeat"); + +frappe.ui.form.on('Auto Repeat', { + setup: function(frm) { + frm.fields_dict['reference_doctype'].get_query = function() { + return { + query: "frappe.automation.doctype.auto_repeat.auto_repeat.get_auto_repeat_doctypes" + }; + }; + + frm.fields_dict['reference_document'].get_query = function() { + return { + filters: { + "auto_repeat": '' + } + }; + }; + + frm.fields_dict['print_format'].get_query = function() { + return { + filters: { + "doc_type": frm.doc.reference_doctype + } + }; + }; + }, + + refresh: function(frm) { + // auto repeat message + if (frm.is_new()) { + let customize_form_link = `${__('Customize Form')}`; + frm.dashboard.set_headline(__('To configure Auto Repeat, enable "Allow Auto Repeat" from {0}.', [customize_form_link])); + } + + // view document button + if (!frm.is_dirty()) { + let label = __('View {0}', [__(frm.doc.reference_doctype)]); + frm.add_custom_button(label, () => + frappe.set_route("List", frm.doc.reference_doctype, { auto_repeat: frm.doc.name }) + ); + } + + // auto repeat schedule + frappe.auto_repeat.render_schedule(frm); + }, + + template: function(frm) { + if (frm.doc.template) { + frappe.model.with_doc("Email Template", frm.doc.template, () => { + let email_template = frappe.get_doc("Email Template", frm.doc.template); + frm.set_value("subject", email_template.subject); + frm.set_value("message", email_template.response); + frm.refresh_field("subject"); + frm.refresh_field("message"); + }); + } + }, + + get_contacts: function(frm) { + frm.call('fetch_linked_contacts'); + }, + + preview_message: function(frm) { + if (frm.doc.message) { + frappe.call({ + method: "frappe.automation.doctype.auto_repeat.auto_repeat.generate_message_preview", + args: { + reference_dt: frm.doc.reference_doctype, + reference_doc: frm.doc.reference_document, + subject: frm.doc.subject, + message: frm.doc.message + }, + callback: function(r) { + if (r.message) { + frappe.msgprint(r.message.message, r.message.subject) + } + } + }); + } else { + frappe.msgprint(__("Please setup a message first"), __("Message not setup")) + } + } +}); + +frappe.auto_repeat.render_schedule = function(frm) { + if (!frm.is_dirty() && frm.doc.status !== 'Disabled') { + frappe.call({ + method: "get_auto_repeat_schedule", + doc: frm.doc + }).done((r) => { + frm.dashboard.wrapper.empty(); + frm.dashboard.add_section( + frappe.render_template("auto_repeat_schedule", { + schedule_details : r.message || [] + }) + ); + frm.dashboard.show(); + }); + } else { + frm.dashboard.hide(); + } +}; diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json new file mode 100644 index 0000000000..8ee6ca1d45 --- /dev/null +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json @@ -0,0 +1,239 @@ +{ + "allow_import": 1, + "allow_rename": 1, + "autoname": "format:AUT-AR-{#####}", + "creation": "2018-03-09 11:22:31.192349", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_1", + "disabled", + "section_break_3", + "reference_doctype", + "reference_document", + "column_break_5", + "start_date", + "end_date", + "section_break_10", + "frequency", + "repeat_on_day", + "repeat_on_last_day", + "column_break_12", + "next_schedule_date", + "notification", + "notify_by_email", + "recipients", + "get_contacts", + "template", + "subject", + "message", + "preview_message", + "print_format", + "status" + ], + "fields": [ + { + "fieldname": "section_break_1", + "fieldtype": "Section Break" + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "reference_document", + "fieldtype": "Dynamic Link", + "label": "Reference Document", + "no_copy": 1, + "options": "reference_doctype", + "reqd": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Start Date", + "reqd": 1 + }, + { + "fieldname": "end_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "End Date" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled", + "no_copy": 1 + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fieldname": "frequency", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Frequency", + "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly", + "reqd": 1 + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: in_list([\"Monthly\", \"Quarterly\", \"Half-yearly\", \"Yearly\"], doc.frequency) && !doc.repeat_on_last_day\n", + "fieldname": "repeat_on_day", + "fieldtype": "Int", + "label": "Repeat on Day" + }, + { + "fieldname": "next_schedule_date", + "fieldtype": "Date", + "label": "Next Schedule Date", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "collapsible": 1, + "fieldname": "notification", + "fieldtype": "Section Break", + "label": "Notification" + }, + { + "default": "0", + "fieldname": "notify_by_email", + "fieldtype": "Check", + "label": "Notify by Email" + }, + { + "depends_on": "notify_by_email", + "fieldname": "recipients", + "fieldtype": "Small Text", + "label": "Recipients" + }, + { + "depends_on": "eval: doc.notify_by_email && doc.reference_doctype && doc.reference_document", + "fieldname": "get_contacts", + "fieldtype": "Button", + "label": "Get Contacts" + }, + { + "depends_on": "eval: doc.notify_by_email", + "fieldname": "template", + "fieldtype": "Link", + "label": "Template", + "options": "Email Template" + }, + { + "depends_on": "eval: doc.notify_by_email", + "description": "To add dynamic subject, use jinja tags like\n\n
New {{ doc.doctype }} #{{ doc.name }}
", + "fieldname": "subject", + "fieldtype": "Data", + "label": "Subject" + }, + { + "default": "Please find attached {{ doc.doctype }} #{{ doc.name }}", + "depends_on": "eval: doc.notify_by_email", + "fieldname": "message", + "fieldtype": "Text", + "label": "Message" + }, + { + "depends_on": "eval: doc.notify_by_email && doc.reference_doctype && doc.reference_document", + "fieldname": "preview_message", + "fieldtype": "Button", + "label": "Preview Message" + }, + { + "depends_on": "notify_by_email", + "fieldname": "print_format", + "fieldtype": "Link", + "label": "Print Format", + "options": "Print Format" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "in_list_view": 1, + "label": "Status", + "options": "\nActive\nDisabled\nCompleted", + "read_only": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "default": "0", + "depends_on": "eval:doc.frequency === 'Monthly'", + "fieldname": "repeat_on_last_day", + "fieldtype": "Check", + "label": "Repeat on Last Day of the Month" + } + ], + "modified": "2019-07-17 11:30:51.412317", + "modified_by": "Administrator", + "module": "Automation", + "name": "Auto Repeat", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "search_fields": "reference_document", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "reference_document", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py new file mode 100644 index 0000000000..71bf9cfeb9 --- /dev/null +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -0,0 +1,375 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.desk.form import assign_to +from frappe.utils.jinja import validate_template +from dateutil.relativedelta import relativedelta +from frappe.utils.user import get_system_managers +from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day +from frappe.model.document import Document +from frappe.core.doctype.communication.email import make +from frappe.utils.background_jobs import get_jobs + +month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} + + +class AutoRepeat(Document): + def validate(self): + self.update_status() + self.validate_reference_doctype() + self.validate_dates() + self.validate_email_id() + self.set_dates() + self.update_auto_repeat_id() + self.unlink_if_applicable() + + validate_template(self.subject or "") + validate_template(self.message or "") + + def before_insert(self): + if not frappe.flags.in_test: + start_date = self.start_date + today_date = today() + if start_date <= today_date: + start_date = today_date + + def after_save(self): + frappe.get_doc(self.reference_doctype, self.reference_document).notify_update() + + def on_trash(self): + frappe.db.set_value(self.reference_doctype, self.reference_document, { + 'auto_repeat': self.name + }, 'auto_repeat', '') + + def set_dates(self): + if self.disabled: + self.next_schedule_date = None + else: + self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, self.end_date) + + def unlink_if_applicable(self): + if self.status == 'Completed' or self.disabled: + frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '') + + def validate_reference_doctype(self): + if not frappe.flags.in_test: + if not frappe.get_meta(self.reference_doctype).allow_auto_repeat: + frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype)) + + def validate_dates(self): + if self.end_date: + self.validate_from_to_dates('start_date', 'end_date') + + if self.end_date == self.start_date: + frappe.throw(_('{0} should not be same as {1}').format(frappe.bold('End Date'), frappe.bold('Start Date'))) + + def validate_email_id(self): + if self.notify_by_email: + if self.recipients: + email_list = split_emails(self.recipients.replace("\n", "")) + from frappe.utils import validate_email_address + + for email in email_list: + if not validate_email_address(email): + frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email)) + else: + frappe.throw(_("'Recipients' not specified")) + + def update_auto_repeat_id(self): + #check if document is already on auto repeat + auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat") + if auto_repeat and auto_repeat != self.name: + frappe.throw(_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat)) + else: + frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name) + + def update_status(self): + if self.disabled: + self.status = "Disabled" + elif self.is_completed(): + self.status = "Completed" + else: + self.status = "Active" + + def is_completed(self): + return self.end_date and getdate(self.end_date) < getdate(today()) + + def get_auto_repeat_schedule(self): + schedule_details = [] + start_date = getdate(self.start_date) + end_date = getdate(self.end_date) + today = frappe.utils.datetime.date.today() + + if start_date < today: + start_date = today + + if not self.end_date: + start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day) + row = { + "reference_document": self.reference_document, + "frequency": self.frequency, + "next_scheduled_date": start_date + } + schedule_details.append(row) + start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day) + + if self.end_date: + start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day) + while (getdate(start_date) < getdate(end_date)): + row = { + "reference_document" : self.reference_document, + "frequency" : self.frequency, + "next_scheduled_date" : start_date + } + schedule_details.append(row) + start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date) + + + return schedule_details + + def create_documents(self): + try: + new_doc = self.make_new_document() + if self.notify_by_email and self.recipients: + self.send_notification(new_doc) + except Exception: + error_log = frappe.log_error(frappe.get_traceback(), _("Auto Repeat Document Creation Failure")) + + self.disable_auto_repeat() + + if self.reference_document and not frappe.flags.in_test: + self.notify_error_to_user(error_log) + + def make_new_document(self): + reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document) + new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False) + self.update_doc(new_doc, reference_doc) + new_doc.insert(ignore_permissions = True) + + return new_doc + + def update_doc(self, new_doc, reference_doc): + new_doc.docstatus = 0 + if new_doc.meta.get_field('set_posting_time'): + new_doc.set('set_posting_time', 1) + + if new_doc.meta.get_field('auto_repeat'): + new_doc.set('auto_repeat', self.name) + + for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']: + if new_doc.meta.get_field(fieldname): + new_doc.set(fieldname, reference_doc.get(fieldname)) + + for data in new_doc.meta.fields: + if data.fieldtype == 'Date' and data.reqd: + new_doc.set(data.fieldname, self.next_schedule_date) + + self.set_auto_repeat_period(new_doc) + + auto_repeat_doc = frappe.get_doc('Auto Repeat', self.name) + + #for any action that needs to take place after the recurring document creation + #on recurring method of that doctype is triggered + new_doc.run_method('on_recurring', reference_doc = reference_doc, auto_repeat_doc = auto_repeat_doc) + + def set_auto_repeat_period(self, new_doc): + mcount = month_map.get(self.frequency) + if mcount and new_doc.meta.get_field('from_date') and new_doc.meta.get_field('to_date'): + last_ref_doc = frappe.db.get_all(doctype = self.reference_doctype, + fields = ['name', 'from_date', 'to_date'], + filters = [ + ['auto_repeat', '=', self.name], + ['docstatus', '<', 2], + ], + order_by = 'creation desc', + limit = 1) + + if not last_ref_doc: + return + + from_date = get_next_date(last_ref_doc[0].from_date, mcount) + + if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \ + (cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)): + to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount)) + else: + to_date = get_next_date(last_ref_doc[0].to_date, mcount) + + new_doc.set('from_date', from_date) + new_doc.set('to_date', to_date) + + def send_notification(self, new_doc): + """Notify concerned people about recurring document generation""" + subject = self.subject or '' + message = self.message or '' + + if not self.subject: + subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name) + elif "{" in self.subject: + subject = frappe.render_template(self.subject, {'doc': new_doc}) + + if not self.message: + message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name) + elif "{" in self.message: + message = frappe.render_template(self.message, {'doc': new_doc}) + + print_format = self.print_format or 'Standard' + + attachments = [frappe.attach_print(new_doc.doctype, new_doc.name, + file_name=new_doc.name, print_format=print_format)] + + make(doctype=new_doc.doctype, name=new_doc.name, recipients=self.recipients, + subject=subject, content=message, attachments=attachments, send_email=1) + + def fetch_linked_contacts(self): + if self.reference_doctype and self.reference_document: + res = frappe.db.get_all('Contact', + fields=['email_id'], + filters=[ + ['Dynamic Link', 'link_doctype', '=', self.reference_doctype], + ['Dynamic Link', 'link_name', '=', self.reference_document] + ]) + + email_ids = list(set([d.email_id for d in res])) + if not email_ids: + frappe.msgprint(_('No contacts linked to document'), alert=True) + else: + self.recipients = ', '.join(email_ids) + + def disable_auto_repeat(self): + frappe.db.set_value('Auto Repeat', self.name, 'disabled', 1) + + def notify_error_to_user(self, error_log): + recipients = get_system_managers(only_name=True) + self.owner + subject = _("Auto Repeat Document Creation Failed") + + form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document) + auto_repeat_failed_for = _('Auto Repeat failed for {0}').format(form_link) + + error_log_link =frappe.utils.get_link_to_form(error_log.reference_doctype, error_log.reference_document) + error_log_message = _('Check the Error Log for more information: {0}').format(error_log_link) + + frappe.sendmail( + recipients=recipients, + subject=subject, + template="auto_repeat_fail", + args={ + 'auto_repeat_failed_for': auto_repeat_failed_for, + 'error_log_message': error_log_message + }, + header=[subject, 'red'] + ) + + +def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day = False, end_date = None): + month_count = month_map.get(frequency) + if month_count and repeat_on_last_day: + next_date = get_next_date(start_date, month_count, 31) + elif month_count and repeat_on_day: + next_date = get_next_date(start_date, month_count, repeat_on_day) + elif month_count: + next_date = get_next_date(start_date, month_count) + else: + days = 7 if frequency == 'Weekly' else 1 + next_date = add_days(start_date, days) + + return next_date + +def get_next_date(dt, mcount, day=None): + dt = getdate(dt) + dt += relativedelta(months=mcount, day=day) + return dt + +#called through hooks +def make_auto_repeat_entry(): + enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' + jobs = get_jobs() + + if not jobs or enqueued_method not in jobs[frappe.local.site]: + date = getdate(today()) + data = get_auto_repeat_entries(date) + frappe.enqueue(enqueued_method, data=data) + +def create_repeated_entries(data): + for d in data: + doc = frappe.get_doc('Auto Repeat', d.name) + + current_date = getdate(today()) + schedule_date = getdate(doc.next_schedule_date) + + while schedule_date <= current_date and not doc.disabled: + doc.create_documents() + schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date) + + if schedule_date and not doc.disabled: + frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) + +def get_auto_repeat_entries(date=None): + if not date: + date = getdate(today()) + return frappe.db.get_all('Auto Repeat', filters=[ + ['next_schedule_date', '<=', date], + ['status', '=', 'Active'] + ]) + +#called through hooks +def set_auto_repeat_as_completed(): + auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']}) + for entry in auto_repeat: + doc = frappe.get_doc("Auto Repeat", entry.name) + if doc.is_completed(): + doc.status = 'Completed' + doc.save() + +@frappe.whitelist() +def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = today(), end_date = None): + doc = frappe.new_doc('Auto Repeat') + doc.reference_doctype = doctype + doc.reference_document = docname + doc.frequency = frequency + doc.start_date = start_date + if end_date: + doc.end_date = end_date + doc.save() + return doc + +#method for reference_doctype filter +def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters): + res = frappe.db.get_all('Property Setter', { + 'property': 'allow_auto_repeat', + 'value': '1', + }, ['doc_type']) + docs = [r.doc_type for r in res] + + res = frappe.db.get_all('DocType', { + 'allow_auto_repeat': 1, + }, ['name']) + docs += [r.name for r in res] + docs = set(list(docs)) + + return [[d] for d in docs] + +@frappe.whitelist() +def update_reference(docname, reference): + result = "" + try: + frappe.db.set_value("Auto Repeat", docname, "reference_document", reference) + result = "success" + except Exception as e: + result = "error" + raise e + return result + +@frappe.whitelist() +def generate_message_preview(reference_dt, reference_doc, message=None, subject=None): + doc = frappe.get_doc(reference_dt, reference_doc) + subject_preview = _("Please add a subject to your email") + msg_preview = frappe.render_template(message, {'doc': doc}) + if subject: + subject_preview = frappe.render_template(subject, {'doc': doc}) + + return {'message': msg_preview, 'subject': subject_preview} diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat_list.js b/frappe/automation/doctype/auto_repeat/auto_repeat_list.js new file mode 100644 index 0000000000..f906580f7e --- /dev/null +++ b/frappe/automation/doctype/auto_repeat/auto_repeat_list.js @@ -0,0 +1,11 @@ +frappe.listview_settings['Auto Repeat'] = { + add_fields: ["next_schedule_date"], + get_indicator: function(doc) { + var colors = { + "Active": "green", + "Disabled": "red", + "Completed": "blue", + }; + return [__(doc.status), colors[doc.status], "status,=," + doc.status]; + } +}; diff --git a/frappe/desk/doctype/auto_repeat/auto_repeat_schedule.html b/frappe/automation/doctype/auto_repeat/auto_repeat_schedule.html similarity index 74% rename from frappe/desk/doctype/auto_repeat/auto_repeat_schedule.html rename to frappe/automation/doctype/auto_repeat/auto_repeat_schedule.html index 7e579821c5..fb194f44a4 100644 --- a/frappe/desk/doctype/auto_repeat/auto_repeat_schedule.html +++ b/frappe/automation/doctype/auto_repeat/auto_repeat_schedule.html @@ -11,10 +11,9 @@ {% for(var i=0; i < schedule_details.length; i++) { %} {{ schedule_details[i].reference_document }} - {{ schedule_details[i].frequency }} - {{ schedule_details[i].next_scheduled_date }} + {{ __(schedule_details[i].frequency) }} + {{ frappe.datetime.str_to_user(schedule_details[i].next_scheduled_date) }} {% } %} - diff --git a/frappe/desk/doctype/auto_repeat/test_auto_repeat.js b/frappe/automation/doctype/auto_repeat/test_auto_repeat.js similarity index 100% rename from frappe/desk/doctype/auto_repeat/test_auto_repeat.js rename to frappe/automation/doctype/auto_repeat/test_auto_repeat.js diff --git a/frappe/desk/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py similarity index 77% rename from frappe/desk/doctype/auto_repeat/test_auto_repeat.py rename to frappe/automation/doctype/auto_repeat/test_auto_repeat.py index f0cf9643ce..cb98d4a8fd 100644 --- a/frappe/desk/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -7,20 +7,19 @@ import unittest import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_field -from frappe.desk.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, disable_auto_repeat +from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries from frappe.utils import today, add_days, getdate, add_months def add_custom_fields(): df = dict( fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender', - options='Auto Repeat') + options='Auto Repeat', hidden=1, print_hide=1, read_only=1) create_custom_field('ToDo', df) - class TestAutoRepeat(unittest.TestCase): def setUp(self): - if not frappe.db.sql("SELECT `name` FROM `tabCustom Field` WHERE `name`='auto_repeat'"): + if not frappe.db.sql("SELECT `fieldname` FROM `tabCustom Field` WHERE `fieldname`='auto_repeat' and `dt`=%s", "Todo"): add_custom_fields() def test_daily_auto_repeat(self): @@ -29,8 +28,8 @@ class TestAutoRepeat(unittest.TestCase): doc = make_auto_repeat(reference_document=todo.name) self.assertEqual(doc.next_schedule_date, today()) - for data in get_auto_repeat_entries(today()): - create_repeated_entries(data) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) frappe.db.commit() todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) @@ -51,8 +50,11 @@ class TestAutoRepeat(unittest.TestCase): dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert() self.monthly_auto_repeat('ToDo', todo.name, start_date, end_date) + #test without end_date + todo = frappe.get_doc(dict(doctype='ToDo', description='test recurring todo without end_date', assigned_by='Administrator')).insert() + self.monthly_auto_repeat('ToDo', todo.name, start_date) - def monthly_auto_repeat(self, doctype, docname, start_date, end_date): + def monthly_auto_repeat(self, doctype, docname, start_date, end_date = None): def get_months(start, end): diff = (12 * end.year + end.month) - (12 * start.year + start.month) return diff + 1 @@ -61,10 +63,10 @@ class TestAutoRepeat(unittest.TestCase): reference_doctype=doctype, frequency='Monthly', reference_document=docname, start_date=start_date, end_date=end_date) - disable_auto_repeat(doc) + doc.disable_auto_repeat() - for data in get_auto_repeat_entries(today()): - create_repeated_entries(data) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name}) self.assertEqual(len(docnames), 1) @@ -72,8 +74,8 @@ class TestAutoRepeat(unittest.TestCase): doc.db_set('disabled', 0) months = get_months(getdate(start_date), getdate(today())) - for data in get_auto_repeat_entries(today()): - create_repeated_entries(data) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name}) self.assertEqual(len(docnames), months) @@ -84,8 +86,8 @@ class TestAutoRepeat(unittest.TestCase): doc = make_auto_repeat(reference_document=todo.name, notify=1, recipients="test@domain.com", subject="New ToDo", message="A new ToDo has just been created for you") - for data in get_auto_repeat_entries(today()): - create_repeated_entries(data) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) frappe.db.commit() new_todo = frappe.db.get_value('ToDo', @@ -100,18 +102,14 @@ def make_auto_repeat(**args): doc = frappe.get_doc({ 'doctype': 'Auto Repeat', 'reference_doctype': args.reference_doctype or 'ToDo', - 'reference_document': args.reference_document or frappe.db.get_value('ToDo', {'docstatus': 1}, 'name'), + 'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'), 'frequency': args.frequency or 'Daily', 'start_date': args.start_date or add_days(today(), -1), - 'end_date': args.end_date or add_days(today(), 2), - 'submit_on_creation': args.submit_on_creation or 0, + 'end_date': args.end_date or "", 'notify_by_email': args.notify or 0, 'recipients': args.recipients or "", 'subject': args.subject or "", 'message': args.message or "" }).insert(ignore_permissions=True) - if not args.do_not_submit: - doc.submit() - return doc diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py index 84e5f4937f..8110f2ec19 100644 --- a/frappe/commands/__init__.py +++ b/frappe/commands/__init__.py @@ -62,7 +62,7 @@ def popen(command, *args, **kwargs): return_ = proc.wait() - if raise_err: + if return_ and raise_err: raise subprocess.CalledProcessError(return_, command) return return_ diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 8e89cf63da..bcc29c9468 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -39,6 +39,10 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N reinstall=False, db_type=None): """Install a new Frappe site""" + if not force and os.path.exists(site): + print('Site {0} already exists'.format(site)) + sys.exit(1) + if not db_name: db_name = '_' + hashlib.sha1(site.encode()).hexdigest()[:16] @@ -276,6 +280,12 @@ def reload_doctype(context, doctype): finally: frappe.destroy() +@click.command('add-to-hosts') +@pass_context +def add_to_hosts(context): + "Add site to hosts" + for site in context.sites: + frappe.commands.popen('echo 127.0.0.1\t{0} | sudo tee -a /etc/hosts'.format(site)) @click.command('use') @click.argument('site') @@ -347,12 +357,13 @@ def uninstall(context, app, dry_run=False, yes=False): @click.option('--root-login', default='root') @click.option('--root-password') @click.option('--archived-sites-path') +@click.option('--no-backup', is_flag=True, default=False) @click.option('--force', help='Force drop-site even if an error is encountered', is_flag=True, default=False) -def drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False): - _drop_site(site, root_login, root_password, archived_sites_path, force) +def drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False, no_backup=False): + _drop_site(site, root_login, root_password, archived_sites_path, force, no_backup) -def _drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False): +def _drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False, no_backup=False): "Remove site from database and filesystem" from frappe.database import drop_user_and_database from frappe.utils.backups import scheduled_backup @@ -361,7 +372,8 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path= frappe.connect() try: - scheduled_backup(ignore_files=False, force=True) + if not no_backup: + scheduled_backup(ignore_files=False, force=True) except Exception as err: if force: pass @@ -490,10 +502,7 @@ def browse(context, site): site = site.lower() if site in frappe.utils.get_sites(): - webbrowser.open('http://{site}:{port}'.format( - site=site, - port=frappe.get_conf(site).webserver_port - ), new=2) + webbrowser.open(frappe.utils.get_site_url(site), new=2) else: click.echo("\nSite named \033[1m{}\033[0m doesn't exist\n".format(site)) @@ -537,4 +546,5 @@ commands = [ browse, start_recording, stop_recording, + add_to_hosts ] diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index c9127d94a2..eb8cf0e088 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -459,26 +459,26 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), sys.exit(ret) @click.command('run-ui-tests') -@click.option('--app', help="App to run tests on, leave blank for all apps") -@click.option('--test', help="Path to the specific test you want to run") -@click.option('--test-list', help="Path to the txt file with the list of test cases") -@click.option('--profile', is_flag=True, default=False) +@click.argument('app') +@click.option('--headless', is_flag=True, help="Run UI Test in headless mode") @pass_context -def run_ui_tests(context, app=None, test=False, test_list=False, profile=False): +def run_ui_tests(context, app, headless=False): "Run UI tests" - import frappe.test_runner site = get_site(context) - frappe.init(site=site) - frappe.connect() + app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), '..')) + site_url = frappe.utils.get_site_url(site) + admin_password = frappe.get_conf(site).admin_password - ret = frappe.test_runner.run_ui_tests(app=app, test=test, test_list=test_list, verbose=context.verbose, - profile=profile) - if len(ret.failures) == 0 and len(ret.errors) == 0: - ret = 0 + # override baseUrl using env variable + site_env = 'CYPRESS_baseUrl={}'.format(site_url) + password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else '' - if os.environ.get('CI'): - sys.exit(ret) + # run for headless mode + run_or_open = 'run' if headless else 'open' + command = '{site_env} {password_env} yarn run cypress {run_or_open}' + formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open) + frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True) @click.command('run-setup-wizard-ui-test') @click.option('--app', help="App to run tests on, leave blank for all apps") diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index ce71051c1d..224fa65d13 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -92,11 +92,6 @@ def get_data(): "name": "Google Settings", "description": _("Google API Settings."), }, - { - "type": "doctype", - "name": "Google Maps Settings", - "description": _("Google Maps integration"), - }, { "type": "doctype", "name": "GCalendar Settings", diff --git a/frappe/config/settings.py b/frappe/config/settings.py index 9577879fc0..2422f2fae2 100644 --- a/frappe/config/settings.py +++ b/frappe/config/settings.py @@ -169,11 +169,27 @@ def get_data(): "name": "Workflow Action", "description": _("Actions for workflow (e.g. Approve, Cancel).") }, + ] + }, + { + "label": _("Automation"), + "icon": "fa fa-random", + "items": [ { "type": "doctype", "name": "Assignment Rule", "description": _("Set up rules for user assignments.") - } + }, + { + "type": "doctype", + "name": "Milestone", + "description": _("Tracks milestones on the lifecycle of a document if it undergoes multiple stages.") + }, + { + "type": "doctype", + "name": "Auto Repeat", + "description": _("Automatically generates recurring documents.") + }, ] }, ] diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 0e0d9aeabc..2018f5469e 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -161,3 +161,16 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters): 'link_name': link_name, 'link_doctype': link_doctype }) + + +def get_contact_with_phone_number(number): + if not number: return + + contacts = frappe.get_all('Contact', or_filters={ + 'phone': ['like', '%{}'.format(number)], + 'mobile_no': ['like', '%{}'.format(number)] + }, limit=1) + + contact = contacts[0].name if contacts else None + + return contact diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 36377a90f7..9f711c1fd9 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -529,7 +529,7 @@ def update_mins_to_first_communication(parent, communication): if frappe.db.get_all('User', filters={'email': communication.sender, 'user_type': 'System User', 'enabled': 1}, limit=1): first_responded_on = communication.creation - if parent.meta.has_field('first_responded_on'): + if parent.meta.has_field('first_responded_on') and communication.sent_or_received == "Sent": parent.db_set('first_responded_on', first_responded_on) parent.db_set('mins_to_first_response', round(time_diff_in_seconds(first_responded_on, parent.creation) / 60), 2) diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index b369aa1b77..9a19185cfc 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -22,9 +22,15 @@ frappe.ui.form.on('DocType', { } if (!frm.is_new() && !frm.doc.istable) { - frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => { - frappe.set_route('List', frm.doc.name, 'List'); - }); + if (frm.doc.issingle) { + frm.add_custom_button(__('Go to {0}', [frm.doc.name]), () => { + frappe.set_route('Form', frm.doc.name); + }); + } else { + frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => { + frappe.set_route('List', frm.doc.name, 'List'); + }); + } } if(!frappe.boot.developer_mode && !frm.doc.custom) { diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 24a7a4c287..a2e5c21202 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -37,6 +37,7 @@ "allow_rename", "allow_import", "allow_events_in_timeline", + "allow_auto_repeat", "view_settings", "title_field", "search_fields", @@ -81,6 +82,7 @@ "search_index": 1 }, { + "default": "0", "depends_on": "eval:!doc.istable", "description": "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended.", "fieldname": "is_submittable", @@ -88,6 +90,7 @@ "label": "Is Submittable" }, { + "default": "0", "description": "Child Tables are shown as a Grid in other DocTypes", "fieldname": "istable", "fieldtype": "Check", @@ -97,6 +100,7 @@ "oldfieldtype": "Check" }, { + "default": "0", "depends_on": "eval:!doc.istable", "description": "Single Types have only one record no tables associated. Values are stored in tabSingles", "fieldname": "issingle", @@ -135,6 +139,7 @@ "label": "Track Changes" }, { + "default": "0", "depends_on": "eval:!doc.istable", "description": "If enabled, the document is marked as seen, the first time a user opens it", "fieldname": "track_seen", @@ -150,11 +155,13 @@ "label": "Track Views" }, { + "default": "0", "fieldname": "custom", "fieldtype": "Check", "label": "Custom?" }, { + "default": "0", "fieldname": "beta", "fieldtype": "Check", "label": "Beta" @@ -236,6 +243,7 @@ "fieldtype": "Column Break" }, { + "default": "0", "fieldname": "hide_toolbar", "fieldtype": "Check", "label": "Hide Sidebar and Menu", @@ -243,6 +251,7 @@ "oldfieldtype": "Check" }, { + "default": "0", "fieldname": "allow_copy", "fieldtype": "Check", "label": "Hide Copy", @@ -250,6 +259,7 @@ "oldfieldtype": "Check" }, { + "default": "0", "fieldname": "allow_rename", "fieldtype": "Check", "label": "Allow Rename", @@ -257,15 +267,23 @@ "oldfieldtype": "Check" }, { + "default": "0", "fieldname": "allow_import", "fieldtype": "Check", "label": "Allow Import (via Data Import Tool)" }, { + "default": "0", "fieldname": "allow_events_in_timeline", "fieldtype": "Check", "label": "Allow events in timeline" }, + { + "default": "0", + "fieldname": "allow_auto_repeat", + "fieldtype": "Check", + "label": "Allow Auto Repeat" + }, { "collapsible": 1, "fieldname": "view_settings", @@ -329,6 +347,13 @@ "label": "Color" }, { + "default": "0", + "fieldname": "show_preview_popup", + "fieldtype": "Check", + "label": "Show Preview Popup" + }, + { + "default": "0", "fieldname": "show_name_in_global_search", "fieldtype": "Check", "label": "Make \"name\" searchable in Global Search" @@ -354,6 +379,7 @@ "options": "Domain" }, { + "default": "0", "fieldname": "read_only", "fieldtype": "Check", "label": "User Cannot Search", @@ -361,6 +387,7 @@ "oldfieldtype": "Check" }, { + "default": "0", "fieldname": "in_create", "fieldtype": "Check", "label": "User Cannot Create", @@ -411,17 +438,11 @@ "fieldtype": "Select", "label": "Database Engine", "options": "InnoDB\nMyISAM" - }, - { - "default": "0", - "fieldname": "show_preview_popup", - "fieldtype": "Check", - "label": "Show Preview Popup" } ], "icon": "fa fa-bolt", "idx": 6, - "modified": "2019-05-16 14:58:33.405381", + "modified": "2019-07-04 23:23:17.174960", "modified_by": "Administrator", "module": "Core", "name": "DocType", @@ -454,4 +475,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 07da5a8fe3..5ec8478d60 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -13,6 +13,7 @@ from frappe.utils import now, cint from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields from frappe.model.document import Document from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.desk.notifications import delete_notification_count_for from frappe.modules import make_boilerplate, get_doc_path from frappe.database.schema import validate_column_name, validate_column_length @@ -47,7 +48,8 @@ class DocType(Document): - Validate series - Check fieldnames (duplication etc) - Clear permission table for child tables - - Add `amended_from` and `amended_by` if Amendable""" + - Add `amended_from` and `amended_by` if Amendable + - Add custom field `auto_repeat` if Repeatable""" self.check_developer_mode() @@ -76,6 +78,7 @@ class DocType(Document): validate_permissions(self) self.make_amendable() + self.make_repeatable() self.validate_website() if not self.is_new(): @@ -384,7 +387,7 @@ class DocType(Document): os.path.join(new_path, fname.replace(frappe.scrub(old), frappe.scrub(new)))]) self.rename_inside_controller(new, old, new_path) - frappe.msgprint('Renamed files and replaced code in controllers, please check!') + frappe.msgprint(_('Renamed files and replaced code in controllers, please check!')) def rename_inside_controller(self, new, old, new_path): for fname in ('{}.js', '{}.py', '{}_list.js', '{}_calendar.js', 'test_{}.py', 'test_{}.js'): @@ -526,6 +529,14 @@ class DocType(Document): "no_copy": 1 }) + def make_repeatable(self): + """If allow_auto_repeat is set, add auto_repeat custom field.""" + if self.allow_auto_repeat: + if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.name}): + insert_after = self.fields[len(self.fields) - 1].fieldname + df = dict(fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', options='Auto Repeat', insert_after=insert_after, read_only=1, no_copy=1, print_hide=1) + create_custom_field(self.name, df) + def get_max_idx(self): """Returns the highest `idx`""" max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""", diff --git a/frappe/core/doctype/domain/domain.json b/frappe/core/doctype/domain/domain.json index 3ba8acdc0e..c235596884 100644 --- a/frappe/core/doctype/domain/domain.json +++ b/frappe/core/doctype/domain/domain.json @@ -1,95 +1,54 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:domain", - "beta": 0, - "creation": "2017-05-03 15:07:39.752820", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "autoname": "field:domain", + "creation": "2017-05-03 15:07:39.752820", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "domain" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "domain", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Domain", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "domain", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Domain", + "reqd": 1, + "unique": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-09-15 12:26:21.827149", - "modified_by": "Administrator", - "module": "Core", - "name": "Domain", - "name_case": "", - "owner": "makarand@erpnext.com", + ], + "modified": "2019-06-30 13:24:13.732202", + "modified_by": "Administrator", + "module": "Core", + "name": "Domain", + "owner": "makarand@erpnext.com", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Administrator", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "domain", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "domain", - "track_changes": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "search_fields": "domain", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "domain" } \ No newline at end of file diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index fff4a5e344..47df90d18c 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -2,6 +2,7 @@ # MIT License. See license.txt from __future__ import unicode_literals +from frappe import _ """ record of files @@ -446,7 +447,7 @@ class File(NestedSet): def validate_url(self, df=None): if self.file_url: if not self.file_url.startswith(("http://", "https://", "/files/", "/private/files/")): - frappe.throw("URL must start with 'http://' or 'https://'") + frappe.throw(_("URL must start with 'http://' or 'https://'")) return self.file_url = unquote(self.file_url) @@ -682,7 +683,7 @@ def get_web_image(file_url): frappe.msgprint(_("Unable to read file format for {0}").format(file_url)) raise - image = Image.open(StringIO(r.content)) + image = Image.open(StringIO(frappe.safe_decode(r.content))) try: filename, extn = file_url.rsplit("/", 1)[1].rsplit(".", 1) diff --git a/frappe/core/doctype/language/language.json b/frappe/core/doctype/language/language.json index 1907253f81..099b383980 100644 --- a/frappe/core/doctype/language/language.json +++ b/frappe/core/doctype/language/language.json @@ -1,173 +1,72 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "field:language_code", - "beta": 0, - "creation": "2014-08-22 16:12:17.249590", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "language_code", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Language Code", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "language_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Language Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "flag", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Flag", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "based_on", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Based On", - "length": 0, - "no_copy": 0, - "options": "Language", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-globe", - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-12-29 14:40:33.210645", - "modified_by": "Administrator", - "module": "Core", - "name": "Language", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "language_name", - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "language_name", - "track_changes": 1, - "track_seen": 0 - } \ No newline at end of file + "allow_rename": 1, + "autoname": "field:language_code", + "creation": "2014-08-22 16:12:17.249590", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "language_code", + "language_name", + "flag", + "based_on" + ], + "fields": [ + { + "fieldname": "language_code", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Language Code", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "language_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Language Name", + "reqd": 1 + }, + { + "fieldname": "flag", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Flag" + }, + { + "fieldname": "based_on", + "fieldtype": "Link", + "label": "Based On", + "options": "Language" + } + ], + "icon": "fa fa-globe", + "modified": "2019-07-19 16:32:12.652550", + "modified_by": "Administrator", + "module": "Core", + "name": "Language", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "read": 1, + "role": "System Manager", + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Guest", + "share": 1 + } + ], + "search_fields": "language_name", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "language_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 1b6957d057..df62f94bcb 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -12,6 +12,7 @@ from frappe.modules.export_file import export_to_files from frappe.modules import make_boilerplate from frappe.core.doctype.page.page import delete_custom_role from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles +from frappe.desk.reportview import append_totals_row from six import iteritems @@ -76,11 +77,6 @@ class Report(Document): if not self.json: self.json = '{}' - if self.json: - data = json.loads(self.json) - data["add_total_row"] = self.add_total_row - self.json = json.dumps(data) - def export_doc(self): if frappe.flags.in_import: return @@ -178,6 +174,9 @@ class Report(Document): out = out + [list(d) for d in result] + if params.get('add_totals_row'): + out = append_totals_row(out) + if as_dict: data = [] for row in out: diff --git a/frappe/integrations/doctype/google_maps_settings/__init__.py b/frappe/core/doctype/session_default/__init__.py similarity index 100% rename from frappe/integrations/doctype/google_maps_settings/__init__.py rename to frappe/core/doctype/session_default/__init__.py diff --git a/frappe/core/doctype/session_default/session_default.json b/frappe/core/doctype/session_default/session_default.json new file mode 100644 index 0000000000..d382e12456 --- /dev/null +++ b/frappe/core/doctype/session_default/session_default.json @@ -0,0 +1,29 @@ +{ + "creation": "2019-07-17 16:21:33.546379", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "ref_doctype" + ], + "fields": [ + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType" + } + ], + "istable": 1, + "modified": "2019-07-21 13:22:25.752553", + "modified_by": "Administrator", + "module": "Core", + "name": "Session Default", + "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/core/doctype/session_default/session_default.py b/frappe/core/doctype/session_default/session_default.py new file mode 100644 index 0000000000..8a8db46ff1 --- /dev/null +++ b/frappe/core/doctype/session_default/session_default.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class SessionDefault(Document): + pass diff --git a/test_sites/apps.txt b/frappe/core/doctype/session_default_settings/__init__.py similarity index 100% rename from test_sites/apps.txt rename to frappe/core/doctype/session_default_settings/__init__.py diff --git a/frappe/core/doctype/session_default_settings/session_default_settings.js b/frappe/core/doctype/session_default_settings/session_default_settings.js new file mode 100644 index 0000000000..f7cce14809 --- /dev/null +++ b/frappe/core/doctype/session_default_settings/session_default_settings.js @@ -0,0 +1,15 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +frappe.ui.form.on('Session Default Settings', { + refresh: function(frm) { + frm.set_query('ref_doctype', 'session_defaults', function() { + return { + filters: { + issingle: 0, + istable: 0 + } + }; + }); + } +}); diff --git a/frappe/core/doctype/session_default_settings/session_default_settings.json b/frappe/core/doctype/session_default_settings/session_default_settings.json new file mode 100644 index 0000000000..3eeb3f4167 --- /dev/null +++ b/frappe/core/doctype/session_default_settings/session_default_settings.json @@ -0,0 +1,39 @@ +{ + "creation": "2019-07-17 16:22:31.300991", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "session_defaults" + ], + "fields": [ + { + "fieldname": "session_defaults", + "fieldtype": "Table", + "label": "Session Defaults", + "options": "Session Default" + } + ], + "issingle": 1, + "modified": "2019-07-19 16:04:33.971089", + "modified_by": "Administrator", + "module": "Core", + "name": "Session Default Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "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/core/doctype/session_default_settings/session_default_settings.py b/frappe/core/doctype/session_default_settings/session_default_settings.py new file mode 100644 index 0000000000..453ece2890 --- /dev/null +++ b/frappe/core/doctype/session_default_settings/session_default_settings.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +import json +from frappe.model.document import Document + +class SessionDefaultSettings(Document): + pass + +@frappe.whitelist() +def get_session_default_values(): + settings = frappe.get_single('Session Default Settings') + fields = [] + for default_values in settings.session_defaults: + reference_doctype = frappe.scrub(default_values.ref_doctype) + fields.append({ + 'fieldname': reference_doctype, + 'fieldtype': 'Link', + 'options': default_values.ref_doctype, + 'label': _('Default {0}').format(_(default_values.ref_doctype)), + 'default': frappe.defaults.get_user_default(reference_doctype) + }) + return json.dumps(fields) + +@frappe.whitelist() +def set_session_default_values(default_values): + if not frappe.flags.in_test: + default_values = json.loads(default_values) + for entry in default_values: + try: + frappe.defaults.set_user_default(entry, default_values.get(entry)) + except Exception: + return + return "success" + +#called on hook 'on_logout' to clear defaults for the session +def clear_session_defaults(): + settings = frappe.get_single('Session Default Settings').session_defaults + for entry in settings: + frappe.defaults.clear_user_default(frappe.scrub(entry.ref_doctype)) diff --git a/frappe/core/doctype/session_default_settings/test_session_default_settings.py b/frappe/core/doctype/session_default_settings/test_session_default_settings.py new file mode 100644 index 0000000000..12aa14d343 --- /dev/null +++ b/frappe/core/doctype/session_default_settings/test_session_default_settings.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from frappe.core.doctype.session_default_settings.session_default_settings import set_session_default_values, clear_session_defaults + +class TestSessionDefaultSettings(unittest.TestCase): + def test_set_session_default_settings(self): + frappe.set_user("Administrator") + settings = frappe.get_single("Session Default Settings") + settings.session_defaults = [] + settings.append("session_defaults", { + "ref_doctype": "Role" + }) + settings.save() + + set_session_default_values({"role": "Website Manager"}) + + todo = frappe.get_doc(dict(doctype="ToDo", description="test session defaults set", assigned_by="Administrator")).insert() + self.assertEqual(todo.role, "Website Manager") + + def test_clear_session_defaults(self): + clear_session_defaults() + todo = frappe.get_doc(dict(doctype="ToDo", description="test session defaults cleared", assigned_by="Administrator")).insert() + self.assertNotEqual(todo.role, "Website Manager") diff --git a/frappe/core/doctype/success_action/success_action.js b/frappe/core/doctype/success_action/success_action.js index b8d56d3c8a..d73d3db326 100644 --- a/frappe/core/doctype/success_action/success_action.js +++ b/frappe/core/doctype/success_action/success_action.js @@ -15,7 +15,7 @@ frappe.ui.form.on('Success Action', { validate: (frm) => { const checked_actions = frm.action_multicheck.get_checked_options(); if (checked_actions.length < 2) { - frappe.msgprint('Select atleast 2 actions'); + frappe.msgprint(__('Select atleast 2 actions')); } else { return true; } diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 318b31d4a2..ca94afd17d 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1,2528 +1,651 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 1, - "beta": 0, "creation": "2014-03-11 14:55:00", - "custom": 0, "description": "Represents a User in the system.", - "docstatus": 0, "doctype": "DocType", - "document_type": "", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "sb0_5", + "enabled", + "section_break_3", + "email", + "first_name", + "middle_name", + "last_name", + "full_name", + "send_welcome_email", + "unsubscribed", + "column_break0", + "username", + "language", + "time_zone", + "user_image", + "sb1", + "role_profile_name", + "roles_html", + "roles", + "short_bio", + "gender", + "phone", + "mobile_no", + "birth_date", + "location", + "banner_image", + "column_break_22", + "interest", + "bio", + "mute_sounds", + "change_password", + "new_password", + "send_password_update_notification", + "logout_all_sessions", + "reset_password_key", + "last_password_reset_date", + "redirect_url", + "document_follow_notifications_section", + "document_follow_notify", + "document_follow_frequency", + "email_settings", + "thread_notify", + "send_me_a_copy", + "allowed_in_mentions", + "email_signature", + "email_inbox", + "user_emails", + "background", + "background_image", + "background_style", + "sb_allow_modules", + "modules_html", + "block_modules", + "home_settings", + "sb2", + "defaults", + "sb3", + "simultaneous_sessions", + "user_type", + "login_after", + "login_before", + "restrict_ip", + "bypass_restrict_ip_check_if_2fa_enabled", + "column_break1", + "last_login", + "last_ip", + "last_active", + "last_known_versions", + "third_party_authentication", + "social_logins", + "api_access", + "api_key", + "generate_keys", + "column_break_65", + "api_secret" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "sb0_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "enabled", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Enabled", - "length": 0, - "no_copy": 0, "oldfieldname": "enabled", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "enabled", "fieldname": "section_break_3", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "email", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Email", - "length": 0, "no_copy": 1, "oldfieldname": "email", "oldfieldtype": "Data", "options": "Email", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "first_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "First Name", - "length": 0, - "no_copy": 0, "oldfieldname": "first_name", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "middle_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Middle Name (Optional)", - "length": 0, - "no_copy": 0, "oldfieldname": "middle_name", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Data" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, "bold": 1, - "collapsible": 0, - "columns": 0, "fieldname": "last_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Last Name", - "length": 0, - "no_copy": 0, "oldfieldname": "last_name", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Data" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "full_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, "in_standard_filter": 1, "label": "Full Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, "bold": 1, - "collapsible": 0, - "columns": 0, "default": "1", "depends_on": "eval:doc.__islocal", "fieldname": "send_welcome_email", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Send Welcome Email", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Send Welcome Email" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "unsubscribed", "fieldtype": "Check", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Unsubscribed", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "no_copy": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break0", "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, "oldfieldtype": "Column Break", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50%", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50%" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "username", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, "in_standard_filter": 1, "label": "Username", - "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": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", "fieldname": "language", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Language", - "length": 0, - "no_copy": 0, - "options": "Language", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Language" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", "fieldname": "time_zone", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Timezone", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Timezone" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Get your globally recognized avatar from Gravatar.com", "fieldname": "user_image", "fieldtype": "Attach Image", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "User Image", - "length": 0, "no_copy": 1, - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "print_hide": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "enabled", - "description": "", "fieldname": "sb1", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Roles", - "length": 0, - "no_copy": 0, "permlevel": 1, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "role_profile_name", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Role Profile", - "length": 0, - "no_copy": 0, - "options": "Role Profile", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Role Profile" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "roles_html", "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Roles HTML", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "roles", "fieldtype": "Table", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Roles Assigned", - "length": 0, - "no_copy": 0, "options": "Has Role", "permlevel": 1, - "precision": "", "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, "depends_on": "enabled", "fieldname": "short_bio", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "More Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "More Information" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "gender", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Gender", - "length": 0, - "no_copy": 0, "oldfieldname": "gender", "oldfieldtype": "Select", - "options": "Gender", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Gender" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "phone", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Phone", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Phone" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "mobile_no", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Mobile No", - "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": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "birth_date", "fieldtype": "Date", - "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": "Birth Date", - "length": 0, "no_copy": 1, "oldfieldname": "birth_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Date" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "location", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Location", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "no_copy": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "banner_image", "fieldtype": "Attach Image", - "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": "Banner Image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Banner Image" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_22", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "interest", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Interest", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Interests" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "bio", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Bio", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "no_copy": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "mute_sounds", "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": "Mute Sounds", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Mute Sounds" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, "depends_on": "eval:doc.enabled && (!doc.__islocal || !cint(doc.send_welcome_email))", "fieldname": "change_password", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Change Password", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Change Password" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "new_password", "fieldtype": "Password", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Set New Password", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "no_copy": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "eval:!doc.__islocal", "fieldname": "send_password_update_notification", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Send Password Update Notification", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Send Password Update Notification" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", "fieldname": "logout_all_sessions", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Logout from all devices while changing Password", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Logout from all devices while changing Password" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "reset_password_key", "fieldtype": "Data", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Reset Password Key", - "length": 0, "no_copy": 1, - "permlevel": 0, "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "last_password_reset_date", "fieldtype": "Date", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Last Password Reset Date", - "length": 0, "no_copy": 1, - "permlevel": 0, - "precision": "", "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "redirect_url", "fieldtype": "Small Text", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Redirect URL", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Redirect URL" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, - "depends_on": "", "fieldname": "document_follow_notifications_section", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Document Follow", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Document Follow" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "document_follow_notify", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Send Notifications for documents followed by me", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Send Notifications for documents followed by me" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Daily", "depends_on": "eval:(doc.document_follow_notify== 1)", "fieldname": "document_follow_frequency", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Frequency", - "length": 0, - "no_copy": 0, - "options": "Hourly\nDaily\nWeekly", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Hourly\nDaily\nWeekly" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, "depends_on": "enabled", "fieldname": "email_settings", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Email Settings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Email Settings" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "thread_notify", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Send Notifications for Email threads", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Send Notifications for Email threads" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "send_me_a_copy", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Send Me A Copy of Outgoing Emails", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Send Me A Copy of Outgoing Emails" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "allowed_in_mentions", "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": "Allowed In Mentions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Allowed In Mentions" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "email_signature", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Email Signature", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "no_copy": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, "fieldname": "email_inbox", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Email Inbox", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Email Inbox" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", "fieldname": "user_emails", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "User Emails", - "length": 0, - "no_copy": 0, "options": "User Email", - "permlevel": 1, - "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 + "permlevel": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, "depends_on": "enabled", "fieldname": "background", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Desktop Background", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Desktop Background" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "background_image", "fieldtype": "Attach", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Background Image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Background Image" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "background_style", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Background Style", - "length": 0, - "no_copy": 0, - "options": "Fill Screen\nTile", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Fill Screen\nTile" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, - "default": "", - "description": "", "fieldname": "sb_allow_modules", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Allow Modules", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "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 + "permlevel": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "modules_html", "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Modules HTML", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "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 + "permlevel": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "block_modules", "fieldtype": "Table", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Block Modules", - "length": 0, - "no_copy": 0, "options": "Block Module", - "permlevel": 1, - "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 + "permlevel": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", "fieldname": "home_settings", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Home Settings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Home Settings" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "These values will be automatically updated in transactions and also will be useful to restrict permissions for this user on transactions containing these values.", "fieldname": "sb2", "fieldtype": "Section Break", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Defaults", - "length": 0, - "no_copy": 0, "oldfieldtype": "Column Break", "permlevel": 1, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50%", "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50%" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Enter default value fields (keys) and values. If you add multiple values for a field, the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields, go to \"Customize Form\".", "fieldname": "defaults", "fieldtype": "Table", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "User Defaults", - "length": 0, "no_copy": 1, - "options": "DefaultValue", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "DefaultValue" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, "depends_on": "enabled", "fieldname": "sb3", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Security Settings", - "length": 0, - "no_copy": 0, "oldfieldtype": "Section Break", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "simultaneous_sessions", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Simultaneous Sessions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Simultaneous Sessions" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "System User", "description": "If the user has any role checked, then the user becomes a \"System User\". \"System User\" has access to the desktop", "fieldname": "user_type", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "User Type", - "length": 0, - "no_copy": 0, "oldfieldname": "user_type", "oldfieldtype": "Select", "options": "System User\nWebsite User", "permlevel": 1, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Allow user to login only after this hour (0-24)", "fieldname": "login_after", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Login After", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "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 + "permlevel": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Allow user to login only before this hour (0-24)", "fieldname": "login_before", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Login Before", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "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 + "permlevel": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Restrict user from this IP address only. Multiple IP addresses can be added by separating with commas. Also accepts partial IP addresses like (111.111.111)", "fieldname": "restrict_ip", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Restrict IP", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "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 + "permlevel": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "eval:doc.restrict_ip && doc.restrict_ip.length", "description": "If enabled, user can login from any IP Address using Two Factor Auth, this can also be set for all users in System Settings", "fieldname": "bypass_restrict_ip_check_if_2fa_enabled", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Bypass restricted IP Address check If Two Factor Auth Enabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Bypass restricted IP Address check If Two Factor Auth Enabled" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break1", "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, "oldfieldtype": "Column Break", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50%", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50%" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "last_login", "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Last Login", - "length": 0, "no_copy": 1, "oldfieldname": "last_login", "oldfieldtype": "Read Only", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "last_ip", "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Last IP", - "length": 0, "no_copy": 1, "oldfieldname": "last_ip", "oldfieldtype": "Read Only", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "last_active", "fieldtype": "Datetime", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Last Active", - "length": 0, "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Stores the JSON of last known versions of various installed apps. It is used to show release notes.", "fieldname": "last_known_versions", "fieldtype": "Text", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Last Known Versions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, "depends_on": "enabled", "fieldname": "third_party_authentication", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Third Party Authentication", - "length": 0, - "no_copy": 0, - "permlevel": 1, - "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 + "permlevel": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "social_logins", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Social Logins", - "length": 0, - "no_copy": 0, - "options": "User Social Login", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "User Social Login" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, "fieldname": "api_access", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Api Access", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Api Access" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "API Key cannot be regenerated", "fieldname": "api_key", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "API Key", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "generate_keys", "fieldtype": "Button", - "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": "Generate Keys", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Generate Keys" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_65", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "api_secret", "fieldtype": "Password", - "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": "API Secret", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-user", "idx": 413, "image_field": "user_image", - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, "max_attachments": 5, - "modified": "2019-04-15 20:25:02.022893", - "modified_by": "Administrator", + "modified": "2019-07-12 11:35:17.469656", + "modified_by": "prasadsherlock@gmail.com", "module": "Core", "name": "User", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, "import": 1, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, "permlevel": 1, - "print": 0, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, "write": 1 } ], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, "search_fields": "full_name", "show_name_in_global_search": 1, + "sort_field": "modified", "sort_order": "DESC", "title_field": "full_name", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/core/doctype/user_permission/user_permission_list.js b/frappe/core/doctype/user_permission/user_permission_list.js index a0b553c43a..3e822f0007 100644 --- a/frappe/core/doctype/user_permission/user_permission_list.js +++ b/frappe/core/doctype/user_permission/user_permission_list.js @@ -166,7 +166,7 @@ frappe.listview_settings['User Permission'] = { return data; } if(data.apply_to_all_doctypes == 0 && !("applicable_doctypes" in data)) { - frappe.throw("Please select applicable Doctypes"); + frappe.throw(__("Please select applicable Doctypes")); } return data; }, diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 5f288fe99c..d4b6bc6352 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -4,6 +4,7 @@ "doctype": "DocType", "document_type": "Document", "editable_grid": 1, + "engine": "InnoDB", "field_order": [ "doc_type", "properties", @@ -16,6 +17,7 @@ "quick_entry", "track_changes", "track_views", + "allow_auto_repeat", "image_view", "column_break_5", "title_field", @@ -59,17 +61,20 @@ "label": "Max Attachments" }, { + "default": "0", "fieldname": "allow_copy", "fieldtype": "Check", "label": "Hide Copy" }, { + "default": "0", "fieldname": "istable", "fieldtype": "Check", "label": "Is Table", "read_only": 1 }, { + "default": "0", "depends_on": "istable", "fieldname": "editable_grid", "fieldtype": "Check", @@ -82,11 +87,13 @@ "label": "Quick Entry" }, { + "default": "0", "fieldname": "track_changes", "fieldtype": "Check", "label": "Track Changes" }, { + "default": "0", "depends_on": "eval: doc.image_field", "fieldname": "image_view", "fieldtype": "Check", @@ -150,16 +157,23 @@ "options": "Customize Form Field" }, { + "default": "0", "fieldname": "track_views", "fieldtype": "Check", "label": "Track Views" + }, + { + "default": "0", + "fieldname": "allow_auto_repeat", + "fieldtype": "Check", + "label": "Allow Auto Repeat" } ], "hide_toolbar": 1, "icon": "fa fa-glass", "idx": 1, "issingle": 1, - "modified": "2019-05-13 18:54:40.610862", + "modified": "2019-07-01 22:50:50.372465", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", @@ -177,6 +191,7 @@ ], "quick_entry": 1, "search_fields": "doc_type", + "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 } \ No newline at end of file diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 78ee82efcb..425191c4eb 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -13,6 +13,7 @@ from frappe.utils import cint from frappe.model.document import Document from frappe.model import no_value_fields, core_doctypes_list from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype +from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.model.docfield import supports_translation doctype_properties = { @@ -29,6 +30,7 @@ doctype_properties = { 'max_attachments': 'Int', 'track_changes': 'Check', 'track_views': 'Check', + 'allow_auto_repeat': 'Check' } docfield_properties = { @@ -65,6 +67,7 @@ docfield_properties = { 'columns': 'Int', 'remember_last_selected_value': 'Check', 'allow_bulk_edit': 'Check', + 'auto_repeat': 'Link' } allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'), @@ -108,6 +111,13 @@ class CustomizeForm(Document): translation = self.get_name_translation() self.label = translation.target_name if translation else '' + #If allow_auto_repeat is set, add auto_repeat custom field. + if self.allow_auto_repeat: + if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.doc_type}): + insert_after = self.fields[len(self.fields) - 1].fieldname + df = dict(fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', options='Auto Repeat', insert_after=insert_after, read_only=1, no_copy=1, print_hide=1) + create_custom_field(self.doc_type, df) + # NOTE doc is sent to clientside by run_method def get_name_translation(self): diff --git a/frappe/database/database.py b/frappe/database/database.py index 3aab284de0..6702ce2a5e 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -553,6 +553,10 @@ class Database(object): val = val[0][0] if val else None df = frappe.get_meta(doctype).get_field(fieldname) + + if not df: + frappe.throw(_('Invalid field name: {0}').format(frappe.bold(fieldname)), self.InvalidColumnName) + if df.fieldtype in frappe.model.numeric_fieldtypes: val = cint(val) @@ -924,7 +928,7 @@ class Database(object): conditions=conditions ), values) else: - frappe.throw('No conditions provided') + frappe.throw(_('No conditions provided')) def log_touched_tables(self, query, values=None): if values: diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py index 0954657b28..0447f97273 100644 --- a/frappe/database/db_manager.py +++ b/frappe/database/db_manager.py @@ -48,7 +48,10 @@ class DbManager: if not host: host = self.get_current_host() - self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host)) + if frappe.conf.get('rds_db', 0) == 1: + self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON `%s`.* TO '%s'@'%s';" % (target, user, host)) + else: + self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host)) def flush_privileges(self): self.db.sql("FLUSH PRIVILEGES") diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 2e3938859c..610f15e011 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -111,13 +111,10 @@ class PostgresDatabase(Database): def format_date(self, date): if not date: - return '0001-01-01::DATE' + return '0001-01-01' - if isinstance(date, frappe.string_types): - if ':' not in date: - date = date + '::DATE' - else: - date = date.strftime('%Y-%m-%d') + '::DATE' + if not isinstance(date, frappe.string_types): + date = date.strftime('%Y-%m-%d') return date diff --git a/frappe/desk/doctype/auto_repeat/auto_repeat.js b/frappe/desk/doctype/auto_repeat/auto_repeat.js deleted file mode 100644 index ca96cb2b45..0000000000 --- a/frappe/desk/doctype/auto_repeat/auto_repeat.js +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies and contributors -// For license information, please see license.txt -frappe.provide("frappe.auto_repeat"); - -frappe.ui.form.on('Auto Repeat', { - setup: function(frm) { - frm.fields_dict['reference_doctype'].get_query = function() { - return { - query: "frappe.desk.doctype.auto_repeat.auto_repeat.auto_repeat_doctype_query" - }; - }; - - frm.fields_dict['reference_document'].get_query = function() { - return { - filters: { - "docstatus": 1, - "auto_repeat": '' - } - }; - }; - - frm.fields_dict['print_format'].get_query = function() { - return { - filters: { - "doc_type": frm.doc.reference_doctype - } - }; - }; - }, - - refresh: function(frm) { - - if(frm.doc.docstatus == 1) { - - let label = __('View {0}', [__(frm.doc.reference_doctype)]); - frm.add_custom_button(__(label), - function() { - frappe.route_options = { - "auto_repeat": frm.doc.name, - }; - frappe.set_route("List", frm.doc.reference_doctype); - } - ); - - if(frm.doc.status != 'Stopped') { - frm.add_custom_button(__("Stop"), - function() { - frm.events.stop_resume_auto_repeat(frm, "Stopped"); - } - ); - } - - if(frm.doc.status == 'Stopped') { - frm.add_custom_button(__("Restart"), - function() { - frm.events.stop_resume_auto_repeat(frm, "Resumed"); - } - ); - } - } - - frm.toggle_display('auto_repeat_schedule', !in_list(['Stopped', 'Cancelled'], frm.doc.status)); - if(frm.doc.start_date && !in_list(['Stopped', 'Cancelled'], frm.doc.status)){ - frappe.auto_repeat.render_schedule(frm); - } - - }, - - stop_resume_auto_repeat: function(frm, status) { - frappe.call({ - method: "frappe.desk.doctype.auto_repeat.auto_repeat.stop_resume_auto_repeat", - args: { - auto_repeat: frm.doc.name, - status: status - }, - callback: function(r) { - if(r.message) { - frm.set_value("status", r.message); - frm.reload_doc(); - } - } - }); - }, - - template: function(frm) { - if (frm.doc.template) { - frappe.model.with_doc("Email Template", frm.doc.template, () => { - let email_template = frappe.get_doc("Email Template", frm.doc.template); - frm.set_value("subject", email_template.subject); - frm.set_value("message", email_template.response); - frm.refresh_field("subject"); - frm.refresh_field("message"); - }); - } - }, - - get_contacts: function(frm) { - frappe.call({ - method: "frappe.desk.doctype.auto_repeat.auto_repeat.get_contacts", - args: { - reference_doctype: frm.doc.reference_doctype, - reference_name: frm.doc.reference_document - }, - callback: function(r) { - if(r.message) { - frm.set_value("recipients", r.message.join()); - frm.refresh_field("recipients"); - } - } - }); - }, - - preview_message: function(frm) { - if (frm.doc.message) { - frappe.call({ - method: "frappe.desk.doctype.auto_repeat.auto_repeat.generate_message_preview", - args: { - reference_dt: frm.doc.reference_doctype, - reference_doc: frm.doc.reference_document, - subject: frm.doc.subject, - message: frm.doc.message - }, - callback: function(r) { - if(r.message) { - frappe.msgprint(r.message.message, r.message.subject) - } - } - }); - } else { - frappe.msgprint(__("Please setup a message first"), __("Message not setup")) - } - } -}); - -frappe.auto_repeat.render_schedule = function(frm) { - frappe.call({ - method: "get_auto_repeat_schedule", - doc: frm.doc - }).done((r) => { - var wrapper = $(frm.fields_dict["auto_repeat_schedule"].wrapper); - wrapper.html(frappe.render_template ("auto_repeat_schedule", {"schedule_details" : r.message || []} )); - frm.refresh_fields(); - }); -}; \ No newline at end of file diff --git a/frappe/desk/doctype/auto_repeat/auto_repeat.json b/frappe/desk/doctype/auto_repeat/auto_repeat.json deleted file mode 100644 index 701f692672..0000000000 --- a/frappe/desk/doctype/auto_repeat/auto_repeat.json +++ /dev/null @@ -1,1081 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "naming_series:", - "beta": 0, - "creation": "2018-03-09 11:22:31.192349", - "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": "section_break_1", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "naming_series", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Series", - "length": 0, - "no_copy": 0, - "options": "SUB-", - "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": "reference_doctype", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Reference Doctype", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_document", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reference Document", - "length": 0, - "no_copy": 1, - "options": "reference_doctype", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "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": "reference_party_doctype", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reference Party Doctype", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_party", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reference Party", - "length": 0, - "no_copy": 0, - "options": "reference_party_doctype", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "start_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Start Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "end_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "End Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "submit_on_creation", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Submit on Creation", - "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": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Disabled", - "length": 0, - "no_copy": 1, - "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": "section_break_10", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "frequency", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Frequency", - "length": 0, - "no_copy": 0, - "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_12", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: in_list([\"Monthly\", \"Quarterly\", \"Yearly\"], doc.frequency)", - "fieldname": "repeat_on_day", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Repeat on Day", - "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": "next_schedule_date", - "fieldtype": "Date", - "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": "Next Schedule Date", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "section_break_13", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Auto Repeat Schedule", - "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, - "depends_on": "eval: !cur_frm.doc.__islocal", - "fieldname": "auto_repeat_schedule", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Auto Repeat Schedule", - "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": 1, - "columns": 0, - "fieldname": "notification", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Notification", - "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": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "notify_by_email", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Notify by Email", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "notify_by_email", - "fieldname": "recipients", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Recipients", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: doc.notify_by_email", - "fieldname": "get_contacts", - "fieldtype": "Button", - "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": "Get Contacts", - "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": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: doc.notify_by_email", - "fieldname": "template", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Template", - "length": 0, - "no_copy": 0, - "options": "Email Template", - "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": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: doc.notify_by_email", - "description": "To add dynamic subject, use jinja tags like\n\n
New {{ doc.doctype }} #{{ doc.name }}
", - "fieldname": "subject", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Subject", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Please find attached {{ doc.doctype }} #{{ doc.name }}", - "depends_on": "eval: doc.notify_by_email", - "fieldname": "message", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Message", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: doc.notify_by_email", - "fieldname": "preview_message", - "fieldtype": "Button", - "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": "Preview Message", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "notify_by_email", - "fieldname": "print_format", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Print Format", - "length": 0, - "no_copy": 0, - "options": "Print Format", - "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": 1, - "columns": 0, - "depends_on": "eval: !doc.__islocal", - "fieldname": "section_break_16", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "\nDraft\nStopped\nSubmitted\nCancelled\nCompleted", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From ", - "length": 0, - "no_copy": 1, - "options": "Auto Repeat", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-10-12 18:59:22.463211", - "modified_by": "Administrator", - "module": "Desk", - "name": "Auto Repeat", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 0, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 0, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "reference_document", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "reference_document", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/frappe/desk/doctype/auto_repeat/auto_repeat.py b/frappe/desk/doctype/auto_repeat/auto_repeat.py deleted file mode 100644 index 0caaf69c3a..0000000000 --- a/frappe/desk/doctype/auto_repeat/auto_repeat.py +++ /dev/null @@ -1,418 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -import calendar -from frappe import _ -from frappe.desk.form import assign_to -from frappe.utils.jinja import validate_template -from dateutil.relativedelta import relativedelta -from frappe.utils.user import get_system_managers -from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day -from frappe.model.document import Document -from frappe.core.doctype.communication.email import make -from frappe.utils.background_jobs import get_jobs - -month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} - - -class AutoRepeat(Document): - def validate(self): - self.update_status() - self.validate_reference_doctype() - self.validate_dates() - self.validate_next_schedule_date() - self.validate_email_id() - self.link_party() - - validate_template(self.subject or "") - validate_template(self.message or "") - - def before_submit(self): - start_date_copy = self.start_date - today_copy = add_days(today(), -1) - - if start_date_copy <= today_copy: - start_date_copy = today_copy - - if not self.next_schedule_date: - self.next_schedule_date = get_next_schedule_date( - start_date_copy, self.frequency, self.repeat_on_day) - - def on_submit(self): - self.update_auto_repeat_id() - - def on_cancel(self): - self.update_status() - - def on_update_after_submit(self): - self.validate_dates() - self.set_next_schedule_date() - - def before_cancel(self): - self.unlink_auto_repeat_id() - self.next_schedule_date = None - - def unlink_auto_repeat_id(self): - frappe.db.sql( - "update `tab{0}` set auto_repeat = null where auto_repeat=%s".format(self.reference_doctype), self.name) - - def validate_reference_doctype(self): - if not frappe.get_meta(self.reference_doctype).has_field('auto_repeat'): - frappe.throw(_("Add custom field Auto Repeat in the doctype {0}").format(self.reference_doctype)) - - def validate_dates(self): - if self.end_date and getdate(self.start_date) > getdate(self.end_date): - frappe.throw(_("End date must be greater than start date")) - - def validate_next_schedule_date(self): - if self.repeat_on_day and self.next_schedule_date: - next_date = getdate(self.next_schedule_date) - if next_date.day != self.repeat_on_day: - # if the repeat day is the last day of the month (31) - # and the current month does not have as many days, - # then the last day of the current month is a valid date - lastday = calendar.monthrange(next_date.year, next_date.month)[1] - if self.repeat_on_day < lastday: - # the specified day of the month is not same as the day specified - # or the last day of the month - frappe.throw(_("Next Date's day and Repeat on Day of Month must be equal")) - - def validate_email_id(self): - if self.notify_by_email: - if self.recipients: - email_list = split_emails(self.recipients.replace("\n", "")) - - from frappe.utils import validate_email_address - for email in email_list: - if not validate_email_address(email): - frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email)) - else: - frappe.throw(_("'Recipients' not specified")) - - def set_next_schedule_date(self): - if self.repeat_on_day: - self.next_schedule_date = get_next_date(self.next_schedule_date, 0, self.repeat_on_day) - - def update_auto_repeat_id(self): - frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name) - - def update_status(self, status=None): - self.status = { - '0': 'Draft', - '1': 'Submitted', - '2': 'Cancelled' - }[cstr(self.docstatus or 0)] - - if status and status != 'Resumed': - self.status = status - - if self.docstatus == 2: - self.db_set("status", self.status) - - def get_auto_repeat_schedule(self): - schedule_details = [] - start_date_copy = getdate(self.start_date) - end_date_copy = getdate(self.end_date) - today_copy = frappe.utils.datetime.date.today() - - if start_date_copy < today_copy: - start_date_copy = today_copy - - if not self.end_date: - days = 60 if self.frequency in ['Daily', 'Weekly'] else 365 - end_date_copy = add_days(today_copy, days) - - start_date_copy = get_next_schedule_date(start_date_copy, self.frequency, self.repeat_on_day) - while (getdate(start_date_copy) < getdate(end_date_copy)): - row = { - "reference_document" : self.reference_document, - "frequency" : self.frequency, - "next_scheduled_date" : start_date_copy - } - schedule_details.append(row) - start_date_copy = get_next_schedule_date(start_date_copy, self.frequency, self.repeat_on_day) - - return schedule_details - - def link_party(self): - reference = frappe.get_meta(self.reference_doctype) - for field in reference.fields: - if field.options in ['Customer', 'Supplier', 'Employee']: - self.reference_party_doctype = field.options - self.reference_party = frappe.db.get_value(self.reference_doctype, self.reference_document, field.fieldname) - break - -def get_next_schedule_date(start_date, frequency, repeat_on_day): - mcount = month_map.get(frequency) - if mcount: - next_date = get_next_date(start_date, mcount, repeat_on_day) - else: - days = 7 if frequency == 'Weekly' else 1 - next_date = add_days(start_date, days) - return next_date - -def make_auto_repeat_entry(date=None): - enqueued_method = 'frappe.desk.doctype.auto_repeat.auto_repeat.create_repeated_entries' - jobs = get_jobs() - - if not jobs or enqueued_method not in jobs[frappe.local.site]: - date = date or today() - for data in get_auto_repeat_entries(date): - frappe.enqueue(enqueued_method, data=data) - -def create_repeated_entries(data): - schedule_date = getdate(data.next_schedule_date) - while schedule_date <= getdate(today()) and not frappe.db.get_value('Auto Repeat', data.name, 'disabled'): - create_documents(data, schedule_date) - schedule_date = get_next_schedule_date(schedule_date, data.frequency, data.repeat_on_day) - - if schedule_date and not frappe.db.get_value('Auto Repeat', data.name, 'disabled'): - frappe.db.set_value('Auto Repeat', data.name, 'next_schedule_date', schedule_date) - frappe.db.commit() - -def get_auto_repeat_entries(date): - return frappe.db.sql(""" select * from `tabAuto Repeat` - where docstatus = 1 and next_schedule_date <=%s - and reference_document is not null and reference_document != '' - and next_schedule_date <= ifnull(end_date, '2199-12-31') - and disabled = 0 and status != 'Stopped' """, (date), as_dict=1) - -def create_documents(data, schedule_date): - try: - doc = make_new_document(data, schedule_date) - if data.notify_by_email and data.recipients: - print_format = data.print_format or "Standard" - send_notification(doc, data, print_format=print_format) - - frappe.db.commit() - except Exception: - frappe.db.rollback() - frappe.db.begin() - frappe.log_error(frappe.get_traceback(), _("Recurring document creation failure")) - disable_auto_repeat(data) - frappe.db.commit() - if data.reference_document and not frappe.flags.in_test: - notify_error_to_user(data) - -def disable_auto_repeat(data): - auto_repeat = frappe.get_doc('Auto Repeat', data.name) - auto_repeat.db_set('disabled', 1) - -def notify_error_to_user(data): - party = '' - party_type = '' - - if data.reference_doctype in ['Sales Order', 'Sales Invoice', 'Delivery Note']: - party_type = 'customer' - elif data.reference_doctype in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']: - party_type = 'supplier' - - if party_type: - party = frappe.db.get_value(data.reference_doctype, data.reference_document, party_type) - - notify_errors(data.reference_document, data.reference_doctype, party, data.owner, data.name) - -def make_new_document(args, schedule_date): - doc = frappe.get_doc(args.reference_doctype, args.reference_document) - new_doc = frappe.copy_doc(doc, ignore_no_copy=False) - update_doc(new_doc, doc, args, schedule_date) - new_doc.insert(ignore_permissions=True) - - if args.submit_on_creation: - new_doc.submit() - - return new_doc - -def update_doc(new_document, reference_doc, args, schedule_date): - new_document.docstatus = 0 - if new_document.meta.get_field('set_posting_time'): - new_document.set('set_posting_time', 1) - - mcount = month_map.get(args.frequency) - - if new_document.meta.get_field('auto_repeat'): - new_document.set('auto_repeat', args.name) - - for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', - 'select_print_heading', 'remarks', 'owner']: - if new_document.meta.get_field(fieldname): - new_document.set(fieldname, reference_doc.get(fieldname)) - - # copy item fields - if new_document.meta.get_field('items'): - for i, item in enumerate(new_document.items): - for fieldname in ("page_break",): - item.set(fieldname, reference_doc.items[i].get(fieldname)) - - for data in new_document.meta.fields: - if data.fieldtype == 'Date' and data.reqd: - new_document.set(data.fieldname, schedule_date) - - set_auto_repeat_period(args, mcount, new_document) - - new_document.run_method("on_recurring", reference_doc=reference_doc, auto_repeat_doc=args) - -def set_auto_repeat_period(args, mcount, new_document): - if mcount and new_document.meta.get_field('from_date') and new_document.meta.get_field('to_date'): - last_ref_doc = frappe.db.sql(""" - select name, from_date, to_date - from `tab{0}` - where auto_repeat=%s and docstatus < 2 - order by creation desc - limit 1 - """.format(args.reference_doctype), args.name, as_dict=1) - - if not last_ref_doc: - return - - from_date = get_next_date(last_ref_doc[0].from_date, mcount) - - if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \ - (cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)): - to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount)) - else: - to_date = get_next_date(last_ref_doc[0].to_date, mcount) - - new_document.set('from_date', from_date) - new_document.set('to_date', to_date) - -def get_next_date(dt, mcount, day=None): - dt = getdate(dt) - dt += relativedelta(months=mcount, day=day) - - return dt - -def send_notification(new_rv, auto_repeat_doc, print_format='Standard'): - """Notify concerned persons about recurring document generation""" - print_format = print_format - subject = auto_repeat_doc.subject or '' - message = auto_repeat_doc.message or '' - - if not auto_repeat_doc.subject: - subject = _("New {0}: #{1}").format(new_rv.doctype, new_rv.name) - elif "{" in auto_repeat_doc.subject: - subject = frappe.render_template(auto_repeat_doc.subject, {'doc': new_rv}) - - if not auto_repeat_doc.message: - message = _("Please find attached {0} #{1}").format(new_rv.doctype, new_rv.name) - elif "{" in auto_repeat_doc.message: - message = frappe.render_template(auto_repeat_doc.message, {'doc': new_rv}) - - attachments = [frappe.attach_print(new_rv.doctype, new_rv.name, - file_name=new_rv.name, print_format=print_format)] - - make(doctype=new_rv.doctype, name=new_rv.name, recipients=auto_repeat_doc.recipients, - subject=subject, content=message, attachments=attachments, send_email=1) - -def notify_errors(doc, doctype, party, owner, name): - recipients = get_system_managers(only_name=True) - frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")], - subject=_("[Urgent] Error while creating recurring %s for %s" % (doctype, doc)), - message=frappe.get_template("templates/emails/recurring_document_failed.html").render({ - "type": _(doctype), - "name": doc, - "party": party or "", - "auto_repeat": name - })) - try: - assign_task_to_owner(name, _("Recurring Documents Failed"), recipients) - except Exception: - frappe.log_error(frappe.get_traceback(), _("Recurring Documents Failed")) - -def assign_task_to_owner(name, msg, users): - for d in users: - args = { - 'doctype': 'Auto Repeat', - 'assign_to': d, - 'name': name, - 'description': msg, - 'priority': 'High' - } - assign_to.add(args) - -@frappe.whitelist() -def make_auto_repeat(doctype, docname): - doc = frappe.new_doc('Auto Repeat') - - reference_doc = frappe.get_doc(doctype, docname) - doc.reference_doctype = doctype - doc.reference_document = docname - doc.start_date = reference_doc.get('posting_date') or reference_doc.get('transaction_date') - return doc - -@frappe.whitelist() -def stop_resume_auto_repeat(auto_repeat, status): - doc = frappe.get_doc('Auto Repeat', auto_repeat) - frappe.msgprint(_("Auto Repeat has been {0}").format(status)) - if status == 'Resumed': - doc.next_schedule_date = get_next_schedule_date(today(), - doc.frequency, doc.repeat_on_day) - - doc.update_status(status) - doc.save() - - return doc.status - -def auto_repeat_doctype_query(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select parent from `tabDocField` - where fieldname = 'auto_repeat' - and parent like %(txt)s - order by - if(locate(%(_txt)s, parent), locate(%(_txt)s, parent), 99999), - parent - limit %(start)s, %(page_len)s""".format(**{ - 'key': searchfield, - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len - }) - -@frappe.whitelist() -def get_contacts(reference_doctype, reference_name): - docfields = frappe.get_meta(reference_doctype).fields - - contact_fields = [] - for field in docfields: - if field.fieldtype == "Link" and field.options == "Contact": - contact_fields.append(field.fieldname) - - if contact_fields: - contacts = [] - for contact_field in contact_fields: - contacts.append(frappe.db.get_value(reference_doctype, reference_name, contact_field)) - else: - return [] - - if contacts: - emails = [] - for contact in contacts: - emails.append(frappe.db.get_value("Contact", contact, "email_id")) - - return emails - else: - return [] - - -@frappe.whitelist() -def update_reference(docname, reference): - try: - frappe.db.set_value("Auto Repeat", docname, "reference_document", reference) - return "success" - except Exception as e: - raise e - return "error" - -@frappe.whitelist() -def generate_message_preview(reference_dt, reference_doc, message=None, subject=None): - doc = frappe.get_doc(reference_dt, reference_doc) - subject_preview = _("Please add a subject to your email") - msg_preview = frappe.render_template(message, {'doc': doc}) - if subject: - subject_preview = frappe.render_template(subject, {'doc': doc}) - - return {'message': msg_preview, 'subject': subject_preview} diff --git a/frappe/desk/doctype/auto_repeat/auto_repeat_list.js b/frappe/desk/doctype/auto_repeat/auto_repeat_list.js deleted file mode 100644 index 295d9391b0..0000000000 --- a/frappe/desk/doctype/auto_repeat/auto_repeat_list.js +++ /dev/null @@ -1,16 +0,0 @@ -frappe.listview_settings['Auto Repeat'] = { - add_fields: ["next_schedule_date"], - get_indicator: function(doc) { - if(doc.disabled) { - return [__("Disabled"), "red"]; - } else if(doc.next_schedule_date >= frappe.datetime.get_today() && doc.status != 'Stopped') { - return [__("Active"), "green"]; - } else if(doc.docstatus === 0) { - return [__("Draft"), "red", "docstatus,=,0"]; - } else if(doc.status === 'Stopped') { - return [__("Stopped"), "red"]; - } else { - return [__("Expired"), "darkgrey"]; - } - } -}; \ No newline at end of file diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index ff54f95031..0273fe80b1 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe, json +from frappe import _ 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 from frappe.model.document import Document @@ -44,7 +45,7 @@ def get(chart_name, from_date=None, to_date=None, refresh = None): '''.format( unit_function = get_unit_function(chart.based_on, timegrain), datefield = chart.based_on, - aggregate_function = chart.chart_type, + aggregate_function = get_aggregate_function(chart.chart_type), value_field = chart.value_based_on or '1', doctype = chart.document_type, conditions = conditions, @@ -66,6 +67,14 @@ def get(chart_name, from_date=None, to_date=None, refresh = None): }] } +def get_aggregate_function(chart_type): + return { + "Sum": "SUM", + "Count": "COUNT", + "Average": "AVG" + }[chart_type] + + def convert_to_dates(data, timegrain): result = [] for d in data: @@ -199,6 +208,6 @@ class DashboardChart(Document): def check_required_field(self): if not self.based_on: - frappe.throw("Time series based on is required to create a dashboard chart") + frappe.throw(_("Time series based on is required to create a dashboard chart")) if not self.document_type: - frappe.throw("Document type is required to create a dashboard chart") \ No newline at end of file + frappe.throw(_("Document type is required to create a dashboard chart")) \ No newline at end of file diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index 1ad57246a6..e88c11b4f8 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -71,7 +71,7 @@ class Event(Document): communication.communication_date = self.starts_on communication.reference_doctype = self.doctype communication.reference_name = self.name - communication.communication_medium = communication_mapping[self.event_category] if self.event_category else "" + communication.communication_medium = communication_mapping.get(self.event_category) if self.event_category else "" communication.status = "Linked" communication.add_link(participant.reference_doctype, participant.reference_docname) communication.save(ignore_permissions=True) diff --git a/frappe/desk/doctype/todo/todo.json b/frappe/desk/doctype/todo/todo.json index 4b70086648..a0343e9f69 100644 --- a/frappe/desk/doctype/todo/todo.json +++ b/frappe/desk/doctype/todo/todo.json @@ -684,4 +684,4 @@ "track_changes": 1, "track_seen": 1, "track_views": 0 -} \ No newline at end of file +} diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 3b67b78144..e790bf6d06 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -252,4 +252,4 @@ def get_view_logs(doctype, docname): if view_logs: logs = view_logs - return logs \ No newline at end of file + return logs diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index b0d9a5d123..da7c7a3a9e 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -18,7 +18,6 @@ def remove_attach(): file_name = frappe.form_dict.get('file_name') frappe.delete_doc('File', fid) - @frappe.whitelist() def validate_link(): """validate link when updated by user""" @@ -84,27 +83,23 @@ def update_comment(name, content): doc.save(ignore_permissions=True) @frappe.whitelist() -def get_next(doctype, value, prev, filters=None, order_by="modified desc"): - - prev = not int(prev) - sort_field, sort_order = order_by.split(" ") +def get_next(doctype, value, prev, filters, sort_order, sort_field): + prev = int(prev) if not filters: filters = [] if isinstance(filters, string_types): filters = json.loads(filters) - # condition based on sort order - condition = ">" if sort_order.lower()=="desc" else "<" + # # condition based on sort order + condition = ">" if sort_order.lower() == "asc" else "<" # switch the condition if prev: - condition = "<" if condition==">" else "<" - else: - sort_order = "asc" if sort_order.lower()=="desc" else "desc" + sort_order = "asc" if sort_order.lower() == "desc" else "desc" + condition = "<" if condition == ">" else ">" - # add condition for next or prev item - if not order_by[0] in [f[1] for f in filters]: - filters.append([doctype, sort_field, condition, value]) + # # add condition for next or prev item + filters.append([doctype, sort_field, condition, frappe.get_value(doctype, value, sort_field)]) res = frappe.get_list(doctype, fields = ["name"], diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py index f31aae401e..f4927dd098 100644 --- a/frappe/desk/listview.py +++ b/frappe/desk/listview.py @@ -3,8 +3,6 @@ from __future__ import unicode_literals import frappe -import json - @frappe.whitelist() def get_list_settings(doctype): @@ -22,31 +20,37 @@ def set_list_settings(doctype, values): doc = frappe.new_doc("List View Setting") doc.name = doctype frappe.clear_messages() - doc.update(json.loads(values)) + doc.update(frappe.parse_json(values)) doc.save() + @frappe.whitelist() -def get_user_assignments_and_count(doctype, current_filters): - +def get_group_by_count(doctype, current_filters, field): + current_filters = frappe.parse_json(current_filters) subquery_condition = '' - if current_filters: - # get the subquery - subquery = frappe.get_all(doctype, - filters=current_filters, return_query = True) + + subquery = frappe.get_all(doctype, filters=current_filters, return_query = True) + if field == 'assigned_to': subquery_condition = ' and `tabToDo`.reference_name in ({subquery})'.format(subquery = subquery) + return frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count + from + `tabToDo`, `tabUser` + where + `tabToDo`.status='Open' and + `tabToDo`.owner = `tabUser`.name and + `tabUser`.user_type = 'System User' + {subquery_condition} + group by + `tabToDo`.owner + order by + count desc + limit 50""".format(subquery_condition = subquery_condition), as_dict=True) + else : + return frappe.db.get_list(doctype, + filters=current_filters, + group_by=field, + fields=['count(*) as count', field + ' as name'], + order_by='count desc', + limit=50, + ) - todo_list = frappe.db.sql("""select `tabToDo`.owner as name, count(*) as count - from - `tabToDo`, `tabUser` - where - `tabToDo`.status='Open' and - `tabToDo`.owner = `tabUser`.name and - `tabUser`.user_type = 'System User' - {subquery_condition} - group by - `tabToDo`.owner - order by - count desc - limit 50""".format(subquery_condition = subquery_condition), as_dict=True) - - return todo_list \ No newline at end of file diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py index ccde3aad40..ca3eef5c52 100644 --- a/frappe/desk/moduleview.py +++ b/frappe/desk/moduleview.py @@ -287,44 +287,141 @@ def get_onboard_items(app, module): return onboard_items or fallback_items - @frappe.whitelist() +def get_links_for_module(app, module): + return [l.get('label') for l in get_links(app, module)] + def get_links(app, module): try: sections = get_config(app, frappe.scrub(module)) except ImportError: return [] - link_names = [] - + links = [] for section in sections: - for item in section["items"]: - link_names.append(item.get("label")) - return link_names + for item in section['items']: + links.append(item) + return links @frappe.whitelist() -def hide_modules_from_desktop(modules): +def get_desktop_settings(): + from frappe.config import get_modules_from_all_apps_for_user + all_modules = get_modules_from_all_apps_for_user() + home_settings = get_home_settings() + + modules_by_name = {} + for m in all_modules: + modules_by_name[m['module_name']] = m + + module_categories = ['Modules', 'Domains', 'Places', 'Administration'] + user_modules_by_category = {} + + user_saved_modules_by_category = home_settings.modules_by_category or {} + user_saved_links_by_module = home_settings.links_by_module or {} + + def apply_user_saved_links(module): + module = frappe._dict(module) + all_links = get_links(module.app, module.module_name) + module_links_by_label = {} + for link in all_links: + module_links_by_label[link['label']] = link + + if module.module_name in user_saved_links_by_module: + user_links = frappe.parse_json(user_saved_links_by_module[module.module_name]) + module.links = [module_links_by_label[l] for l in user_links if l in module_links_by_label] + + return module + + for category in module_categories: + if category in user_saved_modules_by_category: + user_modules = user_saved_modules_by_category[category] + user_modules_by_category[category] = [apply_user_saved_links(modules_by_name[m]) \ + for m in user_modules] + else: + user_modules_by_category[category] = [apply_user_saved_links(m) \ + for m in all_modules if m.get('category') == category] + + # filter out hidden modules + if home_settings.hidden_modules: + for category in user_modules_by_category: + hidden_modules = home_settings.hidden_modules or [] + modules = user_modules_by_category[category] + user_modules_by_category[category] = [module for module in modules if module.module_name not in hidden_modules] + + return user_modules_by_category + +@frappe.whitelist() +def update_hidden_modules(category_map): + category_map = frappe.parse_json(category_map) + home_settings = get_home_settings() + + saved_hidden_modules = home_settings.hidden_modules or [] + + for category in category_map: + config = frappe._dict(category_map[category]) + saved_hidden_modules += config.removed or [] + saved_hidden_modules = [d for d in saved_hidden_modules if d not in (config.added or [])] + + home_settings.hidden_modules = saved_hidden_modules + set_home_settings(home_settings) + + return get_desktop_settings() + + +@frappe.whitelist() +def update_modules_order(module_category, modules): modules = frappe.parse_json(modules) - home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings') - home_settings = frappe.parse_json(home_settings or '{}') - - home_settings['hidden_modules'] = modules - frappe.db.set_value('User', frappe.session.user, 'home_settings', json.dumps(home_settings)) - - return home_settings + home_settings = get_home_settings() + home_settings.modules_by_category = home_settings.modules_by_category or {} + home_settings.modules_by_category[module_category] = modules + set_home_settings(home_settings) @frappe.whitelist() def update_links_for_module(module_name, links): - home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings') - home_settings = frappe.parse_json(home_settings or '{}') + links = frappe.parse_json(links) + home_settings = get_home_settings() - home_settings.setdefault('links', {}) - home_settings['links'].setdefault(module_name, None) - home_settings['links'][module_name] = links + home_settings.setdefault('links_by_module', {}) + home_settings['links_by_module'].setdefault(module_name, None) + home_settings['links_by_module'][module_name] = links + + set_home_settings(home_settings) + + return get_desktop_settings() + +@frappe.whitelist() +def get_options_for_show_hide_cards(): + from frappe.config import get_modules_from_all_apps_for_user + all_modules = get_modules_from_all_apps_for_user() + home_settings = get_home_settings() + + hidden_modules = home_settings.hidden_modules or [] + + options = [] + for module in all_modules: + module = frappe._dict(module) + options.append({ + 'category': module.category, + 'label': module.label, + 'value': module.module_name, + 'checked': module.module_name not in hidden_modules + }) + + return options + +def set_home_settings(home_settings): + frappe.cache().hset('home_settings', frappe.session.user, home_settings) frappe.db.set_value('User', frappe.session.user, 'home_settings', json.dumps(home_settings)) +@frappe.whitelist() +def get_home_settings(): + def get_from_db(): + settings = frappe.db.get_value("User", frappe.session.user, 'home_settings') + return frappe.parse_json(settings or '{}') + + home_settings = frappe.cache().hget('home_settings', frappe.session.user, get_from_db) return home_settings diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index fa131c9c02..9a950a694d 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -213,7 +213,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { } setTimeout(function() { // Reload - window.location.href = ''; + window.location.href = '/desk'; }, 2000); } diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index caf6a7d4f0..9d6f3561cb 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -307,6 +307,7 @@ def export_query(): if isinstance(data.get("file_format_type"), string_types): file_format_type = data["file_format_type"] + include_indentation = data["include_indentation"] if isinstance(data.get("visible_idx"), string_types): visible_idx = json.loads(data.get("visible_idx")) else: @@ -318,7 +319,7 @@ def export_query(): columns = get_columns_dict(data.columns) from frappe.utils.xlsxutils import make_xlsx - xlsx_data = build_xlsx_data(columns, data, visible_idx) + xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation) xlsx_file = make_xlsx(xlsx_data, "Query Report") frappe.response['filename'] = report_name + '.xlsx' @@ -326,7 +327,7 @@ def export_query(): frappe.response['type'] = 'binary' -def build_xlsx_data(columns, data, visible_idx): +def build_xlsx_data(columns, data, visible_idx,include_indentation): result = [[]] # add column headings @@ -344,7 +345,7 @@ def build_xlsx_data(columns, data, visible_idx): label = columns[idx]["label"] fieldname = columns[idx]["fieldname"] cell_value = row.get(fieldname, row.get(label, "")) - if 'indent' in row and idx == 0: + if cint(include_indentation) and 'indent' in row and idx == 0: cell_value = (' ' * cint(row['indent'])) + cell_value row_data.append(cell_value) else: diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 5ed4dc730b..9654e14687 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -185,6 +185,10 @@ def append_totals_row(data): for i in range(len(row)): if isinstance(row[i], (float, int)): totals[i] = (totals[i] or 0) + row[i] + + if not isinstance(totals[0], (int, float)): + totals[0] = 'Total' + data.append(totals) return data diff --git a/frappe/email/doctype/notification/notification.json b/frappe/email/doctype/notification/notification.json index b57a77a953..14eff2251a 100644 --- a/frappe/email/doctype/notification/notification.json +++ b/frappe/email/doctype/notification/notification.json @@ -1,1163 +1,287 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, "allow_rename": 1, "autoname": "Prompt", - "beta": 0, "creation": "2014-07-11 17:18:09.923399", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "System", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "enabled", + "column_break_2", + "channel", + "slack_webhook_url", + "filters", + "subject", + "document_type", + "is_standard", + "module", + "col_break_1", + "event", + "method", + "date_changed", + "days_in_advance", + "value_changed", + "sender", + "sender_email", + "section_break_9", + "condition", + "column_break_6", + "html_7", + "property_section", + "set_property_after_alert", + "property_value", + "column_break_5", + "recipients", + "message_sb", + "message", + "message_examples", + "slack_message_examples", + "view_properties", + "column_break_25", + "attach_print", + "print_format" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "enabled", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Enabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Enabled" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Email", "fieldname": "channel", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Channel", - "length": 0, - "no_copy": 0, "options": "Email\nSlack", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 + "set_only_once": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.channel=='Slack'", "fieldname": "slack_webhook_url", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Slack Channel", - "length": 0, - "no_copy": 0, - "options": "Slack Webhook URL", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Slack Webhook URL" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "filters", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Filters", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Filters" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "To add dynamic subject, use jinja tags like\n\n
{{ doc.name }} Delivered
", "fieldname": "subject", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, "ignore_xss_filter": 1, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Subject", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "document_type", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "Document Type", - "length": 0, - "no_copy": 0, "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "is_standard", "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": "Is Standard", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Is Standard" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "is_standard", "fieldname": "module", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "Module", - "length": 0, - "no_copy": 0, - "options": "Module Def", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Module Def" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "col_break_1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "event", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Send Alert On", - "length": 0, - "no_copy": 0, "options": "\nNew\nSave\nSubmit\nCancel\nDays After\nDays Before\nValue Change\nMethod\nCustom", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "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", - "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": "Trigger Method", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Trigger Method" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "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", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reference Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Reference Date" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", "depends_on": "eval:doc.event==\"Days After\" || doc.event==\"Days Before\"", "description": "Send days before or after the reference date", "fieldname": "days_in_advance", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Days Before or After", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Days Before or After" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.event==\"Value Change\"", "description": "Send alert if this field's value changes", "fieldname": "value_changed", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Value Changed", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Value Changed" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "sender", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Sender", - "length": 0, - "no_copy": 0, - "options": "Email Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "options": "Email Account" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "sender_email", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Sender Email", - "length": 0, - "no_copy": 0, "options": "Email", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", "description": "Optional: The alert will be sent if this expression is true", "fieldname": "condition", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, "ignore_xss_filter": 1, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Condition", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Condition" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_6", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "html_7", "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "options": "

Condition Examples:

\n
doc.status==\"Open\"
doc.due_date==nowdate()
doc.total > 40000\n
\n

Hints:

\n
    \n
  1. To check for an event every day, select \"Date Change\" in Event
  2. \n
  3. To send an alert if a particular value changes, select \"Value Change\"
  4. \n
", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "

Condition Examples:

\n
doc.status==\"Open\"
doc.due_date==nowdate()
doc.total > 40000\n
\n" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, "fieldname": "property_section", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Set Property After Alert", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Set Property After Alert" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "set_property_after_alert", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Set Property After Alert", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Set Property After Alert" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "property_value", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Value To Be Set", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Value To Be Set" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.channel=='Email'", "fieldname": "column_break_5", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Recipients", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Recipients" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "recipients", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Recipients", - "length": 0, - "no_copy": 0, - "options": "Notification Recipient", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Notification Recipient" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "message_sb", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Message", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Message" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Add your message here", - "depends_on": "", "fieldname": "message", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, "ignore_xss_filter": 1, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Message", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Message" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.channel=='Email'", "fieldname": "message_examples", "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Message Examples", - "length": 0, - "no_copy": 0, - "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
", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "
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
" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.channel=='Slack'", "fieldname": "slack_message_examples", "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Message Examples", - "length": 0, - "no_copy": 0, - "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
", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "
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
" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "view_properties", "fieldtype": "Button", - "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": "View Properties (via Customize Form)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "View Properties (via Customize Form)" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, "collapsible_depends_on": "attach_print", - "columns": 0, "fieldname": "column_break_25", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Print Settings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Print Settings" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "attach_print", "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": "Attach Print", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Attach Print" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "attach_print", "fieldname": "print_format", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Print Format", - "length": 0, - "no_copy": 0, - "options": "Print Format", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Print Format" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-envelope", - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-06-05 14:32:02.179599", + "modified": "2019-07-15 13:17:02.585013", "modified_by": "Administrator", "module": "Email", "name": "Notification", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, - "email": 0, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", "title_field": "subject", - "track_changes": 1, - "track_seen": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/hooks.py b/frappe/hooks.py index d3356d64ce..446b38464c 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -18,8 +18,8 @@ app_email = "info@frappe.io" docs_app = "frappe_io" -translation_contribution_url = "https://translate.erpnext.xyz/api/method/translator.api.add_translation" -translation_contribution_status = "https://translate.erpnext.xyz/api/method/translator.api.translation_status" +translation_contribution_url = "https://translate.erpnext.com/api/method/translator.api.add_translation" +translation_contribution_status = "https://translate.erpnext.com/api/method/translator.api.translation_status" before_install = "frappe.utils.install.before_install" after_install = "frappe.utils.install.after_install" @@ -78,6 +78,8 @@ on_session_creation = [ "frappe.utils.scheduler.reset_enabled_scheduler_events", ] +on_logout = "frappe.core.doctype.session_default_settings.session_default_settings.clear_session_defaults" + # permissions permission_query_conditions = { @@ -173,6 +175,8 @@ scheduler_events = { "frappe.desk.form.document_follow.send_daily_updates", "frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points", "frappe.integrations.doctype.google_contacts.google_contacts.sync", + "frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry", + "frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed" ], "daily_long": [ "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", @@ -227,6 +231,7 @@ bot_parsers = [ setup_wizard_exception = "frappe.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception" before_migrate = ['frappe.patches.v11_0.sync_user_permission_doctype_before_migrate.execute'] +after_migrate = ['frappe.website.doctype.website_theme.website_theme.generate_theme_files_if_not_exist'] otp_methods = ['OTP App','Email','SMS'] user_privacy_documents = [ diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js index c264d12dec..ea731fafc2 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js @@ -37,7 +37,8 @@ frappe.ui.form.on('Dropbox Settings', { }, take_backup: function(frm) { - if ((frm.doc.app_access_key && frm.doc.app_secret_key) || (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config)){ + if (frm.doc.enabled && ((frm.doc.app_access_key && frm.doc.app_secret_key) + || (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config))) { frm.add_custom_button(__("Take Backup Now"), function(frm){ frappe.call({ method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup", diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 67f1baf30a..6610173e3e 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -73,6 +73,7 @@ def authorize_access(g_contact, reauthorize=None): if "refresh_token" in r: frappe.db.set_value("Google Contacts", google_contact.name, "refresh_token", r.get("refresh_token")) + frappe.db.commit() frappe.local.response["type"] = "redirect" frappe.local.response["location"] = "/desk#Form/Google%20Contacts/{}".format(google_contact.name) @@ -93,6 +94,7 @@ def google_callback(client_id=None, redirect_uri=None, code=None): else: google_contact = frappe.cache().hget("google_contacts", "google_contact") frappe.db.set_value("Google Contacts", google_contact, "authorization_code", code) + frappe.db.commit() authorize_access(google_contact) @@ -133,23 +135,24 @@ def sync(g_contact=None): for name in connection.get("names"): if name.get("metadata").get("primary"): - for email in connection.get("emailAddresses"): - if not frappe.db.exists("Contact", {"email_id": email.get("value")}): - contacts_updated += 1 + if connection.get("emailAddresses"): + for email in connection.get("emailAddresses"): + if not frappe.db.exists("Contact", {"email_id": email.get("value")}): + contacts_updated += 1 - frappe.get_doc({ - "doctype": "Contact", - "salutation": name.get("honorificPrefix") if name.get("honorificPrefix") else "", - "first_name": name.get("givenName") if name.get("givenName") else "", - "middle_name": name.get("middleName") if name.get("middleName") else "", - "last_name": name.get("familyName") if name.get("familyName") else "", - "email_id": email.get("value") if email.get("value") else "", - "designation": get_indexed_value(connection.get("organizations"), 0, "title"), - "phone": get_indexed_value(connection.get("phoneNumbers"), 0, "value"), - "mobile_no": get_indexed_value(connection.get("phoneNumbers"), 1, "value"), - "source": "Google Contacts", - "google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name") - }).insert(ignore_permissions=True) + frappe.get_doc({ + "doctype": "Contact", + "salutation": name.get("honorificPrefix") if name.get("honorificPrefix") else "", + "first_name": name.get("givenName") if name.get("givenName") else "", + "middle_name": name.get("middleName") if name.get("middleName") else "", + "last_name": name.get("familyName") if name.get("familyName") else "", + "email_id": email.get("value") if email.get("value") else "", + "designation": get_indexed_value(connection.get("organizations"), 0, "title"), + "phone": get_indexed_value(connection.get("phoneNumbers"), 0, "value"), + "mobile_no": get_indexed_value(connection.get("phoneNumbers"), 1, "value"), + "source": "Google Contacts", + "google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name") + }).insert(ignore_permissions=True) if g_contact: return _("{0} Google Contacts synced.").format(contacts_updated) if contacts_updated > 0 else _("No new Google Contacts synced.") diff --git a/frappe/integrations/doctype/google_maps_settings/google_maps_settings.js b/frappe/integrations/doctype/google_maps_settings/google_maps_settings.js deleted file mode 100644 index ef98ce2c47..0000000000 --- a/frappe/integrations/doctype/google_maps_settings/google_maps_settings.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Google Maps Settings', { -}); diff --git a/frappe/integrations/doctype/google_maps_settings/google_maps_settings.json b/frappe/integrations/doctype/google_maps_settings/google_maps_settings.json deleted file mode 100644 index 7a0695906e..0000000000 --- a/frappe/integrations/doctype/google_maps_settings/google_maps_settings.json +++ /dev/null @@ -1,159 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-10-16 17:13:05.684227", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "enabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Enabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "client_key", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Client Key", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "home_address", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Home Address", - "length": 0, - "no_copy": 0, - "options": "Address", - "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": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 14:53:09.170463", - "modified_by": "Administrator", - "module": "Integrations", - "name": "Google Maps Settings", - "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": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/frappe/integrations/doctype/google_maps_settings/google_maps_settings.py b/frappe/integrations/doctype/google_maps_settings/google_maps_settings.py deleted file mode 100644 index 71aafd3558..0000000000 --- a/frappe/integrations/doctype/google_maps_settings/google_maps_settings.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals - -import frappe -from frappe import _ -from frappe.model.document import Document - - -class GoogleMapsSettings(Document): - def validate(self): - if self.enabled: - if not self.client_key: - frappe.throw(_("Client key is required")) - if not self.home_address: - frappe.throw(_("Home Address is required")) - - def get_client(self): - if not self.enabled: - frappe.throw(_("Google Maps integration is not enabled")) - - import googlemaps - - try: - client = googlemaps.Client(key=self.client_key) - except Exception as e: - frappe.throw(e.message) - - return client diff --git a/frappe/integrations/doctype/google_maps_settings/test_google_maps_settings.js b/frappe/integrations/doctype/google_maps_settings/test_google_maps_settings.js deleted file mode 100644 index a3c72bc42a..0000000000 --- a/frappe/integrations/doctype/google_maps_settings/test_google_maps_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Google Maps Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Google Maps - () => frappe.tests.make('Google Maps Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/frappe/integrations/doctype/google_maps_settings/test_google_maps_settings.py b/frappe/integrations/doctype/google_maps_settings/test_google_maps_settings.py deleted file mode 100644 index f5717e19e0..0000000000 --- a/frappe/integrations/doctype/google_maps_settings/test_google_maps_settings.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies and Contributors -# See license.txt -from __future__ import unicode_literals -import unittest - -class TestGoogleMapsSettings(unittest.TestCase): - pass diff --git a/frappe/integrations/doctype/google_settings/google_settings.json b/frappe/integrations/doctype/google_settings/google_settings.json index c61d6606b5..8a316fd67e 100644 --- a/frappe/integrations/doctype/google_settings/google_settings.json +++ b/frappe/integrations/doctype/google_settings/google_settings.json @@ -6,7 +6,8 @@ "enable", "google_credentials", "client_id", - "client_secret" + "client_secret", + "api_key" ], "fields": [ { @@ -32,10 +33,15 @@ "fieldtype": "Password", "in_list_view": 1, "label": "Client Secret" + }, + { + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key" } ], "issingle": 1, - "modified": "2019-06-19 15:28:05.957380", + "modified": "2019-06-29 13:26:33.201060", "modified_by": "Administrator", "module": "Integrations", "name": "Google Settings", diff --git a/frappe/integrations/doctype/ldap_group_mapping/__init__.py b/frappe/integrations/doctype/ldap_group_mapping/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json new file mode 100644 index 0000000000..92db68e962 --- /dev/null +++ b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json @@ -0,0 +1,38 @@ +{ + "creation": "2019-05-29 01:24:29.585060", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "ldap_group", + "erpnext_role" + ], + "fields": [ + { + "fieldname": "ldap_group", + "fieldtype": "Data", + "in_list_view": 1, + "label": "LDAP Group", + "reqd": 1 + }, + { + "fieldname": "erpnext_role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "ERPNext Role", + "options": "Role", + "reqd": 1 + } + ], + "istable": 1, + "modified": "2019-07-15 06:46:38.050408", + "modified_by": "Administrator", + "module": "Integrations", + "name": "LDAP Group Mapping", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py new file mode 100644 index 0000000000..f9f2adeed0 --- /dev/null +++ b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class LDAPGroupMapping(Document): + pass diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.json b/frappe/integrations/doctype/ldap_settings/ldap_settings.json index aa43b2e9d0..5d30a873fb 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.json +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.json @@ -1,594 +1,215 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, "creation": "2016-09-22 04:16:48.829658", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "System", "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enabled", + "ldap_server_url", + "column_break_4", + "base_dn", + "password", + "section_break_5", + "organizational_unit", + "default_role", + "ldap_search_string", + "ldap_email_field", + "ldap_username_field", + "column_break_11", + "ldap_first_name_field", + "ldap_middle_name_field", + "ldap_last_name_field", + "ldap_phone_field", + "ldap_mobile_field", + "ldap_security", + "ssl_tls_mode", + "require_trusted_certificate", + "column_break_17", + "local_private_key_file", + "local_server_certificate_file", + "local_ca_certs_file", + "ldap_group_mappings_section", + "ldap_group_field", + "ldap_groups" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "enabled", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Enabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Enabled" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "ldap_server_url", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "LDAP Server Url", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "organizational_unit", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Organizational Unit", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "column_break_4", + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "base_dn", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Base Distinguished Name (DN)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "password", "fieldtype": "Password", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Password for Base DN", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "section_break_5", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "LDAP User Creation and Mapping" + }, + { + "fieldname": "organizational_unit", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Organizational Unit for Users", + "reqd": 1 + }, + { + "fieldname": "default_role", + "fieldtype": "Link", + "label": "Default Role on Creation", + "options": "Role", + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "ldap_search_string", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "LDAP Search String", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "ldap_first_name_field", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "LDAP First Name Field", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "ldap_email_field", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "LDAP Email Field", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "ldap_username_field", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "LDAP Username Field", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "ldap_first_name_field", + "fieldtype": "Data", + "label": "LDAP First Name Field", + "reqd": 1 + }, + { + "fieldname": "ldap_middle_name_field", + "fieldtype": "Data", + "label": "LDAP Middle Name Field" + }, + { + "fieldname": "ldap_last_name_field", + "fieldtype": "Data", + "label": "LDAP Last Name Field" + }, + { + "fieldname": "ldap_phone_field", + "fieldtype": "Data", + "label": "LDAP Phone Field" + }, + { + "fieldname": "ldap_mobile_field", + "fieldtype": "Data", + "label": "LDAP Mobile Field" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "ldap_security", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "LDAP Security", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "LDAP Security" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Off", - "description": "", - "fetch_if_empty": 0, "fieldname": "ssl_tls_mode", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "SSL/TLS Mode", - "length": 0, - "no_copy": 0, - "options": "Off\nStartTLS", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Off\nStartTLS" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "No", - "fetch_if_empty": 0, "fieldname": "require_trusted_certificate", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Require Trusted Certificate", - "length": 0, - "no_copy": 0, "options": "No\nYes", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "local_private_key_file", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Path to private Key File", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Path to private Key File" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "local_server_certificate_file", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Path to Server Certificate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Path to Server Certificate" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "local_ca_certs_file", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Path to CA Certs File", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Path to CA Certs File" + }, + { + "fieldname": "ldap_group_mappings_section", + "fieldtype": "Section Break", + "label": "LDAP Group Mappings" + }, + { + "fieldname": "ldap_group_field", + "fieldtype": "Data", + "label": "LDAP Group Field" + }, + { + "fieldname": "ldap_groups", + "fieldtype": "Table", + "label": "LDAP Group Mappings", + "options": "LDAP Group Mapping" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, "in_create": 1, - "is_submittable": 0, "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-29 10:56:42.322696", + "modified": "2019-07-15 06:48:16.562109", "modified_by": "Administrator", "module": "Integrations", "name": "LDAP Settings", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, - "report": 0, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, "read_only": 1, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index 5fa54e9628..8cc2489b72 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -15,155 +15,195 @@ class LDAPSettings(Document): if not self.flags.ignore_mandatory: if self.ldap_search_string and self.ldap_search_string.endswith("={0}"): - connect_to_ldap(server_url=self.ldap_server_url, - base_dn=self.base_dn, - password=self.get_password(raise_exception=False), - ssl_tls_mode=self.ssl_tls_mode, - trusted_cert=self.require_trusted_certificate, - private_key_file=self.local_private_key_file, - server_cert_file=self.local_server_certificate_file, - ca_certs_file=self.local_ca_certs_file - ) + self.connect_to_ldap(base_dn=self.base_dn, password=self.get_password(raise_exception=False)) else: frappe.throw(_("LDAP Search String needs to end with a placeholder, eg sAMAccountName={0}")) + def connect_to_ldap(self, base_dn, password): + try: + import ldap3 + import ssl -def get_ldap_client_settings(): - #return the settings to be used on the client side. - result = { - "enabled": False - } - settings = frappe.get_doc("LDAP Settings") + if self.require_trusted_certificate == 'Yes': + tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLSv1) + else: + tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1) - if settings and settings.enabled: - result["enabled"] = True - result["method"] = "frappe.integrations.doctype.ldap_settings.ldap_settings.login" - return result + if self.local_private_key_file: + tls_configuration.private_key_file = self.local_private_key_file + if self.local_server_certificate_file: + tls_configuration.certificate_file = self.local_server_certificate_file + if self.local_ca_certs_file: + tls_configuration.ca_certs_file = self.local_ca_certs_file + server = ldap3.Server(host=self.ldap_server_url, tls=tls_configuration) + bind_type = ldap3.AUTO_BIND_TLS_BEFORE_BIND if self.ssl_tls_mode == "StartTLS" else True -def connect_to_ldap(server_url, - base_dn, - password, - ssl_tls_mode, - trusted_cert, - private_key_file, - server_cert_file, - ca_certs_file): - try: - import ldap3 - import ssl + conn = ldap3.Connection( + server=server, + user=base_dn, + password=password, + auto_bind=bind_type, + read_only=True, + raise_exceptions=True) - if trusted_cert == 'Yes': - tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED, - version=ssl.PROTOCOL_TLSv1) + return conn + + except ImportError: + msg = _("Please Install the ldap3 library via pip to use ldap functionality.") + frappe.throw(msg, title=_("LDAP Not Installed")) + except ldap3.core.exceptions.LDAPInvalidCredentialsResult: + frappe.throw(_("Invalid username or password")) + except Exception as ex: + frappe.throw(_(str(ex))) + + @staticmethod + def get_ldap_client_settings(): + # return the settings to be used on the client side. + result = { + "enabled": False + } + ldap = frappe.get_doc("LDAP Settings") + if ldap.enabled: + result["enabled"] = True + result["method"] = "frappe.integrations.doctype.ldap_settings.ldap_settings.login" + return result + + @classmethod + def update_user_fields(cls, user, user_data): + + updatable_data = {key: value for key, value in user_data.items() if key != 'email'} + + for key, value in updatable_data.items(): + setattr(user, key, value) + user.save(ignore_permissions=True) + + def sync_roles(self, user, additional_groups=None): + + current_roles = set([d.role for d in user.get("roles")]) + + needed_roles = set() + needed_roles.add(self.default_role) + + lower_groups = [g.lower() for g in additional_groups or []] + + all_mapped_roles = {r.erpnext_role for r in self.ldap_groups} + matched_roles = {r.erpnext_role for r in self.ldap_groups if r.ldap_group.lower() in lower_groups} + unmatched_roles = all_mapped_roles.difference(matched_roles) + needed_roles.update(matched_roles) + roles_to_remove = current_roles.intersection(unmatched_roles) + + if not needed_roles.issubset(current_roles): + missing_roles = needed_roles.difference(current_roles) + user.add_roles(*missing_roles) + + user.remove_roles(*roles_to_remove) + + def create_or_update_user(self, user_data, groups=None): + user = None + if frappe.db.exists("User", user_data['email']): + user = frappe.get_doc("User", user_data['email']) + LDAPSettings.update_user_fields(user=user, user_data=user_data) else: - tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE, - version=ssl.PROTOCOL_TLSv1) + doc = user_data + doc.update({ + "doctype": "User", + "send_welcome_email": 0, + "language": "", + "user_type": "System User", + # "roles": [{ + # "role": self.default_role + # }] + }) + user = frappe.get_doc(doc) + user.insert(ignore_permissions=True) + # always add default role. + user.add_roles(self.default_role) + if self.ldap_group_field: + self.sync_roles(user, groups) + return user - if private_key_file: - tls_configuration.private_key_file = private_key_file - if server_cert_file: - tls_configuration.certificate_file = server_cert_file - if ca_certs_file: - tls_configuration.ca_certs_file = ca_certs_file + def get_ldap_attributes(self): + ldap_attributes = [self.ldap_email_field, self.ldap_username_field, self.ldap_first_name_field] - server = ldap3.Server(host=server_url, - tls=tls_configuration) - bind_type = ldap3.AUTO_BIND_TLS_BEFORE_BIND if ssl_tls_mode == "StartTLS" else True + if self.ldap_group_field: + ldap_attributes.append(self.ldap_group_field) - conn = ldap3.Connection(server=server, - user=base_dn, - password=password, - auto_bind=bind_type, - read_only=True, - raise_exceptions=True) + if self.ldap_middle_name_field: + ldap_attributes.append(self.ldap_middle_name_field) - return conn + if self.ldap_last_name_field: + ldap_attributes.append(self.ldap_last_name_field) - except ImportError: - msg = _("Please Install the ldap3 library via pip to use ldap functionality.") - frappe.throw(msg, title=_("LDAP Not Installed")) - except ldap3.core.exceptions.LDAPInvalidCredentialsResult: - frappe.throw(_("Invalid Credentials")) - except Exception as ex: - frappe.throw(_(str(ex))) + if self.ldap_phone_field: + ldap_attributes.append(self.ldap_phone_field) + + if self.ldap_mobile_field: + ldap_attributes.append(self.ldap_mobile_field) + + return ldap_attributes + + def authenticate(self, username, password): + + if not self.enabled: + frappe.throw(_("LDAP is not enabled.")) + + user_filter = self.ldap_search_string.format(username) + ldap_attributes = self.get_ldap_attributes() + + conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False)) + + conn.search( + search_base=self.organizational_unit, + search_filter="({0})".format(user_filter), + attributes=ldap_attributes) + + if len(conn.entries) == 1 and conn.entries[0]: + user = conn.entries[0] + # only try and connect as the user, once we have their fqdn entry. + self.connect_to_ldap(base_dn=user.entry_dn, password=password) + + groups = None + if self.ldap_group_field: + groups = getattr(user, self.ldap_group_field).values + return self.create_or_update_user(self.convert_ldap_entry_to_dict(user), groups=groups) + else: + frappe.throw(_("Invalid username or password")) + + def convert_ldap_entry_to_dict(self, user_entry): + data = { + 'username': user_entry[self.ldap_username_field].value, + 'email': user_entry[self.ldap_email_field].value, + 'first_name': user_entry[self.ldap_first_name_field].value + } + + # optional fields + + if self.ldap_middle_name_field: + data['middle_name'] = user_entry[self.ldap_middle_name_field].value + + if self.ldap_last_name_field: + data['last_name'] = user_entry[self.ldap_last_name_field].value + + if self.ldap_phone_field: + data['phone'] = user_entry[self.ldap_phone_field].value + + if self.ldap_mobile_field: + data['mobile_no'] = user_entry[self.ldap_mobile_field].value + + return data @frappe.whitelist(allow_guest=True) def login(): # LDAP LOGIN LOGIC args = frappe.form_dict - user = authenticate_ldap_user(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd)) + ldap = frappe.get_doc("LDAP Settings") + + user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd)) frappe.local.login_manager.user = user.name frappe.local.login_manager.post_login() # because of a GET request! frappe.db.commit() - - -def authenticate_ldap_user(user=None, - password=None): - - params = {} - settings = frappe.get_doc("LDAP Settings") - if settings and settings.enabled: - conn = connect_to_ldap(server_url=settings.ldap_server_url, - base_dn=settings.base_dn, - password=settings.get_password(raise_exception=False), - ssl_tls_mode=settings.ssl_tls_mode, - trusted_cert=settings.require_trusted_certificate, - private_key_file=settings.local_private_key_file, - server_cert_file=settings.local_server_certificate_file, - ca_certs_file=settings.local_ca_certs_file) - - user_filter = settings.ldap_search_string.format(user) - conn.search(search_base=settings.organizational_unit, - search_filter="({0})".format(user_filter), - attributes=[settings.ldap_email_field, - settings.ldap_username_field, - settings.ldap_first_name_field]) - - if len(conn.entries) > 0 and conn.entries[0]: - user = conn.entries[0] - params["email"] = str(user[settings.ldap_email_field]) - params["username"] = str(user[settings.ldap_username_field]) - params["first_name"] = str(user[settings.ldap_first_name_field]) - connect_to_ldap(server_url=settings.ldap_server_url, - base_dn=user.entry_dn, - password=frappe.as_unicode(password), - ssl_tls_mode=settings.ssl_tls_mode, - trusted_cert=settings.require_trusted_certificate, - private_key_file=settings.local_private_key_file, - server_cert_file=settings.local_server_certificate_file, - ca_certs_file=settings.local_ca_certs_file - ) - return create_user(params) - else: - frappe.throw(_("Not a valid LDAP user")) - else: - frappe.throw(_("LDAP is not enabled.")) - - -def create_user(params): - if frappe.db.exists("User", params["email"]): - user = frappe.get_doc("User", params["email"]) - user.first_name = params["first_name"] - user.username = params["username"] - user.save(ignore_permissions=True) - return user - - else: - params.update({ - "doctype": "User", - "send_welcome_email": 0, - "language": "", - "user_type": "System User", - "roles": [{ - "role": _("Customer") - }] - }) - - user = frappe.get_doc(params).insert(ignore_permissions=True) - - return user diff --git a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py new file mode 100644 index 0000000000..e6cf4eef3a --- /dev/null +++ b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLDAPSettings(unittest.TestCase): + pass diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py index a021cddbe3..d00883f5a3 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -66,15 +66,26 @@ def take_backups_if(freq): @frappe.whitelist() -def take_backups_s3(): +def take_backups_s3(retry_count=0): try: backup_to_s3() send_email(True, "S3 Backup Settings") + except JobTimeoutException: + if retry_count < 2: + args = { + "retry_count" :retry_count + 1 + } + enqueue("frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3", + queue='long', timeout=1500, **args) + else: + notify() except Exception: - error_message = frappe.get_traceback() - frappe.errprint(error_message) - send_email(False, "S3 Backup Settings", error_message) + notify() +def notify(): + error_message = frappe.get_traceback() + frappe.errprint(error_message) + send_email(False, "S3 Backup Settings", error_message) def send_email(success, service_name, error_status=None): if success: @@ -134,6 +145,7 @@ def upload_file_to_s3(filename, folder, conn, bucket): conn.upload_file(filename, bucket, destpath) except Exception as e: + frappe.log_error() print("Error uploading: %s" % (e)) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 47380c6550..570835bc32 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -175,10 +175,11 @@ class BaseDocument(object): if not self.doctype: return value if not isinstance(value, BaseDocument): - if "doctype" not in value: + if "doctype" not in value or value['doctype'] is None: value["doctype"] = self.get_table_field_doctype(key) if not value["doctype"]: raise AttributeError(key) + value = get_controller(value["doctype"])(value) value.init_valid_columns() diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 115d175379..96814984f8 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -477,7 +477,7 @@ def get_field_currency(df, doc=None): if ":" in cstr(df.get("options")): split_opts = df.get("options").split(":") - if len(split_opts)==3: + if len(split_opts)==3 and doc.get(split_opts[1]): currency = frappe.get_cached_value(split_opts[0], doc.get(split_opts[1]), split_opts[2]) else: currency = doc.get(df.get("options")) diff --git a/frappe/model/utils/__init__.py b/frappe/model/utils/__init__.py index e5d329cd79..efbe46a4ab 100644 --- a/frappe/model/utils/__init__.py +++ b/frappe/model/utils/__init__.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals, print_function from six.moves import range import frappe +from frappe import _ from frappe.utils import cstr from frappe.build import html_to_js_template import re @@ -62,7 +63,7 @@ def render_include(content): if "{% include" in content: paths = re.findall(r'''{% include\s['"](.*)['"]\s%}''', content) if not paths: - frappe.throw('Invalid include path', InvalidIncludePath) + frappe.throw(_('Invalid include path'), InvalidIncludePath) for path in paths: app, app_path = path.split('/', 1) diff --git a/frappe/patches.txt b/frappe/patches.txt index c9998a9420..03337c2c6f 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -236,7 +236,7 @@ frappe.patches.v12_0.set_primary_key_in_series execute:frappe.delete_doc("Page", "modules", ignore_missing=True) frappe.patches.v11_0.set_default_letter_head_source frappe.patches.v12_0.setup_comments_from_communications -frappe.patches.v12_0.init_desk_settings #11-03-2019 +frappe.patches.v12_0.init_desk_settings #16-05-2019 frappe.patches.v12_0.replace_null_values_in_tables frappe.patches.v12_0.reset_home_settings frappe.patches.v12_0.update_print_format_type @@ -245,3 +245,4 @@ frappe.patches.v11_0.apply_customization_to_custom_doctype frappe.patches.v12_0.remove_feedback_rating frappe.patches.v12_0.move_form_attachments_to_attachments_folder frappe.patches.v12_0.move_timeline_links_to_dynamic_links +frappe.patches.v12_0.delete_feedback_request_if_exists #1 diff --git a/frappe/patches/v12_0/delete_feedback_request_if_exists.py b/frappe/patches/v12_0/delete_feedback_request_if_exists.py new file mode 100644 index 0000000000..fdbcecfc5a --- /dev/null +++ b/frappe/patches/v12_0/delete_feedback_request_if_exists.py @@ -0,0 +1,8 @@ + +import frappe + +def execute(): + frappe.db.sql(''' + DELETE from `tabDocType` + WHERE name = 'Feedback Request' + ''') \ No newline at end of file diff --git a/frappe/patches/v12_0/init_desk_settings.py b/frappe/patches/v12_0/init_desk_settings.py index 31c6cf9207..ecd9c94d5b 100644 --- a/frappe/patches/v12_0/init_desk_settings.py +++ b/frappe/patches/v12_0/init_desk_settings.py @@ -8,4 +8,4 @@ from frappe.desk.moduleview import get_onboard_items def execute(): """Reset the initial customizations for desk, with modules, indices and links.""" frappe.reload_doc("core", "doctype", "user") - frappe.db.sql("""update `tabUser` set home_settings = %s""", (''), debug=True) + frappe.db.sql("""update tabUser set home_settings = ''""") diff --git a/frappe/patches/v12_0/website_meta_tag_parent.py b/frappe/patches/v12_0/website_meta_tag_parent.py new file mode 100644 index 0000000000..7cc84b1283 --- /dev/null +++ b/frappe/patches/v12_0/website_meta_tag_parent.py @@ -0,0 +1,9 @@ +import frappe + +def execute(): + # convert all /path to path + frappe.db.sql(''' + UPDATE `tabWebsite Meta Tag` + SET parent = SUBSTR(parent, 2) + WHERE parent like '/%' + ''') diff --git a/frappe/patches/v5_0/style_settings_to_website_theme.py b/frappe/patches/v5_0/style_settings_to_website_theme.py index b2f21e780e..40414d4e20 100644 --- a/frappe/patches/v5_0/style_settings_to_website_theme.py +++ b/frappe/patches/v5_0/style_settings_to_website_theme.py @@ -25,7 +25,7 @@ def migrate_style_settings(): website_theme.no_sidebar = cint(frappe.db.get_single_value("Website Settings", "no_sidebar")) website_theme.save() - website_theme.use_theme() + website_theme.set_as_default() def map_color_fields(style_settings, website_theme): color_fields_map = { diff --git a/frappe/printing/doctype/print_settings/print_settings.py b/frappe/printing/doctype/print_settings/print_settings.py index 2758f0aa9c..9bb87ab311 100644 --- a/frappe/printing/doctype/print_settings/print_settings.py +++ b/frappe/printing/doctype/print_settings/print_settings.py @@ -19,7 +19,7 @@ class PrintSettings(Document): try: import cups except ImportError: - frappe.throw("You need to install pycups to use this feature!") + frappe.throw(_("You need to install pycups to use this feature!")) return try: cups.setServer(self.server_ip) diff --git a/frappe/public/build.json b/frappe/public/build.json index dc553bf9ea..cb41ae7eef 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -36,7 +36,7 @@ "public/js/frappe/ui/messages.js", "public/js/frappe/translate.js", "public/js/frappe/utils/pretty_date.js", - "public/js/lib/microtemplate.js", + "public/js/frappe/microtemplate.js", "public/js/frappe/query_string.js", "public/js/frappe/ui/dropzone.js", @@ -145,9 +145,7 @@ "public/js/frappe/router_history.js", "public/js/frappe/defaults.js", "public/js/frappe/roles_editor.js", - "public/js/lib/microtemplate.js", - - "public/js/legacy/handler.js", + "public/js/frappe/microtemplate.js", "public/js/frappe/ui/page.html", "public/js/frappe/ui/page.js", @@ -276,6 +274,7 @@ "public/js/frappe/list/list_sidebar.js", "public/js/frappe/list/list_sidebar.html", "public/js/frappe/list/list_sidebar_stat.html", + "public/js/frappe/list/list_sidebar_group_by.js", "public/js/frappe/list/list_view_permission_restrictions.html", "public/js/frappe/views/gantt/gantt_view.js", @@ -312,7 +311,7 @@ "public/js/frappe/ui/group_by/group_by.js" ], "js/web_form.min.js": [ - "public/js/frappe/misc/datetime.js", + "public/js/frappe/utils/datetime.js", "public/js/frappe/web_form/webform_script.js", "public/js/lib/datepicker/datepicker.min.js", "public/js/lib/datepicker/datepicker.en.js" diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 4dc3b4b0f8..3f4e391461 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -464,6 +464,11 @@ frappe.Application = Class.extend({ return frappe.call('frappe.client.get_hooks', { hook: 'app_logo_url' }) .then(r => { frappe.app.logo_url = (r.message || []).slice(-1)[0]; + if (window.cordova) { + let host = frappe.request.url; + host = host.slice(0, host.length - 1); + frappe.app.logo_url = host + frappe.app.logo_url; + } }); }, diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 27fe04e9dc..68136eba44 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -125,7 +125,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ if(!d.label) { d.label = d.value; } var _label = (me.translate_values) ? __(d.label) : d.label; - var html = "" + _label + ""; + var html = d.html || "" + _label + ""; if(d.description && d.value!==d.description) { html += '
' + __(d.description) + ''; } @@ -174,32 +174,12 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ // show filter description in awesomplete if (args.filters) { - let filter_string = []; - - if (Array.isArray(args.filters)) { - let filters = args.filters; - - filters.forEach((filter) => { - filter_string.push(`${frappe.model.unscrub(filter[1])} ${filter[2]} ${filter[3]}`); - }); - } else { - for (let [key, value] of Object.entries(args.filters)) { - if (Array.isArray(value) && value[1]) { - filter_string.push(`${frappe.model.unscrub(key)} ${value[0]} ${value[1]}`); - } else if (value) { - filter_string.push(`${frappe.model.unscrub(key)} as ${value}`); - } - } - } - - if (filter_string.length > 0) { - filter_string = "Filters applied for " + filter_string.join(", "); - + let filter_string = me.get_filter_description(args.filters); + if (filter_string) { r.results.push({ - label: "" - + __("{0}", [filter_string]) - + "", - value: "" + html: `${filter_string}`, + value: '', + action: () => {} }); } } @@ -234,7 +214,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ me.awesomplete.list = me.$input.cache[doctype][term]; } }); - }, 618)); + }, 500)); this.$input.on("blur", function() { if(me.selected) { @@ -250,8 +230,6 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ this.$input.on("awesomplete-open", function() { me.$wrapper.css({"z-index": 100}); me.$wrapper.find('ul').css({"z-index": 100}); - me.$wrapper.find('.disable-select').parents('li').css({"pointer-events": "none"}); - me.$wrapper.find('.disable-select').unwrap(); me.autocomplete_open = true; }); @@ -297,6 +275,57 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ } }); }, + + get_filter_description(filters) { + let doctype = this.get_options(); + let filter_array = []; + let meta = null; + + frappe.model.with_doctype(doctype, () => { + meta = frappe.get_meta(doctype); + }); + + // convert object style to array + if (!Array.isArray(filters)) { + for (let fieldname in filters) { + let value = filters[fieldname]; + if (!Array.isArray(value)) { + value = ['=', value]; + } + filter_array.push([fieldname, ...value]); // fieldname, operator, value + } + } else { + filter_array = filters; + } + + // add doctype if missing + filter_array = filter_array.map(filter => { + if (filter.length === 3) { + return [doctype, ...filter]; // doctype, fieldname, operator, value + } + return filter; + }); + + function get_filter_description(filter) { + let doctype = filter[0]; + let fieldname = filter[1]; + let docfield = frappe.meta.get_docfield(doctype, fieldname); + let label = docfield ? docfield.label : frappe.model.unscrub(fieldname); + + let value = filter[3] == null || filter[3] === '' + ? __('empty') + : String(filter[3]); + + return [__(label).bold(), filter[2], value.bold()].join(' '); + } + + let filter_string = filter_array + .map(get_filter_description) + .join(', '); + + return __('Filters applied for {0}', [filter_string]); + }, + set_custom_query: function(args) { var set_nulls = function(obj) { $.each(obj, function(key, value) { diff --git a/frappe/public/js/frappe/form/controls/multiselect_list.js b/frappe/public/js/frappe/form/controls/multiselect_list.js index e244c3c12e..7592b80a62 100644 --- a/frappe/public/js/frappe/form/controls/multiselect_list.js +++ b/frappe/public/js/frappe/form/controls/multiselect_list.js @@ -69,6 +69,8 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ this.set_input_attributes(); this.values = []; + this._options = []; + this._selected_values = []; this.highlighted = -1; }, @@ -104,6 +106,20 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ this.update_status(); }, + set_value(value) { + if (!value) return Promise.resolve(); + if (typeof value === 'string') { + value = [value]; + } + this.values = value; + this.values.forEach(value => { + this.update_selected_values(value); + }); + this.parse_validate_and_set_in_model(''); + this.update_status(); + return Promise.resolve(); + }, + update_selected_values(value) { this._selected_values = this._selected_values || []; let option = this._options.find(opt => opt.value === value); @@ -122,7 +138,8 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ text = this.get_placeholder_text(); } else if (this.values.length === 1) { let val = this.values[0]; - text = this._options.find(opt => opt.value === val).label; + let option = this._options.find(opt => opt.value === val); + text = option ? option.label : val; } else { text = __('{0} values selected', [this.values.length]); } diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js index 5fad73364d..85af73823a 100644 --- a/frappe/public/js/frappe/form/controls/table.js +++ b/frappe/public/js/frappe/form/controls/table.js @@ -37,7 +37,7 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({ if (data.length === 1 & data[0].length === 1) return; if (data.length > 100){ data = data.slice(0, 100); - frappe.msgprint('for performance, only the first 100 rows processed!'); + frappe.msgprint(__('For performance, only the first 100 rows were processed.')); } var fieldnames = []; var get_field = function(name_or_label){ diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 33c8fcf3dc..1e4e063204 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -416,7 +416,7 @@ frappe.ui.form.Dashboard = Class.extend({ update_heatmap: function(data) { if(this.heatmap) { - this.heatmap.update(data); + this.heatmap.update({dataPoints: data}); } }, diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index b7eec630ce..2a6813d9da 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -74,8 +74,8 @@ frappe.ui.form.Timeline = class Timeline { }); }); - this.email_link.on("click", ".copy-to-clipboard", function() { - let text = $(".copy-to-clipboard").text(); + this.email_link.on("click", function(e) { + let text = $(e.currentTarget).find(".copy-to-clipboard").text(); frappe.utils.copy_to_clipboard(text); }); } diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 28f4fb8a3a..24b9aed646 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -87,6 +87,9 @@ frappe.ui.form.Form = class FrappeForm { page: this.page }); + // navigate records keyboard shortcuts + this.add_nav_keyboard_shortcuts(); + // print layout this.setup_print_layout(); @@ -112,6 +115,24 @@ frappe.ui.form.Form = class FrappeForm { this.setup_done = true; } + add_nav_keyboard_shortcuts() { + frappe.ui.keys.add_shortcut({ + shortcut: 'shift+>', + action: () => this.navigate_records(0), + page: this.page, + description: __('Go to next record'), + condition: () => !this.is_new() + }); + + frappe.ui.keys.add_shortcut({ + shortcut: 'shift+<', + action: () => this.navigate_records(1), + page: this.page, + description: __('Go to previous record'), + condition: () => !this.is_new() + }); + } + setup_print_layout() { this.print_preview = new frappe.ui.form.PrintPreview({ frm: this @@ -521,7 +542,9 @@ frappe.ui.form.Form = class FrappeForm { me.script_manager.trigger("after_save"); // submit comment if entered - me.timeline.comment_area.submit(); + if (me.timeline) { + me.timeline.comment_area.submit(); + } me.refresh(); } else { if(on_error) { @@ -797,6 +820,24 @@ frappe.ui.form.Form = class FrappeForm { this.print_preview.toggle(); } + navigate_records(prev) { + let list_settings = frappe.get_user_settings(this.doctype)['List']; + let args = { + doctype: this.doctype, + value: this.docname, + filters: list_settings.filters, + sort_order: list_settings.sort_order, + sort_field: list_settings.sort_by, + prev, + }; + + frappe.call('frappe.desk.form.utils.get_next', args).then(r => { + if (r.message) { + frappe.set_route('Form', this.doctype, r.message); + } + }); + } + rename_doc() { frappe.model.rename_doc(this.doctype, this.docname); } diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 72e6f1ae98..2dfca08b0d 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -253,6 +253,7 @@ export default class Grid { // toolbar this.setup_toolbar(); + this.toggle_checkboxes(this.display_status !== 'Read'); // sortable if(this.frm && this.is_sortable() && !this.sortable_setup_done) { @@ -445,6 +446,9 @@ export default class Grid { this.get_docfield(fieldname).hidden = show ? 0 : 1; this.refresh(); } + toggle_checkboxes(enable) { + this.wrapper.find(".grid-row-check").prop('disabled', !enable) + } get_docfield(fieldname) { return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null); } @@ -780,4 +784,4 @@ export default class Grid { // hide all custom buttons this.grid_buttons.find('.btn-custom').addClass('hidden'); } -} \ No newline at end of file +} diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 92f34aa623..0f3f01ac61 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -606,7 +606,7 @@ export default class GridRow { } } - get_visible_columns(blacklist) { + get_visible_columns(blacklist=[]) { var me = this; var visible_columns = $.map(this.docfields, function(df) { var visible = !df.hidden && df.in_list_view && me.grid.frm.get_perm(df.permlevel, "read") diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index 89298607c1..d2caa24e43 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -35,6 +35,20 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ 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"}); @@ -142,6 +156,7 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ clearTimeout($this.data('timeout')); $this.data('timeout', setTimeout(function() { frappe.flags.auto_scroll = false; + me.empty_list(); me.get_results(); }, 300)); }); @@ -198,16 +213,15 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ render_result_list: function(results, more = 0) { var me = this; - var more_btn = me.dialog.fields_dict.more_btn.$wrapper; // Make empty result set if filter is set if (!frappe.flags.auto_scroll) { - this.$results.empty(); + this.empty_list(); } if(results.length === 0) { - this.$results.empty(); + this.empty_list(); more_btn.hide(); return; } else if(more) { @@ -223,6 +237,10 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ } }, + empty_list: function() { + this.$results.find('.list-item-container').remove(); + }, + get_results: function() { let me = this; @@ -286,7 +304,7 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ }); // Preselect oldest entry - if (me.start < 1) { + if (me.start < 1 && r.values.length === 1) { results[0].checked = 1; } } diff --git a/frappe/public/js/frappe/form/print.js b/frappe/public/js/frappe/form/print.js index 60a21e3cff..ec78b30c8a 100644 --- a/frappe/public/js/frappe/form/print.js +++ b/frappe/public/js/frappe/form/print.js @@ -484,7 +484,7 @@ frappe.ui.get_print_settings = function (pdf, callback, letter_head) { default: "Landscape" }]; - frappe.prompt(columns, function (data) { + return frappe.prompt(columns, function (data) { var data = $.extend(print_settings, data); if (!data.with_letter_head) { data.letter_head = null; diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 53dc40925c..38250f2ad8 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -159,17 +159,29 @@ frappe.ui.form.QuickEntryForm = Class.extend({ doc: me.dialog.doc }, callback: function(r) { - me.dialog.hide(); - // delete the old doc - frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name); - me.dialog.doc = r.message; - if(frappe._from_link) { - frappe.ui.form.update_calling_link(me.dialog.doc); + + if (frappe.model.is_submittable(me.doctype)) { + frappe.run_serially([ + () => me.dialog.working = true, + () => { + me.dialog.set_primary_action(__('Submit'), function() { + me.submit(r.message); + }); + } + ]); } else { - if(me.after_insert) { - me.after_insert(me.dialog.doc); + me.dialog.hide(); + // delete the old doc + frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name); + me.dialog.doc = r.message; + if(frappe._from_link) { + frappe.ui.form.update_calling_link(me.dialog.doc); } else { - me.open_form_if_not_list(); + if(me.after_insert) { + me.after_insert(me.dialog.doc); + } else { + me.open_form_if_not_list(); + } } } }, @@ -185,6 +197,26 @@ frappe.ui.form.QuickEntryForm = Class.extend({ }); }, + submit: function(doc) { + var me = this; + frappe.call({ + method: "frappe.client.submit", + args : { + doc: doc + }, + callback: function(r) { + me.dialog.hide(); + // delete the old doc + frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name); + me.dialog.doc = r.message; + if (frappe._from_link) { + frappe.ui.form.update_calling_link(me.dialog.doc); + } + cur_frm.reload_doc(); + } + }); + }, + open_form_if_not_list: function() { let route = frappe.get_route(); let doc = this.dialog.doc; diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index 4b38a2ed20..a78623d566 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -142,13 +142,16 @@ frappe.ui.form.save = function (frm, action, callback, btn) { } }); + if (frm.is_new() && frm.meta.autoname === 'Prompt' && !frm.doc.__newname) { + error_fields = [__('Name'), ...error_fields]; + } + if (error_fields.length) { if (doc.parenttype) { var message = __('Mandatory fields required in table {0}, Row {1}', [__(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label).bold(), doc.idx]); } else { var message = __('Mandatory fields required in {0}', [__(doc.doctype)]); - } message = message + '

"; frappe.msgprint({ diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js index c3739b0daa..fd5b8d3856 100644 --- a/frappe/public/js/frappe/form/script_manager.js +++ b/frappe/public/js/frappe/form/script_manager.js @@ -169,7 +169,7 @@ frappe.ui.form.ScriptManager = Class.extend({ function setup_add_fetch(df) { if((['Data', 'Read Only', 'Text', 'Small Text', 'Currency', - 'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date'].includes(df.fieldtype) || df.read_only==1) + 'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select'].includes(df.fieldtype) || df.read_only==1) && df.fetch_from && df.fetch_from.indexOf(".")!=-1) { var parts = df.fetch_from.split("."); me.frm.add_fetch(parts[0], parts[1], df.fieldname); diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index b766d66a5f..5798d3361e 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -36,6 +36,7 @@ frappe.ui.form.Sidebar = Class.extend({ this.bind_events(); this.setup_keyboard_shortcuts(); + this.show_auto_repeat_status(); frappe.ui.form.setup_user_image_event(this.frm); this.refresh(); @@ -88,6 +89,28 @@ frappe.ui.form.Sidebar = Class.extend({ } }, + show_auto_repeat_status: function() { + if (this.frm.meta.allow_auto_repeat && this.frm.doc.auto_repeat) { + const me = this; + frappe.call({ + method: "frappe.client.get_value", + args:{ + doctype: "Auto Repeat", + filters: { + name: this.frm.doc.auto_repeat + }, + fieldname: ["frequency"] + }, + callback: function(res) { + me.sidebar.find(".auto-repeat-status").html(__("Repeats {0}", [res.message.frequency])); + me.sidebar.find(".auto-repeat-status").on("click", function(){ + frappe.set_route("Form", "Auto Repeat", me.frm.doc.auto_repeat); + }); + } + }); + } + }, + refresh_comments: function() { $.map(this.frm.timeline.get_communications(), function(c) { return (c.communication_type==="Communication" || (c.communication_type=="Comment" && c.comment_type==="Comment")) ? c : null; diff --git a/frappe/public/js/frappe/form/sidebar/user_image.js b/frappe/public/js/frappe/form/sidebar/user_image.js index 8be760179a..6c8099db89 100644 --- a/frappe/public/js/frappe/form/sidebar/user_image.js +++ b/frappe/public/js/frappe/form/sidebar/user_image.js @@ -4,6 +4,7 @@ frappe.ui.form.set_user_image = function(frm) { var image_field = frm.meta.image_field; var image = frm.doc[image_field]; var title_image = frm.page.$title_area.find('.title-image'); + var image_actions = frm.sidebar.image_wrapper.find('.sidebar-image-actions'); image_section.toggleClass('hide', image_field ? false : true); @@ -32,6 +33,8 @@ frappe.ui.form.set_user_image = function(frm) { .css("background-image", 'url("' + image + '")') .html(''); + image_actions.find('.sidebar-image-change, .sidebar-image-remove').show(); + } else { image_section .find(".sidebar-image") @@ -51,6 +54,8 @@ frappe.ui.form.set_user_image = function(frm) { .css({'background-color': frappe.get_palette(title)}) .html(frappe.get_abbr(title)); + image_actions.find('.sidebar-image-change').show(); + image_actions.find('.sidebar-image-remove').hide(); } } @@ -63,12 +68,27 @@ frappe.ui.form.setup_user_image_event = function(frm) { }); } - // bind click on image_wrapper - frm.sidebar.image_wrapper.on('click', function() { - var field = frm.get_field(frm.meta.image_field); - if(!field.$input) { - field.make_input(); + frm.sidebar.image_wrapper.on('click', ':not(.sidebar-image-actions)', (e) => { + let $target = $(e.currentTarget); + if ($target.is('a.dropdown-toggle, .dropdown')) { + return; + } + let dropdown = frm.sidebar.image_wrapper.find('.sidebar-image-actions .dropdown'); + dropdown.toggleClass('open'); + e.stopPropagation(); + }); + + // bind click on image_wrapper + frm.sidebar.image_wrapper.on('click', '.sidebar-image-change, .sidebar-image-remove', function(e) { + let $target = $(e.currentTarget); + var field = frm.get_field(frm.meta.image_field); + if ($target.is('.sidebar-image-change')) { + if(!field.$input) { + field.make_input(); + } + field.$input.trigger('click'); + } else { + field.set_value('').then(() => frm.save()); } - field.$input.trigger('click'); }); } \ 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 34f9b57ef2..b611557c43 100644 --- a/frappe/public/js/frappe/form/templates/form_sidebar.html +++ b/frappe/public/js/frappe/form/templates/form_sidebar.html @@ -12,6 +12,15 @@ + {% if frm.meta.beta %} @@ -72,6 +81,9 @@
  • {%= __("Currently Viewing") %}
  • + diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 244bf75708..43411bd6de 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -129,7 +129,10 @@ frappe.ui.form.Toolbar = Class.extend({ if(frappe.model.can_email(null, me.frm) && me.frm.doc.docstatus < 2) { this.page.add_menu_item(__("Email"), function() { me.frm.email_doc(); - }, true, 'Ctrl+E'); + }, true, { + shortcut: 'Ctrl+E', + condition: () => !this.frm.is_new() + }); } // go to field modal @@ -168,7 +171,10 @@ frappe.ui.form.Toolbar = Class.extend({ && frappe.model.can_delete(me.frm.doctype)) { this.page.add_menu_item(__("Delete"), function() { me.frm.savetrash(); - }, true, 'Shift+Ctrl+D'); + }, true, { + shortcut: 'Shift+Ctrl+D', + condition: () => !this.frm.is_new() + }); } if(frappe.user_roles.includes("System Manager") && me.frm.meta.issingle === 0) { @@ -178,7 +184,7 @@ frappe.ui.form.Toolbar = Class.extend({ }) }, true); - if (frappe.boot.developer_mode===1 && me.frm.meta.issingle) { + if (frappe.boot.developer_mode===1) { // edit doctype this.page.add_menu_item(__("Edit DocType"), function() { frappe.set_route('Form', 'DocType', me.frm.doctype); @@ -186,12 +192,37 @@ frappe.ui.form.Toolbar = Class.extend({ } } + // Auto Repeat + if(this.can_repeat()) { + this.page.add_menu_item(__("Repeat"), function(){ + frappe.utils.new_auto_repeat_prompt(me.frm); + }, true); + } + // New if(p[CREATE] && !this.frm.meta.issingle) { this.page.add_menu_item(__("New {0}", [__(me.frm.doctype)]), function() { frappe.new_doc(me.frm.doctype, true); - }, true, 'Ctrl+B'); + }, true, { + shortcut: 'Ctrl+B', + condition: () => !this.frm.is_new() + }); } + + // Navigate + if(!this.frm.is_new() && !issingle) { + this.page.add_action_icon("fa fa-chevron-left prev-doc", function() { + me.frm.navigate_records(1); + }); + this.page.add_action_icon("fa fa-chevron-right next-doc", function() { + me.frm.navigate_records(0); + }); + } + }, + can_repeat: function() { + return this.frm.meta.allow_auto_repeat + && !this.frm.is_new() + && !this.frm.doc.auto_repeat; }, can_save: function() { return this.get_docstatus()===0; diff --git a/frappe/public/js/frappe/form/workflow.js b/frappe/public/js/frappe/form/workflow.js index 115ad5e702..464e5bf05c 100644 --- a/frappe/public/js/frappe/form/workflow.js +++ b/frappe/public/js/frappe/form/workflow.js @@ -49,7 +49,6 @@ frappe.ui.form.States = Class.extend({ }, refresh: function() { - const me = this; // hide if its not yet saved if(this.frm.doc.__islocal) { this.set_default_state(); @@ -59,8 +58,6 @@ frappe.ui.form.States = Class.extend({ // state text const state = this.get_state(); - let doctype = this.frm.doctype; - if(state) { // show actions from that state this.show_actions(state); @@ -71,8 +68,6 @@ frappe.ui.form.States = Class.extend({ var added = false; var me = this; - this.frm.page.clear_actions_menu(); - // if the loaded doc is dirty, don't show workflow buttons if (this.frm.doc.__unsaved===1) { return; @@ -90,7 +85,8 @@ frappe.ui.form.States = Class.extend({ } frappe.workflow.get_transitions(this.frm.doc).then(transitions => { - $.each(transitions, function(i, d) { + this.frm.page.clear_actions_menu(); + transitions.forEach(d => { if(frappe.user_roles.includes(d.allowed) && has_approval_access(d)) { added = true; me.frm.page.add_action_item(__(d.action), function() { diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 9141bcc525..42c356c358 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -551,12 +551,12 @@ class FilterArea { const fields_dict = this.list_view.page.fields_dict; if (fieldname in fields_dict) { - fields_dict[fieldname].set_value(''); - return; + return fields_dict[fieldname].set_value(''); } let filter = this.filter_list.get_filter(fieldname); if (filter) filter.remove(); + return Promise.resolve(); } clear(refresh = true) { @@ -598,7 +598,6 @@ class FilterArea { } const doctype_fields = this.list_view.meta.fields; - fields = fields.concat(doctype_fields.filter( df => df.in_standard_filter && frappe.model.is_value_type(df.fieldtype) @@ -617,12 +616,17 @@ class FilterArea { options = options.join("\n"); } } + let default_value = (fieldtype === 'Link') ? frappe.defaults.get_user_default(options) : null; + if (['__default', '__global'].includes(default_value)) { + default_value = null; + } return { fieldtype: fieldtype, label: __(df.label), options: options, fieldname: df.fieldname, condition: condition, + default: default_value, onchange: () => this.refresh_list_view(), ignore_link_validation: fieldtype === 'Dynamic Link' }; diff --git a/frappe/public/js/frappe/list/list_sidebar.html b/frappe/public/js/frappe/list/list_sidebar.html index c8c2239b03..885b5f2dae 100644 --- a/frappe/public/js/frappe/list/list_sidebar.html +++ b/frappe/public/js/frappe/list/list_sidebar.html @@ -52,21 +52,31 @@ - {% if(frappe.help.has_help(doctype)) { %}
  • {{ __("Help") }}
  • {% } %} + + +