Merge branch 'frappe:develop' into wspace-code-cleanup
This commit is contained in:
commit
6839f6b4db
176 changed files with 4771 additions and 2662 deletions
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/docs-checker.yml
vendored
2
.github/workflows/docs-checker.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
23
.github/workflows/patch-mariadb-tests.yml
vendored
23
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/publish-assets-develop.yml
vendored
2
.github/workflows/publish-assets-develop.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
5
.github/workflows/server-mariadb-tests.yml
vendored
5
.github/workflows/server-mariadb-tests.yml
vendored
|
|
@ -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
|
||||
verbose: true
|
||||
flags: server
|
||||
3
.github/workflows/server-postgres-tests.yml
vendored
3
.github/workflows/server-postgres-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
28
.github/workflows/ui-tests.yml
vendored
28
.github/workflows/ui-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -67,6 +67,7 @@ coverage.xml
|
|||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
.cypress-coverage
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
|
|
|||
22
README.md
22
README.md
|
|
@ -27,7 +27,7 @@
|
|||
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/frappe/frappe">
|
||||
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj"/>
|
||||
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj&flag=server"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
22
codecov.yml
22
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
|
||||
59
cypress/fixtures/doctype_with_tab_break.js
Normal file
59
cypress/fixtures/doctype_with_tab_break.js
Normal file
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
63
cypress/integration/discussions.js
Normal file
63
cypress/integration/discussions.js
Normal file
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
31
cypress/integration/form_tab_break.js
Normal file
31
cypress/integration/form_tab_break.js
Normal file
|
|
@ -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");
|
||||
|
||||
});
|
||||
});
|
||||
23
cypress/integration/grid_configuration.js
Normal file
23
cypress/integration/grid_configuration.js
Normal file
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
import '@cypress/code-coverage/support';
|
||||
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
134
frappe/build.py
134
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):
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
267
frappe/database/query.py
Normal file
267
frappe/database/query.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
"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
|
||||
}
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = $(`<div class="from-date-field"></div>`).insertAfter(timespan_field).hide();
|
||||
|
||||
let date_field = frappe.ui.form.make_control({
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ def validate_filters(data, filters):
|
|||
|
||||
def setup_group_by(data):
|
||||
'''Add columns for aggregated values e.g. count(name)'''
|
||||
if data.group_by:
|
||||
if data.group_by and data.aggregate_function:
|
||||
if data.aggregate_function.lower() not in ('count', 'sum', 'avg'):
|
||||
frappe.throw(_('Invalid aggregate function'))
|
||||
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@
|
|||
},
|
||||
{
|
||||
"default": "UNSEEN",
|
||||
"depends_on": "eval: doc.enable_incoming",
|
||||
"depends_on": "eval: doc.enable_incoming && doc.use_imap",
|
||||
"fieldname": "email_sync_option",
|
||||
"fieldtype": "Select",
|
||||
"hide_days": 1,
|
||||
|
|
@ -236,7 +236,7 @@
|
|||
},
|
||||
{
|
||||
"default": "250",
|
||||
"depends_on": "eval: doc.enable_incoming",
|
||||
"depends_on": "eval: doc.enable_incoming && doc.use_imap",
|
||||
"description": "Total number of emails to sync in initial sync process ",
|
||||
"fieldname": "initial_sync_count",
|
||||
"fieldtype": "Select",
|
||||
|
|
@ -567,7 +567,7 @@
|
|||
"icon": "fa fa-inbox",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-31 15:23:25.714366",
|
||||
"modified": "2021-09-21 16:44:25.728637",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Account",
|
||||
|
|
@ -589,4 +589,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ def get_context(context):
|
|||
if doc.meta.get_field(fieldname).fieldtype in frappe.model.numeric_fieldtypes:
|
||||
value = frappe.utils.cint(value)
|
||||
|
||||
doc.reload()
|
||||
doc.set(fieldname, value)
|
||||
doc.flags.updater_reference = {
|
||||
'doctype': self.doctype,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ class TestNotification(unittest.TestCase):
|
|||
notification.event = 'Value Change'
|
||||
notification.value_changed = 'status'
|
||||
notification.send_to_all_assignees = 1
|
||||
notification.set_property_after_alert = 'description'
|
||||
notification.property_value = 'Changed by Notification'
|
||||
notification.save()
|
||||
|
||||
if not frappe.db.exists('Notification', {'name': 'Contact Status Update'}, 'name'):
|
||||
|
|
@ -237,6 +239,9 @@ class TestNotification(unittest.TestCase):
|
|||
|
||||
self.assertTrue(email_queue)
|
||||
|
||||
# check if description is changed after alert since set_property_after_alert is set
|
||||
self.assertEquals(todo.description, 'Changed by Notification')
|
||||
|
||||
recipients = [d.recipient for d in email_queue.recipients]
|
||||
self.assertTrue('test2@example.com' in recipients)
|
||||
self.assertTrue('test1@example.com' in recipients)
|
||||
|
|
@ -269,4 +274,7 @@ class TestNotification(unittest.TestCase):
|
|||
self.assertTrue('test2@example.com' in recipients)
|
||||
self.assertTrue('test1@example.com' in recipients)
|
||||
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.delete_doc_if_exists("Notification", "ToDo Status Update")
|
||||
frappe.delete_doc_if_exists("Notification", "Contact Status Update")
|
||||
|
|
@ -408,8 +408,9 @@ def sync_dependencies(document, producer_site):
|
|||
child_table = doc.get(df.fieldname)
|
||||
for entry in child_table:
|
||||
child_doc = producer_site.get_doc(entry.doctype, entry.name)
|
||||
child_doc = frappe._dict(child_doc)
|
||||
set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site)
|
||||
if child_doc:
|
||||
child_doc = frappe._dict(child_doc)
|
||||
set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site)
|
||||
|
||||
def sync_link_dependencies(doc, link_fields, producer_site):
|
||||
set_dependencies(doc, link_fields, producer_site)
|
||||
|
|
|
|||
|
|
@ -223,7 +223,10 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
|
|||
doc = frappe.get_doc(dt, dn)
|
||||
|
||||
else:
|
||||
doc = frappe.get_doc(json.loads(docs))
|
||||
if isinstance(docs, str):
|
||||
docs = json.loads(docs)
|
||||
|
||||
doc = frappe.get_doc(docs)
|
||||
doc._original_modified = doc.modified
|
||||
doc.check_if_latest()
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ def _new_site(
|
|||
):
|
||||
"""Install a new Frappe site"""
|
||||
|
||||
from frappe.commands.scheduler import _is_scheduler_enabled
|
||||
from frappe.utils import get_site_path, scheduler, touch_file
|
||||
|
||||
|
||||
if not force and os.path.exists(site):
|
||||
print("Site {0} already exists".format(site))
|
||||
sys.exit(1)
|
||||
|
|
@ -37,14 +41,11 @@ def _new_site(
|
|||
print("--no-mariadb-socket requires db_type to be set to mariadb.")
|
||||
sys.exit(1)
|
||||
|
||||
if not db_name:
|
||||
import hashlib
|
||||
db_name = "_" + hashlib.sha1(site.encode()).hexdigest()[:16]
|
||||
|
||||
frappe.init(site=site)
|
||||
|
||||
from frappe.commands.scheduler import _is_scheduler_enabled
|
||||
from frappe.utils import get_site_path, scheduler, touch_file
|
||||
if not db_name:
|
||||
import hashlib
|
||||
db_name = "_" + hashlib.sha1(os.path.realpath(frappe.get_site_path()).encode()).hexdigest()[:16]
|
||||
|
||||
try:
|
||||
# enable scheduler post install?
|
||||
|
|
@ -455,9 +456,21 @@ def convert_archive_content(sql_file_path):
|
|||
if frappe.conf.db_type == "mariadb":
|
||||
# ever since mariaDB 10.6, row_format COMPRESSED has been deprecated and removed
|
||||
# this step is added to ease restoring sites depending on older mariaDB servers
|
||||
contents = open(sql_file_path).read()
|
||||
with open(sql_file_path, "w") as f:
|
||||
f.write(contents.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC"))
|
||||
from frappe.utils import random_string
|
||||
from pathlib import Path
|
||||
|
||||
old_sql_file_path = Path(f"{sql_file_path}_{random_string(10)}")
|
||||
sql_file_path = Path(sql_file_path)
|
||||
|
||||
os.rename(sql_file_path, old_sql_file_path)
|
||||
sql_file_path.unlink(missing_ok=True)
|
||||
sql_file_path.touch()
|
||||
|
||||
with open(old_sql_file_path) as r, open(sql_file_path, "a") as w:
|
||||
for line in r:
|
||||
w.write(line.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC"))
|
||||
|
||||
old_sql_file_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def extract_sql_gzip(sql_gz_path):
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@ def capture_payment(is_sandbox=False, sanbox_response=None):
|
|||
doc = frappe.get_doc("Integration Request", doc.name)
|
||||
doc.status = "Failed"
|
||||
doc.error = frappe.get_traceback()
|
||||
doc.save()
|
||||
frappe.log_error(doc.error, '{0} Failed'.format(doc.name))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ data_fieldtypes = (
|
|||
no_value_fields = (
|
||||
'Section Break',
|
||||
'Column Break',
|
||||
'Tab Break',
|
||||
'HTML',
|
||||
'Table',
|
||||
'Table MultiSelect',
|
||||
|
|
@ -53,6 +54,7 @@ no_value_fields = (
|
|||
display_fieldtypes = (
|
||||
'Section Break',
|
||||
'Column Break',
|
||||
'Tab Break',
|
||||
'HTML',
|
||||
'Button',
|
||||
'Image',
|
||||
|
|
|
|||
|
|
@ -267,7 +267,12 @@ class BaseDocument(object):
|
|||
if isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
|
||||
frappe.throw(_('Value for {0} cannot be a list').format(_(df.label)))
|
||||
|
||||
if convert_dates_to_str and isinstance(d[fieldname], (datetime.datetime, datetime.time, datetime.timedelta)):
|
||||
if convert_dates_to_str and isinstance(d[fieldname], (
|
||||
datetime.datetime,
|
||||
datetime.date,
|
||||
datetime.time,
|
||||
datetime.timedelta
|
||||
)):
|
||||
d[fieldname] = str(d[fieldname])
|
||||
|
||||
if d[fieldname] == None and ignore_nulls:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
from typing import List
|
||||
import frappe.defaults
|
||||
from frappe.query_builder.utils import Column
|
||||
import frappe.share
|
||||
from frappe import _
|
||||
import frappe.permissions
|
||||
|
|
@ -491,7 +492,7 @@ class DatabaseQuery(object):
|
|||
f.value = date_range
|
||||
fallback = "'0001-01-01 00:00:00'"
|
||||
|
||||
if f.operator in ('>', '<') and (f.fieldname in ('creation', 'modified')):
|
||||
if (f.fieldname in ('creation', 'modified')):
|
||||
value = cstr(f.value)
|
||||
fallback = "NULL"
|
||||
|
||||
|
|
@ -547,8 +548,12 @@ class DatabaseQuery(object):
|
|||
value = flt(f.value)
|
||||
fallback = 0
|
||||
|
||||
if isinstance(f.value, Column):
|
||||
quote = '"' if frappe.conf.db_type == 'postgres' else "`"
|
||||
value = f"{tname}.{quote}{f.value.name}{quote}"
|
||||
|
||||
# escape value
|
||||
if isinstance(value, str) and not f.operator.lower() == 'between':
|
||||
elif isinstance(value, str) and not f.operator.lower() == 'between':
|
||||
value = f"{frappe.db.escape(value, percent=False)}"
|
||||
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2021, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Network Printer Settings', {
|
||||
onload (frm) {
|
||||
frm.trigger("connect_print_server");
|
||||
},
|
||||
server_ip (frm) {
|
||||
frm.trigger("connect_print_server");
|
||||
},
|
||||
port (frm) {
|
||||
frm.trigger("connect_print_server");
|
||||
},
|
||||
connect_print_server (frm) {
|
||||
if (frm.doc.server_ip && frm.doc.port) {
|
||||
frappe.call({
|
||||
"doc": frm.doc,
|
||||
"method": "get_printers_list",
|
||||
"args": {
|
||||
ip: frm.doc.server_ip,
|
||||
port: frm.doc.port
|
||||
},
|
||||
callback: function(data) {
|
||||
frm.set_df_property('printer_name', 'options', [""].concat(data.message));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "Prompt",
|
||||
"creation": "2021-09-17 11:26:06.943999",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"server_ip",
|
||||
"port",
|
||||
"column_break_4",
|
||||
"printer_name"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "localhost",
|
||||
"fieldname": "server_ip",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Server IP",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "631",
|
||||
"fieldname": "port",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Port",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "printer_name",
|
||||
"fieldtype": "Select",
|
||||
"label": "Printer Name",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-07 11:23:13.799402",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Printing",
|
||||
"name": "Network Printer Settings",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
|
||||
class NetworkPrinterSettings(Document):
|
||||
@frappe.whitelist()
|
||||
def get_printers_list(self,ip="localhost",port=631):
|
||||
printer_list = []
|
||||
try:
|
||||
import cups
|
||||
except ImportError:
|
||||
frappe.throw(_('''This feature can not be used as dependencies are missing.
|
||||
Please contact your system manager to enable this by installing pycups!'''))
|
||||
return
|
||||
try:
|
||||
cups.setServer(self.server_ip)
|
||||
cups.setPort(self.port)
|
||||
conn = cups.Connection()
|
||||
printers = conn.getPrinters()
|
||||
for printer_id,printer in printers.items():
|
||||
printer_list.append({
|
||||
'value': printer_id,
|
||||
'label': printer['printer-make-and-model']
|
||||
})
|
||||
|
||||
except RuntimeError:
|
||||
frappe.throw(_("Failed to connect to server"))
|
||||
except frappe.ValidationError:
|
||||
frappe.throw(_("Failed to connect to server"))
|
||||
return printer_list
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_network_printer_settings():
|
||||
return frappe.db.get_list('Network Printer Settings', pluck='name')
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestNetworkPrinterSettings(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -15,27 +15,5 @@ frappe.ui.form.on('Print Settings', {
|
|||
},
|
||||
onload: function(frm) {
|
||||
frm.script_manager.trigger("print_style");
|
||||
},
|
||||
server_ip: function(frm) {
|
||||
frm.trigger("connect_print_server");
|
||||
},
|
||||
port:function(frm) {
|
||||
frm.trigger("connect_print_server");
|
||||
},
|
||||
connect_print_server:function(frm) {
|
||||
if(frm.doc.server_ip && frm.doc.port){
|
||||
frappe.call({
|
||||
"doc": frm.doc,
|
||||
"method": "get_printers",
|
||||
"args": {
|
||||
ip: frm.doc.server_ip,
|
||||
port: frm.doc.port
|
||||
},
|
||||
callback: function(data) {
|
||||
frm.set_df_property('printer_name', 'options', [""].concat(data.message));
|
||||
},
|
||||
error: (data) => frm.set_value("enable_print_server", 0)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,9 +19,6 @@
|
|||
"allow_print_for_cancelled",
|
||||
"server_printer",
|
||||
"enable_print_server",
|
||||
"server_ip",
|
||||
"printer_name",
|
||||
"port",
|
||||
"raw_printing_section",
|
||||
"enable_raw_printing",
|
||||
"print_style_section",
|
||||
|
|
@ -107,29 +104,11 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "enable_print_server",
|
||||
"fieldname": "enable_print_server",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Print Server"
|
||||
},
|
||||
{
|
||||
"default": "localhost",
|
||||
"depends_on": "enable_print_server",
|
||||
"fieldname": "server_ip",
|
||||
"fieldtype": "Data",
|
||||
"label": "Server IP"
|
||||
},
|
||||
{
|
||||
"depends_on": "enable_print_server",
|
||||
"fieldname": "printer_name",
|
||||
"fieldtype": "Select",
|
||||
"label": "Printer Name"
|
||||
},
|
||||
{
|
||||
"default": "631",
|
||||
"depends_on": "enable_print_server",
|
||||
"fieldname": "port",
|
||||
"fieldtype": "Int",
|
||||
"label": "Port"
|
||||
"label": "Enable Print Server",
|
||||
"mandatory_depends_on": "enable_print_server"
|
||||
},
|
||||
{
|
||||
"fieldname": "raw_printing_section",
|
||||
|
|
@ -183,7 +162,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-15 14:16:18.474254",
|
||||
"modified": "2021-09-17 12:59:14.783694",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Printing",
|
||||
"name": "Print Settings",
|
||||
|
|
|
|||
|
|
@ -12,26 +12,6 @@ class PrintSettings(Document):
|
|||
def on_update(self):
|
||||
frappe.clear_cache()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_printers(self,ip="localhost",port=631):
|
||||
printer_list = []
|
||||
try:
|
||||
import cups
|
||||
except ImportError:
|
||||
frappe.throw(_("You need to install pycups to use this feature!"))
|
||||
return
|
||||
try:
|
||||
cups.setServer(self.server_ip)
|
||||
cups.setPort(self.port)
|
||||
conn = cups.Connection()
|
||||
printers = conn.getPrinters()
|
||||
printer_list = printers.keys()
|
||||
except RuntimeError:
|
||||
frappe.throw(_("Failed to connect to server"))
|
||||
except frappe.ValidationError:
|
||||
frappe.throw(_("Failed to connect to server"))
|
||||
return printer_list
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_print_server_enabled():
|
||||
if not hasattr(frappe.local, 'enable_print_server'):
|
||||
|
|
|
|||
|
|
@ -165,10 +165,7 @@ frappe.ui.form.PrintView = class {
|
|||
frappe.set_route('Form', 'Print Settings');
|
||||
});
|
||||
|
||||
if (
|
||||
frappe.model.get_doc(':Print Settings', 'Print Settings')
|
||||
.enable_raw_printing == '1'
|
||||
) {
|
||||
if (this.print_settings.enable_raw_printing == '1') {
|
||||
this.page.add_menu_item(__('Raw Printing Setting'), () => {
|
||||
this.printer_setting_dialog();
|
||||
});
|
||||
|
|
@ -179,6 +176,12 @@ frappe.ui.form.PrintView = class {
|
|||
this.edit_print_format()
|
||||
);
|
||||
}
|
||||
|
||||
if (cint(this.print_settings.enable_print_server)) {
|
||||
this.page.add_menu_item(__('Select Network Printer'), () =>
|
||||
this.network_printer_setting_dialog()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
show(frm) {
|
||||
|
|
@ -460,72 +463,108 @@ frappe.ui.form.PrintView = class {
|
|||
|
||||
printit() {
|
||||
let me = this;
|
||||
frappe.call({
|
||||
method:
|
||||
'frappe.printing.doctype.print_settings.print_settings.is_print_server_enabled',
|
||||
callback: function(data) {
|
||||
if (data.message) {
|
||||
frappe.call({
|
||||
method: 'frappe.utils.print_format.print_by_server',
|
||||
args: {
|
||||
doctype: me.frm.doc.doctype,
|
||||
name: me.frm.doc.name,
|
||||
print_format: me.selected_format(),
|
||||
no_letterhead: me.with_letterhead(),
|
||||
letterhead: this.get_letterhead(),
|
||||
},
|
||||
callback: function() {},
|
||||
});
|
||||
} else if (me.get_mapped_printer().length === 1) {
|
||||
// printer is already mapped in localstorage (applies for both raw and pdf )
|
||||
if (me.is_raw_printing()) {
|
||||
me.get_raw_commands(function(out) {
|
||||
frappe.ui.form
|
||||
.qz_connect()
|
||||
.then(function() {
|
||||
let printer_map = me.get_mapped_printer()[0];
|
||||
let data = [out.raw_commands];
|
||||
let config = qz.configs.create(printer_map.printer);
|
||||
return qz.print(config, data);
|
||||
})
|
||||
.then(frappe.ui.form.qz_success)
|
||||
.catch((err) => {
|
||||
frappe.ui.form.qz_fail(err);
|
||||
});
|
||||
|
||||
if (cint(me.print_settings.enable_print_server)) {
|
||||
if (localStorage.getItem('network_printer')) {
|
||||
me.print_by_server();
|
||||
} else {
|
||||
me.network_printer_setting_dialog(() => me.print_by_server());
|
||||
}
|
||||
} else if (me.get_mapped_printer().length === 1) {
|
||||
// printer is already mapped in localstorage (applies for both raw and pdf )
|
||||
if (me.is_raw_printing()) {
|
||||
me.get_raw_commands(function(out) {
|
||||
frappe.ui.form
|
||||
.qz_connect()
|
||||
.then(function() {
|
||||
let printer_map = me.get_mapped_printer()[0];
|
||||
let data = [out.raw_commands];
|
||||
let config = qz.configs.create(printer_map.printer);
|
||||
return qz.print(config, data);
|
||||
})
|
||||
.then(frappe.ui.form.qz_success)
|
||||
.catch((err) => {
|
||||
frappe.ui.form.qz_fail(err);
|
||||
});
|
||||
} else {
|
||||
frappe.show_alert(
|
||||
});
|
||||
} else {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __('PDF printing via "Raw Print" is not supported.'),
|
||||
subtitle: __(
|
||||
'Please remove the printer mapping in Printer Settings and try again.'
|
||||
),
|
||||
indicator: 'info',
|
||||
},
|
||||
14
|
||||
);
|
||||
//Note: need to solve "Error: Cannot parse (FILE)<URL> as a PDF file" to enable qz pdf printing.
|
||||
}
|
||||
} else if (me.is_raw_printing()) {
|
||||
// printer not mapped in localstorage and the current print format is raw printing
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __('Printer mapping not set.'),
|
||||
subtitle: __(
|
||||
'Please set a printer mapping for this print format in the Printer Settings'
|
||||
),
|
||||
indicator: 'warning',
|
||||
},
|
||||
14
|
||||
);
|
||||
me.printer_setting_dialog();
|
||||
} else {
|
||||
me.render_page('/printview?', true);
|
||||
}
|
||||
}
|
||||
|
||||
print_by_server() {
|
||||
let me = this;
|
||||
if (localStorage.getItem('network_printer')) {
|
||||
frappe.call({
|
||||
method: 'frappe.utils.print_format.print_by_server',
|
||||
args: {
|
||||
doctype: me.frm.doc.doctype,
|
||||
name: me.frm.doc.name,
|
||||
printer_setting: localStorage.getItem('network_printer'),
|
||||
print_format: me.selected_format(),
|
||||
no_letterhead: me.with_letterhead(),
|
||||
letterhead: me.get_letterhead(),
|
||||
},
|
||||
callback: function() {},
|
||||
});
|
||||
}
|
||||
}
|
||||
network_printer_setting_dialog(callback) {
|
||||
frappe.call({
|
||||
method: 'frappe.printing.doctype.network_printer_settings.network_printer_settings.get_network_printer_settings',
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __('Select Network Printer'),
|
||||
fields: [
|
||||
{
|
||||
message: __('PDF printing via "Raw Print" is not supported.'),
|
||||
subtitle: __(
|
||||
'Please remove the printer mapping in Printer Settings and try again.'
|
||||
),
|
||||
indicator: 'info',
|
||||
},
|
||||
14
|
||||
);
|
||||
//Note: need to solve "Error: Cannot parse (FILE)<URL> as a PDF file" to enable qz pdf printing.
|
||||
}
|
||||
} else if (me.is_raw_printing()) {
|
||||
// printer not mapped in localstorage and the current print format is raw printing
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __('Printer mapping not set.'),
|
||||
subtitle: __(
|
||||
'Please set a printer mapping for this print format in the Printer Settings'
|
||||
),
|
||||
indicator: 'warning',
|
||||
"label": "Printer",
|
||||
"fieldname": "printer",
|
||||
"fieldtype": "Select",
|
||||
"reqd": 1,
|
||||
"options": r.message
|
||||
}
|
||||
],
|
||||
primary_action: function() {
|
||||
localStorage.setItem('network_printer', d.get_values().printer);
|
||||
if (typeof callback == "function") {
|
||||
callback();
|
||||
}
|
||||
d.hide();
|
||||
},
|
||||
14
|
||||
);
|
||||
me.printer_setting_dialog();
|
||||
} else {
|
||||
me.render_page('/printview?', true);
|
||||
primary_action_label: __('Select')
|
||||
});
|
||||
d.show();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render_page(method, printit = false) {
|
||||
let w = window.open(
|
||||
frappe.urllib.get_full_url(
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
|
|||
} else if(f.fieldtype==="Column Break") {
|
||||
set_column();
|
||||
|
||||
} else if(!in_list(["Section Break", "Column Break", "Fold"], f.fieldtype)
|
||||
} else if (!in_list(["Section Break", "Column Break", "Tab Break", "Fold"], f.fieldtype)
|
||||
&& f.label) {
|
||||
if(!column) set_column();
|
||||
|
||||
|
|
@ -298,7 +298,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
|
|||
init_visible_columns(f) {
|
||||
f.visible_columns = []
|
||||
$.each(frappe.get_meta(f.options).fields, function(i, _f) {
|
||||
if(!in_list(["Section Break", "Column Break"], _f.fieldtype) &&
|
||||
if (!in_list(["Section Break", "Column Break", "Tab Break"], _f.fieldtype) &&
|
||||
!_f.print_hide && f.label) {
|
||||
|
||||
// column names set as fieldname|width
|
||||
|
|
@ -606,7 +606,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
|
|||
// add remaining fields
|
||||
$.each(doc_fields, function(j, f) {
|
||||
if (f && !in_list(column_names, f.fieldname)
|
||||
&& !in_list(["Section Break", "Column Break"], f.fieldtype) && f.label) {
|
||||
&& !in_list(["Section Break", "Column Break", "Tab Break"], f.fieldtype) && f.label) {
|
||||
fields.push(f);
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
</div>
|
||||
<div class="print-format-builder-sidebar-fields">
|
||||
{% for (var i=0, l=fields.length; i < l; i++) { var f = fields[i]; %}
|
||||
{% if(!in_list(["Section Break", "Column Break", "Fold"], f.fieldtype)) { %}
|
||||
{% if(!in_list(["Section Break", "Tab Break", "Column Break", "Fold"], f.fieldtype)) { %}
|
||||
<div class="print-format-builder-field-placeholder"
|
||||
data-fieldname="{%= f.fieldname %}">
|
||||
<div title="{{f.label}}" class="field-label btn btn-default btn-sm sidebar-field ellipsis
|
||||
|
|
|
|||
3
frappe/public/icons/timeless/message.svg
Normal file
3
frappe/public/icons/timeless/message.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.66659 9.77761C2.66659 10.0919 2.79145 10.3934 3.01372 10.6157C3.23598 10.8379 3.53744 10.9628 3.85177 10.9628H10.9629L13.3333 13.3332V3.85169C13.3333 3.53736 13.2084 3.2359 12.9861 3.01364C12.7639 2.79137 12.4624 2.6665 12.1481 2.6665H3.85177C3.53744 2.6665 3.23598 2.79137 3.01372 3.01364C2.79145 3.2359 2.66659 3.53736 2.66659 3.85169V9.77761Z" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 529 B |
3
frappe/public/icons/timeless/search.svg
Normal file
3
frappe/public/icons/timeless/search.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 7.44462C3.5 5.26607 5.26607 3.5 7.44462 3.5C9.62318 3.5 11.3892 5.26607 11.3892 7.44462C11.3892 8.50829 10.9683 9.47362 10.2838 10.1831C10.265 10.1972 10.247 10.2128 10.2299 10.2299C10.2128 10.247 10.1972 10.265 10.1831 10.2838C9.47362 10.9683 8.50829 11.3892 7.44462 11.3892C5.26607 11.3892 3.5 9.62318 3.5 7.44462ZM10.5696 11.2767C9.71788 11.9722 8.62996 12.3892 7.44462 12.3892C4.71378 12.3892 2.5 10.1755 2.5 7.44462C2.5 4.71378 4.71378 2.5 7.44462 2.5C10.1755 2.5 12.3892 4.71378 12.3892 7.44462C12.3892 8.62996 11.9722 9.71788 11.2767 10.5696L13.3538 12.6467C13.549 12.8419 13.549 13.1585 13.3538 13.3538C13.1585 13.549 12.8419 13.549 12.6467 13.3538L10.5696 11.2767Z" fill="#4C5A67"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 853 B |
|
|
@ -30,6 +30,9 @@ import "./frappe/ui/slides.js";
|
|||
import "./frappe/ui/find.js";
|
||||
import "./frappe/ui/iconbar.js";
|
||||
import "./frappe/form/layout.js";
|
||||
import "./frappe/form/section.js";
|
||||
import "./frappe/form/tab.js";
|
||||
import "./frappe/form/column.js";
|
||||
import "./frappe/ui/field_group.js";
|
||||
import "./frappe/form/link_selector.js";
|
||||
import "./frappe/form/multi_select_dialog.js";
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import "./frappe/utils/common.js";
|
|||
import "./frappe/ui/messages.js";
|
||||
import "./frappe/translate.js";
|
||||
import "./frappe/utils/pretty_date.js";
|
||||
import "./frappe/utils/datetime.js";
|
||||
import "./frappe/microtemplate.js";
|
||||
import "./frappe/query_string.js";
|
||||
|
||||
|
|
|
|||
49
frappe/public/js/frappe/form/column.js
Normal file
49
frappe/public/js/frappe/form/column.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
export default class Column {
|
||||
constructor(section, df) {
|
||||
if (!df) df = {};
|
||||
|
||||
this.df = df;
|
||||
this.section = section;
|
||||
this.make();
|
||||
this.resize_all_columns();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.wrapper = $(`
|
||||
<div class="form-column">
|
||||
<form>
|
||||
</form>
|
||||
</div>
|
||||
`)
|
||||
.appendTo(this.section.body)
|
||||
.find("form")
|
||||
.on("submit", function () {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (this.df.label) {
|
||||
$(`
|
||||
<label class="control-label">
|
||||
${__(this.df.label)}
|
||||
</label>
|
||||
`)
|
||||
.appendTo(this.wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
resize_all_columns() {
|
||||
// distribute all columns equally
|
||||
let colspan = cint(12 / this.section.wrapper.find(".form-column").length);
|
||||
|
||||
this.section.wrapper
|
||||
.find(".form-column")
|
||||
.removeClass()
|
||||
.addClass("form-column")
|
||||
.addClass("col-sm-" + colspan);
|
||||
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.section.refresh();
|
||||
}
|
||||
}
|
||||
|
|
@ -97,7 +97,7 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for
|
|||
}, 300));
|
||||
|
||||
$(this.quill.root).on('keydown', (e) => {
|
||||
const key = frappe.ui.keys.get_key(e);
|
||||
const key = frappe.ui.keys && frappe.ui.keys.get_key(e);
|
||||
if (['ctrl+b', 'meta+b'].includes(key)) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,61 +1,65 @@
|
|||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// MIT License. See license.txt
|
||||
|
||||
import Section from "./section.js";
|
||||
|
||||
frappe.ui.form.Dashboard = class FormDashboard {
|
||||
constructor(opts) {
|
||||
$.extend(this, opts);
|
||||
constructor(parent, frm) {
|
||||
this.parent = parent;
|
||||
this.frm = frm;
|
||||
this.setup_dashboard_sections();
|
||||
}
|
||||
|
||||
setup_dashboard_sections() {
|
||||
this.progress_area = new Section(this.parent, {
|
||||
this.progress_area = this.make_section({
|
||||
css_class: 'progress-area',
|
||||
hidden: 1,
|
||||
collapsible: 1
|
||||
is_dashboard_section: 1,
|
||||
});
|
||||
|
||||
this.heatmap_area = new Section(this.parent, {
|
||||
title: __("Overview"),
|
||||
this.heatmap_area = this.make_section({
|
||||
label: __("Overview"),
|
||||
css_class: 'form-heatmap',
|
||||
hidden: 1,
|
||||
collapsible: 1,
|
||||
is_dashboard_section: 1,
|
||||
body_html: `
|
||||
<div id="heatmap-${frappe.model.scrub(this.frm.doctype)}" class="heatmap"></div>
|
||||
<div class="text-muted small heatmap-message hidden"></div>
|
||||
`
|
||||
});
|
||||
|
||||
this.chart_area = new Section(this.parent, {
|
||||
title: __("Graph"),
|
||||
this.chart_area = this.make_section({
|
||||
label: __("Graph"),
|
||||
css_class: 'form-graph',
|
||||
hidden: 1,
|
||||
collapsible: 1
|
||||
is_dashboard_section: 1
|
||||
});
|
||||
|
||||
this.stats_area_row = $(`<div class="row"></div>`);
|
||||
this.stats_area = new Section(this.parent, {
|
||||
title: __("Stats"),
|
||||
this.stats_area = this.make_section({
|
||||
label: __("Stats"),
|
||||
css_class: 'form-stats',
|
||||
hidden: 1,
|
||||
collapsible: 1,
|
||||
is_dashboard_section: 1,
|
||||
body_html: this.stats_area_row
|
||||
});
|
||||
|
||||
this.transactions_area = $(`<div class="transactions"></div`);
|
||||
this.links_area = new Section(this.parent, {
|
||||
title: __("Connections"),
|
||||
|
||||
this.links_area = this.make_section({
|
||||
label: __("Connections"),
|
||||
css_class: 'form-links',
|
||||
hidden: 1,
|
||||
collapsible: 1,
|
||||
is_dashboard_section: 1,
|
||||
body_html: this.transactions_area
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
make_section(df) {
|
||||
return new Section(this.parent, df);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.hide();
|
||||
|
||||
// clear progress
|
||||
this.progress_area.body.empty();
|
||||
this.progress_area.hide();
|
||||
|
|
@ -70,19 +74,19 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
|
||||
// clear custom
|
||||
this.parent.find('.custom').remove();
|
||||
this.hide();
|
||||
// this.hide();
|
||||
}
|
||||
|
||||
add_section(body_html, title=null, css_class="custom", hidden=false) {
|
||||
add_section(body_html, label=null, css_class="custom", hidden=false) {
|
||||
let options = {
|
||||
title,
|
||||
label,
|
||||
css_class,
|
||||
hidden,
|
||||
body_html,
|
||||
make_card: true,
|
||||
collapsible: 1
|
||||
is_dashboard_section: 1
|
||||
};
|
||||
return new Section(this.parent, options).body;
|
||||
return new Section(this.frm.layout.wrapper, options).body;
|
||||
}
|
||||
|
||||
add_progress(title, percent, message) {
|
||||
|
|
@ -154,7 +158,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
|
||||
make_progress_chart(title) {
|
||||
this.progress_area.show();
|
||||
var progress_chart = $('<div class="progress-chart" title="'+(title || '')+'"></div>')
|
||||
let progress_chart = $('<div class="progress-chart" title="'+(title || '')+'"></div>')
|
||||
.appendTo(this.progress_area.body);
|
||||
return progress_chart;
|
||||
}
|
||||
|
|
@ -169,7 +173,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
this.init_data();
|
||||
}
|
||||
|
||||
var show = false;
|
||||
let show = false;
|
||||
|
||||
if (this.data && ((this.data.transactions || []).length
|
||||
|| (this.data.reports || []).length)) {
|
||||
|
|
@ -197,11 +201,10 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
}
|
||||
|
||||
after_refresh() {
|
||||
var me = this;
|
||||
// show / hide new buttons (if allowed)
|
||||
this.links_area.body.find('.btn-new').each(function() {
|
||||
if (me.frm.can_create($(this).attr('data-doctype'))) {
|
||||
$(this).removeClass('hidden');
|
||||
this.links_area.body.find('.btn-new').each((i, el) => {
|
||||
if (this.frm.can_create($(this).attr('data-doctype'))) {
|
||||
$(el).removeClass('hidden');
|
||||
}
|
||||
});
|
||||
!this.frm.is_new() && this.set_open_count();
|
||||
|
|
@ -269,7 +272,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
}
|
||||
|
||||
render_links() {
|
||||
var me = this;
|
||||
let me = this;
|
||||
this.links_area.show();
|
||||
this.links_area.body.find('.btn-new').addClass('hidden');
|
||||
if (this.data_rendered) {
|
||||
|
|
@ -329,7 +332,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
|
||||
open_document_list($link, show_open) {
|
||||
// show document list with filters
|
||||
var doctype = $link.attr('data-doctype'),
|
||||
let doctype = $link.attr('data-doctype'),
|
||||
names = $link.attr('data-names') || [];
|
||||
|
||||
if (this.data.internal_links[doctype]) {
|
||||
|
|
@ -351,8 +354,8 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
get_document_filter(doctype) {
|
||||
// return the default filter for the given document
|
||||
// like {"customer": frm.doc.name}
|
||||
var filter = {};
|
||||
var fieldname = this.data.non_standard_fieldnames
|
||||
let filter = {};
|
||||
let fieldname = this.data.non_standard_fieldnames
|
||||
? (this.data.non_standard_fieldnames[doctype] || this.data.fieldname)
|
||||
: this.data.fieldname;
|
||||
|
||||
|
|
@ -371,7 +374,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
}
|
||||
|
||||
// list all items from the transaction list
|
||||
var items = [],
|
||||
let items = [],
|
||||
me = this;
|
||||
|
||||
this.data.transactions.forEach(function(group) {
|
||||
|
|
@ -380,7 +383,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
});
|
||||
});
|
||||
|
||||
var method = this.data.method || 'frappe.desk.notifications.get_open_count';
|
||||
let method = this.data.method || 'frappe.desk.notifications.get_open_count';
|
||||
frappe.call({
|
||||
type: "GET",
|
||||
method: method,
|
||||
|
|
@ -429,7 +432,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
}
|
||||
|
||||
set_badge_count(doctype, open_count, count, names) {
|
||||
var $link = $(this.transactions_area)
|
||||
let $link = $(this.transactions_area)
|
||||
.find('.document-link[data-doctype="'+doctype+'"]');
|
||||
|
||||
if (open_count) {
|
||||
|
|
@ -476,7 +479,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
this.heatmap_area.body.find('svg').css({'margin': 'auto'});
|
||||
|
||||
// message
|
||||
var heatmap_message = this.heatmap_area.body.find('.heatmap-message');
|
||||
let heatmap_message = this.heatmap_area.body.find('.heatmap-message');
|
||||
if (this.data.heatmap_message) {
|
||||
heatmap_message.removeClass('hidden').html(this.data.heatmap_message);
|
||||
} else {
|
||||
|
|
@ -491,9 +494,9 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
|
||||
|
||||
// set colspan
|
||||
var indicators = this.stats_area_row.find('.indicator-column');
|
||||
var n_indicators = indicators.length + 1;
|
||||
var colspan;
|
||||
let indicators = this.stats_area_row.find('.indicator-column');
|
||||
let n_indicators = indicators.length + 1;
|
||||
let colspan;
|
||||
if (n_indicators > 4) {
|
||||
colspan = 3;
|
||||
} else {
|
||||
|
|
@ -505,7 +508,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
indicators.removeClass().addClass('col-sm-'+colspan).addClass('indicator-column');
|
||||
}
|
||||
|
||||
var indicator = $('<div class="col-sm-'+colspan+' indicator-column"><span class="indicator '+color+'">'
|
||||
let indicator = $('<div class="col-sm-'+colspan+' indicator-column"><span class="indicator '+color+'">'
|
||||
+label+'</span></div>').appendTo(this.stats_area_row);
|
||||
|
||||
return indicator;
|
||||
|
|
@ -513,9 +516,9 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
|
||||
// graphs
|
||||
setup_graph() {
|
||||
var me = this;
|
||||
var method = this.data.graph_method;
|
||||
var args = {
|
||||
let me = this;
|
||||
let method = this.data.graph_method;
|
||||
let args = {
|
||||
doctype: this.frm.doctype,
|
||||
docname: this.frm.doc.name,
|
||||
};
|
||||
|
|
@ -579,11 +582,10 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
}
|
||||
|
||||
add_comment(text, alert_class, permanent) {
|
||||
var me = this;
|
||||
this.set_headline_alert(text, alert_class);
|
||||
if (!permanent) {
|
||||
setTimeout(function() {
|
||||
me.clear_headline();
|
||||
setTimeout(() => {
|
||||
this.clear_headline();
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
|
|
@ -600,109 +602,3 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
class Section {
|
||||
constructor(parent, options) {
|
||||
this.parent = parent;
|
||||
this.df = options || {};
|
||||
this.make();
|
||||
|
||||
if (this.df.title && this.df.collapsible && localStorage.getItem(options.css_class + '-closed')) {
|
||||
this.collapse();
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.wrapper = $(`<div class="form-dashboard-section ${ this.df.make_card ? "card-section" : "" }">`)
|
||||
.appendTo(this.parent);
|
||||
|
||||
if (this.df) {
|
||||
if (this.df.title) {
|
||||
this.make_head();
|
||||
}
|
||||
if (this.df.description) {
|
||||
this.description_wrapper = $(
|
||||
`<div class="col-sm-12 form-section-description">
|
||||
${__(this.df.description)}
|
||||
</div>`
|
||||
);
|
||||
|
||||
this.wrapper.append(this.description_wrapper);
|
||||
}
|
||||
if (this.df.css_class) {
|
||||
this.wrapper.addClass(this.df.css_class);
|
||||
}
|
||||
if (this.df.hide_border) {
|
||||
this.wrapper.toggleClass("hide-border", true);
|
||||
}
|
||||
}
|
||||
|
||||
this.body = $('<div class="section-body">').appendTo(this.wrapper);
|
||||
|
||||
if (this.df.body_html) {
|
||||
this.body.append(this.df.body_html);
|
||||
}
|
||||
}
|
||||
|
||||
make_head() {
|
||||
this.head = $(`
|
||||
<div class="section-head">
|
||||
${__(this.df.title)}
|
||||
<span class="ml-2 collapse-indicator mb-1"></span>
|
||||
</div>
|
||||
`);
|
||||
|
||||
this.head.appendTo(this.wrapper);
|
||||
this.indicator = this.head.find('.collapse-indicator');
|
||||
this.indicator.hide();
|
||||
|
||||
if (this.df.collapsible) {
|
||||
// show / hide based on status
|
||||
this.collapse_link = this.head.on("click", () => {
|
||||
this.collapse();
|
||||
});
|
||||
this.set_icon();
|
||||
this.indicator.show();
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (!this.df) return;
|
||||
|
||||
// hide if explicitly hidden
|
||||
let hide = this.df.hidden;
|
||||
this.wrapper.toggle(!hide);
|
||||
}
|
||||
|
||||
collapse(hide) {
|
||||
if (hide === undefined) {
|
||||
hide = !this.body.hasClass("hide");
|
||||
}
|
||||
|
||||
this.body.toggleClass("hide", hide);
|
||||
this.head && this.head.toggleClass("collapsed", hide);
|
||||
|
||||
this.set_icon(hide);
|
||||
|
||||
// save state for next reload ('' is falsy)
|
||||
localStorage.setItem(this.df.css_class + '-closed', hide ? '1' : '');
|
||||
}
|
||||
|
||||
set_icon(hide) {
|
||||
let indicator_icon = hide ? 'down' : 'up-line';
|
||||
this.indicator && this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1'));
|
||||
}
|
||||
|
||||
is_collapsed() {
|
||||
return this.body.hasClass('hide');
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.wrapper.hide();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.wrapper.show();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,9 +97,13 @@ class BaseTimeline {
|
|||
}
|
||||
|
||||
timeline_item.append(`<div class="timeline-content ${item.is_card ? 'frappe-card' : ''}">`);
|
||||
timeline_item.find('.timeline-content').append(item.content);
|
||||
let timeline_content = timeline_item.find('.timeline-content');
|
||||
timeline_content.append(item.content);
|
||||
if (!item.hide_timestamp && !item.is_card) {
|
||||
timeline_item.find('.timeline-content').append(`<span> - ${comment_when(item.creation)}</span>`);
|
||||
timeline_content.append(`<span> - ${comment_when(item.creation)}</span>`);
|
||||
}
|
||||
if (item.id) {
|
||||
timeline_content.attr("id", item.id);
|
||||
}
|
||||
return timeline_item;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ class FormTimeline extends BaseTimeline {
|
|||
render_timeline_items() {
|
||||
super.render_timeline_items();
|
||||
this.set_document_info();
|
||||
frappe.utils.bind_actions_with_object(this.timeline_items_wrapper, this);
|
||||
}
|
||||
|
||||
set_document_info() {
|
||||
|
|
@ -179,6 +180,7 @@ class FormTimeline extends BaseTimeline {
|
|||
is_card: true,
|
||||
content: this.get_communication_timeline_content(communication),
|
||||
doctype: "Communication",
|
||||
id: `communication-${communication.name}`,
|
||||
name: communication.name
|
||||
});
|
||||
});
|
||||
|
|
@ -246,6 +248,7 @@ class FormTimeline extends BaseTimeline {
|
|||
creation: comment.creation,
|
||||
is_card: true,
|
||||
doctype: "Comment",
|
||||
id: `comment-${comment.name}`,
|
||||
name: comment.name,
|
||||
content: this.get_comment_timeline_content(comment),
|
||||
};
|
||||
|
|
@ -394,7 +397,7 @@ class FormTimeline extends BaseTimeline {
|
|||
}
|
||||
|
||||
setup_reply(communication_box, communication_doc) {
|
||||
let actions = communication_box.find('.actions');
|
||||
let actions = communication_box.find('.custom-actions');
|
||||
let reply = $(`<a class="action-btn reply">${frappe.utils.icon('reply', 'md')}</a>`).click(() => {
|
||||
this.compose_mail(communication_doc);
|
||||
});
|
||||
|
|
@ -446,14 +449,16 @@ class FormTimeline extends BaseTimeline {
|
|||
let edit_wrapper = $(`<div class="comment-edit-box">`).hide();
|
||||
let edit_box = this.make_editable(edit_wrapper);
|
||||
let content_wrapper = comment_wrapper.find('.content');
|
||||
|
||||
let delete_button = $();
|
||||
let more_actions_wrapper = comment_wrapper.find('.more-actions');
|
||||
if (frappe.model.can_delete("Comment")) {
|
||||
delete_button = $(`
|
||||
<button class="btn btn-link action-btn">
|
||||
${frappe.utils.icon('close', 'sm')}
|
||||
</button>
|
||||
const delete_option = $(`
|
||||
<li>
|
||||
<a class="dropdown-item">
|
||||
${__("Delete")}
|
||||
</a>
|
||||
</li>
|
||||
`).click(() => this.delete_comment(doc.name));
|
||||
more_actions_wrapper.find('.dropdown-menu').append(delete_option);
|
||||
}
|
||||
|
||||
let dismiss_button = $(`
|
||||
|
|
@ -493,15 +498,14 @@ class FormTimeline extends BaseTimeline {
|
|||
edit_button.toggle_edit_mode = () => {
|
||||
edit_button.edit_mode = !edit_button.edit_mode;
|
||||
edit_button.text(edit_button.edit_mode ? __('Save') : __('Edit'));
|
||||
delete_button.toggle(!edit_button.edit_mode);
|
||||
more_actions_wrapper.toggle(!edit_button.edit_mode);
|
||||
dismiss_button.toggle(edit_button.edit_mode);
|
||||
edit_wrapper.toggle(edit_button.edit_mode);
|
||||
content_wrapper.toggle(!edit_button.edit_mode);
|
||||
};
|
||||
|
||||
comment_wrapper.find('.actions').append(edit_button);
|
||||
comment_wrapper.find('.actions').append(dismiss_button);
|
||||
comment_wrapper.find('.actions').append(delete_button);
|
||||
let actions_wrapper = comment_wrapper.find('.custom-actions');
|
||||
actions_wrapper.append(edit_button);
|
||||
actions_wrapper.append(dismiss_button);
|
||||
}
|
||||
|
||||
make_editable(container) {
|
||||
|
|
@ -559,6 +563,14 @@ class FormTimeline extends BaseTimeline {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
copy_link(ev) {
|
||||
let doc_link = frappe.urllib.get_full_url(
|
||||
frappe.utils.get_form_link(this.frm.doctype, this.frm.docname)
|
||||
);
|
||||
let element_id = $(ev.currentTarget).closest(".timeline-content").attr("id");
|
||||
frappe.utils.copy_to_clipboard(`${doc_link}#${element_id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default FormTimeline;
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
this.watch_model_updates();
|
||||
|
||||
if (!this.meta.hide_toolbar && frappe.boot.desk_settings.timeline) {
|
||||
// this.footer_tab = new frappe.ui.form.Tab(this.layout, {
|
||||
// label: __("Activity"),
|
||||
// fieldname: 'timeline'
|
||||
// });
|
||||
|
||||
this.footer = new frappe.ui.form.Footer({
|
||||
frm: this,
|
||||
parent: $('<div>').appendTo(this.page.main.parent())
|
||||
|
|
@ -128,8 +133,8 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
|
||||
setup_std_layout() {
|
||||
this.form_wrapper = $('<div></div>').appendTo(this.layout_main);
|
||||
this.body = $('<div></div>').appendTo(this.form_wrapper);
|
||||
this.form_wrapper = $('<div></div>').appendTo(this.layout_main);
|
||||
this.body = $('<div></div>').appendTo(this.form_wrapper);
|
||||
|
||||
// only tray
|
||||
this.meta.section_style='Simple'; // always simple!
|
||||
|
|
@ -141,17 +146,19 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
doctype_layout: this.doctype_layout,
|
||||
frm: this,
|
||||
with_dashboard: true,
|
||||
card_layout: true,
|
||||
card_layout: true
|
||||
});
|
||||
|
||||
this.layout.make();
|
||||
|
||||
this.fields_dict = this.layout.fields_dict;
|
||||
this.fields = this.layout.fields_list;
|
||||
|
||||
this.dashboard = new frappe.ui.form.Dashboard({
|
||||
frm: this,
|
||||
parent: $('<div class="form-dashboard">').insertAfter(this.layout.wrapper.find('.form-message'))
|
||||
});
|
||||
let dashboard_parent = $('<div class="form-dashboard">');
|
||||
|
||||
let main_page = this.layout.tabs.length ? this.layout.tabs[0].wrapper : this.layout.wrapper;
|
||||
main_page.prepend(dashboard_parent);
|
||||
this.dashboard = new frappe.ui.form.Dashboard(dashboard_parent, this);
|
||||
|
||||
this.tour = new frappe.ui.form.FormTour({
|
||||
frm: this
|
||||
|
|
@ -181,8 +188,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
|
||||
me.layout.refresh_dependency();
|
||||
me.layout.refresh_sections();
|
||||
let object = me.script_manager.trigger(fieldname, doc.doctype, doc.name);
|
||||
return object;
|
||||
return me.script_manager.trigger(fieldname, doc.doctype, doc.name);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -197,7 +203,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
if(doc.parent===me.docname && doc.parentfield===df.fieldname) {
|
||||
me.dirty();
|
||||
me.fields_dict[df.fieldname].grid.set_value(fieldname, value, doc);
|
||||
me.script_manager.trigger(fieldname, doc.doctype, doc.name);
|
||||
return me.script_manager.trigger(fieldname, doc.doctype, doc.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -459,7 +465,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
},
|
||||
() => this.cscript.is_onload && this.is_new() && this.focus_on_first_input(),
|
||||
() => this.run_after_load_hook(),
|
||||
() => this.dashboard.after_refresh()
|
||||
() => this.dashboard.after_refresh(),
|
||||
]);
|
||||
|
||||
} else {
|
||||
|
|
@ -468,11 +474,22 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
|
||||
this.$wrapper.trigger('render_complete');
|
||||
|
||||
this.cscript.is_onload && this.set_first_tab_as_active();
|
||||
|
||||
if(!this.hidden) {
|
||||
this.layout.show_empty_form_message();
|
||||
}
|
||||
|
||||
this.scroll_to_element();
|
||||
frappe.after_ajax(() => {
|
||||
$(document).ready(() => {
|
||||
this.scroll_to_element();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
set_first_tab_as_active() {
|
||||
this.layout.tabs[0]
|
||||
&& this.layout.tabs[0].set_active();
|
||||
}
|
||||
|
||||
focus_on_first_input() {
|
||||
|
|
@ -585,6 +602,8 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
this.validate_form_action(save_action, resolve);
|
||||
|
||||
var after_save = function(r) {
|
||||
// to remove hash from URL to avoid scroll after save
|
||||
history.replaceState(null, null, ' ');
|
||||
if(!r.exc) {
|
||||
if (["Save", "Update", "Amend"].indexOf(save_action)!==-1) {
|
||||
frappe.utils.play_sound("click");
|
||||
|
|
@ -1182,6 +1201,8 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
if (selector.length) {
|
||||
frappe.utils.scroll_to(selector);
|
||||
}
|
||||
} else if (window.location.hash && $(window.location.hash).length) {
|
||||
frappe.utils.scroll_to(window.location.hash, true, 200, null, null, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1605,6 +1626,11 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
|
||||
let $el = field.$wrapper;
|
||||
|
||||
// set tab as active
|
||||
if (field.tab && !field.tab.is_active()) {
|
||||
field.tab.set_active();
|
||||
}
|
||||
|
||||
// uncollapse section
|
||||
if (field.section.is_collapsed()) {
|
||||
field.section.collapse(false);
|
||||
|
|
|
|||
|
|
@ -212,13 +212,12 @@ export default class Grid {
|
|||
|
||||
delete_all_rows() {
|
||||
frappe.confirm(__("Are you sure you want to delete all rows?"), () => {
|
||||
this.grid_rows.forEach(row => {
|
||||
row.remove();
|
||||
});
|
||||
this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
|
||||
|
||||
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').prop('checked', 0);
|
||||
this.frm.doc[this.df.fieldname] = [];
|
||||
$(this.parent).find('.rows').empty();
|
||||
this.grid_rows = [];
|
||||
this.refresh();
|
||||
this.frm && this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
|
||||
this.frm && this.frm.dirty();
|
||||
this.scroll_to_top();
|
||||
});
|
||||
}
|
||||
|
|
@ -244,8 +243,10 @@ export default class Grid {
|
|||
|
||||
this.remove_rows_button.toggleClass('hidden',
|
||||
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true);
|
||||
this.remove_all_rows_button.toggleClass('hidden',
|
||||
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').length ? false : true);
|
||||
|
||||
let select_all_checkbox_checked = this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').length;
|
||||
let show_delete_all_btn = select_all_checkbox_checked && this.data.length > this.get_selected_children().length;
|
||||
this.remove_all_rows_button.toggleClass('hidden', !show_delete_all_btn);
|
||||
}
|
||||
|
||||
get_selected() {
|
||||
|
|
@ -264,6 +265,14 @@ export default class Grid {
|
|||
});
|
||||
}
|
||||
|
||||
reset_grid() {
|
||||
this.visible_columns = [];
|
||||
this.grid_rows = [];
|
||||
|
||||
$(this.parent).find(".grid-body .grid-row").remove();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
make_head() {
|
||||
// labels
|
||||
if (this.header_row) {
|
||||
|
|
@ -274,7 +283,8 @@ export default class Grid {
|
|||
parent_df: this.df,
|
||||
docfields: this.docfields,
|
||||
frm: this.frm,
|
||||
grid: this
|
||||
grid: this,
|
||||
configure_columns: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -685,10 +695,13 @@ export default class Grid {
|
|||
}
|
||||
|
||||
setup_visible_columns() {
|
||||
if (this.visible_columns) return;
|
||||
if (this.visible_columns && this.visible_columns.length > 0) return;
|
||||
|
||||
this.user_defined_columns = [];
|
||||
this.setup_user_defined_columns();
|
||||
var total_colsize = 1,
|
||||
fields = this.editable_fields || this.docfields;
|
||||
fields = (this.user_defined_columns && this.user_defined_columns.length > 0)
|
||||
? this.user_defined_columns : this.editable_fields || this.docfields;
|
||||
|
||||
this.visible_columns = [];
|
||||
|
||||
|
|
@ -696,9 +709,9 @@ export default class Grid {
|
|||
var _df = fields[ci];
|
||||
|
||||
// get docfield if from fieldname
|
||||
df = this.fields_map[_df.fieldname];
|
||||
df = (this.user_defined_columns && this.user_defined_columns.length > 0) ? _df : this.fields_map[_df.fieldname];
|
||||
|
||||
if (!df.hidden
|
||||
if (df && !df.hidden
|
||||
&& (this.editable_fields || df.in_list_view)
|
||||
&& (this.frm && this.frm.get_perm(df.permlevel, "read") || !this.frm)
|
||||
&& !in_list(frappe.model.layout_fields, df.fieldtype)) {
|
||||
|
|
@ -706,13 +719,7 @@ export default class Grid {
|
|||
if (df.columns) {
|
||||
df.colsize = df.columns;
|
||||
} else {
|
||||
var colsize = 2;
|
||||
switch (df.fieldtype) {
|
||||
case "Text": break;
|
||||
case "Small Text": colsize = 3; break;
|
||||
case "Check": colsize = 1;
|
||||
}
|
||||
df.colsize = colsize;
|
||||
this.update_default_colsize(df);
|
||||
}
|
||||
|
||||
// attach formatter on refresh
|
||||
|
|
@ -755,6 +762,31 @@ export default class Grid {
|
|||
}
|
||||
}
|
||||
|
||||
update_default_colsize(df) {
|
||||
var colsize = 2;
|
||||
switch (df.fieldtype) {
|
||||
case "Text": break;
|
||||
case "Small Text": colsize = 3; break;
|
||||
case "Check": colsize = 1;
|
||||
}
|
||||
df.colsize = colsize;
|
||||
}
|
||||
|
||||
setup_user_defined_columns() {
|
||||
if (this.frm) {
|
||||
let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView');
|
||||
if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) {
|
||||
this.user_defined_columns = user_settings[this.doctype].map(row => {
|
||||
let column = frappe.meta.get_docfield(this.doctype, row.fieldname);
|
||||
if (column) {
|
||||
column.in_list_view = 1;
|
||||
column.columns = row.columns;
|
||||
return column;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is_editable() {
|
||||
return this.display_status == "Write" && !this.static_rows;
|
||||
|
|
@ -835,10 +867,11 @@ export default class Grid {
|
|||
$.each(row, (ci, value) => {
|
||||
var fieldname = fieldnames[ci];
|
||||
var df = frappe.meta.get_docfield(me.df.options, fieldname);
|
||||
|
||||
d[fieldnames[ci]] = value_formatter_map[df.fieldtype]
|
||||
? value_formatter_map[df.fieldtype](value)
|
||||
: value;
|
||||
if (df) {
|
||||
d[fieldnames[ci]] = value_formatter_map[df.fieldtype]
|
||||
? value_formatter_map[df.fieldtype](value)
|
||||
: value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ export default class GridRow {
|
|||
|
||||
this.setup_columns();
|
||||
this.add_open_form_button();
|
||||
this.add_column_configure_button();
|
||||
this.refresh_check();
|
||||
|
||||
if(this.frm && this.doc) {
|
||||
|
|
@ -250,10 +251,269 @@ export default class GridRow {
|
|||
}
|
||||
}
|
||||
|
||||
add_column_configure_button() {
|
||||
if (this.configure_columns) {
|
||||
this.configure_columns_button = $(`
|
||||
<div class="col grid-static-col col-xs-1 d-flex justify-content-center" style="cursor: pointer;">
|
||||
<a>${frappe.utils.icon('setting-gear', 'sm', '', 'filter: opacity(0.5)')}</a>
|
||||
</div>
|
||||
`)
|
||||
.appendTo(this.row)
|
||||
.on('click', () => {
|
||||
this.configure_dialog_for_columns_selector();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
configure_dialog_for_columns_selector() {
|
||||
this.grid_settings_dialog = new frappe.ui.Dialog({
|
||||
title: __("Configure Columns"),
|
||||
fields: [{
|
||||
'fieldtype': 'HTML',
|
||||
'fieldname': 'fields_html'
|
||||
}]
|
||||
});
|
||||
|
||||
this.grid.setup_visible_columns();
|
||||
this.setup_columns_for_dialog();
|
||||
this.prepare_wrapper_for_columns();
|
||||
this.render_selected_columns();
|
||||
this.grid_settings_dialog.show();
|
||||
|
||||
$(this.fields_html_wrapper).find('.add-new-fields').click(() => {
|
||||
this.column_selector_for_dialog();
|
||||
});
|
||||
|
||||
this.grid_settings_dialog.set_primary_action(__('Update'), () => {
|
||||
this.validate_columns_width();
|
||||
this.columns = {};
|
||||
this.update_user_settings_for_grid();
|
||||
this.grid_settings_dialog.hide();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
setup_columns_for_dialog() {
|
||||
this.selected_columns_for_grid = [];
|
||||
this.grid.visible_columns.forEach(row => {
|
||||
this.selected_columns_for_grid.push({
|
||||
fieldname: row[0].fieldname,
|
||||
columns: row[0].columns || row[0].colsize
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
prepare_wrapper_for_columns() {
|
||||
this.fields_html_wrapper = this.grid_settings_dialog.get_field("fields_html").$wrapper[0];
|
||||
|
||||
$(`
|
||||
<div class='form-group'>
|
||||
<div class='row' style='margin:0px; margin-bottom:10px'>
|
||||
<div class='col-md-8'>
|
||||
${__('Fieldname').bold()}
|
||||
</div>
|
||||
<div class='col-md-4' style='padding-left:5px;'>
|
||||
${__('Column Width').bold()}
|
||||
</div>
|
||||
</div>
|
||||
<div class='control-input-wrapper selected-fields'>
|
||||
</div>
|
||||
<p class='help-box small text-muted hidden-xs'>
|
||||
<a class='add-new-fields text-muted'>
|
||||
+ Add / Remove Columns
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
`).appendTo(this.fields_html_wrapper);
|
||||
}
|
||||
|
||||
column_selector_for_dialog() {
|
||||
let docfields = this.prepare_columns_for_dialog(this.selected_columns_for_grid.map(field => field.fieldname));
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("{0} Fields", [__(this.grid.doctype)]),
|
||||
fields: [
|
||||
{
|
||||
label: __("Select Fields"),
|
||||
fieldtype: "MultiCheck",
|
||||
fieldname: "fields",
|
||||
options: docfields,
|
||||
columns: 2
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
d.set_primary_action(__('Add'), () => {
|
||||
let selected_fields = d.get_values().fields;
|
||||
this.selected_columns_for_grid = [];
|
||||
if (selected_fields) {
|
||||
selected_fields.forEach(selected_column => {
|
||||
let docfield = frappe.meta.get_docfield(this.grid.doctype, selected_column);
|
||||
this.grid.update_default_colsize(docfield);
|
||||
|
||||
this.selected_columns_for_grid.push({
|
||||
fieldname: selected_column,
|
||||
columns: docfield.columns || docfield.colsize
|
||||
});
|
||||
});
|
||||
|
||||
this.render_selected_columns();
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
|
||||
d.show();
|
||||
}
|
||||
|
||||
prepare_columns_for_dialog(selected_fields) {
|
||||
let fields = [];
|
||||
|
||||
this.docfields.forEach(column => {
|
||||
if (!column.hidden && !in_list(frappe.model.no_value_type, column.fieldtype)) {
|
||||
fields.push({
|
||||
label: column.label,
|
||||
value: column.fieldname,
|
||||
checked: selected_fields ? in_list(selected_fields, column.fieldname) : false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
render_selected_columns() {
|
||||
let fields = '';
|
||||
if (this.selected_columns_for_grid) {
|
||||
this.selected_columns_for_grid.forEach(d => {
|
||||
let docfield = frappe.meta.get_docfield(this.grid.doctype, d.fieldname);
|
||||
|
||||
fields += `
|
||||
<div class='control-input flex align-center form-control fields_order sortable-handle sortable'
|
||||
style='display: block; margin-bottom: 5px; cursor: pointer;' data-fieldname='${docfield.fieldname}'
|
||||
data-label='${docfield.label}' data-type='${docfield.fieldtype}'>
|
||||
|
||||
<div class='row'>
|
||||
<div class='col-md-1'>
|
||||
<a style='cursor: grabbing;'>${frappe.utils.icon('drag', 'xs')}</a>
|
||||
</div>
|
||||
<div class='col-md-7' style='padding-left:0px;'>
|
||||
${docfield.label}
|
||||
</div>
|
||||
<div class='col-md-3' style='padding-left:0px;margin-top:-2px;' title='${__('Columns')}'>
|
||||
<input class='form-control column-width input-xs text-right'
|
||||
value='${docfield.columns || cint(d.columns)}'
|
||||
data-fieldname='${docfield.fieldname}' style='background-color: #ffff; display: inline'>
|
||||
</div>
|
||||
<div class='col-md-1'>
|
||||
<a class='text-muted remove-field' data-fieldname='${docfield.fieldname}'>
|
||||
<i class='fa fa-trash-o' aria-hidden='true'></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
$(this.fields_html_wrapper).find('.selected-fields').html(fields);
|
||||
|
||||
this.prepare_handler_for_sort();
|
||||
this.select_on_focus();
|
||||
this.update_column_width();
|
||||
this.remove_selected_column();
|
||||
}
|
||||
|
||||
prepare_handler_for_sort() {
|
||||
new Sortable($(this.fields_html_wrapper).find('.selected-fields')[0], {
|
||||
handle: '.sortable-handle',
|
||||
draggable: '.sortable',
|
||||
onUpdate: () => {
|
||||
this.sort_columns();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sort_columns() {
|
||||
this.selected_columns_for_grid = [];
|
||||
|
||||
let columns = $(this.fields_html_wrapper).find('.fields_order') || [];
|
||||
columns.each(idx => {
|
||||
this.selected_columns_for_grid.push({
|
||||
fieldname: $(columns[idx]).attr('data-fieldname'),
|
||||
columns: cint($(columns[idx]).find('.column-width').attr('value'))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
select_on_focus() {
|
||||
$(this.fields_html_wrapper).find('.column-width').click((event) => {
|
||||
$(event.target).select();
|
||||
});
|
||||
}
|
||||
|
||||
update_column_width() {
|
||||
$(this.fields_html_wrapper).find('.column-width').change((event) => {
|
||||
if (cint(event.target.value) === 0) {
|
||||
event.target.value = cint(event.target.defaultValue);
|
||||
frappe.throw(__('Column width cannot be zero.'));
|
||||
}
|
||||
|
||||
this.selected_columns_for_grid.forEach(row => {
|
||||
if (row.fieldname === event.target.dataset.fieldname) {
|
||||
row.columns = cint(event.target.value);
|
||||
event.target.defaultValue = cint(event.target.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
validate_columns_width() {
|
||||
let total_column_width = 0.0;
|
||||
|
||||
this.selected_columns_for_grid.forEach(row => {
|
||||
if (row.columns && row.columns > 0) {
|
||||
total_column_width += cint(row.columns);
|
||||
}
|
||||
});
|
||||
|
||||
if (total_column_width && total_column_width > 10) {
|
||||
frappe.throw(__('The total column width cannot be more than 10.'));
|
||||
}
|
||||
}
|
||||
|
||||
remove_selected_column() {
|
||||
$(this.fields_html_wrapper).find('.remove-field').click((event) => {
|
||||
let fieldname = event.currentTarget.dataset.fieldname;
|
||||
let selected_columns_for_grid = this.selected_columns_for_grid.filter(row => {
|
||||
return (row.fieldname !== fieldname);
|
||||
});
|
||||
|
||||
if (selected_columns_for_grid && selected_columns_for_grid.length === 0) {
|
||||
frappe.throw(__('At least one column is required to show in the grid.'));
|
||||
}
|
||||
|
||||
this.selected_columns_for_grid = selected_columns_for_grid;
|
||||
$(this.fields_html_wrapper).find(`[data-fieldname="${fieldname}"]`).remove();
|
||||
});
|
||||
}
|
||||
|
||||
update_user_settings_for_grid() {
|
||||
if (!this.selected_columns_for_grid || !this.frm) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = {};
|
||||
value[this.grid.doctype] = this.selected_columns_for_grid;
|
||||
frappe.model.user_settings.save(this.frm.doctype, 'GridView', value)
|
||||
.then((r) => {
|
||||
frappe.model.user_settings[this.frm.doctype] = r.message || r;
|
||||
this.grid.reset_grid();
|
||||
});
|
||||
}
|
||||
|
||||
setup_columns() {
|
||||
this.focus_set = false;
|
||||
this.grid.setup_visible_columns();
|
||||
|
||||
this.grid.setup_visible_columns();
|
||||
this.grid.visible_columns.forEach((col, ci) => {
|
||||
// to get update df for the row
|
||||
let df = this.docfields.find(field => field.fieldname === col[0].fieldname);
|
||||
|
|
|
|||
|
|
@ -123,10 +123,12 @@ export default class GridRowForm {
|
|||
.toggle(this.row.grid.is_editable());
|
||||
}
|
||||
refresh_field(fieldname) {
|
||||
if(this.fields_dict[fieldname]) {
|
||||
this.fields_dict[fieldname].refresh();
|
||||
this.layout && this.layout.refresh_dependency();
|
||||
}
|
||||
const field = this.fields_dict[fieldname];
|
||||
if (!field) return;
|
||||
|
||||
field.docname = this.row.doc.name;
|
||||
field.refresh();
|
||||
this.layout && this.layout.refresh_dependency();
|
||||
}
|
||||
set_focus() {
|
||||
// wait for animation and then focus on the first row
|
||||
|
|
|
|||
|
|
@ -1,27 +1,50 @@
|
|||
import '../class';
|
||||
import Section from "./section.js";
|
||||
import Tab from "./tab.js";
|
||||
import Column from "./column.js";
|
||||
|
||||
frappe.ui.form.Layout = class Layout {
|
||||
constructor (opts) {
|
||||
this.views = {};
|
||||
this.pages = [];
|
||||
this.tabs = [];
|
||||
this.sections = [];
|
||||
this.fields_list = [];
|
||||
this.fields_dict = {};
|
||||
|
||||
$.extend(this, opts);
|
||||
}
|
||||
|
||||
make() {
|
||||
if (!this.parent && this.body) {
|
||||
this.parent = this.body;
|
||||
}
|
||||
this.wrapper = $('<div class="form-layout">').appendTo(this.parent);
|
||||
this.message = $('<div class="form-message hidden"></div>').appendTo(this.wrapper);
|
||||
this.page = $('<div class="form-page"></div>').appendTo(this.wrapper);
|
||||
|
||||
if (!this.fields) {
|
||||
this.fields = this.get_doctype_fields();
|
||||
}
|
||||
this.setup_tabbing();
|
||||
|
||||
if (this.is_tabbed_layout()) {
|
||||
this.setup_tabbed_layout();
|
||||
}
|
||||
|
||||
this.setup_tab_events();
|
||||
this.render();
|
||||
}
|
||||
|
||||
setup_tabbed_layout() {
|
||||
$(`
|
||||
<div class="form-tabs-list">
|
||||
<ul class="nav form-tabs" id="form-tabs" role="tablist"></ul>
|
||||
</div>
|
||||
`).appendTo(this.page);
|
||||
this.tabs_list = this.page.find('.form-tabs');
|
||||
this.tabs_content = $(`<div class="form-tab-content tab-content"></div>`).appendTo(this.page);
|
||||
this.setup_events();
|
||||
}
|
||||
|
||||
show_empty_form_message() {
|
||||
if (!(this.wrapper.find(".frappe-control:visible").length || this.wrapper.find(".section-head.collapsed").length)) {
|
||||
this.show_message(__("This form does not have any input"));
|
||||
|
|
@ -87,49 +110,58 @@ frappe.ui.form.Layout = class Layout {
|
|||
this.message.empty().addClass('hidden');
|
||||
}
|
||||
}
|
||||
render (new_fields) {
|
||||
var me = this;
|
||||
var fields = new_fields || this.fields;
|
||||
|
||||
render(new_fields) {
|
||||
let fields = new_fields || this.fields;
|
||||
|
||||
this.section = null;
|
||||
this.column = null;
|
||||
|
||||
if (this.with_dashboard) {
|
||||
this.setup_dashboard_section();
|
||||
if (this.no_opening_section() && !this.is_tabbed_layout()) {
|
||||
this.fields.unshift({fieldtype: 'Section Break'});
|
||||
}
|
||||
|
||||
if (this.no_opening_section()) {
|
||||
this.make_section();
|
||||
if (this.is_tabbed_layout()) {
|
||||
let default_tab = {label: __('Details'), fieldname: 'details', fieldtype: "Tab Break"};
|
||||
let first_tab = this.fields[1].fieldtype === "Tab Break" ? this.fields[1] : null;
|
||||
if (!first_tab) {
|
||||
this.fields.splice(1, 0, default_tab);
|
||||
}
|
||||
}
|
||||
$.each(fields, function (i, df) {
|
||||
|
||||
fields.forEach(df => {
|
||||
switch (df.fieldtype) {
|
||||
case "Fold":
|
||||
me.make_page(df);
|
||||
this.make_page(df);
|
||||
break;
|
||||
case "Section Break":
|
||||
me.make_section(df);
|
||||
this.make_section(df);
|
||||
break;
|
||||
case "Column Break":
|
||||
me.make_column(df);
|
||||
this.make_column(df);
|
||||
break;
|
||||
case "Tab Break":
|
||||
this.make_tab(df);
|
||||
break;
|
||||
default:
|
||||
me.make_field(df);
|
||||
this.make_field(df);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
no_opening_section () {
|
||||
no_opening_section() {
|
||||
return (this.fields[0] && this.fields[0].fieldtype != "Section Break") || !this.fields.length;
|
||||
}
|
||||
|
||||
setup_dashboard_section () {
|
||||
if (this.no_opening_section()) {
|
||||
this.fields.unshift({fieldtype: 'Section Break'});
|
||||
}
|
||||
no_opening_tab() {
|
||||
return (this.fields[1] && this.fields[1].fieldtype != "Tab Break") || !this.fields.length;
|
||||
}
|
||||
|
||||
replace_field (fieldname, df, render) {
|
||||
is_tabbed_layout() {
|
||||
return this.fields.find(f => f.fieldtype === "Tab Break");
|
||||
}
|
||||
|
||||
replace_field(fieldname, df, render) {
|
||||
df.fieldname = fieldname; // change of fieldname is avoided
|
||||
if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) {
|
||||
const fieldobj = this.init_field(df, render);
|
||||
|
|
@ -145,7 +177,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
}
|
||||
|
||||
make_field (df, colspan, render) {
|
||||
make_field(df, colspan, render) {
|
||||
!this.section && this.make_section();
|
||||
!this.column && this.make_column();
|
||||
|
||||
|
|
@ -159,9 +191,15 @@ frappe.ui.form.Layout = class Layout {
|
|||
this.section.fields_list.push(fieldobj);
|
||||
this.section.fields_dict[df.fieldname] = fieldobj;
|
||||
fieldobj.section = this.section;
|
||||
|
||||
if (this.current_tab) {
|
||||
fieldobj.tab = this.current_tab;
|
||||
this.current_tab.fields_list.push(fieldobj);
|
||||
this.current_tab.fields_dict[df.fieldname] = fieldobj;
|
||||
}
|
||||
}
|
||||
|
||||
init_field (df, render = false) {
|
||||
init_field(df, render=false) {
|
||||
const fieldobj = frappe.ui.form.make_control({
|
||||
df: df,
|
||||
doctype: this.doctype,
|
||||
|
|
@ -176,8 +214,8 @@ frappe.ui.form.Layout = class Layout {
|
|||
return fieldobj;
|
||||
}
|
||||
|
||||
make_page (df) { // eslint-disable-line no-unused-vars
|
||||
var me = this,
|
||||
make_page(df) { // eslint-disable-line no-unused-vars
|
||||
let me = this,
|
||||
head = $('<div class="form-clickable-section text-center">\
|
||||
<a class="btn-fold h6 text-muted">' + __("Show more details") + '</a>\
|
||||
</div>').appendTo(this.wrapper);
|
||||
|
|
@ -185,7 +223,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
this.page = $('<div class="form-page second-page hide"></div>').appendTo(this.wrapper);
|
||||
|
||||
this.fold_btn = head.find(".btn-fold").on("click", function () {
|
||||
var page = $(this).parent().next();
|
||||
let page = $(this).parent().next();
|
||||
if (page.hasClass("hide")) {
|
||||
$(this).removeClass("btn-fold").html(__("Hide details"));
|
||||
page.removeClass("hide");
|
||||
|
|
@ -202,12 +240,12 @@ frappe.ui.form.Layout = class Layout {
|
|||
this.folded = true;
|
||||
}
|
||||
|
||||
unfold () {
|
||||
unfold() {
|
||||
this.fold_btn.trigger('click');
|
||||
}
|
||||
|
||||
make_section (df) {
|
||||
this.section = new frappe.ui.form.Section(this, df);
|
||||
make_section(df) {
|
||||
this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout);
|
||||
|
||||
// append to layout fields
|
||||
if (df) {
|
||||
|
|
@ -218,15 +256,23 @@ frappe.ui.form.Layout = class Layout {
|
|||
this.column = null;
|
||||
}
|
||||
|
||||
make_column (df) {
|
||||
this.column = new frappe.ui.form.Column(this.section, df);
|
||||
make_column(df) {
|
||||
this.column = new Column(this.section, df);
|
||||
if (df && df.fieldname) {
|
||||
this.fields_list.push(this.column);
|
||||
}
|
||||
}
|
||||
|
||||
refresh (doc) {
|
||||
var me = this;
|
||||
make_tab(df) {
|
||||
this.section = null;
|
||||
let tab = new Tab(this, df, this.frm, this.tabs_list, this.tabs_content);
|
||||
this.current_tab = tab;
|
||||
this.make_section({fieldtype: 'Section Break'});
|
||||
this.tabs.push(tab);
|
||||
return tab;
|
||||
}
|
||||
|
||||
refresh(doc) {
|
||||
if (doc) this.doc = doc;
|
||||
|
||||
if (this.frm) {
|
||||
|
|
@ -234,7 +280,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
|
||||
// NOTE this might seem redundant at first, but it needs to be executed when frm.refresh_fields is called
|
||||
me.attach_doc_and_docfields(true);
|
||||
this.attach_doc_and_docfields(true);
|
||||
|
||||
if (this.frm && this.frm.wrapper) {
|
||||
$(this.frm.wrapper).trigger("refresh-fields");
|
||||
|
|
@ -246,6 +292,9 @@ frappe.ui.form.Layout = class Layout {
|
|||
// refresh sections
|
||||
this.refresh_sections();
|
||||
|
||||
// refresh tabs
|
||||
this.tabbed_layout && this.refresh_tabs();
|
||||
|
||||
if (this.frm) {
|
||||
// collapse sections
|
||||
this.refresh_section_collapse();
|
||||
|
|
@ -277,7 +326,30 @@ frappe.ui.form.Layout = class Layout {
|
|||
});
|
||||
}
|
||||
|
||||
refresh_fields (fields) {
|
||||
refresh_tabs() {
|
||||
this.tabs.forEach(tab => {
|
||||
if (!tab.wrapper.hasClass('hide') || !tab.parent.hasClass('hide')) {
|
||||
tab.parent.removeClass('show hide');
|
||||
tab.wrapper.removeClass('show hide');
|
||||
if (
|
||||
tab.wrapper.find(
|
||||
".form-section:not(.hide-control, .empty-section), .form-dashboard-section:not(.hide-control, .empty-section)"
|
||||
).length
|
||||
) {
|
||||
tab.toggle(true);
|
||||
} else {
|
||||
tab.toggle(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const visible_tabs = this.tabs.filter(tab => !tab.hidden);
|
||||
if (visible_tabs && visible_tabs.length == 1) {
|
||||
visible_tabs[0].parent.toggleClass('hide show');
|
||||
}
|
||||
}
|
||||
|
||||
refresh_fields(fields) {
|
||||
let fieldnames = fields.map((field) => {
|
||||
if (field.fieldname) return field.fieldname;
|
||||
});
|
||||
|
|
@ -292,7 +364,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
});
|
||||
}
|
||||
|
||||
add_fields (fields) {
|
||||
add_fields(fields) {
|
||||
this.render(fields);
|
||||
this.refresh_fields(fields);
|
||||
}
|
||||
|
|
@ -300,11 +372,11 @@ frappe.ui.form.Layout = class Layout {
|
|||
refresh_section_collapse () {
|
||||
if (!(this.sections && this.sections.length)) return;
|
||||
|
||||
for (var i = 0; i < this.sections.length; i++) {
|
||||
var section = this.sections[i];
|
||||
var df = section.df;
|
||||
for (let i = 0; i < this.sections.length; i++) {
|
||||
let section = this.sections[i];
|
||||
let df = section.df;
|
||||
if (df && df.collapsible) {
|
||||
var collapse = true;
|
||||
let collapse = true;
|
||||
|
||||
if (df.collapsible_depends_on) {
|
||||
collapse = !this.evaluate_depends_on_value(df.collapsible_depends_on);
|
||||
|
|
@ -319,10 +391,10 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
}
|
||||
|
||||
attach_doc_and_docfields (refresh) {
|
||||
var me = this;
|
||||
for (var i = 0, l = this.fields_list.length; i < l; i++) {
|
||||
var fieldobj = this.fields_list[i];
|
||||
attach_doc_and_docfields(refresh) {
|
||||
let me = this;
|
||||
for (let i = 0, l = this.fields_list.length; i < l; i++) {
|
||||
let fieldobj = this.fields_list[i];
|
||||
if (me.doc) {
|
||||
fieldobj.doc = me.doc;
|
||||
fieldobj.doctype = me.doc.doctype;
|
||||
|
|
@ -339,41 +411,49 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
}
|
||||
|
||||
refresh_section_count () {
|
||||
refresh_section_count() {
|
||||
this.wrapper.find(".section-count-label:visible").each(function (i) {
|
||||
$(this).html(i + 1);
|
||||
});
|
||||
}
|
||||
setup_tabbing () {
|
||||
var me = this;
|
||||
this.wrapper.on("keydown", function (ev) {
|
||||
|
||||
setup_events() {
|
||||
this.tabs_list.off('click').on('click', '.nav-link', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
$(e.currentTarget).tab('show');
|
||||
});
|
||||
}
|
||||
|
||||
setup_tab_events() {
|
||||
this.wrapper.on("keydown", (ev) => {
|
||||
if (ev.which == 9) {
|
||||
var current = $(ev.target),
|
||||
doctype = current.attr("data-doctype"),
|
||||
fieldname = current.attr("data-fieldname");
|
||||
if (doctype)
|
||||
return me.handle_tab(doctype, fieldname, ev.shiftKey);
|
||||
let current = $(ev.target);
|
||||
let doctype = current.attr("data-doctype");
|
||||
let fieldname = current.attr("data-fieldname");
|
||||
if (doctype) {
|
||||
return this.handle_tab(doctype, fieldname, ev.shiftKey);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
handle_tab (doctype, fieldname, shift) {
|
||||
var me = this,
|
||||
grid_row = null,
|
||||
|
||||
handle_tab(doctype, fieldname, shift) {
|
||||
let grid_row = null,
|
||||
prev = null,
|
||||
fields = me.fields_list,
|
||||
in_grid = false,
|
||||
fields = this.fields_list,
|
||||
focused = false;
|
||||
|
||||
// in grid
|
||||
if (doctype != me.doctype) {
|
||||
grid_row = me.get_open_grid_row();
|
||||
if (doctype != this.doctype) {
|
||||
grid_row = this.get_open_grid_row();
|
||||
if (!grid_row || !grid_row.layout) {
|
||||
return;
|
||||
}
|
||||
fields = grid_row.layout.fields_list;
|
||||
}
|
||||
|
||||
for (var i = 0, len = fields.length; i < len; i++) {
|
||||
for (let i = 0, len = fields.length; i < len; i++) {
|
||||
if (fields[i].df.fieldname == fieldname) {
|
||||
if (shift) {
|
||||
if (prev) {
|
||||
|
|
@ -384,7 +464,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
break;
|
||||
}
|
||||
if (i < len - 1) {
|
||||
focused = me.focus_on_next_field(i, fields);
|
||||
focused = this.focus_on_next_field(i, fields);
|
||||
}
|
||||
|
||||
if (focused) {
|
||||
|
|
@ -408,17 +488,19 @@ frappe.ui.form.Layout = class Layout {
|
|||
// next row
|
||||
grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true);
|
||||
}
|
||||
} else {
|
||||
} else if (!shift) {
|
||||
// End of tab navigation
|
||||
$(this.primary_button).focus();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
focus_on_next_field (start_idx, fields) {
|
||||
|
||||
focus_on_next_field(start_idx, fields) {
|
||||
// loop to find next eligible fields
|
||||
for (var i = start_idx + 1, len = fields.length; i < len; i++) {
|
||||
var field = fields[i];
|
||||
for (let i = start_idx + 1, len = fields.length; i < len; i++) {
|
||||
let field = fields[i];
|
||||
if (this.is_visible(field)) {
|
||||
if (field.df.fieldtype === "Table") {
|
||||
// open table grid
|
||||
|
|
@ -437,10 +519,15 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
}
|
||||
}
|
||||
is_visible (field) {
|
||||
return field.disp_status === "Write" && (field.$wrapper && field.$wrapper.is(":visible"));
|
||||
|
||||
is_visible(field) {
|
||||
return field.disp_status === "Write" && (field.df && "hidden" in field.df && !field.df.hidden);
|
||||
}
|
||||
set_focus (field) {
|
||||
|
||||
set_focus(field) {
|
||||
if (field.tab) {
|
||||
field.tab.set_active();
|
||||
}
|
||||
// next is table, show the table
|
||||
if (field.df.fieldtype=="Table") {
|
||||
if (!field.grid.grid_rows.length) {
|
||||
|
|
@ -454,18 +541,19 @@ frappe.ui.form.Layout = class Layout {
|
|||
field.$input.focus();
|
||||
}
|
||||
}
|
||||
get_open_grid_row () {
|
||||
|
||||
get_open_grid_row() {
|
||||
return $(".grid-row-open").data("grid_row");
|
||||
}
|
||||
refresh_dependency () {
|
||||
|
||||
refresh_dependency() {
|
||||
// Resolve "depends_on" and show / hide accordingly
|
||||
var me = this;
|
||||
|
||||
// build dependants' dictionary
|
||||
var has_dep = false;
|
||||
let has_dep = false;
|
||||
|
||||
for (var fkey in this.fields_list) {
|
||||
var f = this.fields_list[fkey];
|
||||
for (let fkey in this.fields_list) {
|
||||
let f = this.fields_list[fkey];
|
||||
f.dependencies_clear = true;
|
||||
if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) {
|
||||
has_dep = true;
|
||||
|
|
@ -475,8 +563,8 @@ frappe.ui.form.Layout = class Layout {
|
|||
if (!has_dep) return;
|
||||
|
||||
// show / hide based on values
|
||||
for (var i = me.fields_list.length - 1; i >= 0; i--) {
|
||||
var f = me.fields_list[i];
|
||||
for (let i = this.fields_list.length - 1; i >= 0; i--) {
|
||||
let f = this.fields_list[i];
|
||||
f.guardian_has_value = true;
|
||||
if (f.df.depends_on) {
|
||||
// evaluate guardian
|
||||
|
|
@ -508,7 +596,8 @@ frappe.ui.form.Layout = class Layout {
|
|||
|
||||
this.refresh_section_count();
|
||||
}
|
||||
set_dependant_property (condition, fieldname, property) {
|
||||
|
||||
set_dependant_property(condition, fieldname, property) {
|
||||
let set_property = this.evaluate_depends_on_value(condition);
|
||||
let value = set_property ? 1 : 0;
|
||||
let form_obj;
|
||||
|
|
@ -530,19 +619,20 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
}
|
||||
}
|
||||
evaluate_depends_on_value (expression) {
|
||||
var out = null;
|
||||
var doc = this.doc;
|
||||
|
||||
evaluate_depends_on_value(expression) {
|
||||
let out = null;
|
||||
let doc = this.doc;
|
||||
|
||||
if (!doc && this.get_values) {
|
||||
var doc = this.get_values(true);
|
||||
doc = this.get_values(true);
|
||||
}
|
||||
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
var parent = this.frm ? this.frm.doc : this.doc || null;
|
||||
let parent = this.frm ? this.frm.doc : this.doc || null;
|
||||
|
||||
if (typeof (expression) === 'boolean') {
|
||||
out = expression;
|
||||
|
|
@ -574,160 +664,3 @@ frappe.ui.form.Layout = class Layout {
|
|||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.form.Section = class FormSection {
|
||||
constructor(layout, df) {
|
||||
this.layout = layout;
|
||||
this.df = df || {};
|
||||
this.fields_list = [];
|
||||
this.fields_dict = {};
|
||||
|
||||
this.make();
|
||||
// if (this.frm)
|
||||
// this.section.body.css({"padding":"0px 3%"})
|
||||
this.row = {
|
||||
wrapper: this.wrapper
|
||||
};
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
make() {
|
||||
if (!this.layout.page) {
|
||||
this.layout.page = $('<div class="form-page"></div>').appendTo(this.layout.wrapper);
|
||||
}
|
||||
let make_card = this.layout.card_layout;
|
||||
this.wrapper = $(`<div class="row form-section ${ make_card ? "card-section" : "" }">`)
|
||||
.appendTo(this.layout.page);
|
||||
this.layout.sections.push(this);
|
||||
|
||||
if (this.df) {
|
||||
if (this.df.label) {
|
||||
this.make_head();
|
||||
}
|
||||
if (this.df.description) {
|
||||
$('<div class="col-sm-12 small text-muted form-section-description">' + __(this.df.description) + '</div>')
|
||||
.appendTo(this.wrapper);
|
||||
}
|
||||
if (this.df.cssClass) {
|
||||
this.wrapper.addClass(this.df.cssClass);
|
||||
}
|
||||
if (this.df.hide_border) {
|
||||
this.wrapper.toggleClass("hide-border", true);
|
||||
}
|
||||
}
|
||||
|
||||
// for bc
|
||||
this.body = $('<div class="section-body">').appendTo(this.wrapper);
|
||||
}
|
||||
|
||||
make_head () {
|
||||
this.head = $(`<div class="section-head">
|
||||
${__(this.df.label)}
|
||||
<span class="ml-2 collapse-indicator mb-1">
|
||||
</span>
|
||||
</div>`);
|
||||
this.head.appendTo(this.wrapper);
|
||||
this.indicator = this.head.find('.collapse-indicator');
|
||||
this.indicator.hide();
|
||||
if (this.df.collapsible) {
|
||||
// show / hide based on status
|
||||
this.collapse_link = this.head.on("click", () => {
|
||||
this.collapse();
|
||||
});
|
||||
|
||||
this.indicator.show();
|
||||
}
|
||||
}
|
||||
refresh() {
|
||||
if (!this.df)
|
||||
return;
|
||||
|
||||
// hide if explictly hidden
|
||||
var hide = this.df.hidden || this.df.hidden_due_to_dependency;
|
||||
|
||||
// hide if no perm
|
||||
if (!hide && this.layout && this.layout.frm && !this.layout.frm.get_perm(this.df.permlevel || 0, "read")) {
|
||||
hide = true;
|
||||
}
|
||||
|
||||
this.wrapper.toggleClass("hide-control", !!hide);
|
||||
}
|
||||
collapse (hide) {
|
||||
// unknown edge case
|
||||
if (!(this.head && this.body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hide===undefined) {
|
||||
hide = !this.body.hasClass("hide");
|
||||
}
|
||||
|
||||
this.body.toggleClass("hide", hide);
|
||||
this.head.toggleClass("collapsed", hide);
|
||||
|
||||
let indicator_icon = hide ? 'down' : 'up-line';
|
||||
|
||||
this.indicator & this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1'));
|
||||
|
||||
// refresh signature fields
|
||||
this.fields_list.forEach((f) => {
|
||||
if (f.df.fieldtype == 'Signature') {
|
||||
f.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
is_collapsed() {
|
||||
return this.body.hasClass('hide');
|
||||
}
|
||||
|
||||
has_missing_mandatory () {
|
||||
var missing_mandatory = false;
|
||||
for (var j = 0, l = this.fields_list.length; j < l; j++) {
|
||||
var section_df = this.fields_list[j].df;
|
||||
if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) {
|
||||
missing_mandatory = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return missing_mandatory;
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.form.Column = class FormColumn {
|
||||
constructor(section, df) {
|
||||
if (!df) df = {};
|
||||
|
||||
this.df = df;
|
||||
this.section = section;
|
||||
this.make();
|
||||
this.resize_all_columns();
|
||||
}
|
||||
make () {
|
||||
this.wrapper = $('<div class="form-column">\
|
||||
<form>\
|
||||
</form>\
|
||||
</div>').appendTo(this.section.body)
|
||||
.find("form")
|
||||
.on("submit", function () {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (this.df.label) {
|
||||
$('<label class="control-label">' + __(this.df.label)
|
||||
+ '</label>').appendTo(this.wrapper);
|
||||
}
|
||||
}
|
||||
resize_all_columns () {
|
||||
// distribute all columns equally
|
||||
var colspan = cint(12 / this.section.wrapper.find(".form-column").length);
|
||||
|
||||
this.section.wrapper.find(".form-column").removeClass()
|
||||
.addClass("form-column")
|
||||
.addClass("col-sm-" + colspan);
|
||||
|
||||
}
|
||||
refresh () {
|
||||
this.section.refresh();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
this.dialog = new frappe.ui.Dialog({
|
||||
title: title,
|
||||
fields: this.fields,
|
||||
size: this.size,
|
||||
primary_action_label: this.primary_action_label || __("Get Items"),
|
||||
secondary_action_label: __("Make {0}", [__(this.doctype)]),
|
||||
primary_action: () => {
|
||||
|
|
@ -135,7 +136,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
this.get_child_result().then(r => {
|
||||
this.child_results = r.message || [];
|
||||
this.render_child_datatable();
|
||||
|
||||
|
||||
this.$wrapper.addClass('hidden');
|
||||
this.$child_wrapper.removeClass('hidden');
|
||||
this.dialog.fields_dict.more_btn.$wrapper.hide();
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ frappe.ui.form.ScriptManager = class ScriptManager {
|
|||
|
||||
function setup_add_fetch(df) {
|
||||
if ((['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check',
|
||||
'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select'].includes(df.fieldtype) || df.read_only==1)
|
||||
'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype) || df.read_only==1)
|
||||
&& df.fetch_from && df.fetch_from.indexOf(".")!=-1) {
|
||||
var parts = df.fetch_from.split(".");
|
||||
me.frm.add_fetch(parts[0], parts[1], df.fieldname);
|
||||
|
|
|
|||
146
frappe/public/js/frappe/form/section.js
Normal file
146
frappe/public/js/frappe/form/section.js
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
export default class Section {
|
||||
constructor(parent, df, card_layout) {
|
||||
this.card_layout = card_layout;
|
||||
this.parent = parent;
|
||||
this.df = df || {};
|
||||
this.fields_list = [];
|
||||
this.fields_dict = {};
|
||||
|
||||
this.make();
|
||||
|
||||
if (this.df.label && this.df.collapsible && localStorage.getItem(df.css_class + '-closed')) {
|
||||
this.collapse();
|
||||
}
|
||||
|
||||
this.row = {
|
||||
wrapper: this.wrapper
|
||||
};
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
make() {
|
||||
let make_card = this.card_layout;
|
||||
this.wrapper = $(`<div class="row
|
||||
${this.df.is_dashboard_section ? "form-dashboard-section" : "form-section"}
|
||||
${ make_card ? "card-section" : "" }">
|
||||
`).appendTo(this.parent);
|
||||
|
||||
if (this.df) {
|
||||
if (this.df.label) {
|
||||
this.make_head();
|
||||
}
|
||||
if (this.df.description) {
|
||||
this.description_wrapper = $(
|
||||
`<div class="col-sm-12 form-section-description">
|
||||
${__(this.df.description)}
|
||||
</div>`
|
||||
);
|
||||
|
||||
this.wrapper.append(this.description_wrapper);
|
||||
}
|
||||
if (this.df.css_class) {
|
||||
this.wrapper.addClass(this.df.css_class);
|
||||
}
|
||||
if (this.df.hide_border) {
|
||||
this.wrapper.toggleClass("hide-border", true);
|
||||
}
|
||||
}
|
||||
|
||||
this.body = $('<div class="section-body">').appendTo(this.wrapper);
|
||||
|
||||
if (this.df.body_html) {
|
||||
this.body.append(this.df.body_html);
|
||||
}
|
||||
}
|
||||
|
||||
make_head() {
|
||||
this.head = $(`
|
||||
<div class="section-head">
|
||||
${__(this.df.label)}
|
||||
<span class="ml-2 collapse-indicator mb-1"></span>
|
||||
</div>
|
||||
`);
|
||||
|
||||
this.head.appendTo(this.wrapper);
|
||||
this.indicator = this.head.find('.collapse-indicator');
|
||||
this.indicator.hide();
|
||||
|
||||
if (this.df.collapsible) {
|
||||
// show / hide based on status
|
||||
this.collapse_link = this.head.on("click", () => {
|
||||
this.collapse();
|
||||
});
|
||||
this.set_icon();
|
||||
this.indicator.show();
|
||||
}
|
||||
}
|
||||
|
||||
refresh(hide) {
|
||||
if (!this.df) return;
|
||||
// hide if explicitly hidden
|
||||
hide = hide || this.df.hidden || this.df.hidden_due_to_dependency;
|
||||
this.wrapper.toggleClass("hide-control", !!hide);
|
||||
}
|
||||
|
||||
collapse(hide) {
|
||||
// unknown edge case
|
||||
if (!(this.head && this.body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hide === undefined) {
|
||||
hide = !this.body.hasClass("hide");
|
||||
}
|
||||
|
||||
this.body.toggleClass("hide", hide);
|
||||
this.head && this.head.toggleClass("collapsed", hide);
|
||||
|
||||
this.set_icon(hide);
|
||||
|
||||
// refresh signature fields
|
||||
this.fields_list.forEach((f) => {
|
||||
if (f.df.fieldtype == 'Signature') {
|
||||
f.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// save state for next reload ('' is falsy)
|
||||
if (this.df.css_class)
|
||||
localStorage.setItem(this.df.css_class + '-closed', hide ? '1' : '');
|
||||
}
|
||||
|
||||
set_icon(hide) {
|
||||
let indicator_icon = hide ? 'down' : 'up-line';
|
||||
this.indicator && this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1'));
|
||||
}
|
||||
|
||||
is_collapsed() {
|
||||
return this.body.hasClass('hide');
|
||||
}
|
||||
|
||||
has_missing_mandatory () {
|
||||
let missing_mandatory = false;
|
||||
for (let j = 0, l = this.fields_list.length; j < l; j++) {
|
||||
const section_df = this.fields_list[j].df;
|
||||
if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) {
|
||||
missing_mandatory = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return missing_mandatory;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.on_section_toggle(false);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.on_section_toggle(true);
|
||||
}
|
||||
|
||||
on_section_toggle(show) {
|
||||
this.wrapper.toggleClass("hide-control", !show);
|
||||
// this.on_section_toggle && this.on_section_toggle(show);
|
||||
}
|
||||
}
|
||||
75
frappe/public/js/frappe/form/tab.js
Normal file
75
frappe/public/js/frappe/form/tab.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
export default class Tab {
|
||||
constructor(parent, df, frm, tabs_list, tabs_content) {
|
||||
this.parent = parent;
|
||||
this.df = df || {};
|
||||
this.frm = frm;
|
||||
this.doctype = 'User';
|
||||
this.label = this.df && this.df.label;
|
||||
this.tabs_list = tabs_list;
|
||||
this.tabs_content = tabs_content;
|
||||
this.fields_list = [];
|
||||
this.fields_dict = {};
|
||||
this.make();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
make() {
|
||||
const id = `${frappe.scrub(this.doctype, '-')}-${this.df.fieldname}`;
|
||||
this.parent = $(`
|
||||
<li class="nav-item">
|
||||
<a class="nav-link ${this.df.active ? "active": ""}" id="${id}-tab"
|
||||
data-toggle="tab"
|
||||
href="#${id}"
|
||||
role="tab"
|
||||
aria-controls="${this.label}">
|
||||
${__(this.label)}
|
||||
</a>
|
||||
</li>
|
||||
`).appendTo(this.tabs_list);
|
||||
|
||||
this.wrapper = $(`<div class="tab-pane fade show ${this.df.active ? "active": ""}"
|
||||
id="${id}" role="tabpanel" aria-labelledby="${id}-tab">`).appendTo(this.tabs_content);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (!this.df) return;
|
||||
|
||||
// hide if explicitly hidden
|
||||
let hide = this.df.hidden || this.df.hidden_due_to_dependency;
|
||||
if (!hide && this.frm && !this.frm.get_perm(this.df.permlevel || 0, "read")) {
|
||||
hide = true;
|
||||
}
|
||||
|
||||
hide && this.toggle(false);
|
||||
}
|
||||
|
||||
toggle(show) {
|
||||
this.parent.toggleClass('hide', !show);
|
||||
this.wrapper.toggleClass('hide', !show);
|
||||
this.parent.toggleClass('show', show);
|
||||
this.wrapper.toggleClass('show', show);
|
||||
this.hidden = !show;
|
||||
}
|
||||
|
||||
show() {
|
||||
this.parent.show();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.parent.hide();
|
||||
}
|
||||
|
||||
set_active() {
|
||||
this.parent.find('.nav-link').tab('show');
|
||||
this.wrapper.addClass('show');
|
||||
}
|
||||
|
||||
is_active() {
|
||||
return this.wrapper.hasClass('active');
|
||||
}
|
||||
|
||||
is_hidden() {
|
||||
this.wrapper.hasClass('hide')
|
||||
&& this.parent.hasClass('hide');
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,20 @@
|
|||
</svg>
|
||||
</a>
|
||||
{% } %}
|
||||
<div class="custom-actions"></div>
|
||||
<div class="more-actions">
|
||||
<a type="button" class="action-btn"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<svg class="icon icon-sm">
|
||||
<use xlink:href="#icon-dot-horizontal"></use>
|
||||
</svg>
|
||||
</a>
|
||||
<ul class="dropdown-menu small">
|
||||
<li>
|
||||
<a class="dropdown-item" data-action="copy_link">{{ __('Copy Link') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
<div class="content">
|
||||
|
|
|
|||
|
|
@ -545,7 +545,7 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
|
||||
show_jump_to_field_dialog() {
|
||||
let visible_fields_filter = f =>
|
||||
!['Section Break', 'Column Break'].includes(f.df.fieldtype)
|
||||
!['Section Break', 'Column Break', 'Tab Break'].includes(f.df.fieldtype)
|
||||
&& !f.df.hidden
|
||||
&& f.disp_status !== 'None';
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue