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/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/README.md b/README.md
index 6c2804d843..f8a1907da2 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@
-
+
@@ -35,25 +35,29 @@
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)
## 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..eeba1ff381 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -4,10 +4,28 @@ codecov:
coverage:
status:
project:
- default:
+ default: false
+ server:
target: auto
threshold: 0.5%
+ flags:
+ - server
+ ui-tests:
+ target: auto
+ threshold: 0.5%
+ flags:
+ - ui-tests
comment:
- layout: "diff"
+ layout: "diff, flags"
require_changes: true
+
+flags:
+ server:
+ paths:
+ - ".*\\.py"
+ carryforward: true
+ ui-tests:
+ paths:
+ - ".*\\.js"
+ carryforward: true
\ No newline at end of file
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..3c055d1923
--- /dev/null
+++ b/cypress/integration/discussions.js
@@ -0,0 +1,63 @@
+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', '');
+ };
+
+ 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);
+});
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..bf4436358e 100644
--- a/esbuild/esbuild.js
+++ b/esbuild/esbuild.js
@@ -104,6 +104,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;
}
@@ -528,4 +531,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..05fa213018 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):
diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py
index 6fb33a51b7..82a71ce7b4 100644
--- a/frappe/commands/__init__.py
+++ b/frappe/commands/__init__.py
@@ -102,7 +102,7 @@ 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"
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/utils.py b/frappe/commands/utils.py
index 90cd60c6ec..74af28e3ff 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -679,9 +679,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 +692,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 +700,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/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.py b/frappe/core/doctype/doctype/doctype.py
index 9bf21690fc..8f8a8ed287 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -274,6 +274,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:
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/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/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/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 c48e86d301..e98cc22f41 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
class Database(object):
@@ -55,6 +60,7 @@ class Database(object):
self.password = password or frappe.conf.db_password
self.value_cache = {}
+ self.query = Query()
def setup_type_map(self):
pass
@@ -77,7 +83,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.
@@ -90,7 +96,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
@@ -105,6 +111,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)
@@ -310,59 +319,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)
@@ -424,9 +380,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:
@@ -439,6 +394,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)):
@@ -567,32 +523,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
@@ -819,50 +766,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")
@@ -984,16 +915,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 5ed7991a82..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)
}
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 a06abb1013..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)
}
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/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/form/utils.py b/frappe/desk/form/utils.py
index a4dcee4ab3..bd04e1915c 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -66,7 +66,7 @@ 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)
+ doc.content = extract_images_from_html(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/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 = $(`
+ + + Add / Remove Columns + +
++
+-
+ {{ __('Copy Link') }}
+
+
+