diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 0000000000..4754a63e7e --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,2 @@ +exclude_paths: + - '**.sql' \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000000..c11c0ab6a3 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,2 @@ +disable=access-member-before-definition +disable=no-member \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index df66db88a7..6990a0df8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python dist: trusty -sudo: required addons: hosts: @@ -39,6 +38,16 @@ matrix: env: DB=mariadb TYPE=server script: bench --site test_site run-tests --coverage +before_install: + # install wkhtmltopdf + - wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz + - tar -xf /tmp/wkhtmltox.tar.xz -C /tmp + - sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf + - sudo chmod o+x /usr/local/bin/wkhtmltopdf + + # install cups + - sudo apt-get install libcups2-dev + install: - cd ~ - source ./.nvm/nvm.sh @@ -52,23 +61,20 @@ install: - 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'" + - if [ $DB == "mariadb" ];then + 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"; + fi - - 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 + - if [ $DB == "postgres" ];then + psql -c "CREATE DATABASE test_frappe" -U postgres; + psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres; + fi - cd ./frappe-bench diff --git a/cypress/fixtures/custom_submittable_doctype.js b/cypress/fixtures/custom_submittable_doctype.js new file mode 100644 index 0000000000..c88d37b373 --- /dev/null +++ b/cypress/fixtures/custom_submittable_doctype.js @@ -0,0 +1,53 @@ +export default { + name: 'Custom Submittable DocType', + custom: 1, + actions: [], + is_submittable: 1, + creation: '2019-12-10 06:29:07.215072', + doctype: 'DocType', + editable_grid: 1, + engine: 'InnoDB', + fields: [ + { + fieldname: 'enabled', + fieldtype: 'Check', + label: 'Enabled', + allow_on_submit: 1, + reqd: 1 + }, + { + fieldname: 'title', + fieldtype: 'Data', + label: 'title', + reqd: 1 + }, + { + fieldname: 'description', + fieldtype: 'Text Editor', + label: 'Description' + } + ], + links: [], + modified: '2019-12-10 14:40:53.127615', + modified_by: 'Administrator', + module: 'Custom', + owner: 'Administrator', + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: 'System Manager', + share: 1, + write: 1, + submit: 1, + cancel: 1 + } + ], + quick_entry: 1, + sort_field: 'modified', + sort_order: 'ASC', + track_changes: 1 +}; \ No newline at end of file diff --git a/cypress/fixtures/datetime_doctype.js b/cypress/fixtures/datetime_doctype.js new file mode 100644 index 0000000000..b8c89ced5c --- /dev/null +++ b/cypress/fixtures/datetime_doctype.js @@ -0,0 +1,48 @@ +export default { + name: 'DateTime Test', + custom: 1, + actions: [], + creation: '2019-03-15 06:29:07.215072', + doctype: 'DocType', + editable_grid: 1, + engine: 'InnoDB', + fields: [ + { + fieldname: 'date', + fieldtype: 'Date', + label: 'Date' + }, + { + fieldname: 'time', + fieldtype: 'Time', + label: 'Time' + }, + { + fieldname: 'datetime', + fieldtype: 'Datetime', + label: 'Datetime' + } + ], + issingle: 1, + links: [], + modified: '2019-12-09 14:40:53.127615', + modified_by: 'Administrator', + module: 'Custom', + 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: 'ASC', + track_changes: 1 +}; diff --git a/cypress/integration/api.js b/cypress/integration/api.js index 2db09aec1b..11e18d07c9 100644 --- a/cypress/integration/api.js +++ b/cypress/integration/api.js @@ -6,8 +6,8 @@ context('API Resources', () => { }); it('Creates two Comments', () => { - cy.create_doc('Comment', {comment_type: 'Comment', content: "hello"}); - cy.create_doc('Comment', {comment_type: 'Comment', content: "world"}); + cy.insert_doc('Comment', {comment_type: 'Comment', content: "hello"}); + cy.insert_doc('Comment', {comment_type: 'Comment', content: "world"}); }); it('Lists the Comments', () => { @@ -25,11 +25,11 @@ context('API Resources', () => { }); it('Gets each Comment', () => { - cy.get_list('Comment').then(body => body.data.forEach(comment => { + cy.get_list('Comment').then(body => body.data.forEach(comment => { cy.get_doc('Comment', comment.name); })); }); - + it('Removes the Comments', () => { cy.get_list('Comment').then(body => body.data.forEach(comment => { cy.remove_doc('Comment', comment.name); diff --git a/cypress/integration/control_barcode.js b/cypress/integration/control_barcode.js new file mode 100644 index 0000000000..ac2a687bae --- /dev/null +++ b/cypress/integration/control_barcode.js @@ -0,0 +1,55 @@ +context('Control Barcode', () => { + beforeEach(() => { + cy.login(); + cy.visit('/desk'); + }); + + function get_dialog_with_barcode() { + return cy.dialog({ + title: 'Barcode', + fields: [ + { + label: 'Barcode', + fieldname: 'barcode', + fieldtype: 'Barcode' + } + ] + }); + } + + it('should generate barcode on setting a value', () => { + get_dialog_with_barcode().as('dialog'); + + cy.get('.frappe-control[data-fieldname=barcode] input') + .focus() + .type('123456789') + .blur(); + cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]') + .should('exist'); + + cy.get('@dialog').then(dialog => { + let value = dialog.get_value('barcode'); + expect(value).to.contain(' { + get_dialog_with_barcode().as('dialog'); + + cy.get('.frappe-control[data-fieldname=barcode] input') + .focus() + .type('123456789') + .blur(); + cy.get('.frappe-control[data-fieldname=barcode] input') + .clear() + .blur(); + cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]') + .should('not.exist'); + + cy.get('@dialog').then(dialog => { + let value = dialog.get_value('barcode'); + expect(value).to.equal(''); + }); + }); +}); diff --git a/cypress/integration/datetime.js b/cypress/integration/datetime.js new file mode 100644 index 0000000000..90043434cd --- /dev/null +++ b/cypress/integration/datetime.js @@ -0,0 +1,128 @@ +import datetime_doctype from '../fixtures/datetime_doctype'; +const doctype_name = datetime_doctype.name; + +context('Control Date, Time and DateTime', () => { + before(() => { + cy.login(); + cy.visit('/desk'); + cy.insert_doc('DocType', datetime_doctype, true); + }); + + describe('Date formats', () => { + let date_formats = [ + { + date_format: 'dd-mm-yyyy', + part: 2, + length: 4, + separator: '-' + }, + { + date_format: 'mm/dd/yyyy', + part: 0, + length: 2, + separator: '/' + } + ]; + + date_formats.forEach(d => { + it('test date format ' + d.date_format, () => { + cy.set_value('System Settings', 'System Settings', { + date_format: d.date_format + }); + cy.window() + .its('frappe') + .then(frappe => { + // update sys_defaults value to avoid a reload + frappe.sys_defaults.date_format = d.date_format; + }); + + cy.new_form(doctype_name); + cy.get('.form-control[data-fieldname=date]').focus(); + cy.get('.datepickers-container .datepicker.active') + .should('be.visible'); + cy.get( + '.datepickers-container .datepicker.active .datepicker--cell-day.-current-' + ).click(); + + cy.window() + .its('cur_frm') + .then(cur_frm => { + let formatted_value = cur_frm.get_field('date').input.value; + let parts = formatted_value.split(d.separator); + expect(parts[d.part].length).to.equal(d.length); + }); + }); + }); + }); + + describe('Time formats', () => { + let time_formats = [ + { + time_format: 'HH:mm:ss', + value: ' 11:00:12', + match_value: '11:00:12' + }, + { + time_format: 'HH:mm', + value: ' 11:00:12', + match_value: '11:00' + } + ]; + + time_formats.forEach(d => { + it('test time format ' + d.time_format, () => { + cy.set_value('System Settings', 'System Settings', { + time_format: d.time_format + }); + cy.window() + .its('frappe') + .then(frappe => { + frappe.sys_defaults.time_format = d.time_format; + }); + cy.new_form(doctype_name); + cy.fill_field('time', d.value, 'Time').blur(); + cy.get_field('time').should('have.value', d.match_value); + }); + }); + }); + + describe('DateTime formats', () => { + let datetime_formats = [ + { + date_format: 'dd.mm.yyyy', + time_format: 'HH:mm:ss', + value: ' 02.12.2019 11:00:12', + doc_value: '2019-12-02 11:00:12', + input_value: '02.12.2019 11:00:12' + }, + { + date_format: 'mm-dd-yyyy', + time_format: 'HH:mm', + value: ' 12-02-2019 11:00:00', + doc_value: '2019-12-02 11:00:00', + input_value: '12-02-2019 11:00' + } + ]; + datetime_formats.forEach(d => { + it(`test datetime format ${d.date_format} ${d.time_format}`, () => { + cy.set_value('System Settings', 'System Settings', { + date_format: d.date_format, + time_format: d.time_format + }); + cy.window() + .its('frappe') + .then(frappe => { + frappe.sys_defaults.date_format = d.date_format; + frappe.sys_defaults.time_format = d.time_format; + }); + cy.new_form(doctype_name); + cy.fill_field('datetime', d.value, 'Datetime').blur(); + cy.get_field('datetime').should('have.value', d.input_value); + + cy.window() + .its('cur_frm.doc.datetime') + .should('eq', d.doc_value); + }); + }); + }); +}); diff --git a/cypress/integration/depends_on.js b/cypress/integration/depends_on.js new file mode 100644 index 0000000000..8cb4e42d3e --- /dev/null +++ b/cypress/integration/depends_on.js @@ -0,0 +1,63 @@ +context('Depends On', () => { + beforeEach(() => { + cy.login(); + cy.visit('/desk'); + }); + before(() => { + cy.login(); + cy.visit('/desk'); + cy.window().its('frappe').then(frappe => { + frappe.call('frappe.tests.ui_test_helpers.create_doctype', { + name: 'Test Depends On', + fields: [ + { + "label": "Test Field", + "fieldname": "test_field", + "fieldtype": "Data", + }, + { + "label": "Dependant Field", + "fieldname": "dependant_field", + "fieldtype": "Data", + "mandatory_depends_on": "eval:doc.test_field=='Some Value'", + "read_only_depends_on": "eval:doc.test_field=='Some Other Value'", + }, + { + "label": "Display Dependant Field", + "fieldname": "display_dependant_field", + "fieldtype": "Data", + 'depends_on': "eval:doc.test_field=='Value'" + }, + ] + }); + }); + }); + it('should set the field as mandatory depending on other fields value', () => { + cy.new_form('Test Depends On'); + cy.fill_field('test_field', 'Some Value'); + cy.get('button.primary-action').contains('Save').click(); + cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('be.visible'); + cy.get('body').click(); + cy.fill_field('test_field', 'Random value'); + cy.get('button.primary-action').contains('Save').click(); + cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('not.be.visible'); + }); + it('should set the field as read only depending on other fields value', () => { + cy.new_form('Test Depends On'); + cy.fill_field('dependant_field', 'Some Value'); + cy.fill_field('test_field', 'Some Other Value'); + cy.get('body').click(); + cy.get('.control-input [data-fieldname="dependant_field"]').should('be.disabled'); + cy.fill_field('test_field', 'Random Value'); + cy.get('body').click(); + cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled'); + }); + it('should display the field depending on other fields value', () => { + cy.new_form('Test Depends On'); + cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible'); + cy.get('.control-input [data-fieldname="test_field"]').clear(); + cy.fill_field('test_field', 'Value'); + cy.get('body').click(); + cy.get('.control-input [data-fieldname="display_dependant_field"]').should('be.visible'); + }); +}); diff --git a/cypress/integration/form.js b/cypress/integration/form.js index b7ddd6ecb7..81b52c4421 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -2,8 +2,13 @@ context('Form', () => { before(() => { cy.login(); cy.visit('/desk'); + cy.window().its('frappe').then(frappe => { + frappe.call("frappe.tests.ui_test_helpers.create_contact_records"); + }); + }); + beforeEach(() => { + cy.visit('/desk'); }); - it('create a new form', () => { cy.visit('/desk#Form/ToDo/New ToDo 1'); cy.fill_field('description', 'this is a test todo', 'Text Editor').blur(); @@ -11,6 +16,29 @@ context('Form', () => { cy.get('.primary-action').click(); cy.visit('/desk#List/ToDo'); cy.location('hash').should('eq', '#List/ToDo/List'); + cy.get('h1').should('be.visible').and('contain', 'To Do'); cy.get('.list-row').should('contain', 'this is a test todo'); }); + it('navigates between documents with child table list filters applied', () => { + cy.visit('/desk#List/Contact'); + cy.location('hash').should('eq', '#List/Contact/List'); + cy.get('.tag-filters-area .btn:contains("Add Filter")').click(); + cy.get('.fieldname-select-area').should('exist'); + cy.get('.fieldname-select-area input').type('Number{enter}', { force: true }); + cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true }); + cy.get('.filter-box .btn:contains("Apply")').click({ force: true }); + cy.visit('/desk#Form/Contact/Test Form Contact 3'); + cy.get('.prev-doc').should('be.visible').click(); + cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible'); + cy.get('.btn-modal-close:visible').click(); + cy.get('.next-doc').click(); + cy.wait(200); + cy.contains('Test Form Contact 2').should('not.exist'); + cy.get('.page-title .title-text').should('contain', 'Test Form Contact 1'); + // clear filters + cy.window().its('frappe').then((frappe) => { + let list_view = frappe.get_list_view('Contact'); + list_view.filter_area.filter_list.clear_filters(); + }); + }); }); diff --git a/cypress/integration/grid_pagination.js b/cypress/integration/grid_pagination.js new file mode 100644 index 0000000000..67fdb8acf0 --- /dev/null +++ b/cypress/integration/grid_pagination.js @@ -0,0 +1,51 @@ +context('Grid Pagination', () => { + beforeEach(() => { + cy.login(); + cy.visit('/desk'); + }); + before(() => { + cy.login(); + cy.visit('/desk'); + cy.window().its('frappe').then(frappe => { + frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records"); + }); + }); + it('creates pages for child table', () => { + cy.visit('/desk#Form/Contact/Test Contact'); + cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); + cy.get('@table').find('.current-page-number').should('contain', '1'); + cy.get('@table').find('.total-page-number').should('contain', '20'); + cy.get('@table').find('.grid-body .grid-row').should('have.length', 50); + }); + it('goes to the next and previous page', () => { + cy.visit('/desk#Form/Contact/Test Contact'); + cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); + cy.get('@table').find('.next-page').click(); + cy.get('@table').find('.current-page-number').should('contain', '2'); + cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '51'); + cy.get('@table').find('.prev-page').click(); + cy.get('@table').find('.current-page-number').should('contain', '1'); + cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '1'); + }); + it('adds and deletes rows and changes page', ()=> { + cy.visit('/desk#Form/Contact/Test Contact'); + cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); + cy.get('@table').find('button.grid-add-row').click(); + cy.get('@table').find('.grid-body .row-index').should('contain', 1001); + cy.get('@table').find('.current-page-number').should('contain', '21'); + cy.get('@table').find('.total-page-number').should('contain', '21'); + cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({force: true}); + cy.get('@table').find('button.grid-remove-rows').click(); + cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000); + cy.get('@table').find('.current-page-number').should('contain', '20'); + cy.get('@table').find('.total-page-number').should('contain', '20'); + }); + it('deletes all rows', ()=> { + cy.visit('/desk#Form/Contact/Test Contact'); + cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); + cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true}); + cy.get('@table').find('button.grid-remove-all-rows').click(); + cy.get('.modal-dialog .btn-primary').contains('Yes').click(); + cy.get('@table').find('.grid-body .grid-row').should('have.length', 0); + }); +}); \ No newline at end of file diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js index 20a973c1dd..d6627ea9c4 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -2,10 +2,9 @@ context('List View', () => { before(() => { cy.login(); cy.visit('/desk'); - cy.window().its('frappe').then(frappe => { - frappe.call("frappe.tests.ui_test_helpers.setup_workflow"); + return cy.window().its('frappe').then(frappe => { + return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow"); }); - cy.clear_cache(); }); it('enables "Actions" button', () => { const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Print', 'Delete']; diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js new file mode 100644 index 0000000000..c7aeaa92de --- /dev/null +++ b/cypress/integration/report_view.js @@ -0,0 +1,40 @@ +import custom_submittable_doctype from '../fixtures/custom_submittable_doctype'; +const doctype_name = custom_submittable_doctype.name; + +context('Report View', () => { + before(() => { + cy.login(); + cy.visit('/desk'); + cy.insert_doc('DocType', custom_submittable_doctype, true); + cy.clear_cache(); + cy.insert_doc(doctype_name, { + 'title': 'Doc 1', + 'description': 'Random Text', + 'enabled': 0, + // submit document + 'docstatus': 1 + }, true).as('doc'); + }); + it('Field with enabled allow_on_submit should be editable.', () => { + cy.server(); + cy.route('POST', 'api/method/frappe.client.set_value').as('value-update'); + cy.visit(`/desk#List/${doctype_name}/Report`); + let cell = cy.get('.dt-row-0 > .dt-cell--col-3'); + // select the cell + cell.dblclick(); + cell.find('input[data-fieldname="enabled"]').check({force: true}); + cy.get('.dt-row-0 > .dt-cell--col-4').click(); + cy.wait('@value-update'); + cy.get('@doc').then(doc => { + cy.call('frappe.client.get_value', { + doctype: doc.doctype, + filters: { + name: doc.name, + }, + fieldname: 'enabled' + }).then(r => { + expect(r.message.enabled).to.equals(1); + }); + }); + }); +}); \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 464cbbe1d5..02f0bf236e 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -42,95 +42,156 @@ 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; + 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('get_list', (doctype, fields=[], filters=[]) => { - return cy.window().its('frappe.csrf_token').then(csrf_token => { - return cy.request({ - method: 'GET', - url: `/api/resource/${doctype}?fields=${JSON.stringify(fields)}&filters=${JSON.stringify(filters)}`, - headers: { - 'Accept': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - } - }).then(res => { - expect(res.status).eq(200); - return res.body; +Cypress.Commands.add('get_list', (doctype, fields = [], filters = []) => { + filters = JSON.stringify(filters); + fields = JSON.stringify(fields); + let url = `/api/resource/${doctype}?fields=${fields}&filters=${filters}`; + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'GET', + url, + headers: { + Accept: 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }) + .then(res => { + expect(res.status).eq(200); + return res.body; + }); }); - }); }); Cypress.Commands.add('get_doc', (doctype, name) => { - return cy.window().its('frappe.csrf_token').then(csrf_token => { - return cy.request({ - method: 'GET', - url: `/api/resource/${doctype}/${name}`, - headers: { - 'Accept': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - } - }).then(res => { - expect(res.status).eq(200); - return res.body; + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'GET', + url: `/api/resource/${doctype}/${name}`, + headers: { + Accept: 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }) + .then(res => { + expect(res.status).eq(200); + return res.body; + }); }); - }); }); -Cypress.Commands.add('create_doc', (doctype, args) => { - return cy.window().its('frappe.csrf_token').then(csrf_token => { - return cy.request({ - method: 'POST', - url: `/api/resource/${doctype}`, - 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('insert_doc', (doctype, args, ignore_duplicate) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'POST', + url: `/api/resource/${doctype}`, + body: args, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + failOnStatusCode: !ignore_duplicate + }) + .then(res => { + let status_codes = [200]; + if (ignore_duplicate) { + status_codes.push(409); + } + expect(res.status).to.be.oneOf(status_codes); + return res.body; + }); }); - }); }); Cypress.Commands.add('remove_doc', (doctype, name) => { - return cy.window().its('frappe.csrf_token').then(csrf_token => { - return cy.request({ - method: 'DELETE', - url: `/api/resource/${doctype}/${name}`, - headers: { - 'Accept': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - } - }).then(res => { - expect(res.status).eq(202); - return res.body; + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'DELETE', + url: `/api/resource/${doctype}/${name}`, + headers: { + Accept: 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }) + .then(res => { + expect(res.status).eq(202); + return res.body; + }); }); - }); }); -Cypress.Commands.add('create_records', (doc) => { - return cy.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc }) +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') => { +Cypress.Commands.add('set_value', (doctype, name, obj) => { + return cy.call('frappe.client.set_value', { + doctype, + name, + fieldname: obj + }); +}); + +Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { + cy.get_field(fieldname, fieldtype).as('input'); + + if (['Date', 'Time', 'Datetime'].includes(fieldtype)) { + cy.get('@input').click().wait(200); + cy.get('.datepickers-container .datepicker.active').should('exist'); + } + if (fieldtype === 'Time') { + cy.get('@input').clear(); + } + + if (fieldtype === 'Select') { + cy.get('@input').select(value); + } else { + cy.get('@input').type(value, { waitForAnimations: false }); + } + return cy.get('@input'); +}); + +Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => { let selector = `.form-control[data-fieldname="${fieldname}"]`; if (fieldtype === 'Text Editor') { @@ -140,34 +201,33 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => { selector = `[data-fieldname="${fieldname}"] .ace_text-input`; } - cy.get(selector).as('input'); - - if (fieldtype === 'Select') { - return cy.get('@input').select(value); - } else { - return cy.get('@input').type(value, {waitForAnimations: false}); - } + return cy.get(selector); }); -Cypress.Commands.add('awesomebar', (text) => { +Cypress.Commands.add('awesomebar', text => { cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 }); }); -Cypress.Commands.add('new_form', (doctype) => { - cy.visit(`/desk#Form/${doctype}/New ${doctype} 1`); +Cypress.Commands.add('new_form', doctype => { + let route = `Form/${doctype}/New ${doctype} 1`; + cy.visit(`/desk#${route}`); + cy.get('body').should('have.attr', 'data-route', route); + cy.get('body').should('have.attr', 'data-ajax-state', 'complete'); }); -Cypress.Commands.add('go_to_list', (doctype) => { +Cypress.Commands.add('go_to_list', doctype => { cy.visit(`/desk#List/${doctype}/List`); }); Cypress.Commands.add('clear_cache', () => { - cy.window().its('frappe').then(frappe => { - frappe.ui.toolbar.clear_cache(); - }); + cy.window() + .its('frappe') + .then(frappe => { + frappe.ui.toolbar.clear_cache(); + }); }); -Cypress.Commands.add('dialog', (opts) => { +Cypress.Commands.add('dialog', opts => { return cy.window().then(win => { var d = new win.frappe.ui.Dialog(opts); d.show(); @@ -180,6 +240,36 @@ Cypress.Commands.add('get_open_dialog', () => { }); Cypress.Commands.add('hide_dialog', () => { - cy.get_open_dialog().find('.btn-modal-close').click(); + cy.get_open_dialog() + .find('.btn-modal-close') + .click(); cy.get('.modal:visible').should('not.exist'); }); + +Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'POST', + url: `/api/resource/${doctype}`, + body: args, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + failOnStatusCode: !ignore_duplicate + }) + .then(res => { + let status_codes = [200]; + if (ignore_duplicate) { + status_codes.push(409); + } + expect(res.status).to.be.oneOf(status_codes); + return res.body.data; + }); + }); +}); \ No newline at end of file diff --git a/frappe/__init__.py b/frappe/__init__.py index 6424dcd9b5..b383ae958e 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -23,7 +23,7 @@ if sys.version[0] == '2': reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '12.0.17' +__version__ = '12.1.0' __title__ = "Frappe Framework" local = Local() @@ -123,7 +123,6 @@ def init(site, sites_path=None, new_site=False): local.debug_log = [] local.realtime_log = [] local.flags = _dict({ - "ran_schedulers": [], "currently_saving": [], "redirect_location": "", "in_install_db": False, @@ -290,7 +289,7 @@ def log(msg): debug_log.append(as_unicode(msg)) -def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False): +def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None): """Print a message to the user (via HTTP response). Messages are sent in the `__server_messages` property in the response JSON and shown in a pop-up / modal. @@ -299,6 +298,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, :param title: [optional] Message title. :param raise_exception: [optional] Raise given exception and show message. :param as_table: [optional] If `msg` is a list of lists, render as HTML table. + :param primary_action: [optional] Bind a primary server/client side action. """ from frappe.utils import encode @@ -338,6 +338,9 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, if alert: out.alert = 1 + if primary_action: + out.primary_action = primary_action + message_log.append(json.dumps(out)) if raise_exception and hasattr(raise_exception, '__name__'): @@ -1504,7 +1507,22 @@ def logger(module=None, with_more_info=True): def log_error(message=None, title=None): '''Log error to Error Log''' - return get_doc(dict(doctype='Error Log', error=as_unicode(message or get_traceback()), + + # AI ALERT: + # the title and message may be swapped + # the better API for this is log_error(title, message), and used in many cases this way + # this hack tries to be smart about whats a title (single line ;-)) and fixes it + + if message: + if '\n' not in message: + title = message + error = get_traceback() + else: + error = message + else: + error = get_traceback() + + return get_doc(dict(doctype='Error Log', error=as_unicode(error), method=title)).insert(ignore_permissions=True) def get_desk_link(doctype, name): diff --git a/frappe/api.py b/frappe/api.py index b61c535edd..95a9a408a5 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -82,7 +82,7 @@ def handle(): if frappe.local.request.method=="PUT": if frappe.local.form_dict.data is None: - data = json.loads(frappe.local.request.get_data()) + data = json.loads(frappe.safe_decode(frappe.local.request.get_data())) else: data = json.loads(frappe.local.form_dict.data) doc = frappe.get_doc(doctype, name) @@ -117,7 +117,7 @@ def handle(): if frappe.local.request.method=="POST": if frappe.local.form_dict.data is None: - data = json.loads(frappe.local.request.get_data()) + data = json.loads(frappe.safe_decode(frappe.local.request.get_data())) else: data = json.loads(frappe.local.form_dict.data) data.update({ diff --git a/frappe/auth.py b/frappe/auth.py index 468f45c53e..dba8b05a62 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -142,6 +142,7 @@ class LoginManager: self.validate_hour() self.get_user_info() self.make_session() + self.setup_boot_cache() self.set_user_info() def get_user_info(self, resume=False): @@ -150,6 +151,11 @@ class LoginManager: self.user_type = self.info.user_type + def setup_boot_cache(self): + frappe.cache_manager.build_table_count_cache() + frappe.cache_manager.build_domain_restriced_doctype_cache() + frappe.cache_manager.build_domain_restriced_page_cache() + def set_user_info(self, resume=False): # set sid again frappe.local.cookie_manager.init_cookies() diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index b431c7c473..55792b2648 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -31,12 +31,6 @@ class AssignmentRule(Document): return False - def apply_close(self, doc, assignments): - if (self.close_assignments and - self.name in [d.assignment_rule for d in assignments]): - return self.close_assignments(doc) - - return False def apply_assign(self, doc): if self.safe_eval('assign_condition', doc): @@ -157,16 +151,17 @@ def bulk_apply(doctype, docnames): apply(None, doctype=doctype, name=name) def reopen_closed_assignment(doc): - todo = frappe.db.exists('ToDo', dict( + todo_list = frappe.db.get_all('ToDo', filters = dict( reference_type = doc.doctype, reference_name = doc.name, status = 'Closed' )) - if not todo: + if not todo_list: return False - todo = frappe.get_doc("ToDo", todo) - todo.status = 'Open' - todo.save(ignore_permissions=True) + for todo in todo_list: + todo_doc = frappe.get_doc('ToDo', todo.name) + todo_doc.status = 'Open' + todo_doc.save(ignore_permissions=True) return True def apply(doc, method=None, doctype=None, name=None): @@ -225,13 +220,12 @@ def apply(doc, method=None, doctype=None, name=None): continue if not new_apply: - reopen = reopen_closed_assignment(doc) - if reopen: - break - close = assignment_rule.apply_close(doc, assignments) - if close: - break - + # only reopen if close condition is not satisfied + if not assignment_rule.safe_eval('close_condition', doc): + reopen = reopen_closed_assignment(doc) + if reopen: + break + assignment_rule.close_assignments(doc) def get_assignment_rules(): return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))] diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index a950669d12..2d9428d1fe 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -9,7 +9,7 @@ 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.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day, month_diff from frappe.model.document import Document from frappe.core.doctype.communication.email import make from frappe.utils.background_jobs import get_jobs @@ -48,7 +48,7 @@ class AutoRepeat(Document): 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) + self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date) def unlink_if_applicable(self): if self.status == 'Completed' or self.disabled: @@ -105,32 +105,29 @@ class AutoRepeat(Document): 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) + next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day) row = { "reference_document": self.reference_document, "frequency": self.frequency, - "next_scheduled_date": start_date + "next_scheduled_date": next_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)): + next_date = get_next_schedule_date( + start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True) + + while (getdate(next_date) < getdate(end_date)): row = { "reference_document" : self.reference_document, "frequency" : self.frequency, - "next_scheduled_date" : start_date + "next_scheduled_date" : next_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) - + next_date = get_next_schedule_date( + next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True) return schedule_details @@ -271,18 +268,34 @@ class AutoRepeat(Document): ) -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) +def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False): + if month_map.get(frequency): + month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1 + else: + month_count = 0 + + day_count = 0 if month_count and repeat_on_last_day: next_date = get_next_date(start_date, month_count, 31) + day_count = 31 + next_date = get_next_date(start_date, month_count, day_count) elif month_count and repeat_on_day: next_date = get_next_date(start_date, month_count, repeat_on_day) + day_count = repeat_on_day + next_date = get_next_date(start_date, month_count, day_count) 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) + # next schedule date should be after or on current date + if not for_full_schedule: + while getdate(next_date) < getdate(today()): + if month_count: + month_count += month_map.get(frequency) + next_date = get_next_date(start_date, month_count, day_count) + return next_date def get_next_date(dt, mcount, day=None): @@ -307,10 +320,9 @@ def create_repeated_entries(data): current_date = getdate(today()) schedule_date = getdate(doc.next_schedule_date) - while schedule_date <= current_date and not doc.disabled: + if 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) - + schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, 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) diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index cb98d4a8fd..95f95f3e7d 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -96,6 +96,21 @@ class TestAutoRepeat(unittest.TestCase): linked_comm = frappe.db.exists("Communication", dict(reference_doctype="ToDo", reference_name=new_todo)) self.assertTrue(linked_comm) + def test_next_schedule_date(self): + current_date = getdate(today()) + todo = frappe.get_doc( + dict(doctype='ToDo', description='test next schedule date todo', assigned_by='Administrator')).insert() + doc = make_auto_repeat(frequency='Monthly', reference_document=todo.name, start_date=add_months(today(), -2)) + + #check next_schedule_date is set as per current date + #it should not be a previous month's date + self.assertEqual(doc.next_schedule_date, current_date) + data = get_auto_repeat_entries(current_date) + create_repeated_entries(data) + docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name}) + #the original doc + the repeated doc + self.assertEqual(len(docnames), 2) + def make_auto_repeat(**args): args = frappe._dict(args) diff --git a/frappe/build.py b/frappe/build.py index 265a8c3976..f7437acf8f 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -2,7 +2,7 @@ # MIT License. See license.txt from __future__ import unicode_literals, print_function -import os, frappe, json, shutil, re, warnings +import os, frappe, json, shutil, re, warnings, tempfile from os.path import exists as path_exists, join as join_path, abspath, isdir from distutils.spawn import find_executable from six import iteritems, text_type @@ -12,6 +12,51 @@ from frappe.utils.minify import JavascriptMinify Build the `public` folders and setup languages """ + +def symlink(target, link_name, overwrite=False): + ''' + Create a symbolic link named link_name pointing to target. + If link_name exists then FileExistsError is raised, unless overwrite=True. + When trying to overwrite a directory, IsADirectoryError is raised. + + Source: https://stackoverflow.com/a/55742015/10309266 + ''' + + if not overwrite: + os.symlink(target, linkname) + return + + # os.replace() may fail if files are on different filesystems + link_dir = os.path.dirname(link_name) + + # Create link to target with temporary filename + while True: + temp_link_name = tempfile.mktemp(dir=link_dir) + + # os.* functions mimic as closely as possible system functions + # The POSIX symlink() returns EEXIST if link_name already exists + # https://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html + try: + os.symlink(target, temp_link_name) + break + except FileExistsError: + pass + + # Replace link_name with temp_link_name + try: + # Pre-empt os.replace on a directory with a nicer message + if os.path.isdir(link_name): + raise IsADirectoryError("Cannot symlink over existing directory: '{}'".format(link_name)) + try: + os.replace(temp_link_name, link_name) + except AttributeError: + os.renames(temp_link_name, link_name) + except: + if os.path.islink(temp_link_name): + os.remove(temp_link_name) + raise + + app_paths = None def setup(): global app_paths @@ -118,7 +163,7 @@ def make_asset_dirs(make_copy=False, restore=False): else: shutil.rmtree(target) try: - os.symlink(source, target) + symlink(source, target, overwrite=True) except OSError: print('Cannot link {} to {}'.format(source, target)) else: diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 6e17327616..1e785e12f1 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -115,3 +115,46 @@ def get_doctype_map(doctype, name, filters, order_by=None): def clear_doctype_map(doctype, name): cache_key = frappe.scrub(doctype) + '_map' frappe.cache().hdel(cache_key, name) + +def build_table_count_cache(): + if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import: + return + _cache = frappe.cache() + data = frappe.db.multisql({ + "mariadb": """ + SELECT table_name AS name, + table_rows AS count + FROM information_schema.tables""", + "postgres": """ + SELECT "relname" AS name, + "n_tup_ins" AS count + FROM "pg_stat_all_tables" + """ + }, as_dict=1) + + counts = {d.get('name').lstrip('tab'): d.get('count', None) for d in data} + _cache.set_value("information_schema:counts", counts) + + return counts + +def build_domain_restriced_doctype_cache(): + if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import: + return + _cache = frappe.cache() + active_domains = frappe.get_active_domains() + doctypes = frappe.get_all("DocType", filters={'restrict_to_domain': ('IN', active_domains)}) + doctypes = [doc.name for doc in doctypes] + _cache.set_value("domain_restricted_doctypes", doctypes) + + return doctypes + +def build_domain_restriced_page_cache(): + if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import: + return + _cache = frappe.cache() + active_domains = frappe.get_active_domains() + pages = frappe.get_all("Page", filters={'restrict_to_domain': ('IN', active_domains)}) + pages = [page.name for page in pages] + _cache.set_value("domain_restricted_pages", pages) + + return pages diff --git a/frappe/chat/doctype/chat_profile/chat_profile.json b/frappe/chat/doctype/chat_profile/chat_profile.json index f585924930..eb36f803fe 100644 --- a/frappe/chat/doctype/chat_profile/chat_profile.json +++ b/frappe/chat/doctype/chat_profile/chat_profile.json @@ -22,8 +22,7 @@ "fieldtype": "Link", "label": "User", "options": "User", - "reqd": 1, - "unique": 1 + "reqd": 1 }, { "default": "Online", diff --git a/frappe/chat/doctype/chat_room/chat_room.py b/frappe/chat/doctype/chat_room/chat_room.py index 44a6ce0f0b..609acaef7d 100644 --- a/frappe/chat/doctype/chat_room/chat_room.py +++ b/frappe/chat/doctype/chat_room/chat_room.py @@ -1,17 +1,14 @@ from __future__ import unicode_literals -# imports - standard imports -import json - # imports - module imports -from frappe.model.document import Document -from frappe import _ +from frappe.model.document import Document +from frappe import _ import frappe # imports - frappe module imports -from frappe.chat import authenticate +from frappe.chat import authenticate from frappe.core.doctype.version.version import get_diff -from frappe.chat.doctype.chat_message import chat_message +from frappe.chat.doctype.chat_message import chat_message from frappe.chat.util import ( safe_json_loads, dictify, @@ -22,13 +19,14 @@ from frappe.chat.util import ( session = frappe.session -def is_direct(owner, other, bidirectional = False): + +def is_direct(owner, other, bidirectional=False): def get_room(owner, other): - room = frappe.get_all('Chat Room', filters = [ - ['Chat Room', 'type' , 'in', ('Direct', 'Visitor')], - ['Chat Room', 'owner', '=' , owner], - ['Chat Room User', 'user' , '=' , other] - ], distinct = True) + room = frappe.get_all('Chat Room', filters=[ + ['Chat Room', 'type', 'in', ('Direct', 'Visitor')], + ['Chat Room', 'owner', '=', owner], + ['Chat Room User', 'user', '=', other] + ], distinct=True) return room @@ -38,7 +36,8 @@ def is_direct(owner, other, bidirectional = False): return exists -def get_chat_room_user_set(users, filter_ = None): + +def get_chat_room_user_set(users, filter_=None): seen, uset = set(), list() for u in users: @@ -48,12 +47,13 @@ def get_chat_room_user_set(users, filter_ = None): return uset + class ChatRoom(Document): def validate(self): if self.is_new(): - users = get_chat_room_user_set(self.users, filter_ = lambda u: u.user != session.user) + users = get_chat_room_user_set(self.users, filter_=lambda u: u.user != session.user) self.update(dict( - users = users + users=users )) if self.type == "Direct": @@ -63,7 +63,7 @@ class ChatRoom(Document): other = squashify(self.users) if self.is_new(): - if is_direct(self.owner, other.user, bidirectional = True): + if is_direct(self.owner, other.user, bidirectional=True): frappe.throw(_('Direct room with {0} already exists.').format(other.user)) if self.type == "Group" and not self.room_name: @@ -74,40 +74,44 @@ class ChatRoom(Document): before = self.get_doc_before_save() if not before: return - after = self - diff = dictify(get_diff(before, after)) + after = self + diff = dictify(get_diff(before, after)) if diff: - update = { } + update = {} for changed in diff.changed: field, old, new = changed if field == 'last_message': new = chat_message.get(new) - update.update({ field: new }) + update.update({field: new}) if diff.added or diff.removed: - update.update(dict(users = [u.user for u in self.users])) + update.update(dict(users=[u.user for u in self.users])) - update = dict(room = self.name, data = update) + update = dict(room=self.name, data=update) - frappe.publish_realtime('frappe.chat.room:update', update, room = self.name, after_commit = True) + frappe.publish_realtime('frappe.chat.room:update', update, room=self.name, + after_commit=True) -@frappe.whitelist(allow_guest = True) -def get(user, rooms = None, fields = None, filters = None): + +@frappe.whitelist(allow_guest=True) +def get(user=None, token=None, rooms=None, fields=None, filters=None): # There is this horrible bug out here. - # Looks like if frappe.call sends optional arguments (not in right order), the argument turns to an empty string. + # Looks like if frappe.call sends optional arguments (not in right order), + # the argument turns to an empty string. # I'm not even going to think searching for it. # Hence, the hack was get_if_empty (previous assign_if_none) # - Achilles Rasquinha achilles@frappe.io - authenticate(user) + data = user or token + authenticate(data) rooms, fields, filters = safe_json_loads(rooms, fields, filters) - rooms = listify(get_if_empty(rooms, [ ])) - fields = listify(get_if_empty(fields, [ ])) + rooms = listify(get_if_empty(rooms, [])) + fields = listify(get_if_empty(fields, [])) - const = [ ] # constraints + const = [] # constraints if rooms: const.append(['Chat Room', 'name', 'in', rooms]) if filters: @@ -117,24 +121,24 @@ def get(user, rooms = None, fields = None, filters = None): const.append(filters) default = ['name', 'type', 'room_name', 'creation', 'owner', 'avatar'] - handle = ['users', 'last_message'] + handle = ['users', 'last_message'] - param = [f for f in fields if f not in handle] + param = [f for f in fields if f not in handle] - rooms = frappe.get_all('Chat Room', - or_filters = [ - ['Chat Room', 'owner', '=', user], - ['Chat Room User', 'user', '=', user] - ], - filters = const, - fields = param + ['name'] if param else default, - distinct = True - ) + rooms = frappe.get_all('Chat Room', + or_filters=[ + ['Chat Room', 'owner', '=', frappe.session.user], + ['Chat Room User', 'user', '=', frappe.session.user] + ], + filters=const, + fields=param + ['name'] if param else default, + distinct=True + ) if not fields or 'users' in fields: for i, r in enumerate(rooms): droom = frappe.get_doc('Chat Room', r.name) - rooms[i]['users'] = [ ] + rooms[i]['users'] = [] for duser in droom.users: rooms[i]['users'].append(duser.user) @@ -151,46 +155,47 @@ def get(user, rooms = None, fields = None, filters = None): return rooms -@frappe.whitelist(allow_guest = True) -def create(kind, owner, users = None, name = None): - authenticate(owner) - users = safe_json_loads(users) +@frappe.whitelist(allow_guest=True) +def create(kind, token, users=None, name=None): + authenticate(token) + + users = safe_json_loads(users) create = True if kind == 'Visitor': room = squashify(frappe.db.sql(""" SELECT name FROM `tabChat Room` - WHERE owner = "{owner}" - """.format(owner = owner), as_dict = True)) + WHERE owner=%s + """, (frappe.session.user), as_dict=True)) if room: - room = frappe.get_doc('Chat Room', room.name) + room = frappe.get_doc('Chat Room', room.name) create = False if create: - room = frappe.new_doc('Chat Room') - room.type = kind - room.owner = owner + room = frappe.new_doc('Chat Room') + room.type = kind + room.owner = frappe.session.user room.room_name = name - dusers = [ ] + dusers = [] if kind != 'Visitor': if users: - users = listify(users) + users = listify(users) for user in users: - duser = frappe.new_doc('Chat Room User') + duser = frappe.new_doc('Chat Room User') duser.user = user dusers.append(duser) room.users = dusers else: - dsettings = frappe.get_single('Website Settings') + dsettings = frappe.get_single('Website Settings') room.room_name = dsettings.chat_room_name - users = [user for user in room.users] if hasattr(room, 'users') else [ ] + users = [user for user in room.users] if hasattr(room, 'users') else [] for user in dsettings.chat_operators: if user.user not in users: @@ -199,24 +204,26 @@ def create(kind, owner, users = None, name = None): chat_room_user = {"doctype": "Chat Room User", "user": user.user} room.append('users', chat_room_user) - room.save(ignore_permissions = True) + room.save(ignore_permissions=True) - room = get(owner, rooms = room.name) - users = [room.owner] + [u for u in room.users] + room = get(token=token, rooms=room.name) + if room: + users = [room.owner] + [u for u in room.users] - for u in users: - frappe.publish_realtime('frappe.chat.room:create', room, user = u, after_commit = True) + for user in users: + frappe.publish_realtime('frappe.chat.room:create', room, user=user, after_commit=True) return room -@frappe.whitelist(allow_guest = True) -def history(room, user, fields = None, limit = 10, start = None, end = None): + +@frappe.whitelist(allow_guest=True) +def history(room, user, fields=None, limit=10, start=None, end=None): if frappe.get_doc('Chat Room', room).type != 'Visitor': authenticate(user) fields = safe_json_loads(fields) - mess = chat_message.history(room, limit = limit, start = start, end = end) - mess = squashify(mess) + mess = chat_message.history(room, limit=limit, start=start, end=end) + mess = squashify(mess) - return dictify(mess) \ No newline at end of file + return dictify(mess) diff --git a/frappe/chat/doctype/chat_token/chat_token.json b/frappe/chat/doctype/chat_token/chat_token.json index 40b85c5c6e..b73505ac2c 100644 --- a/frappe/chat/doctype/chat_token/chat_token.json +++ b/frappe/chat/doctype/chat_token/chat_token.json @@ -16,8 +16,7 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Token", - "reqd": 1, - "unique": 1 + "reqd": 1 }, { "fieldname": "ip_address", diff --git a/frappe/commands/site.py b/frappe/commands/site.py index e28fd36346..89e9ab7f34 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -13,6 +13,8 @@ from six import text_type @click.argument('site') @click.option('--db-name', help='Database name') @click.option('--db-type', default='mariadb', type=click.Choice(['mariadb', 'postgres']), help='Optional "postgres" or "mariadb". Default is "mariadb"') +@click.option('--db-host', help='Database Host') +@click.option('--db-port', type=int, help='Database Port') @click.option('--mariadb-root-username', default='root', help='Root username for MariaDB') @click.option('--mariadb-root-password', help='Root password for MariaDB') @click.option('--admin-password', help='Administrator password for new site', default=None) @@ -21,22 +23,22 @@ from six import text_type @click.option('--source_sql', help='Initiate database with a SQL file') @click.option('--install-app', multiple=True, help='Install app after installation') def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None, - verbose=False, install_apps=None, source_sql=None, force=None, install_app=None, - db_name=None, db_type=None): + verbose=False, install_apps=None, source_sql=None, force=None, install_app=None, + db_name=None, db_type=None, db_host=None, db_port=None): "Create a new site" frappe.init(site=site, new_site=True) _new_site(db_name, site, mariadb_root_username=mariadb_root_username, - mariadb_root_password=mariadb_root_password, admin_password=admin_password, - verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force, - db_type=db_type) + mariadb_root_password=mariadb_root_password, admin_password=admin_password, + verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force, + db_type=db_type, db_host=db_host, db_port=db_port) if len(frappe.utils.get_sites()) == 1: use(site) def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None, - admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False, - reinstall=False, db_type=None): + admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False, + reinstall=False, db_type=None, db_host=None, db_port=None): """Install a new Frappe site""" if not force and os.path.exists(site): @@ -65,8 +67,8 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N installing = touch_file(get_site_path('locks', 'installing.lock')) install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, - db_name=db_name, admin_password=admin_password, verbose=verbose, - source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type) + db_name=db_name, admin_password=admin_password, verbose=verbose, + source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type, db_host=db_host, db_port=db_port) apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or []) for app in apps_to_install: diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 9a408430e7..11f8e69b95 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -460,6 +460,15 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), tests = test site = get_site(context) + + allow_tests = frappe.get_conf(site).allow_tests + + if not (allow_tests or os.environ.get('CI')): + click.secho('Testing is disabled for the site!', bold=True) + click.secho('You can enable tests by entering following command:') + click.secho('bench --site {0} set-config allow_tests true'.format(site), fg='green') + return + frappe.init(site=site) frappe.flags.skip_before_tests = skip_before_tests @@ -507,26 +516,6 @@ def run_ui_tests(context, app, headless=False): 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") -@click.option('--profile', is_flag=True, default=False) -@pass_context -def run_setup_wizard_ui_test(context, app=None, profile=False): - "Run setup wizard UI test" - import frappe.test_runner - - site = get_site(context) - frappe.init(site=site) - frappe.connect() - - ret = frappe.test_runner.run_setup_wizard_ui_test(app=app, verbose=context.verbose, - profile=profile) - if len(ret.failures) == 0 and len(ret.errors) == 0: - ret = 0 - - if os.environ.get('CI'): - sys.exit(ret) - @click.command('serve') @click.option('--port', default=8000) @click.option('--profile', is_flag=True, default=False) @@ -752,7 +741,6 @@ commands = [ reset_perms, run_tests, run_ui_tests, - run_setup_wizard_ui_test, serve, set_config, show_config, diff --git a/frappe/config/settings.py b/frappe/config/settings.py index 2422f2fae2..a0a7dcd65f 100644 --- a/frappe/config/settings.py +++ b/frappe/config/settings.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import frappe from frappe import _ from frappe.desk.moduleview import add_setup_section @@ -88,7 +89,7 @@ def get_data(): ] }, { - "label": _("Email"), + "label": _("Email / Notifications"), "icon": "fa fa-envelope", "items": [ { @@ -120,6 +121,12 @@ def get_data(): "type": "doctype", "name": "Newsletter", "description": _("Create and manage newsletter") + }, + { + "type": "doctype", + "route": "Form/Notification Settings/{}".format(frappe.session.user), + "name": "Notification Settings", + "description": _("Configure notifications for mentions, assignments, energy points and more.") } ] }, diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 6795011745..75fd0ad8c6 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -60,10 +60,6 @@ class Address(Document): if not [row for row in self.links if row.link_doctype == "Company"]: frappe.throw(_("Company is mandatory, as it is your company address")) - # removing other links - to_remove = [row for row in self.links if row.link_doctype != "Company"] - [ self.remove(row) for row in to_remove ] - def get_display(self): return get_address_display(self.as_dict()) @@ -145,30 +141,10 @@ def get_list_context(context=None): def get_address_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by = None): from frappe.www.list import get_list user = frappe.session.user - ignore_permissions = False - if is_website_user(): - if not filters: filters = [] - add_name = [] - contact = frappe.db.sql(""" - select - address.name - from - `tabDynamic Link` as link - join - `tabAddress` as address on link.parent = address.name - where - link.parenttype = 'Address' and - link_name in( - select - link.link_name from `tabContact` as contact - join - `tabDynamic Link` as link on contact.name = link.parent - where - contact.user = %s)""",(user)) - for c in contact: - add_name.append(c[0]) - filters.append(("Address", "name", "in", add_name)) - ignore_permissions = True + ignore_permissions = True + + if not filters: filters = [] + filters.append(("Address", "owner", "=", user)) return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions) diff --git a/frappe/core/doctype/activity_log/feed.py b/frappe/core/doctype/activity_log/feed.py index 22a7a3ee14..f51692fe9f 100644 --- a/frappe/core/doctype/activity_log/feed.py +++ b/frappe/core/doctype/activity_log/feed.py @@ -81,9 +81,9 @@ def get_feed_match_conditions(user=None, doctype='Comment'): if user_permissions: can_read_docs = [] - for doctype, obj in user_permissions.items(): + for dt, obj in user_permissions.items(): for n in obj: - can_read_docs.append('{}|{}'.format(doctype, frappe.db.escape(n.get('doc', '')))) + can_read_docs.append('{}|{}'.format(frappe.db.escape(dt), frappe.db.escape(n.get('doc', '')))) if can_read_docs: conditions.append("concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format( diff --git a/frappe/core/doctype/communication/communication.js b/frappe/core/doctype/communication/communication.js index 924c29bee2..1c51319790 100644 --- a/frappe/core/doctype/communication/communication.js +++ b/frappe/core/doctype/communication/communication.js @@ -18,6 +18,10 @@ frappe.ui.form.on("Communication", { frm.convert_to_click && frm.set_convert_button(); frm.subject_field = "subject"; + // content field contains weird table html that does not render well in Quill + // this field is not to be edited directly anyway, so setting it as read only + frm.set_df_property('content', 'read_only', 1); + if(frm.doc.reference_doctype && frm.doc.reference_name) { frm.add_custom_button(__(frm.doc.reference_name), function() { frappe.set_route("Form", frm.doc.reference_doctype, frm.doc.reference_name); diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 74d7fa1654..abd24fb468 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -351,16 +351,26 @@ def get_contacts(email_strings): email = get_email_without_link(email) contact_name = get_contact_name(email) - if not contact_name: - contact = frappe.get_doc({ - "doctype": "Contact", - "first_name": frappe.unscrub(email.split("@")[0]), - }) - contact.add_email(email_id=email, is_primary=True) - contact.insert(ignore_permissions=True) - contact_name = contact.name + if not contact_name and email: + email_parts = email.split("@") + first_name = frappe.unscrub(email_parts[0]) - contacts.append(contact_name) + try: + contact_name = '{0}-{1}'.format(first_name, email_parts[1]) if first_name == 'Contact' else first_name + contact = frappe.get_doc({ + "doctype": "Contact", + "first_name": contact_name, + "name": contact_name + }) + contact.add_email(email_id=email, is_primary=True) + contact.insert(ignore_permissions=True) + contact_name = contact.name + except Exception: + traceback = frappe.get_traceback() + frappe.log_error(traceback) + + if contact_name: + contacts.append(contact_name) return contacts diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index a285941c68..8793c60934 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -10,7 +10,6 @@ from email.utils import formataddr from frappe.core.utils import get_parent_doc from frappe.utils import (get_url, get_formatted_email, cint, validate_email_address, split_emails, time_diff_in_seconds, parse_addr, get_datetime) -from frappe.utils.scheduler import log from frappe.email.email_body import get_message_id import frappe.email.smtp import time @@ -239,8 +238,9 @@ def get_recipients_cc_and_bcc(doc, recipients, cc, bcc, fetched_from_email_accou return recipients, cc, bcc def remove_administrator_from_email_list(email_list): - if 'Administrator' in email_list: - email_list.remove('Administrator') + administrator_email = list(filter(lambda emails: "Administrator" in emails, email_list)) + if administrator_email: + email_list.remove(administrator_email[0]) def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None): """Prepare to make multipart MIME Email @@ -305,27 +305,12 @@ def set_incoming_outgoing_accounts(doc): doc.incoming_email_account = frappe.db.get_value("Email Account", {"append_to": doc.reference_doctype, }, "email_id") - doc.outgoing_email_account = frappe.db.get_value("Email Account", - {"append_to": doc.reference_doctype, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender", "name", - "always_use_account_name_as_sender_name"], as_dict=True) - if not doc.incoming_email_account: doc.incoming_email_account = frappe.db.get_value("Email Account", {"default_incoming": 1, "enable_incoming": 1}, "email_id") - if not doc.outgoing_email_account: - # if from address is not the default email account - doc.outgoing_email_account = frappe.db.get_value("Email Account", - {"email_id": doc.sender, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender", "name", - "send_unsubscribe_message", "always_use_account_name_as_sender_name"], as_dict=True) or frappe._dict() - - if not doc.outgoing_email_account: - doc.outgoing_email_account = frappe.db.get_value("Email Account", - {"default_outgoing": 1, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender", "name", - "send_unsubscribe_message", "always_use_account_name_as_sender_name"],as_dict=True) or frappe._dict() + doc.outgoing_email_account = frappe.email.smtp.get_outgoing_email_account(raise_exception_not_set=False, + append_to=doc.doctype, sender=doc.sender) if doc.sent_or_received == "Sent": doc.db_set("email_account", doc.outgoing_email_account.name) @@ -399,7 +384,7 @@ def get_bcc(doc, recipients=None, fetched_from_email_account=False): return bcc def add_attachments(name, attachments): - '''Add attachments to the given Communiction''' + '''Add attachments to the given Communication''' # loop through attachments for a in attachments: if isinstance(a, string_types): @@ -412,7 +397,9 @@ def add_attachments(name, attachments): "file_url": attach.file_url, "attached_to_doctype": "Communication", "attached_to_name": name, - "folder": "Home/Attachments"}) + "folder": "Home/Attachments", + "is_private": attach.is_private + }) _file.save(ignore_permissions=True) def filter_email_list(doc, email_list, exclude, is_cc=False, is_bcc=False): @@ -509,17 +496,7 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments break except: - traceback = log("frappe.core.doctype.communication.email.sendmail", frappe.as_json({ - "communication_name": communication_name, - "print_html": print_html, - "print_format": print_format, - "attachments": attachments, - "recipients": recipients, - "cc": cc, - "bcc": bcc, - "lang": lang - })) - frappe.logger(__name__).error(traceback) + traceback = frappe.log_error("frappe.core.doctype.communication.email.sendmail") raise def update_mins_to_first_communication(parent, communication): @@ -552,4 +529,4 @@ def mark_email_as_seen(name=None): frappe.response["type"] = 'binary' frappe.response["filename"] = "imaginary_pixel.png" - frappe.response["filecontent"] = buffered_obj.getvalue() \ No newline at end of file + frappe.response["filecontent"] = buffered_obj.getvalue() diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index c5bd4e99c9..9391b262d7 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -3,19 +3,20 @@ frappe.ui.form.on('Data Import', { onload: function(frm) { - if(frm.doc.__islocal) { + if (frm.doc.__islocal) { frm.set_value("action", ""); } frappe.call({ - method: "frappe.core.doctype.data_import.data_import.get_importable_doc", + method: "frappe.core.doctype.data_import.data_import.get_importable_doctypes", callback: function (r) { + let importable_doctypes = r.message; frm.set_query("reference_doctype", function () { return { "filters": { "issingle": 0, "istable": 0, - "name": ['in', r.message] + "name": ['in', importable_doctypes] } }; }); diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index 80f8553121..ecf34d24b0 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -30,9 +30,8 @@ class DataImport(Document): @frappe.whitelist() -def get_importable_doc(): - import_lst = frappe.cache().hget("can_import", frappe.session.user) - return import_lst +def get_importable_doctypes(): + return frappe.cache().hget("can_import", frappe.session.user) @frappe.whitelist() def import_data(data_import): diff --git a/frappe/core/doctype/data_import/importer_new.py b/frappe/core/doctype/data_import/importer_new.py index 6fccbc89ef..b3392bf4ad 100644 --- a/frappe/core/doctype/data_import/importer_new.py +++ b/frappe/core/doctype/data_import/importer_new.py @@ -19,6 +19,8 @@ from frappe.model import no_value_fields, table_fields INVALID_VALUES = ["", None] MAX_ROWS_IN_PREVIEW = 10 +INSERT = "Insert New Records" +UPDATE = "Update Existing Records" # pylint: disable=R0201 class Importer: @@ -34,9 +36,12 @@ class Importer: if self.data_import.template_options: template_options = frappe.parse_json(self.data_import.template_options) self.template_options.update(template_options) + self.import_type = self.data_import.import_type else: self.data_import = None + self.import_type = self.import_type or INSERT + self.header_row = None self.data = None # used to store date formats guessed from data rows per column @@ -54,8 +59,10 @@ class Importer: extension = None if self.data_import and self.data_import.import_file: file_doc = frappe.get_doc("File", {"file_url": self.data_import.import_file}) + parts = file_doc.get_extension() + extension = parts[1] content = file_doc.get_content() - extension = file_doc.file_name.split(".")[1] + extension = extension.lstrip(".") if file_path: content, extension = self.read_file(file_path) @@ -79,6 +86,12 @@ class Importer: return file_content, extn def read_content(self, content, extension): + error_title = _("Template Error") + if extension not in ("csv", "xlsx", "xls"): + frappe.throw( + _("Import template should be of type .csv, .xlsx or .xls"), title=error_title + ) + if extension == "csv": data = read_csv_content(content) elif extension == "xlsx": @@ -86,6 +99,11 @@ class Importer: elif extension == "xls": data = read_xls_file_from_attached_file(content) + if len(data) <= 1: + frappe.throw( + _("Import template should contain a Header and atleast one row."), title=error_title + ) + self.header_row = data[0] self.data = data[1:] @@ -243,7 +261,7 @@ class Importer: "fieldtype": "Data", "fieldname": "name", "label": "ID", - "reqd": self.data_import.import_type == "Update Existing Records", + "reqd": self.import_type == UPDATE, "parent": doctype, } ) @@ -589,8 +607,11 @@ class Importer: return value def parse_doc(doctype, docfields, values, row_number): - # new_doc returns a dict with default values set - doc = frappe.new_doc(doctype, as_dict=True) + doc = frappe._dict() + if self.import_type == INSERT: + # new_doc returns a dict with default values set + doc = frappe.new_doc(doctype, as_dict=True) + # remove standard fields and __islocal for key in frappe.model.default_fields + ("__islocal",): doc.pop(key, None) @@ -603,12 +624,46 @@ class Importer: if value: doc[df.fieldname] = self.parse_value(value, df) + is_table = frappe.get_meta(doctype).istable + is_update = self.import_type == UPDATE + if is_table and is_update and doc.get("name") in INVALID_VALUES: + # for table rows being inserted in update + # create a new doc with defaults set + new_doc = frappe.new_doc(doctype, as_dict=True) + new_doc.update(doc) + doc = new_doc + check_mandatory_fields(doctype, doc, row_number) return doc def check_mandatory_fields(doctype, doc, row_number): - # check if mandatory fields are set (except table fields) + """If import type is Insert: + Check for mandatory fields (except table fields) in doc + if import type is Update: + Check for name field or autoname field in doc + """ meta = frappe.get_meta(doctype) + if self.import_type == UPDATE: + if meta.istable: + # when updating records with table rows, + # there are two scenarios: + # 1. if row 'name' is provided in the template + # the table row will be updated + # 2. if row 'name' is not provided + # then a new row will be added + # so we dont need to check for mandatory + return + + id_field = self.get_id_field(doctype) + if doc.get(id_field.fieldname) in INVALID_VALUES: + self.warnings.append( + { + "row": row_number, + "message": _("{0} is a mandatory field").format(id_field.label), + } + ) + return + fields = [ df for df in meta.fields @@ -685,21 +740,17 @@ class Importer: ) elif mandatory_table_fields: fields_string = ", ".join([df.label for df in mandatory_table_fields]) - self.warnings.append( - { - "row": first_row[0], - "message": _("There should be atleast one row for the following tables: {0}").format(fields_string), - } + message = _("There should be atleast one row for the following tables: {0}").format( + fields_string ) + self.warnings.append({"row": first_row[0], "message": message}) return doc, rows, data[len(rows) :] def process_doc(self, doc): - import_type = self.data_import.import_type - - if import_type == "Insert New Records": + if self.import_type == INSERT: return self.insert_record(doc) - elif import_type == "Update Existing Records": + elif self.import_type == UPDATE: return self.update_record(doc) def insert_record(self, doc): @@ -747,7 +798,7 @@ class Importer: d.missing_values.remove(link_value) def update_record(self, doc): - id_fieldname = self.get_id_fieldname() + id_fieldname = self.get_id_fieldname(self.doctype) id_value = doc[id_fieldname] existing_doc = frappe.get_doc(self.doctype, id_value) existing_doc.flags.via_data_import = self.data_import.name @@ -804,12 +855,6 @@ class Importer: df=col.df, ) - def get_id_fieldname(self): - autoname_field = self.get_autoname_field(self.doctype) - if autoname_field: - return autoname_field.fieldname - return "name" - def get_eta(self, current, total, processing_time): remaining = total - current eta = processing_time * remaining @@ -826,6 +871,15 @@ class Importer: mandatory_fields_count += 1 return mandatory_fields_count == 1 + def get_id_fieldname(self, doctype): + return self.get_id_field(doctype).fieldname + + def get_id_field(self, doctype): + autoname_field = self.get_autoname_field(doctype) + if autoname_field: + return autoname_field + return frappe._dict({"label": "ID", "fieldname": "name", "fieldtype": "Data"}) + def get_autoname_field(self, doctype): meta = frappe.get_meta(doctype) if meta.autoname and meta.autoname.startswith("field:"): @@ -862,15 +916,15 @@ class Importer: if failed_records: print("Failed to import {0} records".format(len(failed_records))) - file_name = '{0}_import_on_{1}.txt'.format(self.doctype, frappe.utils.now()) - print('Check {0} for errors'.format(os.path.join('sites', file_name))) + file_name = "{0}_import_on_{1}.txt".format(self.doctype, frappe.utils.now()) + print("Check {0} for errors".format(os.path.join("sites", file_name))) text = "" for w in failed_records: - text += "Row Indexes: {0}\n".format(str(w.get('row_indexes', []))) - text += "Messages:\n{0}\n".format('\n'.join(w.get('messages', []))) - text += "Traceback:\n{0}\n\n".format(w.get('exception')) + text += "Row Indexes: {0}\n".format(str(w.get("row_indexes", []))) + text += "Messages:\n{0}\n".format("\n".join(w.get("messages", []))) + text += "Traceback:\n{0}\n\n".format(w.get("exception")) - with open(file_name, 'w') as f: + with open(file_name, "w") as f: f.write(text) diff --git a/frappe/core/doctype/data_import_beta/data_import_beta.js b/frappe/core/doctype/data_import_beta/data_import_beta.js index b48a303b6a..873d3065b7 100644 --- a/frappe/core/doctype/data_import_beta/data_import_beta.js +++ b/frappe/core/doctype/data_import_beta/data_import_beta.js @@ -82,10 +82,9 @@ frappe.ui.form.on('Data Import Beta', { () => frappe.set_route('List', frm.doc.reference_doctype)); } + frm.disable_save(); if (frm.doc.status !== 'Success') { - if (frm.import_in_progress) { - frm.disable_save(); - } else if (!frm.is_new() && frm.doc.import_file) { + if (!frm.is_new() && frm.doc.import_file) { let label = frm.doc.status === 'Pending' ? __('Start Import') : __('Retry'); frm.page.set_primary_action(label, () => frm.events.start_import(frm)); } else { @@ -323,13 +322,23 @@ frappe.ui.form.on('Data Import Beta', { .map(log => { let html = ''; if (log.success) { - html = __('Successfully imported {0}', [ - `${frappe.utils.get_form_link( - frm.doc.reference_doctype, - log.docname, - true - )}` - ]); + if (frm.doc.import_type === 'Insert New Records') { + html = __('Successfully imported {0}', [ + `${frappe.utils.get_form_link( + frm.doc.reference_doctype, + log.docname, + true + )}` + ]); + } else { + html = __('Successfully updated {0}', [ + `${frappe.utils.get_form_link( + frm.doc.reference_doctype, + log.docname, + true + )}` + ]); + } } else { let messages = log.messages .map(JSON.parse) diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 622663bca4..e3242887c7 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -1,1726 +1,461 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, "autoname": "hash", - "beta": 0, "creation": "2013-02-22 01:27:33", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "label_and_type", + "label", + "fieldtype", + "fieldname", + "reqd", + "precision", + "length", + "search_index", + "in_list_view", + "in_standard_filter", + "in_global_search", + "in_preview", + "allow_in_quick_entry", + "bold", + "translatable", + "collapsible", + "collapsible_depends_on", + "column_break_6", + "options", + "default", + "fetch_from", + "fetch_if_empty", + "permissions", + "depends_on", + "hidden", + "read_only", + "unique", + "set_only_once", + "allow_bulk_edit", + "column_break_13", + "permlevel", + "ignore_user_permissions", + "allow_on_submit", + "report_hide", + "remember_last_selected_value", + "ignore_xss_filter", + "property_depends_on_section", + "mandatory_depends_on", + "column_break_38", + "read_only_depends_on", + "display", + "in_filter", + "no_copy", + "print_hide", + "print_hide_if_no_value", + "print_width", + "width", + "columns", + "column_break_22", + "description", + "oldfieldname", + "oldfieldtype" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "label_and_type", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 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": 1, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "label", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, "label": "Label", - "length": 0, - "no_copy": 0, "oldfieldname": "label", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "163", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "163" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, "bold": 1, - "collapsible": 0, - "columns": 0, "default": "Data", - "fetch_if_empty": 0, "fieldname": "fieldtype", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, "label": "Type", - "length": 0, - "no_copy": 0, "oldfieldname": "fieldtype", "oldfieldtype": "Select", "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature", - "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": 1, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "fieldname", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, "label": "Name", - "length": 0, - "no_copy": 0, "oldfieldname": "fieldname", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "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, - "fetch_if_empty": 0, + "default": "0", "fieldname": "reqd", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, "label": "Mandatory", - "length": 0, - "no_copy": 0, "oldfieldname": "reqd", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", "description": "Set non-standard precision for a Float or Currency field", - "fetch_if_empty": 0, "fieldname": "precision", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Precision", - "length": 0, - "no_copy": 0, "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "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": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)", - "fetch_if_empty": 0, "fieldname": "length", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Length", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Length" }, { - "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": "search_index", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Index", - "length": 0, - "no_copy": 0, "oldfieldname": "search_index", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "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": "in_list_view", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "In List View", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "70px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "70px" }, { - "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": "in_standard_filter", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "In Standard Filter", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "In Standard Filter" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", - "fetch_if_empty": 0, "fieldname": "in_global_search", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "In Global Search", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "In Global Search" }, { - "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": "in_preview", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "In Preview", - "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": "In Preview" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "allow_in_quick_entry", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Allow in Quick Entry", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Allow in Quick Entry" }, { - "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": "bold", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Bold", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Bold" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", - "fetch_if_empty": 0, "fieldname": "translatable", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Translatable", - "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": "Translatable" }, { - "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.fieldtype===\"Section Break\"", - "fetch_if_empty": 0, "fieldname": "collapsible", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Collapsible", - "length": 255, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "length": 255 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.fieldtype==\"Section Break\"", - "fetch_if_empty": 0, "fieldname": "collapsible_depends_on", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Collapsible Depends On", - "length": 0, - "no_copy": 0, - "options": "JS", - "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": "JS" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_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_preview": 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, "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.", - "fetch_if_empty": 0, "fieldname": "options", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, "label": "Options", - "length": 0, - "no_copy": 0, "oldfieldname": "options", - "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Text" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "default", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Default", - "length": 0, - "no_copy": 0, "oldfieldname": "default", - "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Text" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "fetch_from", "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_preview": 0, - "in_standard_filter": 0, - "label": "Fetch From", - "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": "Fetch From" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.", - "fetch_if_empty": 0, "fieldname": "fetch_if_empty", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Fetch If Empty", - "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": "Fetch If Empty" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "permissions", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Permissions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Permissions" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "depends_on", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Display Depends On", "length": 255, - "no_copy": 0, "oldfieldname": "depends_on", "oldfieldtype": "Data", - "options": "JS", - "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": "JS" }, { - "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": "hidden", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Hidden", - "length": 0, - "no_copy": 0, "oldfieldname": "hidden", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "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": "read_only", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Read Only", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "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": "unique", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Unique", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Unique" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "Do not allow user to change after set the first time", - "fetch_if_empty": 0, "fieldname": "set_only_once", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Set Only Once", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Set Only Once" }, { - "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.fieldtype == \"Table\"", - "fetch_if_empty": 0, "fieldname": "allow_bulk_edit", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Allow Bulk Edit", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Allow Bulk Edit" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_13", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 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, "default": "0", - "fetch_if_empty": 0, "fieldname": "permlevel", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Perm Level", - "length": 0, - "no_copy": 0, "oldfieldname": "permlevel", "oldfieldtype": "Int", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "User permissions should not apply for this Link", - "fetch_if_empty": 0, "fieldname": "ignore_user_permissions", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Ignore User Permissions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Ignore User Permissions" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "eval: parent.is_submittable", - "fetch_if_empty": 0, "fieldname": "allow_on_submit", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Allow on Submit", - "length": 0, - "no_copy": 0, "oldfieldname": "allow_on_submit", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "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": "report_hide", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Report Hide", - "length": 0, - "no_copy": 0, "oldfieldname": "report_hide", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "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.fieldtype == 'Link')", - "fetch_if_empty": 0, "fieldname": "remember_last_selected_value", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Remember Last Selected Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Remember Last Selected Value" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", - "fetch_if_empty": 0, "fieldname": "ignore_xss_filter", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Ignore XSS Filter", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Ignore XSS Filter" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "display", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Display", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Display" }, { - "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": "in_filter", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "In Filter", - "length": 0, - "no_copy": 0, "oldfieldname": "in_filter", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "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": "no_copy", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "No Copy", - "length": 0, - "no_copy": 0, "oldfieldname": "no_copy", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "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": "print_hide", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Print Hide", - "length": 0, - "no_copy": 0, "oldfieldname": "print_hide", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", - "fetch_if_empty": 0, "fieldname": "print_hide_if_no_value", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Print Hide If No Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Print Hide If No Value" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "print_width", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Print Width", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Print Width" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "width", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, "label": "Width", - "length": 0, - "no_copy": 0, "oldfieldname": "width", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", - "fetch_if_empty": 0, "fieldname": "columns", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "label": "Columns", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Columns" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_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_preview": 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, - "fetch_if_empty": 0, "fieldname": "description", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, "label": "Description", - "length": 0, - "no_copy": 0, "oldfieldname": "description", "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "300px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "300px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "oldfieldname", "fieldtype": "Data", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, "oldfieldname": "oldfieldname", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "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, - "fetch_if_empty": 0, "fieldname": "oldfieldtype", "fieldtype": "Data", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, "oldfieldname": "oldfieldtype", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Data" + }, + { + "fieldname": "mandatory_depends_on", + "fieldtype": "Code", + "label": "Mandatory Depends On", + "options": "JS" + }, + { + "fieldname": "read_only_depends_on", + "fieldtype": "Code", + "label": "Read Only Depends On", + "options": "JS" + }, + { + "fieldname": "property_depends_on_section", + "fieldtype": "Section Break", + "label": "Property Depends On" + }, + { + "fieldname": "column_break_38", + "fieldtype": "Column Break" } ], - "has_web_view": 0, - "hide_toolbar": 0, "idx": 1, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, "istable": 1, - "max_attachments": 0, - "modified": "2019-05-28 12:19:53.415372", - "modified_by": "Administrator", + "modified": "2019-11-15 12:28:24.461628", + "modified_by": "umair@erpnext.com", "module": "Core", "name": "DocField", "owner": "Administrator", "permissions": [], - "quick_entry": 0, - "read_only": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index a6a5e258ee..4e3f2fd84a 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "autoname": "Prompt", "creation": "2013-02-18 13:36:19", @@ -28,6 +29,7 @@ "name_case", "column_break_15", "description", + "documentation", "form_settings_section", "image_field", "timeline_field", @@ -57,6 +59,10 @@ "restrict_to_domain", "read_only", "in_create", + "actions_section", + "actions", + "links_section", + "links", "web_view", "has_web_view", "allow_guest_to_view", @@ -454,11 +460,39 @@ "fieldname": "nsm_parent_field", "fieldtype": "Data", "label": "Parent Field (Tree)" + }, + { + "description": "URL for documentation or help", + "fieldname": "documentation", + "fieldtype": "Data", + "label": "Documentation Link" + }, + { + "fieldname": "actions_section", + "fieldtype": "Section Break", + "label": "Actions" + }, + { + "fieldname": "actions", + "fieldtype": "Table", + "label": "Actions", + "options": "DocType Action" + }, + { + "fieldname": "links_section", + "fieldtype": "Section Break", + "label": "Links Section" + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "options": "DocType Link" } ], "icon": "fa fa-bolt", "idx": 6, - "modified": "2019-09-07 14:28:05.392490", + "modified": "2019-11-25 17:24:03.690192", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 1223d50878..de8221da29 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -727,8 +727,8 @@ def validate_fields(meta): if not options: frappe.throw(_("{0}: Options must be a valid DocType for field {1} in row {2}").format(docname, d.label, d.idx), WrongOptionsDoctypeLinkError) elif not (options == d.options): - frappe.throw(_("{0}: Options {1} must be the same as doctype name {2} for the field {3}", DoctypeLinkError) - .format(docname, d.options, options, d.label)) + frappe.throw(_("{0}: Options {1} must be the same as doctype name {2} for the field {3}") + .format(docname, d.options, options, d.label), DoctypeLinkError) else: # fix case d.options = options @@ -905,7 +905,7 @@ def validate_fields(meta): def check_illegal_depends_on_conditions(docfield): ''' assignment operation should not be allowed in the depends on condition.''' - depends_on_fields = ["depends_on", "collapsible_depends_on"] + depends_on_fields = ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"] for field in depends_on_fields: depends_on = docfield.get(field, None) if depends_on and ("=" in depends_on) and \ diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 07a42e73a1..8d8731e012 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -96,14 +96,19 @@ class TestDocType(unittest.TestCase): def test_all_depends_on_fields_conditions(self): import re - docfields = frappe.get_all("DocField", or_filters={ + docfields = frappe.get_all("DocField", + or_filters={ "ifnull(depends_on, '')": ("!=", ''), - "ifnull(collapsible_depends_on, '')": ("!=", '') - }, fields=["parent", "depends_on", "collapsible_depends_on", "fieldname", "fieldtype"]) + "ifnull(collapsible_depends_on, '')": ("!=", ''), + "ifnull(mandatory_depends_on, '')": ("!=", ''), + "ifnull(read_only_depends_on, '')": ("!=", '') + }, + fields=["parent", "depends_on", "collapsible_depends_on", "mandatory_depends_on",\ + "read_only_depends_on", "fieldname", "fieldtype"]) pattern = """[\w\.:_]+\s*={1}\s*[\w\.@'"]+""" for field in docfields: - for depends_on in ["depends_on", "collapsible_depends_on"]: + for depends_on in ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]: condition = field.get(depends_on) if condition: self.assertFalse(re.match(pattern, condition)) diff --git a/frappe/core/doctype/test_runner/__init__.py b/frappe/core/doctype/doctype_action/__init__.py similarity index 100% rename from frappe/core/doctype/test_runner/__init__.py rename to frappe/core/doctype/doctype_action/__init__.py diff --git a/frappe/core/doctype/doctype_action/doctype_action.json b/frappe/core/doctype/doctype_action/doctype_action.json new file mode 100644 index 0000000000..7a1b845af3 --- /dev/null +++ b/frappe/core/doctype/doctype_action/doctype_action.json @@ -0,0 +1,57 @@ +{ + "actions": [], + "creation": "2019-09-23 16:28:13.953520", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "action_type", + "action", + "group" + ], + "fields": [ + { + "columns": 2, + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "fieldname": "group", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Group" + }, + { + "columns": 2, + "fieldname": "action_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Action Type", + "options": "Server Action", + "reqd": 1 + }, + { + "columns": 4, + "fieldname": "action", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Action", + "reqd": 1 + } + ], + "istable": 1, + "modified": "2019-09-24 09:11:39.860100", + "modified_by": "Administrator", + "module": "Core", + "name": "DocType Action", + "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/doctype_action/doctype_action.py b/frappe/core/doctype/doctype_action/doctype_action.py new file mode 100644 index 0000000000..a745c7da40 --- /dev/null +++ b/frappe/core/doctype/doctype_action/doctype_action.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 DocTypeAction(Document): + pass diff --git a/frappe/tests/ui/__init__.py b/frappe/core/doctype/doctype_link/__init__.py similarity index 100% rename from frappe/tests/ui/__init__.py rename to frappe/core/doctype/doctype_link/__init__.py diff --git a/frappe/core/doctype/doctype_link/doctype_link.json b/frappe/core/doctype/doctype_link/doctype_link.json new file mode 100644 index 0000000000..752b4bb5da --- /dev/null +++ b/frappe/core/doctype/doctype_link/doctype_link.json @@ -0,0 +1,46 @@ +{ + "actions": [], + "creation": "2019-09-24 11:41:25.291377", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "link_doctype", + "link_fieldname", + "group" + ], + "fields": [ + { + "fieldname": "link_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Link DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "link_fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Link Fieldname", + "reqd": 1 + }, + { + "fieldname": "group", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Group" + } + ], + "istable": 1, + "modified": "2019-09-24 11:41:25.291377", + "modified_by": "Administrator", + "module": "Core", + "name": "DocType Link", + "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/doctype_link/doctype_link.py b/frappe/core/doctype/doctype_link/doctype_link.py new file mode 100644 index 0000000000..efe8b09809 --- /dev/null +++ b/frappe/core/doctype/doctype_link/doctype_link.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 DocTypeLink(Document): + pass diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index ee139000e1..ac89b157fa 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -89,8 +89,9 @@ class File(Document): def validate(self): if self.is_new(): + self.set_is_private() + self.set_file_name() self.validate_duplicate_entry() - self.validate_file_name() self.validate_folder() if not self.file_url and not self.flags.ignore_file_validate: @@ -133,6 +134,9 @@ class File(Document): frappe.db.set_value(self.attached_to_doctype, self.attached_to_name, self.attached_to_field, self.file_url) + if self.file_url and (self.is_private != self.file_url.startswith('/private')): + frappe.throw(_('Invalid file URL. Please contact System Administrator.')) + def set_folder_name(self): """Make parent folders if not exists based on reference doctype and name""" if self.attached_to_doctype and not self.folder: @@ -157,9 +161,11 @@ class File(Document): def validate_duplicate_entry(self): if not self.flags.ignore_duplicate_entry_error and not self.is_folder: - # check duplicate name + if not self.content_hash: + self.generate_content_hash() - # check duplicate assignement + # check duplicate name + # check duplicate assignment filters = { 'content_hash': self.content_hash, 'is_private': self.is_private, @@ -184,21 +190,20 @@ class File(Document): else: self.file_url = duplicate_file.file_url - def validate_file_name(self): + def set_file_name(self): if not self.file_name and self.file_url: self.file_name = self.file_url.split('/')[-1] def generate_content_hash(self): - if self.content_hash or not self.file_url: + if self.content_hash or not self.file_url or self.file_url.startswith('http'): return - - if self.file_url.startswith("/files/"): - try: - with open(get_files_path(self.file_name.lstrip("/")), "rb") as f: - self.content_hash = get_content_hash(f.read()) - except IOError: - frappe.msgprint(_("File {0} does not exist").format(self.file_url)) - raise + file_name = self.file_url.split('/')[-1] + try: + with open(get_files_path(file_name, is_private=self.is_private), "rb") as f: + self.content_hash = get_content_hash(f.read()) + except IOError: + frappe.msgprint(_("File {0} does not exist").format(self.file_url)) + raise def on_trash(self): if self.is_home_folder or self.is_attachments_folder: @@ -306,39 +311,6 @@ class File(Document): exists = os.path.exists(self.get_full_path()) return exists - def upload(self): - # get record details - self.attached_to_doctype = frappe.form_dict.doctype - self.attached_to_name = frappe.form_dict.docname - self.attached_to_field = frappe.form_dict.docfield - self.file_url = frappe.form_dict.file_url - self.file_name = frappe.form_dict.filename - frappe.form_dict.is_private = cint(frappe.form_dict.is_private) - - if not self.file_name and not self.file_url: - frappe.msgprint(_("Please select a file or url"), - raise_exception=True) - - file_doc = self.get_file_doc() - - comment = {} - if self.attached_to_doctype and self.attached_to_name: - comment = frappe.get_doc(self.attached_to_doctype, self.attached_to_name).add_comment("Attachment", - _ ("added {0}").format("{file_name}{icon}".format(**{ - "icon": ' ' \ - if file_doc.is_private else "", - "file_url": file_doc.file_url.replace("#", "%23") \ - if file_doc.file_name else file_doc.file_url, - "file_name": file_doc.file_name or file_doc.file_url - }))) - - return { - "name": file_doc.name, - "file_name": file_doc.file_name, - "file_url": file_doc.file_url, - "is_private": file_doc.is_private, - "comment": comment.as_dict() if comment else {} - } def get_content(self): """Returns [`file_name`, `content`] for given file name `fname`""" @@ -563,6 +535,9 @@ class File(Document): except frappe.DoesNotExistError: frappe.clear_messages() + def set_is_private(self): + if self.file_url: + self.is_private = cint(self.file_url.startswith('/private')) def on_doctype_update(): frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"]) diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index 071bb4afc0..bc9a1fcdcd 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -25,7 +25,7 @@ class PreparedReport(Document): enqueue(run_background, prepared_report=self.name, timeout=6000) def on_trash(self): - remove_all("PreparedReport", self.name, from_delete=True) + remove_all("Prepared Report", self.name) def run_background(prepared_report): @@ -85,7 +85,8 @@ def create_json_gz_file(data, dt, dn): "file_name": json_filename, "attached_to_doctype": dt, "attached_to_name": dn, - "content": compressed_content + "content": compressed_content, + "is_private": 1 }) _file.save(ignore_permissions=True) diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index f71179d388..099c279dab 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -30,7 +30,7 @@ class Report(Document): if self.is_standard == "No": # allow only script manager to edit scripts - if frappe.session.user!="Administrator": + if self.report_type != 'Report Builder': frappe.only_for('Script Manager', True) if frappe.db.get_value("Report", self.name, "is_standard") == "Yes": diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index cea3fc0096..b8b18205b4 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -86,17 +86,28 @@ class TestReport(unittest.TestCase): report = frappe.get_doc('Report', report_name) report.report_script = ''' +totals = {} +for user in frappe.get_all('User', fields = ['name', 'user_type', 'creation']): + if not user.user_type in totals: + totals[user.user_type] = 0 + totals[user.user_type] = totals[user.user_type] + 1 + data = [ - [{'fieldname': 'name', 'label': 'ID'}], - [frappe.db.get_all('User', dict(user_type="System User"))] + [ + {'fieldname': 'type', 'label': 'Type'}, + {'fieldname': 'value', 'label': 'Value'} + ], + [ + {"type":key, "value": value} for key, value in totals.items() + ] ] ''' report.save() data = report.get_data() # check columns - self.assertEqual(data[0][0]['label'], 'ID') + self.assertEqual(data[0][0]['label'], 'Type') # check values - self.assertTrue('Administrator' in [d.get('name') for d in data[1][0]]) + self.assertTrue('System User' in [d.get('type') for d in data[1]]) diff --git a/frappe/core/doctype/scheduled_job_log/__init__.py b/frappe/core/doctype/scheduled_job_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.js b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.js new file mode 100644 index 0000000000..d43160c658 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Scheduled Job Log', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json new file mode 100644 index 0000000000..9e7f72a722 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json @@ -0,0 +1,64 @@ +{ + "actions": [], + "creation": "2019-09-23 14:36:36.935869", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "status", + "scheduled_job_type", + "details" + ], + "fields": [ + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Scheduled\nSuccess\nFailed", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "details", + "fieldtype": "Code", + "label": "Details", + "read_only": 1 + }, + { + "fieldname": "scheduled_job_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Scheduled Job", + "options": "Scheduled Job Type", + "read_only": 1, + "reqd": 1 + } + ], + "links": [], + "modified": "2019-09-25 11:55:10.646458", + "modified_by": "Administrator", + "module": "Core", + "name": "Scheduled Job Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 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/scheduled_job_log/scheduled_job_log.py b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py new file mode 100644 index 0000000000..26871c9adf --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.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 ScheduledJobLog(Document): + pass diff --git a/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py b/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py new file mode 100644 index 0000000000..1e5290425b --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.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 TestScheduledJobLog(unittest.TestCase): + pass diff --git a/frappe/core/doctype/scheduled_job_type/__init__.py b/frappe/core/doctype/scheduled_job_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.js b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.js new file mode 100644 index 0000000000..55907b17fc --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Scheduled Job Type', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json new file mode 100644 index 0000000000..e2ec921679 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -0,0 +1,98 @@ +{ + "actions": [ + { + "action": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event", + "action_type": "Server Action", + "label": "Execute" + } + ], + "creation": "2019-09-23 14:34:09.205368", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "stopped", + "method", + "frequency", + "cron_format", + "last_execution", + "create_log" + ], + "fields": [ + { + "fieldname": "method", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Method", + "read_only": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "stopped", + "fieldtype": "Check", + "label": "Stopped" + }, + { + "default": "0", + "depends_on": "eval:doc.queue==='All'", + "fieldname": "create_log", + "fieldtype": "Check", + "label": "Create Log" + }, + { + "fieldname": "last_execution", + "fieldtype": "Datetime", + "label": "Last Execution", + "read_only": 1 + }, + { + "allow_in_quick_entry": 1, + "depends_on": "eval:doc.queue==='Cron'", + "fieldname": "cron_format", + "fieldtype": "Data", + "label": "Cron Format", + "read_only": 1 + }, + { + "fieldname": "frequency", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Frequency", + "options": "All\nHourly\nHourly Long\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual", + "read_only": 1, + "reqd": 1 + } + ], + "in_create": 1, + "links": [ + { + "link_doctype": "Scheduled Job Log", + "link_fieldname": "scheduled_job_type" + } + ], + "modified": "2019-12-09 11:10:21.259929", + "modified_by": "Administrator", + "module": "Core", + "name": "Scheduled Job Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 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/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py new file mode 100644 index 0000000000..5d5bd35eec --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals + +import frappe, json +from frappe.model.document import Document +from frappe.utils import now_datetime, get_datetime +from datetime import datetime +from croniter import croniter +from frappe.utils.background_jobs import enqueue, get_jobs + +class ScheduledJobType(Document): + def autoname(self): + self.name = '.'.join(self.method.split('.')[-2:]) + + def validate(self): + if self.frequency != 'All': + # force logging for all events other than continuous ones (ALL) + self.create_log = 1 + + def enqueue(self): + # enqueue event if last execution is done + if self.is_event_due(): + if frappe.flags.enqueued_jobs: + frappe.flags.enqueued_jobs.append(self.method) + + if frappe.flags.execute_job: + self.execute() + else: + if not self.is_job_in_queue(): + enqueue('frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job', + queue = self.get_queue_name(), job_type=self.method) + return True + + return False + + def is_event_due(self, current_time = None): + '''Return true if event is due based on time lapsed since last execution''' + # if the next scheduled event is before NOW, then its due! + return self.get_next_execution() <= (current_time or now_datetime()) + + def is_job_in_queue(self): + queued_jobs = get_jobs(site=frappe.local.site, key='job_type')[frappe.local.site] + return self.method in queued_jobs + + def get_next_execution(self): + CRON_MAP = { + "Yearly": "0 0 1 1 *", + "Annual": "0 0 1 1 *", + "Monthly": "0 0 1 * *", + "Monthly Long": "0 0 1 * *", + "Weekly": "0 0 * * 0", + "Weekly Long": "0 0 * * 0", + "Daily": "0 0 * * *", + "Daily Long": "0 0 * * *", + "Hourly": "0 * * * *", + "Hourly Long": "0 * * * *", + "All": "0/" + str((frappe.get_conf().scheduler_interval or 240) // 60) + " * * * *", + } + + if not self.cron_format: + self.cron_format = CRON_MAP[self.frequency] + + return croniter(self.cron_format, + get_datetime(self.last_execution or datetime(2000, 1, 1))).get_next(datetime) + + def execute(self): + self.scheduler_log = None + try: + self.log_status('Start') + frappe.get_attr(self.method)() + frappe.db.commit() + self.log_status('Complete') + except Exception: + frappe.db.rollback() + self.log_status('Failed') + + def log_status(self, status): + # log file + frappe.logger(__name__).info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site)) + self.update_scheduler_log(status) + + def update_scheduler_log(self, status): + if not self.create_log: + return + if not self.scheduler_log: + self.scheduler_log = frappe.get_doc(dict(doctype = 'Scheduled Job Log', scheduled_job_type=self.name)).insert(ignore_permissions=True) + self.scheduler_log.db_set('status', status) + if status == 'Failed': + self.scheduler_log.db_set('details', frappe.get_traceback()) + if status == 'Start': + self.db_set('last_execution', now_datetime(), update_modified=False) + frappe.db.commit() + + def get_queue_name(self): + return 'long' if ('Long' in self.frequency) else 'default' + + def on_trash(self): + frappe.db.sql('delete from `tabScheduled Job Log` where scheduled_job_type=%s', self.name) + +@frappe.whitelist() +def execute_event(doc): + frappe.only_for('System Manager') + doc = json.loads(doc) + frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue() + +def run_scheduled_job(job_type): + '''This is a wrapper function that runs a hooks.scheduler_events method''' + try: + frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute() + except Exception: + print(frappe.get_traceback()) + +def sync_jobs(): + frappe.reload_doc('core', 'doctype', 'scheduled_job_type') + all_events = [] + scheduler_events = frappe.get_hooks("scheduler_events") + insert_events(all_events, scheduler_events) + clear_events(all_events, scheduler_events) + +def insert_events(all_events, scheduler_events): + for event_type in scheduler_events: + events = scheduler_events.get(event_type) + if isinstance(events, dict): + insert_cron_event(events, all_events) + else: + # hourly, daily etc + insert_event_list(events, event_type, all_events) + +def insert_cron_event(events, all_events): + for cron_format in events: + for event in events.get(cron_format): + all_events.append(event) + insert_single_event('Cron', event, cron_format) + +def insert_event_list(events, event_type, all_events): + for event in events: + all_events.append(event) + frequency = event_type.replace('_', ' ').title() + insert_single_event(frequency, event) + +def insert_single_event(frequency, event, cron_format = None): + if not frappe.db.exists('Scheduled Job Type', dict(method=event)): + frappe.get_doc(dict( + doctype = 'Scheduled Job Type', + method = event, + cron_format = cron_format, + frequency = frequency + )).insert() + +def clear_events(all_events, scheduler_events): + for event in frappe.get_all('Scheduled Job Type', ('name', 'method')): + if event.method not in all_events: + frappe.delete_doc('Scheduled Job Type', event.name) diff --git a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py new file mode 100644 index 0000000000..ec1e70ad6a --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from frappe.utils import get_datetime + +from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs + +class TestScheduledJobType(unittest.TestCase): + def setUp(self): + if not frappe.get_all('Scheduled Job Type', limit=1): + frappe.db.rollback() + frappe.db.sql('truncate `tabScheduled Job Type`') + sync_jobs() + frappe.db.commit() + + def test_sync_jobs(self): + all_job = frappe.get_doc('Scheduled Job Type', + dict(method='frappe.email.queue.flush')) + self.assertEqual(all_job.frequency, 'All') + + daily_job = frappe.get_doc('Scheduled Job Type', + dict(method='frappe.email.queue.clear_outbox')) + self.assertEqual(daily_job.frequency, 'Daily') + + # check if cron jobs are synced + cron_job = frappe.get_doc('Scheduled Job Type', + dict(method='frappe.oauth.delete_oauth2_data')) + self.assertEqual(cron_job.frequency, 'Cron') + self.assertEqual(cron_job.cron_format, '0/15 * * * *') + + def test_daily_job(self): + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.clear_outbox')) + job.db_set('last_execution', '2019-01-01 00:00:00') + self.assertTrue(job.is_event_due(get_datetime('2019-01-02 00:00:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:00:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-01 23:59:59'))) + + def test_weekly_job(self): + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.social.doctype.energy_point_log.energy_point_log.send_weekly_summary')) + job.db_set('last_execution', '2019-01-01 00:00:00') + self.assertTrue(job.is_event_due(get_datetime('2019-01-06 00:00:01'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-02 00:00:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-05 23:59:59'))) + + def test_monthly_job(self): + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.doctype.auto_email_report.auto_email_report.send_monthly')) + job.db_set('last_execution', '2019-01-01 00:00:00') + self.assertTrue(job.is_event_due(get_datetime('2019-02-01 00:00:01'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-15 00:00:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-31 23:59:59'))) + + def test_cron_job(self): + # runs every 15 mins + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.oauth.delete_oauth2_data')) + job.db_set('last_execution', '2019-01-01 00:00:00') + self.assertTrue(job.is_event_due(get_datetime('2019-01-01 00:15:01'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:05:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:14:59'))) diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 82fff31394..36c297cc26 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "Prompt", "creation": "2019-09-30 11:56:57.943241", "doctype": "DocType", @@ -43,7 +44,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete" + "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" }, { "depends_on": "eval:doc.script_type==='API'", @@ -73,7 +74,8 @@ "fieldtype": "Section Break" } ], - "modified": "2019-10-09 15:08:40.085059", + "links": [], + "modified": "2019-12-17 12:55:07.389775", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 0399dea106..2e1a5ae8bb 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -14,6 +14,8 @@ EVENT_MAP = { 'on_cancel': 'After Cancel', 'on_trash': 'Before Delete', 'after_delete': 'After Delete', + 'before_update_after_submit': 'Before Save (Submitted Document)', + 'on_update_after_submit': 'After Save (Submitted Document)' } def run_server_script_api(method): @@ -56,8 +58,10 @@ def get_server_script_map(): script_map = frappe.cache().get_value('server_script_map') if script_map is None: script_map = {} - for script in frappe.get_all('Server Script', ('name', 'reference_doctype', 'doctype_event', - 'api_method', 'script_type')): + enabled_server_scripts = frappe.get_all('Server Script', + fields=('name', 'reference_doctype', 'doctype_event','api_method', 'script_type'), + filters={'disabled': 0}) + for script in enabled_server_scripts: if script.script_type == 'DocType Event': script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name) else: diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 55bc5d49db..41a5a377db 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2014-04-17 16:53:52.640856", "doctype": "DocType", "document_type": "System", @@ -13,6 +14,7 @@ "setup_complete", "date_and_number_format", "date_format", + "time_format", "column_break_7", "number_format", "float_precision", @@ -21,7 +23,7 @@ "backup_limit", "background_workers", "enable_scheduler", - "scheduler_last_event", + "dormant_days", "permissions", "apply_strict_user_permissions", "column_break_21", @@ -117,6 +119,14 @@ "options": "yyyy-mm-dd\ndd-mm-yyyy\ndd/mm/yyyy\ndd.mm.yyyy\nmm/dd/yyyy\nmm-dd-yyyy", "reqd": 1 }, + { + "default": "HH:mm:ss", + "fieldname": "time_format", + "fieldtype": "Select", + "label": "Time Format", + "options": "HH:mm:ss\nHH:mm", + "reqd": 1 + }, { "fieldname": "column_break_7", "fieldtype": "Column Break" @@ -168,13 +178,6 @@ "hidden": 1, "label": "Enable Scheduled Jobs" }, - { - "fieldname": "scheduler_last_event", - "fieldtype": "Data", - "hidden": 1, - "label": "Scheduler Last Event", - "report_hide": 1 - }, { "collapsible": 1, "fieldname": "permissions", @@ -397,11 +400,18 @@ "fieldname": "allow_guests_to_upload_files", "fieldtype": "Check", "label": "Allow Guests to Upload Files" + }, + { + "default": "4", + "description": "Will run scheduled jobs only once a day for inactive sites. Default 4 days if set to 0.", + "fieldname": "dormant_days", + "fieldtype": "Int", + "label": "Run Jobs only Daily if Inactive For (Days)" } ], "icon": "fa fa-cog", "issingle": 1, - "modified": "2019-08-16 08:26:45.936626", + "modified": "2019-09-24 10:04:28.807388", "modified_by": "Administrator", "module": "Core", "name": "System Settings", @@ -419,4 +429,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/test_runner/_test_test_runner.js b/frappe/core/doctype/test_runner/_test_test_runner.js deleted file mode 100644 index 0b0bd9a98b..0000000000 --- a/frappe/core/doctype/test_runner/_test_test_runner.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: Test Runner", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('Test Runner', [ - // insert a new Test Runner - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/frappe/core/doctype/test_runner/test_runner.js b/frappe/core/doctype/test_runner/test_runner.js deleted file mode 100644 index d08a3626a3..0000000000 --- a/frappe/core/doctype/test_runner/test_runner.js +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Test Runner', { - refresh: (frm) => { - frm.disable_save(); - frm.page.set_primary_action(__("Run Tests"), () => { - return new Promise(resolve => { - let wrapper = $(frm.fields_dict.output.wrapper).empty(); - $("

Loading...

").appendTo(wrapper); - - // all tests - frappe.call({ - method: 'frappe.core.doctype.test_runner.test_runner.get_test_js', - args: { test_path: frm.doc.module_path } - }).always((data) => { - $("
").appendTo(wrapper.empty()); - frm.events.run_tests(frm, data.message); - resolve(); - }); - }); - }); - - }, - run_tests: function(frm, files) { - frappe.flags.in_test = true; - let require_list = [ - "assets/frappe/js/lib/jquery/qunit.js", - "assets/frappe/js/lib/jquery/qunit.css" - ].concat(); - - frappe.require(require_list, () => { - files.forEach((f) => { - frappe.dom.eval(f.script); - }); - - QUnit.config.notrycatch = true; - - window.onerror = function(msg, url, lineNo, columnNo, error) { - console.log(error.stack); // eslint-disable-line - $('
').appendTo($('body')); - }; - - QUnit.testDone(function(details) { - // var result = { - // "Module name": details.module, - // "Test name": details.name, - // "Assertions": { - // "Total": details.total, - // "Passed": details.passed, - // "Failed": details.failed - // }, - // "Skipped": details.skipped, - // "Todo": details.todo, - // "Runtime": details.runtime - // }; - - // eslint-disable-next-line - // console.log(JSON.stringify(result, null, 2)); - - details.assertions.map(a => { - // eslint-disable-next-line - console.log(`${a.result ? '✔' : '✗'} ${a.message}`); - }); - - }); - QUnit.load(); - - QUnit.done(({ total, failed, passed, runtime }) => { - // flag for selenium that test is done - - console.log( `Total: ${total}, Failed: ${failed}, Passed: ${passed}, Runtime: ${runtime}` ); // eslint-disable-line - - if(failed) { - console.log('Tests Failed'); // eslint-disable-line - } else { - console.log('Tests Passed'); // eslint-disable-line - } - frappe.set_route('Form', 'Test Runner', 'Test Runner'); - - $('
').appendTo($('body')); - - }); - }); - - } -}); diff --git a/frappe/core/doctype/test_runner/test_runner.json b/frappe/core/doctype/test_runner/test_runner.json deleted file mode 100644 index ccc1361dc9..0000000000 --- a/frappe/core/doctype/test_runner/test_runner.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-06-26 10:57:19.976624", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "module_path", - "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": "Module Path", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "app", - "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": "App", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "output", - "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": "Output", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2017-07-19 03:22:33.221169", - "modified_by": "Administrator", - "module": "Core", - "name": "Test Runner", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "Administrator", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/frappe/core/doctype/test_runner/test_runner.py b/frappe/core/doctype/test_runner/test_runner.py deleted file mode 100644 index 2961e9f38b..0000000000 --- a/frappe/core/doctype/test_runner/test_runner.py +++ /dev/null @@ -1,36 +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, os -from frappe.model.document import Document - -class TestRunner(Document): - pass - -@frappe.whitelist() -def get_test_js(test_path=None): - '''Get test + data for app, example: app/tests/ui/test_name.js''' - if not test_path: - test_path = frappe.db.get_single_value('Test Runner', 'module_path') - test_js = [] - - # split - app, test_path = test_path.split(os.path.sep, 1) - - # now full path - test_path = frappe.get_app_path(app, test_path) - - def add_file(path): - with open(path, 'r') as fileobj: - test_js.append(dict( - script = fileobj.read() - )) - - # add test_lib.js - add_file(frappe.get_app_path('frappe', 'tests', 'ui', 'data', 'test_lib.js')) - add_file(test_path) - - return test_js - diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 0bd7437ae4..5dfa3b0ace 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -8,6 +8,7 @@ from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_ from frappe import throw, msgprint, _ from frappe.utils.password import update_password as _update_password from frappe.desk.notifications import clear_notifications +from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings from frappe.utils.user import get_system_managers from bs4 import BeautifulSoup import frappe.permissions @@ -46,6 +47,9 @@ class User(Document): self.flags.in_insert = True throttle_user_creation() + def after_insert(self): + create_notification_settings(self.name) + def validate(self): self.check_demo() @@ -93,7 +97,9 @@ class User(Document): self.share_with_self() clear_notifications(user=self.name) frappe.clear_cache(user=self.name) - self.send_password_notification(self.__new_password) + if self.__new_password: + self.send_password_notification(self.__new_password) + self.reset_password_key = '' create_contact(self, ignore_mandatory=True) if self.name not in ('Administrator', 'Guest') and not self.user_image: frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name) @@ -362,7 +368,10 @@ class User(Document): (tab, field, '%s', field, '%s'), (new_name, old_name)) if frappe.db.exists("Chat Profile", old_name): - frappe.rename_doc("Chat Profile", old_name, new_name, force=True) + frappe.rename_doc("Chat Profile", old_name, new_name, force=True, show_alert=False) + + if frappe.db.exists("Notification Settings", old_name): + frappe.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False) # set email frappe.db.sql("""UPDATE `tabUser` @@ -1064,4 +1073,4 @@ def generate_keys(user): user_details.save() return {"api_secret": api_secret} - frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) \ No newline at end of file + frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) diff --git a/frappe/core/doctype/version/version.css b/frappe/core/doctype/version/version.css index 987204ed9b..769b352585 100644 --- a/frappe/core/doctype/version/version.css +++ b/frappe/core/doctype/version/version.css @@ -1,3 +1,7 @@ +.version-info { + overflow: auto; +} + .version-info pre { border: 0px; margin: 0px; @@ -14,4 +18,4 @@ .version-info .danger { background-color: #f2dede !important; -} \ No newline at end of file +} diff --git a/frappe/core/page/background_jobs/background_jobs.html b/frappe/core/page/background_jobs/background_jobs.html index c5d598ccd3..08177ecf8a 100644 --- a/frappe/core/page/background_jobs/background_jobs.html +++ b/frappe/core/page/background_jobs/background_jobs.html @@ -11,7 +11,7 @@ {% for j in jobs %} - {{ j.queue.split(".").slice(-1)[0] }} + {{ j.queue.split(".").slice(-1)[0] }}
{{ frappe.utils.encode_tags(j.job_name) }} diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py index 10cb7b97ac..c8a2352968 100644 --- a/frappe/core/page/background_jobs/background_jobs.py +++ b/frappe/core/page/background_jobs/background_jobs.py @@ -29,9 +29,9 @@ def get_info(show_failed=False): jobs.append({ 'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \ or str(j.kwargs.get('job_name')), - 'status': j.status, 'queue': name, + 'status': j.get_status(), 'queue': name, 'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)), - 'color': colors[j.status] + 'color': colors[j.get_status()] }) if j.exc_info: jobs[-1]['exc_info'] = j.exc_info diff --git a/frappe/core/page/dashboard/dashboard.py b/frappe/core/page/dashboard/dashboard.py index ebe8ce2c16..1f4eaee783 100644 --- a/frappe/core/page/dashboard/dashboard.py +++ b/frappe/core/page/dashboard/dashboard.py @@ -36,13 +36,15 @@ def generate_and_cache_results(chart, chart_name, function, cache_key): def get_from_date_from_timespan(to_date, timespan): days = months = years = 0 - if "Last Week" == timespan: + if timespan == "Last Week": days = -7 - if "Last Month" == timespan: + if timespan == "Last Month": months = -1 - elif "Last Quarter" == timespan: + elif timespan == "Last Quarter": months = -3 - elif "Last Year" == timespan: + elif timespan == "Last Year": years = -1 + elif timespan == "All Time": + years = -50 return add_to_date(to_date, years=years, months=months, days=days, as_datetime=True) diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index 9b60ea2b11..22295ce833 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "creation": "2013-01-10 16:34:01", "description": "Adds a custom field to a DocType", @@ -24,10 +25,8 @@ "collapsible_depends_on", "default", "depends_on", - "description", - "permlevel", - "width", - "columns", + "mandatory_depends_on", + "read_only_depends_on", "properties", "reqd", "unique", @@ -46,7 +45,11 @@ "report_hide", "search_index", "ignore_xss_filter", - "translatable" + "translatable", + "description", + "permlevel", + "width", + "columns" ], "fields": [ { @@ -349,11 +352,24 @@ "fieldname": "length", "fieldtype": "Int", "label": "Length" + }, + { + "fieldname": "mandatory_depends_on", + "fieldtype": "Code", + "label": "Mandatory Depends On", + "length": 255 + }, + { + "fieldname": "read_only_depends_on", + "fieldtype": "Code", + "label": "Read Only Depends On", + "length": 255 } ], "icon": "fa fa-glass", "idx": 1, - "modified": "2019-09-11 12:57:19.268934", + "links": [], + "modified": "2019-12-12 21:31:08.209996", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index b851d40b83..8d47a075ba 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -59,6 +59,8 @@ docfield_properties = { 'report_hide': 'Check', 'allow_on_submit': 'Check', 'translatable': 'Check', + 'mandatory_depends_on': 'Data', + 'read_only_depends_on': 'Data', 'depends_on': 'Data', 'description': 'Text', 'default': 'Text', @@ -68,7 +70,8 @@ docfield_properties = { 'columns': 'Int', 'remember_last_selected_value': 'Check', 'allow_bulk_edit': 'Check', - 'auto_repeat': 'Link' + 'auto_repeat': 'Link', + 'allow_in_quick_entry': 'Check' } allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'), diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 1f808c94c5..ab582851b5 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -1,1438 +1,396 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "hash", - "beta": 0, "creation": "2013-02-22 01:27:32", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label_and_type", + "label", + "fieldtype", + "fieldname", + "reqd", + "unique", + "in_list_view", + "in_standard_filter", + "in_global_search", + "bold", + "allow_in_quick_entry", + "translatable", + "column_break_7", + "precision", + "length", + "options", + "fetch_from", + "fetch_if_empty", + "permissions", + "depends_on", + "permlevel", + "hidden", + "read_only", + "collapsible", + "allow_bulk_edit", + "collapsible_depends_on", + "column_break_14", + "ignore_user_permissions", + "allow_on_submit", + "report_hide", + "remember_last_selected_value", + "property_depends_on_section", + "mandatory_depends_on", + "column_break_33", + "read_only_depends_on", + "display", + "default", + "in_filter", + "column_break_21", + "description", + "print_hide", + "print_hide_if_no_value", + "print_width", + "columns", + "width", + "is_custom_field" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "label_and_type", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Label and Type", - "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": "Label and Type" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "label", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Label", - "length": 0, - "no_copy": 0, "oldfieldname": "label", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Data", - "fetch_if_empty": 0, "fieldname": "fieldtype", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Type", - "length": 0, - "no_copy": 0, "oldfieldname": "fieldtype", "oldfieldtype": "Select", "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime", - "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, - "fetch_if_empty": 0, "fieldname": "fieldname", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Name", - "length": 0, - "no_copy": 0, "oldfieldname": "fieldname", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "reqd", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Mandatory", - "length": 0, - "no_copy": 0, "oldfieldname": "reqd", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "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": "unique", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Unique", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Unique" }, { - "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": "in_list_view", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "In List View", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "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": "In List View" }, { - "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": "in_standard_filter", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "In Standard Filter", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "In Standard Filter" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", - "fetch_if_empty": 0, "fieldname": "in_global_search", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "In Global Search", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "In Global Search" }, { - "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": "bold", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Bold", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Bold" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", - "fetch_if_empty": 0, "fieldname": "translatable", "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": "Translatable", - "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": "Translatable" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_7", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "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, "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", "description": "Set non-standard precision for a Float or Currency field", - "fetch_if_empty": 0, "fieldname": "precision", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Precision", - "length": 0, - "no_copy": 0, - "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9", - "permlevel": 0, - "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": "\n1\n2\n3\n4\n5\n6\n7\n8\n9" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)", - "fetch_if_empty": 0, "fieldname": "length", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Length", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Length" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.", - "fetch_if_empty": 0, "fieldname": "options", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Options", - "length": 0, - "no_copy": 0, "oldfieldname": "options", - "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Text" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "fetch_from", "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": "Fetch From", - "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": "Fetch From" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.", - "fetch_if_empty": 0, "fieldname": "fetch_if_empty", "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": "Fetch If Empty", - "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": "Fetch If Empty" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "permissions", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Permissions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "Permissions" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age>18", - "fetch_if_empty": 0, "fieldname": "depends_on", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Depends On", - "length": 0, - "no_copy": 0, "oldfieldname": "depends_on", "oldfieldtype": "Data", - "options": "JS", - "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": "JS" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", - "fetch_if_empty": 0, "fieldname": "permlevel", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Perm Level", - "length": 0, - "no_copy": 0, "oldfieldname": "permlevel", - "oldfieldtype": "Int", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "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": "Int" }, { - "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": "hidden", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Hidden", - "length": 0, - "no_copy": 0, "oldfieldname": "hidden", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "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": "read_only", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Read Only", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "Read Only" }, { - "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.fieldtype==\"Section Break\"", - "fetch_if_empty": 0, "fieldname": "collapsible", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Collapsible", - "length": 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": "Collapsible" }, { - "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.fieldtype == \"Table\"", - "fetch_if_empty": 0, "fieldname": "allow_bulk_edit", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Allow Bulk Edit", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Allow Bulk Edit" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.fieldtype==\"Section Break\"", - "fetch_if_empty": 0, "fieldname": "collapsible_depends_on", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Collapsible Depends On", - "length": 0, - "no_copy": 0, - "options": "JS", - "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": "JS" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_14", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "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, - "fetch_if_empty": 0, + "default": "0", "fieldname": "ignore_user_permissions", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Ignore User Permissions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Ignore User Permissions" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "allow_on_submit", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Allow on Submit", - "length": 0, - "no_copy": 0, "oldfieldname": "allow_on_submit", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "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": "Check" }, { - "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": "report_hide", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Report Hide", - "length": 0, - "no_copy": 0, "oldfieldname": "report_hide", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "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": "Check" }, { - "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.fieldtype == 'Link')", - "fetch_if_empty": 0, "fieldname": "remember_last_selected_value", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Remember Last Selected Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Remember Last Selected Value" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "display", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Display", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "Display" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "default", "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": "Default", - "length": 0, - "no_copy": 0, "oldfieldname": "default", - "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Text" }, { - "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": "in_filter", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "In Filter", - "length": 0, - "no_copy": 0, "oldfieldname": "in_filter", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_21", - "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, - "fetch_if_empty": 0, "fieldname": "description", "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": "Description", - "length": 0, - "no_copy": 0, "oldfieldname": "description", "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "300px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "300px" }, { - "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": "print_hide", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Print Hide", - "length": 0, - "no_copy": 0, "oldfieldname": "print_hide", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "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": "Check" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", - "fetch_if_empty": 0, "fieldname": "print_hide_if_no_value", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Print Hide If No Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Print Hide If No Value" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Print Width of the field, if the field is a column in a table", - "fetch_if_empty": 0, "fieldname": "print_width", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Print Width", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "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": "50px" }, { - "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.istable", "description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)", - "fetch_if_empty": 0, "fieldname": "columns", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Columns", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Columns" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "width", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Width", - "length": 0, - "no_copy": 0, "oldfieldname": "width", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50px" }, { - "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": "is_custom_field", "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": "Is Custom Field", - "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 + }, + { + "default": "0", + "fieldname": "allow_in_quick_entry", + "fieldtype": "Check", + "label": " Allow in Quick Entry " + }, + { + "fieldname": "property_depends_on_section", + "fieldtype": "Section Break", + "label": "Property Depends On" + }, + { + "fieldname": "mandatory_depends_on", + "fieldtype": "Code", + "label": "Mandatory Depends On", + "options": "JS" + }, + { + "fieldname": "column_break_33", + "fieldtype": "Column Break" + }, + { + "fieldname": "read_only_depends_on", + "fieldtype": "Code", + "label": "Read Only Depends On", + "options": "JS" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, "istable": 1, - "max_attachments": 0, - "modified": "2019-03-18 18:03:59.122249", + "links": [], + "modified": "2019-12-27 12:50:51.419763", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", "owner": "Administrator", "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/frappe/database/database.py b/frappe/database/database.py index f5055571ff..1c08dd714e 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -15,7 +15,7 @@ import frappe.model.meta from frappe import _ from time import time -from frappe.utils import now, getdate, cast_fieldtype +from frappe.utils import now, getdate, cast_fieldtype, get_datetime from frappe.utils.background_jobs import execute_job, get_queue from frappe.model.utils.link_count import flush_local_link_count from frappe.utils import cint @@ -941,6 +941,16 @@ class Database(object): else: frappe.throw(_('No conditions provided')) + def get_last_created(self, doctype): + last_record = self.get_all(doctype, ('creation'), limit=1, order_by='creation desc') + if last_record: + return get_datetime(last_record[0].creation) + else: + return None + + def clear_table(self, doctype): + self.sql('truncate `tab{}`'.format(doctype)) + def log_touched_tables(self, query, values=None): if values: query = frappe.safe_decode(self._cursor.mogrify(query, values)) diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py index 0447f97273..80236b2dc2 100644 --- a/frappe/database/db_manager.py +++ b/frappe/database/db_manager.py @@ -80,12 +80,14 @@ class DbManager: if pipe: print('Creating Database...') - command = '{pipe} mysql -u {user} -p{password} -h{host} {target} {source}'.format( + command = '{pipe} mysql -u {user} -p{password} -h{host} ' + ('-P{port}' if frappe.db.port else '') + ' {target} {source}' + command = command.format( pipe=pipe, user=esc(user), password=esc(password), host=esc(frappe.db.host), target=esc(target), - source=source + source=source, + port=frappe.db.port ) os.system(command) diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index 7058ed0325..dbe53df4e4 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -40,6 +40,8 @@ CREATE TABLE `tabDocField` ( `show_preview_popup` int(1) NOT NULL DEFAULT 0, `trigger` varchar(255) DEFAULT NULL, `collapsible_depends_on` text, + `mandatory_depends_on` text, + `read_only_depends_on` text, `depends_on` text, `permlevel` int(11) NOT NULL DEFAULT 0, `ignore_user_permissions` int(1) NOT NULL DEFAULT 0, @@ -105,6 +107,53 @@ CREATE TABLE `tabDocPerm` ( KEY `parent` (`parent`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- +-- Table structure for table `tabDocType Action` +-- + +CREATE TABLE `tabDocType Action` ( + `name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `idx` int(8) NOT NULL DEFAULT 0, + `label` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `action_type` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `action` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`name`), + KEY `parent` (`parent`), + KEY `modified` (`modified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED; + +-- +-- Table structure for table `tabDocType Action` +-- + +CREATE TABLE `tabDocType Link` ( + `name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `idx` int(8) NOT NULL DEFAULT 0, + `group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `link_doctype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `link_fieldname` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`name`), + KEY `parent` (`parent`), + KEY `modified` (`modified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED; + -- -- Table structure for table `tabDocType` -- diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index abacc5ab4c..243d0f934e 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -107,7 +107,7 @@ class PostgresDatabase(Database): from information_schema.tables where table_catalog='{0}' and table_type = 'BASE TABLE' - and table_schema='public'""".format(frappe.conf.db_name))] + and table_schema='{1}'""".format(frappe.conf.db_name, frappe.conf.get("db_schema", "public")))] def format_date(self, date): if not date: diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index df59de92df..457f6c906a 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -40,6 +40,8 @@ CREATE TABLE "tabDocField" ( "show_preview_popup" smallint NOT NULL DEFAULT 0, "trigger" varchar(255) DEFAULT NULL, "collapsible_depends_on" text, + "mandatory_depends_on" text, + "read_only_depends_on" text, "depends_on" text, "permlevel" bigint NOT NULL DEFAULT 0, "ignore_user_permissions" smallint NOT NULL DEFAULT 0, @@ -106,6 +108,57 @@ CREATE TABLE "tabDocPerm" ( create index on "tabDocPerm" ("parent"); +-- +-- Table structure for table "tabDocType Action" +-- + +DROP TABLE IF EXISTS "tabDocType Action"; +CREATE TABLE "tabDocType Action" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "parent" varchar(255) DEFAULT NULL, + "parentfield" varchar(255) DEFAULT NULL, + "parenttype" varchar(255) DEFAULT NULL, + "idx" bigint NOT NULL DEFAULT 0, + "label" varchar(140) NOT NULL, + "group" varchar(140) DEFAULT NULL, + "action_type" varchar(140) NOT NULL, + "action" varchar(140) NOT NULL, + PRIMARY KEY ("name") +) ; + +create index on "tabDocType Action" ("parent"); + +-- +-- Table structure for table "tabDocType Link" +-- + +DROP TABLE IF EXISTS "tabDocType Link"; +CREATE TABLE "tabDocType Link" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "parent" varchar(255) DEFAULT NULL, + "parentfield" varchar(255) DEFAULT NULL, + "parenttype" varchar(255) DEFAULT NULL, + "idx" bigint NOT NULL DEFAULT 0, + "label" varchar(140) DEFAULT NULL, + "group" varchar(140) DEFAULT NULL, + "link_doctype" varchar(140) NOT NULL, + "link_fieldname" varchar(140) NOT NULL, + PRIMARY KEY ("name") +) ; + +create index on "tabDocType Link" ("parent"); + + -- -- Table structure for table "tabDocType" -- diff --git a/frappe/desk/doctype/bulk_update/bulk_update.js b/frappe/desk/doctype/bulk_update/bulk_update.js index 6f2a9c3dde..bb9cf2af51 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.js +++ b/frappe/desk/doctype/bulk_update/bulk_update.js @@ -3,6 +3,15 @@ frappe.ui.form.on('Bulk Update', { refresh: function(frm) { + frm.set_query("document_type", function() { + return { + filters: [ + ['DocType', 'issingle', '=', 0], + ['DocType', 'name', 'not in', frappe.model.core_doctypes_list] + ] + }; + }); + frm.page.set_primary_action(__('Update'), function() { if (!frm.doc.update_value) { frappe.throw(__('Field "value" is mandatory. Please specify value to be updated')); diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index 99ba49bc4f..a330f7e97e 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -41,6 +41,7 @@ frappe.ui.form.on('Dashboard Chart', { timespan: function(frm) { const time_interval_options = { "Select Date Range": ["Quarterly", "Monthly", "Weekly", "Daily"], + "All Time": ["Yearly", "Monthly"], "Last Year": ["Quarterly", "Monthly", "Weekly", "Daily"], "Last Quarter": ["Monthly", "Weekly", "Daily"], "Last Month": ["Weekly", "Daily"], diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index 7e99b86eae..a5f9cead00 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -82,14 +82,14 @@ "fieldname": "timespan", "fieldtype": "Select", "label": "Timespan", - "options": "Last Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range" + "options": "All Time\nLast Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range" }, { "depends_on": "timeseries", "fieldname": "time_interval", "fieldtype": "Select", "label": "Time Interval", - "options": "Quarterly\nMonthly\nWeekly\nDaily" + "options": "Yearly\nQuarterly\nMonthly\nWeekly\nDaily" }, { "default": "0", @@ -187,7 +187,7 @@ "label": "To Date" } ], - "modified": "2019-11-04 12:32:14.525409", + "modified": "2019-11-18 16:20:11.529496", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index e7ec3f8bab..6dec12efc5 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -74,8 +74,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): result = convert_to_dates(data, timegrain) # add missing data points for periods where there was no result - result = add_missing_values(result, timegrain, from_date, to_date) - + result = add_missing_values(result, timegrain, timespan, from_date, to_date) chart_config = { "labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result], "datasets": [{ @@ -133,7 +132,9 @@ def get_aggregate_function(chart_type): "Average": "AVG", }[chart_type] + def convert_to_dates(data, timegrain): + """ Converts individual dates within data to the end of period """ result = [] for d in data: if timegrain == 'Daily': @@ -141,10 +142,11 @@ def convert_to_dates(data, timegrain): elif timegrain == 'Weekly': result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), weeks = d[1] + 1), days = -1), d[2]]) elif timegrain == 'Monthly': - result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months = d[1]), days = -1), d[2]]) + result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1]), days = -1), d[2]]) elif timegrain == 'Quarterly': - result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months = d[1] * 3), days = -1), d[2]]) - + result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1] * 3), days = -1), d[2]]) + elif timegrain == 'Yearly': + result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=12), days = -1), d[2]]) result[-1][0] = getdate(result[-1][0]) return result @@ -164,17 +166,17 @@ def get_unit_function(datefield, timegrain): return unit_function -def add_missing_values(data, timegrain, from_date, to_date): +def add_missing_values(data, timegrain, timespan, from_date, to_date): # add missing intervals result = [] - first_expected_date = get_period_ending(from_date, timegrain) - - # fill out data before the first data point - first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1)) - while first_data_point_date > first_expected_date: - result.append([first_expected_date, 0.0]) - first_expected_date = get_next_expected_date(first_expected_date, timegrain) + if timespan != 'All Time': + first_expected_date = get_period_ending(from_date, timegrain) + # fill out data before the first data point + first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1)) + while first_data_point_date > first_expected_date: + result.append([first_expected_date, 0.0]) + first_expected_date = get_next_expected_date(first_expected_date, timegrain) # fill data points and missing points for i, d in enumerate(data): @@ -212,14 +214,16 @@ def get_next_expected_date(date, timegrain): def get_period_ending(date, timegrain): date = getdate(date) - if timegrain=='Daily': + if timegrain == 'Daily': pass - elif timegrain=='Weekly': + elif timegrain == 'Weekly': date = get_week_ending(date) - elif timegrain=='Monthly': + elif timegrain == 'Monthly': date = get_month_ending(date) - elif timegrain=='Quarterly': + elif timegrain == 'Quarterly': date = get_quarter_ending(date) + elif timegrain == 'Yearly': + date = get_year_ending(date) return getdate(date) @@ -231,7 +235,7 @@ def get_week_ending(date): # first day of next week date = add_to_date('{}-01-01'.format(date.year), weeks = week_of_the_year + 1) # last day of this week - return add_to_date(date, days = -1) + return add_to_date(date, days=-1) def get_month_ending(date): month_of_the_year = int(date.strftime('%m')) @@ -239,7 +243,7 @@ def get_month_ending(date): date = add_to_date('{}-01-01'.format(date.year), months = month_of_the_year) # last day of this month - return add_to_date(date, days = -1) + return add_to_date(date, days=-1) def get_quarter_ending(date): date = getdate(date) @@ -255,8 +259,17 @@ def get_quarter_ending(date): return date +def get_year_ending(date): + ''' returns year ending of the given date ''' + + # first day of next year (note year starts from 1) + date = add_to_date('{}-01-01'.format(date.year), months = 12) + # last day of this month + return add_to_date(date, days=-1) + class DashboardChart(Document): + def on_update(self): frappe.cache().delete_key('chart-data:{}'.format(self.name)) diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index fc74448d10..238d4f28e5 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -36,6 +36,9 @@ class TestDashboardChart(unittest.TestCase): self.assertEqual(get_period_ending('2019-10-01', 'Quarterly'), getdate('2019-12-31')) + self.assertEqual(get_period_ending('2019-10-01', 'Yearly'), + getdate('2019-12-31')) + def test_dashboard_chart(self): if frappe.db.exists('Dashboard Chart', 'Test Dashboard Chart'): frappe.delete_doc('Dashboard Chart', 'Test Dashboard Chart') diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py index 0729fca5cb..85c9687ab3 100644 --- a/frappe/desk/doctype/global_search_settings/global_search_settings.py +++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py @@ -29,12 +29,16 @@ class GlobalSearchSettings(Document): repeated_dts = (", ".join([frappe.bold(dt) for dt in repeated_dts])) frappe.throw(_("Document Type {0} has been repeated.").format(repeated_dts)) -def get_doctypes_for_global_search(): - doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC") - if not doctypes: - return [] + # reset cache + frappe.cache().hdel('global_search', 'search_priorities') + +def get_doctypes_for_global_search(): + def get_from_db(): + doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC") + return [d.document_type for d in doctypes] or [] + + return frappe.cache().hget("global_search", "search_priorities", get_from_db) - return [d.document_type for d in doctypes] @frappe.whitelist() def reset_global_search_settings_doctypes(): @@ -57,7 +61,7 @@ def update_global_search_doctypes(): if search_doctypes.get(domain): global_search_doctypes.extend(search_doctypes.get(domain)) - doctype_list = set([dt.name for dt in frappe.get_list("DocType")]) + doctype_list = set([dt.name for dt in frappe.get_all("DocType")]) allowed_in_global_search = [] for dt in global_search_doctypes: diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json index 32b66ef1ea..ecb746df64 100644 --- a/frappe/desk/doctype/notification_log/notification_log.json +++ b/frappe/desk/doctype/notification_log/notification_log.json @@ -10,7 +10,7 @@ "email_content", "column_break_4", "document_type", - "seen", + "read", "document_name", "from_user" ], @@ -57,14 +57,6 @@ "read_only": 1, "search_index": 1 }, - { - "default": "0", - "fieldname": "seen", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 1, - "label": "Seen" - }, { "fieldname": "document_name", "fieldtype": "Data", @@ -79,11 +71,19 @@ "options": "User", "read_only": 1, "search_index": 1 + }, + { + "default": "0", + "fieldname": "read", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 1, + "label": "Read" } ], "in_create": 1, - "modified": "2019-10-23 12:48:01.119356", - "modified_by": "Administrator", + "modified": "2019-11-12 15:22:35.283678", + "modified_by": "umair@erpnext.com", "module": "Desk", "name": "Notification Log", "owner": "Administrator", diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index f58c14d363..398a3de351 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -7,11 +7,12 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.desk.doctype.notification_settings.notification_settings import (is_notifications_enabled, - is_email_notifications_enabled, is_email_notifications_enabled_for_type) + is_email_notifications_enabled, is_email_notifications_enabled_for_type, set_seen_value) class NotificationLog(Document): def after_insert(self): frappe.publish_realtime('notification', after_commit=True, user=self.for_user) + set_notifications_as_unseen(self.for_user) if is_email_notifications_enabled(self.for_user): send_notification_email(self) @@ -41,7 +42,6 @@ def enqueue_create_notification(users, doc): This breaks new site creation if Redis server is not running. We do not need any notifications in fresh installation ''' - if frappe.flags.in_install: return @@ -64,13 +64,13 @@ def make_notification_logs(doc, users): if is_notifications_enabled(user): if doc.type == 'Energy Point' and not is_energy_point_enabled(): return - else: - _doc = frappe.new_doc('Notification Log') - _doc.update(doc) - _doc.for_user = user - _doc.subject = _doc.subject.replace('
', '').replace('
', '') - if _doc.for_user != _doc.from_user or doc.type == 'Energy Point': - _doc.insert(ignore_permissions=True) + + _doc = frappe.new_doc('Notification Log') + _doc.update(doc) + _doc.for_user = user + _doc.subject = _doc.subject.replace('
', '').replace('
', '') + if _doc.for_user != _doc.from_user or doc.type == 'Energy Point': + _doc.insert(ignore_permissions=True) def send_notification_email(doc): is_type_enabled = is_email_notifications_enabled_for_type(doc.for_user, doc.type) @@ -112,11 +112,25 @@ def get_email_header(doc): @frappe.whitelist() -def mark_as_seen(docname): - if docname: - frappe.db.set_value('Notification Log', docname, 'seen', 1, update_modified=False) +def mark_all_as_read(): + unread_docs_list = frappe.db.get_all('Notification Log', filters = {'read': 0, 'for_user': frappe.session.user}) + unread_docnames = [doc.name for doc in unread_docs_list] + if unread_docnames: + filters = {'name': ['in', unread_docnames]} + frappe.db.set_value('Notification Log', filters, 'read', 1, update_modified=False) +@frappe.whitelist() +def mark_as_read(docname): + if docname: + frappe.db.set_value('Notification Log', docname, 'read', 1, update_modified=False) + @frappe.whitelist() def trigger_indicator_hide(): frappe.publish_realtime('indicator_hide', user=frappe.session.user) + +def set_notifications_as_unseen(user): + try: + frappe.db.set_value('Notification Settings', user, 'seen', 0) + except frappe.DoesNotExistError: + return diff --git a/frappe/desk/doctype/notification_settings/notification_settings.js b/frappe/desk/doctype/notification_settings/notification_settings.js new file mode 100644 index 0000000000..d4e3b08def --- /dev/null +++ b/frappe/desk/doctype/notification_settings/notification_settings.js @@ -0,0 +1,12 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Notification Settings', { + onload: () => { + frappe.breadcrumbs.add({ + label: __('Settings'), + route: '#modules/Settings', + type: 'Custom' + }); + } +}); diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json index 68eec92125..6af325507b 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.json +++ b/frappe/desk/doctype/notification_settings/notification_settings.json @@ -13,7 +13,8 @@ "enable_email_assignment", "enable_email_energy_point", "enable_email_share", - "user" + "user", + "seen" ], "fields": [ { @@ -72,14 +73,20 @@ "fieldname": "user", "fieldtype": "Link", "hidden": 1, - "in_list_view": 1, "label": "User", "options": "User", "read_only": 1 + }, + { + "default": "0", + "fieldname": "seen", + "fieldtype": "Check", + "hidden": 1, + "label": "Seen" } ], "in_create": 1, - "modified": "2019-10-23 12:42:56.175928", + "modified": "2019-11-19 12:57:59.356786", "modified_by": "Administrator", "module": "Desk", "name": "Notification Settings", diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py index 3bb3cf9320..295b4c8afd 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.py +++ b/frappe/desk/doctype/notification_settings/notification_settings.py @@ -31,12 +31,12 @@ def is_email_notifications_enabled_for_type(user, notification_type): return True return enabled -@frappe.whitelist() -def create_notification_settings(): - _doc = frappe.new_doc('Notification Settings') - _doc.name = frappe.session.user - _doc.insert(ignore_permissions=True) - frappe.db.commit() +def create_notification_settings(user): + if not frappe.db.exists("Notification Settings", user): + _doc = frappe.new_doc('Notification Settings') + _doc.name = user + _doc.insert(ignore_permissions=True) + frappe.db.commit() @frappe.whitelist() @@ -60,3 +60,7 @@ def get_permission_query_conditions(user): if not user: user = frappe.session.user return '''(`tabNotification Settings`.user = '{user}')'''.format(user=user) + +@frappe.whitelist() +def set_seen_value(value, user): + frappe.db.set_value('Notification Settings', user, 'seen', value, update_modified=False) \ No newline at end of file diff --git a/frappe/desk/doctype/onboarding_slide/__init__.py b/frappe/desk/doctype/onboarding_slide/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/onboarding_slide/onboarding_slide.js b/frappe/desk/doctype/onboarding_slide/onboarding_slide.js new file mode 100644 index 0000000000..dc91f42913 --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide/onboarding_slide.js @@ -0,0 +1,45 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Onboarding Slide', { + refresh: function(frm) { + frm.toggle_reqd('ref_doctype', (frm.doc.slide_type=='Create' || frm.doc.slide_type=='Settings')); + frm.toggle_reqd('slide_module', (frm.doc.slide_type=='Information' || frm.doc.slide_type=='Continue')); + }, + + ref_doctype: function(frm) { + frm.set_query('ref_doctype', function() { + if (frm.doc.slide_type === 'Create') { + return { + filters: { + 'issingle': 0, + 'istable': 0 + } + }; + } else if (frm.doc.slide_type === 'Settings') { + return { + filters: { + 'issingle': 1, + 'istable': 0 + } + }; + } + }); + + //fetch mandatory fields automatically + if (frm.doc.ref_doctype) { + frappe.model.clear_table(frm.doc, 'slide_fields'); + let fields = frappe.meta.get_docfields(frm.doc.ref_doctype, null, { + reqd: 1 + }); + $.each(fields, function(_i, data) { + let row = frappe.model.add_child(frm.doc, 'Onboarding Slide', 'slide_fields'); + row.label = data.label; + row.fieldtype = data.fieldtype; + row.fieldname = data.fieldname; + row.options = data.options; + }); + refresh_field('slide_fields'); + } + } +}); diff --git a/frappe/desk/doctype/onboarding_slide/onboarding_slide.json b/frappe/desk/doctype/onboarding_slide/onboarding_slide.json new file mode 100644 index 0000000000..3f6f0d719f --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide/onboarding_slide.json @@ -0,0 +1,184 @@ +{ + "autoname": "field:slide_title", + "creation": "2019-11-13 14:39:56.834658", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "slide_title", + "app", + "slide_order", + "column_break_4", + "image_src", + "slide_module", + "description_section_break", + "slide_desc", + "action_section_break", + "slide_type", + "column_break_6", + "max_count", + "add_more_button", + "section_break_18", + "ref_doctype", + "slide_fields", + "section_break_10", + "domains", + "column_break_12", + "help_links", + "is_completed" + ], + "fields": [ + { + "fieldname": "slide_title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Slide Title", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "slide_desc", + "fieldtype": "HTML Editor", + "label": "Slide Description" + }, + { + "default": "3", + "depends_on": "add_more_button", + "description": "The amount of times you want to repeat the set of fields (eg: if you want 3 customers in the slide, set this field to 3. Only the first set of fields is shown as mandatory in the slide)", + "fieldname": "max_count", + "fieldtype": "Int", + "label": "Max Count" + }, + { + "default": "0", + "depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'", + "fieldname": "add_more_button", + "fieldtype": "Check", + "label": "Add More Button" + }, + { + "depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'", + "fieldname": "slide_fields", + "fieldtype": "Table", + "label": "Slide Fields", + "options": "Onboarding Slide Field" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "description": "Specify in what all domains should the slides show up. If nothing is specified the slide is shown in all domains by default.", + "fieldname": "domains", + "fieldtype": "Table", + "label": "Domains", + "options": "Has Domain" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "description": "Add a help video link just in case user has no idea about what to fill in the slide.", + "fieldname": "help_links", + "fieldtype": "Table", + "label": "Help Links", + "options": "Onboarding Slide Help Link" + }, + { + "fieldname": "action_section_break", + "fieldtype": "Section Break", + "label": "Action Settings" + }, + { + "description": "If Slide Type is Create or Settings there should be a 'create_onboarding_docs' method in the {ref_doctype}.py file bound to be executed after the slide is completed.", + "fieldname": "slide_type", + "fieldtype": "Select", + "label": "Slide Type", + "options": "Information\nCreate\nSettings\nContinue", + "reqd": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "app", + "fieldtype": "Select", + "label": "App", + "options": "Frappe\nERPNext", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "image_src", + "fieldtype": "Data", + "label": "Slide Image Source" + }, + { + "fieldname": "description_section_break", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'", + "fieldname": "ref_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType" + }, + { + "default": "0", + "description": "Determines the order of the slide in the wizard. If the slide is not to be displayed, priority should be set to 0.", + "fieldname": "slide_order", + "fieldtype": "Int", + "label": "Slide Order" + }, + { + "depends_on": "eval:doc.slide_type=='Information' || doc.slide_type=='Continue'", + "fieldname": "slide_module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" + }, + { + "collapsible_depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'", + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "label": "Fields" + }, + { + "default": "0", + "fieldname": "is_completed", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Completed", + "print_hide": 1 + } + ], + "modified": "2019-12-04 10:50:43.528901", + "modified_by": "Administrator", + "module": "Desk", + "name": "Onboarding Slide", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/onboarding_slide/onboarding_slide.py b/frappe/desk/doctype/onboarding_slide/onboarding_slide.py new file mode 100644 index 0000000000..8c75d10b9a --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide/onboarding_slide.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from frappe import _ +from frappe.model.document import Document +from frappe.modules.export_file import export_to_files + +class OnboardingSlide(Document): + def validate(self): + if self.slide_type == 'Continue' and frappe.db.exists('Onboarding Slide', {'slide_type': 'Continue', 'name': ('!=', self.name)}): + frappe.throw(_('An Onboarding Slide of Slide Type Continue already exists.')) + + if self.slide_order: + same_order_slide = frappe.db.exists('Onboarding Slide', {'slide_order': self.slide_order, 'name': ('!=', self.name)}) + if same_order_slide: + frappe.throw(_('An Onboarding Slide {0} with the same slide order already exists').format(same_order_slide)) + + def on_update(self): + if self.ref_doctype: + module = frappe.db.get_value('DocType', self.ref_doctype, 'module') + else: + module = self.slide_module + export_to_files(record_list=[['Onboarding Slide', self.name]], record_module=module) + +def get_onboarding_slides_as_list(): + slides = [] + slide_docs = frappe.db.get_all('Onboarding Slide', + filters={'is_completed': 0}, + or_filters={'slide_order': ('!=', 0), 'slide_type': 'Continue'}, + order_by='slide_order') + + # to check if continue slide is required + first_slide = get_first_slide() + + for entry in slide_docs: + # using get_doc because child table fields are not fetched in get_all + slide_doc = frappe.get_doc('Onboarding Slide', entry.name) + if frappe.scrub(slide_doc.app) in frappe.get_installed_apps(): + slide = frappe._dict( + slide_type=slide_doc.slide_type, + title=slide_doc.slide_title, + help=slide_doc.slide_desc, + fields=slide_doc.slide_fields, + help_links=get_help_links(slide_doc), + add_more=slide_doc.add_more_button, + max_count=slide_doc.max_count, + image_src=get_slide_image(slide_doc), + ref_doctype=slide_doc.ref_doctype, + app=slide_doc.app + ) + if slide.slide_type == 'Continue': + if is_continue_slide_required(first_slide): + slides.insert(0, slide) + else: + slides.append(slide) + + return slides + +@frappe.whitelist() +def get_onboarding_slides(): + slides = [] + slide_list = get_onboarding_slides_as_list() + + active_domains = frappe.get_active_domains() + for slide in slide_list: + if not slide.domains or any(domain in active_domains for domain in slide.domains): + slides.append(slide) + return slides + +def get_help_links(slide_doc): + links=[] + for link in slide_doc.help_links: + links.append({ + 'label': link.label, + 'video_id': link.video_id + }) + return links + +def get_slide_image(slide_doc): + if slide_doc.image_src: + return slide_doc.image_src + return None + +def is_continue_slide_required(first_slide): + # check if first slide itself is not completed + if not first_slide.is_completed: + return False + + # check if there is any active slide which is not completed + return frappe.db.exists('Onboarding Slide', { + 'is_completed': 0, + 'slide_order': ('!=', 0), + 'slide_type': ('!=', 'Continue') + }) + +@frappe.whitelist() +def create_onboarding_docs(values, doctype=None, app=None, slide_type=None): + data = json.loads(values) + doc = frappe.new_doc(doctype) + if hasattr(doc, 'create_onboarding_docs'): + doc.create_onboarding_docs(data) + else: + create_generic_onboarding_doc(data, doctype, slide_type) + +def create_generic_onboarding_doc(data, doctype, slide_type): + if slide_type == 'Settings': + doc = frappe.get_single(doctype) + for entry in data: + doc.set(entry, data.get(entry)) + doc.save() + + elif slide_type == 'Create': + doc = frappe.new_doc(doctype) + for entry in data: + doc.set(entry, data.get(entry)) + doc.flags.ignore_mandatory = True + doc.flags.ignore_links = True + doc.insert() + +@frappe.whitelist() +def mark_slide_as_completed(slide_title): + frappe.db.set_value('Onboarding Slide', slide_title, 'is_completed', 1) + +def get_first_slide(): + slides = frappe.db.get_all('Onboarding Slide', + filters={'slide_order': ('!=', 0), 'slide_type': ('!=', 'Continue')}, + order_by='slide_order', + fields=['name', 'is_completed'] + ) + return slides[0] \ No newline at end of file diff --git a/frappe/desk/doctype/onboarding_slide/test_onboarding_slide.py b/frappe/desk/doctype/onboarding_slide/test_onboarding_slide.py new file mode 100644 index 0000000000..d78b9b6158 --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide/test_onboarding_slide.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 TestOnboardingSlide(unittest.TestCase): + pass diff --git a/frappe/desk/doctype/onboarding_slide_field/__init__.py b/frappe/desk/doctype/onboarding_slide_field/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.json b/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.json new file mode 100644 index 0000000000..0992aed092 --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.json @@ -0,0 +1,74 @@ +{ + "creation": "2019-11-13 13:35:08.617909", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "fieldtype", + "fieldname", + "align", + "placeholder", + "reqd", + "column_break_4", + "options" + ], + "fields": [ + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label" + }, + { + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Fieldtype", + "options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nFloat\nHTML\nInt\nRating\nSelect\nLink\nSmall Text\nText\nText Editor\nSection Break\nColumn Break" + }, + { + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Fieldname" + }, + { + "fieldname": "align", + "fieldtype": "Select", + "label": "Align", + "options": "\ncenter\nleft\nright" + }, + { + "fieldname": "placeholder", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Placeholder" + }, + { + "default": "0", + "fieldname": "reqd", + "fieldtype": "Check", + "label": "Mandatory" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "options", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Options" + } + ], + "istable": 1, + "modified": "2019-12-02 16:43:51.930018", + "modified_by": "Administrator", + "module": "Desk", + "name": "Onboarding Slide Field", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.py b/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.py new file mode 100644 index 0000000000..74b6782ff8 --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.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 OnboardingSlideField(Document): + pass diff --git a/frappe/desk/doctype/onboarding_slide_help_link/__init__.py b/frappe/desk/doctype/onboarding_slide_help_link/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.json b/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.json new file mode 100644 index 0000000000..a09ba50553 --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.json @@ -0,0 +1,35 @@ +{ + "creation": "2019-11-19 12:22:42.805741", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "video_id" + ], + "fields": [ + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label" + }, + { + "fieldname": "video_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Video" + } + ], + "istable": 1, + "modified": "2019-11-19 13:39:57.716248", + "modified_by": "Administrator", + "module": "Desk", + "name": "Onboarding Slide Help Link", + "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/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.py b/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.py new file mode 100644 index 0000000000..6ee5e4f7d7 --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.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 OnboardingSlideHelpLink(Document): + pass diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 6676bd1908..498ab50645 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -84,7 +84,7 @@ def update_comment(name, content): doc.save(ignore_permissions=True) @frappe.whitelist() -def get_next(doctype, value, prev, filters, sort_order, sort_field): +def get_next(doctype, value, prev, filters=None, sort_order='desc', sort_field='modified'): prev = int(prev) if not filters: filters = [] @@ -105,7 +105,7 @@ def get_next(doctype, value, prev, filters, sort_order, sort_field): res = frappe.get_list(doctype, fields = ["name"], filters = filters, - order_by = sort_field + " " + sort_order, + order_by = "`tab{0}`.{1}".format(doctype, sort_field) + " " + sort_order, limit_start=0, limit_page_length=1, as_list=True) if not res: diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py index 2cb163342d..1bce14fb2d 100644 --- a/frappe/desk/listview.py +++ b/frappe/desk/listview.py @@ -47,7 +47,7 @@ def get_group_by_count(doctype, current_filters, field): else: return frappe.db.get_list(doctype, filters=current_filters, - group_by=field, + group_by='`tab{0}`.{1}'.format(doctype, field), fields=['count(*) as count', '`{}` as name'.format(field)], order_by='count desc', limit=50, diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py index c5e5ea7c2b..604d47de9f 100644 --- a/frappe/desk/moduleview.py +++ b/frappe/desk/moduleview.py @@ -7,6 +7,7 @@ import json from frappe import _ from frappe.boot import get_allowed_pages, get_allowed_reports from frappe.desk.doctype.desktop_icon.desktop_icon import set_hidden, clear_desktop_icons_cache +from frappe.cache_manager import build_domain_restriced_doctype_cache, build_domain_restriced_page_cache, build_table_count_cache @frappe.whitelist() def get(module): @@ -25,6 +26,13 @@ def hide_module(module): set_hidden(module, frappe.session.user, 1) clear_desktop_icons_cache() +def get_table_with_counts(): + counts = frappe.cache().get_value("information_schema:counts") + if counts: + return counts + else: + return build_table_count_cache() + def get_data(module, build=True): """Get module data for the module view `desk/#Module/[name]`""" doctype_info = get_doctype_info(module) @@ -44,10 +52,10 @@ def get_data(module, build=True): # set_last_modified(data) if build: - exists_cache = {} + exists_cache = get_table_with_counts() def doctype_contains_a_record(name): exists = exists_cache.get(name) - if not exists: + if not type(exists) == int: if not frappe.db.get_value('DocType', name, 'issingle'): exists = frappe.db.count(name) else: @@ -96,19 +104,17 @@ def build_config_from_file(module): def filter_by_restrict_to_domain(data): """ filter Pages and DocType depending on the Active Module(s) """ - mapper = { - "page": "Page", - "doctype": "DocType" - } - active_domains = frappe.get_active_domains() + doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache() + pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache() for d in data: _items = [] for item in d.get("items", []): - doctype = mapper.get(item.get("type")) - doctype_domain = frappe.db.get_value(doctype, item.get("name"), "restrict_to_domain") or '' - if not doctype_domain or (doctype_domain in active_domains): + item_type = item.get("type") + item_name = item.get("name") + + if (item_name in pages) or (item_name in doctypes) or item_type == 'report': _items.append(item) d.update({ "items": _items }) @@ -234,8 +240,11 @@ def get_config(app, module): for item in section["items"]: if item["type"]=="report" and item["name"] in disabled_reports: continue + # some module links might not have name + if not item.get("name"): + item["name"] = item.get("label") if not item.get("label"): - item["label"] = _(item["name"]) + item["label"] = _(item.get("name")) items.append(item) section['items'] = items @@ -297,7 +306,7 @@ def get_onboard_items(app, module): @frappe.whitelist() def get_links_for_module(app, module): - return [l.get('label') for l in get_links(app, module)] + return [{'value': l.get('name'), 'label': l.get('label')} for l in get_links(app, module)] def get_links(app, module): try: @@ -330,13 +339,13 @@ def get_desktop_settings(): def apply_user_saved_links(module): module = frappe._dict(module) all_links = get_links(module.app, module.module_name) - module_links_by_label = {} + module_links_by_name = {} for link in all_links: - module_links_by_label[link['label']] = link + module_links_by_name[link['name']] = 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] + module.links = [module_links_by_name[l] for l in user_links if l in module_links_by_name] return module diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 84d515050c..3a8815ca71 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -11,15 +11,19 @@ import json @frappe.whitelist() @frappe.read_only() def get_notifications(): + out = { + "open_count_doctype": {}, + "targets": {}, + } if (frappe.flags.in_install or not frappe.db.get_single_value('System Settings', 'setup_complete')): - return { - "open_count_doctype": {}, - "targets": {}, - } + return out config = get_notification_config() + if not config: + return out + groups = list(config.get("for_doctype")) + list(config.get("for_module")) cache = frappe.cache() @@ -31,10 +35,10 @@ def get_notifications(): if count is not None: notification_count[name] = count - return { - "open_count_doctype": get_notifications_for_doctypes(config, notification_count), - "targets": get_notifications_for_targets(config, notification_percent), - } + out['open_count_doctype'] = get_notifications_for_doctypes(config, notification_count) + out['targets'] = get_notifications_for_targets(config, notification_percent) + + return out def get_notifications_for_doctypes(config, notification_count): """Notifications for DocTypes""" @@ -118,6 +122,10 @@ def clear_notifications(user=None): return cache = frappe.cache() config = get_notification_config() + + if not config: + return + for_doctype = list(config.get('for_doctype')) if config.get('for_doctype') else [] for_module = list(config.get('for_module')) if config.get('for_module') else [] groups = for_doctype + for_module @@ -139,6 +147,8 @@ def delete_notification_count_for(doctype): def clear_doctype_notifications(doc, method=None, *args, **kwargs): config = get_notification_config() + if not config: + return if isinstance(doc, string_types): doctype = doc # assuming doctype name was passed directly else: @@ -172,6 +182,8 @@ def get_notification_info(): return out def get_notification_config(): + user = frappe.session.user or 'Guest' + def _get(): subscribed_documents = get_subscribed_documents() config = frappe._dict() @@ -195,7 +207,7 @@ def get_notification_config(): config[key].update(nc.get(key, {})) return config - return frappe.cache().hget("notification_config", frappe.session.user, _get) + return frappe.cache().hget("notification_config", user, _get) def get_filters_for(doctype): '''get open filters for doctype''' diff --git a/frappe/desk/page/leaderboard/leaderboard.css b/frappe/desk/page/leaderboard/leaderboard.css index dbe9cca5b8..a3cb4d09c4 100644 --- a/frappe/desk/page/leaderboard/leaderboard.css +++ b/frappe/desk/page/leaderboard/leaderboard.css @@ -19,6 +19,14 @@ background: #f0f4f7; } +.from-date-field .clearfix{ + display: none; +} + +.from-date-field { + margin-left: 10px; +} + .select-time:focus, .select-doctype:focus, .select-filter:focus, .select-sort:focus { background: #f0f4f7; } diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js index 3e4c36add0..c64d2dcb4f 100644 --- a/frappe/desk/page/leaderboard/leaderboard.js +++ b/frappe/desk/page/leaderboard/leaderboard.js @@ -41,7 +41,11 @@ class Leaderboard { return field; }); } - this.timespans = ["Week", "Month", "Quarter", "Year", "All Time"]; + this.timespans = [ + "This Week", "This Month", "This Quarter", "This Year", + "Last Week", "Last Month", "Last Quarter", "Last Year", + "All Time", "Select From Date" + ]; // for saving current selected filters const _initial_doctype = frappe.get_route()[1] || this.doctypes[0]; @@ -103,7 +107,8 @@ class Leaderboard { this.timespans.map(d => { return {"label": __(d), value: d }; }) - ); + ); + this.create_from_date_field(); this.type_select = this.page.add_select(__("Field"), this.options.selected_filter.map(d => { @@ -113,7 +118,12 @@ class Leaderboard { this.timespan_select.on("change", (e) => { this.options.selected_timespan = e.currentTarget.value; - this.make_request(); + if (this.options.selected_timespan === 'Select From Date') { + this.from_date_field.show(); + } else { + this.from_date_field.hide(); + this.make_request(); + } }); this.type_select.on("change", (e) => { @@ -122,6 +132,28 @@ class Leaderboard { }); } + create_from_date_field() { + let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`); + this.from_date_field = $(`
`).insertAfter(timespan_field).hide(); + + let date_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'Date', + fieldname: 'selected_from_date', + placeholder: frappe.datetime.month_start(), + default: frappe.datetime.month_start(), + input_class: 'input-sm', + reqd: 1, + change: () => { + this.selected_from_date = date_field.get_value(); + if (this.selected_from_date) this.make_request(); + } + }, + parent: $(this.parent).find('.from-date-field'), + render_input: 1 + }); + } + render_selected_doctype() { this.$sidebar_list.on("click", "li", (e)=> { @@ -207,7 +239,6 @@ class Leaderboard { this.leaderboard_config[this.options.selected_doctype].method, { 'from_date': this.get_from_date(), - 'timespan': this.options.selected_timespan, 'company': this.options.selected_company, 'field': this.options.selected_filter_item, 'limit': this.leaderboard_limit, @@ -360,17 +391,20 @@ class Leaderboard { get_from_date() { let timespan = this.options.selected_timespan.toLowerCase(); let current_date = frappe.datetime.now_date(); - let date = ''; - if (timespan === "month") { - date = frappe.datetime.add_months(current_date, -1); - } else if (timespan === "quarter") { - date = frappe.datetime.add_months(current_date, -3); - } else if (timespan === "year") { - date = frappe.datetime.add_months(current_date, -12); - } else if (timespan === "week") { - date = frappe.datetime.add_days(current_date, -7); + let get_from_date = { + "this week": frappe.datetime.week_start(), + "this month": frappe.datetime.month_start(), + "this quarter": frappe.datetime.quarter_start(), + "this year": frappe.datetime.year_start(), + "last week": frappe.datetime.add_days(current_date, -7), + "last month": frappe.datetime.add_months(current_date, -1), + "last quarter": frappe.datetime.add_months(current_date, -3), + "last year": frappe.datetime.add_months(current_date, -12), + "all time": "", + "select from date": this.selected_from_date || frappe.datetime.month_start() } - return date; + + return get_from_date[timespan]; } } diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py index bb598ab180..e7e147fb7d 100644 --- a/frappe/desk/page/setup_wizard/install_fixtures.py +++ b/frappe/desk/page/setup_wizard/install_fixtures.py @@ -10,6 +10,7 @@ from frappe.desk.doctype.global_search_settings.global_search_settings import up def install(): update_genders_and_salutations() update_global_search_doctypes() + setup_email_linking() @frappe.whitelist() def update_genders_and_salutations(): @@ -20,13 +21,12 @@ def update_genders_and_salutations(): for record in records: doc = frappe.new_doc(record.get("doctype")) doc.update(record) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) - try: - doc.insert(ignore_permissions=True) - except frappe.DuplicateEntryError as e: - # pass DuplicateEntryError and continue - if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name: - # make sure DuplicateEntryError is for the exact same doc and not a related doc - pass - else: - raise \ No newline at end of file +def setup_email_linking(): + doc = frappe.get_doc({ + "doctype": "Email Account", + "email_id": "email_linking@example.com", + }) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + \ No newline at end of file diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index a13b6c7a8d..43d4e8dde4 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -141,6 +141,7 @@ def update_system_settings(args): "time_zone": args.get("timezone"), "float_precision": 3, 'date_format': frappe.db.get_value("Country", args.get("country"), "date_format"), + 'time_format': frappe.db.get_value("Country", args.get("country"), "time_format"), 'number_format': number_format, 'enable_scheduler': 1 if not frappe.flags.in_test else 0, 'backup_limit': 3 # Default for downloadable backups diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 21a69f5111..7dc561193f 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -510,7 +510,7 @@ def has_match(row, linked_doctypes, doctype_match_filters, ref_doctype, if_owner cell_value = None if isinstance(row, dict): cell_value = row.get(idx) - elif isinstance(row, list): + elif isinstance(row, (list, tuple)): cell_value = row[idx] if dt in match_filters and cell_value not in match_filters.get(dt) and frappe.db.exists(dt, cell_value): diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index d5b43807a8..c0685b67f2 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -9,8 +9,10 @@ from six.moves import range import frappe.permissions from frappe.model.db_query import DatabaseQuery from frappe import _ -from six import text_type, string_types, StringIO +from six import string_types, StringIO from frappe.core.doctype.access_log.access_log import make_access_log +from frappe.utils import cstr + @frappe.whitelist() @frappe.read_only() @@ -170,11 +172,11 @@ def export_query(): writer = csv.writer(f) for r in data: # encode only unicode type strings and not int, floats etc. - writer.writerow([handle_html(frappe.as_unicode(v)).encode('utf-8') \ + writer.writerow([handle_html(frappe.as_unicode(v)) \ if isinstance(v, string_types) else v for v in r]) f.seek(0) - frappe.response['result'] = text_type(f.read(), 'utf-8') + frappe.response['result'] = cstr(f.read()) frappe.response['type'] = 'csv' frappe.response['doctype'] = title @@ -261,17 +263,32 @@ def delete_bulk(doctype, items): @frappe.whitelist() @frappe.read_only() def get_sidebar_stats(stats, doctype, filters=[]): + _user_tags, tag_list = [], [] + data = frappe._dict(frappe.local.form_dict) + filters = json.loads(data["filters"]) - if not frappe.cache().hget("tags_count", doctype): - tags = [tag.name for tag in frappe.get_list("Tag")] - _user_tags = [] - for tag in tags: - count = frappe.db.count("Tag Link", filters={"document_type": doctype, "tag": tag}) - if count > 0: - _user_tags.append([tag, count]) - frappe.cache().hset("tags_count", doctype, _user_tags) + # Show Tags irrespective of any tag filter set + for idx, filter in enumerate(filters): + if filter[0] == "Tag Link": + filters.pop(idx) + break - return {"stats": {"_user_tags": frappe.cache().hget("tags_count", doctype)}} + for tag in frappe.get_all("Tag Link", filters={"document_type": doctype}, fields=["tag"]): + if tag.tag in tag_list: + continue + + tag_list.append(tag.tag) + tag_filters = [] + tag_filters.extend(filters) + tag_filters.extend([['Tag Link', 'tag', '=', tag.tag]]) + + fields = ["count(distinct `tab{0}`.`name`) AS total_count".format(doctype)] + count = frappe.get_all(doctype, filters=tag_filters, fields=fields) + + if count[0].get("total_count") > 0: + _user_tags.append([tag.tag, count[0].get("total_count")]) + + return {"stats": {"_user_tags": _user_tags}} @frappe.whitelist() @frappe.read_only() diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 7bcfe646ab..c70b650945 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -50,7 +50,7 @@ def sanitize_searchfield(searchfield): # this is called by the Link Field @frappe.whitelist() def search_link(doctype, txt, query=None, filters=None, page_length=20, searchfield=None, reference_doctype=None, ignore_user_permissions=False): - search_widget(doctype, txt, query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions) + search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions) frappe.response['results'] = build_for_autosuggest(frappe.response["values"]) del frappe.response["values"] diff --git a/frappe/desk/user_progress.py b/frappe/desk/user_progress.py deleted file mode 100644 index f62bb2a29d..0000000000 --- a/frappe/desk/user_progress.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals - -import frappe -from frappe.utils import cint - -@frappe.whitelist() -def get_user_progress_slides(): - ''' - Return user progress slides for the desktop (called via `get_user_progress_slides` hook) - ''' - slides = [] - if cint(frappe.db.get_single_value('System Settings', 'setup_complete')): - for fn in frappe.get_hooks('get_user_progress_slides'): - slides += frappe.get_attr(fn)() - - return slides - -@frappe.whitelist() -def update_and_get_user_progress(): - ''' - Return setup progress action states (called via `update_and_get_user_progress` hook) - ''' - states = {} - for fn in frappe.get_hooks('update_and_get_user_progress'): - states.update(frappe.get_attr(fn)()) - - return states diff --git a/frappe/email/doctype/email_account/email_account.js b/frappe/email/doctype/email_account/email_account.js index 7a175e7192..71f9cccb0d 100644 --- a/frappe/email/doctype/email_account/email_account.js +++ b/frappe/email/doctype/email_account/email_account.js @@ -184,7 +184,7 @@ frappe.ui.form.on("Email Account", { read as well as unread message from server. This may also cause the duplication\ of Communication (emails)."); frappe.confirm(msg, null, function() { - frm.set_value("email_sync_option", "UNSEEN"); + frm.set_value("email_sync_option", "ALL"); }); } } diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json index 5154514c22..e724102fdf 100644 --- a/frappe/email/doctype/email_account/email_account.json +++ b/frappe/email/doctype/email_account/email_account.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "autoname": "field:email_account_name", "creation": "2014-09-11 12:04:34.163728", @@ -21,6 +22,7 @@ "use_imap", "email_server", "use_ssl", + "append_emails_to_sent_folder", "incoming_port", "attachment_limit", "append_to", @@ -37,6 +39,7 @@ "enable_outgoing", "smtp_server", "use_tls", + "use_ssl_for_outgoing", "smtp_port", "default_outgoing", "always_use_account_email_id_as_sender", @@ -389,10 +392,25 @@ "fieldname": "incoming_port", "fieldtype": "Data", "label": "Port" + }, + { + "default": "0", + "depends_on": "eval:!doc.domain && doc.enable_outgoing", + "fieldname": "append_emails_to_sent_folder", + "fieldtype": "Check", + "label": "Append Emails to Sent Folder" + }, + { + "default": "0", + "depends_on": "eval:!doc.domain && doc.enable_outgoing", + "fieldname": "use_ssl_for_outgoing", + "fieldtype": "Check", + "label": "Use SSL for Outgoing" } ], "icon": "fa fa-inbox", - "modified": "2019-08-31 18:01:15.568831", + "links": [], + "modified": "2019-12-18 15:56:39.744520", "modified_by": "Administrator", "module": "Email", "name": "Email Account", diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index f10f08664c..50daf1cf72 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -7,6 +7,7 @@ import imaplib import re import json import socket +import time from frappe import _ from frappe.model.document import Document from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html @@ -21,7 +22,6 @@ from frappe.desk.form import assign_to from frappe.utils.user import get_system_managers from frappe.utils.background_jobs import enqueue, get_jobs from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts -from frappe.utils.scheduler import log from frappe.utils.html_utils import clean_email_html from frappe.email.utils import get_port @@ -117,7 +117,8 @@ class EmailAccount(Document): fields = [ "name as domain", "use_imap", "email_server", "use_ssl", "smtp_server", "use_tls", - "smtp_port", "incoming_port" + "smtp_port", "incoming_port", "append_emails_to_sent_folder", + "use_ssl_for_outgoing" ] return frappe.db.get_value("Email Domain", domain[1], fields, as_dict=True) except Exception: @@ -129,11 +130,12 @@ class EmailAccount(Document): if not self.smtp_server: frappe.throw(_("{0} is required").format("SMTP Server")) - server = SMTPServer(login = getattr(self, "login_id", None) \ - or self.email_id, - server = self.smtp_server, - port = cint(self.smtp_port), - use_tls = cint(self.use_tls) + server = SMTPServer( + login = getattr(self, "login_id", None) or self.email_id, + server=self.smtp_server, + port=cint(self.smtp_port), + use_tls=cint(self.use_tls), + use_ssl=cint(self.use_ssl_for_outgoing) ) if self.password and not self.no_smtp_authentication: server.password = self.get_password() @@ -284,7 +286,7 @@ class EmailAccount(Document): except Exception: frappe.db.rollback() - log('email_account.receive') + frappe.log_error('email_account.receive') if self.use_imap: self.handle_bad_emails(email_server, uid, msg, frappe.get_traceback()) exceptions.append(frappe.get_traceback()) @@ -323,16 +325,16 @@ class EmailAccount(Document): unhandled_email.insert(ignore_permissions=True) frappe.db.commit() - def insert_communication(self, msg, args={}): + def insert_communication(self, msg, args=None): if isinstance(msg, list): raw, uid, seen = msg else: raw = msg uid = -1 seen = 0 - - if args.get("uid", -1): uid = args.get("uid", -1) - if args.get("seen", 0): seen = args.get("seen", 0) + if isinstance(args, dict): + if args.get("uid", -1): uid = args.get("uid", -1) + if args.get("seen", 0): seen = args.get("seen", 0) email = Email(raw) @@ -356,7 +358,7 @@ class EmailAccount(Document): name = names[0].get("name") # email is already available update communication uid instead frappe.db.set_value("Communication", name, "uid", uid, update_modified=False) - return + return frappe.get_doc("Communication", name) if email.content_type == 'text/html': email.content = clean_email_html(email.content) @@ -649,6 +651,24 @@ class EmailAccount(Document): if frappe.db.exists("Email Account", {"enable_automatic_linking": 1, "name": ('!=', self.name)}): frappe.throw(_("Automatic Linking can be activated only for one Email Account.")) + + def append_email_to_sent_folder(self, message): + + email_server = None + try: + email_server = self.get_incoming_server(in_receive=True) + except Exception: + frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name)) + + if not email_server: + return + + email_server.connect() + + if email_server.imap: + email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message) + + @frappe.whitelist() def get_append_to(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None): if not txt: txt = "" diff --git a/frappe/email/doctype/email_domain/email_domain.json b/frappe/email/doctype/email_domain/email_domain.json index 677bf876aa..a4ca19a0bd 100644 --- a/frappe/email/doctype/email_domain/email_domain.json +++ b/frappe/email/doctype/email_domain/email_domain.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "field:domain_name", "creation": "2016-03-29 10:50:48.848239", "doctype": "DocType", @@ -18,6 +19,8 @@ "outgoing_mail_settings", "smtp_server", "use_tls", + "use_ssl_for_outgoing", + "append_emails_to_sent_folder", "smtp_port" ], "fields": [ @@ -30,7 +33,7 @@ "fieldtype": "Data", "label": "domain name", "read_only": 1, - "unique": 0 + "unique": 1 }, { "fieldname": "email_id", @@ -106,10 +109,23 @@ "fieldname": "incoming_port", "fieldtype": "Data", "label": "Port" + }, + { + "default": "0", + "fieldname": "append_emails_to_sent_folder", + "fieldtype": "Check", + "label": "Append Emails to Sent Folder" + }, + { + "default": "0", + "fieldname": "use_ssl_for_outgoing", + "fieldtype": "Check", + "label": "Use SSL for Outgoing" } ], "icon": "icon-inbox", - "modified": "2019-10-09 17:56:48.834704", + "links": [], + "modified": "2019-12-18 15:57:34.445308", "modified_by": "Administrator", "module": "Email", "name": "Email Domain", @@ -127,4 +143,4 @@ ], "sort_field": "modified", "sort_order": "DESC" -} +} \ No newline at end of file diff --git a/frappe/email/doctype/email_domain/email_domain.py b/frappe/email/doctype/email_domain/email_domain.py index e800b839b9..b6585d966b 100644 --- a/frappe/email/doctype/email_domain/email_domain.py +++ b/frappe/email/doctype/email_domain/email_domain.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import validate_email_address ,cint +from frappe.utils import validate_email_address ,cint, cstr import imaplib,poplib,smtplib from frappe.email.utils import get_port @@ -49,9 +49,16 @@ class EmailDomain(Document): except Exception: pass try: - if self.use_tls and not self.smtp_port: - self.smtp_port = 587 - sess = smtplib.SMTP((self.smtp_server or "").encode('utf-8'), cint(self.smtp_port) or None) + if self.use_ssl_for_outgoing: + if not self.smtp_port: + self.smtp_port = 465 + + sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'), + cint(self.smtp_port) or None) + else: + if self.use_tls and not self.smtp_port: + self.smtp_port = 587 + sess = smtplib.SMTP(cstr(self.smtp_server or ""), cint(self.smtp_port) or None) sess.quit() except Exception: frappe.throw(_("Outgoing email account not correct")) @@ -73,6 +80,8 @@ class EmailDomain(Document): email_account.set("attachment_limit",self.attachment_limit) email_account.set("smtp_server",self.smtp_server) email_account.set("smtp_port",self.smtp_port) + email_account.set("use_ssl_for_outgoing", self.use_ssl_for_outgoing) + email_account.set("append_emails_to_sent_folder", self.append_emails_to_sent_folder) email_account.save() except Exception as e: frappe.msgprint(email_account.name) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 4f3e7994a5..b0d1756643 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -9,7 +9,6 @@ from frappe import throw, _ from frappe.website.website_generator import WebsiteGenerator from frappe.utils.verified_command import get_signed_params, verify_request from frappe.utils.background_jobs import enqueue -from frappe.utils.scheduler import log from frappe.email.queue import send from frappe.email.doctype.email_group.email_group import add_subscribers from frappe.utils import parse_addr @@ -213,7 +212,7 @@ def send_newsletter(newsletter): doc.db_set("email_sent", 0) frappe.db.commit() - log("send_newsletter") + frappe.log_error("send_newsletter") raise diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py index 4d3c3167a5..b9bbde172d 100644 --- a/frappe/email/doctype/notification/test_notification.py +++ b/frappe/email/doctype/notification/test_notification.py @@ -136,7 +136,7 @@ class TestNotification(unittest.TestCase): "reference_name": event.name, "status": "Not Sent"})) frappe.set_user('Administrator') - frappe.utils.scheduler.trigger(frappe.local.site, "daily", now=True) + frappe.get_doc('Scheduled Job Type', dict(method='frappe.email.doctype.notification.notification.trigger_daily_alerts')).execute() # not today, so no alert self.assertFalse(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", @@ -150,7 +150,7 @@ class TestNotification(unittest.TestCase): self.assertFalse(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", "reference_name": event.name, "status": "Not Sent"})) - frappe.utils.scheduler.trigger(frappe.local.site, "daily", now=True) + frappe.get_doc('Scheduled Job Type', dict(method='frappe.email.doctype.notification.notification.trigger_daily_alerts')).execute() # today so show alert self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", diff --git a/frappe/email/queue.py b/frappe/email/queue.py index f51451751d..4a0a34c76e 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -3,23 +3,25 @@ from __future__ import unicode_literals import frappe +import sys from six.moves import html_parser as HTMLParser import smtplib, quopri, json -from frappe import msgprint, throw, _, safe_decode +from frappe import msgprint, _, safe_decode from frappe.email.smtp import SMTPServer, get_outgoing_email_account from frappe.email.email_body import get_email, get_formatted_html, add_attachment from frappe.utils.verified_command import get_signed_params, verify_request from html2text import html2text -from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr, cint +from frappe.utils import get_url, nowdate, now_datetime, add_days, split_emails, cstr, cint from rq.timeouts import JobTimeoutException -from frappe.utils.scheduler import log -from six import text_type, string_types +from six import text_type, string_types, PY3 +from email.parser import Parser + class EmailLimitCrossedError(frappe.ValidationError): pass def send(recipients=None, sender=None, subject=None, message=None, text_content=None, reference_doctype=None, reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, - attachments=None, reply_to=None, cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, + attachments=None, reply_to=None, cc=None, bcc=None, message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None, queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None, header=None, print_letterhead=False): @@ -53,6 +55,11 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= if not recipients and not cc: return + if not cc: + cc = [] + if not bcc: + bcc = [] + if isinstance(recipients, string_types): recipients = split_emails(recipients) @@ -69,7 +76,6 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= if not sender or sender == "Administrator": sender = email_account.default_sender - if not text_content: try: text_content = html2text(message) @@ -374,7 +380,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals for update''', email, as_dict=True)[0] recipients_list = frappe.db.sql('''select name, recipient, status from - `tabEmail Queue Recipient` where parent=%s''',email.name,as_dict=1) + `tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1) if frappe.are_emails_muted(): frappe.msgprint(_("Emails are muted")) @@ -395,8 +401,16 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit) try: + message = None + if not frappe.flags.in_test: - if not smtpserver: smtpserver = SMTPServer() + if not smtpserver: + smtpserver = SMTPServer() + + # to avoid always using default email account for outgoing + if getattr(frappe.local, "outgoing_email_account", None): + frappe.local.outgoing_email_account = {} + smtpserver.setup_email_account(email.reference_doctype, sender=email.sender) for recipient in recipients_list: @@ -405,14 +419,16 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals message = prepare_message(email, recipient.recipient, recipients_list) if not frappe.flags.in_test: - smtpserver.sess.sendmail(email.sender, recipient.recipient, encode(message)) + smtpserver.sess.sendmail(email.sender, recipient.recipient, message) recipient.status = "Sent" frappe.db.sql("""update `tabEmail Queue Recipient` set status='Sent', modified=%s where name=%s""", (now_datetime(), recipient.name), auto_commit=auto_commit) + email_sent_to_any_recipient = any("Sent" == s.status for s in recipients_list) + #if all are sent set status - if any("Sent" == s.status for s in recipients_list): + if email_sent_to_any_recipient: frappe.db.sql("""update `tabEmail Queue` set status='Sent', modified=%s where name=%s""", (now_datetime(), email.name), auto_commit=auto_commit) else: @@ -424,6 +440,9 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals if email.communication: frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit) + if smtpserver.append_emails_to_sent_folder and email_sent_to_any_recipient: + smtpserver.email_account.append_email_to_sent_folder(encode(message)) + except (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError, smtplib.SMTPHeloError, @@ -433,7 +452,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals # bad connection/timeout, retry later - if any("Sent" == s.status for s in recipients_list): + if email_sent_to_any_recipient: frappe.db.sql("""update `tabEmail Queue` set status='Partially Sent', modified=%s where name=%s""", (now_datetime(), email.name), auto_commit=auto_commit) else: @@ -453,7 +472,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals frappe.db.sql("""update `tabEmail Queue` set status='Not Sent', modified=%s, retry=retry+1 where name=%s""", (now_datetime(), email.name), auto_commit=auto_commit) else: - if any("Sent" == s.status for s in recipients_list): + if email_sent_to_any_recipient: frappe.db.sql("""update `tabEmail Queue` set status='Partially Errored', error=%s where name=%s""", (text_type(e), email.name), auto_commit=auto_commit) else: @@ -469,7 +488,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals else: # log to Error Log - log('frappe.email.queue.flush', text_type(e)) + frappe.log_error('frappe.email.queue.flush') def prepare_message(email, recipient, recipients_list): message = email.message @@ -510,37 +529,41 @@ def prepare_message(email, recipient, recipients_list): message = (message and message.encode('utf8')) or '' message = safe_decode(message) - if not email.attachments: - return message - # On-demand attachments - from email.parser import Parser + if PY3: + from email.policy import SMTPUTF8 + message = Parser(policy=SMTPUTF8).parsestr(message) + else: + message = Parser().parsestr(message) - msg_obj = Parser().parsestr(message) - attachments = json.loads(email.attachments) + if email.attachments: + # On-demand attachments - for attachment in attachments: - if attachment.get('fcontent'): continue + attachments = json.loads(email.attachments) - fid = attachment.get("fid") - if fid: - _file = frappe.get_doc("File", fid) - fcontent = _file.get_content() - attachment.update({ - 'fname': _file.file_name, - 'fcontent': fcontent, - 'parent': msg_obj - }) - attachment.pop("fid", None) - add_attachment(**attachment) + for attachment in attachments: + if attachment.get('fcontent'): + continue - elif attachment.get("print_format_attachment") == 1: - attachment.pop("print_format_attachment", None) - print_format_file = frappe.attach_print(**attachment) - print_format_file.update({"parent": msg_obj}) - add_attachment(**print_format_file) + fid = attachment.get("fid") + if fid: + _file = frappe.get_doc("File", fid) + fcontent = _file.get_content() + attachment.update({ + 'fname': _file.file_name, + 'fcontent': fcontent, + 'parent': message + }) + attachment.pop("fid", None) + add_attachment(**attachment) - return msg_obj.as_string() + elif attachment.get("print_format_attachment") == 1: + attachment.pop("print_format_attachment", None) + print_format_file = frappe.attach_print(**attachment) + print_format_file.update({"parent": message}) + add_attachment(**print_format_file) + + return message.as_string() def clear_outbox(): """Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days. diff --git a/frappe/email/receive.py b/frappe/email/receive.py index b8fde57a43..9ba080bfda 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -12,7 +12,6 @@ import frappe from frappe import _, safe_decode, safe_encode from frappe.utils import (extract_email_id, convert_utc_to_user_timezone, now, cint, cstr, strip, markdown, parse_addr) -from frappe.utils.scheduler import log from frappe.core.doctype.file.file import get_random_filename, MaxFileSizeReachedError class EmailSizeExceededError(frappe.ValidationError): pass @@ -80,7 +79,7 @@ class EmailServer: except _socket.error: # log performs rollback and logs error in Error Log - log("receive.connect_pop") + frappe.log_error("receive.connect_pop") # Invalid mail server -- due to refusing connection frappe.msgprint(_('Invalid Mail Server. Please rectify and try again.')) @@ -255,7 +254,7 @@ class EmailServer: else: # log performs rollback and logs error in Error Log - log("receive.get_messages", self.make_error_msg(msg_num, incoming_mail)) + frappe.log_error("receive.get_messages", self.make_error_msg(msg_num, incoming_mail)) self.errors = True frappe.db.rollback() @@ -457,9 +456,9 @@ class Email: def show_attached_email_headers_in_content(self, part): # get the multipart/alternative message try: - from html import escape # python 3.x + from html import escape # python 3.x except ImportError: - from cgi import escape # python 2.x + from cgi import escape # python 2.x message = list(part.walk())[1] headers = [] @@ -481,7 +480,7 @@ class Email: """Detect chartset.""" charset = part.get_content_charset() if not charset: - charset = chardet.detect(frappe.safe_encode(part))['encoding'] + charset = chardet.detect(safe_encode(cstr(part)))['encoding'] return charset @@ -515,7 +514,7 @@ class Email: 'fcontent': fcontent, }) - cid = (part.get("Content-Id") or "").strip("><") + cid = (cstr(part.get("Content-Id")) or "").strip("><") if cid: self.cid_map[fname] = cid diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index c09f3f9cdd..aa025465e5 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -8,7 +8,7 @@ import smtplib import email.utils import _socket, sys from frappe import _ -from frappe.utils import cint, parse_addr +from frappe.utils import cint, cstr, parse_addr def send(email, append_to=None, retry=1): """Deprecated: Send the message or add it to Outbox Email""" @@ -41,6 +41,8 @@ def get_outgoing_email_account(raise_exception_not_set=True, append_to=None, sen try getting settings from `site_config.json`.""" sender_email_id = None + _email_account = None + if sender: sender_email_id = parse_addr(sender)[1] @@ -52,35 +54,38 @@ def get_outgoing_email_account(raise_exception_not_set=True, append_to=None, sen or frappe.local.outgoing_email_account.get("default")): email_account = None - if append_to: - # append_to is only valid when enable_incoming is checked + if sender_email_id: + # check if the sender has an email account with enable_outgoing + email_account = _get_email_account({"enable_outgoing": 1, + "email_id": sender_email_id}) - # in case of multiple Email Accounts with same append_to - # narrow it down based on email_id - email_account = _get_email_account({ + if not email_account and append_to: + # append_to is only valid when enable_incoming is checked + email_accounts = frappe.db.get_values("Email Account", { "enable_outgoing": 1, "enable_incoming": 1, "append_to": append_to, - "email_id": sender_email_id - }) + }, cache=True) - # else find the first Email Account with append_to - if not email_account: + if email_accounts: + _email_account = email_accounts[0] + + else: email_account = _get_email_account({ "enable_outgoing": 1, "enable_incoming": 1, "append_to": append_to }) - if not email_account and sender_email_id: - # check if the sender has email account with enable_outgoing - email_account = _get_email_account({"enable_outgoing": 1, "email_id": sender_email_id}) - if not email_account: # sender don't have the outging email account sender_email_id = None email_account = get_default_outgoing_email_account(raise_exception_not_set=raise_exception_not_set) + if not email_account and _email_account: + # if default email account is not configured then setup first email account based on append to + email_account = _email_account + if not email_account and raise_exception_not_set and cint(frappe.db.get_single_value('System Settings', 'setup_complete')): frappe.throw(_("Please setup default Email Account from Setup > Email > Email Account"), frappe.OutgoingEmailError) @@ -152,16 +157,19 @@ def _get_email_account(filters): return frappe.get_doc("Email Account", name) if name else None class SMTPServer: - def __init__(self, login=None, password=None, server=None, port=None, use_tls=None, append_to=None): + def __init__(self, login=None, password=None, server=None, port=None, use_tls=None, use_ssl=None, append_to=None): # get defaults from mail settings self._sess = None self.email_account = None self.server = None + self.append_emails_to_sent_folder = None + if server: self.server = server self.port = port self.use_tls = cint(use_tls) + self.use_ssl = cint(use_ssl) self.login = login self.password = password @@ -183,6 +191,8 @@ class SMTPServer: self.port = self.email_account.smtp_port self.use_tls = self.email_account.use_tls self.sender = self.email_account.email_id + self.use_ssl = self.email_account.use_ssl_for_outgoing + self.append_emails_to_sent_folder = self.email_account.append_emails_to_sent_folder self.always_use_account_email_id_as_sender = cint(self.email_account.get("always_use_account_email_id_as_sender")) self.always_use_account_name_as_sender_name = cint(self.email_account.get("always_use_account_name_as_sender_name")) @@ -199,11 +209,18 @@ class SMTPServer: raise frappe.OutgoingEmailError(err_msg) try: - if self.use_tls and not self.port: - self.port = 587 + if self.use_ssl: + if not self.port: + self.smtp_port = 465 - self._sess = smtplib.SMTP((self.server or "").encode('utf-8'), - cint(self.port) or None) + self._sess = smtplib.SMTP_SSL((self.server or "").encode('utf-8'), + cint(self.port) or None) + else: + if self.use_tls and not self.port: + self.port = 587 + + self._sess = smtplib.SMTP(cstr(self.server or ""), + cint(self.port) or None) if not self._sess: err_msg = _('Could not connect to outgoing email server') diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index feb8e80007..26c4e5ba5d 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -5,7 +5,10 @@ from __future__ import unicode_literals import unittest, os, base64 from frappe.email.receive import Email from frappe.email.email_body import (replace_filename_with_cid, - get_email, inline_style_in_html, get_header) + get_email, inline_style_in_html, get_header) +from frappe.email.queue import prepare_message, get_email_queue +from six import PY3 + class TestEmailBody(unittest.TestCase): def setUp(self): @@ -37,6 +40,53 @@ This is the text version of this email text_content=email_text ).as_string() + def test_prepare_message_returns_already_encoded_string(self): + + if PY3: + uni_chr1 = chr(40960) + uni_chr2 = chr(1972) + else: + uni_chr1 = unichr(40960) + uni_chr2 = unichr(1972) + + email = get_email_queue( + recipients=['test@example.com'], + sender='me@example.com', + subject='Test Subject', + content='

' + uni_chr1 + 'abcd' + uni_chr2 + '

', + formatted='

' + uni_chr1 + 'abcd' + uni_chr2 + '

', + text_content='whatever') + result = prepare_message(email=email, recipient='test@test.com', recipients_list=[]) + self.assertTrue("

=EA=80=80abcd=DE=B4

" in result) + + def test_prepare_message_returns_cr_lf(self): + email = get_email_queue( + recipients=['test@example.com'], + sender='me@example.com', + subject='Test Subject', + content='

\n this is a test of newlines\n' + '

', + formatted='

\n this is a test of newlines\n' + '

', + text_content='whatever') + result = prepare_message(email=email, recipient='test@test.com', recipients_list=[]) + if PY3: + self.assertTrue(result.count('\n') == result.count("\r")) + else: + self.assertTrue(True) + + def test_rfc_5322_header_is_wrapped_at_998_chars(self): + # unfortunately the db can only hold 140 chars so this can't be tested properly. test at max chars anyway. + email = get_email_queue( + recipients=['test@example.com'], + sender='me@example.com', + subject='Test Subject', + content='

Whatever

', + text_content='whatever', + message_id= "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" + + ".really.long.message.id.that.should.not.wrap.unti") + result = prepare_message(email=email, recipient='test@test.com', recipients_list=[]) + self.assertTrue( + "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" + + ".really.long.message.id.that.should.not.wrap.unti" in result) def test_image(self): img_signature = ''' @@ -49,7 +99,6 @@ Content-Disposition: inline; filename="favicon.png" self.assertTrue(img_signature in self.email_string) self.assertTrue(self.img_base64 in self.email_string) - def test_text_content(self): text_content = ''' Content-Type: text/plain; charset="utf-8" @@ -62,7 +111,6 @@ This is the text version of this email ''' self.assertTrue(text_content in self.email_string) - def test_email_content(self): html_head = ''' Content-Type: text/html; charset="utf-8" @@ -79,7 +127,6 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> self.assertTrue(html_head in self.email_string) self.assertTrue(html in self.email_string) - def test_replace_filename_with_cid(self): original_message = '''
@@ -152,6 +199,7 @@ Reply-To: test2_@erpnext.com mail = Email(content_bytes) self.assertEqual(mail.text_content, text_content) + def fixed_column_width(string, chunk_size): - parts = [string[0+i:chunk_size+i] for i in range(0, len(string), chunk_size)] - return '\n'.join(parts) \ No newline at end of file + parts = [string[0 + i:chunk_size + i] for i in range(0, len(string), chunk_size)] + return '\n'.join(parts) diff --git a/frappe/geo/country_info.py b/frappe/geo/country_info.py index f30f96fba6..4f878325ad 100644 --- a/frappe/geo/country_info.py +++ b/frappe/geo/country_info.py @@ -10,8 +10,10 @@ from frappe.utils.momentjs import get_all_timezones def get_country_info(country=None): data = get_all() data = frappe._dict(data.get(country, {})) - if not 'date_format' in data: + if 'date_format' not in data: data.date_format = "dd-mm-yyyy" + if 'time_format' not in data: + data.time_format = "HH:mm:ss" return data diff --git a/frappe/geo/doctype/country/country.json b/frappe/geo/doctype/country/country.json index 37bcbfdaf5..a164611155 100644 --- a/frappe/geo/doctype/country/country.json +++ b/frappe/geo/doctype/country/country.json @@ -76,6 +76,12 @@ "translatable": 0, "unique": 0 }, + { + "fieldname": "time_format", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Time format" + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -205,4 +211,4 @@ "track_changes": 1, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} diff --git a/frappe/geo/doctype/currency/currency.js b/frappe/geo/doctype/currency/currency.js index 1bc9865999..af2d6ebc4e 100644 --- a/frappe/geo/doctype/currency/currency.js +++ b/frappe/geo/doctype/currency/currency.js @@ -7,10 +7,5 @@ frappe.ui.form.on('Currency', { if(!frm.doc.enabled) { frm.set_intro(__("This Currency is disabled. Enable to use in transactions")); } - }, - - after_save(frm) { - if (frm.doc.enabled) - locals[':Currency'][frm.doc.name] = Object.assign(frm.doc, { doctype: ':Currency' }); } }); diff --git a/frappe/hooks.py b/frappe/hooks.py index b35387efe9..4922911e82 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -76,8 +76,7 @@ leaderboards = "frappe.desk.leaderboard.get_leaderboards" on_session_creation = [ "frappe.core.doctype.activity_log.feed.login_feed", - "frappe.core.doctype.user.user.notify_admin_access_to_system_manager", - "frappe.utils.scheduler.reset_enabled_scheduler_events", + "frappe.core.doctype.user.user.notify_admin_access_to_system_manager" ] on_logout = "frappe.core.doctype.session_default_settings.session_default_settings.clear_session_defaults" @@ -140,6 +139,7 @@ doc_events = { "on_change": [ "frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points" ], + "aftet_insert": "frappe.cache_manager.build_table_count_cache", }, "Event": { "after_insert": "frappe.integrations.doctype.google_calendar.google_calendar.insert_event_in_google_calendar", @@ -149,18 +149,28 @@ doc_events = { "Contact": { "after_insert": "frappe.integrations.doctype.google_contacts.google_contacts.insert_contacts_to_google_contacts", "on_update": "frappe.integrations.doctype.google_contacts.google_contacts.update_contacts_to_google_contacts", + }, + "DocType": { + "after_save": "frappe.cache_manager.build_domain_restriced_doctype_cache", + }, + "Page": { + "after_save": "frappe.cache_manager.build_domain_restriced_page_cache", } } scheduler_events = { + "cron": { + "0/15 * * * *": [ + "frappe.oauth.delete_oauth2_data", + "frappe.website.doctype.web_page.web_page.check_publish_status", + "frappe.twofactor.delete_all_barcodes_for_users" + ] + }, "all": [ "frappe.email.queue.flush", "frappe.email.doctype.email_account.email_account.pull", "frappe.email.doctype.email_account.email_account.notify_unreplied", - "frappe.oauth.delete_oauth2_data", "frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment", - "frappe.twofactor.delete_all_barcodes_for_users", - "frappe.website.doctype.web_page.web_page.check_publish_status", 'frappe.utils.global_search.sync_global_search' ], "hourly": [ diff --git a/frappe/installer.py b/frappe/installer.py index 764a0b6780..4b07ab8ce8 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -18,15 +18,16 @@ from frappe.utils.fixtures import sync_fixtures from frappe.website import render from frappe.modules.utils import sync_customizations from frappe.database import setup_database +from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs def install_db(root_login="root", root_password=None, db_name=None, source_sql=None, - admin_password=None, verbose=True, force=0, site_config=None, reinstall=False, - db_type=None): + admin_password=None, verbose=True, force=0, site_config=None, reinstall=False, + db_type=None, db_host=None, db_port=None): if not db_type: db_type = frappe.conf.db_type or 'mariadb' - make_conf(db_name, site_config=site_config, db_type=db_type) + make_conf(db_name, site_config=site_config, db_type=db_type, db_host=db_host, db_port=db_port) frappe.flags.in_install_db = True frappe.flags.root_login = root_login @@ -91,6 +92,7 @@ def install_app(name, verbose=False, set_as_patched=True): for after_install in app_hooks.after_install or []: frappe.get_attr(after_install)() + sync_jobs() sync_fixtures(name) sync_customizations(name) @@ -189,14 +191,14 @@ def init_singles(): doc.flags.ignore_validate=True doc.save() -def make_conf(db_name=None, db_password=None, site_config=None, db_type=None): +def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None): site = frappe.local.site - make_site_config(db_name, db_password, site_config, db_type=db_type) + make_site_config(db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port) sites_path = frappe.local.sites_path frappe.destroy() frappe.init(site, sites_path=sites_path) -def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None): +def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None): frappe.create_folder(os.path.join(frappe.local.site_path)) site_file = get_site_config_path() @@ -207,6 +209,12 @@ def make_site_config(db_name=None, db_password=None, site_config=None, db_type=N if db_type: site_config['db_type'] = db_type + if db_host: + site_config['db_host'] = db_host + + if db_port: + site_config['db_port'] = db_port + with open(site_file, "w") as f: f.write(json.dumps(site_config, indent=1, sort_keys=True)) diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py index c6ef12ff08..5e464d4882 100644 --- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py +++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py @@ -180,6 +180,33 @@ class RazorpaySettings(Document): integration_request = create_request_log(kwargs, "Host", "Razorpay") return get_url("./integrations/razorpay_checkout?token={0}".format(integration_request.name)) + def create_order(self, **kwargs): + # Creating Orders https://razorpay.com/docs/api/orders/ + + # convert rupees to paisa + kwargs['amount'] *= 100 + + # Create integration log + integration_request = create_request_log(kwargs, "Host", "Razorpay") + + # Setup payment options + payment_options = { + "amount": kwargs.get('amount'), + "currency": kwargs.get('currency', 'INR'), + "receipt": kwargs.get('receipt'), + "payment_capture": kwargs.get('payment_capture') + } + if self.api_key and self.api_secret: + try: + order = make_post_request("https://api.razorpay.com/v1/orders", + auth=(self.api_key, self.get_password(fieldname="api_secret", raise_exception=False)), + data=payment_options) + order['integration_request'] = integration_request.name + return order # Order returned to be consumed by razorpay.js + except Exception: + frappe.log(frappe.get_traceback()) + frappe.throw(_("Could not create razorpay order")) + def create_request(self, data): self.data = frappe._dict(data) @@ -213,6 +240,10 @@ class RazorpaySettings(Document): self.integration_request.update_status(data, 'Authorized') self.flags.status_changed_to = "Authorized" + if resp.get("status") == "captured": + self.integration_request.update_status(data, 'Completed') + self.flags.status_changed_to = "Completed" + elif data.get('subscription_id'): if resp.get("status") == "refunded": # if subscription start date is in future then @@ -222,14 +253,6 @@ class RazorpaySettings(Document): self.integration_request.update_status(data, 'Completed') self.flags.status_changed_to = "Verified" - if resp.get("status") == "captured": - # if subscription starts immediately then - # razorpay charge the actual amount - # thus changing status to Completed - - self.integration_request.update_status(data, 'Completed') - self.flags.status_changed_to = "Completed" - else: frappe.log_error(str(resp), 'Razorpay Payment not authorized') @@ -329,6 +352,63 @@ def capture_payment(is_sandbox=False, sanbox_response=None): doc.error = frappe.get_traceback() frappe.log_error(doc.error, '{0} Failed'.format(doc.name)) + +@frappe.whitelist(allow_guest=True) +def get_api_key(): + controller = frappe.get_doc("Razorpay Settings") + return controller.api_key + +@frappe.whitelist(allow_guest=True) +def get_order(doctype, docname): + # Order returned to be consumed by razorpay.js + doc = frappe.get_doc(doctype, docname) + try: + # Do not use run_method here as it fails silently + return doc.get_razorpay_order() + except AttributeError: + frappe.log_error(frappe.get_traceback(), _("Controller method get_razorpay_order missing")) + frappe.throw(_("Could not create Razorpay order. Please contact Administrator")) + +@frappe.whitelist(allow_guest=True) +def order_payment_success(integration_request, params): + """Called by razorpay.js on order payment success, the params + contains razorpay_payment_id, razorpay_order_id, razorpay_signature + that is updated in the data field of integration request + + Args: + integration_request (string): Name for integration request doc + params (string): Params to be updated for integration request. + """ + params = json.loads(params) + integration = frappe.get_doc("Integration Request", integration_request) + + # Update integration request + integration.update_status(params, integration.status) + integration.reload() + + data = json.loads(integration.data) + controller = frappe.get_doc("Razorpay Settings") + + # Update payment and integration data for payment controller object + controller.integration_request = integration + controller.data = frappe._dict(data) + + # Authorize payment + controller.authorize_payment() + +@frappe.whitelist(allow_guest=True) +def order_payment_failure(integration_request, params): + """Called by razorpay.js on failure + + Args: + integration_request (TYPE): Description + params (TYPE): error data to be updated + """ + frappe.log_error(params, 'Razorpay Payment Failure') + params = json.loads(params) + integration = frappe.get_doc("Integration Request", integration_request) + integration.update_status(params, integration.status) + def convert_rupee_to_paisa(**kwargs): for addon in kwargs.get('addons'): addon['item']['amount'] *= 100 @@ -382,4 +462,4 @@ def validate_payment_callback(data): _throw() def handle_subscription_notification(doctype, docname): - call_hook_method("handle_subscription_notification", doctype=doctype, docname=docname) \ No newline at end of file + call_hook_method("handle_subscription_notification", doctype=doctype, docname=docname) diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.json b/frappe/integrations/doctype/social_login_key/social_login_key.json index 8e415e6c20..6c0fbdb26c 100644 --- a/frappe/integrations/doctype/social_login_key/social_login_key.json +++ b/frappe/integrations/doctype/social_login_key/social_login_key.json @@ -1,700 +1,183 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "beta": 0, - "creation": "2017-11-18 15:36:09.676722", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "allow_import": 1, + "creation": "2017-11-18 15:36:09.676722", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enable_social_login", + "client_credentials", + "social_login_provider", + "client_id", + "column_break_0", + "provider_name", + "client_secret", + "sb_identity_details", + "icon", + "column_break_1", + "base_url", + "client_urls", + "authorize_url", + "access_token_url", + "column_break_3", + "redirect_url", + "api_endpoint", + "custom_base_url", + "client_information", + "api_endpoint_args", + "auth_url_data", + "user_id_property" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "enable_social_login", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Enable Social Login", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "enable_social_login", + "fieldtype": "Check", + "label": "Enable Social Login" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "eval:doc.enable_social_login", - "columns": 0, - "fieldname": "client_credentials", - "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": "Client Credentials", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.enable_social_login", + "fieldname": "client_credentials", + "fieldtype": "Section Break", + "label": "Client Credentials" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Custom", - "depends_on": "eval:doc.custom!=1", - "fieldname": "social_login_provider", - "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": "Social Login Provider", - "length": 0, - "no_copy": 0, - "options": "Custom\nFacebook\nFrappe\nGitHub\nGoogle\nOffice 365\nSalesforce\nfairlogin", - "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": 1, - "unique": 0 - }, + "default": "Custom", + "depends_on": "eval:doc.custom!=1", + "fieldname": "social_login_provider", + "fieldtype": "Select", + "label": "Social Login Provider", + "options": "Custom\nFacebook\nFrappe\nGitHub\nGoogle\nOffice 365\nSalesforce\nfairlogin", + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "client_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Client ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "client_id", + "fieldtype": "Data", + "label": "Client ID" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_0", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_0", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "provider_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Provider Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 1, - "unique": 0 - }, + "fieldname": "provider_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Provider Name", + "reqd": 1, + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "client_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": "Client Secret", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "client_secret", + "fieldtype": "Password", + "label": "Client Secret" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "eval:doc.custom_base_url", - "columns": 0, - "depends_on": "", - "fieldname": "sb_identity_details", - "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": "Identity Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.custom_base_url", + "fieldname": "sb_identity_details", + "fieldtype": "Section Break", + "label": "Identity Details" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "icon", - "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": "Icon", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "icon", + "fieldtype": "Data", + "label": "Icon" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_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, - "unique": 0 - }, + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_url", - "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": "Base 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, - "unique": 0 - }, + "fieldname": "base_url", + "fieldtype": "Data", + "label": "Base URL" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"", - "columns": 0, - "depends_on": "", - "fieldname": "client_urls", - "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": "Client URLs", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"", + "fieldname": "client_urls", + "fieldtype": "Section Break", + "label": "Client URLs" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "authorize_url", - "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": "Authorize 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, - "unique": 0 - }, + "fieldname": "authorize_url", + "fieldtype": "Data", + "label": "Authorize URL" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "access_token_url", - "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": "Access Token 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, - "unique": 0 - }, + "fieldname": "access_token_url", + "fieldtype": "Data", + "label": "Access Token URL" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "redirect_url", - "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": "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, - "unique": 0 - }, + "fieldname": "redirect_url", + "fieldtype": "Data", + "label": "Redirect URL" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "api_endpoint", - "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 Endpoint", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "api_endpoint", + "fieldtype": "Data", + "label": "API Endpoint" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "custom_base_url", - "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": "Custom Base 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, - "unique": 0 - }, + "default": "0", + "fieldname": "custom_base_url", + "fieldtype": "Check", + "hidden": 1, + "label": "Custom Base URL" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"", - "columns": 0, - "depends_on": "", - "fieldname": "client_information", - "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": "Client Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"", + "fieldname": "client_information", + "fieldtype": "Section Break", + "label": "Client Information" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "api_endpoint_args", - "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": "API Endpoint Args", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "api_endpoint_args", + "fieldtype": "Code", + "label": "API Endpoint Args" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "auth_url_data", - "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": "Auth URL Data", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "auth_url_data", + "fieldtype": "Code", + "label": "Auth URL Data" + }, + { + "depends_on": "eval:doc.social_login_provider===\"Custom\"", + "fieldname": "user_id_property", + "fieldtype": "Data", + "label": "User ID Property" } - ], - "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": "2018-09-15 09:00:00.000000", - "modified_by": "Administrator", - "module": "Integrations", - "name": "Social Login Key", - "name_case": "", - "owner": "Administrator", + ], + "modified": "2019-12-03 13:13:46.989099", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Social Login Key", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "provider_name", - "track_changes": 1, - "track_seen": 0 -} + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "provider_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index f1f50c7662..b70a3e2d8c 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -106,13 +106,12 @@ def get_webhook_headers(doc, webhook): def get_webhook_data(doc, webhook): data = {} + doc = doc.as_dict(convert_dates_to_str=True) + if webhook.webhook_data: - for w in webhook.webhook_data: - value = doc.get(w.fieldname) - if isinstance(value, datetime.datetime): - value = frappe.utils.get_datetime_str(value) - data[w.key] = value + data = {w.key: doc.get(w.fieldname) for w in webhook.webhook_data} elif webhook.webhook_json: data = frappe.render_template(webhook.webhook_json, get_context(doc)) data = json.loads(data) + return data diff --git a/frappe/migrate.py b/frappe/migrate.py index 6778a3f18f..043b6817d7 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -15,6 +15,7 @@ from frappe.desk.notifications import clear_notifications from frappe.website import render from frappe.core.doctype.language.language import sync_languages from frappe.modules.utils import sync_customizations +from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.utils import global_search def migrate(verbose=True, rebuild_website=False, skip_failing=False): @@ -46,9 +47,11 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False): # run patches frappe.modules.patch_handler.run_all(skip_failing) + # sync frappe.model.sync.sync_all(verbose=verbose) frappe.translate.clear_cache() + sync_jobs() sync_fixtures() sync_customizations() sync_languages() diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 09db9bb68a..1fe92d7a67 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -45,7 +45,7 @@ default_fields = ('doctype','name','owner','creation','modified','modified_by', 'parent','parentfield','parenttype','idx','docstatus') optional_fields = ("_user_tags", "_comments", "_assign", "_liked_by", "_seen") table_fields = ('Table', 'Table MultiSelect') -core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'User', 'Role', 'Has Role', +core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link', 'User', 'Role', 'Has Role', 'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form', 'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script') diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 6c917b8d4d..cbbe2b267e 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -22,6 +22,8 @@ max_positive_value = { 'bigint': 2 ** 63 } +DOCTYPES_FOR_DOCTYPE = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link') + _classes = {} def get_controller(doctype): @@ -255,7 +257,7 @@ class BaseDocument(object): def get_valid_columns(self): if self.doctype not in frappe.local.valid_columns: - if self.doctype in ("DocField", "DocPerm") and self.parent in ("DocType", "DocField", "DocPerm"): + if self.doctype in DOCTYPES_FOR_DOCTYPE: from frappe.model.meta import get_table_columns valid = get_table_columns(self.doctype) else: @@ -273,7 +275,7 @@ class BaseDocument(object): doc["doctype"] = self.doctype for df in self.meta.get_table_fields(): children = self.get(df.fieldname) or [] - doc[df.fieldname] = [d.as_dict(no_nulls=no_nulls) for d in children] + doc[df.fieldname] = [d.as_dict(convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls) for d in children] if no_nulls: for k in list(doc): @@ -312,7 +314,7 @@ class BaseDocument(object): self.created_by = self.modified_by = frappe.session.user # if doctype is "DocType", don't insert null values as we don't know who is valid yet - d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in ('DocType', 'DocField', 'DocPerm')) + d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE) columns = list(d) try: @@ -347,7 +349,7 @@ class BaseDocument(object): self.db_insert() return - d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in ('DocType', 'DocField', 'DocPerm')) + d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE) # don't update name, as case might've been changed name = d['name'] diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index 6dd0ff3eec..f697d8051a 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -61,7 +61,9 @@ def set_user_and_static_default_values(doc): user_default_value = get_user_default_value(df, defaults, doctype_user_permissions, allowed_records, default_doc) if user_default_value is not None: - doc.set(df.fieldname, user_default_value) + # if fieldtype is link check if doc exists + if not df.fieldtype == "Link" or frappe.db.exists(df.options, user_default_value): + doc.set(df.fieldname, user_default_value) else: if df.fieldname != doc.meta.title_field: diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index b7feee82f4..f9016d7fcf 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -501,6 +501,10 @@ class DatabaseQuery(object): value = f.value or "''" fallback = "''" + elif f.fieldname == 'name': + value = f.value or "''" + fallback = "''" + else: value = flt(f.value) fallback = 0 @@ -513,6 +517,8 @@ class DatabaseQuery(object): or not can_be_null or (f.value and f.operator.lower() in ('=', 'like')) or 'ifnull(' in column_name.lower()): + if f.operator.lower() == 'like' and frappe.conf.get('db_type') == 'postgres': + f.operator = 'ilike' condition = '{column_name} {operator} {value}'.format( column_name=column_name, operator=f.operator, value=value) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index af67350ab6..43ffc6ac35 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -80,8 +80,8 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa if not (for_reload or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_test): try: delete_controllers(name, doc.module) - except (FileNotFoundError, OSError): - # in case a doctype doesnt have any controller code + except (FileNotFoundError, OSError, KeyError): + # in case a doctype doesnt have any controller code nor any app and module pass else: @@ -332,8 +332,8 @@ def clear_references(doctype, reference_doctype, reference_name, (reference_doctype, reference_name)) def clear_timeline_references(link_doctype, link_name): - frappe.db.sql("""delete from `tabCommunication Link` - where `tabCommunication Link`.link_doctype='{0}' and `tabCommunication Link`.link_name='{1}'""".format(link_doctype, link_name)) # nosec + frappe.db.sql("""DELETE FROM `tabCommunication Link` + WHERE `tabCommunication Link`.link_doctype=%s AND `tabCommunication Link`.link_name=%s""", (link_doctype, link_name)) def insert_feed(doc): from frappe.utils import get_fullname diff --git a/frappe/model/document.py b/frappe/model/document.py index 7f04895308..c58e09ef5a 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -150,8 +150,8 @@ class Document(BaseDocument): super(Document, self).__init__(d) if self.name=="DocType" and self.doctype=="DocType": - from frappe.model.meta import doctype_table_fields - table_fields = doctype_table_fields + from frappe.model.meta import DOCTYPE_TABLE_FIELDS + table_fields = DOCTYPE_TABLE_FIELDS else: table_fields = self.meta.get_table_fields() diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 0b1011b119..927a56b6b8 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -151,7 +151,7 @@ class Meta(Document): if self.name!="DocType": self._table_fields = self.get('fields', {"fieldtype": ['in', table_fields]}) else: - self._table_fields = doctype_table_fields + self._table_fields = DOCTYPE_TABLE_FIELDS return self._table_fields @@ -165,7 +165,7 @@ class Meta(Document): def get_valid_columns(self): if not hasattr(self, "_valid_columns"): - if self.name in ("DocType", "DocField", "DocPerm", "Property Setter"): + if self.name in ("DocType", "DocField", "DocPerm", 'DocType Action', 'DocType Link', "Property Setter"): self._valid_columns = get_table_columns(self.name) else: self._valid_columns = self.default_fields + \ @@ -174,7 +174,12 @@ class Meta(Document): return self._valid_columns def get_table_field_doctype(self, fieldname): - return { "fields": "DocField", "permissions": "DocPerm"}.get(fieldname) + return { + "fields": "DocField", + "permissions": "DocPerm", + "actions": "DocType Action", + 'links': 'DocType Link' + }.get(fieldname) def get_field(self, fieldname): '''Return docfield from meta''' @@ -419,11 +424,44 @@ class Meta(Document): except ImportError: pass + self.add_doctype_links(data) + for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []): data = frappe.get_attr(hook)(data=data) return data + def add_doctype_links(self, data): + '''add `links` child table in standard link dashboard format''' + if hasattr(self, 'links') and self.links: + if not data.transactions: + # init groups + data.transactions = [] + data.non_standard_fieldnames = {} + + for link in self.links: + link.added = False + for group in data.transactions: + # group found + if group.label == link.label: + if not link.link_doctype in group.items: + group.items.append(link.link_doctype) + link.added = True + + if not link.added: + # group not found, make a new group + data.transactions.append(dict( + label = link.group, + items = [link.link_doctype] + )) + + if link.link_fieldname != data.fieldname: + if data.fieldname: + data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname + else: + data.fieldname = link.link_fieldname + + def get_row_template(self): return self.get_web_template(suffix='_row') @@ -441,9 +479,11 @@ class Meta(Document): def is_nested_set(self): return self.has_field('lft') and self.has_field('rgt') -doctype_table_fields = [ +DOCTYPE_TABLE_FIELDS = [ frappe._dict({"fieldname": "fields", "options": "DocField"}), - frappe._dict({"fieldname": "permissions", "options": "DocPerm"}) + frappe._dict({"fieldname": "permissions", "options": "DocPerm"}), + frappe._dict({"fieldname": "actions", "options": "DocType Action"}), + frappe._dict({"fieldname": "links", "options": "DocType Link"}), ] ####### diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 7dc3944750..a42b83fe97 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -27,7 +27,7 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne @frappe.whitelist() -def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=False, ignore_if_exists=False): +def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=False, ignore_if_exists=False, show_alert=True): """ Renames a doc(dt, old) to doc(dt, new) and updates all linked fields of type "Link" @@ -99,7 +99,9 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F frappe.clear_cache() frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype) - frappe.msgprint(_('Document renamed from {0} to {1}').format(bold(old), bold(new)), alert=True, indicator='green') + + if show_alert: + frappe.msgprint(_('Document renamed from {0} to {1}').format(bold(old), bold(new)), alert=True, indicator='green') return new diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 18bf827c5f..b50ddb1160 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -29,6 +29,8 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe # these need to go first at time of install for d in (("core", "docfield"), ("core", "docperm"), + ("core", "doctype_action"), + ("core", "doctype_link"), ("core", "role"), ("core", "has_role"), ("core", "doctype"), @@ -41,7 +43,10 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe ("data_migration", "data_migration_mapping_detail"), ("data_migration", "data_migration_mapping"), ("data_migration", "data_migration_plan_mapping"), - ("data_migration", "data_migration_plan")): + ("data_migration", "data_migration_plan"), + ("desk", "onboarding_slide_field"), + ("desk", "onboarding_slide_help_link"), + ("desk", "onboarding_slide")): files.append(os.path.join(frappe.get_app_path("frappe"), d[0], "doctype", d[1], d[1] + ".json")) @@ -70,7 +75,7 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F # load in sequence - warning for devs document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format', 'website_theme', 'web_form', 'notification', 'print_style', - 'data_migration_mapping', 'data_migration_plan'] + 'data_migration_mapping', 'data_migration_plan', 'onboarding_slide'] for doctype in document_types: doctype_path = os.path.join(start_path, doctype) if os.path.exists(doctype_path): diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 2851db6780..548d713e6f 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -105,6 +105,18 @@ def apply_workflow(doc, action): return doc +@frappe.whitelist() +def can_cancel_document(doc): + doc = frappe.get_doc(frappe.parse_json(doc)) + workflow = get_workflow(doc.doctype) + for state_doc in workflow.states: + if state_doc.doc_status == '2': + for transition in workflow.transitions: + if transition.next_state == state_doc.state: + return False + return True + return True + def validate_workflow(doc): '''Validate Workflow State and Transition for the current user. diff --git a/frappe/patches.txt b/frappe/patches.txt index d4aaec5bfc..1599fd50ac 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -6,9 +6,11 @@ frappe.patches.v8_0.update_global_search_table frappe.patches.v7_0.update_auth frappe.patches.v8_0.drop_in_dialog #2017-09-22 frappe.patches.v7_2.remove_in_filter -frappe.patches.v11_0.drop_column_apply_user_permissions +execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23 +execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2019-09-23 execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 +frappe.patches.v11_0.drop_column_apply_user_permissions execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') execute:frappe.reload_doc('core', 'doctype', 'docperm') #2018-05-29 execute:frappe.reload_doc('core', 'doctype', 'comment') @@ -253,7 +255,11 @@ frappe.patches.v12_0.move_email_and_phone_to_child_table frappe.patches.v12_0.delete_duplicate_indexes frappe.patches.v12_0.set_default_incoming_email_port frappe.patches.v12_0.update_global_search -execute:frappe.reload_doc('desk', 'doctype', 'notification_settings') frappe.patches.v12_0.setup_tags frappe.patches.v12_0.update_auto_repeat_status_and_not_submittable frappe.patches.v12_0.copy_to_parent_for_tags +frappe.patches.v12_0.create_notification_settings_for_user +frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26 +frappe.patches.v12_0.setup_email_linking +frappe.patches.v12_0.fix_home_settings_for_all_users +execute:frappe.delete_doc("Test Runner") \ No newline at end of file diff --git a/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py new file mode 100644 index 0000000000..f7b9e476a9 --- /dev/null +++ b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py @@ -0,0 +1,26 @@ +from __future__ import unicode_literals +import frappe + + +def execute(): + if frappe.db.count("File", filters={"attached_to_doctype": "Prepared Report", "is_private": 0}) > 10000: + frappe.db.auto_commit_on_many_writes = True + + files = frappe.get_all("File", fields=["name", "attached_to_name"], filters={"attached_to_doctype": "Prepared Report", "is_private": 0}) + for file_dict in files: + # For some reason Prepared Report doc might not exist, check if it exists first + if frappe.db.exists("Prepared Report", file_dict.attached_to_name): + try: + file_doc = frappe.get_doc("File", file_dict.name) + file_doc.is_private = 1 + file_doc.save() + except Exception: + # File might not exist on the file system in that case delete both Prepared Report and File doc + frappe.delete_doc("Prepared Report", file_dict.attached_to_name) + else: + # If Prepared Report doc doesn't exist then the file doc is useless. Delete it. + frappe.delete_doc("File", file_dict.name) + + if frappe.db.auto_commit_on_many_writes: + frappe.db.auto_commit_on_many_writes = False + diff --git a/frappe/patches/v12_0/create_notification_settings_for_user.py b/frappe/patches/v12_0/create_notification_settings_for_user.py new file mode 100644 index 0000000000..63eeccc07a --- /dev/null +++ b/frappe/patches/v12_0/create_notification_settings_for_user.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals +import frappe +from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings + +def execute(): + frappe.reload_doc('desk', 'doctype', 'notification_settings') + frappe.reload_doc('desk', 'doctype', 'notification_subscribed_document') + + users = frappe.db.get_all('User', fields=['name']) + for user in users: + create_notification_settings(user.name) \ No newline at end of file diff --git a/frappe/patches/v12_0/fix_home_settings_for_all_users.py b/frappe/patches/v12_0/fix_home_settings_for_all_users.py new file mode 100644 index 0000000000..e26cbd9eef --- /dev/null +++ b/frappe/patches/v12_0/fix_home_settings_for_all_users.py @@ -0,0 +1,41 @@ +import frappe +from frappe.config import get_modules_from_all_apps_for_user +import json +def execute(): + users = frappe.get_all('User', fields=['name', 'home_settings']) + + for user in users: + + if not user.home_settings: + continue + + home_settings = json.loads(user.home_settings) + + modules_by_category = home_settings.get('modules_by_category') + if not modules_by_category: + continue + visible_modules = [] + category_to_check = [] + + for category, modules in modules_by_category.items(): + visible_modules += modules + category_to_check.append(category) + + all_modules = get_modules_from_all_apps_for_user(user.name) + all_modules = set([m.get('name') or m.get('module_name') or m.get('label') \ + for m in all_modules if m.get('category') in category_to_check]) + + hidden_modules = home_settings.get("hidden_modules", []) + + modules_in_home_settings = set(visible_modules + hidden_modules) + + all_modules = all_modules.union(modules_in_home_settings) + + missing_modules = all_modules - modules_in_home_settings + + if missing_modules: + home_settings['hidden_modules'] = hidden_modules + list(missing_modules) + home_settings = json.dumps(home_settings) + frappe.set_value('User', user.name, 'home_settings', home_settings) + + frappe.cache().delete_key('home_settings') diff --git a/frappe/patches/v12_0/setup_email_linking.py b/frappe/patches/v12_0/setup_email_linking.py new file mode 100644 index 0000000000..08f57ca5e4 --- /dev/null +++ b/frappe/patches/v12_0/setup_email_linking.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals + +from frappe.desk.page.setup_wizard.install_fixtures import setup_email_linking + +def execute(): + setup_email_linking() \ No newline at end of file diff --git a/frappe/printing/doctype/letter_head/letter_head.py b/frappe/printing/doctype/letter_head/letter_head.py index 1a70ebcb08..d75b253dd3 100644 --- a/frappe/printing/doctype/letter_head/letter_head.py +++ b/frappe/printing/doctype/letter_head/letter_head.py @@ -4,8 +4,7 @@ from __future__ import unicode_literals import frappe from frappe.utils import is_image - - +from frappe import _ from frappe.model.document import Document class LetterHead(Document): @@ -43,3 +42,16 @@ class LetterHead(Document): # update control panel - so it loads new letter directly frappe.db.set_default("default_letter_head_content", self.content) + + def create_onboarding_docs(self, args): + letterhead = args.get('letterhead') + if letterhead: + try: + frappe.get_doc({ + 'doctype': self.doctype, + 'image': letterhead, + 'letter_head_name': _('Standard'), + 'is_default': 1 + }).insert() + except frappe.NameError: + pass \ No newline at end of file diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index a5d41ee099..951a863776 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -1,905 +1,223 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2013-01-23 19:54:43", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 0, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fetch_if_empty": 0, - "fieldname": "doc_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "DocType", - "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": 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": "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": 0, - "label": "Module", - "length": 0, - "no_copy": 0, - "options": "Module Def", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "disabled", - "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": "Disabled", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_3", - "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 - }, - { - "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": "standard", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Standard", - "length": 0, - "no_copy": 1, - "oldfieldname": "standard", - "oldfieldtype": "Select", - "options": "No\nYes", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "custom_format", - "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": "Custom Format", - "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": "custom_format", - "fetch_if_empty": 0, - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "Jinja", - "depends_on": "custom_format", - "description": "", - "fetch_if_empty": 0, - "fieldname": "print_format_type", - "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": "Print Format Type", - "length": 0, - "no_copy": 0, - "options": "Jinja\nJS", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "raw_printing", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Raw Printing", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.raw_printing", - "fetch_if_empty": 0, - "fieldname": "html", - "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": "HTML", - "length": 0, - "no_copy": 0, - "oldfieldname": "html", - "oldfieldtype": "Text Editor", - "options": "HTML", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "raw_printing", - "description": "Any string-based printer languages can be used. Writing raw commands requires knowledge of the printer's native language provided by the printer manufacturer. Please refer to the developer manual provided by the printer manufacturer on how to write their native commands. These commands are rendered on the server side using the Jinja Templating Language.", - "fetch_if_empty": 0, - "fieldname": "raw_commands", - "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": "Raw Commands", - "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:!doc.custom_format", - "fetch_if_empty": 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, - "label": "Style 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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "align_labels_right", - "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": "Align Labels to the Right", - "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": "0", - "fetch_if_empty": 0, - "fieldname": "show_section_headings", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Show Section Headings", - "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": "0", - "fetch_if_empty": 0, - "fieldname": "line_breaks", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Show Line Breaks after Sections", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_11", - "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, - "fetch_if_empty": 0, - "fieldname": "default_print_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": "Default Print Language", - "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, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Default", - "depends_on": "eval:!doc.custom_format", - "fetch_if_empty": 0, - "fieldname": "font", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Font", - "length": 0, - "no_copy": 0, - "options": "Default\nArial\nHelvetica\nVerdana\nMonospace", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.raw_printing", - "fetch_if_empty": 0, - "fieldname": "css_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, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "css", - "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": "Custom CSS", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "custom_html_help", - "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": "Custom HTML Help", - "length": 0, - "no_copy": 0, - "options": "

Custom CSS Help

\n\n

Notes:

\n\n
    \n
  1. All field groups (label + value) are set attributes data-fieldtype and data-fieldname
  2. \n
  3. All values are given class value
  4. \n
  5. All Section Breaks are given class section-break
  6. \n
  7. All Column Breaks are given class column-break
  8. \n
\n\n

Examples

\n\n

1. Left align integers

\n\n
[data-fieldtype=\"Int\"] .value { text-left: left; }
\n\n

1. Add border to sections except the last section

\n\n
.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px;  }
\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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "custom_format", - "fetch_if_empty": 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, - "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": "custom_format", - "fetch_if_empty": 0, - "fieldname": "print_format_help", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Print Format Help", - "length": 0, - "no_copy": 0, - "options": "

Print Format Help

\n
\n

Introduction

\n

Print itemsFormats are rendered on the server side using the Jinja Templating Language. All forms have access to the doc object which contains information about the document that is being formatted. You can also access common utilities via the frappe module.

\n

For styling, the Boostrap CSS framework is provided and you can enjoy the full range of classes.

\n
\n

References

\n
    \n\t
  1. Jinja Tempalting Language: Reference
  2. \n\t
  3. Bootstrap CSS Framework
  4. \n
\n
\n

Example

\n
<h3>{{ doc.select_print_heading or \"Invoice\" }}</h3>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Customer Name</div>\n\t<div class=\"col-md-9\">{{ doc.customer_name }}</div>\n</div>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Date</div>\n\t<div class=\"col-md-9\">{{ doc.get_formatted(\"invoice_date\") }}</div>\n</div>\n<table class=\"table table-bordered\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<th>Sr</th>\n\t\t\t<th>Item Name</th>\n\t\t\t<th>Description</th>\n\t\t\t<th class=\"text-right\">Qty</th>\n\t\t\t<th class=\"text-right\">Rate</th>\n\t\t\t<th class=\"text-right\">Amount</th>\n\t\t</tr>\n\t\t{%- for row in doc.items -%}\n\t\t<tr>\n\t\t\t<td style=\"width: 3%;\">{{ row.idx }}</td>\n\t\t\t<td style=\"width: 20%;\">\n\t\t\t\t{{ row.item_name }}\n\t\t\t\t{% if row.item_code != row.item_name -%}\n\t\t\t\t<br>Item Code: {{ row.item_code}}\n\t\t\t\t{%- endif %}\n\t\t\t</td>\n\t\t\t<td style=\"width: 37%;\">\n\t\t\t\t<div style=\"border: 0px;\">{{ row.description }}</div></td>\n\t\t\t<td style=\"width: 10%; text-align: right;\">{{ row.qty }} {{ row.uom or row.stock_uom }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"rate\", doc) }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"amount\", doc) }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>
\n
\n

Common Functions

\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
doc.get_formatted(\"[fieldname]\", [parent_doc])Get document value formatted as Date, Currency etc. Pass parent doc for curreny type fields.
frappe.db.get_value(\"[doctype]\", \"[name]\", \"fieldname\")Get value from another document.
\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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "format_data", - "fieldtype": "Code", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Format Data", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "print_format_builder", - "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": "Print Format Builder", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "icon": "fa fa-print", - "idx": 1, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-06-05 12:45:25.869180", - "modified_by": "Administrator", - "module": "Printing", - "name": "Print Format", - "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": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 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 + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2013-01-23 19:54:43", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "doc_type", + "module", + "disabled", + "column_break_3", + "standard", + "custom_format", + "section_break_6", + "print_format_type", + "raw_printing", + "html", + "raw_commands", + "section_break_9", + "align_labels_right", + "show_section_headings", + "line_breaks", + "column_break_11", + "default_print_language", + "font", + "css_section", + "css", + "custom_html_help", + "section_break_13", + "print_format_help", + "format_data", + "print_format_builder" + ], + "fields": [ + { + "fieldname": "doc_type", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "No", + "fieldname": "standard", + "fieldtype": "Select", + "in_filter": 1, + "label": "Standard", + "no_copy": 1, + "oldfieldname": "standard", + "oldfieldtype": "Select", + "options": "No\nYes", + "reqd": 1, + "search_index": 1 + }, + { + "default": "0", + "fieldname": "custom_format", + "fieldtype": "Check", + "label": "Custom Format" + }, + { + "depends_on": "custom_format", + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "default": "Jinja", + "depends_on": "custom_format", + "fieldname": "print_format_type", + "fieldtype": "Select", + "label": "Print Format Type", + "options": "Jinja\nJS" + }, + { + "default": "0", + "fieldname": "raw_printing", + "fieldtype": "Check", + "label": "Raw Printing" + }, + { + "depends_on": "eval:!doc.raw_printing", + "fieldname": "html", + "fieldtype": "Code", + "label": "HTML", + "oldfieldname": "html", + "oldfieldtype": "Text Editor", + "options": "HTML" + }, + { + "depends_on": "raw_printing", + "description": "Any string-based printer languages can be used. Writing raw commands requires knowledge of the printer's native language provided by the printer manufacturer. Please refer to the developer manual provided by the printer manufacturer on how to write their native commands. These commands are rendered on the server side using the Jinja Templating Language.", + "fieldname": "raw_commands", + "fieldtype": "Code", + "label": "Raw Commands" + }, + { + "depends_on": "eval:!doc.custom_format", + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Style Settings" + }, + { + "default": "0", + "fieldname": "align_labels_right", + "fieldtype": "Check", + "label": "Align Labels to the Right" + }, + { + "default": "0", + "fieldname": "show_section_headings", + "fieldtype": "Check", + "label": "Show Section Headings" + }, + { + "default": "0", + "fieldname": "line_breaks", + "fieldtype": "Check", + "label": "Show Line Breaks after Sections" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_print_language", + "fieldtype": "Link", + "label": "Default Print Language", + "options": "Language" + }, + { + "default": "Default", + "depends_on": "eval:!doc.custom_format", + "fieldname": "font", + "fieldtype": "Select", + "label": "Font", + "options": "Default\nArial\nHelvetica\nVerdana\nMonospace" + }, + { + "depends_on": "eval:!doc.raw_printing", + "fieldname": "css_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "css", + "fieldtype": "Code", + "label": "Custom CSS", + "options": "CSS" + }, + { + "fieldname": "custom_html_help", + "fieldtype": "HTML", + "label": "Custom HTML Help", + "options": "

Custom CSS Help

\n\n

Notes:

\n\n
    \n
  1. All field groups (label + value) are set attributes data-fieldtype and data-fieldname
  2. \n
  3. All values are given class value
  4. \n
  5. All Section Breaks are given class section-break
  6. \n
  7. All Column Breaks are given class column-break
  8. \n
\n\n

Examples

\n\n

1. Left align integers

\n\n
[data-fieldtype=\"Int\"] .value { text-left: left; }
\n\n

1. Add border to sections except the last section

\n\n
.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px;  }
\n" + }, + { + "depends_on": "custom_format", + "fieldname": "section_break_13", + "fieldtype": "Section Break" + }, + { + "depends_on": "custom_format", + "fieldname": "print_format_help", + "fieldtype": "HTML", + "label": "Print Format Help", + "options": "

Print Format Help

\n
\n

Introduction

\n

Print itemsFormats are rendered on the server side using the Jinja Templating Language. All forms have access to the doc object which contains information about the document that is being formatted. You can also access common utilities via the frappe module.

\n

For styling, the Boostrap CSS framework is provided and you can enjoy the full range of classes.

\n
\n

References

\n
    \n\t
  1. Jinja Tempalting Language: Reference
  2. \n\t
  3. Bootstrap CSS Framework
  4. \n
\n
\n

Example

\n
<h3>{{ doc.select_print_heading or \"Invoice\" }}</h3>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Customer Name</div>\n\t<div class=\"col-md-9\">{{ doc.customer_name }}</div>\n</div>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Date</div>\n\t<div class=\"col-md-9\">{{ doc.get_formatted(\"invoice_date\") }}</div>\n</div>\n<table class=\"table table-bordered\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<th>Sr</th>\n\t\t\t<th>Item Name</th>\n\t\t\t<th>Description</th>\n\t\t\t<th class=\"text-right\">Qty</th>\n\t\t\t<th class=\"text-right\">Rate</th>\n\t\t\t<th class=\"text-right\">Amount</th>\n\t\t</tr>\n\t\t{%- for row in doc.items -%}\n\t\t<tr>\n\t\t\t<td style=\"width: 3%;\">{{ row.idx }}</td>\n\t\t\t<td style=\"width: 20%;\">\n\t\t\t\t{{ row.item_name }}\n\t\t\t\t{% if row.item_code != row.item_name -%}\n\t\t\t\t<br>Item Code: {{ row.item_code}}\n\t\t\t\t{%- endif %}\n\t\t\t</td>\n\t\t\t<td style=\"width: 37%;\">\n\t\t\t\t<div style=\"border: 0px;\">{{ row.description }}</div></td>\n\t\t\t<td style=\"width: 10%; text-align: right;\">{{ row.qty }} {{ row.uom or row.stock_uom }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"rate\", doc) }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"amount\", doc) }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>
\n
\n

Common Functions

\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
doc.get_formatted(\"[fieldname]\", [parent_doc])Get document value formatted as Date, Currency etc. Pass parent doc for curreny type fields.
frappe.db.get_value(\"[doctype]\", \"[name]\", \"fieldname\")Get value from another document.
\n" + }, + { + "fieldname": "format_data", + "fieldtype": "Code", + "hidden": 1, + "label": "Format Data" + }, + { + "default": "0", + "fieldname": "print_format_builder", + "fieldtype": "Check", + "hidden": 1, + "label": "Print Format Builder" + } + ], + "icon": "fa fa-print", + "idx": 1, + "modified": "2019-11-28 12:40:40.364699", + "modified_by": "faris@erpnext.com", + "module": "Printing", + "name": "Print Format", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json b/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json new file mode 100644 index 0000000000..e0125bad3c --- /dev/null +++ b/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json @@ -0,0 +1,37 @@ +{ + "add_more_button": 0, + "app": "ERPNext", + "creation": "2019-11-22 13:25:42.892593", + "docstatus": 0, + "doctype": "Onboarding Slide", + "domains": [], + "help_links": [ + { + "label": "Need Help?", + "video_id": "cKZHcx1znMc" + } + ], + "idx": 0, + "image_src": "", + "is_completed": 1, + "max_count": 0, + "modified": "2019-12-09 15:12:45.588567", + "modified_by": "Administrator", + "name": "Company Letter Head", + "owner": "Administrator", + "ref_doctype": "Letter Head", + "slide_desc": "

The letter head will appear across all print formats and PDFs

\n

Keep it web friendly as 1024px by 128px

", + "slide_fields": [ + { + "align": "center", + "fieldname": "letterhead", + "fieldtype": "Attach Image", + "label": "Attach Letterhead", + "options": "image", + "reqd": 0 + } + ], + "slide_order": 20, + "slide_title": "Company Letter Head", + "slide_type": "Create" +} \ No newline at end of file diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index 8605fb762e..f9bb524d54 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -695,7 +695,8 @@ frappe.PrintFormatBuilder = Class.extend({ { fieldname: "content", fieldtype: "Code", - label: label + label: label, + options: "HTML" }, { fieldname: "help", diff --git a/frappe/public/build.json b/frappe/public/build.json index ddca0bbc7e..9085606caf 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -18,6 +18,9 @@ "js/frappe-recorder.min.js": [ "public/js/frappe/recorder/recorder.js" ], + "js/checkout.min.js": [ + "public/js/integrations/razorpay.js" + ], "js/frappe-web.min.js": [ "public/js/frappe/class.js", "public/js/frappe/polyfill.js", @@ -49,6 +52,22 @@ "website/js/bootstrap-4.js" ], "js/control.min.js": [ + "node_modules/air-datepicker/dist/js/datepicker.min.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.cs.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.da.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.de.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.en.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.es.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.fi.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.fr.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.hu.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.nl.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.pl.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.pt-BR.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.pt.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.ro.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.sk.js", + "node_modules/air-datepicker/dist/js/i18n/datepicker.zh.js", "public/js/frappe/ui/capture.js", "public/js/frappe/form/controls/control.js" ], @@ -111,8 +130,6 @@ "public/js/lib/socket.io.min.js", "public/js/lib/jSignature.min.js", "public/js/frappe/translate.js", - "public/js/lib/datepicker/datepicker.min.js", - "public/js/lib/datepicker/locale-all.js", "public/js/lib/leaflet/leaflet.js", "public/js/lib/leaflet/leaflet.draw.js", "public/js/lib/leaflet/L.Control.Locate.js", @@ -145,6 +162,7 @@ "public/js/frappe/ui/page.html", "public/js/frappe/ui/page.js", "public/js/frappe/ui/slides.js", + "public/js/frappe/ui/onboarding_dialog.js", "public/js/frappe/ui/find.js", "public/js/frappe/ui/iconbar.js", "public/js/frappe/form/layout.js", @@ -310,9 +328,7 @@ ], "js/web_form.min.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" + "public/js/frappe/web_form/webform_script.js" ], "css/web_form.css": [ "public/less/list.less", diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index 8e43be12ea..6ddf93df6a 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -1158,16 +1158,6 @@ input[type="checkbox"]:focus { color: #fff; border-color: #b1bdca; } -.user-progress-dialog .slides-progress { +.onboarding-dialog .slides-progress { margin-top: 15px; } -.user-progress-dialog .done-state .check-container { - font-size: 64px; - margin: 40px; -} -.user-progress-dialog .done-state .title { - font-weight: normal; -} -.user-progress-dialog .done-state .help-links a { - margin: 0px 10px; -} diff --git a/frappe/public/images/ui-states/empty.png b/frappe/public/images/ui-states/empty.png new file mode 100644 index 0000000000..de2a893508 Binary files /dev/null and b/frappe/public/images/ui-states/empty.png differ diff --git a/frappe/public/js/frappe/change_log.html b/frappe/public/js/frappe/change_log.html index c05aadfe27..fce6539abc 100644 --- a/frappe/public/js/frappe/change_log.html +++ b/frappe/public/js/frappe/change_log.html @@ -6,11 +6,13 @@ {{ app_info.title }} {{ __("updated to {0}", [app_info.version]) }} +
{% for (var x=0, y=app_info.change_log.length; x < y; x++) { var version_info = app_info.change_log[x]; if(version_info) { %}

{{ frappe.markdown(version_info[1]) }}

{% } } %} +
{% } %} diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js index f451227815..d15f2183a9 100644 --- a/frappe/public/js/frappe/chat.js +++ b/frappe/public/js/frappe/chat.js @@ -718,7 +718,7 @@ frappe.chat.room.create = function (kind, owner, users, name, fn) { return new Promise(resolve => { frappe.call("frappe.chat.doctype.chat_room.chat_room.create", - { kind: kind, owner: owner || frappe.session.user, users: users, name: name }, + { kind: kind, token: owner || frappe.session.user, users: users, name: name }, r => { let room = r.message room = { ...room, creation: new frappe.datetime.datetime(room.creation) } diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 4fbea6684f..23d7bffec2 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -88,6 +88,9 @@ frappe.Application = Class.extend({ } this.show_update_available(); + if (frappe.boot.is_first_startup) { + this.setup_onboarding_wizard(); + } if(frappe.ui.startup_setup_dialog && !frappe.boot.setup_complete) { frappe.ui.startup_setup_dialog.pre_show(); @@ -463,12 +466,27 @@ frappe.Application = Class.extend({ show_change_log: function() { var me = this; - var d = frappe.msgprint( - frappe.render_template("change_log", {"change_log": frappe.boot.change_log}), - __("Updated To New Version") - ); - d.keep_open = true; - d.custom_onhide = function() { + let change_log = frappe.boot.change_log; + + // frappe.boot.change_log = [{ + // "change_log": [ + // [, ], + // [, ], + // ], + // "description": "ERP made simple", + // "title": "ERPNext", + // "version": "12.2.0" + // }]; + + // Iterate over changelog + var change_log_dialog = frappe.msgprint({ + message: frappe.render_template("change_log", {"change_log": change_log}), + title: __("Updated To New Version 🎉"), + wide: true, + scroll: true + }); + change_log_dialog.keep_open = true; + change_log_dialog.custom_onhide = function() { frappe.call({ "method": "frappe.utils.change_log.update_last_known_versions" }); @@ -482,6 +500,20 @@ frappe.Application = Class.extend({ }); }, + setup_onboarding_wizard: () => { + frappe.call('frappe.desk.doctype.onboarding_slide.onboarding_slide.get_onboarding_slides').then(res => { + if (res.message) { + let slides = res.message; + if (slides.length) { + this.progress_dialog = new frappe.setup.OnboardingDialog({ + slides: slides + }); + this.progress_dialog.show(); + } + } + }); + }, + setup_analytics: function() { if(window.mixpanel) { window.mixpanel.identify(frappe.session.user); diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index 6411c2fb2b..819ecb526e 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -291,7 +291,7 @@ frappe.get_modal = function(title, content) {