diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 93189d2b1f..6c81d6298a 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -59,4 +59,4 @@ cd ../..
bench start &
bench --site test_site reinstall --yes
if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi
-bench build --app frappe
+CI=Yes bench build --app frappe
diff --git a/.github/helper/semgrep_rules/frappe_correctness.yml b/.github/helper/semgrep_rules/frappe_correctness.yml
index d9603e89aa..33a22fba6a 100644
--- a/.github/helper/semgrep_rules/frappe_correctness.yml
+++ b/.github/helper/semgrep_rules/frappe_correctness.yml
@@ -131,3 +131,16 @@ rules:
key `$X` is uselessly assigned twice. This could be a potential bug.
languages: [python]
severity: ERROR
+
+- id: frappe-using-db-sql
+ pattern-either:
+ - pattern: frappe.db.sql(...)
+ - pattern: frappe.db.sql_ddl(...)
+ - pattern: frappe.db.sql_list(...)
+ paths:
+ exclude:
+ - "test_*.py"
+ message: |
+ The PR contains a SQL query that may be re-written with frappe.qb (https://frappeframework.com/docs/user/en/api/query-builder) or the Database API (https://frappeframework.com/docs/user/en/api/database)
+ languages: [python]
+ severity: ERROR
diff --git a/.github/try-on-f-cloud-button.svg b/.github/try-on-f-cloud-button.svg
new file mode 100644
index 0000000000..fe0bb2c52d
--- /dev/null
+++ b/.github/try-on-f-cloud-button.svg
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml
index 90453cd1b4..02a01bf4e4 100644
--- a/.github/workflows/docs-checker.yml
+++ b/.github/workflows/docs-checker.yml
@@ -12,7 +12,7 @@ jobs:
- name: 'Setup Environment'
uses: actions/setup-python@v2
with:
- python-version: 3.6
+ python-version: 3.7
- name: 'Clone repo'
uses: actions/checkout@v2
diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml
index 3ac5cfa349..8758c4e273 100644
--- a/.github/workflows/patch-mariadb-tests.yml
+++ b/.github/workflows/patch-mariadb-tests.yml
@@ -29,7 +29,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: '3.9'
- name: Check if build should be run
id: check-build
@@ -102,4 +102,25 @@ jobs:
cd ~/frappe-bench/
wget https://frappeframework.com/files/v10-frappe.sql.gz
bench --site test_site --force restore ~/frappe-bench/v10-frappe.sql.gz
+
+ source env/bin/activate
+ cd apps/frappe/
+ git remote set-url upstream https://github.com/frappe/frappe.git
+ git fetch --all --tags
+
+ taglist=$(git tag --sort version:refname | grep -v "beta")
+ last_release=$(echo "$taglist" | tail -1 | cut -d . -f 1 | cut -c 2-)
+
+ for version in $(seq 12 "$last_release")
+ do
+ last_tag=$(echo "$taglist" | grep "v$version" | tail -1)
+ echo "Updating to $last_tag"
+ git checkout -q -f "$last_tag"
+ pip install -q -r requirements.txt
+ bench --site test_site migrate
+ done
+
+ echo "Updating to last commit"
+ git checkout -q -f "$GITHUB_SHA"
+ bench setup requirements --python
bench --site test_site migrate
diff --git a/.github/workflows/publish-assets-develop.yml b/.github/workflows/publish-assets-develop.yml
index a23885b508..f56d1460b5 100644
--- a/.github/workflows/publish-assets-develop.yml
+++ b/.github/workflows/publish-assets-develop.yml
@@ -18,7 +18,7 @@ jobs:
node-version: 14
- uses: actions/setup-python@v2
with:
- python-version: '3.6'
+ python-version: '3.9'
- name: Set up bench and build assets
run: |
npm install -g yarn
diff --git a/.github/workflows/publish-assets-releases.yml b/.github/workflows/publish-assets-releases.yml
index a697517c23..2582632fa0 100644
--- a/.github/workflows/publish-assets-releases.yml
+++ b/.github/workflows/publish-assets-releases.yml
@@ -21,7 +21,7 @@ jobs:
python-version: '12.x'
- uses: actions/setup-python@v2
with:
- python-version: '3.6'
+ python-version: '3.9'
- name: Set up bench and build assets
run: |
npm install -g yarn
diff --git a/.github/workflows/server-mariadb-tests.yml b/.github/workflows/server-mariadb-tests.yml
index 0b187fc44c..588f357f26 100644
--- a/.github/workflows/server-mariadb-tests.yml
+++ b/.github/workflows/server-mariadb-tests.yml
@@ -38,7 +38,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: '3.9'
- name: Check if build should be run
id: check-build
@@ -127,4 +127,5 @@ jobs:
name: MariaDB
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
- verbose: true
\ No newline at end of file
+ verbose: true
+ flags: server
\ No newline at end of file
diff --git a/.github/workflows/server-postgres-tests.yml b/.github/workflows/server-postgres-tests.yml
index a5630121a4..78f379837b 100644
--- a/.github/workflows/server-postgres-tests.yml
+++ b/.github/workflows/server-postgres-tests.yml
@@ -41,7 +41,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: '3.9'
- name: Check if build should be run
id: check-build
@@ -131,3 +131,4 @@ jobs:
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true
+ flags: server
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index 0727b06043..fcc53ba59c 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -37,7 +37,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: '3.9'
- name: Check if build should be run
id: check-build
@@ -122,12 +122,36 @@ jobs:
DB: mariadb
TYPE: ui
+ - name: Instrument Source Code
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
+ run: cd ~/frappe-bench/apps/frappe/ && npx nyc instrument -x 'frappe/public/dist/**' -x 'frappe/public/js/lib/**' -x '**/*.bundle.js' --compact=false --in-place frappe
+
+ - name: Build
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
+ run: cd ~/frappe-bench/ && bench build --apps frappe
+
- name: Site Setup
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard
- name: UI Tests
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
- run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID
+ run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID
env:
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb
+
+ - name: Check If Coverage Report Exists
+ id: check_coverage
+ uses: andstor/file-existence-action@v1
+ with:
+ files: "/home/runner/frappe-bench/apps/frappe/.cypress-coverage/clover.xml"
+
+ - name: Upload Coverage Data
+ if: ${{ steps.check-build.outputs.build == 'strawberry' && steps.check_coverage.outputs.files_exists == 'true' }}
+ uses: codecov/codecov-action@v2
+ with:
+ name: Cypress
+ fail_ci_if_error: true
+ directory: /home/runner/frappe-bench/apps/frappe/.cypress-coverage/
+ verbose: true
+ flags: ui-tests
diff --git a/.gitignore b/.gitignore
index 1ff3122d70..c9dd8f38f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,6 +67,7 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
+.cypress-coverage
# Translations
*.mo
diff --git a/CODEOWNERS b/CODEOWNERS
index 30cdb4d64d..69ca578b6c 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -15,5 +15,6 @@ core/ @surajshetty3416
database @gavindsouza
model @gavindsouza
requirements.txt @gavindsouza
+query_builder/ @gavindsouza
commands/ @gavindsouza
workspace @shariquerik
diff --git a/README.md b/README.md
index 6c2804d843..ef471aa05a 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@
-
+
@@ -35,25 +35,36 @@
Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com)
-### Table of Contents
-* [Installation](https://frappeframework.com/docs/user/en/installation)
-* [Documentation](https://frappeframework.com/docs)
+
+
+## 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 41b22001a5..90fec8fbcb 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -4,10 +4,23 @@ codecov:
coverage:
status:
project:
- default:
+ default: false
+ server:
target: auto
threshold: 0.5%
+ flags:
+ - server
comment:
- layout: "diff"
+ 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/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/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 298bb20432..ce9e87274b 100644
--- a/cypress/integration/list_view.js
+++ b/cypress/integration/list_view.js
@@ -6,6 +6,23 @@ 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', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete'];
cy.go_to_list('ToDo');
@@ -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/navigation.js b/cypress/integration/navigation.js
index ba45137cbd..c4d0638f26 100644
--- a/cypress/integration/navigation.js
+++ b/cypress/integration/navigation.js
@@ -13,6 +13,7 @@ context('Navigation', () => {
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();
diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js
index 6387485220..3071330b61 100644
--- a/cypress/integration/timeline.js
+++ b/cypress/integration/timeline.js
@@ -11,6 +11,7 @@ context('Timeline', () => {
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();
@@ -43,13 +44,14 @@ context('Timeline', () => {
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('.menu-btn-group [data-original-title="Menu"]').click();
+ cy.get('.menu-btn-group .dropdown-item').contains('Delete').click();
cy.findByRole('button', {name: 'Yes'}).click();
});
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 47c37a56a0..6484370946 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -353,5 +353,5 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => {
});
Cypress.Commands.add('click_timeline_action_btn', (btn_name) => {
- cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').contains(btn_name).click();
+ 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 9074beae06..af2ffd3fc5 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;
}
@@ -475,7 +483,8 @@ async function notify_redis({ error, success }) {
}
if (success) {
payload = {
- success: true
+ success: true,
+ live_reload: argv["live-reload"]
};
}
@@ -528,4 +537,4 @@ function log_rebuilt_assets(prev_assets, new_assets) {
log(" " + filename);
}
log();
-}
\ No newline at end of file
+}
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 38904c68d0..64e445973f 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
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 9b0ecee896..2bd3110481 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -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 90cd60c6ec..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)
@@ -504,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',
@@ -519,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:
@@ -679,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), '..'))
@@ -691,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)
@@ -698,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'
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/core/doctype/access_log/access_log.py b/frappe/core/doctype/access_log/access_log.py
index d93da02d25..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
@@ -10,25 +11,40 @@ class AccessLog(Document):
@frappe.whitelist()
@frappe.write_only()
-def make_access_log(doctype=None, document=None, method=None, file_type=None,
- report_name=None, filters=None, page=None, columns=None):
+@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:\nfield:[fieldname] - By Fieldnaming_series: - By Naming Series (field called naming_series must be presentPrompt - Prompt user for a name[series] - Series by prefix (separated by a dot); for example PRE.##### \nformat: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. ",
- "fieldname": "autoname",
- "fieldtype": "Data",
- "label": "Auto Name",
- "oldfieldname": "autoname",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "name_case",
- "fieldtype": "Select",
- "label": "Name Case",
- "oldfieldname": "name_case",
- "oldfieldtype": "Select",
- "options": "\nTitle Case\nUPPER CASE"
- },
- {
- "fieldname": "column_break_15",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "description",
- "fieldtype": "Small Text",
- "label": "Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text"
- },
- {
- "collapsible": 1,
- "fieldname": "form_settings_section",
- "fieldtype": "Section Break",
- "label": "Form Settings"
- },
- {
- "description": "Must be of type \"Attach Image\"",
- "fieldname": "image_field",
- "fieldtype": "Data",
- "label": "Image Field"
- },
- {
- "depends_on": "eval:!doc.istable",
- "description": "Comments and Communications will be associated with this linked document",
- "fieldname": "timeline_field",
- "fieldtype": "Data",
- "label": "Timeline Field"
- },
- {
- "fieldname": "max_attachments",
- "fieldtype": "Int",
- "label": "Max Attachments",
- "oldfieldname": "max_attachments",
- "oldfieldtype": "Int"
- },
- {
- "fieldname": "column_break_23",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "hide_toolbar",
- "fieldtype": "Check",
- "label": "Hide Sidebar and Menu",
- "oldfieldname": "hide_toolbar",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "allow_copy",
- "fieldtype": "Check",
- "label": "Hide Copy",
- "oldfieldname": "allow_copy",
- "oldfieldtype": "Check"
- },
- {
- "default": "1",
- "fieldname": "allow_rename",
- "fieldtype": "Check",
- "label": "Allow Rename",
- "oldfieldname": "allow_rename",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "allow_import",
- "fieldtype": "Check",
- "label": "Allow Import (via Data Import Tool)"
- },
- {
- "default": "0",
- "fieldname": "allow_events_in_timeline",
- "fieldtype": "Check",
- "label": "Allow events in timeline"
- },
- {
- "default": "0",
- "fieldname": "allow_auto_repeat",
- "fieldtype": "Check",
- "label": "Allow Auto Repeat"
- },
- {
- "collapsible": 1,
- "fieldname": "view_settings",
- "fieldtype": "Section Break",
- "label": "View Settings"
- },
- {
- "depends_on": "eval:!doc.istable",
- "fieldname": "title_field",
- "fieldtype": "Data",
- "label": "Title Field"
- },
- {
- "depends_on": "eval:!doc.istable",
- "fieldname": "search_fields",
- "fieldtype": "Data",
- "label": "Search Fields",
- "oldfieldname": "search_fields",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "default_print_format",
- "fieldtype": "Data",
- "label": "Default Print Format"
- },
- {
- "default": "modified",
- "depends_on": "eval:!doc.istable",
- "fieldname": "sort_field",
- "fieldtype": "Data",
- "label": "Default Sort Field"
- },
- {
- "default": "DESC",
- "depends_on": "eval:!doc.istable",
- "fieldname": "sort_order",
- "fieldtype": "Select",
- "label": "Default Sort Order",
- "options": "ASC\nDESC"
- },
- {
- "fieldname": "column_break_29",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "document_type",
- "fieldtype": "Select",
- "label": "Show in Module Section",
- "oldfieldname": "document_type",
- "oldfieldtype": "Select",
- "options": "\nDocument\nSetup\nSystem\nOther"
- },
- {
- "fieldname": "icon",
- "fieldtype": "Data",
- "label": "Icon"
- },
- {
- "fieldname": "color",
- "fieldtype": "Data",
- "label": "Color"
- },
- {
- "default": "0",
- "fieldname": "show_preview_popup",
- "fieldtype": "Check",
- "label": "Show Preview Popup"
- },
- {
- "default": "0",
- "fieldname": "show_name_in_global_search",
- "fieldtype": "Check",
- "label": "Make \"name\" searchable in Global Search"
- },
- {
- "depends_on": "eval:!doc.istable",
- "fieldname": "sb2",
- "fieldtype": "Section Break",
- "label": "Permission Rules"
- },
- {
- "fieldname": "permissions",
- "fieldtype": "Table",
- "label": "Permissions",
- "oldfieldname": "permissions",
- "oldfieldtype": "Table",
- "options": "DocPerm"
- },
- {
- "fieldname": "restrict_to_domain",
- "fieldtype": "Link",
- "label": "Restrict To Domain",
- "options": "Domain"
- },
- {
- "default": "0",
- "fieldname": "read_only",
- "fieldtype": "Check",
- "label": "User Cannot Search",
- "oldfieldname": "read_only",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "in_create",
- "fieldtype": "Check",
- "label": "User Cannot Create",
- "oldfieldname": "in_create",
- "oldfieldtype": "Check"
- },
- {
- "depends_on": "eval:doc.custom===0",
- "fieldname": "web_view",
- "fieldtype": "Section Break",
- "label": "Web View"
- },
- {
- "default": "0",
- "fieldname": "has_web_view",
- "fieldtype": "Check",
- "label": "Has Web View"
- },
- {
- "default": "0",
- "depends_on": "has_web_view",
- "fieldname": "allow_guest_to_view",
- "fieldtype": "Check",
- "label": "Allow Guest to View"
- },
- {
- "depends_on": "eval:!doc.istable",
- "fieldname": "route",
- "fieldtype": "Data",
- "label": "Route"
- },
- {
- "depends_on": "has_web_view",
- "fieldname": "is_published_field",
- "fieldtype": "Data",
- "label": "Is Published Field"
- },
- {
- "collapsible": 1,
- "fieldname": "advanced",
- "fieldtype": "Section Break",
- "hidden": 1,
- "label": "Advanced"
- },
- {
- "default": "InnoDB",
- "depends_on": "eval:!doc.issingle",
- "fieldname": "engine",
- "fieldtype": "Select",
- "label": "Database Engine",
- "options": "InnoDB\nMyISAM"
- },
- {
- "default": "0",
- "description": "Tree structures are implemented using Nested Set",
- "fieldname": "is_tree",
- "fieldtype": "Check",
- "label": "Is Tree"
- },
- {
- "depends_on": "is_tree",
- "fieldname": "nsm_parent_field",
- "fieldtype": "Data",
- "label": "Parent Field (Tree)"
- },
- {
- "description": "URL for documentation or help",
- "fieldname": "documentation",
- "fieldtype": "Data",
- "label": "Documentation Link"
- },
- {
- "collapsible": 1,
- "collapsible_depends_on": "actions",
- "fieldname": "actions_section",
- "fieldtype": "Section Break",
- "label": "Actions"
- },
- {
- "fieldname": "actions",
- "fieldtype": "Table",
- "label": "Actions",
- "options": "DocType Action"
- },
- {
- "collapsible": 1,
- "collapsible_depends_on": "links",
- "fieldname": "links_section",
- "fieldtype": "Section Break",
- "label": "Linked Documents"
- },
- {
- "fieldname": "links",
- "fieldtype": "Table",
- "label": "Links",
- "options": "DocType Link"
- },
- {
- "depends_on": "email_append_to",
- "fieldname": "subject_field",
- "fieldtype": "Data",
- "label": "Subject Field"
- },
- {
- "depends_on": "email_append_to",
- "fieldname": "sender_field",
- "fieldtype": "Data",
- "label": "Sender Field",
- "mandatory_depends_on": "email_append_to"
- },
- {
- "default": "0",
- "fieldname": "email_append_to",
- "fieldtype": "Check",
- "label": "Allow document creation via Email"
- },
- {
- "collapsible": 1,
- "fieldname": "email_settings_sb",
- "fieldtype": "Section Break",
- "label": "Email Settings"
- },
- {
- "default": "1",
- "fieldname": "index_web_pages_for_search",
- "fieldtype": "Check",
- "label": "Index Web Pages for Search"
- },
- {
- "default": "0",
- "fieldname": "is_virtual",
- "fieldtype": "Check",
- "label": "Is Virtual"
- },
- {
- "fieldname": "default_email_template",
- "fieldtype": "Link",
- "label": "Default Email Template",
- "options": "Email Template"
- },
- {
- "fieldname": "column_break_51",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "has_web_view",
- "fieldname": "website_search_field",
- "fieldtype": "Data",
- "label": "Website Search Field"
- },
- {
- "fieldname": "naming_rule",
- "fieldtype": "Select",
- "label": "Naming Rule",
- "length": 40,
- "options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
- }
- ],
- "icon": "fa fa-bolt",
- "idx": 6,
- "links": [
- {
- "group": "Views",
- "link_doctype": "Report",
- "link_fieldname": "ref_doctype"
- },
- {
- "group": "Workflow",
- "link_doctype": "Workflow",
- "link_fieldname": "document_type"
- },
- {
- "group": "Workflow",
- "link_doctype": "Notification",
- "link_fieldname": "document_type"
- },
- {
- "group": "Customization",
- "link_doctype": "Custom Field",
- "link_fieldname": "dt"
- },
- {
- "group": "Customization",
- "link_doctype": "Client Script",
- "link_fieldname": "dt"
- },
- {
- "group": "Customization",
- "link_doctype": "Server Script",
- "link_fieldname": "reference_doctype"
- },
- {
- "group": "Workflow",
- "link_doctype": "Webhook",
- "link_fieldname": "webhook_doctype"
- },
- {
- "group": "Views",
- "link_doctype": "Print Format",
- "link_fieldname": "doc_type"
- },
- {
- "group": "Views",
- "link_doctype": "Web Form",
- "link_fieldname": "doc_type"
- },
- {
- "group": "Views",
- "link_doctype": "Calendar View",
- "link_fieldname": "reference_doctype"
- },
- {
- "group": "Views",
- "link_doctype": "Kanban Board",
- "link_fieldname": "reference_doctype"
- },
- {
- "group": "Workflow",
- "link_doctype": "Onboarding Step",
- "link_fieldname": "reference_document"
- },
- {
- "group": "Rules",
- "link_doctype": "Auto Repeat",
- "link_fieldname": "reference_doctype"
- },
- {
- "group": "Rules",
- "link_doctype": "Assignment Rule",
- "link_fieldname": "document_type"
- },
- {
- "group": "Rules",
- "link_doctype": "Energy Point Rule",
- "link_fieldname": "reference_doctype"
- }
- ],
- "modified": "2021-09-05 15:39:13.233403",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "DocType",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Administrator",
- "share": 1,
- "write": 1
- }
- ],
- "route": "doctype",
- "search_fields": "module",
- "show_name_in_global_search": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
+ "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",
+ "migration_hash"
+ ],
+ "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:\nfield:[fieldname] - By Fieldnaming_series: - By Naming Series (field called naming_series must be presentPrompt - Prompt user for a name[series] - Series by prefix (separated by a dot); for example PRE.##### \nformat: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. ",
+ "fieldname": "autoname",
+ "fieldtype": "Data",
+ "label": "Auto Name",
+ "oldfieldname": "autoname",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "name_case",
+ "fieldtype": "Select",
+ "label": "Name Case",
+ "oldfieldname": "name_case",
+ "oldfieldtype": "Select",
+ "options": "\nTitle Case\nUPPER CASE"
+ },
+ {
+ "fieldname": "column_break_15",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "form_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Form Settings"
+ },
+ {
+ "description": "Must be of type \"Attach Image\"",
+ "fieldname": "image_field",
+ "fieldtype": "Data",
+ "label": "Image Field"
+ },
+ {
+ "depends_on": "eval:!doc.istable",
+ "description": "Comments and Communications will be associated with this linked document",
+ "fieldname": "timeline_field",
+ "fieldtype": "Data",
+ "label": "Timeline Field"
+ },
+ {
+ "fieldname": "max_attachments",
+ "fieldtype": "Int",
+ "label": "Max Attachments",
+ "oldfieldname": "max_attachments",
+ "oldfieldtype": "Int"
+ },
+ {
+ "fieldname": "column_break_23",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "hide_toolbar",
+ "fieldtype": "Check",
+ "label": "Hide Sidebar and Menu",
+ "oldfieldname": "hide_toolbar",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_copy",
+ "fieldtype": "Check",
+ "label": "Hide Copy",
+ "oldfieldname": "allow_copy",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "1",
+ "fieldname": "allow_rename",
+ "fieldtype": "Check",
+ "label": "Allow Rename",
+ "oldfieldname": "allow_rename",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_import",
+ "fieldtype": "Check",
+ "label": "Allow Import (via Data Import Tool)"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_events_in_timeline",
+ "fieldtype": "Check",
+ "label": "Allow events in timeline"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_auto_repeat",
+ "fieldtype": "Check",
+ "label": "Allow Auto Repeat"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "view_settings",
+ "fieldtype": "Section Break",
+ "label": "View Settings"
+ },
+ {
+ "depends_on": "eval:!doc.istable",
+ "fieldname": "title_field",
+ "fieldtype": "Data",
+ "label": "Title Field"
+ },
+ {
+ "depends_on": "eval:!doc.istable",
+ "fieldname": "search_fields",
+ "fieldtype": "Data",
+ "label": "Search Fields",
+ "oldfieldname": "search_fields",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "default_print_format",
+ "fieldtype": "Data",
+ "label": "Default Print Format"
+ },
+ {
+ "default": "modified",
+ "depends_on": "eval:!doc.istable",
+ "fieldname": "sort_field",
+ "fieldtype": "Data",
+ "label": "Default Sort Field"
+ },
+ {
+ "default": "DESC",
+ "depends_on": "eval:!doc.istable",
+ "fieldname": "sort_order",
+ "fieldtype": "Select",
+ "label": "Default Sort Order",
+ "options": "ASC\nDESC"
+ },
+ {
+ "fieldname": "column_break_29",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "document_type",
+ "fieldtype": "Select",
+ "label": "Show in Module Section",
+ "oldfieldname": "document_type",
+ "oldfieldtype": "Select",
+ "options": "\nDocument\nSetup\nSystem\nOther"
+ },
+ {
+ "fieldname": "icon",
+ "fieldtype": "Data",
+ "label": "Icon"
+ },
+ {
+ "fieldname": "color",
+ "fieldtype": "Data",
+ "label": "Color"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_preview_popup",
+ "fieldtype": "Check",
+ "label": "Show Preview Popup"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_name_in_global_search",
+ "fieldtype": "Check",
+ "label": "Make \"name\" searchable in Global Search"
+ },
+ {
+ "depends_on": "eval:!doc.istable",
+ "fieldname": "sb2",
+ "fieldtype": "Section Break",
+ "label": "Permission Rules"
+ },
+ {
+ "fieldname": "permissions",
+ "fieldtype": "Table",
+ "label": "Permissions",
+ "oldfieldname": "permissions",
+ "oldfieldtype": "Table",
+ "options": "DocPerm"
+ },
+ {
+ "fieldname": "restrict_to_domain",
+ "fieldtype": "Link",
+ "label": "Restrict To Domain",
+ "options": "Domain"
+ },
+ {
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "User Cannot Search",
+ "oldfieldname": "read_only",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_create",
+ "fieldtype": "Check",
+ "label": "User Cannot Create",
+ "oldfieldname": "in_create",
+ "oldfieldtype": "Check"
+ },
+ {
+ "depends_on": "eval:doc.custom===0",
+ "fieldname": "web_view",
+ "fieldtype": "Section Break",
+ "label": "Web View"
+ },
+ {
+ "default": "0",
+ "fieldname": "has_web_view",
+ "fieldtype": "Check",
+ "label": "Has Web View"
+ },
+ {
+ "default": "0",
+ "depends_on": "has_web_view",
+ "fieldname": "allow_guest_to_view",
+ "fieldtype": "Check",
+ "label": "Allow Guest to View"
+ },
+ {
+ "depends_on": "eval:!doc.istable",
+ "fieldname": "route",
+ "fieldtype": "Data",
+ "label": "Route"
+ },
+ {
+ "depends_on": "has_web_view",
+ "fieldname": "is_published_field",
+ "fieldtype": "Data",
+ "label": "Is Published Field"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "advanced",
+ "fieldtype": "Section Break",
+ "hidden": 1,
+ "label": "Advanced"
+ },
+ {
+ "default": "InnoDB",
+ "depends_on": "eval:!doc.issingle",
+ "fieldname": "engine",
+ "fieldtype": "Select",
+ "label": "Database Engine",
+ "options": "InnoDB\nMyISAM"
+ },
+ {
+ "default": "0",
+ "description": "Tree structures are implemented using Nested Set",
+ "fieldname": "is_tree",
+ "fieldtype": "Check",
+ "label": "Is Tree"
+ },
+ {
+ "depends_on": "is_tree",
+ "fieldname": "nsm_parent_field",
+ "fieldtype": "Data",
+ "label": "Parent Field (Tree)"
+ },
+ {
+ "description": "URL for documentation or help",
+ "fieldname": "documentation",
+ "fieldtype": "Data",
+ "label": "Documentation Link"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "actions",
+ "fieldname": "actions_section",
+ "fieldtype": "Section Break",
+ "label": "Actions"
+ },
+ {
+ "fieldname": "actions",
+ "fieldtype": "Table",
+ "label": "Actions",
+ "options": "DocType Action"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "links",
+ "fieldname": "links_section",
+ "fieldtype": "Section Break",
+ "label": "Linked Documents"
+ },
+ {
+ "fieldname": "links",
+ "fieldtype": "Table",
+ "label": "Links",
+ "options": "DocType Link"
+ },
+ {
+ "depends_on": "email_append_to",
+ "fieldname": "subject_field",
+ "fieldtype": "Data",
+ "label": "Subject Field"
+ },
+ {
+ "depends_on": "email_append_to",
+ "fieldname": "sender_field",
+ "fieldtype": "Data",
+ "label": "Sender Field",
+ "mandatory_depends_on": "email_append_to"
+ },
+ {
+ "default": "0",
+ "fieldname": "email_append_to",
+ "fieldtype": "Check",
+ "label": "Allow document creation via Email"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "email_settings_sb",
+ "fieldtype": "Section Break",
+ "label": "Email Settings"
+ },
+ {
+ "default": "1",
+ "fieldname": "index_web_pages_for_search",
+ "fieldtype": "Check",
+ "label": "Index Web Pages for Search"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_virtual",
+ "fieldtype": "Check",
+ "label": "Is Virtual"
+ },
+ {
+ "fieldname": "default_email_template",
+ "fieldtype": "Link",
+ "label": "Default Email Template",
+ "options": "Email Template"
+ },
+ {
+ "fieldname": "column_break_51",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "has_web_view",
+ "fieldname": "website_search_field",
+ "fieldtype": "Data",
+ "label": "Website Search Field"
+ },
+ {
+ "fieldname": "naming_rule",
+ "fieldtype": "Select",
+ "label": "Naming Rule",
+ "length": 40,
+ "options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
+ },
+ {
+ "fieldname": "migration_hash",
+ "fieldtype": "Data",
+ "hidden": 1
+ }
+ ],
+ "icon": "fa fa-bolt",
+ "idx": 6,
+ "links": [
+ {
+ "group": "Views",
+ "link_doctype": "Report",
+ "link_fieldname": "ref_doctype"
+ },
+ {
+ "group": "Workflow",
+ "link_doctype": "Workflow",
+ "link_fieldname": "document_type"
+ },
+ {
+ "group": "Workflow",
+ "link_doctype": "Notification",
+ "link_fieldname": "document_type"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Custom Field",
+ "link_fieldname": "dt"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Client Script",
+ "link_fieldname": "dt"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Server Script",
+ "link_fieldname": "reference_doctype"
+ },
+ {
+ "group": "Workflow",
+ "link_doctype": "Webhook",
+ "link_fieldname": "webhook_doctype"
+ },
+ {
+ "group": "Views",
+ "link_doctype": "Print Format",
+ "link_fieldname": "doc_type"
+ },
+ {
+ "group": "Views",
+ "link_doctype": "Web Form",
+ "link_fieldname": "doc_type"
+ },
+ {
+ "group": "Views",
+ "link_doctype": "Calendar View",
+ "link_fieldname": "reference_doctype"
+ },
+ {
+ "group": "Views",
+ "link_doctype": "Kanban Board",
+ "link_fieldname": "reference_doctype"
+ },
+ {
+ "group": "Workflow",
+ "link_doctype": "Onboarding Step",
+ "link_fieldname": "reference_document"
+ },
+ {
+ "group": "Rules",
+ "link_doctype": "Auto Repeat",
+ "link_fieldname": "reference_doctype"
+ },
+ {
+ "group": "Rules",
+ "link_doctype": "Assignment Rule",
+ "link_fieldname": "document_type"
+ },
+ {
+ "group": "Rules",
+ "link_doctype": "Energy Point Rule",
+ "link_fieldname": "reference_doctype"
+ }
+ ],
+ "modified": "2021-09-05 15:39:13.233403",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "DocType",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "route": "doctype",
+ "search_fields": "module",
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 9bf21690fc..5a91016e32 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -23,6 +23,7 @@ from frappe.modules.import_file import get_file_path
from frappe.model.meta import Meta
from frappe.desk.utils import validate_route_conflict
from frappe.website.utils import clear_cache
+from frappe.query_builder.functions import Concat
class InvalidFieldNameError(frappe.ValidationError): pass
class UniqueFieldnameError(frappe.ValidationError): pass
@@ -274,6 +275,8 @@ class DocType(Document):
d.fieldname = d.fieldname + '_section'
elif d.fieldtype=='Column Break':
d.fieldname = d.fieldname + '_column'
+ elif d.fieldtype=='Tab Break':
+ d.fieldname = d.fieldname + '_tab'
else:
d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx)
else:
@@ -463,7 +466,7 @@ class DocType(Document):
return
# check if atleast 1 record exists
- if not (frappe.db.table_exists(self.name) and frappe.db.sql("select name from `tab{}` limit 1".format(self.name))):
+ if not (frappe.db.table_exists(self.name) and frappe.get_all(self.name, fields=["name"], limit=1, as_list=True)):
return
existing_property_setter = frappe.db.get_value("Property Setter", {"doc_type": self.name,
@@ -569,17 +572,17 @@ class DocType(Document):
def make_amendable(self):
"""If is_submittable is set, add amended_from docfields."""
if self.is_submittable:
- if not frappe.db.sql("""select name from tabDocField
- where fieldname = 'amended_from' and parent = %s""", self.name):
- self.append("fields", {
- "label": "Amended From",
- "fieldtype": "Link",
- "fieldname": "amended_from",
- "options": self.name,
- "read_only": 1,
- "print_hide": 1,
- "no_copy": 1
- })
+ docfield_exists = frappe.get_all("DocField", filters={"fieldname": "amended_from", "parent": self.name}, pluck="name", limit=1)
+ if not docfield_exists:
+ self.append("fields", {
+ "label": "Amended From",
+ "fieldtype": "Link",
+ "fieldname": "amended_from",
+ "options": self.name,
+ "read_only": 1,
+ "print_hide": 1,
+ "no_copy": 1
+ })
def make_repeatable(self):
"""If allow_auto_repeat is set, add auto_repeat custom field."""
@@ -704,12 +707,13 @@ def validate_series(dt, autoname=None, name=None):
and (not autoname.startswith('format:')):
prefix = autoname.split('.')[0]
- used_in = frappe.db.sql("""
- SELECT `name`
- FROM `tabDocType`
- WHERE `autoname` LIKE CONCAT(%s, '.%%')
- AND `name`!=%s
- """, (prefix, name))
+ doctype = frappe.qb.DocType("DocType")
+ used_in = (frappe.qb
+ .from_(doctype)
+ .select(doctype.name)
+ .where(doctype.autoname.like(Concat(prefix,".%")))
+ .where(doctype.name != name)
+ ).run()
if used_in:
frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))
diff --git a/frappe/core/doctype/feedback/test_feedback.py b/frappe/core/doctype/feedback/test_feedback.py
index 7a722ae0d1..f3cf8dfe6b 100644
--- a/frappe/core/doctype/feedback/test_feedback.py
+++ b/frappe/core/doctype/feedback/test_feedback.py
@@ -5,6 +5,13 @@ import frappe
import unittest
class TestFeedback(unittest.TestCase):
+ def tearDown(self):
+ frappe.form_dict.reference_doctype = None
+ frappe.form_dict.reference_name = None
+ frappe.form_dict.rating = None
+ frappe.form_dict.feedback = None
+ frappe.local.request_ip = None
+
def test_feedback_creation_updation(self):
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
test_blog = make_test_blog()
@@ -12,7 +19,14 @@ class TestFeedback(unittest.TestCase):
frappe.db.delete("Feedback", {"reference_doctype": "Blog Post"})
from frappe.templates.includes.feedback.feedback import add_feedback, update_feedback
- feedback = add_feedback('Blog Post', test_blog.name, 5, 'New feedback')
+
+ frappe.form_dict.reference_doctype = 'Blog Post'
+ frappe.form_dict.reference_name = test_blog.name
+ frappe.form_dict.rating = 5
+ frappe.form_dict.feedback = 'New feedback'
+ frappe.local.request_ip = '127.0.0.1'
+
+ feedback = add_feedback()
self.assertEqual(feedback.feedback, 'New feedback')
self.assertEqual(feedback.rating, 5)
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index d9ecd85533..4df9ef3132 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -813,7 +813,7 @@ def extract_images_from_doc(doc, fieldname):
doc.set(fieldname, content)
-def extract_images_from_html(doc, content):
+def extract_images_from_html(doc, content, is_private=False):
frappe.flags.has_dataurl = False
def _save_file(match):
@@ -846,7 +846,8 @@ def extract_images_from_html(doc, content):
"attached_to_doctype": doctype,
"attached_to_name": name,
"content": content,
- "decode": False
+ "decode": False,
+ "is_private": is_private
})
_file.save(ignore_permissions=True)
file_url = _file.file_url
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index 4538ffb6bb..9a758b53f5 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -204,10 +204,14 @@ class TestFile(unittest.TestCase):
def delete_test_data(self):
- for f in frappe.db.sql('''select name, file_name from tabFile where
- is_home_folder = 0 and is_attachments_folder = 0 order by creation desc'''):
- frappe.delete_doc("File", f[0])
-
+ test_file_data = frappe.db.get_all(
+ "File",
+ pluck="name",
+ filters={"is_home_folder": 0, "is_attachments_folder": 0},
+ order_by="creation desc",
+ )
+ for f in test_file_data:
+ frappe.delete_doc("File", f)
def upload_file(self):
_file = frappe.get_doc({
diff --git a/frappe/core/doctype/language/language.json b/frappe/core/doctype/language/language.json
index eed29883c1..9ab8f55f6b 100644
--- a/frappe/core/doctype/language/language.json
+++ b/frappe/core/doctype/language/language.json
@@ -7,6 +7,7 @@
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
+ "enabled",
"language_code",
"language_name",
"flag",
@@ -39,15 +40,22 @@
"fieldtype": "Link",
"label": "Based On",
"options": "Language"
+ },
+ {
+ "default": "1",
+ "fieldname": "enabled",
+ "fieldtype": "Check",
+ "label": "Enabled"
}
],
"icon": "fa fa-globe",
"in_create": 1,
"links": [],
- "modified": "2020-04-16 22:11:33.066852",
+ "modified": "2021-10-18 14:02:06.818219",
"modified_by": "Administrator",
"module": "Core",
"name": "Language",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py
index 8a471b9173..c505302c52 100644
--- a/frappe/core/doctype/log_settings/log_settings.py
+++ b/frappe/core/doctype/log_settings/log_settings.py
@@ -38,7 +38,7 @@ def has_unseen_error_log(user):
'message': _("You have unseen {0}").format(' Error Logs ')
}
- if frappe.db.sql_list("select name from `tabError Log` where seen = 0 limit 1"):
+ if frappe.get_all("Error Log", filters={"seen": 0}, limit=1):
log_settings = frappe.get_cached_doc('Log Settings')
if log_settings.users_to_notify:
diff --git a/frappe/core/doctype/navbar_settings/navbar_settings.py b/frappe/core/doctype/navbar_settings/navbar_settings.py
index fd8db31d10..46eb5c3e7a 100644
--- a/frappe/core/doctype/navbar_settings/navbar_settings.py
+++ b/frappe/core/doctype/navbar_settings/navbar_settings.py
@@ -22,7 +22,6 @@ class NavbarSettings(Document):
if not frappe.flags.in_patch and (len(before_save_items) > len(after_save_items)):
frappe.throw(_("Please hide the standard navbar items instead of deleting them"))
-@frappe.whitelist(allow_guest=True)
def get_app_logo():
app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo', cache=True)
if not app_logo:
diff --git a/frappe/core/doctype/package_release/package_release.py b/frappe/core/doctype/package_release/package_release.py
index 1fb8796882..d23ae917c4 100644
--- a/frappe/core/doctype/package_release/package_release.py
+++ b/frappe/core/doctype/package_release/package_release.py
@@ -6,16 +6,27 @@ from frappe.model.document import Document
from frappe.modules.export_file import export_doc
import os
import subprocess
+from frappe.query_builder.functions import Max
+
class PackageRelease(Document):
def set_version(self):
# set the next patch release by default
+ doctype = frappe.qb.DocType("Package Release")
if not self.major:
- self.major = frappe.db.max('Package Release', 'major', dict(package=self.package))
+ self.major = frappe.qb.from_(doctype) \
+ .where(doctype.package == self.package) \
+ .select(Max(doctype.minor)).run()[0][0] or 0
+
if not self.minor:
- self.minor = frappe.db.max('Package Release', 'minor', dict(package=self.package))
+ self.minor = frappe.qb.from_(doctype) \
+ .where(doctype.package == self.package) \
+ .select(Max("minor")).run()[0][0] or 0
if not self.patch:
- self.patch = frappe.db.max('Package Release', 'patch', dict(package=self.package)) + 1
+ value = frappe.qb.from_(doctype) \
+ .where(doctype.package == self.package) \
+ .select(Max("patch")).run()[0][0] or 0
+ self.patch = value + 1
def autoname(self):
self.set_version()
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 79fe7a9140..5b1aab1241 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -94,7 +94,7 @@ class ServerScript(Document):
Args:
doc (Document): Executes script with for a certain document's events
"""
- safe_exec(self.script, _locals={"doc": doc})
+ safe_exec(self.script, _locals={"doc": doc}, restrict_commit_rollback=True)
def execute_scheduled_method(self):
"""Specific to Scheduled Jobs via Server Scripts
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index 6c028ff136..de858327a9 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -59,6 +59,26 @@ conditions = '1 = 1'
reference_doctype = 'Note',
script = '''
frappe.method_that_doesnt_exist("do some magic")
+'''
+ ),
+ dict(
+ name='test_todo_commit',
+ script_type = 'DocType Event',
+ doctype_event = 'Before Save',
+ reference_doctype = 'ToDo',
+ disabled = 1,
+ script = '''
+frappe.db.commit()
+'''
+ ),
+ dict(
+ name='test_cache_methods',
+ script_type = 'DocType Event',
+ doctype_event = 'Before Save',
+ reference_doctype = 'ToDo',
+ disabled = 1,
+ script = '''
+frappe.cache().set_value('test_key', doc.name)
'''
)
]
@@ -119,3 +139,24 @@ class TestServerScript(unittest.TestCase):
self.assertTrue("invalid python code" in str(se.exception).lower(),
msg="Python code validation not working")
+
+ def test_commit_in_doctype_event(self):
+ server_script = frappe.get_doc('Server Script', 'test_todo_commit')
+ server_script.disabled = 0
+ server_script.save()
+
+ self.assertRaises(AttributeError, frappe.get_doc(dict(doctype='ToDo', description='test me')).insert)
+
+ server_script.disabled = 1
+ server_script.save()
+
+ def test_cache_methods_in_server_script(self):
+ server_script = frappe.get_doc('Server Script', 'test_cache_methods')
+ server_script.disabled = 0
+ server_script.save()
+
+ todo = frappe.get_doc(dict(doctype='ToDo', description='test me')).insert()
+ self.assertEqual(todo.name, frappe.cache().get_value('test_key'))
+
+ server_script.disabled = 1
+ server_script.save()
diff --git a/frappe/core/doctype/sms_settings/sms_settings.json b/frappe/core/doctype/sms_settings/sms_settings.json
index 073fb88bc7..d29949af45 100755
--- a/frappe/core/doctype/sms_settings/sms_settings.json
+++ b/frappe/core/doctype/sms_settings/sms_settings.json
@@ -1,238 +1,80 @@
{
- "allow_copy": 1,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-01-10 16:34:24",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "editable_grid": 0,
+ "actions": [],
+ "allow_copy": 1,
+ "creation": "2013-01-10 16:34:24",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "sms_gateway_url",
+ "message_parameter",
+ "receiver_parameter",
+ "static_parameters_section",
+ "parameters",
+ "use_post"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Eg. smsgateway.com/api/send_sms.cgi",
- "fieldname": "sms_gateway_url",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "SMS Gateway URL",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Eg. smsgateway.com/api/send_sms.cgi",
+ "fieldname": "sms_gateway_url",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "SMS Gateway URL",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Enter url parameter for message",
- "fieldname": "message_parameter",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Message Parameter",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Enter url parameter for message",
+ "fieldname": "message_parameter",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Message Parameter",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Enter url parameter for receiver nos",
- "fieldname": "receiver_parameter",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Receiver Parameter",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Enter url parameter for receiver nos",
+ "fieldname": "receiver_parameter",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Receiver Parameter",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "static_parameters_section",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "static_parameters_section",
+ "fieldtype": "Column Break",
"width": "50%"
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)",
- "fieldname": "parameters",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Static Parameters",
- "length": 0,
- "no_copy": 0,
- "options": "SMS Parameter",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)",
+ "fieldname": "parameters",
+ "fieldtype": "Table",
+ "label": "Static Parameters",
+ "options": "SMS Parameter"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "use_post",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Use POST",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "default": "0",
+ "fieldname": "use_post",
+ "fieldtype": "Check",
+ "label": "Use POST"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-cog",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2021-03-02 18:06:00.868688",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "SMS Settings",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-cog",
+ "idx": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-09-21 19:45:26.809793",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "SMS Settings",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "track_changes": 1,
- "track_seen": 0
-}
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/transaction_log/transaction_log.py b/frappe/core/doctype/transaction_log/transaction_log.py
index bb94642f48..e2e75b130c 100644
--- a/frappe/core/doctype/transaction_log/transaction_log.py
+++ b/frappe/core/doctype/transaction_log/transaction_log.py
@@ -14,10 +14,9 @@ class TransactionLog(Document):
self.row_index = index
self.timestamp = now_datetime()
if index != 1:
- prev_hash = frappe.db.sql(
- "SELECT `chaining_hash` FROM `tabTransaction Log` WHERE `row_index` = '{0}'".format(index - 1))
+ prev_hash = frappe.get_all("Transaction Log", filters={"row_index":str(index-1)}, pluck="chaining_hash", limit=1)
if prev_hash:
- self.previous_hash = prev_hash[0][0]
+ self.previous_hash = prev_hash[0]
else:
self.previous_hash = "Indexing broken"
else:
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 1d5f89897d..cd7dcd6a34 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -202,7 +202,8 @@
"fieldname": "role_profile_name",
"fieldtype": "Link",
"label": "Role Profile",
- "options": "Role Profile"
+ "options": "Role Profile",
+ "permlevel": 1
},
{
"fieldname": "roles_html",
@@ -670,7 +671,7 @@
}
],
"max_attachments": 5,
- "modified": "2021-02-02 16:11:06.037543",
+ "modified": "2021-10-18 16:56:05.578379",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 147f4ddfee..e4b94cdbb6 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -788,7 +788,7 @@ def sign_up(email, full_name, redirect_to):
return 2, _("Please ask your administrator to verify your sign-up")
@frappe.whitelist(allow_guest=True)
-@rate_limit(key='user', limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST'])
+@rate_limit(limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST'])
def reset_password(user):
if user=="Administrator":
return 'not allowed'
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index 66ffd48822..1366ace115 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -54,7 +54,7 @@ class UserPermission(Document):
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name)
frappe.throw(_("{0} has already assigned default value for {1}.").format(ref_link, self.allow))
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
def get_user_permissions(user=None):
'''Get all users permissions for the user as a dict of doctype'''
# if this is called from client-side,
diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py
index 5d9bb815da..4d9deca526 100644
--- a/frappe/core/page/background_jobs/background_jobs.py
+++ b/frappe/core/page/background_jobs/background_jobs.py
@@ -67,7 +67,8 @@ def get_info(show_failed=False) -> List[Dict]:
fail_registry = queue.failed_job_registry
for job_id in fail_registry.get_job_ids():
job = queue.fetch_job(job_id)
- add_job(job, queue.name)
+ if job:
+ add_job(job, queue.name)
return jobs
diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js
index 41cc900a97..6b427fdebf 100644
--- a/frappe/core/page/permission_manager/permission_manager.js
+++ b/frappe/core/page/permission_manager/permission_manager.js
@@ -325,15 +325,15 @@ frappe.PermissionEngine = class PermissionEngine {
.attr("data-doctype", d.parent)
.attr("data-role", d.role)
.attr("data-permlevel", d.permlevel)
- .click(function () {
+ .on("click", () => {
return frappe.call({
module: "frappe.core",
page: "permission_manager",
method: "remove",
args: {
- doctype: $(this).attr("data-doctype"),
- role: $(this).attr("data-role"),
- permlevel: $(this).attr("data-permlevel")
+ doctype: d.parent,
+ role: d.role,
+ permlevel: d.permlevel
},
callback: (r) => {
if (r.exc) {
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index a8b1fb0e23..235f11aad8 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -1,460 +1,458 @@
{
- "actions": [],
- "allow_import": 1,
- "creation": "2013-01-10 16:34:01",
- "description": "Adds a custom field to a DocType",
- "doctype": "DocType",
- "document_type": "Setup",
- "engine": "InnoDB",
- "field_order": [
- "dt",
- "module",
- "label",
- "label_help",
- "fieldname",
- "insert_after",
- "length",
- "column_break_6",
- "fieldtype",
- "precision",
- "hide_seconds",
- "hide_days",
- "options",
- "fetch_from",
- "fetch_if_empty",
- "options_help",
- "section_break_11",
- "collapsible",
- "collapsible_depends_on",
- "default",
- "depends_on",
- "mandatory_depends_on",
- "read_only_depends_on",
- "properties",
- "non_negative",
- "reqd",
- "unique",
- "read_only",
- "ignore_user_permissions",
- "hidden",
- "print_hide",
- "print_hide_if_no_value",
- "print_width",
- "no_copy",
- "allow_on_submit",
- "in_list_view",
- "in_standard_filter",
- "in_global_search",
- "in_preview",
- "bold",
- "report_hide",
- "search_index",
- "allow_in_quick_entry",
- "ignore_xss_filter",
- "translatable",
- "hide_border",
- "description",
- "permlevel",
- "width",
- "columns"
- ],
- "fields": [
- {
- "bold": 1,
- "fieldname": "dt",
- "fieldtype": "Link",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Document",
- "oldfieldname": "dt",
- "oldfieldtype": "Link",
- "options": "DocType",
- "reqd": 1,
- "search_index": 1
- },
- {
- "bold": 1,
- "fieldname": "label",
- "fieldtype": "Data",
- "in_filter": 1,
- "label": "Label",
- "no_copy": 1,
- "oldfieldname": "label",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "label_help",
- "fieldtype": "HTML",
- "label": "Label Help",
- "oldfieldtype": "HTML"
- },
- {
- "fieldname": "fieldname",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Fieldname",
- "no_copy": 1,
- "oldfieldname": "fieldname",
- "oldfieldtype": "Data",
- "read_only": 1
- },
- {
- "description": "Select the label after which you want to insert new field.",
- "fieldname": "insert_after",
- "fieldtype": "Select",
- "label": "Insert After",
- "no_copy": 1,
- "oldfieldname": "insert_after",
- "oldfieldtype": "Select"
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Column Break"
- },
- {
- "bold": 1,
- "default": "Data",
- "fieldname": "fieldtype",
- "fieldtype": "Select",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Field Type",
- "oldfieldname": "fieldtype",
- "oldfieldtype": "Select",
- "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\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
- },
- {
- "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
- "description": "Set non-standard precision for a Float or Currency field",
- "fieldname": "precision",
- "fieldtype": "Select",
- "label": "Precision",
- "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
- },
- {
- "fieldname": "options",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Options",
- "oldfieldname": "options",
- "oldfieldtype": "Text"
- },
- {
- "fieldname": "fetch_from",
- "fieldtype": "Small Text",
- "label": "Fetch From"
- },
- {
- "default": "0",
- "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
- "fieldname": "fetch_if_empty",
- "fieldtype": "Check",
- "label": "Fetch If Empty"
- },
- {
- "fieldname": "options_help",
- "fieldtype": "HTML",
- "label": "Options Help",
- "oldfieldtype": "HTML"
- },
- {
- "fieldname": "section_break_11",
- "fieldtype": "Section Break"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible",
- "fieldtype": "Check",
- "label": "Collapsible"
- },
- {
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible_depends_on",
- "fieldtype": "Code",
- "label": "Collapsible Depends On"
- },
- {
- "fieldname": "default",
- "fieldtype": "Text",
- "label": "Default Value",
- "oldfieldname": "default",
- "oldfieldtype": "Text"
- },
- {
- "fieldname": "depends_on",
- "fieldtype": "Code",
- "label": "Depends On",
- "length": 255
- },
- {
- "fieldname": "description",
- "fieldtype": "Text",
- "label": "Field Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "print_width": "300px",
- "width": "300px"
- },
- {
- "default": "0",
- "fieldname": "permlevel",
- "fieldtype": "Int",
- "label": "Permission Level",
- "oldfieldname": "permlevel",
- "oldfieldtype": "Int"
- },
- {
- "fieldname": "width",
- "fieldtype": "Data",
- "label": "Width",
- "oldfieldname": "width",
- "oldfieldtype": "Data"
- },
- {
- "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
- "fieldname": "columns",
- "fieldtype": "Int",
- "label": "Columns"
- },
- {
- "fieldname": "properties",
- "fieldtype": "Column Break",
- "oldfieldtype": "Column Break",
- "print_width": "50%",
- "width": "50%"
- },
- {
- "default": "0",
- "fieldname": "reqd",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Is Mandatory Field",
- "oldfieldname": "reqd",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "unique",
- "fieldtype": "Check",
- "label": "Unique"
- },
- {
- "default": "0",
- "fieldname": "read_only",
- "fieldtype": "Check",
- "label": "Read Only"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype===\"Link\"",
- "fieldname": "ignore_user_permissions",
- "fieldtype": "Check",
- "label": "Ignore User Permissions"
- },
- {
- "default": "0",
- "fieldname": "hidden",
- "fieldtype": "Check",
- "label": "Hidden"
- },
- {
- "default": "0",
- "fieldname": "print_hide",
- "fieldtype": "Check",
- "label": "Print Hide",
- "oldfieldname": "print_hide",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
- "fieldname": "print_hide_if_no_value",
- "fieldtype": "Check",
- "label": "Print Hide If No Value"
- },
- {
- "fieldname": "print_width",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Print Width",
- "no_copy": 1,
- "print_hide": 1
- },
- {
- "default": "0",
- "fieldname": "no_copy",
- "fieldtype": "Check",
- "label": "No Copy",
- "oldfieldname": "no_copy",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "allow_on_submit",
- "fieldtype": "Check",
- "label": "Allow on Submit",
- "oldfieldname": "allow_on_submit",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "in_list_view",
- "fieldtype": "Check",
- "label": "In List View"
- },
- {
- "default": "0",
- "fieldname": "in_standard_filter",
- "fieldtype": "Check",
- "label": "In Standard Filter"
- },
- {
- "default": "0",
- "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
- "fieldname": "in_global_search",
- "fieldtype": "Check",
- "label": "In Global Search"
- },
- {
- "default": "0",
- "fieldname": "bold",
- "fieldtype": "Check",
- "label": "Bold"
- },
- {
- "default": "0",
- "fieldname": "report_hide",
- "fieldtype": "Check",
- "label": "Report Hide",
- "oldfieldname": "report_hide",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "search_index",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Index",
- "no_copy": 1,
- "print_hide": 1
- },
- {
- "default": "0",
- "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
- "fieldname": "ignore_xss_filter",
- "fieldtype": "Check",
- "label": "Ignore XSS Filter"
- },
- {
- "default": "1",
- "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
- "fieldname": "translatable",
- "fieldtype": "Check",
- "label": "Translatable"
- },
- {
- "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
- "fieldname": "length",
- "fieldtype": "Int",
- "label": "Length"
- },
- {
- "fieldname": "mandatory_depends_on",
- "fieldtype": "Code",
- "label": "Mandatory Depends On",
- "length": 255
- },
- {
- "fieldname": "read_only_depends_on",
- "fieldtype": "Code",
- "label": "Read Only Depends On",
- "length": 255
- },
- {
- "default": "0",
- "fieldname": "allow_in_quick_entry",
- "fieldtype": "Check",
- "label": "Allow in Quick Entry"
- },
- {
- "default": "0",
- "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
- "fieldname": "in_preview",
- "fieldtype": "Check",
- "label": "In Preview"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Duration'",
- "fieldname": "hide_seconds",
- "fieldtype": "Check",
- "label": "Hide Seconds"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Duration'",
- "fieldname": "hide_days",
- "fieldtype": "Check",
- "label": "Hide Days"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Section Break'",
- "fieldname": "hide_border",
- "fieldtype": "Check",
- "label": "Hide Border"
- },
- {
- "default": "0",
- "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
- "fieldname": "non_negative",
- "fieldtype": "Check",
- "label": "Non Negative"
- },
- {
- "fieldname": "module",
- "fieldtype": "Link",
- "label": "Module (for export)",
- "options": "Module Def"
- }
- ],
- "icon": "fa fa-glass",
- "idx": 1,
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-09-04 12:45:22.810120",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Custom Field",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Administrator",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "search_fields": "dt,label,fieldtype,options",
- "sort_field": "modified",
- "sort_order": "ASC",
- "track_changes": 1
+ "actions": [],
+ "allow_import": 1,
+ "creation": "2013-01-10 16:34:01",
+ "description": "Adds a custom field to a DocType",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "dt",
+ "module",
+ "label",
+ "label_help",
+ "fieldname",
+ "insert_after",
+ "length",
+ "column_break_6",
+ "fieldtype",
+ "precision",
+ "hide_seconds",
+ "hide_days",
+ "options",
+ "fetch_from",
+ "fetch_if_empty",
+ "options_help",
+ "section_break_11",
+ "collapsible",
+ "collapsible_depends_on",
+ "default",
+ "depends_on",
+ "mandatory_depends_on",
+ "read_only_depends_on",
+ "properties",
+ "non_negative",
+ "reqd",
+ "unique",
+ "read_only",
+ "ignore_user_permissions",
+ "hidden",
+ "print_hide",
+ "print_hide_if_no_value",
+ "print_width",
+ "no_copy",
+ "allow_on_submit",
+ "in_list_view",
+ "in_standard_filter",
+ "in_global_search",
+ "in_preview",
+ "bold",
+ "report_hide",
+ "search_index",
+ "allow_in_quick_entry",
+ "ignore_xss_filter",
+ "translatable",
+ "hide_border",
+ "description",
+ "permlevel",
+ "width",
+ "columns"
+ ],
+ "fields": [{
+ "bold": 1,
+ "fieldname": "dt",
+ "fieldtype": "Link",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Document",
+ "oldfieldname": "dt",
+ "oldfieldtype": "Link",
+ "options": "DocType",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_filter": 1,
+ "label": "Label",
+ "no_copy": 1,
+ "oldfieldname": "label",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "label_help",
+ "fieldtype": "HTML",
+ "label": "Label Help",
+ "oldfieldtype": "HTML"
+ },
+ {
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Fieldname",
+ "no_copy": 1,
+ "oldfieldname": "fieldname",
+ "oldfieldtype": "Data",
+ "read_only": 1
+ },
+ {
+ "description": "Select the label after which you want to insert new field.",
+ "fieldname": "insert_after",
+ "fieldtype": "Select",
+ "label": "Insert After",
+ "no_copy": 1,
+ "oldfieldname": "insert_after",
+ "oldfieldtype": "Select"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "bold": 1,
+ "default": "Data",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Field Type",
+ "oldfieldname": "fieldtype",
+ "oldfieldtype": "Select",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\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
+ },
+ {
+ "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
+ "description": "Set non-standard precision for a Float or Currency field",
+ "fieldname": "precision",
+ "fieldtype": "Select",
+ "label": "Precision",
+ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
+ },
+ {
+ "fieldname": "options",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Options",
+ "oldfieldname": "options",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "fetch_from",
+ "fieldtype": "Small Text",
+ "label": "Fetch From"
+ },
+ {
+ "default": "0",
+ "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
+ "fieldname": "fetch_if_empty",
+ "fieldtype": "Check",
+ "label": "Fetch If Empty"
+ },
+ {
+ "fieldname": "options_help",
+ "fieldtype": "HTML",
+ "label": "Options Help",
+ "oldfieldtype": "HTML"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible",
+ "fieldtype": "Check",
+ "label": "Collapsible"
+ },
+ {
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible_depends_on",
+ "fieldtype": "Code",
+ "label": "Collapsible Depends On"
+ },
+ {
+ "fieldname": "default",
+ "fieldtype": "Text",
+ "label": "Default Value",
+ "oldfieldname": "default",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "depends_on",
+ "fieldtype": "Code",
+ "label": "Depends On",
+ "length": 255
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Field Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "print_width": "300px",
+ "width": "300px"
+ },
+ {
+ "default": "0",
+ "fieldname": "permlevel",
+ "fieldtype": "Int",
+ "label": "Permission Level",
+ "oldfieldname": "permlevel",
+ "oldfieldtype": "Int"
+ },
+ {
+ "fieldname": "width",
+ "fieldtype": "Data",
+ "label": "Width",
+ "oldfieldname": "width",
+ "oldfieldtype": "Data"
+ },
+ {
+ "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
+ "fieldname": "columns",
+ "fieldtype": "Int",
+ "label": "Columns"
+ },
+ {
+ "fieldname": "properties",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "print_width": "50%",
+ "width": "50%"
+ },
+ {
+ "default": "0",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Is Mandatory Field",
+ "oldfieldname": "reqd",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "unique",
+ "fieldtype": "Check",
+ "label": "Unique"
+ },
+ {
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "Read Only"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype===\"Link\"",
+ "fieldname": "ignore_user_permissions",
+ "fieldtype": "Check",
+ "label": "Ignore User Permissions"
+ },
+ {
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden"
+ },
+ {
+ "default": "0",
+ "fieldname": "print_hide",
+ "fieldtype": "Check",
+ "label": "Print Hide",
+ "oldfieldname": "print_hide",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
+ "fieldname": "print_hide_if_no_value",
+ "fieldtype": "Check",
+ "label": "Print Hide If No Value"
+ },
+ {
+ "fieldname": "print_width",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Print Width",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "no_copy",
+ "fieldtype": "Check",
+ "label": "No Copy",
+ "oldfieldname": "no_copy",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_on_submit",
+ "fieldtype": "Check",
+ "label": "Allow on Submit",
+ "oldfieldname": "allow_on_submit",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_list_view",
+ "fieldtype": "Check",
+ "label": "In List View"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_standard_filter",
+ "fieldtype": "Check",
+ "label": "In Standard Filter"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
+ "fieldname": "in_global_search",
+ "fieldtype": "Check",
+ "label": "In Global Search"
+ },
+ {
+ "default": "0",
+ "fieldname": "bold",
+ "fieldtype": "Check",
+ "label": "Bold"
+ },
+ {
+ "default": "0",
+ "fieldname": "report_hide",
+ "fieldtype": "Check",
+ "label": "Report Hide",
+ "oldfieldname": "report_hide",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "search_index",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Index",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
+ "fieldname": "ignore_xss_filter",
+ "fieldtype": "Check",
+ "label": "Ignore XSS Filter"
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
+ "fieldname": "translatable",
+ "fieldtype": "Check",
+ "label": "Translatable"
+ },
+ {
+ "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
+ "fieldname": "length",
+ "fieldtype": "Int",
+ "label": "Length"
+ },
+ {
+ "fieldname": "mandatory_depends_on",
+ "fieldtype": "Code",
+ "label": "Mandatory Depends On",
+ "length": 255
+ },
+ {
+ "fieldname": "read_only_depends_on",
+ "fieldtype": "Code",
+ "label": "Read Only Depends On",
+ "length": 255
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_in_quick_entry",
+ "fieldtype": "Check",
+ "label": "Allow in Quick Entry"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
+ "fieldtype": "Check",
+ "label": "Hide Seconds"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
+ "fieldtype": "Check",
+ "label": "Hide Days"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Section Break'",
+ "fieldname": "hide_border",
+ "fieldtype": "Check",
+ "label": "Hide Border"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
+ "fieldname": "non_negative",
+ "fieldtype": "Check",
+ "label": "Non Negative"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
+ }
+ ],
+ "icon": "fa fa-glass",
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-04 12:45:23.810120",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Custom Field",
+ "owner": "Administrator",
+ "permissions": [{
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "dt,label,fieldtype,options",
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index 61fc4486bd..bf606701da 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -18,7 +18,7 @@ class CustomField(Document):
if not self.fieldname:
label = self.label
if not label:
- if self.fieldtype in ["Section Break", "Column Break"]:
+ if self.fieldtype in ["Section Break", "Column Break", "Tab Break"]:
label = self.fieldtype + "_" + str(self.idx)
else:
frappe.throw(_("Label is mandatory"))
diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json
index 0a456b1026..986b99a7af 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.json
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json
@@ -82,7 +82,7 @@
"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\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
+ "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\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nTab Break",
"reqd": 1,
"search_index": 1
},
@@ -428,7 +428,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-07-10 21:57:24.479749",
+ "modified": "2021-07-11 21:57:24.479749",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",
diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py
index 7b95408060..d71b7b0021 100644
--- a/frappe/custom/doctype/property_setter/property_setter.py
+++ b/frappe/custom/doctype/property_setter/property_setter.py
@@ -34,7 +34,7 @@ class PropertySetter(Document):
fields=['fieldname', 'label', 'fieldtype'],
filters={
'parent': dt,
- 'fieldtype': ['not in', ('Section Break', 'Column Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
+ 'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
'fieldname': ['!=', '']
},
order_by='label asc',
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 14f2297398..037374dc09 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -14,8 +14,13 @@ import frappe.model.meta
from frappe import _
from time import time
-from frappe.utils import now, getdate, cast, get_datetime, get_table_name
+from frappe.utils import now, getdate, cast, get_datetime
from frappe.model.utils.link_count import flush_local_link_count
+from frappe.query_builder.functions import Count
+from frappe.query_builder.functions import Min, Max, Avg, Sum
+from frappe.query_builder.utils import Column
+from .query import Query
+from pypika.terms import PseudoColumn
def get_human_friendly_error_message():
@@ -62,6 +67,7 @@ class Database(object):
self.password = password or frappe.conf.db_password
self.value_cache = {}
+ self.query = Query()
def setup_type_map(self):
pass
@@ -84,7 +90,7 @@ class Database(object):
pass
def sql(self, query, values=(), as_dict = 0, as_list = 0, formatted = 0,
- debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None, explain=False):
+ debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None, explain=False, run=True):
"""Execute a SQL query and fetch all rows.
:param query: SQL query.
@@ -97,7 +103,7 @@ class Database(object):
:param as_utf8: Encode values as UTF 8.
:param auto_commit: Commit after executing the query.
:param update: Update this dict to all rows (if returned `as_dict`).
-
+ :param run: Returns query without executing it if False.
Examples:
# return customer names as dicts
@@ -112,6 +118,9 @@ class Database(object):
"""
query = str(query)
+ if not run:
+ return query
+
if re.search(r'ifnull\(', query, flags=re.IGNORECASE):
# replaces ifnull in query with coalesce
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE)
@@ -331,59 +340,6 @@ class Database(object):
nres.append(nr)
return nres
- def build_conditions(self, filters):
- """Convert filters sent as dict, lists to SQL conditions. filter's key
- is passed by map function, build conditions like:
-
- * ifnull(`fieldname`, default_value) = %(fieldname)s
- * `fieldname` [=, !=, >, >=, <, <=] %(fieldname)s
- """
- conditions = []
- values = {}
- def _build_condition(key):
- """
- filter's key is passed by map function
- build conditions like:
- * ifnull(`fieldname`, default_value) = %(fieldname)s
- * `fieldname` [=, !=, >, >=, <, <=] %(fieldname)s
- """
- _operator = "="
- _rhs = " %(" + key + ")s"
- value = filters.get(key)
- values[key] = value
- if isinstance(value, (list, tuple)):
- # value is a tuple like ("!=", 0)
- _operator = value[0]
- values[key] = value[1]
- if isinstance(value[1], (tuple, list)):
- # value is a list in tuple ("in", ("A", "B"))
- _rhs = " ({0})".format(", ".join(self.escape(v) for v in value[1]))
- del values[key]
-
- if _operator not in ["=", "!=", ">", ">=", "<", "<=", "like", "in", "not in", "not like"]:
- _operator = "="
-
- if "[" in key:
- split_key = key.split("[")
- condition = "coalesce(`" + split_key[0] + "`, " + split_key[1][:-1] + ") " \
- + _operator + _rhs
- else:
- condition = "`" + key + "` " + _operator + _rhs
-
- conditions.append(condition)
-
- if isinstance(filters, int):
- # docname is a number, convert to string
- filters = str(filters)
-
- if isinstance(filters, str):
- filters = { "name": filters }
-
- for f in filters:
- _build_condition(f)
-
- return " and ".join(conditions), values
-
def get(self, doctype, filters=None, as_dict=True, cache=False):
"""Returns `get_value` with fieldname='*'"""
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
@@ -445,9 +401,8 @@ class Database(object):
(doctype, filters, fieldname) in self.value_cache:
return self.value_cache[(doctype, filters, fieldname)]
- if not order_by: order_by = 'modified desc'
-
if isinstance(filters, list):
+ order_by = order_by or "modified_desc"
out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug)
else:
@@ -460,6 +415,7 @@ class Database(object):
if (filters is not None) and (filters!=doctype or doctype=="DocType"):
try:
+ order_by = order_by or "modified"
out = self._get_values_from_table(fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update)
except Exception as e:
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
@@ -588,32 +544,23 @@ class Database(object):
return self.get_single_value(*args, **kwargs)
def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None, update=None, for_update=False):
- fl = []
+ field_objects = []
+
+ for field in fields:
+ if "(" in field or " as " in field:
+ field_objects.append(PseudoColumn(field))
+ else:
+ field_objects.append(field)
+
+ criterion = self.query.build_conditions(table=doctype, filters=filters, orderby=order_by, for_update=for_update)
+
if isinstance(fields, (list, tuple)):
- for f in fields:
- if "(" in f or " as " in f: # function
- fl.append(f)
- else:
- fl.append("`" + f + "`")
- fl = ", ".join(fl)
+ query = criterion.select(*field_objects)
else:
- fl = fields
if fields=="*":
+ query = criterion.select(fields)
as_dict = True
-
- conditions, values = self.build_conditions(filters)
-
- order_by = ("order by " + order_by) if order_by else ""
-
- r = self.sql("select {fields} from `tab{doctype}` {where} {conditions} {order_by} {for_update}"
- .format(
- for_update = 'for update' if for_update else '',
- fields = fl,
- doctype = doctype,
- where = "where" if conditions else "",
- conditions = conditions,
- order_by = order_by),
- values, as_dict=as_dict, debug=debug, update=update)
+ r = self.sql(query, as_dict=as_dict, debug=debug, update=update)
return r
@@ -840,50 +787,34 @@ class Database(object):
except Exception:
return None
+ def min(self, dt, fieldname, filters=None, **kwargs):
+ return self.query.build_conditions(dt, filters=filters).select(Min(Column(fieldname))).run(**kwargs)[0][0] or 0
+
+ def max(self, dt, fieldname, filters=None, **kwargs):
+ return self.query.build_conditions(dt, filters=filters).select(Max(Column(fieldname))).run(**kwargs)[0][0] or 0
+
+ def avg(self, dt, fieldname, filters=None, **kwargs):
+ return self.query.build_conditions(dt, filters=filters).select(Avg(Column(fieldname))).run(**kwargs)[0][0] or 0
+
+ def sum(self, dt, fieldname, filters=None, **kwargs):
+ return self.query.build_conditions(dt, filters=filters).select(Sum(Column(fieldname))).run(**kwargs)[0][0] or 0
+
def count(self, dt, filters=None, debug=False, cache=False):
"""Returns `COUNT(*)` for given DocType and filters."""
if cache and not filters:
cache_count = frappe.cache().get_value('doctype:count:{}'.format(dt))
if cache_count is not None:
return cache_count
+ query = self.query.build_conditions(table=dt, filters=filters).select(Count("*"))
if filters:
- conditions, filters = self.build_conditions(filters)
- count = self.sql("""select count(*)
- from `tab%s` where %s""" % (dt, conditions), filters, debug=debug)[0][0]
+ count = self.sql(query, debug=debug)[0][0]
return count
else:
- count = self.sql("""select count(*)
- from `tab%s`""" % (dt,))[0][0]
-
+ count = self.sql(query, debug=debug)[0][0]
if cache:
frappe.cache().set_value('doctype:count:{}'.format(dt), count, expires_in_sec = 86400)
-
return count
- def sum(self, dt, fieldname, filters=None):
- return self._get_aggregation('SUM', dt, fieldname, filters)
-
- def avg(self, dt, fieldname, filters=None):
- return self._get_aggregation('AVG', dt, fieldname, filters)
-
- def min(self, dt, fieldname, filters=None):
- return self._get_aggregation('MIN', dt, fieldname, filters)
-
- def max(self, dt, fieldname, filters=None):
- return self._get_aggregation('MAX', dt, fieldname, filters)
-
- def _get_aggregation(self, function, dt, fieldname, filters=None):
- if not self.has_column(dt, fieldname):
- frappe.throw(frappe._('Invalid column'), self.InvalidColumnName)
-
- query = f'SELECT {function}({fieldname}) AS value FROM `tab{dt}`'
- values = ()
- if filters:
- conditions, values = self.build_conditions(filters)
- query = f"{query} WHERE {conditions}"
-
- return self.sql(query, values)[0][0] or 0
-
@staticmethod
def format_date(date):
return getdate(date).strftime("%Y-%m-%d")
@@ -1005,16 +936,9 @@ class Database(object):
"""
values = ()
filters = filters or kwargs.get("conditions")
- table = get_table_name(doctype)
- query = f"DELETE FROM `{table}`"
-
+ query = self.query.build_conditions(table=doctype, filters=filters).delete()
if "debug" not in kwargs:
kwargs["debug"] = debug
-
- if filters:
- conditions, values = self.build_conditions(filters)
- query = f"{query} WHERE {conditions}"
-
return self.sql(query, values, **kwargs)
def truncate(self, doctype: str):
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 54cf349d19..2f6d640743 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -22,11 +22,11 @@ class MariaDBDatabase(Database):
def setup_type_map(self):
self.db_type = 'mariadb'
self.type_map = {
- 'Currency': ('decimal', '18,6'),
+ 'Currency': ('decimal', '21,9'),
'Int': ('int', '11'),
'Long Int': ('bigint', '20'),
- 'Float': ('decimal', '18,6'),
- 'Percent': ('decimal', '18,6'),
+ 'Float': ('decimal', '21,9'),
+ 'Percent': ('decimal', '21,9'),
'Check': ('int', '1'),
'Small Text': ('text', ''),
'Long Text': ('longtext', ''),
@@ -51,7 +51,7 @@ class MariaDBDatabase(Database):
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('longtext', ''),
'Geolocation': ('longtext', ''),
- 'Duration': ('decimal', '18,6'),
+ 'Duration': ('decimal', '21,9'),
'Icon': ('varchar', self.VARCHAR_LEN)
}
@@ -135,8 +135,8 @@ class MariaDBDatabase(Database):
table_name = get_table_name(doctype)
return self.sql(f"DESC `{table_name}`")
- def change_column_type(self, table: str, column: str, type: str) -> Union[List, Tuple]:
- table_name = get_table_name(table)
+ def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} NOT NULL")
# exception types
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index 670fb71aa2..73b98f0ff3 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -226,6 +226,7 @@ CREATE TABLE `tabDocType` (
`email_append_to` int(1) NOT NULL DEFAULT 0,
`subject_field` varchar(255) DEFAULT NULL,
`sender_field` varchar(255) DEFAULT NULL,
+ `migration_hash` varchar(255) DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `parent` (`parent`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index c5091cfe8f..5768a2f23d 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -4,18 +4,22 @@ from frappe.database.schema import DBTable
class MariaDBTable(DBTable):
def create(self):
- add_text = ''
+ additional_definitions = ""
+ engine = self.meta.get("engine") or "InnoDB"
+ varchar_len = frappe.db.VARCHAR_LEN
# columns
column_defs = self.get_column_definitions()
- if column_defs: add_text += ',\n'.join(column_defs) + ',\n'
+ if column_defs:
+ additional_definitions += ',\n'.join(column_defs) + ',\n'
# index
index_defs = self.get_index_definitions()
- if index_defs: add_text += ',\n'.join(index_defs) + ',\n'
+ if index_defs:
+ additional_definitions += ',\n'.join(index_defs) + ',\n'
# create table
- frappe.db.sql("""create table `%s` (
+ query = f"""create table `{self.table_name}` (
name varchar({varchar_len}) not null primary key,
creation datetime(6),
modified datetime(6),
@@ -26,13 +30,15 @@ class MariaDBTable(DBTable):
parentfield varchar({varchar_len}),
parenttype varchar({varchar_len}),
idx int(8) not null default '0',
- %sindex parent(parent),
+ {additional_definitions}
+ index parent(parent),
index modified(modified))
ENGINE={engine}
ROW_FORMAT=DYNAMIC
CHARACTER SET=utf8mb4
- COLLATE=utf8mb4_unicode_ci""".format(varchar_len=frappe.db.VARCHAR_LEN,
- engine=self.meta.get("engine") or 'InnoDB') % (self.table_name, add_text))
+ COLLATE=utf8mb4_unicode_ci"""
+
+ frappe.db.sql(query)
def alter(self):
for col in self.columns.values():
diff --git a/frappe/database/mariadb/setup_db.py b/frappe/database/mariadb/setup_db.py
index 6be08c66bb..8088cc2331 100644
--- a/frappe/database/mariadb/setup_db.py
+++ b/frappe/database/mariadb/setup_db.py
@@ -34,25 +34,23 @@ def setup_database(force, source_sql, verbose, no_mariadb_socket=False):
db_name = frappe.local.conf.db_name
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
dbman = DbManager(root_conn)
+ dbman_kwargs = {}
+ if no_mariadb_socket:
+ dbman_kwargs["host"] = "%"
+
if force or (db_name not in dbman.get_database_list()):
- dbman.delete_user(db_name)
- if no_mariadb_socket:
- dbman.delete_user(db_name, host="%")
+ dbman.delete_user(db_name, **dbman_kwargs)
dbman.drop_database(db_name)
else:
raise Exception("Database %s already exists" % (db_name,))
- dbman.create_user(db_name, frappe.conf.db_password)
- if no_mariadb_socket:
- dbman.create_user(db_name, frappe.conf.db_password, host="%")
+ dbman.create_user(db_name, frappe.conf.db_password, **dbman_kwargs)
if verbose: print("Created user %s" % db_name)
dbman.create_database(db_name)
if verbose: print("Created database %s" % db_name)
- dbman.grant_all_privileges(db_name, db_name)
- if no_mariadb_socket:
- dbman.grant_all_privileges(db_name, db_name, host="%")
+ dbman.grant_all_privileges(db_name, db_name, **dbman_kwargs)
dbman.flush_privileges()
if verbose: print("Granted privileges to user %s and database %s" % (db_name, db_name))
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index 890f1c8d0e..bfa5515111 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -32,11 +32,11 @@ class PostgresDatabase(Database):
def setup_type_map(self):
self.db_type = 'postgres'
self.type_map = {
- 'Currency': ('decimal', '18,6'),
+ 'Currency': ('decimal', '21,9'),
'Int': ('bigint', None),
'Long Int': ('bigint', None),
- 'Float': ('decimal', '18,6'),
- 'Percent': ('decimal', '18,6'),
+ 'Float': ('decimal', '21,9'),
+ 'Percent': ('decimal', '21,9'),
'Check': ('smallint', None),
'Small Text': ('text', ''),
'Long Text': ('text', ''),
@@ -61,7 +61,7 @@ class PostgresDatabase(Database):
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('text', ''),
'Geolocation': ('text', ''),
- 'Duration': ('decimal', '18,6'),
+ 'Duration': ('decimal', '21,9'),
'Icon': ('varchar', self.VARCHAR_LEN)
}
@@ -183,8 +183,8 @@ class PostgresDatabase(Database):
table_name = get_table_name(doctype)
return self.sql(f"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '{table_name}'")
- def change_column_type(self, table: str, column: str, type: str) -> Union[List, Tuple]:
- table_name = get_table_name(table)
+ def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
return self.sql(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}')
def create_auth_table(self):
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index 868f98fc98..e8e047f194 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -231,6 +231,7 @@ CREATE TABLE "tabDocType" (
"email_append_to" smallint NOT NULL DEFAULT 0,
"subject_field" varchar(255) DEFAULT NULL,
"sender_field" varchar(255) DEFAULT NULL,
+ "migration_hash" varchar(255) DEFAULT NULL,
PRIMARY KEY ("name")
) ;
diff --git a/frappe/database/query.py b/frappe/database/query.py
new file mode 100644
index 0000000000..7d7de85646
--- /dev/null
+++ b/frappe/database/query.py
@@ -0,0 +1,267 @@
+import operator
+from typing import Any, Dict, List, Tuple, Union
+
+import frappe
+from frappe.query_builder import Criterion, Order, Field
+
+
+def like(key: str, value: str) -> frappe.qb:
+ """Wrapper method for `LIKE`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `LIKE`
+ """
+ return Field(key).like(value)
+
+
+def func_in(key: str, value: Union[List, Tuple]) -> frappe.qb:
+ """Wrapper method for `IN`
+
+ Args:
+ key (str): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `IN`
+ """
+ return Field(key).isin(value)
+
+
+def not_like(key: str, value: str) -> frappe.qb:
+ """Wrapper method for `NOT LIKE`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `NOT LIKE`
+ """
+ return Field(key).not_like(value)
+
+
+def func_not_in(key: str, value: Union[List, Tuple]):
+ """Wrapper method for `NOT IN`
+
+ Args:
+ key (str): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `NOT IN`
+ """
+ return Field(key).notin(value)
+
+
+def func_regex(key: str, value: str) -> frappe.qb:
+ """Wrapper method for `REGEX`
+
+ Args:
+ key (str): field
+ value (str): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `REGEX`
+ """
+ return Field(key).regex(value)
+
+
+def func_between(key: str, value: Union[List, Tuple]) -> frappe.qb:
+ """Wrapper method for `BETWEEN`
+
+ Args:
+ key (str): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: `frappe.qb object with `BETWEEN`
+ """
+ return Field(key)[slice(*value)]
+
+def make_function(key: Any, value: Union[int, str]):
+ """returns fucntion query
+
+ Args:
+ key (Any): field
+ value (Union[int, str]): criterion
+
+ Returns:
+ frappe.qb: frappe.qb object
+ """
+ return OPERATOR_MAP[value[0]](key, value[1])
+
+
+def change_orderby(order: str):
+ """Convert orderby to standart Order object
+
+ Args:
+ order (str): Field, order
+
+ Returns:
+ tuple: field, order
+ """
+ order = order.split()
+ if order[1].lower() == "asc":
+ orderby, order = order[0], Order.asc
+ return orderby, order
+ orderby, order = order[0], Order.desc
+ return orderby, order
+
+
+OPERATOR_MAP = {
+ "+": operator.add,
+ "=": operator.eq,
+ "-": operator.sub,
+ "!=": operator.ne,
+ "<": operator.lt,
+ ">": operator.gt,
+ "<=": operator.le,
+ ">=": operator.ge,
+ "in": func_in,
+ "not in": func_not_in,
+ "like": like,
+ "not like": not_like,
+ "regex": func_regex,
+ "between": func_between
+ }
+
+
+class Query:
+ def get_condition(self, table: str, **kwargs) -> frappe.qb:
+ """Get initial table object
+
+ Args:
+ table (str): DocType
+
+ Returns:
+ frappe.qb: DocType with initial condition
+ """
+ if kwargs.get("update"):
+ return frappe.qb.update(table)
+ if kwargs.get("into"):
+ return frappe.qb.into(table)
+ return frappe.qb.from_(table)
+
+ def criterion_query(self, table: str, criterion: Criterion, **kwargs) -> frappe.qb:
+ """Generate filters from Criterion objects
+
+ Args:
+ table (str): DocType
+ criterion (Criterion): Filters
+
+ Returns:
+ frappe.qb: condition object
+ """
+ condition = self.get_condition(table, **kwargs)
+ return condition.where(criterion)
+
+ def add_conditions(self, conditions: frappe.qb, **kwargs):
+ """Adding additional conditions
+
+ Args:
+ conditions (frappe.qb): built conditions
+
+ Returns:
+ conditions (frappe.qb): frappe.qb object
+ """
+ if kwargs.get("orderby"):
+ orderby = kwargs.get("orderby")
+ order = kwargs.get("order") if kwargs.get("order") else Order.desc
+ if isinstance(orderby, str) and len(orderby.split()) > 1:
+ orderby, order = change_orderby(orderby)
+ conditions = conditions.orderby(orderby, order=order)
+
+ if kwargs.get("limit"):
+ conditions = conditions.limit(kwargs.get("limit"))
+
+ if kwargs.get("distinct"):
+ conditions = conditions.distinct()
+
+ if kwargs.get("for_update"):
+ conditions = conditions.for_update()
+
+ return conditions
+
+ def misc_query(self, table: str, filters: Union[List, Tuple] = None, **kwargs):
+ """Build conditions using the given Lists or Tuple filters
+
+ Args:
+ table (str): DocType
+ filters (Union[List, Tuple], optional): Filters. Defaults to None.
+ """
+ conditions = self.get_condition(table, **kwargs)
+ if not filters:
+ return conditions
+ if isinstance(filters, list):
+ for f in filters:
+ if not isinstance(f, (list, tuple)):
+ _operator = OPERATOR_MAP[filters[1]]
+ if not isinstance(filters[0], str):
+ conditions = make_function(filters[0], filters[2])
+ break
+ conditions = conditions.where(_operator(Field(filters[0]), filters[2]))
+ break
+ else:
+ _operator = OPERATOR_MAP[f[1]]
+ conditions = conditions.where(_operator(Field(f[0]), f[2]))
+
+ conditions = self.add_conditions(conditions, **kwargs)
+ return conditions
+
+ def dict_query(self, table: str, filters: Dict[str, Union[str, int]] = None, **kwargs) -> frappe.qb:
+ """Build conditions using the given dictionary filters
+
+ Args:
+ table (str): DocType
+ filters (Dict[str, Union[str, int]], optional): Filters. Defaults to None.
+
+ Returns:
+ frappe.qb: conditions object
+ """
+ conditions = self.get_condition(table, **kwargs)
+ if not filters:
+ return conditions
+
+ for key in filters:
+ value = filters.get(key)
+ _operator = OPERATOR_MAP["="]
+
+ if not isinstance(key, str):
+ conditions = conditions.where(make_function(key, value))
+ continue
+ if isinstance(value, (list, tuple)):
+ if isinstance(value[1], (list, tuple)) or value[0] in list(OPERATOR_MAP.keys())[-4:]:
+ _operator = OPERATOR_MAP[value[0]]
+ conditions = conditions.where(_operator(key, value[1]))
+ else:
+ _operator = OPERATOR_MAP[value[0]]
+ conditions = conditions.where(_operator(Field(key), value[1]))
+ else:
+ conditions = conditions.where(_operator(Field(key), value))
+ conditions = self.add_conditions(conditions, **kwargs)
+ return conditions
+
+ def build_conditions(self, table: str, filters: Union[Dict[str, Union[str, int]], str, int] = None, **kwargs) -> frappe.qb:
+ """Build conditions for sql query
+
+ Args:
+ filters (Union[Dict[str, Union[str, int]], str, int]): conditions in Dict
+ table (str): DocType
+
+ Returns:
+ frappe.qb: frappe.qb conditions object
+ """
+ if isinstance(filters, Criterion):
+ return self.criterion_query(table, filters, **kwargs)
+
+ if isinstance(filters, int) or isinstance(filters, str):
+ filters = {"name": str(filters)}
+
+ if isinstance(filters, (list, tuple)):
+ return self.misc_query(table, filters, **kwargs)
+
+ return self.dict_query(filters=filters, table=table, **kwargs)
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index 31f11dbd5e..ce9fcb4147 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -303,6 +303,8 @@ def get_definition(fieldtype, precision=None, length=None):
size = d[1] if d[1] else None
if size:
+ # This check needs to exist for backward compatibility.
+ # Till V13, default size used for float, currency and percent are (18, 6).
if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6:
size = '21,9'
diff --git a/frappe/desk/doctype/note/note.json b/frappe/desk/doctype/note/note.json
index 8d476e83fe..69a9518ac4 100644
--- a/frappe/desk/doctype/note/note.json
+++ b/frappe/desk/doctype/note/note.json
@@ -1,322 +1,106 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "beta": 0,
- "creation": "2013-05-24 13:41:00",
- "custom": 0,
- "description": "",
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 0,
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "title",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Title",
- "length": 0,
- "no_copy": 1,
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "description": "",
- "fieldname": "public",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Public",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "public",
- "fieldname": "notify_on_login",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Notify users with a popup when they log in",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "depends_on": "notify_on_login",
- "description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.",
- "fieldname": "notify_on_every_login",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Notify Users On Every Login",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.notify_on_login && doc.public",
- "fieldname": "expire_notification_on",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Expire Notification On",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "description": "Help: To link to another record in the system, use \"#Form/Note/[Note Name]\" as the Link URL. (don't use \"http://\")",
- "fieldname": "content",
- "fieldtype": "Text Editor",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Content",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 1,
- "columns": 0,
- "fieldname": "seen_by_section",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Seen By",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "seen_by",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Seen By Table",
- "length": 0,
- "no_copy": 0,
- "options": "Note Seen By",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-file-text",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-09-21 15:15:44.909636",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "Note",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 0,
- "role": "All",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 1,
- "show_name_in_global_search": 0,
- "sort_order": "ASC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
- }
\ No newline at end of file
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2013-05-24 13:41:00",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "public",
+ "notify_on_login",
+ "notify_on_every_login",
+ "expire_notification_on",
+ "content",
+ "seen_by_section",
+ "seen_by"
+ ],
+ "fields": [
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "label": "Title",
+ "no_copy": 1,
+ "print_hide": 1,
+ "reqd": 1
+ },
+ {
+ "bold": 1,
+ "default": "0",
+ "fieldname": "public",
+ "fieldtype": "Check",
+ "label": "Public",
+ "print_hide": 1
+ },
+ {
+ "bold": 1,
+ "default": "0",
+ "depends_on": "public",
+ "fieldname": "notify_on_login",
+ "fieldtype": "Check",
+ "label": "Notify users with a popup when they log in"
+ },
+ {
+ "bold": 1,
+ "default": "0",
+ "depends_on": "notify_on_login",
+ "description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.",
+ "fieldname": "notify_on_every_login",
+ "fieldtype": "Check",
+ "label": "Notify Users On Every Login"
+ },
+ {
+ "depends_on": "eval:doc.notify_on_login && doc.public",
+ "fieldname": "expire_notification_on",
+ "fieldtype": "Date",
+ "label": "Expire Notification On",
+ "search_index": 1
+ },
+ {
+ "bold": 1,
+ "description": "Help: To link to another record in the system, use \"/app/note/[Note Name]\" as the Link URL. (don't use \"http://\")",
+ "fieldname": "content",
+ "fieldtype": "Text Editor",
+ "in_global_search": 1,
+ "label": "Content"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "seen_by_section",
+ "fieldtype": "Section Break",
+ "label": "Seen By"
+ },
+ {
+ "fieldname": "seen_by",
+ "fieldtype": "Table",
+ "label": "Seen By Table",
+ "options": "Note Seen By"
+ }
+ ],
+ "icon": "fa fa-file-text",
+ "idx": 1,
+ "links": [],
+ "modified": "2021-09-18 10:57:51.352643",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Note",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "All",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js
index 2d097f01ad..0fe3932671 100644
--- a/frappe/desk/doctype/system_console/system_console.js
+++ b/frappe/desk/doctype/system_console/system_console.js
@@ -10,18 +10,56 @@ frappe.ui.form.on('System Console', {
description: __('Execute Console script'),
ignore_inputs: true,
});
+ frm.set_value("type", "Python");
},
refresh: function(frm) {
frm.disable_save();
frm.page.set_primary_action(__("Execute"), $btn => {
- $btn.text(__('Executing...'));
- return frm.execute_action("Execute").then(() => {
- $btn.text(__('Execute'));
- });
+ $btn.text(__("Executing..."));
+ return frm
+ .execute_action("Execute")
+ .then(() => frm.trigger("render_sql_output"))
+ .finally(() => $btn.text(__("Execute")));
});
},
+ type: function(frm) {
+ if (frm.doc.type == "Python") {
+ frm.set_value("output", "");
+ if (frm.sql_output) {
+ frm.sql_output.destroy();
+ frm.get_field("sql_output").html("");
+ }
+ }
+ },
+
+ render_sql_output: function(frm) {
+ if (frm.doc.type !== "SQL") return;
+ if (frm.sql_output) {
+ frm.sql_output.destroy();
+ frm.get_field("sql_output").html("");
+ }
+
+ if (frm.doc.output.startsWith("Traceback")) {
+ return;
+ }
+
+ let result = JSON.parse(frm.doc.output);
+ frm.set_value("output", `${result.length} ${result.length == 1 ? 'row' : 'rows'}`);
+
+ if (result.length) {
+ let columns = Object.keys(result[0]);
+ frm.sql_output = new DataTable(
+ frm.get_field("sql_output").$wrapper.get(0),
+ {
+ columns,
+ data: result
+ }
+ );
+ }
+ },
+
show_processlist: function(frm) {
if (frm.doc.show_processlist) {
// keep refreshing every 5 seconds
@@ -32,6 +70,7 @@ frappe.ui.form.on('System Console', {
// end it
clearInterval(frm.processlist_interval);
+ frm.get_field("processlist").html('');
}
}
},
diff --git a/frappe/desk/doctype/system_console/system_console.json b/frappe/desk/doctype/system_console/system_console.json
index 753e672cdc..657e9df89d 100644
--- a/frappe/desk/doctype/system_console/system_console.json
+++ b/frappe/desk/doctype/system_console/system_console.json
@@ -18,9 +18,11 @@
"engine": "InnoDB",
"field_order": [
"execute_section",
+ "type",
"console",
"commit",
"output",
+ "sql_output",
"database_processes_section",
"show_processlist",
"processlist"
@@ -65,13 +67,26 @@
"fieldname": "processlist",
"fieldtype": "HTML",
"label": "processlist"
+ },
+ {
+ "default": "Python",
+ "fieldname": "type",
+ "fieldtype": "Select",
+ "label": "Type",
+ "options": "Python\nSQL"
+ },
+ {
+ "depends_on": "eval:doc.type == 'SQL'",
+ "fieldname": "sql_output",
+ "fieldtype": "HTML",
+ "label": "SQL Output"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-09-09 13:10:14.237113",
+ "modified": "2021-09-15 17:17:44.844767",
"modified_by": "Administrator",
"module": "Desk",
"name": "System Console",
diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py
index 8382dc8638..107ab2f932 100644
--- a/frappe/desk/doctype/system_console/system_console.py
+++ b/frappe/desk/doctype/system_console/system_console.py
@@ -5,7 +5,7 @@
import json
import frappe
-from frappe.utils.safe_exec import safe_exec
+from frappe.utils.safe_exec import safe_exec, read_sql
from frappe.model.document import Document
class SystemConsole(Document):
@@ -13,8 +13,11 @@ class SystemConsole(Document):
frappe.only_for('System Manager')
try:
frappe.debug_log = []
- safe_exec(self.console)
- self.output = '\n'.join(frappe.debug_log)
+ if self.type == 'Python':
+ safe_exec(self.console)
+ self.output = '\n'.join(frappe.debug_log)
+ elif self.type == 'SQL':
+ self.output = frappe.as_json(read_sql(self.console, as_dict=1))
except: # noqa: E722
self.output = frappe.get_traceback()
diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py
index 44bb780681..aff1bd6973 100644
--- a/frappe/desk/doctype/tag/tag.py
+++ b/frappe/desk/doctype/tag/tag.py
@@ -128,46 +128,35 @@ def delete_tags_for_document(doc):
})
def update_tags(doc, tags):
- """
- Adds tags for documents
- :param doc: Document to be added to global tags
- """
+ """Adds tags for documents
+ :param doc: Document to be added to global tags
+ """
new_tags = {tag.strip() for tag in tags.split(",") if tag}
-
- for tag in new_tags:
- if not frappe.db.exists("Tag Link", {"parenttype": doc.doctype, "parent": doc.name, "tag": tag}):
- frappe.get_doc({
- "doctype": "Tag Link",
- "document_type": doc.doctype,
- "document_name": doc.name,
- "parenttype": doc.doctype,
- "parent": doc.name,
- "title": doc.get_title() or '',
- "tag": tag
- }).insert(ignore_permissions=True)
-
existing_tags = [tag.tag for tag in frappe.get_list("Tag Link", filters={
"document_type": doc.doctype,
"document_name": doc.name
}, fields=["tag"])]
- deleted_tags = get_deleted_tags(new_tags, existing_tags)
+ added_tags = set(new_tags) - set(existing_tags)
+ for tag in added_tags:
+ frappe.get_doc({
+ "doctype": "Tag Link",
+ "document_type": doc.doctype,
+ "document_name": doc.name,
+ "parenttype": doc.doctype,
+ "parent": doc.name,
+ "title": doc.get_title() or '',
+ "tag": tag
+ }).insert(ignore_permissions=True)
- if deleted_tags:
- for tag in deleted_tags:
- delete_tag_for_document(doc.doctype, doc.name, tag)
-
-def get_deleted_tags(new_tags, existing_tags):
-
- return list(set(existing_tags) - set(new_tags))
-
-def delete_tag_for_document(dt, dn, tag):
- frappe.db.delete("Tag Link", {
- "document_type": dt,
- "document_name": dn,
- "tag": tag
- })
+ deleted_tags = list(set(existing_tags) - set(new_tags))
+ for tag in deleted_tags:
+ frappe.db.delete("Tag Link", {
+ "document_type": doc.doctype,
+ "document_name": doc.name,
+ "tag": tag
+ })
@frappe.whitelist()
def get_documents_for_tag(tag):
diff --git a/frappe/desk/doctype/tag_link/tag_link.json b/frappe/desk/doctype/tag_link/tag_link.json
index 00a7349c5c..9142279fa3 100644
--- a/frappe/desk/doctype/tag_link/tag_link.json
+++ b/frappe/desk/doctype/tag_link/tag_link.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-09-24 13:25:36.435685",
"doctype": "DocType",
"editable_grid": 1,
@@ -44,7 +45,8 @@
"read_only": 1
}
],
- "modified": "2019-10-03 16:42:35.932409",
+ "links": [],
+ "modified": "2021-09-20 16:53:37.217998",
"modified_by": "Administrator",
"module": "Desk",
"name": "Tag Link",
@@ -61,6 +63,17 @@
"role": "System Manager",
"share": 1,
"write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1,
+ "write": 1
}
],
"read_only": 1,
diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json
index 1e111b8d12..756a40da4b 100644
--- a/frappe/desk/doctype/workspace/workspace.json
+++ b/frappe/desk/doctype/workspace/workspace.json
@@ -165,8 +165,6 @@
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
- "in_list_view": 1,
- "in_standard_filter": 1,
"label": "Is Standard",
"search_index": 1
},
@@ -181,7 +179,6 @@
"depends_on": "eval:doc.extends_another_page == 1 || doc.for_user",
"fieldname": "extends",
"fieldtype": "Link",
- "in_standard_filter": 1,
"label": "Extends",
"options": "Workspace",
"search_index": 1
@@ -228,6 +225,8 @@
"default": "0",
"fieldname": "public",
"fieldtype": "Check",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
"label": "Public"
},
{
@@ -265,11 +264,13 @@
"label": "Roles"
}
],
+ "in_create": 1,
"links": [],
- "modified": "2021-08-30 18:47:18.227154",
+ "modified": "2021-09-16 12:01:06.450621",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index 25dd9b26d2..a0a22a43fc 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -208,17 +208,17 @@ def save_page(title, icon, parent, public, sb_public_items, sb_private_items, de
if loads(deleted_pages):
return delete_pages(loads(deleted_pages))
- return {"name": title, "public": public}
+ return {"name": title, "public": public, "label": doc.label}
def delete_pages(deleted_pages):
for page in deleted_pages:
if page.get("public") and "Workspace Manager" not in frappe.get_roles():
- return {"name": page.get("title"), "public": 1}
+ return {"name": page.get("title"), "public": 1, "label": page.get("label")}
if frappe.db.exists("Workspace", page.get("name")):
frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True)
- return {"name": "Home", "public": 1}
+ return {"name": "Home", "public": 1, "label": "Home"}
def sort_pages(sb_public_items, sb_private_items):
wspace_public_pages = get_page_list(['name', 'title'], {'public': 1})
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index d276a9707f..89e6598859 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -13,7 +13,7 @@ from frappe.desk.form.document_follow import is_document_followed
from frappe import _
from urllib.parse import quote
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
def getdoc(doctype, name, user=None):
"""
Loads a doclist for a given document. This method is called directly from the client.
@@ -52,7 +52,7 @@ def getdoc(doctype, name, user=None):
frappe.response.docs.append(doc)
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
def getdoctype(doctype, with_parent=False, cached_timestamp=None):
"""load doctype"""
diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py
index a4dcee4ab3..1c954edff0 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -66,7 +66,8 @@ def add_comment(reference_doctype, reference_name, content, comment_email, comme
comment_type='Comment',
comment_by=comment_by
))
- doc.content = extract_images_from_html(doc, content)
+ reference_doc = frappe.get_doc(reference_doctype, reference_name)
+ doc.content = extract_images_from_html(reference_doc, content, is_private=True)
doc.insert(ignore_permissions=True)
follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user)
diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py
index f079205cb0..e733adf868 100644
--- a/frappe/desk/listview.py
+++ b/frappe/desk/listview.py
@@ -2,7 +2,7 @@
# License: MIT. See LICENSE
import frappe
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
def get_list_settings(doctype):
try:
return frappe.get_cached_doc("List View Settings", doctype)
diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js
index b3fccf84f9..076d672db5 100644
--- a/frappe/desk/page/leaderboard/leaderboard.js
+++ b/frappe/desk/page/leaderboard/leaderboard.js
@@ -141,7 +141,7 @@ class Leaderboard {
}
create_date_range_field() {
- let timespan_field = $(this.parent).find(`.frappe-control[data-original-title=${__('Timespan')}]`);
+ let timespan_field = $(this.parent).find(`.frappe-control[data-original-title="${__('Timespan')}"]`);
this.date_range_field = $(`
`).insertAfter(timespan_field).hide();
let date_field = frappe.ui.form.make_control({
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
index d758ebb590..6c9fa2e937 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -14,7 +14,7 @@ from frappe.utils import cstr, format_duration
from frappe.model.base_document import get_controller
-@frappe.whitelist(allow_guest=True)
+@frappe.whitelist()
@frappe.read_only()
def get():
args = get_form_params()
@@ -121,12 +121,14 @@ def validate_filters(data, filters):
def setup_group_by(data):
'''Add columns for aggregated values e.g. count(name)'''
- if data.group_by:
+ if data.group_by and data.aggregate_function:
if data.aggregate_function.lower() not in ('count', 'sum', 'avg'):
frappe.throw(_('Invalid aggregate function'))
if frappe.db.has_column(data.aggregate_on_doctype, data.aggregate_on_field):
data.fields.append('{aggregate_function}(`tab{aggregate_on_doctype}`.`{aggregate_on_field}`) AS _aggregate_column'.format(**data))
+ if data.aggregate_on_field:
+ data.fields.append(f"`tab{data.aggregate_on_doctype}`.`{data.aggregate_on_field}`")
else:
raise_invalid_field(data.aggregate_on_field)
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py
index 37089d58df..7081a84e7a 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -249,7 +249,7 @@ def make_links(columns, data):
if col.options and row.get(col.fieldname) and row.get(col.options):
row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname])
elif col.fieldtype == "Currency" and row.get(col.fieldname):
- doc = frappe.get_doc(col.parent, doc_name) if doc_name else None
+ doc = frappe.get_doc(col.parent, doc_name) if doc_name and col.parent else None
# Pass the Document to get the currency based on docfield option
row[col.fieldname] = frappe.format_value(row[col.fieldname], col, doc=doc)
return columns, data
diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json
index 8cfd75d839..e20f38c74a 100644
--- a/frappe/email/doctype/email_account/email_account.json
+++ b/frappe/email/doctype/email_account/email_account.json
@@ -226,7 +226,7 @@
},
{
"default": "UNSEEN",
- "depends_on": "eval: doc.enable_incoming",
+ "depends_on": "eval: doc.enable_incoming && doc.use_imap",
"fieldname": "email_sync_option",
"fieldtype": "Select",
"hide_days": 1,
@@ -236,7 +236,7 @@
},
{
"default": "250",
- "depends_on": "eval: doc.enable_incoming",
+ "depends_on": "eval: doc.enable_incoming && doc.use_imap",
"description": "Total number of emails to sync in initial sync process ",
"fieldname": "initial_sync_count",
"fieldtype": "Select",
@@ -567,7 +567,7 @@
"icon": "fa fa-inbox",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-08-31 15:23:25.714366",
+ "modified": "2021-09-21 16:44:25.728637",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",
@@ -589,4 +589,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index f523d835e9..6b4ee92043 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -146,6 +146,7 @@ def get_context(context):
if doc.meta.get_field(fieldname).fieldtype in frappe.model.numeric_fieldtypes:
value = frappe.utils.cint(value)
+ doc.reload()
doc.set(fieldname, value)
doc.flags.updater_reference = {
'doctype': self.doctype,
diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py
index 8e814e0245..f05d35be3e 100644
--- a/frappe/email/doctype/notification/test_notification.py
+++ b/frappe/email/doctype/notification/test_notification.py
@@ -20,6 +20,8 @@ class TestNotification(unittest.TestCase):
notification.event = 'Value Change'
notification.value_changed = 'status'
notification.send_to_all_assignees = 1
+ notification.set_property_after_alert = 'description'
+ notification.property_value = 'Changed by Notification'
notification.save()
if not frappe.db.exists('Notification', {'name': 'Contact Status Update'}, 'name'):
@@ -237,6 +239,9 @@ class TestNotification(unittest.TestCase):
self.assertTrue(email_queue)
+ # check if description is changed after alert since set_property_after_alert is set
+ self.assertEquals(todo.description, 'Changed by Notification')
+
recipients = [d.recipient for d in email_queue.recipients]
self.assertTrue('test2@example.com' in recipients)
self.assertTrue('test1@example.com' in recipients)
@@ -269,4 +274,7 @@ class TestNotification(unittest.TestCase):
self.assertTrue('test2@example.com' in recipients)
self.assertTrue('test1@example.com' in recipients)
-
+ @classmethod
+ def tearDownClass(cls):
+ frappe.delete_doc_if_exists("Notification", "ToDo Status Update")
+ frappe.delete_doc_if_exists("Notification", "Contact Status Update")
\ No newline at end of file
diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py
index 0be15e461b..05771a89d3 100644
--- a/frappe/event_streaming/doctype/event_producer/event_producer.py
+++ b/frappe/event_streaming/doctype/event_producer/event_producer.py
@@ -408,8 +408,9 @@ def sync_dependencies(document, producer_site):
child_table = doc.get(df.fieldname)
for entry in child_table:
child_doc = producer_site.get_doc(entry.doctype, entry.name)
- child_doc = frappe._dict(child_doc)
- set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site)
+ if child_doc:
+ child_doc = frappe._dict(child_doc)
+ set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site)
def sync_link_dependencies(doc, link_fields, producer_site):
set_dependencies(doc, link_fields, producer_site)
diff --git a/frappe/handler.py b/frappe/handler.py
index 352a9672bf..ea654517c3 100755
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -223,7 +223,10 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
doc = frappe.get_doc(dt, dn)
else:
- doc = frappe.get_doc(json.loads(docs))
+ if isinstance(docs, str):
+ docs = json.loads(docs)
+
+ doc = frappe.get_doc(docs)
doc._original_modified = doc.modified
doc.check_if_latest()
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 3cfdebc12e..2ae5a59066 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -12,11 +12,11 @@ source_link = "https://github.com/frappe/frappe"
app_license = "MIT"
app_logo_url = '/assets/frappe/images/frappe-framework-logo.svg'
-develop_version = '13.x.x-develop'
+develop_version = '14.x.x-develop'
-app_email = "info@frappe.io"
+app_email = "developers@frappe.io"
-docs_app = "frappe_io"
+docs_app = "frappe_docs"
translator_url = "https://translate.erpnext.com"
diff --git a/frappe/installer.py b/frappe/installer.py
index f0bf0cb51c..d1a13fdaab 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -4,6 +4,8 @@
import json
import os
import sys
+from collections import OrderedDict
+from typing import List, Dict
import frappe
from frappe.defaults import _clear_cache
@@ -29,6 +31,10 @@ def _new_site(
):
"""Install a new Frappe site"""
+ from frappe.commands.scheduler import _is_scheduler_enabled
+ from frappe.utils import get_site_path, scheduler, touch_file
+
+
if not force and os.path.exists(site):
print("Site {0} already exists".format(site))
sys.exit(1)
@@ -37,14 +43,11 @@ def _new_site(
print("--no-mariadb-socket requires db_type to be set to mariadb.")
sys.exit(1)
- if not db_name:
- import hashlib
- db_name = "_" + hashlib.sha1(site.encode()).hexdigest()[:16]
-
frappe.init(site=site)
- from frappe.commands.scheduler import _is_scheduler_enabled
- from frappe.utils import get_site_path, scheduler, touch_file
+ if not db_name:
+ import hashlib
+ db_name = "_" + hashlib.sha1(os.path.realpath(frappe.get_site_path()).encode()).hexdigest()[:16]
try:
# enable scheduler post install?
@@ -157,7 +160,7 @@ def install_app(name, verbose=False, set_as_patched=True):
if name != "frappe":
add_module_defs(name)
- sync_for(name, force=True, sync_everything=True, verbose=verbose, reset_permissions=True)
+ sync_for(name, force=True, reset_permissions=True)
add_to_installed_apps(name)
@@ -229,9 +232,29 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
scheduled_backup(ignore_files=True)
frappe.flags.in_uninstall = True
- drop_doctypes = []
modules = frappe.get_all("Module Def", filters={"app_name": app_name}, pluck="name")
+
+ drop_doctypes = _delete_modules(modules, dry_run=dry_run)
+ _delete_doctypes(drop_doctypes, dry_run=dry_run)
+
+ if not dry_run:
+ remove_from_installed_apps(app_name)
+ frappe.db.commit()
+
+ click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
+ frappe.flags.in_uninstall = False
+
+
+def _delete_modules(modules: List[str], dry_run: bool) -> List[str]:
+ """ Delete modules belonging to the app and all related doctypes.
+
+ Note: All record linked linked to Module Def are also deleted.
+
+ Returns: list of deleted doctypes."""
+ drop_doctypes = []
+
+ doctype_link_field_map = _get_module_linked_doctype_field_map()
for module_name in modules:
print(f"Deleting Module '{module_name}'")
@@ -241,45 +264,67 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
print(f"* removing DocType '{doctype.name}'...")
if not dry_run:
- frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True)
-
- if not doctype.issingle:
+ if doctype.issingle:
+ frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True)
+ else:
drop_doctypes.append(doctype.name)
- linked_doctypes = frappe.get_all(
- "DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=["parent"]
- )
- ordered_doctypes = ["Workspace", "Report", "Page", "Web Form"]
- all_doctypes_with_linked_modules = ordered_doctypes + [
- doctype.parent
- for doctype in linked_doctypes
- if doctype.parent not in ordered_doctypes
- ]
- doctypes_with_linked_modules = [
- x for x in all_doctypes_with_linked_modules if frappe.db.exists("DocType", x)
- ]
- for doctype in doctypes_with_linked_modules:
- for record in frappe.get_all(doctype, filters={"module": module_name}, pluck="name"):
- print(f"* removing {doctype} '{record}'...")
- if not dry_run:
- frappe.delete_doc(doctype, record, ignore_on_trash=True, force=True)
+ _delete_linked_documents(module_name, doctype_link_field_map, dry_run=dry_run)
print(f"* removing Module Def '{module_name}'...")
if not dry_run:
frappe.delete_doc("Module Def", module_name, ignore_on_trash=True, force=True)
- for doctype in set(drop_doctypes):
+ return drop_doctypes
+
+
+def _delete_linked_documents(
+ module_name: str,
+ doctype_linkfield_map: Dict[str, str],
+ dry_run: bool
+ ) -> None:
+
+ """Deleted all records linked with module def"""
+ for doctype, fieldname in doctype_linkfield_map.items():
+ for record in frappe.get_all(doctype, filters={fieldname: module_name}, pluck="name"):
+ print(f"* removing {doctype} '{record}'...")
+ if not dry_run:
+ frappe.delete_doc(doctype, record, ignore_on_trash=True, force=True)
+
+def _get_module_linked_doctype_field_map() -> Dict[str, str]:
+ """ Get all the doctypes which have module linked with them.
+
+ returns ordered dictionary with doctype->link field mapping."""
+
+ # Hardcoded to change order of deletion
+ ordered_doctypes = [
+ ("Workspace", "module"),
+ ("Report", "module"),
+ ("Page", "module"),
+ ("Web Form", "module")
+ ]
+ doctype_to_field_map = OrderedDict(ordered_doctypes)
+
+ linked_doctypes = frappe.get_all(
+ "DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=["parent", "fieldname"]
+ )
+ existing_linked_doctypes = [d for d in linked_doctypes if frappe.db.exists("DocType", d.parent)]
+
+ for d in existing_linked_doctypes:
+ # DocType deletion is handled separately in the end
+ if d.parent not in doctype_to_field_map and d.parent != "DocType":
+ doctype_to_field_map[d.parent] = d.fieldname
+
+ return doctype_to_field_map
+
+
+def _delete_doctypes(doctypes: List[str], dry_run: bool) -> None:
+ for doctype in set(doctypes):
print(f"* dropping Table for '{doctype}'...")
if not dry_run:
+ frappe.delete_doc("DocType", doctype, ignore_on_trash=True)
frappe.db.sql_ddl(f"drop table `tab{doctype}`")
- if not dry_run:
- remove_from_installed_apps(app_name)
- frappe.db.commit()
-
- click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
- frappe.flags.in_uninstall = False
-
def post_install(rebuild_website=False):
from frappe.website.utils import clear_website_cache
@@ -455,9 +500,20 @@ def convert_archive_content(sql_file_path):
if frappe.conf.db_type == "mariadb":
# ever since mariaDB 10.6, row_format COMPRESSED has been deprecated and removed
# this step is added to ease restoring sites depending on older mariaDB servers
- contents = open(sql_file_path).read()
- with open(sql_file_path, "w") as f:
- f.write(contents.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC"))
+ from frappe.utils import random_string
+ from pathlib import Path
+
+ old_sql_file_path = Path(f"{sql_file_path}_{random_string(10)}")
+ sql_file_path = Path(sql_file_path)
+
+ os.rename(sql_file_path, old_sql_file_path)
+ sql_file_path.touch()
+
+ with open(old_sql_file_path) as r, open(sql_file_path, "a") as w:
+ for line in r:
+ w.write(line.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC"))
+
+ old_sql_file_path.unlink()
def extract_sql_gzip(sql_gz_path):
diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
index 90927e13f8..9ccd1c0210 100644
--- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
+++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
@@ -336,7 +336,6 @@ def dropbox_auth_finish(return_access_token=False):
_("Dropbox access is approved!") + close,
indicator_color='green')
-@frappe.whitelist(allow_guest=True)
def set_dropbox_access_token(access_token):
frappe.db.set_value("Dropbox Settings", None, 'dropbox_access_token', access_token)
frappe.db.commit()
diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
index 9ae16a31f8..9bbab9db9b 100644
--- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
+++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py
@@ -371,6 +371,7 @@ def capture_payment(is_sandbox=False, sanbox_response=None):
doc = frappe.get_doc("Integration Request", doc.name)
doc.status = "Failed"
doc.error = frappe.get_traceback()
+ doc.save()
frappe.log_error(doc.error, '{0} Failed'.format(doc.name))
diff --git a/frappe/migrate.py b/frappe/migrate.py
index 92258502e4..6abc38796f 100644
--- a/frappe/migrate.py
+++ b/frappe/migrate.py
@@ -18,6 +18,7 @@ from frappe.core.doctype.language.language import sync_languages
from frappe.modules.utils import sync_customizations
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.search.website_search import build_index_for_all_routes
+from frappe.database.schema import add_column
def migrate(verbose=True, skip_failing=False, skip_search_index=False):
@@ -26,9 +27,10 @@ def migrate(verbose=True, skip_failing=False, skip_search_index=False):
- run patches
- sync doctypes (schema)
- sync dashboards
+ - sync jobs
- sync fixtures
- - sync desktop icons
- - sync web pages (from /www)
+ - sync customizations
+ - sync languages
- sync web pages (from /www)
- run after migrate hooks
'''
@@ -51,6 +53,7 @@ Otherwise, check the server logs and ensure that all the required services are r
os.remove(touched_tables_file)
try:
+ add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
frappe.flags.touched_tables = set()
frappe.flags.in_migrate = True
@@ -65,7 +68,7 @@ Otherwise, check the server logs and ensure that all the required services are r
frappe.modules.patch_handler.run_all(skip_failing)
# sync
- frappe.model.sync.sync_all(verbose=verbose)
+ frappe.model.sync.sync_all()
frappe.translate.clear_cache()
sync_jobs()
sync_fixtures()
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index ce0a208d50..b460db29a7 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -41,6 +41,7 @@ data_fieldtypes = (
no_value_fields = (
'Section Break',
'Column Break',
+ 'Tab Break',
'HTML',
'Table',
'Table MultiSelect',
@@ -53,6 +54,7 @@ no_value_fields = (
display_fieldtypes = (
'Section Break',
'Column Break',
+ 'Tab Break',
'HTML',
'Button',
'Image',
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 5605ac61ed..1826cca9a3 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -267,7 +267,12 @@ class BaseDocument(object):
if isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
frappe.throw(_('Value for {0} cannot be a list').format(_(df.label)))
- if convert_dates_to_str and isinstance(d[fieldname], (datetime.datetime, datetime.time, datetime.timedelta)):
+ if convert_dates_to_str and isinstance(d[fieldname], (
+ datetime.datetime,
+ datetime.date,
+ datetime.time,
+ datetime.timedelta
+ )):
d[fieldname] = str(d[fieldname])
if d[fieldname] == None and ignore_nulls:
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index fd74a8cfe4..8f0e0aaefc 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -4,6 +4,7 @@
from typing import List
import frappe.defaults
+from frappe.query_builder.utils import Column
import frappe.share
from frappe import _
import frappe.permissions
@@ -491,7 +492,7 @@ class DatabaseQuery(object):
f.value = date_range
fallback = "'0001-01-01 00:00:00'"
- if f.operator in ('>', '<') and (f.fieldname in ('creation', 'modified')):
+ if (f.fieldname in ('creation', 'modified')):
value = cstr(f.value)
fallback = "NULL"
@@ -547,8 +548,12 @@ class DatabaseQuery(object):
value = flt(f.value)
fallback = 0
+ if isinstance(f.value, Column):
+ quote = '"' if frappe.conf.db_type == 'postgres' else "`"
+ value = f"{tname}.{quote}{f.value.name}{quote}"
+
# escape value
- if isinstance(value, str) and not f.operator.lower() == 'between':
+ elif isinstance(value, str) and not f.operator.lower() == 'between':
value = f"{frappe.db.escape(value, percent=False)}"
if (
@@ -592,8 +597,8 @@ class DatabaseQuery(object):
self.conditions.append(self.get_share_condition())
else:
- #if has if_owner permission skip user perm check
- if role_permissions.get("has_if_owner_enabled") and role_permissions.get("if_owner", {}):
+ # skip user perm check if owner constraint is required
+ if requires_owner_constraint(role_permissions):
self.match_conditions.append(
f"`tab{self.doctype}`.`owner` = {frappe.db.escape(self.user, percent=False)}"
)
@@ -890,3 +895,22 @@ def get_date_range(operator, value):
timespan = period_map[operator] + ' ' + timespan_map[value] if operator != 'timespan' else value
return get_timespan_date_range(timespan)
+
+def requires_owner_constraint(role_permissions):
+ """Returns True if "select" or "read" isn't available without being creator."""
+
+ if not role_permissions.get("has_if_owner_enabled"):
+ return
+
+ if_owner_perms = role_permissions.get("if_owner")
+ if not if_owner_perms:
+ return
+
+ # has select or read without if owner, no need for constraint
+ for perm_type in ("select", "read"):
+ if role_permissions.get(perm_type) and perm_type not in if_owner_perms:
+ return
+
+ # not checking if either select or read if present in if_owner_perms
+ # because either of those is required to perform a query
+ return True
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index 207aca089b..cd0d8e0f3a 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -15,6 +15,7 @@ Example:
'''
from datetime import datetime
+import click
import frappe, json, os
from frappe.utils import cstr, cint, cast
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields
@@ -658,27 +659,48 @@ def get_default_df(fieldname):
fieldtype = "Data"
)
-def trim_tables(doctype=None):
+def trim_tables(doctype=None, dry_run=False, quiet=False):
"""
Removes database fields that don't exist in the doctype (json or custom field). This may be needed
as maintenance since removing a field in a DocType doesn't automatically
delete the db field.
"""
- ignore_fields = default_fields + optional_fields
-
- filters={ "issingle": 0 }
+ UPDATED_TABLES = {}
+ filters = {"issingle": 0}
if doctype:
filters["name"] = doctype
- for doctype in frappe.db.get_all("DocType", filters=filters):
- doctype = doctype.name
- columns = frappe.db.get_table_columns(doctype)
- fields = frappe.get_meta(doctype).get_fieldnames_with_value()
- columns_to_remove = [f for f in list(set(columns) - set(fields)) if f not in ignore_fields
- and not f.startswith("_")]
- if columns_to_remove:
- print(doctype, "columns removed:", columns_to_remove)
- columns_to_remove = ", ".join("drop `{0}`".format(c) for c in columns_to_remove)
- query = """alter table `tab{doctype}` {columns}""".format(
- doctype=doctype, columns=columns_to_remove)
- frappe.db.sql_ddl(query)
+ for doctype in frappe.db.get_all("DocType", filters=filters, pluck="name"):
+ try:
+ dropped_columns = trim_table(doctype, dry_run=dry_run)
+ if dropped_columns:
+ UPDATED_TABLES[doctype] = dropped_columns
+ except frappe.db.TableMissingError:
+ if quiet:
+ continue
+ click.secho(f"Ignoring missing table for DocType: {doctype}", fg="yellow", err=True)
+ click.secho(f"Consider removing record in the DocType table for {doctype}", fg="yellow", err=True)
+ except Exception as e:
+ if quiet:
+ continue
+ click.echo(e, err=True)
+
+ return UPDATED_TABLES
+
+
+def trim_table(doctype, dry_run=True):
+ frappe.cache().hdel('table_columns', f"tab{doctype}")
+ ignore_fields = default_fields + optional_fields
+ columns = frappe.db.get_table_columns(doctype)
+ fields = frappe.get_meta(doctype, cached=False).get_fieldnames_with_value()
+ is_internal = lambda f: f not in ignore_fields and not f.startswith("_")
+ columns_to_remove = [
+ f for f in list(set(columns) - set(fields)) if is_internal(f)
+ ]
+ DROPPED_COLUMNS = columns_to_remove[:]
+
+ if columns_to_remove and not dry_run:
+ columns_to_remove = ", ".join(f"DROP `{c}`" for c in columns_to_remove)
+ frappe.db.sql_ddl(f"ALTER TABLE `tab{doctype}` {columns_to_remove}")
+
+ return DROPPED_COLUMNS
diff --git a/frappe/model/sync.py b/frappe/model/sync.py
index 138f9eaad4..42bb16cbc2 100644
--- a/frappe/model/sync.py
+++ b/frappe/model/sync.py
@@ -10,62 +10,67 @@ from frappe.modules.import_file import import_file_by_path
from frappe.modules.patch_handler import block_user
from frappe.utils import update_progress_bar
-def sync_all(force=0, verbose=False, reset_permissions=False):
+
+def sync_all(force=0, reset_permissions=False):
block_user(True)
for app in frappe.get_installed_apps():
- sync_for(app, force, verbose=verbose, reset_permissions=reset_permissions)
+ sync_for(app, force, reset_permissions=reset_permissions)
block_user(False)
frappe.clear_cache()
-def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_permissions=False):
+
+def sync_for(app_name, force=0, reset_permissions=False):
files = []
if app_name == "frappe":
# these need to go first at time of install
- for d in (("core", "docfield"),
- ("core", "docperm"),
- ("core", "doctype_action"),
- ("core", "doctype_link"),
- ("core", "role"),
- ("core", "has_role"),
- ("core", "doctype"),
- ("core", "user"),
- ("custom", "custom_field"),
- ("custom", "property_setter"),
- ("website", "web_form"),
- ("website", "web_template"),
- ("website", "web_form_field"),
- ("website", "portal_menu_item"),
- ("data_migration", "data_migration_mapping_detail"),
- ("data_migration", "data_migration_mapping"),
- ("data_migration", "data_migration_plan_mapping"),
- ("data_migration", "data_migration_plan"),
- ("desk", "number_card"),
- ("desk", "dashboard_chart"),
- ("desk", "dashboard"),
- ("desk", "onboarding_permission"),
- ("desk", "onboarding_step"),
- ("desk", "onboarding_step_map"),
- ("desk", "module_onboarding"),
- ("desk", "workspace_link"),
- ("desk", "workspace_chart"),
- ("desk", "workspace_shortcut"),
- ("desk", "workspace")):
- files.append(os.path.join(frappe.get_app_path("frappe"), d[0],
- "doctype", d[1], d[1] + ".json"))
+
+ FRAPPE_PATH = frappe.get_app_path("frappe")
+
+ for core_module in ["docfield", "docperm", "doctype_action", "doctype_link", "role", "has_role", "doctype"]:
+ files.append(os.path.join(FRAPPE_PATH, "core", "doctype", core_module, f"{core_module}.json"))
+
+ for custom_module in ["custom_field", "property_setter"]:
+ files.append(os.path.join(FRAPPE_PATH, "custom", "doctype", custom_module, f"{custom_module}.json"))
+
+ for website_module in ["web_form", "web_template", "web_form_field", "portal_menu_item"]:
+ files.append(os.path.join(FRAPPE_PATH, "website", "doctype", website_module, f"{website_module}.json"))
+
+ for data_migration_module in [
+ "data_migration_mapping_detail",
+ "data_migration_mapping",
+ "data_migration_plan_mapping",
+ "data_migration_plan",
+ ]:
+ files.append(os.path.join(FRAPPE_PATH, "data_migration", "doctype", data_migration_module, f"{data_migration_module}.json"))
+
+ for desk_module in [
+ "number_card",
+ "dashboard_chart",
+ "dashboard",
+ "onboarding_permission",
+ "onboarding_step",
+ "onboarding_step_map",
+ "module_onboarding",
+ "workspace_link",
+ "workspace_chart",
+ "workspace_shortcut",
+ "workspace",
+ ]:
+ files.append(os.path.join(FRAPPE_PATH, "desk", "doctype", desk_module, f"{desk_module}.json"))
for module_name in frappe.local.app_modules.get(app_name) or []:
folder = os.path.dirname(frappe.get_module(app_name + "." + module_name).__file__)
- get_doc_files(files, folder)
+ files = get_doc_files(files=files, start_path=folder)
l = len(files)
+
if l:
for i, doc_path in enumerate(files):
- import_file_by_path(doc_path, force=force, ignore_version=True,
- reset_permissions=reset_permissions, for_sync=True)
+ import_file_by_path(doc_path, force=force, ignore_version=True, reset_permissions=reset_permissions)
frappe.db.commit()
@@ -75,17 +80,36 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
# print each progress bar on new line
print()
+
def get_doc_files(files, start_path):
"""walk and sync all doctypes and pages"""
- # load in sequence - warning for devs
- document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
- 'web_page', 'website_theme', 'web_form', 'web_template',
- 'notification', 'print_style',
- 'data_migration_mapping', 'data_migration_plan',
- 'workspace', 'onboarding_step', 'module_onboarding', 'form_tour',
- 'client_script', 'server_script', 'custom_field', 'property_setter']
+ files = files or []
+ # load in sequence - warning for devs
+ document_types = [
+ "doctype",
+ "page",
+ "report",
+ "dashboard_chart_source",
+ "print_format",
+ "web_page",
+ "website_theme",
+ "web_form",
+ "web_template",
+ "notification",
+ "print_style",
+ "data_migration_mapping",
+ "data_migration_plan",
+ "workspace",
+ "onboarding_step",
+ "module_onboarding",
+ "form_tour",
+ "client_script",
+ "server_script",
+ "custom_field",
+ "property_setter",
+ ]
for doctype in document_types:
doctype_path = os.path.join(start_path, doctype)
if os.path.exists(doctype_path):
@@ -95,3 +119,5 @@ def get_doc_files(files, start_path):
if os.path.exists(doc_path):
if not doc_path in files:
files.append(doc_path)
+
+ return files
diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py
index e7a1f5f97c..cf8ec46d76 100644
--- a/frappe/modules/import_file.py
+++ b/frappe/modules/import_file.py
@@ -1,31 +1,53 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-import frappe, os, json
-from frappe.modules import get_module_path, scrub_dt_dn
-from frappe.utils import get_datetime_str
+import hashlib
+import json
+import os
+
+import frappe
from frappe.model.base_document import get_controller
+from frappe.modules import get_module_path, scrub_dt_dn
+from frappe.query_builder import DocType
+from frappe.utils import get_datetime_str, now
+
+
+def caclulate_hash(path: str) -> str:
+ """Calculate md5 hash of the file in binary mode
+
+ Args:
+ path (str): Path to the file to be hashed
+
+ Returns:
+ str: The calculated hash
+ """
+ hash_md5 = hashlib.md5()
+ with open(path, "rb") as f:
+ for chunk in iter(lambda: f.read(4096), b""):
+ hash_md5.update(chunk)
+ return hash_md5.hexdigest()
+
ignore_values = {
"Report": ["disabled", "prepared_report", "add_total_row"],
"Print Format": ["disabled"],
"Notification": ["enabled"],
"Print Style": ["disabled"],
- "Module Onboarding": ['is_complete'],
- "Onboarding Step": ['is_complete', 'is_skipped']
+ "Module Onboarding": ["is_complete"],
+ "Onboarding Step": ["is_complete", "is_skipped"],
}
ignore_doctypes = [""]
+
def import_files(module, dt=None, dn=None, force=False, pre_process=None, reset_permissions=False):
if type(module) is list:
out = []
for m in module:
- out.append(import_file(m[0], m[1], m[2], force=force, pre_process=pre_process,
- reset_permissions=reset_permissions))
+ out.append(import_file(m[0], m[1], m[2], force=force, pre_process=pre_process, reset_permissions=reset_permissions))
return out
else:
- return import_file(module, dt, dn, force=force, pre_process=pre_process,
- reset_permissions=reset_permissions)
+ return import_file(module, dt, dn, force=force, pre_process=pre_process, reset_permissions=reset_permissions)
+
def import_file(module, dt, dn, force=False, pre_process=None, reset_permissions=False):
"""Sync a file from txt if modifed, return false if not updated"""
@@ -33,77 +55,160 @@ def import_file(module, dt, dn, force=False, pre_process=None, reset_permissions
ret = import_file_by_path(path, force, pre_process=pre_process, reset_permissions=reset_permissions)
return ret
+
def get_file_path(module, dt, dn):
dt, dn = scrub_dt_dn(dt, dn)
- path = os.path.join(get_module_path(module),
- os.path.join(dt, dn, dn + ".json"))
+ path = os.path.join(get_module_path(module), os.path.join(dt, dn, f"{dn}.json"))
return path
-def import_file_by_path(path, force=False, data_import=False, pre_process=None, ignore_version=None,
- reset_permissions=False, for_sync=False):
+
+def import_file_by_path(path: str,force: bool = False,data_import: bool = False,pre_process = None,ignore_version: bool = None,reset_permissions: bool = False):
+ """Import file from the given path
+
+ Some conditions decide if a file should be imported or not.
+ Evaluation takes place in the order they are mentioned below.
+
+ - Check if `force` is true. Import the file. If not, move ahead.
+ - Get `db_modified_timestamp`(value of the modified field in the database for the file).
+ If the return is `none,` this file doesn't exist in the DB, so Import the file. If not, move ahead.
+ - Check if there is a hash in DB for that file. If there is, Calculate the Hash of the file to import and compare it with the one in DB if they are not equal.
+ Import the file. If Hash doesn't exist, move ahead.
+ - Check if `db_modified_timestamp` is older than the timestamp in the file; if it is, we import the file.
+
+ If timestamp comparison happens for doctypes, that means the Hash for it doesn't exist.
+ So, even if the timestamp is newer on DB (When comparing timestamps), we import the file and add the calculated Hash to the DB.
+ So in the subsequent imports, we can use hashes to compare. As a precautionary measure, the timestamp is updated to the current time as well.
+
+ Args:
+ path (str): Path to the file.
+ force (bool, optional): Load the file without checking any conditions. Defaults to False.
+ data_import (bool, optional): [description]. Defaults to False.
+ pre_process ([type], optional): Any preprocesing that may need to take place on the doc. Defaults to None.
+ ignore_version (bool, optional): ignore current version. Defaults to None.
+ reset_permissions (bool, optional): reset permissions for the file. Defaults to False.
+
+ Returns:
+ [bool]: True if import takes place. False if it wasn't imported.
+ """
+ frappe.flags.dt = frappe.flags.dt or []
try:
docs = read_doc_from_file(path)
except IOError:
- print (path + " missing")
+ print(f"{path} missing")
return
+ calculated_hash = caclulate_hash(path)
+
if docs:
if not isinstance(docs, list):
docs = [docs]
for doc in docs:
- if not force and not is_changed(doc):
- return False
- original_modified = doc.get("modified")
+ # modified timestamp in db, none if doctype's first import
+ db_modified_timestamp = frappe.db.get_value(doc["doctype"], doc["name"], "modified")
+ is_db_timestamp_latest = db_modified_timestamp and doc.get("modified") <= get_datetime_str(db_modified_timestamp)
- import_doc(doc, force=force, data_import=data_import, pre_process=pre_process,
- ignore_version=ignore_version, reset_permissions=reset_permissions, path=path)
+ if not force or db_modified_timestamp:
+ try:
+ stored_hash = frappe.db.get_value(doc["doctype"], doc["name"], "migration_hash")
+ except Exception:
+ frappe.flags.dt += [doc["doctype"]]
+ stored_hash = None
- if original_modified:
- update_modified(original_modified, doc)
+ # if hash exists and is equal no need to update
+ if stored_hash and stored_hash == calculated_hash:
+ return False
+
+ # if hash doesn't exist, check if db timestamp is same as json timestamp, add hash if from doctype
+ if is_db_timestamp_latest and doc["doctype"] != "DocType":
+ return False
+
+ import_doc(
+ docdict=doc,
+ force=force,
+ data_import=data_import,
+ pre_process=pre_process,
+ ignore_version=ignore_version,
+ reset_permissions=reset_permissions,
+ path=path,
+ )
+
+ if doc["doctype"] == "DocType":
+ doctype_table = DocType("DocType")
+ frappe.qb.update(
+ doctype_table
+ ).set(
+ doctype_table.migration_hash, calculated_hash
+ ).where(
+ doctype_table.name == doc["name"]
+ ).run()
+
+ new_modified_timestamp = doc.get("modified")
+
+ # if db timestamp is newer, hash must have changed, must update db timestamp
+ if is_db_timestamp_latest and doc["doctype"] == "DocType":
+ new_modified_timestamp = now()
+
+ if new_modified_timestamp:
+ update_modified(new_modified_timestamp, doc)
return True
-def is_changed(doc):
+
+def is_timestamp_changed(doc):
# check if timestamps match
- db_modified = frappe.db.get_value(doc['doctype'], doc['name'], 'modified')
- if db_modified and doc.get('modified')==get_datetime_str(db_modified):
- return False
- return True
+ db_modified = frappe.db.get_value(doc["doctype"], doc["name"], "modified")
+ return not (db_modified and doc.get("modified") == get_datetime_str(db_modified))
+
def read_doc_from_file(path):
doc = None
if os.path.exists(path):
- with open(path, 'r') as f:
+ with open(path, "r") as f:
try:
doc = json.loads(f.read())
except ValueError:
print("bad json: {0}".format(path))
raise
else:
- raise IOError('%s missing' % path)
+ raise IOError("%s missing" % path)
return doc
+
def update_modified(original_modified, doc):
# since there is a new timestamp on the file, update timestamp in
- if doc["doctype"] == doc["name"] and doc["name"]!="DocType":
- frappe.db.sql("""update tabSingles set value=%s where field="modified" and doctype=%s""",
- (original_modified, doc["name"]))
- else:
- frappe.db.sql("update `tab%s` set modified=%s where name=%s" % (doc['doctype'],
- '%s', '%s'), (original_modified, doc['name']))
+ if doc["doctype"] == doc["name"] and doc["name"] != "DocType":
+ singles_table = DocType("Singles")
-def import_doc(docdict, force=False, data_import=False, pre_process=None,
- ignore_version=None, reset_permissions=False, path=None):
+ frappe.qb.update(
+ singles_table
+ ).set(
+ singles_table.value,original_modified
+ ).where(
+ singles_table.field == "modified"
+ ).where(
+ singles_table.doctype == doc["name"]
+ ).run()
+ else:
+ doctype_table = DocType(doc['doctype'])
+
+ frappe.qb.update(doctype_table
+ ).set(
+ doctype_table.modified, original_modified
+ ).where(
+ doctype_table.name == doc["name"]
+ ).run()
+
+def import_doc(docdict, force=False, data_import=False, pre_process=None, ignore_version=None, reset_permissions=False, path=None):
frappe.flags.in_import = True
docdict["__islocal"] = 1
- controller = get_controller(docdict['doctype'])
- if controller and hasattr(controller, 'prepare_for_import') and callable(getattr(controller, 'prepare_for_import')):
+ controller = get_controller(docdict["doctype"])
+ if controller and hasattr(controller, "prepare_for_import") and callable(getattr(controller, "prepare_for_import")):
controller.prepare_for_import(docdict)
doc = frappe.get_doc(docdict)
@@ -132,15 +237,16 @@ def import_doc(docdict, force=False, data_import=False, pre_process=None,
return doc
+
def load_code_properties(doc, path):
- '''Load code files stored in separate files with extensions'''
+ """Load code files stored in separate files with extensions"""
if path:
- if hasattr(doc, 'get_code_fields'):
+ if hasattr(doc, "get_code_fields"):
dirname, filename = os.path.split(path)
for key, extn in doc.get_code_fields().items():
- codefile = os.path.join(dirname, filename.split('.')[0]+'.'+extn)
+ codefile = os.path.join(dirname, filename.split(".")[0] + "." + extn)
if os.path.exists(codefile):
- with open(codefile,'r') as txtfile:
+ with open(codefile, "r") as txtfile:
doc.set(key, txtfile.read())
@@ -164,12 +270,13 @@ def delete_old_doc(doc, reset_permissions):
doc.flags.ignore_children_type = ignore
+
def reset_tree_properties(doc):
# Note on Tree DocTypes:
# The tree structure is maintained in the database via the fields "lft" and
# "rgt". They are automatically set and kept up-to-date. Importing them
# would destroy any existing tree structure.
- if getattr(doc.meta, 'is_tree', None) and any([doc.lft, doc.rgt]):
+ if getattr(doc.meta, "is_tree", None) and any([doc.lft, doc.rgt]):
print('Ignoring values of `lft` and `rgt` for {} "{}"'.format(doc.doctype, doc.name))
doc.lft = None
doc.rgt = None
diff --git a/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py b/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py
index 55a7b74f7e..6b7a7695f6 100644
--- a/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py
+++ b/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py
@@ -1,7 +1,7 @@
-
import frappe
+
def execute():
frappe.flags.in_patch = True
- frappe.reload_doc('core', 'doctype', 'user_permission')
+ frappe.reload_doc("core", "doctype", "user_permission")
frappe.db.commit()
diff --git a/frappe/patches/v13_0/increase_password_length.py b/frappe/patches/v13_0/increase_password_length.py
index 62ca2ed779..deb7d7e98a 100644
--- a/frappe/patches/v13_0/increase_password_length.py
+++ b/frappe/patches/v13_0/increase_password_length.py
@@ -1,4 +1,4 @@
import frappe
def execute():
- frappe.db.change_column_type(table="__Auth", column="password", type="TEXT")
+ frappe.db.change_column_type("__Auth", column="password", type="TEXT")
diff --git a/frappe/permissions.py b/frappe/permissions.py
index 7ee1119ebb..a086c73920 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -107,13 +107,9 @@ def get_doc_permissions(doc, user=None, ptype=None):
meta = frappe.get_meta(doc.doctype)
def is_user_owner():
- doc_owner = doc.get('owner') or ''
- doc_owner = doc_owner.lower()
- session_user = frappe.session.user.lower()
- return doc_owner == session_user
+ return (doc.get("owner") or "").lower() == frappe.session.user.lower()
-
- if has_controller_permissions(doc, ptype, user=user) == False :
+ if has_controller_permissions(doc, ptype, user=user) is False:
push_perm_check_log('Not allowed via controller permission check')
return {ptype: 0}
@@ -182,22 +178,23 @@ def get_role_permissions(doctype_meta, user=None, is_owner=None):
applicable_permissions = list(filter(is_perm_applicable, getattr(doctype_meta, 'permissions', [])))
has_if_owner_enabled = any(p.get('if_owner', 0) for p in applicable_permissions)
-
perms['has_if_owner_enabled'] = has_if_owner_enabled
for ptype in rights:
pvalue = any(p.get(ptype, 0) for p in applicable_permissions)
# check if any perm object allows perm type
perms[ptype] = cint(pvalue)
- if (pvalue
- and has_if_owner_enabled
- and not has_permission_without_if_owner_enabled(ptype)
- and ptype != 'create'):
+ if (
+ pvalue
+ and has_if_owner_enabled
+ and not has_permission_without_if_owner_enabled(ptype)
+ and ptype != 'create'
+ ):
perms['if_owner'][ptype] = cint(pvalue and is_owner)
# has no access if not owner
# only provide select or read access so that user is able to at-least access list
# (and the documents will be filtered based on owner sin further checks)
- perms[ptype] = 1 if ptype in ['select', 'read'] else 0
+ perms[ptype] = 1 if ptype in ('select', 'read') else 0
frappe.local.role_permissions[cache_key] = perms
diff --git a/frappe/printing/doctype/network_printer_settings/__init__.py b/frappe/printing/doctype/network_printer_settings/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.js b/frappe/printing/doctype/network_printer_settings/network_printer_settings.js
new file mode 100644
index 0000000000..043afd388f
--- /dev/null
+++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.js
@@ -0,0 +1,29 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Network Printer Settings', {
+ onload (frm) {
+ frm.trigger("connect_print_server");
+ },
+ server_ip (frm) {
+ frm.trigger("connect_print_server");
+ },
+ port (frm) {
+ frm.trigger("connect_print_server");
+ },
+ connect_print_server (frm) {
+ if (frm.doc.server_ip && frm.doc.port) {
+ frappe.call({
+ "doc": frm.doc,
+ "method": "get_printers_list",
+ "args": {
+ ip: frm.doc.server_ip,
+ port: frm.doc.port
+ },
+ callback: function(data) {
+ frm.set_df_property('printer_name', 'options', [""].concat(data.message));
+ }
+ });
+ }
+ }
+});
diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.json b/frappe/printing/doctype/network_printer_settings/network_printer_settings.json
new file mode 100644
index 0000000000..11f1382225
--- /dev/null
+++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.json
@@ -0,0 +1,76 @@
+{
+ "actions": [],
+ "autoname": "Prompt",
+ "creation": "2021-09-17 11:26:06.943999",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "server_ip",
+ "port",
+ "column_break_4",
+ "printer_name"
+ ],
+ "fields": [
+ {
+ "default": "localhost",
+ "fieldname": "server_ip",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Server IP",
+ "reqd": 1
+ },
+ {
+ "default": "631",
+ "fieldname": "port",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Port",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "printer_name",
+ "fieldtype": "Select",
+ "label": "Printer Name",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-10-07 11:23:13.799402",
+ "modified_by": "Administrator",
+ "module": "Printing",
+ "name": "Network Printer Settings",
+ "naming_rule": "Set by user",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py
new file mode 100644
index 0000000000..e42ed818c7
--- /dev/null
+++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+from frappe import _
+
+class NetworkPrinterSettings(Document):
+ @frappe.whitelist()
+ def get_printers_list(self,ip="localhost",port=631):
+ printer_list = []
+ try:
+ import cups
+ except ImportError:
+ frappe.throw(_('''This feature can not be used as dependencies are missing.
+ Please contact your system manager to enable this by installing pycups!'''))
+ return
+ try:
+ cups.setServer(self.server_ip)
+ cups.setPort(self.port)
+ conn = cups.Connection()
+ printers = conn.getPrinters()
+ for printer_id,printer in printers.items():
+ printer_list.append({
+ 'value': printer_id,
+ 'label': printer['printer-make-and-model']
+ })
+
+ except RuntimeError:
+ frappe.throw(_("Failed to connect to server"))
+ except frappe.ValidationError:
+ frappe.throw(_("Failed to connect to server"))
+ return printer_list
+
+@frappe.whitelist()
+def get_network_printer_settings():
+ return frappe.db.get_list('Network Printer Settings', pluck='name')
diff --git a/frappe/printing/doctype/network_printer_settings/test_network_printer_settings.py b/frappe/printing/doctype/network_printer_settings/test_network_printer_settings.py
new file mode 100644
index 0000000000..86509b239f
--- /dev/null
+++ b/frappe/printing/doctype/network_printer_settings/test_network_printer_settings.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestNetworkPrinterSettings(unittest.TestCase):
+ pass
diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js
index adc5e2363c..7b7009dbaf 100644
--- a/frappe/printing/doctype/print_format/print_format.js
+++ b/frappe/printing/doctype/print_format/print_format.js
@@ -36,7 +36,7 @@ frappe.ui.form.on("Print Format", {
else if (frm.doc.custom_format && !frm.doc.raw_printing) {
frm.set_df_property("html", "reqd", 1);
}
- if (frappe.perm.has_perm('DocType', 0, 'read', frm.doc.doc_type)) {
+ if (frappe.model.can_read(frm.doc.doc_type)) {
frappe.db.get_value('DocType', frm.doc.doc_type, 'default_print_format', (r) => {
if (r.default_print_format != frm.doc.name) {
frm.add_custom_button(__("Set as Default"), function () {
diff --git a/frappe/printing/doctype/print_settings/print_settings.js b/frappe/printing/doctype/print_settings/print_settings.js
index 9616892a31..b1311166ee 100644
--- a/frappe/printing/doctype/print_settings/print_settings.js
+++ b/frappe/printing/doctype/print_settings/print_settings.js
@@ -15,27 +15,5 @@ frappe.ui.form.on('Print Settings', {
},
onload: function(frm) {
frm.script_manager.trigger("print_style");
- },
- server_ip: function(frm) {
- frm.trigger("connect_print_server");
- },
- port:function(frm) {
- frm.trigger("connect_print_server");
- },
- connect_print_server:function(frm) {
- if(frm.doc.server_ip && frm.doc.port){
- frappe.call({
- "doc": frm.doc,
- "method": "get_printers",
- "args": {
- ip: frm.doc.server_ip,
- port: frm.doc.port
- },
- callback: function(data) {
- frm.set_df_property('printer_name', 'options', [""].concat(data.message));
- },
- error: (data) => frm.set_value("enable_print_server", 0)
- });
- }
}
});
diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json
index 31962be050..babbae248d 100644
--- a/frappe/printing/doctype/print_settings/print_settings.json
+++ b/frappe/printing/doctype/print_settings/print_settings.json
@@ -19,9 +19,6 @@
"allow_print_for_cancelled",
"server_printer",
"enable_print_server",
- "server_ip",
- "printer_name",
- "port",
"raw_printing_section",
"enable_raw_printing",
"print_style_section",
@@ -107,29 +104,11 @@
},
{
"default": "0",
+ "depends_on": "enable_print_server",
"fieldname": "enable_print_server",
"fieldtype": "Check",
- "label": "Enable Print Server"
- },
- {
- "default": "localhost",
- "depends_on": "enable_print_server",
- "fieldname": "server_ip",
- "fieldtype": "Data",
- "label": "Server IP"
- },
- {
- "depends_on": "enable_print_server",
- "fieldname": "printer_name",
- "fieldtype": "Select",
- "label": "Printer Name"
- },
- {
- "default": "631",
- "depends_on": "enable_print_server",
- "fieldname": "port",
- "fieldtype": "Int",
- "label": "Port"
+ "label": "Enable Print Server",
+ "mandatory_depends_on": "enable_print_server"
},
{
"fieldname": "raw_printing_section",
@@ -183,7 +162,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-02-15 14:16:18.474254",
+ "modified": "2021-09-17 12:59:14.783694",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Settings",
diff --git a/frappe/printing/doctype/print_settings/print_settings.py b/frappe/printing/doctype/print_settings/print_settings.py
index a7e59c9078..ff00317cf8 100644
--- a/frappe/printing/doctype/print_settings/print_settings.py
+++ b/frappe/printing/doctype/print_settings/print_settings.py
@@ -12,26 +12,6 @@ class PrintSettings(Document):
def on_update(self):
frappe.clear_cache()
- @frappe.whitelist()
- def get_printers(self,ip="localhost",port=631):
- printer_list = []
- try:
- import cups
- except ImportError:
- frappe.throw(_("You need to install pycups to use this feature!"))
- return
- try:
- cups.setServer(self.server_ip)
- cups.setPort(self.port)
- conn = cups.Connection()
- printers = conn.getPrinters()
- printer_list = printers.keys()
- except RuntimeError:
- frappe.throw(_("Failed to connect to server"))
- except frappe.ValidationError:
- frappe.throw(_("Failed to connect to server"))
- return printer_list
-
@frappe.whitelist()
def is_print_server_enabled():
if not hasattr(frappe.local, 'enable_print_server'):
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index da34dfda96..1e158c616e 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -165,20 +165,23 @@ frappe.ui.form.PrintView = class {
frappe.set_route('Form', 'Print Settings');
});
- if (
- frappe.model.get_doc(':Print Settings', 'Print Settings')
- .enable_raw_printing == '1'
- ) {
+ if (this.print_settings.enable_raw_printing == '1') {
this.page.add_menu_item(__('Raw Printing Setting'), () => {
this.printer_setting_dialog();
});
}
- if (frappe.perm.has_perm('Print Format', 0, 'create')) {
+ if (frappe.model.can_create('Print Format')) {
this.page.add_menu_item(__('Customize'), () =>
this.edit_print_format()
);
}
+
+ if (cint(this.print_settings.enable_print_server)) {
+ this.page.add_menu_item(__('Select Network Printer'), () =>
+ this.network_printer_setting_dialog()
+ );
+ }
}
show(frm) {
@@ -460,72 +463,108 @@ frappe.ui.form.PrintView = class {
printit() {
let me = this;
- frappe.call({
- method:
- 'frappe.printing.doctype.print_settings.print_settings.is_print_server_enabled',
- callback: function(data) {
- if (data.message) {
- frappe.call({
- method: 'frappe.utils.print_format.print_by_server',
- args: {
- doctype: me.frm.doc.doctype,
- name: me.frm.doc.name,
- print_format: me.selected_format(),
- no_letterhead: me.with_letterhead(),
- letterhead: this.get_letterhead(),
- },
- callback: function() {},
- });
- } else if (me.get_mapped_printer().length === 1) {
- // printer is already mapped in localstorage (applies for both raw and pdf )
- if (me.is_raw_printing()) {
- me.get_raw_commands(function(out) {
- frappe.ui.form
- .qz_connect()
- .then(function() {
- let printer_map = me.get_mapped_printer()[0];
- let data = [out.raw_commands];
- let config = qz.configs.create(printer_map.printer);
- return qz.print(config, data);
- })
- .then(frappe.ui.form.qz_success)
- .catch((err) => {
- frappe.ui.form.qz_fail(err);
- });
+
+ if (cint(me.print_settings.enable_print_server)) {
+ if (localStorage.getItem('network_printer')) {
+ me.print_by_server();
+ } else {
+ me.network_printer_setting_dialog(() => me.print_by_server());
+ }
+ } else if (me.get_mapped_printer().length === 1) {
+ // printer is already mapped in localstorage (applies for both raw and pdf )
+ if (me.is_raw_printing()) {
+ me.get_raw_commands(function(out) {
+ frappe.ui.form
+ .qz_connect()
+ .then(function() {
+ let printer_map = me.get_mapped_printer()[0];
+ let data = [out.raw_commands];
+ let config = qz.configs.create(printer_map.printer);
+ return qz.print(config, data);
+ })
+ .then(frappe.ui.form.qz_success)
+ .catch((err) => {
+ frappe.ui.form.qz_fail(err);
});
- } else {
- frappe.show_alert(
+ });
+ } else {
+ frappe.show_alert(
+ {
+ message: __('PDF printing via "Raw Print" is not supported.'),
+ subtitle: __(
+ 'Please remove the printer mapping in Printer Settings and try again.'
+ ),
+ indicator: 'info',
+ },
+ 14
+ );
+ //Note: need to solve "Error: Cannot parse (FILE) as a PDF file" to enable qz pdf printing.
+ }
+ } else if (me.is_raw_printing()) {
+ // printer not mapped in localstorage and the current print format is raw printing
+ frappe.show_alert(
+ {
+ message: __('Printer mapping not set.'),
+ subtitle: __(
+ 'Please set a printer mapping for this print format in the Printer Settings'
+ ),
+ indicator: 'warning',
+ },
+ 14
+ );
+ me.printer_setting_dialog();
+ } else {
+ me.render_page('/printview?', true);
+ }
+ }
+
+ print_by_server() {
+ let me = this;
+ if (localStorage.getItem('network_printer')) {
+ frappe.call({
+ method: 'frappe.utils.print_format.print_by_server',
+ args: {
+ doctype: me.frm.doc.doctype,
+ name: me.frm.doc.name,
+ printer_setting: localStorage.getItem('network_printer'),
+ print_format: me.selected_format(),
+ no_letterhead: me.with_letterhead(),
+ letterhead: me.get_letterhead(),
+ },
+ callback: function() {},
+ });
+ }
+ }
+ network_printer_setting_dialog(callback) {
+ frappe.call({
+ method: 'frappe.printing.doctype.network_printer_settings.network_printer_settings.get_network_printer_settings',
+ callback: function(r) {
+ if (r.message) {
+ let d = new frappe.ui.Dialog({
+ title: __('Select Network Printer'),
+ fields: [
{
- message: __('PDF printing via "Raw Print" is not supported.'),
- subtitle: __(
- 'Please remove the printer mapping in Printer Settings and try again.'
- ),
- indicator: 'info',
- },
- 14
- );
- //Note: need to solve "Error: Cannot parse (FILE) as a PDF file" to enable qz pdf printing.
- }
- } else if (me.is_raw_printing()) {
- // printer not mapped in localstorage and the current print format is raw printing
- frappe.show_alert(
- {
- message: __('Printer mapping not set.'),
- subtitle: __(
- 'Please set a printer mapping for this print format in the Printer Settings'
- ),
- indicator: 'warning',
+ "label": "Printer",
+ "fieldname": "printer",
+ "fieldtype": "Select",
+ "reqd": 1,
+ "options": r.message
+ }
+ ],
+ primary_action: function() {
+ localStorage.setItem('network_printer', d.get_values().printer);
+ if (typeof callback == "function") {
+ callback();
+ }
+ d.hide();
},
- 14
- );
- me.printer_setting_dialog();
- } else {
- me.render_page('/printview?', true);
+ primary_action_label: __('Select')
+ });
+ d.show();
}
},
});
}
-
render_page(method, printit = false) {
let w = window.open(
frappe.urllib.get_full_url(
diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js
index ca2a8bc378..b73ff31d32 100644
--- a/frappe/printing/page/print_format_builder/print_format_builder.js
+++ b/frappe/printing/page/print_format_builder/print_format_builder.js
@@ -261,7 +261,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
} else if(f.fieldtype==="Column Break") {
set_column();
- } else if(!in_list(["Section Break", "Column Break", "Fold"], f.fieldtype)
+ } else if (!in_list(["Section Break", "Column Break", "Tab Break", "Fold"], f.fieldtype)
&& f.label) {
if(!column) set_column();
@@ -298,7 +298,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
init_visible_columns(f) {
f.visible_columns = []
$.each(frappe.get_meta(f.options).fields, function(i, _f) {
- if(!in_list(["Section Break", "Column Break"], _f.fieldtype) &&
+ if (!in_list(["Section Break", "Column Break", "Tab Break"], _f.fieldtype) &&
!_f.print_hide && f.label) {
// column names set as fieldname|width
@@ -606,7 +606,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
// add remaining fields
$.each(doc_fields, function(j, f) {
if (f && !in_list(column_names, f.fieldname)
- && !in_list(["Section Break", "Column Break"], f.fieldtype) && f.label) {
+ && !in_list(["Section Break", "Column Break", "Tab Break"], f.fieldtype) && f.label) {
fields.push(f);
}
})
diff --git a/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html b/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html
index 1ebb87ac31..c608eecbbd 100644
--- a/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html
+++ b/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html
@@ -4,7 +4,7 @@
+ + + Add / Remove Columns + +
++
+-
+ {{ __('Copy Link') }}
+
+
+