+
+## Table of Contents
+* [Installation](#installation)
+* [Contributing](#contributing)
+* [Resources](#resources)
* [License](#license)
-### Installation
+## Installation
* [Install via Docker](https://github.com/frappe/frappe_docker)
* [Install via Frappe Bench](https://github.com/frappe/bench)
+* [Offical Documentation](https://frappeframework.com/docs/user/en/installation)
+* [Managed Hosting on Frappe Cloud](https://frappecloud.com/deploy?apps=frappe&source=frappe_readme)
## Contributing
+1. [Code of Conduct](CODE_OF_CONDUCT.md)
1. [Contribution Guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
+1. [Security Policy](SECURITY.md)
1. [Translations](https://translate.erpnext.com)
-### Website
+## Resources
-For details and documentation, see the website
-[https://frappeframework.com](https://frappeframework.com)
+1. [frappeframework.com](https://frappeframework.com) - Official documentation of the Frappe Framework.
+1. [frappe.school](https://frappe.school) - Pick from the various courses by the maintainers or from the community.
-### License
+## License
This repository has been released under the [MIT License](LICENSE).
diff --git a/codecov.yml b/codecov.yml
index eb81252b61..a9f6df0296 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1,9 +1,27 @@
codecov:
require_ci_to_pass: yes
+
+coverage:
status:
+ patch: off
project:
- default:
+ default: false
+ server:
+ target: auto
threshold: 0.5%
+ flags:
+ - server
+
comment:
- layout: "diff, flags, files"
+ layout: "diff, flags"
require_changes: true
+
+flags:
+ server:
+ paths:
+ - ".*\\.py"
+ carryforward: true
+ ui-tests:
+ paths:
+ - ".*\\.js"
+ carryforward: true
diff --git a/cypress/fixtures/doctype_with_tab_break.js b/cypress/fixtures/doctype_with_tab_break.js
new file mode 100644
index 0000000000..bc346e8fb8
--- /dev/null
+++ b/cypress/fixtures/doctype_with_tab_break.js
@@ -0,0 +1,59 @@
+export default {
+ name: 'Form With Tab Break',
+ custom: 1,
+ actions: [],
+ doctype: 'DocType',
+ engine: 'InnoDB',
+ fields: [
+ {
+ fieldname: 'username',
+ fieldtype: 'Data',
+ label: 'Name',
+ options: 'Name'
+ },
+ {
+ fieldname: 'tab',
+ fieldtype: 'Tab Break',
+ label: 'Tab 2',
+ },
+ {
+ fieldname: 'Phone',
+ fieldtype: 'Data',
+ label: 'Phone',
+ options: 'Phone',
+ reqd: 1
+ },
+ ],
+ links: [
+ {
+ "group": "Profile",
+ "link_doctype": "Contact",
+ "link_fieldname": "user"
+ },
+ {
+ "group": "Profile",
+ "link_doctype": "Chat Profile",
+ "link_fieldname": "user"
+ },
+ ],
+ 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,
+ autoname: "format: Test-{####}",
+ sort_field: 'modified',
+ sort_order: 'ASC',
+ track_changes: 1
+};
diff --git a/cypress/integration/api.js b/cypress/integration/api.js
index 7a5b1611b0..e8c39e6e25 100644
--- a/cypress/integration/api.js
+++ b/cypress/integration/api.js
@@ -31,8 +31,13 @@ context('API Resources', () => {
});
it('Removes the Comments', () => {
- cy.get_list('Comment').then(body => body.data.forEach(comment => {
- cy.remove_doc('Comment', comment.name);
- }));
+ cy.get_list('Comment').then(body => {
+ let comment_names = [];
+ body.data.map(comment => comment_names.push(comment.name));
+ comment_names = [...new Set(comment_names)]; // remove duplicates
+ comment_names.forEach((comment_name) => {
+ cy.remove_doc('Comment', comment_name);
+ });
+ });
});
});
diff --git a/cypress/integration/control_float.js b/cypress/integration/control_float.js
new file mode 100644
index 0000000000..670d1fe73e
--- /dev/null
+++ b/cypress/integration/control_float.js
@@ -0,0 +1,93 @@
+context("Control Float", () => {
+ before(() => {
+ cy.login();
+ cy.visit("/app/website");
+ });
+
+ function get_dialog_with_float() {
+ return cy.dialog({
+ title: "Float Check",
+ fields: [
+ {
+ fieldname: "float_number",
+ fieldtype: "Float",
+ Label: "Float"
+ }
+ ]
+ });
+ }
+
+ it("check value changes", () => {
+ get_dialog_with_float().as("dialog");
+
+ let data = get_data();
+ data.forEach(x => {
+ cy.window()
+ .its("frappe")
+ .then(frappe => {
+ frappe.boot.sysdefaults.number_format = x.number_format;
+ });
+ x.values.forEach(d => {
+ cy.get_field("float_number", "Float").clear();
+ cy.fill_field("float_number", d.input, "Float").blur();
+ cy.get_field("float_number", "Float").should(
+ "have.value",
+ d.blur_expected
+ );
+
+ cy.get_field("float_number", "Float").focus();
+ cy.get_field("float_number", "Float").blur();
+ cy.get_field("float_number", "Float").focus();
+ cy.get_field("float_number", "Float").should(
+ "have.value",
+ d.focus_expected
+ );
+ });
+ });
+ });
+
+ function get_data() {
+ return [
+ {
+ number_format: "#.###,##",
+ values: [
+ {
+ input: "364.87,334",
+ blur_expected: "36.487,334",
+ focus_expected: "36487.334"
+ },
+ {
+ input: "36487,334",
+ blur_expected: "36.487,334",
+ focus_expected: "36487.334"
+ },
+ {
+ input: "100",
+ blur_expected: "100,000",
+ focus_expected: "100"
+ }
+ ]
+ },
+ {
+ number_format: "#,###.##",
+ values: [
+ {
+ input: "364,87.334",
+ blur_expected: "36,487.334",
+ focus_expected: "36487.334"
+ },
+ {
+ input: "36487.334",
+ blur_expected: "36,487.334",
+ focus_expected: "36487.334"
+ },
+ {
+ input: "100",
+ blur_expected: "100.000",
+ focus_expected: "100"
+ }
+ ]
+ }
+ ];
+ }
+});
diff --git a/cypress/integration/dashboard_links.js b/cypress/integration/dashboard_links.js
index b77965ee1a..16ffd41cf4 100644
--- a/cypress/integration/dashboard_links.js
+++ b/cypress/integration/dashboard_links.js
@@ -9,17 +9,20 @@ context('Dashboard links', () => {
cy.clear_filters();
cy.visit('/app/user');
- cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
+ cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click({ force: true });
//To check if initially the dashboard contains only the "Contact" link and there is no counter
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
//Adding a new contact
- cy.get('.btn[data-doctype="Contact"]').click();
+ cy.get('.document-link-badge[data-doctype="Contact"]').click();
+ cy.wait(300);
+ cy.findByRole('button', {name: 'Add Contact'}).should('be.visible');
+ cy.findByRole('button', {name: 'Add Contact'}).click();
cy.get('[data-doctype="Contact"][data-fieldname="first_name"]').type('Admin');
cy.findByRole('button', {name: 'Save'}).click();
cy.visit('/app/user');
- cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
+ cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click({ force: true });
//To check if the counter for contact doc is "1" after adding the contact
cy.get('[data-doctype="Contact"] > .count').should('contain', '1');
@@ -27,7 +30,7 @@ context('Dashboard links', () => {
//Deleting the newly created contact
cy.visit('/app/contact');
- cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click();
+ cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click({ force: true });
cy.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
cy.findByRole('button', {name: 'Yes'}).click({delay: 700});
@@ -36,7 +39,7 @@ context('Dashboard links', () => {
//To check if the counter from the "Contact" doc link is removed
cy.wait(700);
cy.visit('/app/user');
- cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
+ cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click({ force: true });
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
});
@@ -51,13 +54,12 @@ context('Dashboard links', () => {
cur_frm.dashboard.data.reports = [
{
'label': 'Reports',
- 'items': ['Permitted Documents For User']
+ 'items': ['Website Analytics']
}
];
cur_frm.dashboard.render_report_links();
- cy.get('[data-report="Permitted Documents For User"]').contains('Permitted Documents For User').click();
- cy.findByText('Permitted Documents For User');
- cy.findByPlaceholderText('User').should("have.value", "Administrator");
+ cy.get('[data-report="Website Analytics"]').contains('Website Analytics').click();
+ cy.findByText('Website Analytics');
});
});
});
diff --git a/cypress/integration/datetime_field_form_validation.js b/cypress/integration/datetime_field_form_validation.js
index 66fdde6863..ef47a0fbf7 100644
--- a/cypress/integration/datetime_field_form_validation.js
+++ b/cypress/integration/datetime_field_form_validation.js
@@ -1,19 +1,19 @@
-context('Datetime Field Validation', () => {
- before(() => {
- cy.login();
- cy.visit('/app/communication');
- cy.window().its('frappe').then(frappe => {
- frappe.call("frappe.tests.ui_test_helpers.create_communication_records");
- });
- });
+// TODO: Enable this again
+// currently this is flaky possibly because of different timezone in CI
- // validating datetime field value when value is set from backend and get validated on form load.
- it('datetime field form validation', () => {
- cy.visit('/app/communication');
- cy.get('a[title="Test Form Communication 1"]').invoke('attr', 'data-name')
- .then((name) => {
- cy.visit(`/app/communication/${name}`);
- cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red');
- });
- });
-});
\ No newline at end of file
+// context('Datetime Field Validation', () => {
+// before(() => {
+// cy.login();
+// cy.visit('/app/communication');
+// });
+
+// it('datetime field form validation', () => {
+// // validating datetime field value when value is set from backend and get validated on form load.
+// cy.window().its('frappe').then(frappe => {
+// return frappe.xcall("frappe.tests.ui_test_helpers.create_communication_record");
+// }).then(doc => {
+// cy.visit(`/app/communication/${doc.name}`);
+// cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red');
+// });
+// });
+// });
\ No newline at end of file
diff --git a/cypress/integration/discussions.js b/cypress/integration/discussions.js
new file mode 100644
index 0000000000..a6e0ff9b56
--- /dev/null
+++ b/cypress/integration/discussions.js
@@ -0,0 +1,79 @@
+context('Discussions', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app');
+ return cy.window().its('frappe').then(frappe => {
+ return frappe.call('frappe.tests.ui_test_helpers.create_data_for_discussions');
+ });
+ });
+
+ const reply_through_modal = () => {
+ cy.visit('/test-page-discussions');
+
+ // Open the modal
+ cy.get('.reply').click();
+ cy.wait(500);
+ cy.get('.discussion-modal').should('be.visible');
+
+ // Enter title
+ cy.get('.modal .topic-title').type('Discussion from tests')
+ .should('have.value', 'Discussion from tests');
+
+ // Enter comment
+ cy.get('.modal .comment-field')
+ .type('This is a discussion from the cypress ui tests.')
+ .should('have.value', 'This is a discussion from the cypress ui tests.');
+
+ // Submit
+ cy.get('.modal .submit-discussion').click();
+ cy.wait(2000);
+
+ // Check if discussion is added to page and content is visible
+ cy.get('.sidebar-parent:first .discussion-topic-title').should('have.text', 'Discussion from tests');
+ cy.get('.discussion-on-page:visible').should('have.class', 'show');
+ cy.get('.discussion-on-page:visible .reply-card .reply-text')
+ .should('have.text', 'This is a discussion from the cypress ui tests.\n');
+
+ };
+
+ const reply_through_comment_box = () => {
+ cy.get('.discussion-on-page:visible .comment-field')
+ .type('This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.')
+ .should('have.value', 'This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.');
+
+ cy.get('.discussion-on-page:visible .submit-discussion').click();
+ cy.wait(3000);
+ cy.get('.discussion-on-page:visible').should('have.class', 'show');
+ cy.get('.discussion-on-page:visible').children(".reply-card").eq(1).children(".reply-text")
+ .should('have.text', 'This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.\n');
+ };
+
+ const cancel_and_clear_comment_box = () => {
+ cy.get('.discussion-on-page:visible .comment-field')
+ .type('This is a discussion from the cypress ui tests.')
+ .should('have.value', 'This is a discussion from the cypress ui tests.');
+
+ cy.get('.discussion-on-page:visible .cancel-comment').click();
+ cy.get('.discussion-on-page:visible .comment-field').should('have.value', '');
+ };
+
+ const single_thread_discussion = () => {
+ cy.visit('/test-single-thread');
+ cy.get('.discussions-sidebar').should('have.length', 0);
+ cy.get('.reply').should('have.length', 0);
+
+ cy.get('.discussion-on-page .comment-field')
+ .type('This comment is being made on a single thread discussion.')
+ .should('have.value', 'This comment is being made on a single thread discussion.');
+
+ cy.get('.discussion-on-page .submit-discussion').click();
+ cy.wait(3000);
+ cy.get('.discussion-on-page').children(".reply-card").eq(-1).children(".reply-text")
+ .should('have.text', 'This comment is being made on a single thread discussion.\n');
+ };
+
+ it('reply through modal', reply_through_modal);
+ it('reply through comment box', reply_through_comment_box);
+ it('cancel and clear comment box', cancel_and_clear_comment_box);
+ it('single thread discussion', single_thread_discussion);
+});
diff --git a/cypress/integration/folder_navigation.js b/cypress/integration/folder_navigation.js
index 1b7c02d98c..cec7edb59f 100644
--- a/cypress/integration/folder_navigation.js
+++ b/cypress/integration/folder_navigation.js
@@ -71,7 +71,7 @@ context('Folder Navigation', () => {
it('Deleting Test Folder from the home', () => {
//Deleting the Test Folder added in the home directory
cy.visit('/app/file/view/home');
- cy.get('.level-left > .list-subject > .list-row-checkbox').eq(0).click({force: true, delay: 500});
+ cy.get('.level-left > .list-subject > .file-select >.list-row-checkbox').eq(0).click({force: true, delay: 500});
cy.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
cy.findByRole('button', {name: 'Yes'}).click();
diff --git a/cypress/integration/form.js b/cypress/integration/form.js
index d20750b1d5..f860a742ef 100644
--- a/cypress/integration/form.js
+++ b/cypress/integration/form.js
@@ -8,7 +8,10 @@ context('Form', () => {
});
it('create a new form', () => {
cy.visit('/app/todo/new');
- cy.fill_field('description', 'this is a test todo', 'Text Editor');
+ cy.get('[data-fieldname="description"] .ql-editor')
+ .first()
+ .click()
+ .type('this is a test todo');
cy.wait(300);
cy.get('.page-title').should('contain', 'Not Saved');
cy.intercept({
diff --git a/cypress/integration/form_tab_break.js b/cypress/integration/form_tab_break.js
new file mode 100644
index 0000000000..45c3c92084
--- /dev/null
+++ b/cypress/integration/form_tab_break.js
@@ -0,0 +1,31 @@
+import doctype_with_tab_break from '../fixtures/doctype_with_tab_break';
+const doctype_name = doctype_with_tab_break.name;
+context("Form Tab Break", () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app/website');
+ return cy.insert_doc('DocType', doctype_with_tab_break, true);
+ });
+ it("Should switch tab and open correct tabs on validation error", () => {
+ cy.new_form(doctype_name);
+ // test tab switch
+ cy.findByRole("tab", {name: "Tab 2"}).click();
+ cy.findByText("Phone");
+ cy.findByRole("tab", {name: "Details"}).click();
+ cy.findByText("Name");
+
+ // form should switch to the tab with un-filled mandatory field
+ cy.fill_field("username", "Test");
+ cy.findByRole("button", {name: "Save"}).click();
+ cy.findByText("Missing Fields");
+ cy.hide_dialog();
+ cy.findByText("Phone");
+ cy.fill_field("phone", "12345678");
+ cy.findByRole("button", {name: "Save"}).click();
+
+ // After save, first tab should have dashboard
+ cy.get(".form-tabs > .nav-item").eq(0).click();
+ cy.findByText("Connections");
+
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/grid_configuration.js b/cypress/integration/grid_configuration.js
new file mode 100644
index 0000000000..7193d804c2
--- /dev/null
+++ b/cypress/integration/grid_configuration.js
@@ -0,0 +1,23 @@
+context('Grid Configuration', () => {
+ beforeEach(() => {
+ cy.login();
+ cy.visit('/app/doctype/User');
+ });
+ it('Set user wise grid settings', () => {
+ cy.wait(100);
+ cy.get('.frappe-control[data-fieldname="fields"]').as('table');
+ cy.get('@table').find('.icon-sm').click();
+ cy.wait(100);
+ cy.get('.frappe-control[data-fieldname="fields_html"]').as('modal');
+ cy.get('@modal').find('.add-new-fields').click();
+ cy.wait(100);
+ cy.get('[type="checkbox"][data-unit="read_only"]').check();
+ cy.findByRole('button', {name: 'Add'}).click();
+ cy.wait(100);
+ cy.get('[data-fieldname="options"]').invoke('attr', 'value', '1');
+ cy.get('.form-control.column-width[data-fieldname="options"]').trigger('change');
+ cy.findByRole('button', {name: 'Update'}).click();
+ cy.wait(200);
+ cy.get('[title="Read Only"').should('be.visible');
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js
index 633d1335ab..ce9e87274b 100644
--- a/cypress/integration/list_view.js
+++ b/cypress/integration/list_view.js
@@ -6,12 +6,29 @@ context('List View', () => {
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow");
});
});
+
+ it('Keep checkbox checked after Bulk Update', () => {
+ cy.go_to_list('ToDo');
+ cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true });
+ cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
+ cy.get('.dropdown-menu li:visible .dropdown-item .menu-item-label[data-label="Edit"]').click();
+
+ cy.get('.modal-body .form-control[data-fieldname="field"]').first().select('Due Date').wait(200);
+ cy.fill_field('value', '09-28-21', 'Date');
+
+ cy.get('.modal-footer .standard-actions .btn-primary').click();
+ cy.wait(500);
+
+ cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
+ cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible');
+ });
+
it('enables "Actions" button', () => {
- const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete'];
+ const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete'];
cy.go_to_list('ToDo');
cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true });
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
- cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 8).each((el, index) => {
+ cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 9).each((el, index) => {
cy.wrap(el).contains(actions[index]);
}).then((elements) => {
cy.intercept({
@@ -24,10 +41,11 @@ context('List View', () => {
}).as('real-time-update');
cy.wrap(elements).contains('Approve').click();
cy.wait(['@bulk-approval', '@real-time-update']);
- cy.hide_dialog();
+ cy.wait(300);
+ cy.get_open_dialog().find('.btn-modal-close').click();
+ cy.reload();
cy.clear_filters();
cy.get('.list-row-container:visible').should('contain', 'Approved');
});
});
});
-
diff --git a/cypress/integration/multi_select_dialog.js b/cypress/integration/multi_select_dialog.js
new file mode 100644
index 0000000000..a45fba8d32
--- /dev/null
+++ b/cypress/integration/multi_select_dialog.js
@@ -0,0 +1,58 @@
+context('MultiSelectDialog', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app');
+ });
+
+ function open_multi_select_dialog() {
+ cy.window().its('frappe').then(frappe => {
+ new frappe.ui.form.MultiSelectDialog({
+ doctype: "Assignment Rule",
+ target: {},
+ setters: {
+ document_type: null,
+ priority: null
+ },
+ add_filters_group: 1,
+ allow_child_item_selection: 1,
+ child_fieldname: "assignment_days",
+ child_columns: ["day"]
+ });
+ });
+ }
+
+ it('multi select dialog api works', () => {
+ open_multi_select_dialog();
+ cy.get_open_dialog().should('contain', 'Select Assignment Rules');
+ });
+
+ it('checks for filters', () => {
+ ['search_term', 'document_type', 'priority'].forEach(fieldname => {
+ cy.get_open_dialog().get(`.frappe-control[data-fieldname="${fieldname}"]`).should('exist');
+ });
+
+ // add_filters_group: 1 should add a filter group
+ cy.get_open_dialog().get(`.frappe-control[data-fieldname="filter_area"]`).should('exist');
+
+ });
+
+ it('checks for child item selection', () => {
+ cy.get_open_dialog()
+ .get(`.dt-row-header`).should('not.exist');
+
+ cy.get_open_dialog()
+ .get(`.frappe-control[data-fieldname="allow_child_item_selection"]`)
+ .should('exist')
+ .click();
+
+ cy.get_open_dialog()
+ .get(`.frappe-control[data-fieldname="child_selection_area"]`)
+ .should('exist');
+
+ cy.get_open_dialog()
+ .get(`.dt-row-header`).should('contain', 'Assignment Rule');
+
+ cy.get_open_dialog()
+ .get(`.dt-row-header`).should('contain', 'Day');
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/navigation.js b/cypress/integration/navigation.js
index 7e1426aa46..c4d0638f26 100644
--- a/cypress/integration/navigation.js
+++ b/cypress/integration/navigation.js
@@ -1,7 +1,6 @@
context('Navigation', () => {
before(() => {
cy.login();
- cy.visit('/app/website');
});
it('Navigate to route with hash in document name', () => {
cy.insert_doc('ToDo', {'__newname': 'ABC#123', 'description': 'Test this', 'ignore_duplicate': true});
@@ -11,4 +10,16 @@ context('Navigation', () => {
cy.go('back');
cy.title().should('eq', 'Website');
});
+
+ it.only('Navigate to previous page after login', () => {
+ cy.visit('/app/todo');
+ cy.findByTitle('To Do').should('be.visible');
+ cy.request('/api/method/logout');
+ cy.reload();
+ cy.get('.btn-primary').contains('Login').click();
+ cy.location('pathname').should('eq', '/login');
+ cy.login();
+ cy.visit('/app');
+ cy.location('pathname').should('eq', '/app/todo');
+ });
});
diff --git a/cypress/integration/relative_time_filters.js b/cypress/integration/relative_time_filters.js
index cbb0524c24..362d3a219b 100644
--- a/cypress/integration/relative_time_filters.js
+++ b/cypress/integration/relative_time_filters.js
@@ -1,44 +1,47 @@
-context('Relative Timeframe', () => {
- before(() => {
- cy.login();
- cy.visit('/app/website');
- cy.window().its('frappe').then(frappe => {
- frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
- });
- });
- it('sets relative timespan filter for last week and filters list', () => {
- cy.visit('/app/List/ToDo/List');
- cy.clear_filters();
- cy.get('.list-row:contains("this is fourth todo")').should('exist');
- cy.add_filter();
- cy.get('.fieldname-select-area').should('exist');
- cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
- cy.get('select.condition.form-control').select("Timespan");
- cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
- cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
- cy.get('.filter-popover .apply-filters').click({ force: true });
- cy.wait('@list_refresh');
- cy.get('.list-row-container').its('length').should('eq', 1);
- cy.get('.list-row-container').should('contain', 'this is second todo');
- cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
- .as('save_user_settings');
- cy.clear_filters();
- cy.wait('@save_user_settings');
- });
- it('sets relative timespan filter for next week and filters list', () => {
- cy.visit('/app/List/ToDo/List');
- cy.clear_filters();
- cy.get('.list-row:contains("this is fourth todo")').should('exist');
- cy.add_filter();
- cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
- cy.get('select.condition.form-control').select("Timespan");
- cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
- cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
- cy.get('.filter-popover .apply-filters').click({ force: true });
- cy.wait('@list_refresh');
- cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
- .as('save_user_settings');
- cy.clear_filters();
- cy.wait('@save_user_settings');
- });
-});
+// TODO: Enable this again
+// currently this is flaky possibly because of different timezone in CI
+
+// context('Relative Timeframe', () => {
+// before(() => {
+// cy.login();
+// cy.visit('/app/website');
+// cy.window().its('frappe').then(frappe => {
+// frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
+// });
+// });
+// it('sets relative timespan filter for last week and filters list', () => {
+// cy.visit('/app/List/ToDo/List');
+// cy.clear_filters();
+// cy.get('.list-row:contains("this is fourth todo")').should('exist');
+// cy.add_filter();
+// cy.get('.fieldname-select-area').should('exist');
+// cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
+// cy.get('select.condition.form-control').select("Timespan");
+// cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
+// cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
+// cy.get('.filter-popover .apply-filters').click({ force: true });
+// cy.wait('@list_refresh');
+// cy.get('.list-row-container').its('length').should('eq', 1);
+// cy.get('.list-row-container').should('contain', 'this is second todo');
+// cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
+// .as('save_user_settings');
+// cy.clear_filters();
+// cy.wait('@save_user_settings');
+// });
+// it('sets relative timespan filter for next week and filters list', () => {
+// cy.visit('/app/List/ToDo/List');
+// cy.clear_filters();
+// cy.get('.list-row:contains("this is fourth todo")').should('exist');
+// cy.add_filter();
+// cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
+// cy.get('select.condition.form-control').select("Timespan");
+// cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
+// cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
+// cy.get('.filter-popover .apply-filters').click({ force: true });
+// cy.wait('@list_refresh');
+// cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
+// .as('save_user_settings');
+// cy.clear_filters();
+// cy.wait('@save_user_settings');
+// });
+// });
diff --git a/cypress/integration/sidebar.js b/cypress/integration/sidebar.js
index e05f1877bf..cd771430c6 100644
--- a/cypress/integration/sidebar.js
+++ b/cypress/integration/sidebar.js
@@ -6,12 +6,12 @@ context('Sidebar', () => {
});
it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => {
- cy.click_sidebar_button(0);
+ cy.click_sidebar_button("Assigned To");
//To check if no filter is available in "Assigned To" dropdown
cy.get('.empty-state').should('contain', 'No filters found');
- cy.click_sidebar_button(1);
+ cy.click_sidebar_button("Created By");
//To check if "Created By" dropdown contains filter
cy.get('.group-by-item > .dropdown-item').should('contain', 'Me');
@@ -22,7 +22,7 @@ context('Sidebar', () => {
cy.get_field('assign_to_me', 'Check').click();
cy.get('.modal-footer > .standard-actions > .btn-primary').click();
cy.visit('/app/doctype');
- cy.click_sidebar_button(0);
+ cy.click_sidebar_button("Assigned To");
//To check if filter is added in "Assigned To" dropdown after assignment
cy.get('.group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item').should('contain', '1');
@@ -38,20 +38,19 @@ context('Sidebar', () => {
cy.get('.fieldname-select-area > .awesomplete > .form-control').should('have.value', 'Assigned To');
cy.get('.condition').should('have.value', 'like');
cy.get('.filter-field > .form-group > .input-with-feedback').should('have.value', '%Administrator%');
+ cy.click_filter_button();
//To remove the applied filter
- cy.get('.filter-action-buttons > div > .btn-secondary').contains('Clear Filters').click();
- cy.click_filter_button();
- cy.get('.filter-selector > .btn').should('contain', 'Filter');
+ cy.clear_filters();
//To remove the assignment
cy.visit('/app/doctype');
cy.click_listview_row_item(0);
cy.get('.assignments > .avatar-group > .avatar > .avatar-frame').click();
cy.get('.remove-btn').click({force: true});
- cy.get('.modal.show > .modal-dialog > .modal-content > .modal-header > .modal-actions > .btn-modal-close').click();
+ cy.hide_dialog();
cy.visit('/app/doctype');
- cy.click_sidebar_button(0);
+ cy.click_sidebar_button("Assigned To");
cy.get('.empty-state').should('contain', 'No filters found');
});
});
\ No newline at end of file
diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js
index 7a8f3a159b..191b5a2b2c 100644
--- a/cypress/integration/timeline.js
+++ b/cypress/integration/timeline.js
@@ -4,13 +4,14 @@ context('Timeline', () => {
before(() => {
cy.visit('/login');
cy.login();
- cy.visit('/app/todo');
});
it('Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo', () => {
//Adding new ToDo
+ cy.visit('/app/todo');
cy.click_listview_primary_button('Add ToDo');
cy.findByRole('button', {name: 'Edit in full page'}).click();
+ cy.findByTitle('New ToDo').should('be.visible');
cy.get('[data-fieldname="description"] .ql-editor').eq(0).type('Test ToDo', {force: true});
cy.wait(200);
cy.findByRole('button', {name: 'Save'}).click();
@@ -28,28 +29,29 @@ context('Timeline', () => {
cy.get('.timeline-content').should('contain', 'Testing Timeline');
//Editing comment
- cy.click_timeline_action_btn(0);
+ cy.click_timeline_action_btn("Edit");
cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(' 123');
- cy.click_timeline_action_btn(0);
+ cy.click_timeline_action_btn("Save");
//To check if the edited comment text is visible in timeline content
cy.get('.timeline-content').should('contain', 'Testing Timeline 123');
//Discarding comment
- cy.click_timeline_action_btn(0);
+ cy.click_timeline_action_btn("Edit");
cy.findByRole('button', {name: 'Dismiss'}).click();
//To check if after discarding the timeline content is same as previous
cy.get('.timeline-content').should('contain', 'Testing Timeline 123');
//Deleting the added comment
- cy.get('.actions > .btn > .icon').first().click();
+ cy.get('.more-actions > .action-btn').click();
+ cy.get('.more-actions .dropdown-item').contains('Delete').click();
cy.findByRole('button', {name: 'Yes'}).click();
cy.click_modal_primary_button('Yes');
//Deleting the added ToDo
- cy.get('.menu-btn-group button').eq(1).click();
- cy.get('.menu-btn-group [data-label="Delete"]').click();
+ cy.get('[id="page-ToDo"] .menu-btn-group [data-original-title="Menu"]').click();
+ cy.get('[id="page-ToDo"] .menu-btn-group .dropdown-item').contains('Delete').click();
cy.findByRole('button', {name: 'Yes'}).click();
});
@@ -81,7 +83,7 @@ context('Timeline', () => {
cy.visit('/app/custom-submittable-doctype');
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click();
cy.findByRole('button', {name: 'Actions'}).click();
- cy.get('.actions-btn-group > .dropdown-menu > li > .grey-link').eq(7).click();
+ cy.get('.actions-btn-group > .dropdown-menu > li > .dropdown-item').contains("Delete").click();
cy.click_modal_primary_button('Yes', {force: true, delay: 700});
//Deleting the custom doctype
diff --git a/cypress/integration/timeline_email.js b/cypress/integration/timeline_email.js
index 82af24e822..dfe80e0019 100644
--- a/cypress/integration/timeline_email.js
+++ b/cypress/integration/timeline_email.js
@@ -5,14 +5,16 @@ context('Timeline Email', () => {
cy.visit('/app/todo');
});
- it('Adding new ToDo, adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => {
- //Adding new ToDo
+ it('Adding new ToDo', () => {
cy.click_listview_primary_button('Add ToDo');
cy.get('.custom-actions:visible > .btn').contains("Edit in full page").click({delay: 500});
cy.fill_field("description", "Test ToDo", "Text Editor");
cy.wait(500);
cy.get('.primary-action').contains('Save').click({force: true});
cy.wait(700);
+ });
+
+ it('Adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => {
cy.visit('/app/todo');
cy.get('.list-row > .level-left > .list-subject').eq(0).click();
@@ -41,11 +43,13 @@ context('Timeline Email', () => {
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click();
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .dropdown-menu > li > .grey-link').eq(9).click();
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click();
+
cy.visit('/app/todo');
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();
//Removing the added attachment
cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click();
+ cy.wait(500);
cy.get('.modal-footer:visible > .standard-actions > .btn-primary').contains('Yes').click();
//To check if the removed attachment is shown in the timeline content
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
index 07d9804a73..9720faa666 100644
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -11,7 +11,7 @@
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
-module.exports = () => {
- // `on` is used to hook into various events Cypress emits
- // `config` is the resolved Cypress config
-};
+module.exports = (on, config) => {
+ require('@cypress/code-coverage/task')(on, config);
+ return config;
+};
\ No newline at end of file
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index c941652487..6484370946 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -187,7 +187,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
- cy.get('@input').type(value, {waitForAnimations: false, force: true});
+ cy.get('@input').type(value, {waitForAnimations: false, force: true, delay: 100});
}
return cy.get('@input');
});
@@ -252,7 +252,8 @@ Cypress.Commands.add('new_form', doctype => {
});
Cypress.Commands.add('go_to_list', doctype => {
- cy.visit(`/app/list/${doctype}/list`);
+ let dt_in_route = doctype.toLowerCase().replace(/ /g, '-');
+ cy.visit(`/app/${dt_in_route}`);
});
Cypress.Commands.add('clear_cache', () => {
@@ -316,7 +317,11 @@ Cypress.Commands.add('add_filter', () => {
});
Cypress.Commands.add('clear_filters', () => {
- cy.get('.filter-section .filter-button').click();
+ cy.intercept({
+ method: 'POST',
+ url: 'api/method/frappe.model.utils.user_settings.save'
+ }).as('filter-saved');
+ cy.get('.filter-section .filter-button').click({force: true});
cy.wait(300);
cy.get('.filter-popover').should('exist');
cy.get('.filter-popover').find('.clear-filters').click();
@@ -324,16 +329,15 @@ Cypress.Commands.add('clear_filters', () => {
cy.window().its('cur_list').then(cur_list => {
cur_list && cur_list.filter_area && cur_list.filter_area.clear();
});
-
-
+ cy.wait('@filter-saved');
});
Cypress.Commands.add('click_modal_primary_button', (btn_name) => {
cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).trigger('click', {force: true});
});
-Cypress.Commands.add('click_sidebar_button', (btn_no) => {
- cy.get('.list-group-by-fields > .group-by-field > .btn').eq(btn_no).click();
+Cypress.Commands.add('click_sidebar_button', (btn_name) => {
+ cy.get('.list-group-by-fields .list-link > a').contains(btn_name).click({force: true});
});
Cypress.Commands.add('click_listview_row_item', (row_no) => {
@@ -348,6 +352,6 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => {
cy.get('.primary-action').contains(btn_name).click({force: true});
});
-Cypress.Commands.add('click_timeline_action_btn', (btn_no) => {
- cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').eq(btn_no).first().click();
+Cypress.Commands.add('click_timeline_action_btn', (btn_name) => {
+ cy.get('.timeline-message-box .custom-actions > .btn').contains(btn_name).click();
});
\ No newline at end of file
diff --git a/cypress/support/index.js b/cypress/support/index.js
index 1bee72d2ca..9cd770a31e 100644
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -15,6 +15,7 @@
// Import commands.js using ES2015 syntax:
import './commands';
+import '@cypress/code-coverage/support';
// Alternatively you can use CommonJS syntax:
diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js
index 442846c73f..9320258125 100644
--- a/esbuild/esbuild.js
+++ b/esbuild/esbuild.js
@@ -44,6 +44,11 @@ let argv = yargs
type: "boolean",
description: "Run in watch mode and rebuild on file changes"
})
+ .option("live-reload", {
+ type: "boolean",
+ description: `Automatically reload web pages when assets are rebuilt.
+ Can only be used with the --watch flag.`
+ })
.option("production", {
type: "boolean",
description: "Run build in production mode"
@@ -104,6 +109,9 @@ async function execute() {
log_error("There were some problems during build");
log();
log(chalk.dim(e.stack));
+ if (process.env.CI) {
+ process.kill(process.pid);
+ }
return;
}
@@ -490,7 +498,8 @@ async function notify_redis({ error, success, changed_files }) {
if (success) {
payload = {
success: true,
- changed_files
+ changed_files,
+ live_reload: argv["live-reload"]
};
}
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 7c6005a350..1b4429d55b 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -235,12 +235,13 @@ def connect_replica():
from frappe.database import get_db
user = local.conf.db_name
password = local.conf.db_password
+ port = local.conf.replica_db_port
if local.conf.different_credentials_for_replica:
user = local.conf.replica_db_name
password = local.conf.replica_db_password
- local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password)
+ local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password, port=port)
# swap db connections
local.primary_db = local.db
@@ -618,8 +619,6 @@ def read_only():
try:
retval = fn(*args, **get_newargs(fn, kwargs))
- except:
- raise
finally:
if local and hasattr(local, 'primary_db'):
local.db.close()
@@ -629,6 +628,29 @@ def read_only():
return wrapper_fn
return innfn
+def write_only():
+ # if replica connection exists, we have to replace it momentarily with the primary connection
+ def innfn(fn):
+ def wrapper_fn(*args, **kwargs):
+ primary_db = getattr(local, "primary_db", None)
+ replica_db = getattr(local, "replica_db", None)
+ in_read_only = getattr(local, "db", None) != primary_db
+
+ # switch to primary connection
+ if in_read_only and primary_db:
+ local.db = local.primary_db
+
+ try:
+ retval = fn(*args, **get_newargs(fn, kwargs))
+ finally:
+ # switch back to replica connection
+ if in_read_only and replica_db:
+ local.db = replica_db
+
+ return retval
+ return wrapper_fn
+ return innfn
+
def only_for(roles, message=False):
"""Raise `frappe.PermissionError` if the user does not have any of the given **Roles**.
@@ -1458,7 +1480,10 @@ def get_value(*args, **kwargs):
def as_json(obj, indent=1):
from frappe.utils.response import json_handler
- return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': '))
+ try:
+ return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': '))
+ except TypeError:
+ return json.dumps(obj, indent=indent, default=json_handler, separators=(',', ': '))
def are_emails_muted():
from frappe.utils import cint
diff --git a/frappe/build.py b/frappe/build.py
index dfbe20f31e..8b32b03d60 100644
--- a/frappe/build.py
+++ b/frappe/build.py
@@ -1,10 +1,11 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import os
import re
import json
import shutil
import subprocess
+from subprocess import getoutput
from io import StringIO
from tempfile import mkdtemp, mktemp
from distutils.spawn import find_executable
@@ -17,6 +18,8 @@ import psutil
from urllib.parse import urlparse
from simple_chalk import green
from semantic_version import Version
+from requests import head
+from requests.exceptions import HTTPError
timestamps = {}
@@ -24,6 +27,12 @@ app_paths = None
sites_path = os.path.abspath(os.getcwd())
+class AssetsNotDownloadedError(Exception):
+ pass
+
+class AssetsDontExistError(HTTPError):
+ pass
+
def download_file(url, prefix):
from requests import get
@@ -70,81 +79,94 @@ def build_missing_files():
bundle(build_mode, apps="frappe")
-def get_assets_link(frappe_head):
- from subprocess import getoutput
- from requests import head
-
+def get_assets_link(frappe_head) -> str:
tag = getoutput(
- r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
- r" refs/tags/,,' -e 's/\^{}//'"
- % frappe_head
- )
+ r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
+ r" refs/tags/,,' -e 's/\^{}//'"
+ % frappe_head
+ )
if tag:
# if tag exists, download assets from github release
- url = "https://github.com/frappe/frappe/releases/download/{0}/assets.tar.gz".format(tag)
+ url = f"https://github.com/frappe/frappe/releases/download/{tag}/assets.tar.gz"
else:
- url = "http://assets.frappeframework.com/{0}.tar.gz".format(frappe_head)
+ url = f"http://assets.frappeframework.com/{frappe_head}.tar.gz"
if not head(url):
- raise ValueError("URL {0} doesn't exist".format(url))
+ reference = f"Release {tag}" if tag else f"Commit {frappe_head}"
+ raise AssetsDontExistError(f"Assets for {reference} don't exist")
return url
+def fetch_assets(url, frappe_head):
+ click.secho("Retrieving assets...", fg="yellow")
+
+ prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head)
+ assets_archive = download_file(url, prefix)
+
+ if not assets_archive:
+ raise AssetsNotDownloadedError(f"Assets could not be retrived from {url}")
+
+ print(f"\n{green('✔')} Downloaded Frappe assets from {url}")
+
+ return assets_archive
+
+
+def setup_assets(assets_archive):
+ import tarfile
+ directories_created = set()
+
+ click.secho("\nExtracting assets...\n", fg="yellow")
+ with tarfile.open(assets_archive) as tar:
+ for file in tar:
+ if not file.isdir():
+ dest = "." + file.name.replace("./frappe-bench/sites", "")
+ asset_directory = os.path.dirname(dest)
+ show = dest.replace("./assets/", "")
+
+ if asset_directory not in directories_created:
+ if not os.path.exists(asset_directory):
+ os.makedirs(asset_directory, exist_ok=True)
+ directories_created.add(asset_directory)
+
+ tar.makefile(file, dest)
+ print("{0} Restored {1}".format(green('✔'), show))
+
+ return directories_created
+
+
def download_frappe_assets(verbose=True):
"""Downloads and sets up Frappe assets if they exist based on the current
commit HEAD.
Returns True if correctly setup else returns False.
"""
- from subprocess import getoutput
-
- assets_setup = False
frappe_head = getoutput("cd ../apps/frappe && git rev-parse HEAD")
- if frappe_head:
+ if not frappe_head:
+ return False
+
+ try:
+ url = get_assets_link(frappe_head)
+ assets_archive = fetch_assets(url, frappe_head)
+ setup_assets(assets_archive)
+ build_missing_files()
+ return True
+
+ except AssetsDontExistError as e:
+ click.secho(str(e), fg="yellow")
+
+ except Exception as e:
+ # TODO: log traceback in bench.log
+ click.secho(str(e), fg="red")
+
+ finally:
try:
- url = get_assets_link(frappe_head)
- click.secho("Retrieving assets...", fg="yellow")
- prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head)
- assets_archive = download_file(url, prefix)
- print("\n{0} Downloaded Frappe assets from {1}".format(green('✔'), url))
-
- if assets_archive:
- import tarfile
- directories_created = set()
-
- click.secho("\nExtracting assets...\n", fg="yellow")
- with tarfile.open(assets_archive) as tar:
- for file in tar:
- if not file.isdir():
- dest = "." + file.name.replace("./frappe-bench/sites", "")
- asset_directory = os.path.dirname(dest)
- show = dest.replace("./assets/", "")
-
- if asset_directory not in directories_created:
- if not os.path.exists(asset_directory):
- os.makedirs(asset_directory, exist_ok=True)
- directories_created.add(asset_directory)
-
- tar.makefile(file, dest)
- print("{0} Restored {1}".format(green('✔'), show))
-
- build_missing_files()
- return True
- else:
- raise
+ shutil.rmtree(os.path.dirname(assets_archive))
except Exception:
- # TODO: log traceback in bench.log
- click.secho("An Error occurred while downloading assets...", fg="red")
- assets_setup = False
- finally:
- try:
- shutil.rmtree(os.path.dirname(assets_archive))
- except Exception:
- pass
+ pass
- return assets_setup
+ return False
def symlink(target, link_name, overwrite=False):
@@ -224,7 +246,7 @@ def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, ver
check_node_executable()
frappe_app_path = frappe.get_app_path("frappe", "..")
- frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
+ frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env(), raise_err=True)
def watch(apps=None):
@@ -235,6 +257,13 @@ def watch(apps=None):
if apps:
command += " --apps {apps}".format(apps=apps)
+ live_reload = frappe.utils.cint(
+ os.environ.get("LIVE_RELOAD", frappe.conf.live_reload)
+ )
+
+ if live_reload:
+ command += " --live-reload"
+
check_node_executable()
frappe_app_path = frappe.get_app_path("frappe", "..")
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py
index 6eccdac4fb..82a71ce7b4 100644
--- a/frappe/commands/__init__.py
+++ b/frappe/commands/__init__.py
@@ -102,9 +102,24 @@ def get_commands():
from .site import commands as site_commands
from .translate import commands as translate_commands
from .utils import commands as utils_commands
- from .redis import commands as redis_commands
+ from .redis_utils import commands as redis_commands
+
+ clickable_link = (
+ "\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a"
+ )
+ all_commands = (
+ scheduler_commands
+ + site_commands
+ + translate_commands
+ + utils_commands
+ + redis_commands
+ )
+
+ for command in all_commands:
+ if not command.help:
+ command.help = f"Refer to {clickable_link}"
+
+ return all_commands
- all_commands = scheduler_commands + site_commands + translate_commands + utils_commands + redis_commands
- return list(set(all_commands))
commands = get_commands()
diff --git a/frappe/commands/redis.py b/frappe/commands/redis_utils.py
similarity index 97%
rename from frappe/commands/redis.py
rename to frappe/commands/redis_utils.py
index 38a46c2142..3556050782 100644
--- a/frappe/commands/redis.py
+++ b/frappe/commands/redis_utils.py
@@ -3,7 +3,7 @@ import os
import click
import frappe
-from frappe.utils.rq import RedisQueue
+from frappe.utils.redis_queue import RedisQueue
from frappe.installer import update_site_config
@click.command('create-rq-users')
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index 9098e31738..2bd3110481 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -67,6 +67,9 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
validate_database_sql
)
+ site = get_site(context)
+ frappe.init(site=site)
+
force = context.force or force
decompressed_file_name = extract_sql_from_archive(sql_file_path)
@@ -85,9 +88,6 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
# check if valid SQL file
validate_database_sql(decompressed_file_name, _raise=not force)
- site = get_site(context)
- frappe.init(site=site)
-
# dont allow downgrading to older versions of frappe without force
if not force and is_downgrade(decompressed_file_name, verbose=True):
warn_message = (
@@ -474,7 +474,7 @@ def remove_from_installed_apps(context, app):
@click.command('uninstall-app')
@click.argument('app')
-@click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False, multiple=True)
+@click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False)
@click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False)
@click.option('--no-backup', help='Do not backup the site', is_flag=True, default=False)
@click.option('--force', help='Force remove app from site', is_flag=True, default=False)
@@ -738,6 +738,131 @@ def build_search_index(context):
finally:
frappe.destroy()
+@click.command('trim-database')
+@click.option('--dry-run', is_flag=True, default=False, help='Show what would be deleted')
+@click.option('--format', '-f', default='text', type=click.Choice(['json', 'text']), help='Output format')
+@click.option('--no-backup', is_flag=True, default=False, help='Do not backup the site')
+@pass_context
+def trim_database(context, dry_run, format, no_backup):
+ if not context.sites:
+ raise SiteNotSpecifiedError
+
+ from frappe.utils.backups import scheduled_backup
+
+ ALL_DATA = {}
+
+ for site in context.sites:
+ frappe.init(site=site)
+ frappe.connect()
+
+ TABLES_TO_DROP = []
+ STANDARD_TABLES = get_standard_tables()
+ information_schema = frappe.qb.Schema("information_schema")
+ table_name = frappe.qb.Field("table_name").as_("name")
+
+ queried_result = frappe.qb.from_(
+ information_schema.tables
+ ).select(table_name).where(
+ information_schema.tables.table_schema == frappe.conf.db_name
+ ).run()
+
+ database_tables = [x[0] for x in queried_result]
+ doctype_tables = frappe.get_all("DocType", pluck="name")
+
+ for x in database_tables:
+ doctype = x.lstrip("tab")
+ if not (doctype in doctype_tables or x.startswith("__") or x in STANDARD_TABLES):
+ TABLES_TO_DROP.append(x)
+
+ if not TABLES_TO_DROP:
+ if format == "text":
+ click.secho(f"No ghost tables found in {frappe.local.site}...Great!", fg="green")
+ else:
+ if not (no_backup or dry_run):
+ if format == "text":
+ print(f"Backing Up Tables: {', '.join(TABLES_TO_DROP)}")
+
+ odb = scheduled_backup(
+ ignore_conf=False,
+ include_doctypes=",".join(x.lstrip("tab") for x in TABLES_TO_DROP),
+ ignore_files=True,
+ force=True,
+ )
+ if format == "text":
+ odb.print_summary()
+ print("\nTrimming Database")
+
+ for table in TABLES_TO_DROP:
+ if format == "text":
+ print(f"* Dropping Table '{table}'...")
+ if not dry_run:
+ frappe.db.sql_ddl(f"drop table `{table}`")
+
+ ALL_DATA[frappe.local.site] = TABLES_TO_DROP
+ frappe.destroy()
+
+ if format == "json":
+ import json
+ print(json.dumps(ALL_DATA, indent=1))
+
+
+def get_standard_tables():
+ import re
+
+ tables = []
+ sql_file = os.path.join(
+ "..", "apps", "frappe", "frappe", "database", frappe.conf.db_type, f'framework_{frappe.conf.db_type}.sql'
+ )
+ content = open(sql_file).read().splitlines()
+
+ for line in content:
+ table_found = re.search(r"""CREATE TABLE ("|`)(.*)?("|`) \(""", line)
+ if table_found:
+ tables.append(table_found.group(2))
+
+ return tables
+
+@click.command('trim-tables')
+@click.option('--dry-run', is_flag=True, default=False, help='Show what would be deleted')
+@click.option('--format', '-f', default='table', type=click.Choice(['json', 'table']), help='Output format')
+@click.option('--no-backup', is_flag=True, default=False, help='Do not backup the site')
+@pass_context
+def trim_tables(context, dry_run, format, no_backup):
+ if not context.sites:
+ raise SiteNotSpecifiedError
+
+ from frappe.model.meta import trim_tables
+ from frappe.utils.backups import scheduled_backup
+
+ for site in context.sites:
+ frappe.init(site=site)
+ frappe.connect()
+
+ if not (no_backup or dry_run):
+ click.secho(f"Taking backup for {frappe.local.site}", fg="green")
+ odb = scheduled_backup(ignore_files=False, force=True)
+ odb.print_summary()
+
+ try:
+ trimmed_data = trim_tables(dry_run=dry_run, quiet=format == 'json')
+
+ if format == 'table' and not dry_run:
+ click.secho(f"The following data have been removed from {frappe.local.site}", fg='green')
+
+ handle_data(trimmed_data, format=format)
+ finally:
+ frappe.destroy()
+
+def handle_data(data: dict, format='json'):
+ if format == 'json':
+ import json
+ print(json.dumps({frappe.local.site: data}, indent=1, sort_keys=True))
+ else:
+ from frappe.utils.commands import render_table
+ data = [["DocType", "Fields"]] + [[table, ", ".join(columns)] for table, columns in data.items()]
+ render_table(data)
+
+
commands = [
add_system_manager,
backup,
@@ -766,5 +891,7 @@ commands = [
add_to_hosts,
start_ngrok,
build_search_index,
- partial_restore
+ partial_restore,
+ trim_tables,
+ trim_database,
]
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index b0151106db..416f014164 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -12,10 +12,9 @@ from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import update_progress_bar, cint
from frappe.coverage import CodeCoverage
-DATA_IMPORT_DEPRECATION = click.style(
+DATA_IMPORT_DEPRECATION = (
"[DEPRECATED] The `import-csv` command used 'Data Import Legacy' which has been deprecated.\n"
- "Use `data-import` command instead to import data via 'Data Import'.",
- fg="yellow"
+ "Use `data-import` command instead to import data via 'Data Import'."
)
@@ -364,7 +363,7 @@ def import_doc(context, path, force=False):
@click.option('--no-email', default=True, is_flag=True, help='Send email if applicable')
@pass_context
def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True):
- click.secho(DATA_IMPORT_DEPRECATION)
+ click.secho(DATA_IMPORT_DEPRECATION, fg="yellow")
sys.exit(1)
@@ -408,20 +407,47 @@ def bulk_rename(context, doctype, path):
frappe.destroy()
+@click.command('db-console')
+@pass_context
+def database(context):
+ """
+ Enter into the Database console for given site.
+ """
+ site = get_site(context)
+ if not site:
+ raise SiteNotSpecifiedError
+ frappe.init(site=site)
+ if not frappe.conf.db_type or frappe.conf.db_type == "mariadb":
+ _mariadb()
+ elif frappe.conf.db_type == "postgres":
+ _psql()
+
+
@click.command('mariadb')
@pass_context
def mariadb(context):
"""
Enter into mariadb console for a given site.
"""
- import os
-
site = get_site(context)
if not site:
raise SiteNotSpecifiedError
frappe.init(site=site)
+ _mariadb()
- # This is assuming you're within the bench instance.
+
+@click.command('postgres')
+@pass_context
+def postgres(context):
+ """
+ Enter into postgres console for a given site.
+ """
+ site = get_site(context)
+ frappe.init(site=site)
+ _psql()
+
+
+def _mariadb():
mysql = find_executable('mysql')
os.execv(mysql, [
mysql,
@@ -434,15 +460,7 @@ def mariadb(context):
"-A"])
-@click.command('postgres')
-@pass_context
-def postgres(context):
- """
- Enter into postgres console for a given site.
- """
- site = get_site(context)
- frappe.init(site=site)
- # This is assuming you're within the bench instance.
+def _psql():
psql = find_executable('psql')
subprocess.run([ psql, '-d', frappe.conf.db_name])
@@ -485,6 +503,12 @@ frappe.db.connect()
])
+def _console_cleanup():
+ # Execute rollback_observers on console close
+ frappe.db.rollback()
+ frappe.destroy()
+
+
@click.command('console')
@click.option(
'--autoreload',
@@ -500,6 +524,9 @@ def console(context, autoreload=False):
frappe.local.lang = frappe.db.get_default("lang")
from IPython.terminal.embed import InteractiveShellEmbed
+ from atexit import register
+
+ register(_console_cleanup)
terminal = InteractiveShellEmbed()
if autoreload:
@@ -525,6 +552,74 @@ def console(context, autoreload=False):
terminal()
+@click.command('transform-database', help="Change tables' internal settings changing engine and row formats")
+@click.option('--table', required=True, help="Comma separated name of tables to convert. To convert all tables, pass 'all'")
+@click.option('--engine', default=None, type=click.Choice(["InnoDB", "MyISAM"]), help="Choice of storage engine for said table(s)")
+@click.option('--row_format', default=None, type=click.Choice(["DYNAMIC", "COMPACT", "REDUNDANT", "COMPRESSED"]), help="Set ROW_FORMAT parameter for said table(s)")
+@click.option('--failfast', is_flag=True, default=False, help="Exit on first failure occurred")
+@pass_context
+def transform_database(context, table, engine, row_format, failfast):
+ "Transform site database through given parameters"
+ site = get_site(context)
+ check_table = []
+ add_line = False
+ skipped = 0
+ frappe.init(site=site)
+
+ if frappe.conf.db_type and frappe.conf.db_type != "mariadb":
+ click.secho("This command only has support for MariaDB databases at this point", fg="yellow")
+ sys.exit(1)
+
+ if not (engine or row_format):
+ click.secho("Values for `--engine` or `--row_format` must be set")
+ sys.exit(1)
+
+ frappe.connect()
+
+ if table == "all":
+ information_schema = frappe.qb.Schema("information_schema")
+ queried_tables = frappe.qb.from_(
+ information_schema.tables
+ ).select("table_name").where(
+ (information_schema.tables.row_format != row_format)
+ & (information_schema.tables.table_schema == frappe.conf.db_name)
+ ).run()
+ tables = [x[0] for x in queried_tables]
+ else:
+ tables = [x.strip() for x in table.split(",")]
+
+ total = len(tables)
+
+ for current, table in enumerate(tables):
+ values_to_set = ""
+ if engine:
+ values_to_set += f" ENGINE={engine}"
+ if row_format:
+ values_to_set += f" ROW_FORMAT={row_format}"
+
+ try:
+ frappe.db.sql(f"ALTER TABLE `{table}`{values_to_set}")
+ update_progress_bar("Updating table schema", current - skipped, total)
+ add_line = True
+
+ except Exception as e:
+ check_table.append([table, e.args])
+ skipped += 1
+
+ if failfast:
+ break
+
+ if add_line:
+ print()
+
+ for errored_table in check_table:
+ table, err = errored_table
+ err_msg = f"{table}: ERROR {err[0]}: {err[1]}"
+ click.secho(err_msg, fg="yellow")
+
+ frappe.destroy()
+
+
@click.command('run-tests')
@click.option('--app', help="For App")
@click.option('--doctype', help="For DocType")
@@ -592,9 +687,10 @@ def run_parallel_tests(context, app, build_number, total_builds, with_coverage=F
@click.argument('app')
@click.option('--headless', is_flag=True, help="Run UI Test in headless mode")
@click.option('--parallel', is_flag=True, help="Run UI Test in parallel mode")
+@click.option('--with-coverage', is_flag=True, help="Generate coverage report")
@click.option('--ci-build-id')
@pass_context
-def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
+def run_ui_tests(context, app, headless=False, parallel=True, with_coverage=False, ci_build_id=None):
"Run UI tests"
site = get_site(context)
app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), '..'))
@@ -604,6 +700,7 @@ def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
# override baseUrl using env variable
site_env = f'CYPRESS_baseUrl={site_url}'
password_env = f'CYPRESS_adminPassword={admin_password}' if admin_password else ''
+ coverage_env = f'CYPRESS_coverage={str(with_coverage).lower()}'
os.chdir(app_base_path)
@@ -611,22 +708,23 @@ def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
cypress_path = f"{node_bin}/cypress"
plugin_path = f"{node_bin}/../cypress-file-upload"
testing_library_path = f"{node_bin}/../@testing-library"
+ coverage_plugin_path = f"{node_bin}/../@cypress/code-coverage"
# check if cypress in path...if not, install it.
if not (
os.path.exists(cypress_path)
and os.path.exists(plugin_path)
and os.path.exists(testing_library_path)
+ and os.path.exists(coverage_plugin_path)
and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6
):
# install cypress
click.secho("Installing Cypress...", fg="yellow")
- frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 --no-lockfile")
+ frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile")
# run for headless mode
run_or_open = 'run --browser firefox --record' if headless else 'open'
- command = '{site_env} {password_env} {cypress} {run_or_open}'
- formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)
+ formatted_command = f'{site_env} {password_env} {coverage_env} {cypress_path} {run_or_open}'
if parallel:
formatted_command += ' --parallel'
@@ -811,6 +909,8 @@ commands = [
build,
clear_cache,
clear_website_cache,
+ database,
+ transform_database,
jupyter,
console,
destroy_all_sessions,
diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py
index 6b71ec50f9..79c3358665 100644
--- a/frappe/contacts/address_and_contact.py
+++ b/frappe/contacts/address_and_contact.py
@@ -178,4 +178,4 @@ def set_link_title(doc):
for link in doc.links:
if not link.link_title:
linked_doc = frappe.get_doc(link.link_doctype, link.link_name)
- link.link_title = linked_doc.get("title_field") or linked_doc.get("name")
+ link.link_title = linked_doc.get_title() or link.link_name
diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py
index 5f0619d170..5d0ed18d5f 100644
--- a/frappe/contacts/doctype/address/address.py
+++ b/frappe/contacts/doctype/address/address.py
@@ -65,7 +65,7 @@ class Address(Document):
def has_link(self, doctype, name):
for link in self.links:
- if link.link_doctype==doctype and link.link_name== name:
+ if link.link_doctype == doctype and link.link_name == name:
return True
def has_common_link(self, doc):
diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py
index a1aa8408bf..dfb9ff2973 100644
--- a/frappe/contacts/doctype/contact/contact.py
+++ b/frappe/contacts/doctype/contact/contact.py
@@ -47,14 +47,14 @@ class Contact(Document):
def get_link_for(self, link_doctype):
'''Return the link name, if exists for the given link DocType'''
for link in self.links:
- if link.link_doctype==link_doctype:
+ if link.link_doctype == link_doctype:
return link.link_name
return None
def has_link(self, doctype, name):
for link in self.links:
- if link.link_doctype==doctype and link.link_name== name:
+ if link.link_doctype == doctype and link.link_name == name:
return True
def has_common_link(self, doc):
diff --git a/frappe/core/doctype/access_log/access_log.py b/frappe/core/doctype/access_log/access_log.py
index 0f5776ce2f..f631353d56 100644
--- a/frappe/core/doctype/access_log/access_log.py
+++ b/frappe/core/doctype/access_log/access_log.py
@@ -1,6 +1,7 @@
-# Copyright (c) 2019, Frappe Technologies and contributors
+# Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
+from tenacity import retry, retry_if_exception_type, stop_after_attempt
from frappe.model.document import Document
@@ -9,25 +10,41 @@ class AccessLog(Document):
@frappe.whitelist()
-def make_access_log(doctype=None, document=None, method=None, file_type=None,
- report_name=None, filters=None, page=None, columns=None):
+@frappe.write_only()
+@retry(
+ stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError)
+)
+def make_access_log(
+ doctype=None,
+ document=None,
+ method=None,
+ file_type=None,
+ report_name=None,
+ filters=None,
+ page=None,
+ columns=None,
+):
user = frappe.session.user
+ in_request = frappe.request and frappe.request.method == "GET"
- doc = frappe.get_doc({
- 'doctype': 'Access Log',
- 'user': user,
- 'export_from': doctype,
- 'reference_document': document,
- 'file_type': file_type,
- 'report_name': report_name,
- 'page': page,
- 'method': method,
- 'filters': frappe.utils.cstr(filters) if filters else None,
- 'columns': columns
- })
+ doc = frappe.get_doc(
+ {
+ "doctype": "Access Log",
+ "user": user,
+ "export_from": doctype,
+ "reference_document": document,
+ "file_type": file_type,
+ "report_name": report_name,
+ "page": page,
+ "method": method,
+ "filters": frappe.utils.cstr(filters) if filters else None,
+ "columns": columns,
+ }
+ )
doc.insert(ignore_permissions=True)
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview`
- if frappe.request and frappe.request.method == 'GET':
+ # dont commit in test mode
+ if not frappe.flags.in_test or in_request:
frappe.db.commit()
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index 66bb3909da..bd33189d58 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -255,7 +255,7 @@ class Communication(Document, CommunicationEmailMixin):
def set_delivery_status(self, commit=False):
'''Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication'''
delivery_status = None
- status_counts = Counter(frappe.db.sql_list('''select status from `tabEmail Queue` where communication=%s''', self.name))
+ status_counts = Counter(frappe.get_all("Email Queue", pluck="status", filters={"communication": self.name}))
if self.sent_or_received == "Received":
return
diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py
index 52cd370890..b6d8070d00 100644
--- a/frappe/core/doctype/communication/mixins.py
+++ b/frappe/core/doctype/communication/mixins.py
@@ -217,17 +217,7 @@ class CommunicationEmailMixin:
if not emails:
return []
- disabled_users = frappe.db.sql_list("""
- SELECT
- email
- FROM
- `tabUser`
- where
- email in %(emails)s
- and
- thread_notify=0
- """, {'emails': tuple(emails)})
- return disabled_users
+ return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0})
@staticmethod
def filter_disabled_users(emails):
@@ -236,17 +226,7 @@ class CommunicationEmailMixin:
if not emails:
return []
- disabled_users = frappe.db.sql_list("""
- SELECT
- email
- FROM
- `tabUser`
- where
- email in %(emails)s
- and
- enabled=0
- """, {'emails': tuple(emails)})
- return disabled_users
+ return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "enabled": 0})
def sendmail_input_dict(self, print_html=None, print_format=None,
send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None):
diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py
index 7c660c7180..c5cf67ba57 100644
--- a/frappe/core/doctype/data_export/exporter.py
+++ b/frappe/core/doctype/data_export/exporter.py
@@ -261,6 +261,7 @@ class DataExporter:
self.writer.writerow([self.data_keys.data_separator])
def add_data(self):
+ from frappe.query_builder import DocType
if self.template and not self.with_data:
return
@@ -305,9 +306,15 @@ class DataExporter:
if self.all_doctypes:
# add child tables
for c in self.child_doctypes:
- for ci, child in enumerate(frappe.db.sql("""select * from `tab{0}`
- where parent=%s and parentfield=%s order by idx""".format(c['doctype']),
- (doc.name, c['parentfield']), as_dict=1)):
+ child_doctype_table = DocType(c["doctype"])
+ data_row = (
+ frappe.qb.from_(child_doctype_table)
+ .select("*")
+ .where(child_doctype_table.parent == doc.name)
+ .where(child_doctype_table.parentfield == c["parentfield"])
+ .orderby(child_doctype_table.idx)
+ )
+ for ci, child in enumerate(data_row.run()):
self.add_data_row(rows, c['doctype'], c['parentfield'], child, ci)
for row in rows:
diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json
index b240d29446..6910d615d3 100644
--- a/frappe/core/doctype/docfield/docfield.json
+++ b/frappe/core/doctype/docfield/docfield.json
@@ -1,544 +1,543 @@
{
- "actions": [],
- "autoname": "hash",
- "creation": "2013-02-22 01:27:33",
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "label_and_type",
- "label",
- "fieldtype",
- "fieldname",
- "precision",
- "length",
- "non_negative",
- "hide_days",
- "hide_seconds",
- "reqd",
- "search_index",
- "column_break_18",
- "options",
- "defaults_section",
- "default",
- "column_break_6",
- "fetch_from",
- "fetch_if_empty",
- "visibility_section",
- "hidden",
- "bold",
- "allow_in_quick_entry",
- "translatable",
- "print_hide",
- "print_hide_if_no_value",
- "report_hide",
- "column_break_28",
- "depends_on",
- "collapsible",
- "collapsible_depends_on",
- "hide_border",
- "list__search_settings_section",
- "in_list_view",
- "in_standard_filter",
- "in_preview",
- "column_break_35",
- "in_filter",
- "in_global_search",
- "permissions",
- "read_only",
- "allow_on_submit",
- "ignore_user_permissions",
- "allow_bulk_edit",
- "column_break_13",
- "permlevel",
- "ignore_xss_filter",
- "constraints_section",
- "unique",
- "no_copy",
- "set_only_once",
- "remember_last_selected_value",
- "column_break_38",
- "mandatory_depends_on",
- "read_only_depends_on",
- "display",
- "print_width",
- "width",
- "max_height",
- "columns",
- "column_break_22",
- "description",
- "oldfieldname",
- "oldfieldtype"
- ],
- "fields": [
- {
- "fieldname": "label_and_type",
- "fieldtype": "Section Break"
- },
- {
- "bold": 1,
- "fieldname": "label",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Label",
- "oldfieldname": "label",
- "oldfieldtype": "Data",
- "print_width": "163",
- "search_index": 1,
- "width": "163"
- },
- {
- "bold": 1,
- "default": "Data",
- "fieldname": "fieldtype",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Type",
- "oldfieldname": "fieldtype",
- "oldfieldtype": "Select",
- "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
- "reqd": 1,
- "search_index": 1
- },
- {
- "bold": 1,
- "fieldname": "fieldname",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Name",
- "oldfieldname": "fieldname",
- "oldfieldtype": "Data",
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
- "fieldname": "reqd",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Mandatory",
- "oldfieldname": "reqd",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
- "description": "Set non-standard precision for a Float or Currency field",
- "fieldname": "precision",
- "fieldtype": "Select",
- "label": "Precision",
- "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9",
- "print_hide": 1
- },
- {
- "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
- "fieldname": "length",
- "fieldtype": "Int",
- "label": "Length"
- },
- {
- "default": "0",
- "fieldname": "search_index",
- "fieldtype": "Check",
- "label": "Index",
- "oldfieldname": "search_index",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "in_list_view",
- "fieldtype": "Check",
- "label": "In List View",
- "print_width": "70px",
- "width": "70px"
- },
- {
- "default": "0",
- "fieldname": "in_standard_filter",
- "fieldtype": "Check",
- "label": "In List Filter"
- },
- {
- "default": "0",
- "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
- "fieldname": "in_global_search",
- "fieldtype": "Check",
- "label": "In Global Search"
- },
- {
- "default": "0",
- "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
- "fieldname": "in_preview",
- "fieldtype": "Check",
- "label": "In Preview"
- },
- {
- "default": "0",
- "fieldname": "allow_in_quick_entry",
- "fieldtype": "Check",
- "label": "Allow in Quick Entry"
- },
- {
- "default": "0",
- "fieldname": "bold",
- "fieldtype": "Check",
- "label": "Bold"
- },
- {
- "default": "0",
- "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
- "fieldname": "translatable",
- "fieldtype": "Check",
- "label": "Translatable"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype===\"Section Break\"",
- "fieldname": "collapsible",
- "fieldtype": "Check",
- "label": "Collapsible",
- "length": 255
- },
- {
- "depends_on": "eval:doc.fieldtype==\"Section Break\" && doc.collapsible",
- "fieldname": "collapsible_depends_on",
- "fieldtype": "Code",
- "label": "Collapsible Depends On (JS)",
- "max_height": "3rem",
- "options": "JS"
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Column Break"
- },
- {
- "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
- "fieldname": "options",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Options",
- "oldfieldname": "options",
- "oldfieldtype": "Text"
- },
- {
- "fieldname": "default",
- "fieldtype": "Small Text",
- "label": "Default",
- "max_height": "3rem",
- "oldfieldname": "default",
- "oldfieldtype": "Text"
- },
- {
- "fieldname": "fetch_from",
- "fieldtype": "Small Text",
- "label": "Fetch From"
- },
- {
- "default": "0",
- "fieldname": "fetch_if_empty",
- "fieldtype": "Check",
- "label": "Fetch only if value is not set"
- },
- {
- "fieldname": "permissions",
- "fieldtype": "Section Break",
- "label": "Permissions"
- },
- {
- "fieldname": "depends_on",
- "fieldtype": "Code",
- "label": "Display Depends On (JS)",
- "length": 255,
- "max_height": "3rem",
- "oldfieldname": "depends_on",
- "oldfieldtype": "Data",
- "options": "JS"
- },
- {
- "default": "0",
- "fieldname": "hidden",
- "fieldtype": "Check",
- "label": "Hidden",
- "oldfieldname": "hidden",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "read_only",
- "fieldtype": "Check",
- "label": "Read Only",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "unique",
- "fieldtype": "Check",
- "label": "Unique"
- },
- {
- "default": "0",
- "fieldname": "set_only_once",
- "fieldtype": "Check",
- "label": "Set only once"
- },
- {
- "default": "0",
- "depends_on": "eval: doc.fieldtype == \"Table\"",
- "fieldname": "allow_bulk_edit",
- "fieldtype": "Check",
- "label": "Allow Bulk Edit"
- },
- {
- "fieldname": "column_break_13",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "permlevel",
- "fieldtype": "Int",
- "label": "Perm Level",
- "oldfieldname": "permlevel",
- "oldfieldtype": "Int",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "ignore_user_permissions",
- "fieldtype": "Check",
- "label": "Ignore User Permissions"
- },
- {
- "default": "0",
- "depends_on": "eval: parent.is_submittable",
- "fieldname": "allow_on_submit",
- "fieldtype": "Check",
- "label": "Allow on Submit",
- "oldfieldname": "allow_on_submit",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "report_hide",
- "fieldtype": "Check",
- "label": "Report Hide",
- "oldfieldname": "report_hide",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "depends_on": "eval:(doc.fieldtype == 'Link')",
- "fieldname": "remember_last_selected_value",
- "fieldtype": "Check",
- "label": "Remember Last Selected Value"
- },
- {
- "default": "0",
- "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
- "fieldname": "ignore_xss_filter",
- "fieldtype": "Check",
- "label": "Ignore XSS Filter"
- },
- {
- "fieldname": "display",
- "fieldtype": "Section Break",
- "label": "Display"
- },
- {
- "default": "0",
- "fieldname": "in_filter",
- "fieldtype": "Check",
- "label": "In Filter",
- "oldfieldname": "in_filter",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "no_copy",
- "fieldtype": "Check",
- "label": "No Copy",
- "oldfieldname": "no_copy",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "print_hide",
- "fieldtype": "Check",
- "label": "Print Hide",
- "oldfieldname": "print_hide",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
- "fieldname": "print_hide_if_no_value",
- "fieldtype": "Check",
- "label": "Print Hide If No Value"
- },
- {
- "fieldname": "print_width",
- "fieldtype": "Data",
- "label": "Print Width",
- "length": 10
- },
- {
- "fieldname": "width",
- "fieldtype": "Data",
- "label": "Width",
- "length": 10,
- "oldfieldname": "width",
- "oldfieldtype": "Data",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
- "fieldname": "columns",
- "fieldtype": "Int",
- "label": "Columns"
- },
- {
- "fieldname": "column_break_22",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "description",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "print_width": "300px",
- "width": "300px"
- },
- {
- "fieldname": "oldfieldname",
- "fieldtype": "Data",
- "hidden": 1,
- "oldfieldname": "oldfieldname",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "oldfieldtype",
- "fieldtype": "Data",
- "hidden": 1,
- "oldfieldname": "oldfieldtype",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "mandatory_depends_on",
- "fieldtype": "Code",
- "label": "Mandatory Depends On (JS)",
- "max_height": "3rem",
- "options": "JS"
- },
- {
- "fieldname": "read_only_depends_on",
- "fieldtype": "Code",
- "label": "Read Only Depends On (JS)",
- "max_height": "3rem",
- "options": "JS"
- },
- {
- "fieldname": "column_break_38",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Duration'",
- "fieldname": "hide_days",
- "fieldtype": "Check",
- "label": "Hide Days"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Duration'",
- "fieldname": "hide_seconds",
- "fieldtype": "Check",
- "label": "Hide Seconds"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Section Break'",
- "fieldname": "hide_border",
- "fieldtype": "Check",
- "label": "Hide Border"
- },
- {
- "default": "0",
- "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
- "fieldname": "non_negative",
- "fieldtype": "Check",
- "label": "Non Negative"
- },
- {
- "fieldname": "column_break_18",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "defaults_section",
- "fieldtype": "Section Break",
- "label": "Defaults",
- "max_height": "2rem"
- },
- {
- "fieldname": "visibility_section",
- "fieldtype": "Section Break",
- "label": "Visibility"
- },
- {
- "fieldname": "column_break_28",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "constraints_section",
- "fieldtype": "Section Break",
- "label": "Constraints"
- },
- {
- "fieldname": "max_height",
- "fieldtype": "Data",
- "label": "Max Height",
- "length": 10
- },
- {
- "fieldname": "list__search_settings_section",
- "fieldtype": "Section Break",
- "label": "List / Search Settings"
- },
- {
- "fieldname": "column_break_35",
- "fieldtype": "Column Break"
- }
- ],
- "idx": 1,
- "index_web_pages_for_search": 1,
- "istable": 1,
- "links": [],
- "modified": "2021-09-04 19:41:53.684094",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "DocField",
- "naming_rule": "Random",
- "owner": "Administrator",
- "permissions": [],
- "sort_field": "modified",
- "sort_order": "ASC"
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2013-02-22 01:27:33",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "label_and_type",
+ "label",
+ "fieldtype",
+ "fieldname",
+ "precision",
+ "length",
+ "non_negative",
+ "hide_days",
+ "hide_seconds",
+ "reqd",
+ "search_index",
+ "column_break_18",
+ "options",
+ "defaults_section",
+ "default",
+ "column_break_6",
+ "fetch_from",
+ "fetch_if_empty",
+ "visibility_section",
+ "hidden",
+ "bold",
+ "allow_in_quick_entry",
+ "translatable",
+ "print_hide",
+ "print_hide_if_no_value",
+ "report_hide",
+ "column_break_28",
+ "depends_on",
+ "collapsible",
+ "collapsible_depends_on",
+ "hide_border",
+ "list__search_settings_section",
+ "in_list_view",
+ "in_standard_filter",
+ "in_preview",
+ "column_break_35",
+ "in_filter",
+ "in_global_search",
+ "permissions",
+ "read_only",
+ "allow_on_submit",
+ "ignore_user_permissions",
+ "allow_bulk_edit",
+ "column_break_13",
+ "permlevel",
+ "ignore_xss_filter",
+ "constraints_section",
+ "unique",
+ "no_copy",
+ "set_only_once",
+ "remember_last_selected_value",
+ "column_break_38",
+ "mandatory_depends_on",
+ "read_only_depends_on",
+ "display",
+ "print_width",
+ "width",
+ "max_height",
+ "columns",
+ "column_break_22",
+ "description",
+ "oldfieldname",
+ "oldfieldtype"
+ ],
+ "fields": [{
+ "fieldname": "label_and_type",
+ "fieldtype": "Section Break"
+ },
+ {
+ "bold": 1,
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label",
+ "oldfieldname": "label",
+ "oldfieldtype": "Data",
+ "print_width": "163",
+ "search_index": 1,
+ "width": "163"
+ },
+ {
+ "bold": 1,
+ "default": "Data",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Type",
+ "oldfieldname": "fieldtype",
+ "oldfieldtype": "Select",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\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\nTab Break",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Name",
+ "oldfieldname": "fieldname",
+ "oldfieldtype": "Data",
+ "search_index": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Mandatory",
+ "oldfieldname": "reqd",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
+ "description": "Set non-standard precision for a Float or Currency field",
+ "fieldname": "precision",
+ "fieldtype": "Select",
+ "label": "Precision",
+ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
+ "fieldname": "length",
+ "fieldtype": "Int",
+ "label": "Length"
+ },
+ {
+ "default": "0",
+ "fieldname": "search_index",
+ "fieldtype": "Check",
+ "label": "Index",
+ "oldfieldname": "search_index",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_list_view",
+ "fieldtype": "Check",
+ "label": "In List View",
+ "print_width": "70px",
+ "width": "70px"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_standard_filter",
+ "fieldtype": "Check",
+ "label": "In List Filter"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
+ "fieldname": "in_global_search",
+ "fieldtype": "Check",
+ "label": "In Global Search"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_in_quick_entry",
+ "fieldtype": "Check",
+ "label": "Allow in Quick Entry"
+ },
+ {
+ "default": "0",
+ "fieldname": "bold",
+ "fieldtype": "Check",
+ "label": "Bold"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
+ "fieldname": "translatable",
+ "fieldtype": "Check",
+ "label": "Translatable"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype===\"Section Break\"",
+ "fieldname": "collapsible",
+ "fieldtype": "Check",
+ "label": "Collapsible",
+ "length": 255
+ },
+ {
+ "depends_on": "eval:doc.fieldtype==\"Section Break\" && doc.collapsible",
+ "fieldname": "collapsible_depends_on",
+ "fieldtype": "Code",
+ "label": "Collapsible Depends On (JS)",
+ "max_height": "3rem",
+ "options": "JS"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
+ "fieldname": "options",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Options",
+ "oldfieldname": "options",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "default",
+ "fieldtype": "Small Text",
+ "label": "Default",
+ "max_height": "3rem",
+ "oldfieldname": "default",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "fetch_from",
+ "fieldtype": "Small Text",
+ "label": "Fetch From"
+ },
+ {
+ "default": "0",
+ "fieldname": "fetch_if_empty",
+ "fieldtype": "Check",
+ "label": "Fetch only if value is not set"
+ },
+ {
+ "fieldname": "permissions",
+ "fieldtype": "Section Break",
+ "label": "Permissions"
+ },
+ {
+ "fieldname": "depends_on",
+ "fieldtype": "Code",
+ "label": "Display Depends On (JS)",
+ "length": 255,
+ "max_height": "3rem",
+ "oldfieldname": "depends_on",
+ "oldfieldtype": "Data",
+ "options": "JS"
+ },
+ {
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden",
+ "oldfieldname": "hidden",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "Read Only",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "unique",
+ "fieldtype": "Check",
+ "label": "Unique"
+ },
+ {
+ "default": "0",
+ "fieldname": "set_only_once",
+ "fieldtype": "Check",
+ "label": "Set only once"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.fieldtype == \"Table\"",
+ "fieldname": "allow_bulk_edit",
+ "fieldtype": "Check",
+ "label": "Allow Bulk Edit"
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "permlevel",
+ "fieldtype": "Int",
+ "label": "Perm Level",
+ "oldfieldname": "permlevel",
+ "oldfieldtype": "Int",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "ignore_user_permissions",
+ "fieldtype": "Check",
+ "label": "Ignore User Permissions"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: parent.is_submittable",
+ "fieldname": "allow_on_submit",
+ "fieldtype": "Check",
+ "label": "Allow on Submit",
+ "oldfieldname": "allow_on_submit",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "report_hide",
+ "fieldtype": "Check",
+ "label": "Report Hide",
+ "oldfieldname": "report_hide",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:(doc.fieldtype == 'Link')",
+ "fieldname": "remember_last_selected_value",
+ "fieldtype": "Check",
+ "label": "Remember Last Selected Value"
+ },
+ {
+ "default": "0",
+ "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
+ "fieldname": "ignore_xss_filter",
+ "fieldtype": "Check",
+ "label": "Ignore XSS Filter"
+ },
+ {
+ "fieldname": "display",
+ "fieldtype": "Section Break",
+ "label": "Display"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_filter",
+ "fieldtype": "Check",
+ "label": "In Filter",
+ "oldfieldname": "in_filter",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "no_copy",
+ "fieldtype": "Check",
+ "label": "No Copy",
+ "oldfieldname": "no_copy",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "print_hide",
+ "fieldtype": "Check",
+ "label": "Print Hide",
+ "oldfieldname": "print_hide",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
+ "fieldname": "print_hide_if_no_value",
+ "fieldtype": "Check",
+ "label": "Print Hide If No Value"
+ },
+ {
+ "fieldname": "print_width",
+ "fieldtype": "Data",
+ "label": "Print Width",
+ "length": 10
+ },
+ {
+ "fieldname": "width",
+ "fieldtype": "Data",
+ "label": "Width",
+ "length": 10,
+ "oldfieldname": "width",
+ "oldfieldtype": "Data",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
+ "fieldname": "columns",
+ "fieldtype": "Int",
+ "label": "Columns"
+ },
+ {
+ "fieldname": "column_break_22",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "print_width": "300px",
+ "width": "300px"
+ },
+ {
+ "fieldname": "oldfieldname",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "oldfieldname": "oldfieldname",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "oldfieldtype",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "oldfieldname": "oldfieldtype",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "mandatory_depends_on",
+ "fieldtype": "Code",
+ "label": "Mandatory Depends On (JS)",
+ "max_height": "3rem",
+ "options": "JS"
+ },
+ {
+ "fieldname": "read_only_depends_on",
+ "fieldtype": "Code",
+ "label": "Read Only Depends On (JS)",
+ "max_height": "3rem",
+ "options": "JS"
+ },
+ {
+ "fieldname": "column_break_38",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
+ "fieldtype": "Check",
+ "label": "Hide Days"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
+ "fieldtype": "Check",
+ "label": "Hide Seconds"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Section Break'",
+ "fieldname": "hide_border",
+ "fieldtype": "Check",
+ "label": "Hide Border"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
+ "fieldname": "non_negative",
+ "fieldtype": "Check",
+ "label": "Non Negative"
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "defaults_section",
+ "fieldtype": "Section Break",
+ "label": "Defaults",
+ "max_height": "2rem"
+ },
+ {
+ "fieldname": "visibility_section",
+ "fieldtype": "Section Break",
+ "label": "Visibility"
+ },
+ {
+ "fieldname": "column_break_28",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "constraints_section",
+ "fieldtype": "Section Break",
+ "label": "Constraints"
+ },
+ {
+ "fieldname": "max_height",
+ "fieldtype": "Data",
+ "label": "Max Height",
+ "length": 10
+ },
+ {
+ "fieldname": "list__search_settings_section",
+ "fieldtype": "Section Break",
+ "label": "List / Search Settings"
+ },
+ {
+ "fieldname": "column_break_35",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-04 19:41:23.684094",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "DocField",
+ "naming_rule": "Random",
+ "owner": "Administrator",
+ "permissions": [],
+ "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 18435f8873..c85b4e8f67 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -1,680 +1,686 @@
{
- "actions": [],
- "allow_rename": 1,
- "autoname": "Prompt",
- "creation": "2013-02-18 13:36:19",
- "description": "DocType is a Table / Form in the application.",
- "doctype": "DocType",
- "document_type": "Document",
- "engine": "InnoDB",
- "field_order": [
- "sb0",
- "module",
- "is_submittable",
- "istable",
- "issingle",
- "is_tree",
- "editable_grid",
- "quick_entry",
- "cb01",
- "track_changes",
- "track_seen",
- "track_views",
- "custom",
- "beta",
- "is_virtual",
- "fields_section_break",
- "fields",
- "sb1",
- "naming_rule",
- "autoname",
- "name_case",
- "allow_rename",
- "column_break_15",
- "description",
- "documentation",
- "form_settings_section",
- "image_field",
- "timeline_field",
- "nsm_parent_field",
- "max_attachments",
- "column_break_23",
- "hide_toolbar",
- "allow_copy",
- "allow_import",
- "allow_events_in_timeline",
- "allow_auto_repeat",
- "view_settings",
- "title_field",
- "search_fields",
- "default_print_format",
- "sort_field",
- "sort_order",
- "column_break_29",
- "document_type",
- "icon",
- "color",
- "show_preview_popup",
- "show_name_in_global_search",
- "email_settings_sb",
- "default_email_template",
- "column_break_51",
- "email_append_to",
- "sender_field",
- "subject_field",
- "sb2",
- "permissions",
- "restrict_to_domain",
- "read_only",
- "in_create",
- "actions_section",
- "actions",
- "links_section",
- "links",
- "web_view",
- "has_web_view",
- "allow_guest_to_view",
- "index_web_pages_for_search",
- "route",
- "is_published_field",
- "website_search_field",
- "advanced",
- "engine"
- ],
- "fields": [
- {
- "fieldname": "sb0",
- "fieldtype": "Section Break",
- "oldfieldtype": "Section Break"
- },
- {
- "fieldname": "module",
- "fieldtype": "Link",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Module",
- "oldfieldname": "module",
- "oldfieldtype": "Link",
- "options": "Module Def",
- "reqd": 1,
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.istable",
- "description": "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended.",
- "fieldname": "is_submittable",
- "fieldtype": "Check",
- "label": "Is Submittable"
- },
- {
- "default": "0",
- "description": "Child Tables are shown as a Grid in other DocTypes",
- "fieldname": "istable",
- "fieldtype": "Check",
- "in_standard_filter": 1,
- "label": "Is Child Table",
- "oldfieldname": "istable",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.istable",
- "description": "Single Types have only one record no tables associated. Values are stored in tabSingles",
- "fieldname": "issingle",
- "fieldtype": "Check",
- "in_standard_filter": 1,
- "label": "Is Single",
- "oldfieldname": "issingle",
- "oldfieldtype": "Check",
- "set_only_once": 1
- },
- {
- "default": "1",
- "depends_on": "istable",
- "fieldname": "editable_grid",
- "fieldtype": "Check",
- "label": "Editable Grid"
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.istable && !doc.issingle",
- "description": "Open a dialog with mandatory fields to create a new record quickly",
- "fieldname": "quick_entry",
- "fieldtype": "Check",
- "label": "Quick Entry"
- },
- {
- "fieldname": "cb01",
- "fieldtype": "Column Break"
- },
- {
- "default": "1",
- "depends_on": "eval:!doc.istable",
- "description": "If enabled, changes to the document are tracked and shown in timeline",
- "fieldname": "track_changes",
- "fieldtype": "Check",
- "label": "Track Changes"
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.istable",
- "description": "If enabled, the document is marked as seen, the first time a user opens it",
- "fieldname": "track_seen",
- "fieldtype": "Check",
- "label": "Track Seen"
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.istable",
- "description": "If enabled, document views are tracked, this can happen multiple times",
- "fieldname": "track_views",
- "fieldtype": "Check",
- "label": "Track Views"
- },
- {
- "default": "0",
- "fieldname": "custom",
- "fieldtype": "Check",
- "label": "Custom?"
- },
- {
- "default": "0",
- "fieldname": "beta",
- "fieldtype": "Check",
- "label": "Beta"
- },
- {
- "fieldname": "fields_section_break",
- "fieldtype": "Section Break",
- "label": "Fields",
- "oldfieldtype": "Section Break"
- },
- {
- "fieldname": "fields",
- "fieldtype": "Table",
- "label": "Fields",
- "oldfieldname": "fields",
- "oldfieldtype": "Table",
- "options": "DocField"
- },
- {
- "fieldname": "sb1",
- "fieldtype": "Section Break",
- "label": "Naming"
- },
- {
- "description": "Naming Options:\n
field:[fieldname] - By Field
naming_series: - By Naming Series (field called naming_series must be present
Prompt - Prompt user for a name
[series] - Series by prefix (separated by a dot); for example PRE.#####
\n
format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
naming_series: - By Naming Series (field called naming_series must be present
Prompt - Prompt user for a name
[series] - Series by prefix (separated by a dot); for example PRE.#####
\n
format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
+ + + Add / Remove Columns + +
++
+-
+ {{ __('Copy Link') }}
+
+
+