Merge branch 'frappe:develop' into wspace-code-cleanup

This commit is contained in:
Shariq Ansari 2021-10-11 16:35:34 +05:30 committed by GitHub
commit 6839f6b4db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
176 changed files with 4771 additions and 2662 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
View file

@ -67,6 +67,7 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
.cypress-coverage
# Translations
*.mo

View file

@ -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).

View file

@ -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

View 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
};

View file

@ -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');
});
});
});

View 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);
});

View file

@ -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();

View file

@ -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({

View 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");
});
});

View 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');
});
});

View file

@ -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');
});
});
});

View file

@ -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();

View file

@ -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();
});

View file

@ -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

View file

@ -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;
};

View file

@ -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();
});

View file

@ -15,6 +15,7 @@
// Import commands.js using ES2015 syntax:
import './commands';
import '@cypress/code-coverage/support';
// Alternatively you can use CommonJS syntax:

View file

@ -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();
}
}

View file

@ -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

View file

@ -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):

View file

@ -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"

View file

@ -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')

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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
}

View file

@ -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'

View file

@ -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

View file

@ -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) {

View file

@ -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 &lt;script&gt; or just characters like &lt; or &gt;, 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 &lt;script&gt; or just characters like &lt; or &gt;, 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
}

View file

@ -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"))

View file

@ -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",

View file

@ -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',

View file

@ -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):

View file

@ -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)
}

View file

@ -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))

View file

@ -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
View 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)

View file

@ -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'

View file

@ -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
}

View file

@ -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):

View file

@ -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,

View file

@ -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)

View file

@ -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({

View file

@ -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'))

View file

@ -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
}
}

View file

@ -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,

View file

@ -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")

View file

@ -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)

View file

@ -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()

View file

@ -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):

View file

@ -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))

View file

@ -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',

View file

@ -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:

View file

@ -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 (

View file

@ -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));
}
});
}
}
});

View file

@ -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
}

View file

@ -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')

View file

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
class TestNetworkPrinterSettings(unittest.TestCase):
pass

View file

@ -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)
});
}
}
});

View file

@ -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",

View file

@ -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'):

View file

@ -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(

View file

@ -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);
}
})

View file

@ -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

View 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

View 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

View file

@ -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";

View file

@ -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";

View 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();
}
}

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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);

View file

@ -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;
}
});
}
}

View file

@ -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);

View file

@ -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

View file

@ -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();
}
};

View file

@ -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();

View file

@ -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);

View 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);
}
}

View 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');
}
}

View file

@ -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">

View file

@ -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