Merge branch 'develop' into coverage
This commit is contained in:
commit
4d435e271e
164 changed files with 3954 additions and 5812 deletions
3
.github/helper/install.sh
vendored
3
.github/helper/install.sh
vendored
|
|
@ -17,6 +17,7 @@ if [ "$TYPE" == "server" ]; then
|
|||
fi
|
||||
|
||||
if [ "$DB" == "mariadb" ];then
|
||||
sudo apt install mariadb-client-10.3
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
|
||||
|
||||
|
|
@ -58,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
|
||||
bench build --app frappe
|
||||
|
|
|
|||
2
.github/workflows/docs-checker.yml
vendored
2
.github/workflows/docs-checker.yml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
- name: 'Setup Environment'
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.6
|
||||
python-version: 3.7
|
||||
|
||||
- name: 'Clone repo'
|
||||
uses: actions/checkout@v2
|
||||
|
|
|
|||
2
.github/workflows/patch-mariadb-tests.yml
vendored
2
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -9,7 +9,7 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: Patch Test
|
||||
|
||||
|
|
|
|||
2
.github/workflows/publish-assets-develop.yml
vendored
2
.github/workflows/publish-assets-develop.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
node-version: 14
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.6'
|
||||
python-version: '3.7'
|
||||
- name: Set up bench and build assets
|
||||
run: |
|
||||
npm install -g yarn
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
python-version: '12.x'
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.6'
|
||||
python-version: '3.7'
|
||||
- name: Set up bench and build assets
|
||||
run: |
|
||||
npm install -g yarn
|
||||
|
|
|
|||
4
.github/workflows/server-mariadb-tests.yml
vendored
4
.github/workflows/server-mariadb-tests.yml
vendored
|
|
@ -13,7 +13,7 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -127,4 +127,4 @@ jobs:
|
|||
name: MariaDB
|
||||
fail_ci_if_error: true
|
||||
files: /home/runner/frappe-bench/sites/coverage.xml
|
||||
verbose: true
|
||||
verbose: true
|
||||
2
.github/workflows/server-postgres-tests.yml
vendored
2
.github/workflows/server-postgres-tests.yml
vendored
|
|
@ -12,7 +12,7 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
22
.github/workflows/translation_linter.yml
vendored
22
.github/workflows/translation_linter.yml
vendored
|
|
@ -1,22 +0,0 @@
|
|||
name: Frappe Linter
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- version-12-hotfix
|
||||
- version-11-hotfix
|
||||
jobs:
|
||||
check_translation:
|
||||
name: Translation Syntax Check
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.6
|
||||
- name: Validating Translation Syntax
|
||||
run: |
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
python $GITHUB_WORKSPACE/.github/helper/translation.py $files
|
||||
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
|
|
@ -12,7 +12,7 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
59
cypress/fixtures/doctype_with_tab_break.js
Normal file
59
cypress/fixtures/doctype_with_tab_break.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
export default {
|
||||
name: 'Form With Tab Break',
|
||||
custom: 1,
|
||||
actions: [],
|
||||
doctype: 'DocType',
|
||||
engine: 'InnoDB',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'username',
|
||||
fieldtype: 'Data',
|
||||
label: 'Name',
|
||||
options: 'Name'
|
||||
},
|
||||
{
|
||||
fieldname: 'tab',
|
||||
fieldtype: 'Tab Break',
|
||||
label: 'Tab 2',
|
||||
},
|
||||
{
|
||||
fieldname: 'Phone',
|
||||
fieldtype: 'Data',
|
||||
label: 'Phone',
|
||||
options: 'Phone',
|
||||
reqd: 1
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
"group": "Profile",
|
||||
"link_doctype": "Contact",
|
||||
"link_fieldname": "user"
|
||||
},
|
||||
{
|
||||
"group": "Profile",
|
||||
"link_doctype": "Chat Profile",
|
||||
"link_fieldname": "user"
|
||||
},
|
||||
],
|
||||
modified_by: 'Administrator',
|
||||
module: 'Custom',
|
||||
owner: 'Administrator',
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1
|
||||
}
|
||||
],
|
||||
quick_entry: 1,
|
||||
autoname: "format: Test-{####}",
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
93
cypress/integration/control_float.js
Normal file
93
cypress/integration/control_float.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
context("Control Float", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit("/app/website");
|
||||
});
|
||||
|
||||
function get_dialog_with_float() {
|
||||
return cy.dialog({
|
||||
title: "Float Check",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "float_number",
|
||||
fieldtype: "Float",
|
||||
Label: "Float"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
it("check value changes", () => {
|
||||
get_dialog_with_float().as("dialog");
|
||||
|
||||
let data = get_data();
|
||||
data.forEach(x => {
|
||||
cy.window()
|
||||
.its("frappe")
|
||||
.then(frappe => {
|
||||
frappe.boot.sysdefaults.number_format = x.number_format;
|
||||
});
|
||||
x.values.forEach(d => {
|
||||
cy.get_field("float_number", "Float").clear();
|
||||
cy.fill_field("float_number", d.input, "Float").blur();
|
||||
cy.get_field("float_number", "Float").should(
|
||||
"have.value",
|
||||
d.blur_expected
|
||||
);
|
||||
|
||||
cy.get_field("float_number", "Float").focus();
|
||||
cy.get_field("float_number", "Float").blur();
|
||||
cy.get_field("float_number", "Float").focus();
|
||||
cy.get_field("float_number", "Float").should(
|
||||
"have.value",
|
||||
d.focus_expected
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function get_data() {
|
||||
return [
|
||||
{
|
||||
number_format: "#.###,##",
|
||||
values: [
|
||||
{
|
||||
input: "364.87,334",
|
||||
blur_expected: "36.487,334",
|
||||
focus_expected: "36487.334"
|
||||
},
|
||||
{
|
||||
input: "36487,334",
|
||||
blur_expected: "36.487,334",
|
||||
focus_expected: "36487.334"
|
||||
},
|
||||
{
|
||||
input: "100",
|
||||
blur_expected: "100,000",
|
||||
focus_expected: "100"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
number_format: "#,###.##",
|
||||
values: [
|
||||
{
|
||||
input: "364,87.334",
|
||||
blur_expected: "36,487.334",
|
||||
focus_expected: "36487.334"
|
||||
},
|
||||
{
|
||||
input: "36487.334",
|
||||
blur_expected: "36,487.334",
|
||||
focus_expected: "36487.334"
|
||||
},
|
||||
{
|
||||
input: "100",
|
||||
blur_expected: "100.000",
|
||||
focus_expected: "100"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ context('Folder Navigation', () => {
|
|||
it('Deleting Test Folder from the home', () => {
|
||||
//Deleting the Test Folder added in the home directory
|
||||
cy.visit('/app/file/view/home');
|
||||
cy.get('.level-left > .list-subject > .list-row-checkbox').eq(0).click({force: true, delay: 500});
|
||||
cy.get('.level-left > .list-subject > .file-select >.list-row-checkbox').eq(0).click({force: true, delay: 500});
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ context('Form', () => {
|
|||
});
|
||||
it('create a new form', () => {
|
||||
cy.visit('/app/todo/new');
|
||||
cy.fill_field('description', 'this is a test todo', 'Text Editor');
|
||||
cy.get('[data-fieldname="description"] .ql-editor')
|
||||
.first()
|
||||
.click()
|
||||
.type('this is a test todo');
|
||||
cy.wait(300);
|
||||
cy.get('.page-title').should('contain', 'Not Saved');
|
||||
cy.intercept({
|
||||
|
|
|
|||
31
cypress/integration/form_tab_break.js
Normal file
31
cypress/integration/form_tab_break.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import doctype_with_tab_break from '../fixtures/doctype_with_tab_break';
|
||||
const doctype_name = doctype_with_tab_break.name;
|
||||
context("Form Tab Break", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
return cy.insert_doc('DocType', doctype_with_tab_break, true);
|
||||
});
|
||||
it("Should switch tab and open correct tabs on validation error", () => {
|
||||
cy.new_form(doctype_name);
|
||||
// test tab switch
|
||||
cy.findByRole("tab", {name: "Tab 2"}).click();
|
||||
cy.findByText("Phone");
|
||||
cy.findByRole("tab", {name: "Details"}).click();
|
||||
cy.findByText("Name");
|
||||
|
||||
// form should switch to the tab with un-filled mandatory field
|
||||
cy.fill_field("username", "Test");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
cy.findByText("Missing Fields");
|
||||
cy.hide_dialog();
|
||||
cy.findByText("Phone");
|
||||
cy.fill_field("phone", "12345678");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
|
||||
// After save, first tab should have dashboard
|
||||
cy.get(".form-tabs > .nav-item").eq(0).click();
|
||||
cy.findByText("Connections");
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
58
cypress/integration/multi_select_dialog.js
Normal file
58
cypress/integration/multi_select_dialog.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
context('MultiSelectDialog', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
});
|
||||
|
||||
function open_multi_select_dialog() {
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
new frappe.ui.form.MultiSelectDialog({
|
||||
doctype: "Assignment Rule",
|
||||
target: {},
|
||||
setters: {
|
||||
document_type: null,
|
||||
priority: null
|
||||
},
|
||||
add_filters_group: 1,
|
||||
allow_child_item_selection: 1,
|
||||
child_fieldname: "assignment_days",
|
||||
child_columns: ["day"]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('multi select dialog api works', () => {
|
||||
open_multi_select_dialog();
|
||||
cy.get_open_dialog().should('contain', 'Select Assignment Rules');
|
||||
});
|
||||
|
||||
it('checks for filters', () => {
|
||||
['search_term', 'document_type', 'priority'].forEach(fieldname => {
|
||||
cy.get_open_dialog().get(`.frappe-control[data-fieldname="${fieldname}"]`).should('exist');
|
||||
});
|
||||
|
||||
// add_filters_group: 1 should add a filter group
|
||||
cy.get_open_dialog().get(`.frappe-control[data-fieldname="filter_area"]`).should('exist');
|
||||
|
||||
});
|
||||
|
||||
it('checks for child item selection', () => {
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('not.exist');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.frappe-control[data-fieldname="allow_child_item_selection"]`)
|
||||
.should('exist')
|
||||
.click();
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.frappe-control[data-fieldname="child_selection_area"]`)
|
||||
.should('exist');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('contain', 'Assignment Rule');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('contain', 'Day');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
context('Navigation', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
it('Navigate to route with hash in document name', () => {
|
||||
cy.insert_doc('ToDo', {'__newname': 'ABC#123', 'description': 'Test this', 'ignore_duplicate': true});
|
||||
|
|
@ -11,4 +10,16 @@ context('Navigation', () => {
|
|||
cy.go('back');
|
||||
cy.title().should('eq', 'Website');
|
||||
});
|
||||
|
||||
it.only('Navigate to previous page after login', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.findByTitle('To Do').should('be.visible');
|
||||
cy.request('/api/method/logout');
|
||||
cy.reload();
|
||||
cy.get('.btn-primary').contains('Login').click();
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
cy.location('pathname').should('eq', '/app/todo');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
132
frappe/build.py
132
frappe/build.py
|
|
@ -1,10 +1,11 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from subprocess import getoutput
|
||||
from io import StringIO
|
||||
from tempfile import mkdtemp, mktemp
|
||||
from distutils.spawn import find_executable
|
||||
|
|
@ -17,6 +18,8 @@ import psutil
|
|||
from urllib.parse import urlparse
|
||||
from simple_chalk import green
|
||||
from semantic_version import Version
|
||||
from requests import head
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
|
||||
timestamps = {}
|
||||
|
|
@ -24,6 +27,12 @@ app_paths = None
|
|||
sites_path = os.path.abspath(os.getcwd())
|
||||
|
||||
|
||||
class AssetsNotDownloadedError(Exception):
|
||||
pass
|
||||
|
||||
class AssetsDontExistError(HTTPError):
|
||||
pass
|
||||
|
||||
def download_file(url, prefix):
|
||||
from requests import get
|
||||
|
||||
|
|
@ -70,81 +79,94 @@ def build_missing_files():
|
|||
bundle(build_mode, apps="frappe")
|
||||
|
||||
|
||||
def get_assets_link(frappe_head):
|
||||
from subprocess import getoutput
|
||||
from requests import head
|
||||
|
||||
def get_assets_link(frappe_head) -> str:
|
||||
tag = getoutput(
|
||||
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
|
||||
r" refs/tags/,,' -e 's/\^{}//'"
|
||||
% frappe_head
|
||||
)
|
||||
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
|
||||
r" refs/tags/,,' -e 's/\^{}//'"
|
||||
% frappe_head
|
||||
)
|
||||
|
||||
if tag:
|
||||
# if tag exists, download assets from github release
|
||||
url = "https://github.com/frappe/frappe/releases/download/{0}/assets.tar.gz".format(tag)
|
||||
url = f"https://github.com/frappe/frappe/releases/download/{tag}/assets.tar.gz"
|
||||
else:
|
||||
url = "http://assets.frappeframework.com/{0}.tar.gz".format(frappe_head)
|
||||
url = f"http://assets.frappeframework.com/{frappe_head}.tar.gz"
|
||||
|
||||
if not head(url):
|
||||
raise ValueError("URL {0} doesn't exist".format(url))
|
||||
reference = f"Release {tag}" if tag else f"Commit {frappe_head}"
|
||||
raise AssetsDontExistError(f"Assets for {reference} don't exist")
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def fetch_assets(url, frappe_head):
|
||||
click.secho("Retrieving assets...", fg="yellow")
|
||||
|
||||
prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head)
|
||||
assets_archive = download_file(url, prefix)
|
||||
|
||||
if not assets_archive:
|
||||
raise AssetsNotDownloadedError(f"Assets could not be retrived from {url}")
|
||||
|
||||
print(f"\n{green('✔')} Downloaded Frappe assets from {url}")
|
||||
|
||||
return assets_archive
|
||||
|
||||
|
||||
def setup_assets(assets_archive):
|
||||
import tarfile
|
||||
directories_created = set()
|
||||
|
||||
click.secho("\nExtracting assets...\n", fg="yellow")
|
||||
with tarfile.open(assets_archive) as tar:
|
||||
for file in tar:
|
||||
if not file.isdir():
|
||||
dest = "." + file.name.replace("./frappe-bench/sites", "")
|
||||
asset_directory = os.path.dirname(dest)
|
||||
show = dest.replace("./assets/", "")
|
||||
|
||||
if asset_directory not in directories_created:
|
||||
if not os.path.exists(asset_directory):
|
||||
os.makedirs(asset_directory, exist_ok=True)
|
||||
directories_created.add(asset_directory)
|
||||
|
||||
tar.makefile(file, dest)
|
||||
print("{0} Restored {1}".format(green('✔'), show))
|
||||
|
||||
return directories_created
|
||||
|
||||
|
||||
def download_frappe_assets(verbose=True):
|
||||
"""Downloads and sets up Frappe assets if they exist based on the current
|
||||
commit HEAD.
|
||||
Returns True if correctly setup else returns False.
|
||||
"""
|
||||
from subprocess import getoutput
|
||||
|
||||
assets_setup = False
|
||||
frappe_head = getoutput("cd ../apps/frappe && git rev-parse HEAD")
|
||||
|
||||
if frappe_head:
|
||||
if not frappe_head:
|
||||
return False
|
||||
|
||||
try:
|
||||
url = get_assets_link(frappe_head)
|
||||
assets_archive = fetch_assets(url, frappe_head)
|
||||
setup_assets(assets_archive)
|
||||
build_missing_files()
|
||||
return True
|
||||
|
||||
except AssetsDontExistError as e:
|
||||
click.secho(str(e), fg="yellow")
|
||||
|
||||
except Exception as e:
|
||||
# TODO: log traceback in bench.log
|
||||
click.secho(str(e), fg="red")
|
||||
|
||||
finally:
|
||||
try:
|
||||
url = get_assets_link(frappe_head)
|
||||
click.secho("Retrieving assets...", fg="yellow")
|
||||
prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head)
|
||||
assets_archive = download_file(url, prefix)
|
||||
print("\n{0} Downloaded Frappe assets from {1}".format(green('✔'), url))
|
||||
|
||||
if assets_archive:
|
||||
import tarfile
|
||||
directories_created = set()
|
||||
|
||||
click.secho("\nExtracting assets...\n", fg="yellow")
|
||||
with tarfile.open(assets_archive) as tar:
|
||||
for file in tar:
|
||||
if not file.isdir():
|
||||
dest = "." + file.name.replace("./frappe-bench/sites", "")
|
||||
asset_directory = os.path.dirname(dest)
|
||||
show = dest.replace("./assets/", "")
|
||||
|
||||
if asset_directory not in directories_created:
|
||||
if not os.path.exists(asset_directory):
|
||||
os.makedirs(asset_directory, exist_ok=True)
|
||||
directories_created.add(asset_directory)
|
||||
|
||||
tar.makefile(file, dest)
|
||||
print("{0} Restored {1}".format(green('✔'), show))
|
||||
|
||||
build_missing_files()
|
||||
return True
|
||||
else:
|
||||
raise
|
||||
shutil.rmtree(os.path.dirname(assets_archive))
|
||||
except Exception:
|
||||
# TODO: log traceback in bench.log
|
||||
click.secho("An Error occurred while downloading assets...", fg="red")
|
||||
assets_setup = False
|
||||
finally:
|
||||
try:
|
||||
shutil.rmtree(os.path.dirname(assets_archive))
|
||||
except Exception:
|
||||
pass
|
||||
pass
|
||||
|
||||
return assets_setup
|
||||
return False
|
||||
|
||||
|
||||
def symlink(target, link_name, overwrite=False):
|
||||
|
|
|
|||
|
|
@ -102,9 +102,24 @@ def get_commands():
|
|||
from .site import commands as site_commands
|
||||
from .translate import commands as translate_commands
|
||||
from .utils import commands as utils_commands
|
||||
from .redis import commands as redis_commands
|
||||
from .redis_utils import commands as redis_commands
|
||||
|
||||
clickable_link = (
|
||||
"\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a"
|
||||
)
|
||||
all_commands = (
|
||||
scheduler_commands
|
||||
+ site_commands
|
||||
+ translate_commands
|
||||
+ utils_commands
|
||||
+ redis_commands
|
||||
)
|
||||
|
||||
for command in all_commands:
|
||||
if not command.help:
|
||||
command.help = f"Refer to {clickable_link}"
|
||||
|
||||
return all_commands
|
||||
|
||||
all_commands = scheduler_commands + site_commands + translate_commands + utils_commands + redis_commands
|
||||
return list(set(all_commands))
|
||||
|
||||
commands = get_commands()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -67,6 +67,9 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
validate_database_sql
|
||||
)
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
|
||||
force = context.force or force
|
||||
decompressed_file_name = extract_sql_from_archive(sql_file_path)
|
||||
|
||||
|
|
@ -85,9 +88,6 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
# check if valid SQL file
|
||||
validate_database_sql(decompressed_file_name, _raise=not force)
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
|
||||
# dont allow downgrading to older versions of frappe without force
|
||||
if not force and is_downgrade(decompressed_file_name, verbose=True):
|
||||
warn_message = (
|
||||
|
|
@ -474,7 +474,7 @@ def remove_from_installed_apps(context, app):
|
|||
|
||||
@click.command('uninstall-app')
|
||||
@click.argument('app')
|
||||
@click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False, multiple=True)
|
||||
@click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False)
|
||||
@click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False)
|
||||
@click.option('--no-backup', help='Do not backup the site', is_flag=True, default=False)
|
||||
@click.option('--force', help='Force remove app from site', is_flag=True, default=False)
|
||||
|
|
@ -738,6 +738,131 @@ def build_search_index(context):
|
|||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
@click.command('trim-database')
|
||||
@click.option('--dry-run', is_flag=True, default=False, help='Show what would be deleted')
|
||||
@click.option('--format', '-f', default='text', type=click.Choice(['json', 'text']), help='Output format')
|
||||
@click.option('--no-backup', is_flag=True, default=False, help='Do not backup the site')
|
||||
@pass_context
|
||||
def trim_database(context, dry_run, format, no_backup):
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
from frappe.utils.backups import scheduled_backup
|
||||
|
||||
ALL_DATA = {}
|
||||
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
|
||||
TABLES_TO_DROP = []
|
||||
STANDARD_TABLES = get_standard_tables()
|
||||
information_schema = frappe.qb.Schema("information_schema")
|
||||
table_name = frappe.qb.Field("table_name").as_("name")
|
||||
|
||||
queried_result = frappe.qb.from_(
|
||||
information_schema.tables
|
||||
).select(table_name).where(
|
||||
information_schema.tables.table_schema == frappe.conf.db_name
|
||||
).run()
|
||||
|
||||
database_tables = [x[0] for x in queried_result]
|
||||
doctype_tables = frappe.get_all("DocType", pluck="name")
|
||||
|
||||
for x in database_tables:
|
||||
doctype = x.lstrip("tab")
|
||||
if not (doctype in doctype_tables or x.startswith("__") or x in STANDARD_TABLES):
|
||||
TABLES_TO_DROP.append(x)
|
||||
|
||||
if not TABLES_TO_DROP:
|
||||
if format == "text":
|
||||
click.secho(f"No ghost tables found in {frappe.local.site}...Great!", fg="green")
|
||||
else:
|
||||
if not (no_backup or dry_run):
|
||||
if format == "text":
|
||||
print(f"Backing Up Tables: {', '.join(TABLES_TO_DROP)}")
|
||||
|
||||
odb = scheduled_backup(
|
||||
ignore_conf=False,
|
||||
include_doctypes=",".join(x.lstrip("tab") for x in TABLES_TO_DROP),
|
||||
ignore_files=True,
|
||||
force=True,
|
||||
)
|
||||
if format == "text":
|
||||
odb.print_summary()
|
||||
print("\nTrimming Database")
|
||||
|
||||
for table in TABLES_TO_DROP:
|
||||
if format == "text":
|
||||
print(f"* Dropping Table '{table}'...")
|
||||
if not dry_run:
|
||||
frappe.db.sql_ddl(f"drop table `{table}`")
|
||||
|
||||
ALL_DATA[frappe.local.site] = TABLES_TO_DROP
|
||||
frappe.destroy()
|
||||
|
||||
if format == "json":
|
||||
import json
|
||||
print(json.dumps(ALL_DATA, indent=1))
|
||||
|
||||
|
||||
def get_standard_tables():
|
||||
import re
|
||||
|
||||
tables = []
|
||||
sql_file = os.path.join(
|
||||
"..", "apps", "frappe", "frappe", "database", frappe.conf.db_type, f'framework_{frappe.conf.db_type}.sql'
|
||||
)
|
||||
content = open(sql_file).read().splitlines()
|
||||
|
||||
for line in content:
|
||||
table_found = re.search(r"""CREATE TABLE ("|`)(.*)?("|`) \(""", line)
|
||||
if table_found:
|
||||
tables.append(table_found.group(2))
|
||||
|
||||
return tables
|
||||
|
||||
@click.command('trim-tables')
|
||||
@click.option('--dry-run', is_flag=True, default=False, help='Show what would be deleted')
|
||||
@click.option('--format', '-f', default='table', type=click.Choice(['json', 'table']), help='Output format')
|
||||
@click.option('--no-backup', is_flag=True, default=False, help='Do not backup the site')
|
||||
@pass_context
|
||||
def trim_tables(context, dry_run, format, no_backup):
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
from frappe.model.meta import trim_tables
|
||||
from frappe.utils.backups import scheduled_backup
|
||||
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
|
||||
if not (no_backup or dry_run):
|
||||
click.secho(f"Taking backup for {frappe.local.site}", fg="green")
|
||||
odb = scheduled_backup(ignore_files=False, force=True)
|
||||
odb.print_summary()
|
||||
|
||||
try:
|
||||
trimmed_data = trim_tables(dry_run=dry_run, quiet=format == 'json')
|
||||
|
||||
if format == 'table' and not dry_run:
|
||||
click.secho(f"The following data have been removed from {frappe.local.site}", fg='green')
|
||||
|
||||
handle_data(trimmed_data, format=format)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
|
||||
def handle_data(data: dict, format='json'):
|
||||
if format == 'json':
|
||||
import json
|
||||
print(json.dumps({frappe.local.site: data}, indent=1, sort_keys=True))
|
||||
else:
|
||||
from frappe.utils.commands import render_table
|
||||
data = [["DocType", "Fields"]] + [[table, ", ".join(columns)] for table, columns in data.items()]
|
||||
render_table(data)
|
||||
|
||||
|
||||
commands = [
|
||||
add_system_manager,
|
||||
backup,
|
||||
|
|
@ -766,5 +891,7 @@ commands = [
|
|||
add_to_hosts,
|
||||
start_ngrok,
|
||||
build_search_index,
|
||||
partial_restore
|
||||
partial_restore,
|
||||
trim_tables,
|
||||
trim_database,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -408,20 +408,47 @@ def bulk_rename(context, doctype, path):
|
|||
frappe.destroy()
|
||||
|
||||
|
||||
@click.command('db-console')
|
||||
@pass_context
|
||||
def database(context):
|
||||
"""
|
||||
Enter into the Database console for given site.
|
||||
"""
|
||||
site = get_site(context)
|
||||
if not site:
|
||||
raise SiteNotSpecifiedError
|
||||
frappe.init(site=site)
|
||||
if not frappe.conf.db_type or frappe.conf.db_type == "mariadb":
|
||||
_mariadb()
|
||||
elif frappe.conf.db_type == "postgres":
|
||||
_psql()
|
||||
|
||||
|
||||
@click.command('mariadb')
|
||||
@pass_context
|
||||
def mariadb(context):
|
||||
"""
|
||||
Enter into mariadb console for a given site.
|
||||
"""
|
||||
import os
|
||||
|
||||
site = get_site(context)
|
||||
if not site:
|
||||
raise SiteNotSpecifiedError
|
||||
frappe.init(site=site)
|
||||
_mariadb()
|
||||
|
||||
# This is assuming you're within the bench instance.
|
||||
|
||||
@click.command('postgres')
|
||||
@pass_context
|
||||
def postgres(context):
|
||||
"""
|
||||
Enter into postgres console for a given site.
|
||||
"""
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
_psql()
|
||||
|
||||
|
||||
def _mariadb():
|
||||
mysql = find_executable('mysql')
|
||||
os.execv(mysql, [
|
||||
mysql,
|
||||
|
|
@ -434,15 +461,7 @@ def mariadb(context):
|
|||
"-A"])
|
||||
|
||||
|
||||
@click.command('postgres')
|
||||
@pass_context
|
||||
def postgres(context):
|
||||
"""
|
||||
Enter into postgres console for a given site.
|
||||
"""
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
# This is assuming you're within the bench instance.
|
||||
def _psql():
|
||||
psql = find_executable('psql')
|
||||
subprocess.run([ psql, '-d', frappe.conf.db_name])
|
||||
|
||||
|
|
@ -525,6 +544,74 @@ def console(context, autoreload=False):
|
|||
terminal()
|
||||
|
||||
|
||||
@click.command('transform-database', help="Change tables' internal settings changing engine and row formats")
|
||||
@click.option('--table', required=True, help="Comma separated name of tables to convert. To convert all tables, pass 'all'")
|
||||
@click.option('--engine', default=None, type=click.Choice(["InnoDB", "MyISAM"]), help="Choice of storage engine for said table(s)")
|
||||
@click.option('--row_format', default=None, type=click.Choice(["DYNAMIC", "COMPACT", "REDUNDANT", "COMPRESSED"]), help="Set ROW_FORMAT parameter for said table(s)")
|
||||
@click.option('--failfast', is_flag=True, default=False, help="Exit on first failure occurred")
|
||||
@pass_context
|
||||
def transform_database(context, table, engine, row_format, failfast):
|
||||
"Transform site database through given parameters"
|
||||
site = get_site(context)
|
||||
check_table = []
|
||||
add_line = False
|
||||
skipped = 0
|
||||
frappe.init(site=site)
|
||||
|
||||
if frappe.conf.db_type and frappe.conf.db_type != "mariadb":
|
||||
click.secho("This command only has support for MariaDB databases at this point", fg="yellow")
|
||||
sys.exit(1)
|
||||
|
||||
if not (engine or row_format):
|
||||
click.secho("Values for `--engine` or `--row_format` must be set")
|
||||
sys.exit(1)
|
||||
|
||||
frappe.connect()
|
||||
|
||||
if table == "all":
|
||||
information_schema = frappe.qb.Schema("information_schema")
|
||||
queried_tables = frappe.qb.from_(
|
||||
information_schema.tables
|
||||
).select("table_name").where(
|
||||
(information_schema.tables.row_format != row_format)
|
||||
& (information_schema.tables.table_schema == frappe.conf.db_name)
|
||||
).run()
|
||||
tables = [x[0] for x in queried_tables]
|
||||
else:
|
||||
tables = [x.strip() for x in table.split(",")]
|
||||
|
||||
total = len(tables)
|
||||
|
||||
for current, table in enumerate(tables):
|
||||
values_to_set = ""
|
||||
if engine:
|
||||
values_to_set += f" ENGINE={engine}"
|
||||
if row_format:
|
||||
values_to_set += f" ROW_FORMAT={row_format}"
|
||||
|
||||
try:
|
||||
frappe.db.sql(f"ALTER TABLE `{table}`{values_to_set}")
|
||||
update_progress_bar("Updating table schema", current - skipped, total)
|
||||
add_line = True
|
||||
|
||||
except Exception as e:
|
||||
check_table.append([table, e.args])
|
||||
skipped += 1
|
||||
|
||||
if failfast:
|
||||
break
|
||||
|
||||
if add_line:
|
||||
print()
|
||||
|
||||
for errored_table in check_table:
|
||||
table, err = errored_table
|
||||
err_msg = f"{table}: ERROR {err[0]}: {err[1]}"
|
||||
click.secho(err_msg, fg="yellow")
|
||||
|
||||
frappe.destroy()
|
||||
|
||||
|
||||
@click.command('run-tests')
|
||||
@click.option('--app', help="For App")
|
||||
@click.option('--doctype', help="For DocType")
|
||||
|
|
@ -814,6 +901,8 @@ commands = [
|
|||
build,
|
||||
clear_cache,
|
||||
clear_website_cache,
|
||||
database,
|
||||
transform_database,
|
||||
jupyter,
|
||||
console,
|
||||
destroy_all_sessions,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class Address(Document):
|
|||
|
||||
def has_link(self, doctype, name):
|
||||
for link in self.links:
|
||||
if link.link_doctype==doctype and link.link_name== name:
|
||||
if link.link_doctype == doctype and link.link_name == name:
|
||||
return True
|
||||
|
||||
def has_common_link(self, doc):
|
||||
|
|
|
|||
|
|
@ -47,14 +47,14 @@ class Contact(Document):
|
|||
def get_link_for(self, link_doctype):
|
||||
'''Return the link name, if exists for the given link DocType'''
|
||||
for link in self.links:
|
||||
if link.link_doctype==link_doctype:
|
||||
if link.link_doctype == link_doctype:
|
||||
return link.link_name
|
||||
|
||||
return None
|
||||
|
||||
def has_link(self, doctype, name):
|
||||
for link in self.links:
|
||||
if link.link_doctype==doctype and link.link_name== name:
|
||||
if link.link_doctype == doctype and link.link_name == name:
|
||||
return True
|
||||
|
||||
def has_common_link(self, doc):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
|
@ -10,25 +11,40 @@ class AccessLog(Document):
|
|||
|
||||
@frappe.whitelist()
|
||||
@frappe.write_only()
|
||||
def make_access_log(doctype=None, document=None, method=None, file_type=None,
|
||||
report_name=None, filters=None, page=None, columns=None):
|
||||
@retry(
|
||||
stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError)
|
||||
)
|
||||
def make_access_log(
|
||||
doctype=None,
|
||||
document=None,
|
||||
method=None,
|
||||
file_type=None,
|
||||
report_name=None,
|
||||
filters=None,
|
||||
page=None,
|
||||
columns=None,
|
||||
):
|
||||
|
||||
user = frappe.session.user
|
||||
in_request = frappe.request and frappe.request.method == "GET"
|
||||
|
||||
doc = frappe.get_doc({
|
||||
'doctype': 'Access Log',
|
||||
'user': user,
|
||||
'export_from': doctype,
|
||||
'reference_document': document,
|
||||
'file_type': file_type,
|
||||
'report_name': report_name,
|
||||
'page': page,
|
||||
'method': method,
|
||||
'filters': frappe.utils.cstr(filters) if filters else None,
|
||||
'columns': columns
|
||||
})
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Access Log",
|
||||
"user": user,
|
||||
"export_from": doctype,
|
||||
"reference_document": document,
|
||||
"file_type": file_type,
|
||||
"report_name": report_name,
|
||||
"page": page,
|
||||
"method": method,
|
||||
"filters": frappe.utils.cstr(filters) if filters else None,
|
||||
"columns": columns,
|
||||
}
|
||||
)
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview`
|
||||
if frappe.request and frappe.request.method == 'GET':
|
||||
# dont commit in test mode
|
||||
if not frappe.flags.in_test or in_request:
|
||||
frappe.db.commit()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -274,6 +274,8 @@ class DocType(Document):
|
|||
d.fieldname = d.fieldname + '_section'
|
||||
elif d.fieldtype=='Column Break':
|
||||
d.fieldname = d.fieldname + '_column'
|
||||
elif d.fieldtype=='Tab Break':
|
||||
d.fieldname = d.fieldname + '_tab'
|
||||
else:
|
||||
d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"fieldname": "counter",
|
||||
"fieldtype": "Int",
|
||||
"label": "Counter",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -79,7 +80,7 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-04 14:38:14.836056",
|
||||
"modified": "2021-09-13 20:07:47.617615",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Document Naming Rule",
|
||||
|
|
|
|||
|
|
@ -1,238 +1,80 @@
|
|||
{
|
||||
"allow_copy": 1,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2013-01-10 16:34:24",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 0,
|
||||
"actions": [],
|
||||
"allow_copy": 1,
|
||||
"creation": "2013-01-10 16:34:24",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"sms_gateway_url",
|
||||
"message_parameter",
|
||||
"receiver_parameter",
|
||||
"static_parameters_section",
|
||||
"parameters",
|
||||
"use_post"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Eg. smsgateway.com/api/send_sms.cgi",
|
||||
"fieldname": "sms_gateway_url",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "SMS Gateway URL",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"description": "Eg. smsgateway.com/api/send_sms.cgi",
|
||||
"fieldname": "sms_gateway_url",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "SMS Gateway URL",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Enter url parameter for message",
|
||||
"fieldname": "message_parameter",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Message Parameter",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"description": "Enter url parameter for message",
|
||||
"fieldname": "message_parameter",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Message Parameter",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Enter url parameter for receiver nos",
|
||||
"fieldname": "receiver_parameter",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Receiver Parameter",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"description": "Enter url parameter for receiver nos",
|
||||
"fieldname": "receiver_parameter",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Receiver Parameter",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "static_parameters_section",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"fieldname": "static_parameters_section",
|
||||
"fieldtype": "Column Break",
|
||||
"width": "50%"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)",
|
||||
"fieldname": "parameters",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Static Parameters",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "SMS Parameter",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)",
|
||||
"fieldname": "parameters",
|
||||
"fieldtype": "Table",
|
||||
"label": "Static Parameters",
|
||||
"options": "SMS Parameter"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "use_post",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Use POST",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"default": "0",
|
||||
"fieldname": "use_post",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use POST"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 1,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2021-03-02 18:06:00.868688",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "SMS Settings",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-21 19:45:26.809793",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "SMS Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 0,
|
||||
"email": 0,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -788,7 +788,7 @@ def sign_up(email, full_name, redirect_to):
|
|||
return 2, _("Please ask your administrator to verify your sign-up")
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(key='user', limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST'])
|
||||
@rate_limit(limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST'])
|
||||
def reset_password(user):
|
||||
if user=="Administrator":
|
||||
return 'not allowed'
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
.version-info {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.version-info pre {
|
||||
border: 0px;
|
||||
margin: 0px;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.version-info .table {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.version-info .success {
|
||||
background-color: #dff0d8 !important;
|
||||
}
|
||||
|
||||
.version-info .danger {
|
||||
background-color: #f2dede !important;
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe, json
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,460 +1,458 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"creation": "2013-01-10 16:34:01",
|
||||
"description": "Adds a custom field to a DocType",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"dt",
|
||||
"module",
|
||||
"label",
|
||||
"label_help",
|
||||
"fieldname",
|
||||
"insert_after",
|
||||
"length",
|
||||
"column_break_6",
|
||||
"fieldtype",
|
||||
"precision",
|
||||
"hide_seconds",
|
||||
"hide_days",
|
||||
"options",
|
||||
"fetch_from",
|
||||
"fetch_if_empty",
|
||||
"options_help",
|
||||
"section_break_11",
|
||||
"collapsible",
|
||||
"collapsible_depends_on",
|
||||
"default",
|
||||
"depends_on",
|
||||
"mandatory_depends_on",
|
||||
"read_only_depends_on",
|
||||
"properties",
|
||||
"non_negative",
|
||||
"reqd",
|
||||
"unique",
|
||||
"read_only",
|
||||
"ignore_user_permissions",
|
||||
"hidden",
|
||||
"print_hide",
|
||||
"print_hide_if_no_value",
|
||||
"print_width",
|
||||
"no_copy",
|
||||
"allow_on_submit",
|
||||
"in_list_view",
|
||||
"in_standard_filter",
|
||||
"in_global_search",
|
||||
"in_preview",
|
||||
"bold",
|
||||
"report_hide",
|
||||
"search_index",
|
||||
"allow_in_quick_entry",
|
||||
"ignore_xss_filter",
|
||||
"translatable",
|
||||
"hide_border",
|
||||
"description",
|
||||
"permlevel",
|
||||
"width",
|
||||
"columns"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "dt",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Document",
|
||||
"oldfieldname": "dt",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_filter": 1,
|
||||
"label": "Label",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "label",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "label_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Label Help",
|
||||
"oldfieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldname",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "fieldname",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Select the label after which you want to insert new field.",
|
||||
"fieldname": "insert_after",
|
||||
"fieldtype": "Select",
|
||||
"label": "Insert After",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "insert_after",
|
||||
"oldfieldtype": "Select"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"default": "Data",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Field Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"description": "Set non-standard precision for a Float or Currency field",
|
||||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
},
|
||||
{
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "fetch_from",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fetch From"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
|
||||
"fieldname": "fetch_if_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch If Empty"
|
||||
},
|
||||
{
|
||||
"fieldname": "options_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Options Help",
|
||||
"oldfieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible",
|
||||
"fieldtype": "Check",
|
||||
"label": "Collapsible"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Collapsible Depends On"
|
||||
},
|
||||
{
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"label": "Default Value",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Field Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"label": "Permission Level",
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int"
|
||||
},
|
||||
{
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Data",
|
||||
"label": "Width",
|
||||
"oldfieldname": "width",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Int",
|
||||
"label": "Columns"
|
||||
},
|
||||
{
|
||||
"fieldname": "properties",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"print_width": "50%",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Mandatory Field",
|
||||
"oldfieldname": "reqd",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "unique",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "read_only",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read Only"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype===\"Link\"",
|
||||
"fieldname": "ignore_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore User Permissions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "print_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide",
|
||||
"oldfieldname": "print_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
|
||||
"fieldname": "print_hide_if_no_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide If No Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "print_width",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Print Width",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "no_copy",
|
||||
"fieldtype": "Check",
|
||||
"label": "No Copy",
|
||||
"oldfieldname": "no_copy",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
"oldfieldname": "allow_on_submit",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "In List View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_standard_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Standard Filter"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
|
||||
"fieldname": "in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Global Search"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "bold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "report_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Report Hide",
|
||||
"oldfieldname": "report_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "search_index",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Index",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
|
||||
"fieldname": "ignore_xss_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore XSS Filter"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Translatable"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
|
||||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
},
|
||||
{
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_seconds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Seconds"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Days"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Section Break'",
|
||||
"fieldname": "hide_border",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Border"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
|
||||
"fieldname": "non_negative",
|
||||
"fieldtype": "Check",
|
||||
"label": "Non Negative"
|
||||
},
|
||||
{
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module (for export)",
|
||||
"options": "Module Def"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-04 12:45:22.810120",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "dt,label,fieldtype,options",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"creation": "2013-01-10 16:34:01",
|
||||
"description": "Adds a custom field to a DocType",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"dt",
|
||||
"module",
|
||||
"label",
|
||||
"label_help",
|
||||
"fieldname",
|
||||
"insert_after",
|
||||
"length",
|
||||
"column_break_6",
|
||||
"fieldtype",
|
||||
"precision",
|
||||
"hide_seconds",
|
||||
"hide_days",
|
||||
"options",
|
||||
"fetch_from",
|
||||
"fetch_if_empty",
|
||||
"options_help",
|
||||
"section_break_11",
|
||||
"collapsible",
|
||||
"collapsible_depends_on",
|
||||
"default",
|
||||
"depends_on",
|
||||
"mandatory_depends_on",
|
||||
"read_only_depends_on",
|
||||
"properties",
|
||||
"non_negative",
|
||||
"reqd",
|
||||
"unique",
|
||||
"read_only",
|
||||
"ignore_user_permissions",
|
||||
"hidden",
|
||||
"print_hide",
|
||||
"print_hide_if_no_value",
|
||||
"print_width",
|
||||
"no_copy",
|
||||
"allow_on_submit",
|
||||
"in_list_view",
|
||||
"in_standard_filter",
|
||||
"in_global_search",
|
||||
"in_preview",
|
||||
"bold",
|
||||
"report_hide",
|
||||
"search_index",
|
||||
"allow_in_quick_entry",
|
||||
"ignore_xss_filter",
|
||||
"translatable",
|
||||
"hide_border",
|
||||
"description",
|
||||
"permlevel",
|
||||
"width",
|
||||
"columns"
|
||||
],
|
||||
"fields": [{
|
||||
"bold": 1,
|
||||
"fieldname": "dt",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Document",
|
||||
"oldfieldname": "dt",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_filter": 1,
|
||||
"label": "Label",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "label",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "label_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Label Help",
|
||||
"oldfieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldname",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "fieldname",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Select the label after which you want to insert new field.",
|
||||
"fieldname": "insert_after",
|
||||
"fieldtype": "Select",
|
||||
"label": "Insert After",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "insert_after",
|
||||
"oldfieldtype": "Select"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"default": "Data",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Field Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
|
||||
"description": "Set non-standard precision for a Float or Currency field",
|
||||
"fieldname": "precision",
|
||||
"fieldtype": "Select",
|
||||
"label": "Precision",
|
||||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
},
|
||||
{
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options",
|
||||
"oldfieldname": "options",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "fetch_from",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fetch From"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
|
||||
"fieldname": "fetch_if_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch If Empty"
|
||||
},
|
||||
{
|
||||
"fieldname": "options_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Options Help",
|
||||
"oldfieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible",
|
||||
"fieldtype": "Check",
|
||||
"label": "Collapsible"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
|
||||
"fieldname": "collapsible_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Collapsible Depends On"
|
||||
},
|
||||
{
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Text",
|
||||
"label": "Default Value",
|
||||
"oldfieldname": "default",
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Field Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"label": "Permission Level",
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int"
|
||||
},
|
||||
{
|
||||
"fieldname": "width",
|
||||
"fieldtype": "Data",
|
||||
"label": "Width",
|
||||
"oldfieldname": "width",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Int",
|
||||
"label": "Columns"
|
||||
},
|
||||
{
|
||||
"fieldname": "properties",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"print_width": "50%",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Mandatory Field",
|
||||
"oldfieldname": "reqd",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "unique",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unique"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "read_only",
|
||||
"fieldtype": "Check",
|
||||
"label": "Read Only"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype===\"Link\"",
|
||||
"fieldname": "ignore_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore User Permissions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "print_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide",
|
||||
"oldfieldname": "print_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
|
||||
"fieldname": "print_hide_if_no_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Hide If No Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "print_width",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Print Width",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "no_copy",
|
||||
"fieldtype": "Check",
|
||||
"label": "No Copy",
|
||||
"oldfieldname": "no_copy",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
"oldfieldname": "allow_on_submit",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_list_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "In List View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_standard_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Standard Filter"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
|
||||
"fieldname": "in_global_search",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Global Search"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "bold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "report_hide",
|
||||
"fieldtype": "Check",
|
||||
"label": "Report Hide",
|
||||
"oldfieldname": "report_hide",
|
||||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "search_index",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Index",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
|
||||
"fieldname": "ignore_xss_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore XSS Filter"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
|
||||
"fieldname": "translatable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Translatable"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
|
||||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
},
|
||||
{
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_seconds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Seconds"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Duration'",
|
||||
"fieldname": "hide_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Days"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype=='Section Break'",
|
||||
"fieldname": "hide_border",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Border"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
|
||||
"fieldname": "non_negative",
|
||||
"fieldtype": "Check",
|
||||
"label": "Non Negative"
|
||||
},
|
||||
{
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module (for export)",
|
||||
"options": "Module Def"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-04 12:45:23.810120",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "dt,label,fieldtype,options",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ class CustomField(Document):
|
|||
if not self.fieldname:
|
||||
label = self.label
|
||||
if not label:
|
||||
if self.fieldtype in ["Section Break", "Column Break"]:
|
||||
if self.fieldtype in ["Section Break", "Column Break", "Tab Break"]:
|
||||
label = self.fieldtype + "_" + str(self.idx)
|
||||
else:
|
||||
frappe.throw(_("Label is mandatory"))
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nTab Break",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
|
|
@ -428,7 +428,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-07-10 21:57:24.479749",
|
||||
"modified": "2021-07-11 21:57:24.479749",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form Field",
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class PropertySetter(Document):
|
|||
fields=['fieldname', 'label', 'fieldtype'],
|
||||
filters={
|
||||
'parent': dt,
|
||||
'fieldtype': ['not in', ('Section Break', 'Column Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
|
||||
'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
|
||||
'fieldname': ['!=', '']
|
||||
},
|
||||
order_by='label asc',
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
{
|
||||
"db_name": "testdb",
|
||||
"db_password": "password",
|
||||
"mute_emails": true,
|
||||
|
||||
"limits": {
|
||||
"emails": 1500,
|
||||
"space": 0.157,
|
||||
"expiry": "2016-07-25",
|
||||
"users": 1
|
||||
},
|
||||
|
||||
"developer_mode": 1,
|
||||
"auto_cache_clear": true,
|
||||
"disable_website_cache": true,
|
||||
"max_file_size": 1000000,
|
||||
|
||||
"mail_server": "localhost",
|
||||
"mail_login": null,
|
||||
"mail_password": null,
|
||||
"mail_port": 25,
|
||||
"use_ssl": 0,
|
||||
"auto_email_id": "hello@example.com",
|
||||
|
||||
"google_analytics_id": "google_analytics_id",
|
||||
"google_analytics_anonymize_ip": 1,
|
||||
|
||||
"google_login": {
|
||||
"client_id": "google_client_id",
|
||||
"client_secret": "google_client_secret"
|
||||
},
|
||||
"github_login": {
|
||||
"client_id": "github_client_id",
|
||||
"client_secret": "github_client_secret"
|
||||
},
|
||||
"facebook_login": {
|
||||
"client_id": "facebook_client_id",
|
||||
"client_secret": "facebook_client_secret"
|
||||
},
|
||||
|
||||
"celery_broker": "redis://localhost",
|
||||
"celery_result_backend": null,
|
||||
"scheduler_interval": 300,
|
||||
"celery_queue_per_site": true
|
||||
}
|
||||
|
|
@ -332,7 +332,7 @@ class Database(object):
|
|||
values[key] = value
|
||||
if isinstance(value, (list, tuple)):
|
||||
# value is a tuple like ("!=", 0)
|
||||
_operator = value[0]
|
||||
_operator = value[0].lower()
|
||||
values[key] = value[1]
|
||||
if isinstance(value[1], (tuple, list)):
|
||||
# value is a list in tuple ("in", ("A", "B"))
|
||||
|
|
@ -919,13 +919,13 @@ class Database(object):
|
|||
WHERE table_name = 'tab{0}' AND column_name = '{1}' '''.format(doctype, column))[0][0]
|
||||
|
||||
def has_index(self, table_name, index_name):
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
def add_index(self, doctype, fields, index_name=None):
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
def add_unique(self, doctype, fields, constraint_name=None):
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def get_index_name(fields):
|
||||
|
|
@ -951,7 +951,7 @@ class Database(object):
|
|||
def escape(s, percent=True):
|
||||
"""Excape quotes and percent in given string."""
|
||||
# implemented in specific class
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def is_column_missing(e):
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@ class MariaDBDatabase(Database):
|
|||
def setup_type_map(self):
|
||||
self.db_type = 'mariadb'
|
||||
self.type_map = {
|
||||
'Currency': ('decimal', '18,6'),
|
||||
'Currency': ('decimal', '21,9'),
|
||||
'Int': ('int', '11'),
|
||||
'Long Int': ('bigint', '20'),
|
||||
'Float': ('decimal', '18,6'),
|
||||
'Percent': ('decimal', '18,6'),
|
||||
'Float': ('decimal', '21,9'),
|
||||
'Percent': ('decimal', '21,9'),
|
||||
'Check': ('int', '1'),
|
||||
'Small Text': ('text', ''),
|
||||
'Long Text': ('longtext', ''),
|
||||
|
|
@ -51,7 +51,7 @@ class MariaDBDatabase(Database):
|
|||
'Color': ('varchar', self.VARCHAR_LEN),
|
||||
'Barcode': ('longtext', ''),
|
||||
'Geolocation': ('longtext', ''),
|
||||
'Duration': ('decimal', '18,6'),
|
||||
'Duration': ('decimal', '21,9'),
|
||||
'Icon': ('varchar', self.VARCHAR_LEN)
|
||||
}
|
||||
|
||||
|
|
@ -135,8 +135,8 @@ class MariaDBDatabase(Database):
|
|||
table_name = get_table_name(doctype)
|
||||
return self.sql(f"DESC `{table_name}`")
|
||||
|
||||
def change_column_type(self, table: str, column: str, type: str) -> Union[List, Tuple]:
|
||||
table_name = get_table_name(table)
|
||||
def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]:
|
||||
table_name = get_table_name(doctype)
|
||||
return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} NOT NULL")
|
||||
|
||||
# exception types
|
||||
|
|
@ -195,7 +195,7 @@ class MariaDBDatabase(Database):
|
|||
`password` TEXT NOT NULL,
|
||||
`encrypted` INT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`doctype`, `name`, `fieldname`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
|
||||
|
||||
def create_global_search_table(self):
|
||||
if not '__global_search' in self.get_tables():
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ CREATE TABLE `tabDocField` (
|
|||
KEY `label` (`label`),
|
||||
KEY `fieldtype` (`fieldtype`),
|
||||
KEY `fieldname` (`fieldname`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
--
|
||||
|
|
@ -109,7 +109,7 @@ CREATE TABLE `tabDocPerm` (
|
|||
`email` int(1) NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`name`),
|
||||
KEY `parent` (`parent`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Table structure for table `tabDocType Action`
|
||||
|
|
@ -133,7 +133,7 @@ CREATE TABLE `tabDocType Action` (
|
|||
PRIMARY KEY (`name`),
|
||||
KEY `parent` (`parent`),
|
||||
KEY `modified` (`modified`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
--
|
||||
-- Table structure for table `tabDocType Action`
|
||||
|
|
@ -156,7 +156,7 @@ CREATE TABLE `tabDocType Link` (
|
|||
PRIMARY KEY (`name`),
|
||||
KEY `parent` (`parent`),
|
||||
KEY `modified` (`modified`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
--
|
||||
-- Table structure for table `tabDocType`
|
||||
|
|
@ -228,7 +228,7 @@ CREATE TABLE `tabDocType` (
|
|||
`sender_field` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`name`),
|
||||
KEY `parent` (`parent`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Table structure for table `tabSeries`
|
||||
|
|
@ -239,7 +239,7 @@ CREATE TABLE `tabSeries` (
|
|||
`name` varchar(100),
|
||||
`current` int(10) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(`name`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
--
|
||||
|
|
@ -256,7 +256,7 @@ CREATE TABLE `tabSessions` (
|
|||
`device` varchar(255) DEFAULT 'desktop',
|
||||
`status` varchar(20) DEFAULT NULL,
|
||||
KEY `sid` (`sid`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
--
|
||||
|
|
@ -269,7 +269,7 @@ CREATE TABLE `tabSingles` (
|
|||
`field` varchar(255) DEFAULT NULL,
|
||||
`value` text,
|
||||
KEY `singles_doctype_field_index` (`doctype`, `field`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Table structure for table `__Auth`
|
||||
|
|
@ -283,7 +283,7 @@ CREATE TABLE `__Auth` (
|
|||
`password` TEXT NOT NULL,
|
||||
`encrypted` INT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`doctype`, `name`, `fieldname`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Table structure for table `tabFile`
|
||||
|
|
@ -311,7 +311,7 @@ CREATE TABLE `tabFile` (
|
|||
KEY `parent` (`parent`),
|
||||
KEY `attached_to_name` (`attached_to_name`),
|
||||
KEY `attached_to_doctype` (`attached_to_doctype`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
--
|
||||
-- Table structure for table `tabDefaultValue`
|
||||
|
|
@ -334,4 +334,4 @@ CREATE TABLE `tabDefaultValue` (
|
|||
PRIMARY KEY (`name`),
|
||||
KEY `parent` (`parent`),
|
||||
KEY `defaultvalue_parent_defkey_index` (`parent`,`defkey`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
|
|
|||
|
|
@ -4,18 +4,22 @@ from frappe.database.schema import DBTable
|
|||
|
||||
class MariaDBTable(DBTable):
|
||||
def create(self):
|
||||
add_text = ''
|
||||
additional_definitions = ""
|
||||
engine = self.meta.get("engine") or "InnoDB"
|
||||
varchar_len = frappe.db.VARCHAR_LEN
|
||||
|
||||
# columns
|
||||
column_defs = self.get_column_definitions()
|
||||
if column_defs: add_text += ',\n'.join(column_defs) + ',\n'
|
||||
if column_defs:
|
||||
additional_definitions += ',\n'.join(column_defs) + ',\n'
|
||||
|
||||
# index
|
||||
index_defs = self.get_index_definitions()
|
||||
if index_defs: add_text += ',\n'.join(index_defs) + ',\n'
|
||||
if index_defs:
|
||||
additional_definitions += ',\n'.join(index_defs) + ',\n'
|
||||
|
||||
# create table
|
||||
frappe.db.sql("""create table `%s` (
|
||||
query = f"""create table `{self.table_name}` (
|
||||
name varchar({varchar_len}) not null primary key,
|
||||
creation datetime(6),
|
||||
modified datetime(6),
|
||||
|
|
@ -26,13 +30,15 @@ class MariaDBTable(DBTable):
|
|||
parentfield varchar({varchar_len}),
|
||||
parenttype varchar({varchar_len}),
|
||||
idx int(8) not null default '0',
|
||||
%sindex parent(parent),
|
||||
{additional_definitions}
|
||||
index parent(parent),
|
||||
index modified(modified))
|
||||
ENGINE={engine}
|
||||
ROW_FORMAT=COMPRESSED
|
||||
ROW_FORMAT=DYNAMIC
|
||||
CHARACTER SET=utf8mb4
|
||||
COLLATE=utf8mb4_unicode_ci""".format(varchar_len=frappe.db.VARCHAR_LEN,
|
||||
engine=self.meta.get("engine") or 'InnoDB') % (self.table_name, add_text))
|
||||
COLLATE=utf8mb4_unicode_ci"""
|
||||
|
||||
frappe.db.sql(query)
|
||||
|
||||
def alter(self):
|
||||
for col in self.columns.values():
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from typing import List, Tuple, Union
|
|||
import psycopg2
|
||||
import psycopg2.extensions
|
||||
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
|
||||
from psycopg2.errorcodes import STRING_DATA_RIGHT_TRUNCATION
|
||||
|
||||
import frappe
|
||||
from frappe.database.database import Database
|
||||
|
|
@ -31,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', ''),
|
||||
|
|
@ -60,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)
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +172,7 @@ class PostgresDatabase(Database):
|
|||
|
||||
@staticmethod
|
||||
def is_data_too_long(e):
|
||||
return e.pgcode == '22001'
|
||||
return e.pgcode == STRING_DATA_RIGHT_TRUNCATION
|
||||
|
||||
def rename_table(self, old_name: str, new_name: str) -> Union[List, Tuple]:
|
||||
old_name = get_table_name(old_name)
|
||||
|
|
@ -182,8 +183,8 @@ class PostgresDatabase(Database):
|
|||
table_name = get_table_name(doctype)
|
||||
return self.sql(f"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '{table_name}'")
|
||||
|
||||
def change_column_type(self, table: str, column: str, type: str) -> Union[List, Tuple]:
|
||||
table_name = get_table_name(table)
|
||||
def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]:
|
||||
table_name = get_table_name(doctype)
|
||||
return self.sql(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}')
|
||||
|
||||
def create_auth_table(self):
|
||||
|
|
|
|||
|
|
@ -303,6 +303,8 @@ def get_definition(fieldtype, precision=None, length=None):
|
|||
size = d[1] if d[1] else None
|
||||
|
||||
if size:
|
||||
# This check needs to exist for backward compatibility.
|
||||
# Till V13, default size used for float, currency and percent are (18, 6).
|
||||
if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6:
|
||||
size = '21,9'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,322 +1,106 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 1,
|
||||
"beta": 0,
|
||||
"creation": "2013-05-24 13:41:00",
|
||||
"custom": 0,
|
||||
"description": "",
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 0,
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Title",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"permlevel": 0,
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 1,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "",
|
||||
"fieldname": "public",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Public",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 1,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "public",
|
||||
"fieldname": "notify_on_login",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Notify users with a popup when they log in",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 1,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"depends_on": "notify_on_login",
|
||||
"description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.",
|
||||
"fieldname": "notify_on_every_login",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Notify Users On Every Login",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.notify_on_login && doc.public",
|
||||
"fieldname": "expire_notification_on",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Expire Notification On",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 1,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 1,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Help: To link to another record in the system, use \"#Form/Note/[Note Name]\" as the Link URL. (don't use \"http://\")",
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Text Editor",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Content",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"fieldname": "seen_by_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Seen By",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "seen_by",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Seen By Table",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Note Seen By",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-09-21 15:15:44.909636",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Note",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "All",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 1,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2013-05-24 13:41:00",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"public",
|
||||
"notify_on_login",
|
||||
"notify_on_every_login",
|
||||
"expire_notification_on",
|
||||
"content",
|
||||
"seen_by_section",
|
||||
"seen_by"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"default": "0",
|
||||
"fieldname": "public",
|
||||
"fieldtype": "Check",
|
||||
"label": "Public",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"default": "0",
|
||||
"depends_on": "public",
|
||||
"fieldname": "notify_on_login",
|
||||
"fieldtype": "Check",
|
||||
"label": "Notify users with a popup when they log in"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"default": "0",
|
||||
"depends_on": "notify_on_login",
|
||||
"description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.",
|
||||
"fieldname": "notify_on_every_login",
|
||||
"fieldtype": "Check",
|
||||
"label": "Notify Users On Every Login"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.notify_on_login && doc.public",
|
||||
"fieldname": "expire_notification_on",
|
||||
"fieldtype": "Date",
|
||||
"label": "Expire Notification On",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"description": "Help: To link to another record in the system, use \"/app/note/[Note Name]\" as the Link URL. (don't use \"http://\")",
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_global_search": 1,
|
||||
"label": "Content"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "seen_by_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Seen By"
|
||||
},
|
||||
{
|
||||
"fieldname": "seen_by",
|
||||
"fieldtype": "Table",
|
||||
"label": "Seen By Table",
|
||||
"options": "Note Seen By"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-18 10:57:51.352643",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Note",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "All",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -10,15 +10,95 @@ frappe.ui.form.on('System Console', {
|
|||
description: __('Execute Console script'),
|
||||
ignore_inputs: true,
|
||||
});
|
||||
frm.set_value("type", "Python");
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.disable_save();
|
||||
frm.page.set_primary_action(__("Execute"), $btn => {
|
||||
$btn.text(__('Executing...'));
|
||||
return frm.execute_action("Execute").then(() => {
|
||||
$btn.text(__('Execute'));
|
||||
});
|
||||
$btn.text(__("Executing..."));
|
||||
return frm
|
||||
.execute_action("Execute")
|
||||
.then(() => frm.trigger("render_sql_output"))
|
||||
.finally(() => $btn.text(__("Execute")));
|
||||
});
|
||||
},
|
||||
|
||||
type: function(frm) {
|
||||
if (frm.doc.type == "Python") {
|
||||
frm.set_value("output", "");
|
||||
if (frm.sql_output) {
|
||||
frm.sql_output.destroy();
|
||||
frm.get_field("sql_output").html("");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render_sql_output: function(frm) {
|
||||
if (frm.doc.type !== "SQL") return;
|
||||
if (frm.sql_output) {
|
||||
frm.sql_output.destroy();
|
||||
frm.get_field("sql_output").html("");
|
||||
}
|
||||
|
||||
if (frm.doc.output.startsWith("Traceback")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result = JSON.parse(frm.doc.output);
|
||||
frm.set_value("output", `${result.length} ${result.length == 1 ? 'row' : 'rows'}`);
|
||||
|
||||
if (result.length) {
|
||||
let columns = Object.keys(result[0]);
|
||||
frm.sql_output = new DataTable(
|
||||
frm.get_field("sql_output").$wrapper.get(0),
|
||||
{
|
||||
columns,
|
||||
data: result
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
show_processlist: function(frm) {
|
||||
if (frm.doc.show_processlist) {
|
||||
// keep refreshing every 5 seconds
|
||||
frm.events.refresh_processlist(frm);
|
||||
frm.processlist_interval = setInterval(() => frm.events.refresh_processlist(frm), 5000);
|
||||
} else {
|
||||
if (frm.processlist_interval) {
|
||||
|
||||
// end it
|
||||
clearInterval(frm.processlist_interval);
|
||||
frm.get_field("processlist").html('');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
refresh_processlist: function(frm) {
|
||||
let timestamp = new Date();
|
||||
frappe.call('frappe.desk.doctype.system_console.system_console.show_processlist').then(r => {
|
||||
let rows = '';
|
||||
for (let row of r.message) {
|
||||
rows += `<tr>
|
||||
<td>${row.Id}</td>
|
||||
<td>${row.Time}</td>
|
||||
<td>${row.State}</td>
|
||||
<td>${row.Info}</td>
|
||||
<td>${row.Progress}</td>
|
||||
</tr>`
|
||||
}
|
||||
frm.get_field('processlist').html(`
|
||||
<p class='text-muted'>Requested on: ${timestamp}</p>
|
||||
<table class='table-bordered' style='width: 100%'>
|
||||
<thead><tr>
|
||||
<th width='10%'>Id</ht>
|
||||
<th width='10%'>Time</ht>
|
||||
<th width='10%'>State</ht>
|
||||
<th width='60%'>Info</ht>
|
||||
<th width='10%'>Progress</ht>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</thead>`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,9 +17,15 @@
|
|||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"execute_section",
|
||||
"type",
|
||||
"console",
|
||||
"commit",
|
||||
"output"
|
||||
"output",
|
||||
"sql_output",
|
||||
"database_processes_section",
|
||||
"show_processlist",
|
||||
"processlist"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -40,13 +46,47 @@
|
|||
"fieldname": "commit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Commit"
|
||||
},
|
||||
{
|
||||
"fieldname": "execute_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Execute"
|
||||
},
|
||||
{
|
||||
"fieldname": "database_processes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Database Processes"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_processlist",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Processlist"
|
||||
},
|
||||
{
|
||||
"fieldname": "processlist",
|
||||
"fieldtype": "HTML",
|
||||
"label": "processlist"
|
||||
},
|
||||
{
|
||||
"default": "Python",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Type",
|
||||
"options": "Python\nSQL"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type == 'SQL'",
|
||||
"fieldname": "sql_output",
|
||||
"fieldtype": "HTML",
|
||||
"label": "SQL Output"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-21 14:44:35.296877",
|
||||
"modified": "2021-09-15 17:17:44.844767",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "System Console",
|
||||
|
|
@ -65,4 +105,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.utils.safe_exec import safe_exec
|
||||
from frappe.utils.safe_exec import safe_exec, read_sql
|
||||
from frappe.model.document import Document
|
||||
|
||||
class SystemConsole(Document):
|
||||
|
|
@ -13,8 +13,11 @@ class SystemConsole(Document):
|
|||
frappe.only_for('System Manager')
|
||||
try:
|
||||
frappe.debug_log = []
|
||||
safe_exec(self.console)
|
||||
self.output = '\n'.join(frappe.debug_log)
|
||||
if self.type == 'Python':
|
||||
safe_exec(self.console)
|
||||
self.output = '\n'.join(frappe.debug_log)
|
||||
elif self.type == 'SQL':
|
||||
self.output = frappe.as_json(read_sql(self.console, as_dict=1))
|
||||
except: # noqa: E722
|
||||
self.output = frappe.get_traceback()
|
||||
|
||||
|
|
@ -33,4 +36,9 @@ class SystemConsole(Document):
|
|||
def execute_code(doc):
|
||||
console = frappe.get_doc(json.loads(doc))
|
||||
console.run()
|
||||
return console.as_dict()
|
||||
return console.as_dict()
|
||||
|
||||
@frappe.whitelist()
|
||||
def show_processlist():
|
||||
frappe.only_for('System Manager')
|
||||
return frappe.db.sql('show full processlist', as_dict=1)
|
||||
|
|
|
|||
|
|
@ -128,46 +128,35 @@ def delete_tags_for_document(doc):
|
|||
})
|
||||
|
||||
def update_tags(doc, tags):
|
||||
"""
|
||||
Adds tags for documents
|
||||
:param doc: Document to be added to global tags
|
||||
"""
|
||||
"""Adds tags for documents
|
||||
|
||||
:param doc: Document to be added to global tags
|
||||
"""
|
||||
new_tags = {tag.strip() for tag in tags.split(",") if tag}
|
||||
|
||||
for tag in new_tags:
|
||||
if not frappe.db.exists("Tag Link", {"parenttype": doc.doctype, "parent": doc.name, "tag": tag}):
|
||||
frappe.get_doc({
|
||||
"doctype": "Tag Link",
|
||||
"document_type": doc.doctype,
|
||||
"document_name": doc.name,
|
||||
"parenttype": doc.doctype,
|
||||
"parent": doc.name,
|
||||
"title": doc.get_title() or '',
|
||||
"tag": tag
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
existing_tags = [tag.tag for tag in frappe.get_list("Tag Link", filters={
|
||||
"document_type": doc.doctype,
|
||||
"document_name": doc.name
|
||||
}, fields=["tag"])]
|
||||
|
||||
deleted_tags = get_deleted_tags(new_tags, existing_tags)
|
||||
added_tags = set(new_tags) - set(existing_tags)
|
||||
for tag in added_tags:
|
||||
frappe.get_doc({
|
||||
"doctype": "Tag Link",
|
||||
"document_type": doc.doctype,
|
||||
"document_name": doc.name,
|
||||
"parenttype": doc.doctype,
|
||||
"parent": doc.name,
|
||||
"title": doc.get_title() or '',
|
||||
"tag": tag
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
if deleted_tags:
|
||||
for tag in deleted_tags:
|
||||
delete_tag_for_document(doc.doctype, doc.name, tag)
|
||||
|
||||
def get_deleted_tags(new_tags, existing_tags):
|
||||
|
||||
return list(set(existing_tags) - set(new_tags))
|
||||
|
||||
def delete_tag_for_document(dt, dn, tag):
|
||||
frappe.db.delete("Tag Link", {
|
||||
"document_type": dt,
|
||||
"document_name": dn,
|
||||
"tag": tag
|
||||
})
|
||||
deleted_tags = list(set(existing_tags) - set(new_tags))
|
||||
for tag in deleted_tags:
|
||||
frappe.db.delete("Tag Link", {
|
||||
"document_type": doc.doctype,
|
||||
"document_name": doc.name,
|
||||
"tag": tag
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_documents_for_tag(tag):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2019-09-24 13:25:36.435685",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
|
|
@ -44,7 +45,8 @@
|
|||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"modified": "2019-10-03 16:42:35.932409",
|
||||
"links": [],
|
||||
"modified": "2021-09-20 16:53:37.217998",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Tag Link",
|
||||
|
|
@ -61,6 +63,17 @@
|
|||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
|
|
|
|||
|
|
@ -165,8 +165,6 @@
|
|||
"default": "0",
|
||||
"fieldname": "is_standard",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Is Standard",
|
||||
"search_index": 1
|
||||
},
|
||||
|
|
@ -181,7 +179,6 @@
|
|||
"depends_on": "eval:doc.extends_another_page == 1 || doc.for_user",
|
||||
"fieldname": "extends",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Extends",
|
||||
"options": "Workspace",
|
||||
"search_index": 1
|
||||
|
|
@ -228,6 +225,8 @@
|
|||
"default": "0",
|
||||
"fieldname": "public",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Public"
|
||||
},
|
||||
{
|
||||
|
|
@ -265,11 +264,13 @@
|
|||
"label": "Roles"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-30 18:47:18.227154",
|
||||
"modified": "2021-09-16 12:01:06.450621",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Workspace",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -208,17 +208,17 @@ def save_page(title, icon, parent, public, sb_public_items, sb_private_items, de
|
|||
if loads(deleted_pages):
|
||||
return delete_pages(loads(deleted_pages))
|
||||
|
||||
return {"name": title, "public": public}
|
||||
return {"name": title, "public": public, "label": doc.label}
|
||||
|
||||
def delete_pages(deleted_pages):
|
||||
for page in deleted_pages:
|
||||
if page.get("public") and "Workspace Manager" not in frappe.get_roles():
|
||||
return {"name": page.get("title"), "public": 1}
|
||||
return {"name": page.get("title"), "public": 1, "label": page.get("label")}
|
||||
|
||||
if frappe.db.exists("Workspace", page.get("name")):
|
||||
frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True)
|
||||
|
||||
return {"name": "Home", "public": 1}
|
||||
return {"name": "Home", "public": 1, "label": "Home"}
|
||||
|
||||
def sort_pages(sb_public_items, sb_private_items):
|
||||
wspace_public_pages = get_page_list(['name', 'title'], {'public': 1})
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ def validate_filters(data, filters):
|
|||
|
||||
def setup_group_by(data):
|
||||
'''Add columns for aggregated values e.g. count(name)'''
|
||||
if data.group_by:
|
||||
if data.group_by and data.aggregate_function:
|
||||
if data.aggregate_function.lower() not in ('count', 'sum', 'avg'):
|
||||
frappe.throw(_('Invalid aggregate function'))
|
||||
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@
|
|||
},
|
||||
{
|
||||
"default": "UNSEEN",
|
||||
"depends_on": "eval: doc.enable_incoming",
|
||||
"depends_on": "eval: doc.enable_incoming && doc.use_imap",
|
||||
"fieldname": "email_sync_option",
|
||||
"fieldtype": "Select",
|
||||
"hide_days": 1,
|
||||
|
|
@ -236,7 +236,7 @@
|
|||
},
|
||||
{
|
||||
"default": "250",
|
||||
"depends_on": "eval: doc.enable_incoming",
|
||||
"depends_on": "eval: doc.enable_incoming && doc.use_imap",
|
||||
"description": "Total number of emails to sync in initial sync process ",
|
||||
"fieldname": "initial_sync_count",
|
||||
"fieldtype": "Select",
|
||||
|
|
@ -567,7 +567,7 @@
|
|||
"icon": "fa fa-inbox",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-31 15:23:25.714366",
|
||||
"modified": "2021-09-21 16:44:25.728637",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Account",
|
||||
|
|
@ -589,4 +589,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ def get_context(context):
|
|||
if doc.meta.get_field(fieldname).fieldtype in frappe.model.numeric_fieldtypes:
|
||||
value = frappe.utils.cint(value)
|
||||
|
||||
doc.reload()
|
||||
doc.set(fieldname, value)
|
||||
doc.flags.updater_reference = {
|
||||
'doctype': self.doctype,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ class TestNotification(unittest.TestCase):
|
|||
notification.event = 'Value Change'
|
||||
notification.value_changed = 'status'
|
||||
notification.send_to_all_assignees = 1
|
||||
notification.set_property_after_alert = 'description'
|
||||
notification.property_value = 'Changed by Notification'
|
||||
notification.save()
|
||||
|
||||
if not frappe.db.exists('Notification', {'name': 'Contact Status Update'}, 'name'):
|
||||
|
|
@ -237,6 +239,9 @@ class TestNotification(unittest.TestCase):
|
|||
|
||||
self.assertTrue(email_queue)
|
||||
|
||||
# check if description is changed after alert since set_property_after_alert is set
|
||||
self.assertEquals(todo.description, 'Changed by Notification')
|
||||
|
||||
recipients = [d.recipient for d in email_queue.recipients]
|
||||
self.assertTrue('test2@example.com' in recipients)
|
||||
self.assertTrue('test1@example.com' in recipients)
|
||||
|
|
|
|||
|
|
@ -408,8 +408,9 @@ def sync_dependencies(document, producer_site):
|
|||
child_table = doc.get(df.fieldname)
|
||||
for entry in child_table:
|
||||
child_doc = producer_site.get_doc(entry.doctype, entry.name)
|
||||
child_doc = frappe._dict(child_doc)
|
||||
set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site)
|
||||
if child_doc:
|
||||
child_doc = frappe._dict(child_doc)
|
||||
set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site)
|
||||
|
||||
def sync_link_dependencies(doc, link_fields, producer_site):
|
||||
set_dependencies(doc, link_fields, producer_site)
|
||||
|
|
|
|||
|
|
@ -223,7 +223,10 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
|
|||
doc = frappe.get_doc(dt, dn)
|
||||
|
||||
else:
|
||||
doc = frappe.get_doc(json.loads(docs))
|
||||
if isinstance(docs, str):
|
||||
docs = json.loads(docs)
|
||||
|
||||
doc = frappe.get_doc(docs)
|
||||
doc._original_modified = doc.modified
|
||||
doc.check_if_latest()
|
||||
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ source_link = "https://github.com/frappe/frappe"
|
|||
app_license = "MIT"
|
||||
app_logo_url = '/assets/frappe/images/frappe-framework-logo.svg'
|
||||
|
||||
develop_version = '13.x.x-develop'
|
||||
develop_version = '14.x.x-develop'
|
||||
|
||||
app_email = "info@frappe.io"
|
||||
app_email = "developers@frappe.io"
|
||||
|
||||
docs_app = "frappe_io"
|
||||
docs_app = "frappe_docs"
|
||||
|
||||
translator_url = "https://translate.erpnext.com"
|
||||
|
||||
|
|
@ -164,7 +164,8 @@ doc_events = {
|
|||
"after_rename": "frappe.desk.notifications.clear_doctype_notifications",
|
||||
"on_cancel": [
|
||||
"frappe.desk.notifications.clear_doctype_notifications",
|
||||
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions"
|
||||
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
|
||||
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers"
|
||||
],
|
||||
"on_trash": [
|
||||
"frappe.desk.notifications.clear_doctype_notifications",
|
||||
|
|
|
|||
|
|
@ -445,9 +445,21 @@ def extract_sql_from_archive(sql_file_path):
|
|||
else:
|
||||
decompressed_file_name = sql_file_path
|
||||
|
||||
# convert archive sql to latest compatible
|
||||
convert_archive_content(decompressed_file_name)
|
||||
|
||||
return decompressed_file_name
|
||||
|
||||
|
||||
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"))
|
||||
|
||||
|
||||
def extract_sql_gzip(sql_gz_path):
|
||||
import subprocess
|
||||
|
||||
|
|
@ -457,7 +469,7 @@ def extract_sql_gzip(sql_gz_path):
|
|||
decompressed_file = original_file.rstrip(".gz")
|
||||
cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file)
|
||||
subprocess.check_call(cmd, shell=True)
|
||||
except:
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
return decompressed_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',
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ class BaseDocument(object):
|
|||
doc["doctype"] = self.doctype
|
||||
for df in self.meta.get_table_fields():
|
||||
children = self.get(df.fieldname) or []
|
||||
doc[df.fieldname] = [d.as_dict(convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls) for d in children]
|
||||
doc[df.fieldname] = [d.as_dict(convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls, no_default_fields=no_default_fields) for d in children]
|
||||
|
||||
if no_nulls:
|
||||
for k in list(doc):
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ Example:
|
|||
|
||||
'''
|
||||
from datetime import datetime
|
||||
import click
|
||||
import frappe, json, os
|
||||
from frappe.utils import cstr, cint, cast
|
||||
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields
|
||||
|
|
@ -658,27 +659,48 @@ def get_default_df(fieldname):
|
|||
fieldtype = "Data"
|
||||
)
|
||||
|
||||
def trim_tables(doctype=None):
|
||||
def trim_tables(doctype=None, dry_run=False, quiet=False):
|
||||
"""
|
||||
Removes database fields that don't exist in the doctype (json or custom field). This may be needed
|
||||
as maintenance since removing a field in a DocType doesn't automatically
|
||||
delete the db field.
|
||||
"""
|
||||
ignore_fields = default_fields + optional_fields
|
||||
|
||||
filters={ "issingle": 0 }
|
||||
UPDATED_TABLES = {}
|
||||
filters = {"issingle": 0}
|
||||
if doctype:
|
||||
filters["name"] = doctype
|
||||
|
||||
for doctype in frappe.db.get_all("DocType", filters=filters):
|
||||
doctype = doctype.name
|
||||
columns = frappe.db.get_table_columns(doctype)
|
||||
fields = frappe.get_meta(doctype).get_fieldnames_with_value()
|
||||
columns_to_remove = [f for f in list(set(columns) - set(fields)) if f not in ignore_fields
|
||||
and not f.startswith("_")]
|
||||
if columns_to_remove:
|
||||
print(doctype, "columns removed:", columns_to_remove)
|
||||
columns_to_remove = ", ".join("drop `{0}`".format(c) for c in columns_to_remove)
|
||||
query = """alter table `tab{doctype}` {columns}""".format(
|
||||
doctype=doctype, columns=columns_to_remove)
|
||||
frappe.db.sql_ddl(query)
|
||||
for doctype in frappe.db.get_all("DocType", filters=filters, pluck="name"):
|
||||
try:
|
||||
dropped_columns = trim_table(doctype, dry_run=dry_run)
|
||||
if dropped_columns:
|
||||
UPDATED_TABLES[doctype] = dropped_columns
|
||||
except frappe.db.TableMissingError:
|
||||
if quiet:
|
||||
continue
|
||||
click.secho(f"Ignoring missing table for DocType: {doctype}", fg="yellow", err=True)
|
||||
click.secho(f"Consider removing record in the DocType table for {doctype}", fg="yellow", err=True)
|
||||
except Exception as e:
|
||||
if quiet:
|
||||
continue
|
||||
click.echo(e, err=True)
|
||||
|
||||
return UPDATED_TABLES
|
||||
|
||||
|
||||
def trim_table(doctype, dry_run=True):
|
||||
frappe.cache().hdel('table_columns', f"tab{doctype}")
|
||||
ignore_fields = default_fields + optional_fields
|
||||
columns = frappe.db.get_table_columns(doctype)
|
||||
fields = frappe.get_meta(doctype, cached=False).get_fieldnames_with_value()
|
||||
is_internal = lambda f: f not in ignore_fields and not f.startswith("_")
|
||||
columns_to_remove = [
|
||||
f for f in list(set(columns) - set(fields)) if is_internal(f)
|
||||
]
|
||||
DROPPED_COLUMNS = columns_to_remove[:]
|
||||
|
||||
if columns_to_remove and not dry_run:
|
||||
columns_to_remove = ", ".join(f"DROP `{c}`" for c in columns_to_remove)
|
||||
frappe.db.sql_ddl(f"ALTER TABLE `tab{doctype}` {columns_to_remove}")
|
||||
|
||||
return DROPPED_COLUMNS
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.db.change_column_type(table="__Auth", column="password", type="TEXT")
|
||||
frappe.db.change_column_type("__Auth", column="password", type="TEXT")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2021, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Network Printer Settings', {
|
||||
onload (frm) {
|
||||
frm.trigger("connect_print_server");
|
||||
},
|
||||
server_ip (frm) {
|
||||
frm.trigger("connect_print_server");
|
||||
},
|
||||
port (frm) {
|
||||
frm.trigger("connect_print_server");
|
||||
},
|
||||
connect_print_server (frm) {
|
||||
if (frm.doc.server_ip && frm.doc.port) {
|
||||
frappe.call({
|
||||
"doc": frm.doc,
|
||||
"method": "get_printers_list",
|
||||
"args": {
|
||||
ip: frm.doc.server_ip,
|
||||
port: frm.doc.port
|
||||
},
|
||||
callback: function(data) {
|
||||
frm.set_df_property('printer_name', 'options', [""].concat(data.message));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"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-09-17 11:30:16.781655",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Printing",
|
||||
"name": "Network Printer Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
|
||||
class NetworkPrinterSettings(Document):
|
||||
@frappe.whitelist()
|
||||
def get_printers_list(self,ip="localhost",port=631):
|
||||
printer_list = []
|
||||
try:
|
||||
import cups
|
||||
except ImportError:
|
||||
frappe.throw(_('''This feature can not be used as dependencies are missing.
|
||||
Please contact your system manager to enable this by installing pycups!'''))
|
||||
return
|
||||
try:
|
||||
cups.setServer(self.server_ip)
|
||||
cups.setPort(self.port)
|
||||
conn = cups.Connection()
|
||||
printers = conn.getPrinters()
|
||||
for printer_id,printer in printers.items():
|
||||
printer_list.append({
|
||||
'value': printer_id,
|
||||
'label': printer['printer-make-and-model']
|
||||
})
|
||||
|
||||
except RuntimeError:
|
||||
frappe.throw(_("Failed to connect to server"))
|
||||
except frappe.ValidationError:
|
||||
frappe.throw(_("Failed to connect to server"))
|
||||
return printer_list
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_network_printer_settings():
|
||||
return frappe.db.get_list('Network Printer Settings', pluck='name')
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestNetworkPrinterSettings(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -15,27 +15,5 @@ frappe.ui.form.on('Print Settings', {
|
|||
},
|
||||
onload: function(frm) {
|
||||
frm.script_manager.trigger("print_style");
|
||||
},
|
||||
server_ip: function(frm) {
|
||||
frm.trigger("connect_print_server");
|
||||
},
|
||||
port:function(frm) {
|
||||
frm.trigger("connect_print_server");
|
||||
},
|
||||
connect_print_server:function(frm) {
|
||||
if(frm.doc.server_ip && frm.doc.port){
|
||||
frappe.call({
|
||||
"doc": frm.doc,
|
||||
"method": "get_printers",
|
||||
"args": {
|
||||
ip: frm.doc.server_ip,
|
||||
port: frm.doc.port
|
||||
},
|
||||
callback: function(data) {
|
||||
frm.set_df_property('printer_name', 'options', [""].concat(data.message));
|
||||
},
|
||||
error: (data) => frm.set_value("enable_print_server", 0)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,9 +19,6 @@
|
|||
"allow_print_for_cancelled",
|
||||
"server_printer",
|
||||
"enable_print_server",
|
||||
"server_ip",
|
||||
"printer_name",
|
||||
"port",
|
||||
"raw_printing_section",
|
||||
"enable_raw_printing",
|
||||
"print_style_section",
|
||||
|
|
@ -107,29 +104,11 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "enable_print_server",
|
||||
"fieldname": "enable_print_server",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Print Server"
|
||||
},
|
||||
{
|
||||
"default": "localhost",
|
||||
"depends_on": "enable_print_server",
|
||||
"fieldname": "server_ip",
|
||||
"fieldtype": "Data",
|
||||
"label": "Server IP"
|
||||
},
|
||||
{
|
||||
"depends_on": "enable_print_server",
|
||||
"fieldname": "printer_name",
|
||||
"fieldtype": "Select",
|
||||
"label": "Printer Name"
|
||||
},
|
||||
{
|
||||
"default": "631",
|
||||
"depends_on": "enable_print_server",
|
||||
"fieldname": "port",
|
||||
"fieldtype": "Int",
|
||||
"label": "Port"
|
||||
"label": "Enable Print Server",
|
||||
"mandatory_depends_on": "enable_print_server"
|
||||
},
|
||||
{
|
||||
"fieldname": "raw_printing_section",
|
||||
|
|
@ -183,7 +162,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-15 14:16:18.474254",
|
||||
"modified": "2021-09-17 12:59:14.783694",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Printing",
|
||||
"name": "Print Settings",
|
||||
|
|
|
|||
|
|
@ -12,26 +12,6 @@ class PrintSettings(Document):
|
|||
def on_update(self):
|
||||
frappe.clear_cache()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_printers(self,ip="localhost",port=631):
|
||||
printer_list = []
|
||||
try:
|
||||
import cups
|
||||
except ImportError:
|
||||
frappe.throw(_("You need to install pycups to use this feature!"))
|
||||
return
|
||||
try:
|
||||
cups.setServer(self.server_ip)
|
||||
cups.setPort(self.port)
|
||||
conn = cups.Connection()
|
||||
printers = conn.getPrinters()
|
||||
printer_list = printers.keys()
|
||||
except RuntimeError:
|
||||
frappe.throw(_("Failed to connect to server"))
|
||||
except frappe.ValidationError:
|
||||
frappe.throw(_("Failed to connect to server"))
|
||||
return printer_list
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_print_server_enabled():
|
||||
if not hasattr(frappe.local, 'enable_print_server'):
|
||||
|
|
|
|||
|
|
@ -165,10 +165,7 @@ frappe.ui.form.PrintView = class {
|
|||
frappe.set_route('Form', 'Print Settings');
|
||||
});
|
||||
|
||||
if (
|
||||
frappe.model.get_doc(':Print Settings', 'Print Settings')
|
||||
.enable_raw_printing == '1'
|
||||
) {
|
||||
if (this.print_settings.enable_raw_printing == '1') {
|
||||
this.page.add_menu_item(__('Raw Printing Setting'), () => {
|
||||
this.printer_setting_dialog();
|
||||
});
|
||||
|
|
@ -179,6 +176,12 @@ frappe.ui.form.PrintView = class {
|
|||
this.edit_print_format()
|
||||
);
|
||||
}
|
||||
|
||||
if (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 (me.print_settings.enable_print_server) {
|
||||
if (localStorage.getItem('network_printer')) {
|
||||
me.print_by_server();
|
||||
} else {
|
||||
me.network_printer_setting_dialog(() => me.print_by_server());
|
||||
}
|
||||
} else if (me.get_mapped_printer().length === 1) {
|
||||
// printer is already mapped in localstorage (applies for both raw and pdf )
|
||||
if (me.is_raw_printing()) {
|
||||
me.get_raw_commands(function(out) {
|
||||
frappe.ui.form
|
||||
.qz_connect()
|
||||
.then(function() {
|
||||
let printer_map = me.get_mapped_printer()[0];
|
||||
let data = [out.raw_commands];
|
||||
let config = qz.configs.create(printer_map.printer);
|
||||
return qz.print(config, data);
|
||||
})
|
||||
.then(frappe.ui.form.qz_success)
|
||||
.catch((err) => {
|
||||
frappe.ui.form.qz_fail(err);
|
||||
});
|
||||
} else {
|
||||
frappe.show_alert(
|
||||
});
|
||||
} else {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __('PDF printing via "Raw Print" is not supported.'),
|
||||
subtitle: __(
|
||||
'Please remove the printer mapping in Printer Settings and try again.'
|
||||
),
|
||||
indicator: 'info',
|
||||
},
|
||||
14
|
||||
);
|
||||
//Note: need to solve "Error: Cannot parse (FILE)<URL> as a PDF file" to enable qz pdf printing.
|
||||
}
|
||||
} else if (me.is_raw_printing()) {
|
||||
// printer not mapped in localstorage and the current print format is raw printing
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __('Printer mapping not set.'),
|
||||
subtitle: __(
|
||||
'Please set a printer mapping for this print format in the Printer Settings'
|
||||
),
|
||||
indicator: 'warning',
|
||||
},
|
||||
14
|
||||
);
|
||||
me.printer_setting_dialog();
|
||||
} else {
|
||||
me.render_page('/printview?', true);
|
||||
}
|
||||
}
|
||||
|
||||
print_by_server() {
|
||||
let me = this;
|
||||
if (localStorage.getItem('network_printer')) {
|
||||
frappe.call({
|
||||
method: 'frappe.utils.print_format.print_by_server',
|
||||
args: {
|
||||
doctype: me.frm.doc.doctype,
|
||||
name: me.frm.doc.name,
|
||||
printer_setting: localStorage.getItem('network_printer'),
|
||||
print_format: me.selected_format(),
|
||||
no_letterhead: me.with_letterhead(),
|
||||
letterhead: me.get_letterhead(),
|
||||
},
|
||||
callback: function() {},
|
||||
});
|
||||
}
|
||||
}
|
||||
network_printer_setting_dialog(callback) {
|
||||
frappe.call({
|
||||
method: 'frappe.printing.doctype.network_printer_settings.network_printer_settings.get_network_printer_settings',
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __('Select Network Printer'),
|
||||
fields: [
|
||||
{
|
||||
message: __('PDF printing via "Raw Print" is not supported.'),
|
||||
subtitle: __(
|
||||
'Please remove the printer mapping in Printer Settings and try again.'
|
||||
),
|
||||
indicator: 'info',
|
||||
},
|
||||
14
|
||||
);
|
||||
//Note: need to solve "Error: Cannot parse (FILE)<URL> as a PDF file" to enable qz pdf printing.
|
||||
}
|
||||
} else if (me.is_raw_printing()) {
|
||||
// printer not mapped in localstorage and the current print format is raw printing
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __('Printer mapping not set.'),
|
||||
subtitle: __(
|
||||
'Please set a printer mapping for this print format in the Printer Settings'
|
||||
),
|
||||
indicator: 'warning',
|
||||
"label": "Printer",
|
||||
"fieldname": "printer",
|
||||
"fieldtype": "Select",
|
||||
"reqd": 1,
|
||||
"options": r.message
|
||||
}
|
||||
],
|
||||
primary_action: function() {
|
||||
localStorage.setItem('network_printer', d.get_values().printer);
|
||||
if (typeof callback == "function") {
|
||||
callback();
|
||||
}
|
||||
d.hide();
|
||||
},
|
||||
14
|
||||
);
|
||||
me.printer_setting_dialog();
|
||||
} else {
|
||||
me.render_page('/printview?', true);
|
||||
primary_action_label: __('Select')
|
||||
});
|
||||
d.show();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render_page(method, printit = false) {
|
||||
let w = window.open(
|
||||
frappe.urllib.get_full_url(
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
|
|||
} else if(f.fieldtype==="Column Break") {
|
||||
set_column();
|
||||
|
||||
} else if(!in_list(["Section Break", "Column Break", "Fold"], f.fieldtype)
|
||||
} else if (!in_list(["Section Break", "Column Break", "Tab Break", "Fold"], f.fieldtype)
|
||||
&& f.label) {
|
||||
if(!column) set_column();
|
||||
|
||||
|
|
@ -298,7 +298,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
|
|||
init_visible_columns(f) {
|
||||
f.visible_columns = []
|
||||
$.each(frappe.get_meta(f.options).fields, function(i, _f) {
|
||||
if(!in_list(["Section Break", "Column Break"], _f.fieldtype) &&
|
||||
if (!in_list(["Section Break", "Column Break", "Tab Break"], _f.fieldtype) &&
|
||||
!_f.print_hide && f.label) {
|
||||
|
||||
// column names set as fieldname|width
|
||||
|
|
@ -606,7 +606,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
|
|||
// add remaining fields
|
||||
$.each(doc_fields, function(j, f) {
|
||||
if (f && !in_list(column_names, f.fieldname)
|
||||
&& !in_list(["Section Break", "Column Break"], f.fieldtype) && f.label) {
|
||||
&& !in_list(["Section Break", "Column Break", "Tab Break"], f.fieldtype) && f.label) {
|
||||
fields.push(f);
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
</div>
|
||||
<div class="print-format-builder-sidebar-fields">
|
||||
{% for (var i=0, l=fields.length; i < l; i++) { var f = fields[i]; %}
|
||||
{% if(!in_list(["Section Break", "Column Break", "Fold"], f.fieldtype)) { %}
|
||||
{% if(!in_list(["Section Break", "Tab Break", "Column Break", "Fold"], f.fieldtype)) { %}
|
||||
<div class="print-format-builder-field-placeholder"
|
||||
data-fieldname="{%= f.fieldname %}">
|
||||
<div title="{{f.label}}" class="field-label btn btn-default btn-sm sidebar-field ellipsis
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
49
frappe/public/js/frappe/form/column.js
Normal file
49
frappe/public/js/frappe/form/column.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
export default class Column {
|
||||
constructor(section, df) {
|
||||
if (!df) df = {};
|
||||
|
||||
this.df = df;
|
||||
this.section = section;
|
||||
this.make();
|
||||
this.resize_all_columns();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.wrapper = $(`
|
||||
<div class="form-column">
|
||||
<form>
|
||||
</form>
|
||||
</div>
|
||||
`)
|
||||
.appendTo(this.section.body)
|
||||
.find("form")
|
||||
.on("submit", function () {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (this.df.label) {
|
||||
$(`
|
||||
<label class="control-label">
|
||||
${__(this.df.label)}
|
||||
</label>
|
||||
`)
|
||||
.appendTo(this.wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
resize_all_columns() {
|
||||
// distribute all columns equally
|
||||
let colspan = cint(12 / this.section.wrapper.find(".form-column").length);
|
||||
|
||||
this.section.wrapper
|
||||
.find(".form-column")
|
||||
.removeClass()
|
||||
.addClass("form-column")
|
||||
.addClass("col-sm-" + colspan);
|
||||
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.section.refresh();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,17 @@
|
|||
frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlInt {
|
||||
|
||||
make_input() {
|
||||
super.make_input();
|
||||
const change_handler = e => {
|
||||
if (this.change) this.change(e);
|
||||
else {
|
||||
let value = this.get_input_value();
|
||||
this.parse_validate_and_set_in_model(value, e);
|
||||
}
|
||||
};
|
||||
// convert to number format on focusout since focus converts it to flt.
|
||||
this.$input.on("focusout", change_handler);
|
||||
}
|
||||
parse(value) {
|
||||
value = this.eval_expression(value);
|
||||
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision());
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,7 +147,9 @@ class FormTimeline extends BaseTimeline {
|
|||
}
|
||||
|
||||
get_user_link(user) {
|
||||
const user_display_text = (frappe.user_info(user).fullname || '').bold();
|
||||
const user_display_text = (
|
||||
(frappe.session.user == user ? __("You") : frappe.user_info(user).fullname) || ''
|
||||
).bold();
|
||||
return frappe.utils.get_form_link('User', user, true, user_display_text);
|
||||
}
|
||||
|
||||
|
|
@ -353,7 +355,7 @@ class FormTimeline extends BaseTimeline {
|
|||
icon: 'branch',
|
||||
icon_size: 'sm',
|
||||
creation: workflow_log.creation,
|
||||
content: __(workflow_log.content),
|
||||
content: `${this.get_user_link(workflow_log.owner)} ${__(workflow_log.content)}`,
|
||||
title: "Workflow",
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,6 +474,8 @@ 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();
|
||||
}
|
||||
|
|
@ -475,6 +483,11 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
this.scroll_to_element();
|
||||
}
|
||||
|
||||
set_first_tab_as_active() {
|
||||
this.layout.tabs[0]
|
||||
&& this.layout.tabs[0].set_active();
|
||||
}
|
||||
|
||||
focus_on_first_input() {
|
||||
let first = this.form_wrapper.find('.form-layout :input:visible:first');
|
||||
if (!in_list(["Date", "Datetime"], first.attr("data-fieldtype"))) {
|
||||
|
|
@ -1605,6 +1618,11 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
|
||||
let $el = field.$wrapper;
|
||||
|
||||
// set tab as active
|
||||
if (field.tab && !field.tab.is_active()) {
|
||||
field.tab.set_active();
|
||||
}
|
||||
|
||||
// uncollapse section
|
||||
if (field.section.is_collapsed()) {
|
||||
field.section.collapse(false);
|
||||
|
|
|
|||
|
|
@ -212,13 +212,12 @@ export default class Grid {
|
|||
|
||||
delete_all_rows() {
|
||||
frappe.confirm(__("Are you sure you want to delete all rows?"), () => {
|
||||
this.grid_rows.forEach(row => {
|
||||
row.remove();
|
||||
});
|
||||
this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
|
||||
|
||||
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').prop('checked', 0);
|
||||
this.frm.doc[this.df.fieldname] = [];
|
||||
$(this.parent).find('.rows').empty();
|
||||
this.grid_rows = [];
|
||||
this.refresh();
|
||||
this.frm && this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
|
||||
this.frm && this.frm.dirty();
|
||||
this.scroll_to_top();
|
||||
});
|
||||
}
|
||||
|
|
@ -244,8 +243,10 @@ export default class Grid {
|
|||
|
||||
this.remove_rows_button.toggleClass('hidden',
|
||||
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true);
|
||||
this.remove_all_rows_button.toggleClass('hidden',
|
||||
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').length ? false : true);
|
||||
|
||||
let select_all_checkbox_checked = this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').length;
|
||||
let show_delete_all_btn = select_all_checkbox_checked && this.data.length > this.get_selected_children().length;
|
||||
this.remove_all_rows_button.toggleClass('hidden', !show_delete_all_btn);
|
||||
}
|
||||
|
||||
get_selected() {
|
||||
|
|
@ -835,10 +836,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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,10 +123,12 @@ export default class GridRowForm {
|
|||
.toggle(this.row.grid.is_editable());
|
||||
}
|
||||
refresh_field(fieldname) {
|
||||
if(this.fields_dict[fieldname]) {
|
||||
this.fields_dict[fieldname].refresh();
|
||||
this.layout && this.layout.refresh_dependency();
|
||||
}
|
||||
const field = this.fields_dict[fieldname];
|
||||
if (!field) return;
|
||||
|
||||
field.docname = this.row.doc.name;
|
||||
field.refresh();
|
||||
this.layout && this.layout.refresh_dependency();
|
||||
}
|
||||
set_focus() {
|
||||
// wait for animation and then focus on the first row
|
||||
|
|
|
|||
|
|
@ -1,27 +1,50 @@
|
|||
import '../class';
|
||||
import Section from "./section.js";
|
||||
import Tab from "./tab.js";
|
||||
import Column from "./column.js";
|
||||
|
||||
frappe.ui.form.Layout = class Layout {
|
||||
constructor (opts) {
|
||||
this.views = {};
|
||||
this.pages = [];
|
||||
this.tabs = [];
|
||||
this.sections = [];
|
||||
this.fields_list = [];
|
||||
this.fields_dict = {};
|
||||
|
||||
$.extend(this, opts);
|
||||
}
|
||||
|
||||
make() {
|
||||
if (!this.parent && this.body) {
|
||||
this.parent = this.body;
|
||||
}
|
||||
this.wrapper = $('<div class="form-layout">').appendTo(this.parent);
|
||||
this.message = $('<div class="form-message hidden"></div>').appendTo(this.wrapper);
|
||||
this.page = $('<div class="form-page"></div>').appendTo(this.wrapper);
|
||||
|
||||
if (!this.fields) {
|
||||
this.fields = this.get_doctype_fields();
|
||||
}
|
||||
this.setup_tabbing();
|
||||
|
||||
if (this.is_tabbed_layout()) {
|
||||
this.setup_tabbed_layout();
|
||||
}
|
||||
|
||||
this.setup_tab_events();
|
||||
this.render();
|
||||
}
|
||||
|
||||
setup_tabbed_layout() {
|
||||
$(`
|
||||
<div class="form-tabs-list">
|
||||
<ul class="nav form-tabs" id="form-tabs" role="tablist"></ul>
|
||||
</div>
|
||||
`).appendTo(this.page);
|
||||
this.tabs_list = this.page.find('.form-tabs');
|
||||
this.tabs_content = $(`<div class="form-tab-content tab-content"></div>`).appendTo(this.page);
|
||||
this.setup_events();
|
||||
}
|
||||
|
||||
show_empty_form_message() {
|
||||
if (!(this.wrapper.find(".frappe-control:visible").length || this.wrapper.find(".section-head.collapsed").length)) {
|
||||
this.show_message(__("This form does not have any input"));
|
||||
|
|
@ -87,49 +110,58 @@ frappe.ui.form.Layout = class Layout {
|
|||
this.message.empty().addClass('hidden');
|
||||
}
|
||||
}
|
||||
render (new_fields) {
|
||||
var me = this;
|
||||
var fields = new_fields || this.fields;
|
||||
|
||||
render(new_fields) {
|
||||
let fields = new_fields || this.fields;
|
||||
|
||||
this.section = null;
|
||||
this.column = null;
|
||||
|
||||
if (this.with_dashboard) {
|
||||
this.setup_dashboard_section();
|
||||
if (this.no_opening_section() && !this.is_tabbed_layout()) {
|
||||
this.fields.unshift({fieldtype: 'Section Break'});
|
||||
}
|
||||
|
||||
if (this.no_opening_section()) {
|
||||
this.make_section();
|
||||
if (this.is_tabbed_layout()) {
|
||||
let default_tab = {label: __('Details'), fieldname: 'details', fieldtype: "Tab Break"};
|
||||
let first_tab = this.fields[1].fieldtype === "Tab Break" ? this.fields[1] : null;
|
||||
if (!first_tab) {
|
||||
this.fields.splice(1, 0, default_tab);
|
||||
}
|
||||
}
|
||||
$.each(fields, function (i, df) {
|
||||
|
||||
fields.forEach(df => {
|
||||
switch (df.fieldtype) {
|
||||
case "Fold":
|
||||
me.make_page(df);
|
||||
this.make_page(df);
|
||||
break;
|
||||
case "Section Break":
|
||||
me.make_section(df);
|
||||
this.make_section(df);
|
||||
break;
|
||||
case "Column Break":
|
||||
me.make_column(df);
|
||||
this.make_column(df);
|
||||
break;
|
||||
case "Tab Break":
|
||||
this.make_tab(df);
|
||||
break;
|
||||
default:
|
||||
me.make_field(df);
|
||||
this.make_field(df);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
no_opening_section () {
|
||||
no_opening_section() {
|
||||
return (this.fields[0] && this.fields[0].fieldtype != "Section Break") || !this.fields.length;
|
||||
}
|
||||
|
||||
setup_dashboard_section () {
|
||||
if (this.no_opening_section()) {
|
||||
this.fields.unshift({fieldtype: 'Section Break'});
|
||||
}
|
||||
no_opening_tab() {
|
||||
return (this.fields[1] && this.fields[1].fieldtype != "Tab Break") || !this.fields.length;
|
||||
}
|
||||
|
||||
replace_field (fieldname, df, render) {
|
||||
is_tabbed_layout() {
|
||||
return this.fields.find(f => f.fieldtype === "Tab Break");
|
||||
}
|
||||
|
||||
replace_field(fieldname, df, render) {
|
||||
df.fieldname = fieldname; // change of fieldname is avoided
|
||||
if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) {
|
||||
const fieldobj = this.init_field(df, render);
|
||||
|
|
@ -145,7 +177,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
}
|
||||
|
||||
make_field (df, colspan, render) {
|
||||
make_field(df, colspan, render) {
|
||||
!this.section && this.make_section();
|
||||
!this.column && this.make_column();
|
||||
|
||||
|
|
@ -159,9 +191,15 @@ frappe.ui.form.Layout = class Layout {
|
|||
this.section.fields_list.push(fieldobj);
|
||||
this.section.fields_dict[df.fieldname] = fieldobj;
|
||||
fieldobj.section = this.section;
|
||||
|
||||
if (this.current_tab) {
|
||||
fieldobj.tab = this.current_tab;
|
||||
this.current_tab.fields_list.push(fieldobj);
|
||||
this.current_tab.fields_dict[df.fieldname] = fieldobj;
|
||||
}
|
||||
}
|
||||
|
||||
init_field (df, render = false) {
|
||||
init_field(df, render=false) {
|
||||
const fieldobj = frappe.ui.form.make_control({
|
||||
df: df,
|
||||
doctype: this.doctype,
|
||||
|
|
@ -176,8 +214,8 @@ frappe.ui.form.Layout = class Layout {
|
|||
return fieldobj;
|
||||
}
|
||||
|
||||
make_page (df) { // eslint-disable-line no-unused-vars
|
||||
var me = this,
|
||||
make_page(df) { // eslint-disable-line no-unused-vars
|
||||
let me = this,
|
||||
head = $('<div class="form-clickable-section text-center">\
|
||||
<a class="btn-fold h6 text-muted">' + __("Show more details") + '</a>\
|
||||
</div>').appendTo(this.wrapper);
|
||||
|
|
@ -185,7 +223,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
this.page = $('<div class="form-page second-page hide"></div>').appendTo(this.wrapper);
|
||||
|
||||
this.fold_btn = head.find(".btn-fold").on("click", function () {
|
||||
var page = $(this).parent().next();
|
||||
let page = $(this).parent().next();
|
||||
if (page.hasClass("hide")) {
|
||||
$(this).removeClass("btn-fold").html(__("Hide details"));
|
||||
page.removeClass("hide");
|
||||
|
|
@ -202,12 +240,12 @@ frappe.ui.form.Layout = class Layout {
|
|||
this.folded = true;
|
||||
}
|
||||
|
||||
unfold () {
|
||||
unfold() {
|
||||
this.fold_btn.trigger('click');
|
||||
}
|
||||
|
||||
make_section (df) {
|
||||
this.section = new frappe.ui.form.Section(this, df);
|
||||
make_section(df) {
|
||||
this.section = new Section(this.current_tab ? this.current_tab.wrapper : this.page, df, this.card_layout);
|
||||
|
||||
// append to layout fields
|
||||
if (df) {
|
||||
|
|
@ -218,15 +256,23 @@ frappe.ui.form.Layout = class Layout {
|
|||
this.column = null;
|
||||
}
|
||||
|
||||
make_column (df) {
|
||||
this.column = new frappe.ui.form.Column(this.section, df);
|
||||
make_column(df) {
|
||||
this.column = new Column(this.section, df);
|
||||
if (df && df.fieldname) {
|
||||
this.fields_list.push(this.column);
|
||||
}
|
||||
}
|
||||
|
||||
refresh (doc) {
|
||||
var me = this;
|
||||
make_tab(df) {
|
||||
this.section = null;
|
||||
let tab = new Tab(this, df, this.frm, this.tabs_list, this.tabs_content);
|
||||
this.current_tab = tab;
|
||||
this.make_section({fieldtype: 'Section Break'});
|
||||
this.tabs.push(tab);
|
||||
return tab;
|
||||
}
|
||||
|
||||
refresh(doc) {
|
||||
if (doc) this.doc = doc;
|
||||
|
||||
if (this.frm) {
|
||||
|
|
@ -234,7 +280,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
|
||||
// NOTE this might seem redundant at first, but it needs to be executed when frm.refresh_fields is called
|
||||
me.attach_doc_and_docfields(true);
|
||||
this.attach_doc_and_docfields(true);
|
||||
|
||||
if (this.frm && this.frm.wrapper) {
|
||||
$(this.frm.wrapper).trigger("refresh-fields");
|
||||
|
|
@ -246,6 +292,9 @@ frappe.ui.form.Layout = class Layout {
|
|||
// refresh sections
|
||||
this.refresh_sections();
|
||||
|
||||
// refresh tabs
|
||||
this.tabbed_layout && this.refresh_tabs();
|
||||
|
||||
if (this.frm) {
|
||||
// collapse sections
|
||||
this.refresh_section_collapse();
|
||||
|
|
@ -277,7 +326,30 @@ frappe.ui.form.Layout = class Layout {
|
|||
});
|
||||
}
|
||||
|
||||
refresh_fields (fields) {
|
||||
refresh_tabs() {
|
||||
this.tabs.forEach(tab => {
|
||||
if (!tab.wrapper.hasClass('hide') || !tab.parent.hasClass('hide')) {
|
||||
tab.parent.removeClass('show hide');
|
||||
tab.wrapper.removeClass('show hide');
|
||||
if (
|
||||
tab.wrapper.find(
|
||||
".form-section:not(.hide-control, .empty-section), .form-dashboard-section:not(.hide-control, .empty-section)"
|
||||
).length
|
||||
) {
|
||||
tab.toggle(true);
|
||||
} else {
|
||||
tab.toggle(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const visible_tabs = this.tabs.filter(tab => !tab.hidden);
|
||||
if (visible_tabs && visible_tabs.length == 1) {
|
||||
visible_tabs[0].parent.toggleClass('hide show');
|
||||
}
|
||||
}
|
||||
|
||||
refresh_fields(fields) {
|
||||
let fieldnames = fields.map((field) => {
|
||||
if (field.fieldname) return field.fieldname;
|
||||
});
|
||||
|
|
@ -292,7 +364,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
});
|
||||
}
|
||||
|
||||
add_fields (fields) {
|
||||
add_fields(fields) {
|
||||
this.render(fields);
|
||||
this.refresh_fields(fields);
|
||||
}
|
||||
|
|
@ -300,11 +372,11 @@ frappe.ui.form.Layout = class Layout {
|
|||
refresh_section_collapse () {
|
||||
if (!(this.sections && this.sections.length)) return;
|
||||
|
||||
for (var i = 0; i < this.sections.length; i++) {
|
||||
var section = this.sections[i];
|
||||
var df = section.df;
|
||||
for (let i = 0; i < this.sections.length; i++) {
|
||||
let section = this.sections[i];
|
||||
let df = section.df;
|
||||
if (df && df.collapsible) {
|
||||
var collapse = true;
|
||||
let collapse = true;
|
||||
|
||||
if (df.collapsible_depends_on) {
|
||||
collapse = !this.evaluate_depends_on_value(df.collapsible_depends_on);
|
||||
|
|
@ -319,10 +391,10 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
}
|
||||
|
||||
attach_doc_and_docfields (refresh) {
|
||||
var me = this;
|
||||
for (var i = 0, l = this.fields_list.length; i < l; i++) {
|
||||
var fieldobj = this.fields_list[i];
|
||||
attach_doc_and_docfields(refresh) {
|
||||
let me = this;
|
||||
for (let i = 0, l = this.fields_list.length; i < l; i++) {
|
||||
let fieldobj = this.fields_list[i];
|
||||
if (me.doc) {
|
||||
fieldobj.doc = me.doc;
|
||||
fieldobj.doctype = me.doc.doctype;
|
||||
|
|
@ -339,41 +411,49 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
}
|
||||
|
||||
refresh_section_count () {
|
||||
refresh_section_count() {
|
||||
this.wrapper.find(".section-count-label:visible").each(function (i) {
|
||||
$(this).html(i + 1);
|
||||
});
|
||||
}
|
||||
setup_tabbing () {
|
||||
var me = this;
|
||||
this.wrapper.on("keydown", function (ev) {
|
||||
|
||||
setup_events() {
|
||||
this.tabs_list.off('click').on('click', '.nav-link', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
$(e.currentTarget).tab('show');
|
||||
});
|
||||
}
|
||||
|
||||
setup_tab_events() {
|
||||
this.wrapper.on("keydown", (ev) => {
|
||||
if (ev.which == 9) {
|
||||
var current = $(ev.target),
|
||||
doctype = current.attr("data-doctype"),
|
||||
fieldname = current.attr("data-fieldname");
|
||||
if (doctype)
|
||||
return me.handle_tab(doctype, fieldname, ev.shiftKey);
|
||||
let current = $(ev.target);
|
||||
let doctype = current.attr("data-doctype");
|
||||
let fieldname = current.attr("data-fieldname");
|
||||
if (doctype) {
|
||||
return this.handle_tab(doctype, fieldname, ev.shiftKey);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
handle_tab (doctype, fieldname, shift) {
|
||||
var me = this,
|
||||
grid_row = null,
|
||||
|
||||
handle_tab(doctype, fieldname, shift) {
|
||||
let grid_row = null,
|
||||
prev = null,
|
||||
fields = me.fields_list,
|
||||
in_grid = false,
|
||||
fields = this.fields_list,
|
||||
focused = false;
|
||||
|
||||
// in grid
|
||||
if (doctype != me.doctype) {
|
||||
grid_row = me.get_open_grid_row();
|
||||
if (doctype != this.doctype) {
|
||||
grid_row = this.get_open_grid_row();
|
||||
if (!grid_row || !grid_row.layout) {
|
||||
return;
|
||||
}
|
||||
fields = grid_row.layout.fields_list;
|
||||
}
|
||||
|
||||
for (var i = 0, len = fields.length; i < len; i++) {
|
||||
for (let i = 0, len = fields.length; i < len; i++) {
|
||||
if (fields[i].df.fieldname == fieldname) {
|
||||
if (shift) {
|
||||
if (prev) {
|
||||
|
|
@ -384,7 +464,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
break;
|
||||
}
|
||||
if (i < len - 1) {
|
||||
focused = me.focus_on_next_field(i, fields);
|
||||
focused = this.focus_on_next_field(i, fields);
|
||||
}
|
||||
|
||||
if (focused) {
|
||||
|
|
@ -408,17 +488,19 @@ frappe.ui.form.Layout = class Layout {
|
|||
// next row
|
||||
grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true);
|
||||
}
|
||||
} else {
|
||||
} else if (!shift) {
|
||||
// End of tab navigation
|
||||
$(this.primary_button).focus();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
focus_on_next_field (start_idx, fields) {
|
||||
|
||||
focus_on_next_field(start_idx, fields) {
|
||||
// loop to find next eligible fields
|
||||
for (var i = start_idx + 1, len = fields.length; i < len; i++) {
|
||||
var field = fields[i];
|
||||
for (let i = start_idx + 1, len = fields.length; i < len; i++) {
|
||||
let field = fields[i];
|
||||
if (this.is_visible(field)) {
|
||||
if (field.df.fieldtype === "Table") {
|
||||
// open table grid
|
||||
|
|
@ -437,10 +519,15 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
}
|
||||
}
|
||||
is_visible (field) {
|
||||
return field.disp_status === "Write" && (field.$wrapper && field.$wrapper.is(":visible"));
|
||||
|
||||
is_visible(field) {
|
||||
return field.disp_status === "Write" && (field.df && "hidden" in field.df && !field.df.hidden);
|
||||
}
|
||||
set_focus (field) {
|
||||
|
||||
set_focus(field) {
|
||||
if (field.tab) {
|
||||
field.tab.set_active();
|
||||
}
|
||||
// next is table, show the table
|
||||
if (field.df.fieldtype=="Table") {
|
||||
if (!field.grid.grid_rows.length) {
|
||||
|
|
@ -454,18 +541,19 @@ frappe.ui.form.Layout = class Layout {
|
|||
field.$input.focus();
|
||||
}
|
||||
}
|
||||
get_open_grid_row () {
|
||||
|
||||
get_open_grid_row() {
|
||||
return $(".grid-row-open").data("grid_row");
|
||||
}
|
||||
refresh_dependency () {
|
||||
|
||||
refresh_dependency() {
|
||||
// Resolve "depends_on" and show / hide accordingly
|
||||
var me = this;
|
||||
|
||||
// build dependants' dictionary
|
||||
var has_dep = false;
|
||||
let has_dep = false;
|
||||
|
||||
for (var fkey in this.fields_list) {
|
||||
var f = this.fields_list[fkey];
|
||||
for (let fkey in this.fields_list) {
|
||||
let f = this.fields_list[fkey];
|
||||
f.dependencies_clear = true;
|
||||
if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) {
|
||||
has_dep = true;
|
||||
|
|
@ -475,8 +563,8 @@ frappe.ui.form.Layout = class Layout {
|
|||
if (!has_dep) return;
|
||||
|
||||
// show / hide based on values
|
||||
for (var i = me.fields_list.length - 1; i >= 0; i--) {
|
||||
var f = me.fields_list[i];
|
||||
for (let i = this.fields_list.length - 1; i >= 0; i--) {
|
||||
let f = this.fields_list[i];
|
||||
f.guardian_has_value = true;
|
||||
if (f.df.depends_on) {
|
||||
// evaluate guardian
|
||||
|
|
@ -508,7 +596,8 @@ frappe.ui.form.Layout = class Layout {
|
|||
|
||||
this.refresh_section_count();
|
||||
}
|
||||
set_dependant_property (condition, fieldname, property) {
|
||||
|
||||
set_dependant_property(condition, fieldname, property) {
|
||||
let set_property = this.evaluate_depends_on_value(condition);
|
||||
let value = set_property ? 1 : 0;
|
||||
let form_obj;
|
||||
|
|
@ -530,19 +619,20 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
}
|
||||
}
|
||||
evaluate_depends_on_value (expression) {
|
||||
var out = null;
|
||||
var doc = this.doc;
|
||||
|
||||
evaluate_depends_on_value(expression) {
|
||||
let out = null;
|
||||
let doc = this.doc;
|
||||
|
||||
if (!doc && this.get_values) {
|
||||
var doc = this.get_values(true);
|
||||
doc = this.get_values(true);
|
||||
}
|
||||
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
var parent = this.frm ? this.frm.doc : this.doc || null;
|
||||
let parent = this.frm ? this.frm.doc : this.doc || null;
|
||||
|
||||
if (typeof (expression) === 'boolean') {
|
||||
out = expression;
|
||||
|
|
@ -574,160 +664,3 @@ frappe.ui.form.Layout = class Layout {
|
|||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.form.Section = class FormSection {
|
||||
constructor(layout, df) {
|
||||
this.layout = layout;
|
||||
this.df = df || {};
|
||||
this.fields_list = [];
|
||||
this.fields_dict = {};
|
||||
|
||||
this.make();
|
||||
// if (this.frm)
|
||||
// this.section.body.css({"padding":"0px 3%"})
|
||||
this.row = {
|
||||
wrapper: this.wrapper
|
||||
};
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
make() {
|
||||
if (!this.layout.page) {
|
||||
this.layout.page = $('<div class="form-page"></div>').appendTo(this.layout.wrapper);
|
||||
}
|
||||
let make_card = this.layout.card_layout;
|
||||
this.wrapper = $(`<div class="row form-section ${ make_card ? "card-section" : "" }">`)
|
||||
.appendTo(this.layout.page);
|
||||
this.layout.sections.push(this);
|
||||
|
||||
if (this.df) {
|
||||
if (this.df.label) {
|
||||
this.make_head();
|
||||
}
|
||||
if (this.df.description) {
|
||||
$('<div class="col-sm-12 small text-muted form-section-description">' + __(this.df.description) + '</div>')
|
||||
.appendTo(this.wrapper);
|
||||
}
|
||||
if (this.df.cssClass) {
|
||||
this.wrapper.addClass(this.df.cssClass);
|
||||
}
|
||||
if (this.df.hide_border) {
|
||||
this.wrapper.toggleClass("hide-border", true);
|
||||
}
|
||||
}
|
||||
|
||||
// for bc
|
||||
this.body = $('<div class="section-body">').appendTo(this.wrapper);
|
||||
}
|
||||
|
||||
make_head () {
|
||||
this.head = $(`<div class="section-head">
|
||||
${__(this.df.label)}
|
||||
<span class="ml-2 collapse-indicator mb-1">
|
||||
</span>
|
||||
</div>`);
|
||||
this.head.appendTo(this.wrapper);
|
||||
this.indicator = this.head.find('.collapse-indicator');
|
||||
this.indicator.hide();
|
||||
if (this.df.collapsible) {
|
||||
// show / hide based on status
|
||||
this.collapse_link = this.head.on("click", () => {
|
||||
this.collapse();
|
||||
});
|
||||
|
||||
this.indicator.show();
|
||||
}
|
||||
}
|
||||
refresh() {
|
||||
if (!this.df)
|
||||
return;
|
||||
|
||||
// hide if explictly hidden
|
||||
var hide = this.df.hidden || this.df.hidden_due_to_dependency;
|
||||
|
||||
// hide if no perm
|
||||
if (!hide && this.layout && this.layout.frm && !this.layout.frm.get_perm(this.df.permlevel || 0, "read")) {
|
||||
hide = true;
|
||||
}
|
||||
|
||||
this.wrapper.toggleClass("hide-control", !!hide);
|
||||
}
|
||||
collapse (hide) {
|
||||
// unknown edge case
|
||||
if (!(this.head && this.body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hide===undefined) {
|
||||
hide = !this.body.hasClass("hide");
|
||||
}
|
||||
|
||||
this.body.toggleClass("hide", hide);
|
||||
this.head.toggleClass("collapsed", hide);
|
||||
|
||||
let indicator_icon = hide ? 'down' : 'up-line';
|
||||
|
||||
this.indicator & this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1'));
|
||||
|
||||
// refresh signature fields
|
||||
this.fields_list.forEach((f) => {
|
||||
if (f.df.fieldtype == 'Signature') {
|
||||
f.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
is_collapsed() {
|
||||
return this.body.hasClass('hide');
|
||||
}
|
||||
|
||||
has_missing_mandatory () {
|
||||
var missing_mandatory = false;
|
||||
for (var j = 0, l = this.fields_list.length; j < l; j++) {
|
||||
var section_df = this.fields_list[j].df;
|
||||
if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) {
|
||||
missing_mandatory = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return missing_mandatory;
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.form.Column = class FormColumn {
|
||||
constructor(section, df) {
|
||||
if (!df) df = {};
|
||||
|
||||
this.df = df;
|
||||
this.section = section;
|
||||
this.make();
|
||||
this.resize_all_columns();
|
||||
}
|
||||
make () {
|
||||
this.wrapper = $('<div class="form-column">\
|
||||
<form>\
|
||||
</form>\
|
||||
</div>').appendTo(this.section.body)
|
||||
.find("form")
|
||||
.on("submit", function () {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (this.df.label) {
|
||||
$('<label class="control-label">' + __(this.df.label)
|
||||
+ '</label>').appendTo(this.wrapper);
|
||||
}
|
||||
}
|
||||
resize_all_columns () {
|
||||
// distribute all columns equally
|
||||
var colspan = cint(12 / this.section.wrapper.find(".form-column").length);
|
||||
|
||||
this.section.wrapper.find(".form-column").removeClass()
|
||||
.addClass("form-column")
|
||||
.addClass("col-sm-" + colspan);
|
||||
|
||||
}
|
||||
refresh () {
|
||||
this.section.refresh();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,86 +2,191 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
constructor(opts) {
|
||||
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */
|
||||
Object.assign(this, opts);
|
||||
var me = this;
|
||||
if (this.doctype != "[Select]") {
|
||||
frappe.model.with_doctype(this.doctype, function () {
|
||||
me.make();
|
||||
});
|
||||
this.for_select = this.doctype == "[Select]";
|
||||
if (!this.for_select) {
|
||||
frappe.model.with_doctype(this.doctype, () => this.init());
|
||||
} else {
|
||||
this.make();
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
make() {
|
||||
let me = this;
|
||||
init() {
|
||||
this.page_length = 20;
|
||||
this.start = 0;
|
||||
let fields = this.get_primary_filters();
|
||||
this.fields = this.get_fields();
|
||||
|
||||
// Make results area
|
||||
fields = fields.concat([
|
||||
{ fieldtype: "HTML", fieldname: "results_area" },
|
||||
this.make();
|
||||
}
|
||||
|
||||
get_fields() {
|
||||
const primary_fields = this.get_primary_filters();
|
||||
const result_fields = this.get_result_fields();
|
||||
const data_fields = this.get_data_fields();
|
||||
const child_selection_fields = this.get_child_selection_fields();
|
||||
|
||||
return [...primary_fields, ...result_fields, ...data_fields, ...child_selection_fields];
|
||||
}
|
||||
|
||||
get_result_fields() {
|
||||
const show_next_page = () => {
|
||||
this.start += 20;
|
||||
this.get_results();
|
||||
};
|
||||
return [
|
||||
{
|
||||
fieldtype: "Button", fieldname: "more_btn", label: __("More"),
|
||||
click: () => {
|
||||
this.start += 20;
|
||||
this.get_results();
|
||||
}
|
||||
fieldtype: "HTML", fieldname: "results_area"
|
||||
},
|
||||
{
|
||||
fieldtype: "Button", fieldname: "more_btn",
|
||||
label: __("More"), click: show_next_page.bind(this)
|
||||
}
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
// Custom Data Fields
|
||||
if (this.data_fields) {
|
||||
fields.push({ fieldtype: "Section Break" });
|
||||
fields = fields.concat(this.data_fields);
|
||||
get_data_fields() {
|
||||
if (this.data_fields && this.data_fields.length) {
|
||||
// Custom Data Fields
|
||||
return [
|
||||
{ fieldtype: "Section Break" },
|
||||
...this.data_fields
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
get_child_selection_fields() {
|
||||
const fields = [];
|
||||
if (this.allow_child_item_selection && this.child_fieldname) {
|
||||
fields.push({ fieldtype: "HTML", fieldname: "child_selection_area" });
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
make() {
|
||||
let doctype_plural = this.doctype.plural();
|
||||
let title = __("Select {0}", [this.for_select ? __("value") : __(doctype_plural)]);
|
||||
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
title: __("Select {0}", [(this.doctype == '[Select]') ? __("value") : __(doctype_plural)]),
|
||||
fields: fields,
|
||||
title: title,
|
||||
fields: this.fields,
|
||||
primary_action_label: this.primary_action_label || __("Get Items"),
|
||||
secondary_action_label: __("Make {0}", [__(me.doctype)]),
|
||||
primary_action: function () {
|
||||
let filters_data = me.get_custom_filters();
|
||||
me.action(me.get_checked_values(), cur_dialog.get_values(), me.args, filters_data);
|
||||
secondary_action_label: __("Make {0}", [__(this.doctype)]),
|
||||
primary_action: () => {
|
||||
let filters_data = this.get_custom_filters();
|
||||
const data_values = cur_dialog.get_values(); // to pass values of data fields
|
||||
const filtered_children = this.get_selected_child_names();
|
||||
const selected_documents = [...this.get_checked_values(), ...this.get_parent_name_of_selected_children()];
|
||||
this.action(selected_documents, {
|
||||
...this.args,
|
||||
...data_values,
|
||||
...filters_data,
|
||||
filtered_children
|
||||
});
|
||||
},
|
||||
secondary_action: function (e) {
|
||||
// If user wants to close the modal
|
||||
if (e) {
|
||||
frappe.route_options = {};
|
||||
if (Array.isArray(me.setters)) {
|
||||
for (let df of me.setters) {
|
||||
frappe.route_options[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined;
|
||||
}
|
||||
} else {
|
||||
Object.keys(me.setters).forEach(function (setter) {
|
||||
frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined;
|
||||
});
|
||||
}
|
||||
|
||||
frappe.new_doc(me.doctype, true);
|
||||
}
|
||||
}
|
||||
secondary_action: this.make_new_document.bind(this)
|
||||
});
|
||||
|
||||
if (this.add_filters_group) {
|
||||
this.make_filter_area();
|
||||
}
|
||||
|
||||
this.args = {};
|
||||
|
||||
this.setup_results();
|
||||
this.bind_events();
|
||||
this.get_results();
|
||||
this.dialog.show();
|
||||
}
|
||||
|
||||
make_new_document(e) {
|
||||
// If user wants to close the modal
|
||||
if (e) {
|
||||
this.set_route_options();
|
||||
frappe.new_doc(this.doctype, true);
|
||||
}
|
||||
}
|
||||
|
||||
set_route_options() {
|
||||
// set route options to get pre-filled form fields
|
||||
frappe.route_options = {};
|
||||
if (Array.isArray(this.setters)) {
|
||||
for (let df of this.setters) {
|
||||
frappe.route_options[df.fieldname] = this.dialog.fields_dict[df.fieldname].get_value() || undefined;
|
||||
}
|
||||
} else {
|
||||
Object.keys(this.setters).forEach(setter => {
|
||||
frappe.route_options[setter] = this.dialog.fields_dict[setter].get_value() || undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setup_results() {
|
||||
this.$parent = $(this.dialog.body);
|
||||
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results"
|
||||
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results mt-3"
|
||||
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`);
|
||||
|
||||
this.$results = this.$wrapper.find('.results');
|
||||
this.$results.append(this.make_list_row());
|
||||
}
|
||||
|
||||
this.args = {};
|
||||
toggle_child_selection() {
|
||||
if (this.dialog.fields_dict['allow_child_item_selection'].get_value()) {
|
||||
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();
|
||||
});
|
||||
} else {
|
||||
this.child_results = [];
|
||||
this.get_results();
|
||||
this.$wrapper.removeClass('hidden');
|
||||
this.$child_wrapper.addClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
this.bind_events();
|
||||
this.get_results();
|
||||
this.dialog.show();
|
||||
render_child_datatable() {
|
||||
if (!this.child_datatable) {
|
||||
this.setup_child_datatable();
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.child_datatable.rowmanager.checkMap = [];
|
||||
this.child_datatable.refresh(this.get_child_datatable_rows());
|
||||
this.$child_wrapper.find('.dt-scrollable').css('height', '300px');
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
get_child_datatable_columns() {
|
||||
const parent = this.doctype;
|
||||
return [parent, ...this.child_columns].map(d => ({ name: frappe.unscrub(d), editable: false }));
|
||||
}
|
||||
|
||||
get_child_datatable_rows() {
|
||||
return this.child_results.map(d => Object.values(d).slice(1)); // slice name field
|
||||
}
|
||||
|
||||
setup_child_datatable() {
|
||||
const header_columns = this.get_child_datatable_columns();
|
||||
const rows = this.get_child_datatable_rows();
|
||||
this.$child_wrapper = this.dialog.fields_dict.child_selection_area.$wrapper;
|
||||
this.$child_wrapper.addClass('mt-3');
|
||||
|
||||
this.child_datatable = new frappe.DataTable(this.$child_wrapper.get(0), {
|
||||
columns: header_columns,
|
||||
data: rows,
|
||||
layout: 'fluid',
|
||||
inlineFilters: true,
|
||||
serialNoColumn: false,
|
||||
checkboxColumn: true,
|
||||
cellHeight: 35,
|
||||
noDataMessage: __('No Data'),
|
||||
disableReorderColumn: true
|
||||
});
|
||||
this.$child_wrapper.find('.dt-scrollable').css('height', '300px');
|
||||
}
|
||||
|
||||
get_primary_filters() {
|
||||
|
|
@ -94,7 +199,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
columns[0] = [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Search"),
|
||||
label: __("Name"),
|
||||
fieldname: "search_term"
|
||||
}
|
||||
];
|
||||
|
|
@ -127,6 +232,16 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
// now a is a fixed-size array with mutable entries
|
||||
}
|
||||
|
||||
if (this.allow_child_item_selection) {
|
||||
this.child_doctype = frappe.meta.get_docfield(this.doctype, this.child_fieldname).options;
|
||||
columns[0].push({
|
||||
fieldtype: "Check",
|
||||
label: __("Select {0}", [this.child_doctype]),
|
||||
fieldname: "allow_child_item_selection",
|
||||
onchange: this.toggle_child_selection.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
fields = [
|
||||
...columns[0],
|
||||
{ fieldtype: "Column Break" },
|
||||
|
|
@ -156,6 +271,9 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
this.get_results();
|
||||
}
|
||||
});
|
||||
// 'Apply Filter' breaks since the filers are not in a popover
|
||||
// Hence keeping it hidden
|
||||
this.filter_group.wrapper.find('.apply-filters').hide();
|
||||
}
|
||||
|
||||
get_custom_filters() {
|
||||
|
|
@ -166,7 +284,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
});
|
||||
}, {});
|
||||
} else {
|
||||
return [];
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,6 +318,34 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
});
|
||||
}
|
||||
|
||||
get_parent_name_of_selected_children() {
|
||||
if (!this.child_datatable || !this.child_datatable.datamanager.rows.length) return [];
|
||||
|
||||
let parent_names = this.child_datatable.rowmanager.checkMap.reduce((parent_names, checked, index) => {
|
||||
if (checked == 1) {
|
||||
const parent_name = this.child_results[index].parent;
|
||||
parent_names.push(parent_name);
|
||||
}
|
||||
return parent_names;
|
||||
}, []);
|
||||
|
||||
return parent_names;
|
||||
}
|
||||
|
||||
get_selected_child_names() {
|
||||
if (!this.child_datatable || !this.child_datatable.datamanager.rows.length) return [];
|
||||
|
||||
let checked_names = this.child_datatable.rowmanager.checkMap.reduce((checked_names, checked, index) => {
|
||||
if (checked == 1) {
|
||||
const child_row_name = this.child_results[index].name;
|
||||
checked_names.push(child_row_name);
|
||||
}
|
||||
return checked_names;
|
||||
}, []);
|
||||
|
||||
return checked_names;
|
||||
}
|
||||
|
||||
get_checked_values() {
|
||||
// Return name of checked value.
|
||||
return this.$results.find('.list-item-container').map(function () {
|
||||
|
|
@ -276,6 +422,8 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
me.$results.append(me.make_list_row(result));
|
||||
});
|
||||
|
||||
this.$results.find(".list-item--head").css("z-index", 0);
|
||||
|
||||
if (frappe.flags.auto_scroll) {
|
||||
this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500);
|
||||
}
|
||||
|
|
@ -297,7 +445,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
this.render_result_list(checked, 0, false);
|
||||
}
|
||||
|
||||
get_results() {
|
||||
get_filters_from_setters() {
|
||||
let me = this;
|
||||
let filters = this.get_query ? this.get_query().filters : {} || {};
|
||||
let filter_fields = [];
|
||||
|
|
@ -321,12 +469,18 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
});
|
||||
}
|
||||
|
||||
let filter_group = this.get_custom_filters();
|
||||
Object.assign(filters, filter_group);
|
||||
return [filters, filter_fields];
|
||||
}
|
||||
|
||||
let args = {
|
||||
doctype: me.doctype,
|
||||
txt: me.dialog.fields_dict["search_term"].get_value(),
|
||||
get_args_for_search() {
|
||||
let [filters, filter_fields] = this.get_filters_from_setters();
|
||||
|
||||
let custom_filters = this.get_custom_filters();
|
||||
Object.assign(filters, custom_filters);
|
||||
|
||||
return {
|
||||
doctype: this.doctype,
|
||||
txt: this.dialog.fields_dict["search_term"].get_value(),
|
||||
filters: filters,
|
||||
filter_fields: filter_fields,
|
||||
start: this.start,
|
||||
|
|
@ -334,25 +488,81 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
query: this.get_query ? this.get_query().query : '',
|
||||
as_dict: 1
|
||||
};
|
||||
frappe.call({
|
||||
}
|
||||
|
||||
async perform_search(args) {
|
||||
const res = await frappe.call({
|
||||
type: "GET",
|
||||
method: 'frappe.desk.search.search_widget',
|
||||
no_spinner: true,
|
||||
args: args,
|
||||
callback: function (r) {
|
||||
let more = 0;
|
||||
me.results = [];
|
||||
if (r.values.length) {
|
||||
if (r.values.length > me.page_length) {
|
||||
r.values.pop();
|
||||
more = 1;
|
||||
}
|
||||
r.values.forEach(function (result) {
|
||||
result.checked = 0;
|
||||
me.results.push(result);
|
||||
});
|
||||
});
|
||||
const more = res.values.length && res.values.length > this.page_length ? 1 : 0;
|
||||
if (more) {
|
||||
res.values.pop();
|
||||
}
|
||||
|
||||
return [res, more];
|
||||
}
|
||||
|
||||
async get_results() {
|
||||
const args = this.get_args_for_search();
|
||||
const [res, more] = await this.perform_search(args);
|
||||
|
||||
this.results = [];
|
||||
if (res.values.length) {
|
||||
res.values.forEach(result => {
|
||||
result.checked = 0;
|
||||
this.results.push(result);
|
||||
});
|
||||
}
|
||||
this.render_result_list(this.results, more);
|
||||
}
|
||||
|
||||
async get_filtered_parents_for_child_search() {
|
||||
const parent_search_args = this.get_args_for_search();
|
||||
parent_search_args.filter_fields = ['name'];
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [response, _] = await this.perform_search(parent_search_args);
|
||||
|
||||
let parent_names = [];
|
||||
if (response.values.length) {
|
||||
parent_names = response.values.map(v => v.name);
|
||||
}
|
||||
return parent_names;
|
||||
}
|
||||
|
||||
async add_parent_filters(filters) {
|
||||
const parent_names = await this.get_filtered_parents_for_child_search();
|
||||
if (parent_names.length) {
|
||||
filters.push([ "parent", "in", parent_names ]);
|
||||
}
|
||||
}
|
||||
|
||||
add_custom_child_filters(filters) {
|
||||
if (this.add_filters_group && this.filter_group) {
|
||||
this.filter_group.get_filters().forEach(filter => {
|
||||
if (filter[0] == this.child_doctype) {
|
||||
filters.push([filter[1], filter[2], filter[3]]);
|
||||
}
|
||||
me.render_result_list(me.results, more);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async get_child_result() {
|
||||
let filters = [["parentfield", "=", this.child_fieldname]];
|
||||
|
||||
await this.add_parent_filters(filters);
|
||||
this.add_custom_child_filters(filters);
|
||||
|
||||
return frappe.call({
|
||||
method: "frappe.client.get_list",
|
||||
args: {
|
||||
doctype: this.child_doctype,
|
||||
filters: filters,
|
||||
fields: ['name', 'parent', ...this.child_columns],
|
||||
parent: this.doctype,
|
||||
order_by: 'parent'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
146
frappe/public/js/frappe/form/section.js
Normal file
146
frappe/public/js/frappe/form/section.js
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
export default class Section {
|
||||
constructor(parent, df, card_layout) {
|
||||
this.card_layout = card_layout;
|
||||
this.parent = parent;
|
||||
this.df = df || {};
|
||||
this.fields_list = [];
|
||||
this.fields_dict = {};
|
||||
|
||||
this.make();
|
||||
|
||||
if (this.df.label && this.df.collapsible && localStorage.getItem(df.css_class + '-closed')) {
|
||||
this.collapse();
|
||||
}
|
||||
|
||||
this.row = {
|
||||
wrapper: this.wrapper
|
||||
};
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
make() {
|
||||
let make_card = this.card_layout;
|
||||
this.wrapper = $(`<div class="row
|
||||
${this.df.is_dashboard_section ? "form-dashboard-section" : "form-section"}
|
||||
${ make_card ? "card-section" : "" }">
|
||||
`).appendTo(this.parent);
|
||||
|
||||
if (this.df) {
|
||||
if (this.df.label) {
|
||||
this.make_head();
|
||||
}
|
||||
if (this.df.description) {
|
||||
this.description_wrapper = $(
|
||||
`<div class="col-sm-12 form-section-description">
|
||||
${__(this.df.description)}
|
||||
</div>`
|
||||
);
|
||||
|
||||
this.wrapper.append(this.description_wrapper);
|
||||
}
|
||||
if (this.df.css_class) {
|
||||
this.wrapper.addClass(this.df.css_class);
|
||||
}
|
||||
if (this.df.hide_border) {
|
||||
this.wrapper.toggleClass("hide-border", true);
|
||||
}
|
||||
}
|
||||
|
||||
this.body = $('<div class="section-body">').appendTo(this.wrapper);
|
||||
|
||||
if (this.df.body_html) {
|
||||
this.body.append(this.df.body_html);
|
||||
}
|
||||
}
|
||||
|
||||
make_head() {
|
||||
this.head = $(`
|
||||
<div class="section-head">
|
||||
${__(this.df.label)}
|
||||
<span class="ml-2 collapse-indicator mb-1"></span>
|
||||
</div>
|
||||
`);
|
||||
|
||||
this.head.appendTo(this.wrapper);
|
||||
this.indicator = this.head.find('.collapse-indicator');
|
||||
this.indicator.hide();
|
||||
|
||||
if (this.df.collapsible) {
|
||||
// show / hide based on status
|
||||
this.collapse_link = this.head.on("click", () => {
|
||||
this.collapse();
|
||||
});
|
||||
this.set_icon();
|
||||
this.indicator.show();
|
||||
}
|
||||
}
|
||||
|
||||
refresh(hide) {
|
||||
if (!this.df) return;
|
||||
// hide if explicitly hidden
|
||||
hide = hide || this.df.hidden || this.df.hidden_due_to_dependency;
|
||||
this.wrapper.toggleClass("hide-control", !!hide);
|
||||
}
|
||||
|
||||
collapse(hide) {
|
||||
// unknown edge case
|
||||
if (!(this.head && this.body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hide === undefined) {
|
||||
hide = !this.body.hasClass("hide");
|
||||
}
|
||||
|
||||
this.body.toggleClass("hide", hide);
|
||||
this.head && this.head.toggleClass("collapsed", hide);
|
||||
|
||||
this.set_icon(hide);
|
||||
|
||||
// refresh signature fields
|
||||
this.fields_list.forEach((f) => {
|
||||
if (f.df.fieldtype == 'Signature') {
|
||||
f.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// save state for next reload ('' is falsy)
|
||||
if (this.df.css_class)
|
||||
localStorage.setItem(this.df.css_class + '-closed', hide ? '1' : '');
|
||||
}
|
||||
|
||||
set_icon(hide) {
|
||||
let indicator_icon = hide ? 'down' : 'up-line';
|
||||
this.indicator && this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1'));
|
||||
}
|
||||
|
||||
is_collapsed() {
|
||||
return this.body.hasClass('hide');
|
||||
}
|
||||
|
||||
has_missing_mandatory () {
|
||||
let missing_mandatory = false;
|
||||
for (let j = 0, l = this.fields_list.length; j < l; j++) {
|
||||
const section_df = this.fields_list[j].df;
|
||||
if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) {
|
||||
missing_mandatory = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return missing_mandatory;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.on_section_toggle(false);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.on_section_toggle(true);
|
||||
}
|
||||
|
||||
on_section_toggle(show) {
|
||||
this.wrapper.toggleClass("hide-control", !show);
|
||||
// this.on_section_toggle && this.on_section_toggle(show);
|
||||
}
|
||||
}
|
||||
75
frappe/public/js/frappe/form/tab.js
Normal file
75
frappe/public/js/frappe/form/tab.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
export default class Tab {
|
||||
constructor(parent, df, frm, tabs_list, tabs_content) {
|
||||
this.parent = parent;
|
||||
this.df = df || {};
|
||||
this.frm = frm;
|
||||
this.doctype = 'User';
|
||||
this.label = this.df && this.df.label;
|
||||
this.tabs_list = tabs_list;
|
||||
this.tabs_content = tabs_content;
|
||||
this.fields_list = [];
|
||||
this.fields_dict = {};
|
||||
this.make();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
make() {
|
||||
const id = `${frappe.scrub(this.doctype, '-')}-${this.df.fieldname}`;
|
||||
this.parent = $(`
|
||||
<li class="nav-item">
|
||||
<a class="nav-link ${this.df.active ? "active": ""}" id="${id}-tab"
|
||||
data-toggle="tab"
|
||||
href="#${id}"
|
||||
role="tab"
|
||||
aria-controls="${this.label}">
|
||||
${__(this.label)}
|
||||
</a>
|
||||
</li>
|
||||
`).appendTo(this.tabs_list);
|
||||
|
||||
this.wrapper = $(`<div class="tab-pane fade show ${this.df.active ? "active": ""}"
|
||||
id="${id}" role="tabpanel" aria-labelledby="${id}-tab">`).appendTo(this.tabs_content);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (!this.df) return;
|
||||
|
||||
// hide if explicitly hidden
|
||||
let hide = this.df.hidden || this.df.hidden_due_to_dependency;
|
||||
if (!hide && this.frm && !this.frm.get_perm(this.df.permlevel || 0, "read")) {
|
||||
hide = true;
|
||||
}
|
||||
|
||||
hide && this.toggle(false);
|
||||
}
|
||||
|
||||
toggle(show) {
|
||||
this.parent.toggleClass('hide', !show);
|
||||
this.wrapper.toggleClass('hide', !show);
|
||||
this.parent.toggleClass('show', show);
|
||||
this.wrapper.toggleClass('show', show);
|
||||
this.hidden = !show;
|
||||
}
|
||||
|
||||
show() {
|
||||
this.parent.show();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.parent.hide();
|
||||
}
|
||||
|
||||
set_active() {
|
||||
this.parent.find('.nav-link').tab('show');
|
||||
this.wrapper.addClass('show');
|
||||
}
|
||||
|
||||
is_active() {
|
||||
return this.wrapper.hasClass('active');
|
||||
}
|
||||
|
||||
is_hidden() {
|
||||
this.wrapper.hasClass('hide')
|
||||
&& this.parent.hasClass('hide');
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ frappe.views.BaseList = class BaseList {
|
|||
}
|
||||
|
||||
show() {
|
||||
frappe.run_serially([
|
||||
return frappe.run_serially([
|
||||
() => this.show_skeleton(),
|
||||
() => this.fetch_meta(),
|
||||
() => this.hide_skeleton(),
|
||||
() => this.check_permissions(),
|
||||
() => this.init(),
|
||||
() => this.before_refresh(),
|
||||
() => this.refresh(),
|
||||
|
|
@ -150,6 +154,22 @@ frappe.views.BaseList = class BaseList {
|
|||
}
|
||||
}
|
||||
|
||||
fetch_meta() {
|
||||
return frappe.model.with_doctype(this.doctype);
|
||||
}
|
||||
|
||||
show_skeleton() {
|
||||
|
||||
}
|
||||
|
||||
hide_skeleton() {
|
||||
|
||||
}
|
||||
|
||||
check_permissions() {
|
||||
return true;
|
||||
}
|
||||
|
||||
setup_page() {
|
||||
this.page = this.parent.page;
|
||||
this.$page = $(this.parent);
|
||||
|
|
@ -387,6 +407,14 @@ frappe.views.BaseList = class BaseList {
|
|||
);
|
||||
}
|
||||
|
||||
get_group_by() {
|
||||
let name_field = this.fields && this.fields.find(f => f[0] == 'name');
|
||||
if (name_field) {
|
||||
return frappe.model.get_full_column_name(name_field[0], name_field[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
setup_view() {
|
||||
// for child classes
|
||||
}
|
||||
|
|
@ -417,6 +445,7 @@ frappe.views.BaseList = class BaseList {
|
|||
start: this.start,
|
||||
page_length: this.page_length,
|
||||
view: this.view,
|
||||
group_by: this.get_group_by()
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -463,8 +492,6 @@ frappe.views.BaseList = class BaseList {
|
|||
} else {
|
||||
this.data = this.data.concat(data);
|
||||
}
|
||||
|
||||
this.data = this.data.uniqBy((d) => d.name);
|
||||
}
|
||||
|
||||
freeze() {
|
||||
|
|
|
|||
|
|
@ -9,35 +9,31 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory {
|
|||
var me = this;
|
||||
var doctype = route[1];
|
||||
|
||||
frappe.model.with_doctype(doctype, function () {
|
||||
if (locals['DocType'][doctype].issingle) {
|
||||
frappe.set_re_route('Form', doctype);
|
||||
} else {
|
||||
// List / Gantt / Kanban / etc
|
||||
// File is a special view
|
||||
const view_name = doctype !== 'File' ? frappe.utils.to_title_case(route[2] || 'List') : 'File';
|
||||
let view_class = frappe.views[view_name + 'View'];
|
||||
if (!view_class) view_class = frappe.views.ListView;
|
||||
// List / Gantt / Kanban / etc
|
||||
// File is a special view
|
||||
const view_name = doctype !== 'File' ? frappe.utils.to_title_case(route[2] || 'List') : 'File';
|
||||
let view_class = frappe.views[view_name + 'View'];
|
||||
if (!view_class) view_class = frappe.views.ListView;
|
||||
|
||||
if (view_class && view_class.load_last_view && view_class.load_last_view()) {
|
||||
// view can have custom routing logic
|
||||
return;
|
||||
}
|
||||
if (view_class && view_class.load_last_view && view_class.load_last_view()) {
|
||||
// view can have custom routing logic
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.provide('frappe.views.list_view.' + doctype);
|
||||
const page_name = frappe.get_route_str();
|
||||
|
||||
if (!frappe.views.list_view[page_name]) {
|
||||
frappe.views.list_view[page_name] = new view_class({
|
||||
doctype: doctype,
|
||||
parent: me.make_page(true, page_name)
|
||||
});
|
||||
} else {
|
||||
frappe.container.change_to(page_name);
|
||||
}
|
||||
me.set_cur_list();
|
||||
|
||||
frappe.provide('frappe.views.list_view.' + doctype);
|
||||
const page_name = frappe.get_route_str();
|
||||
|
||||
if (!frappe.views.list_view[page_name]) {
|
||||
frappe.views.list_view[page_name] = new view_class({
|
||||
doctype: doctype,
|
||||
parent: me.make_page(true, page_name)
|
||||
});
|
||||
} else {
|
||||
frappe.container.change_to(page_name);
|
||||
}
|
||||
me.set_cur_list();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
|
|
|
|||
|
|
@ -33,14 +33,38 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
|
||||
show() {
|
||||
this.parent.disable_scroll_to_top = true;
|
||||
super.show();
|
||||
}
|
||||
|
||||
check_permissions() {
|
||||
if (!this.has_permissions()) {
|
||||
frappe.set_route('');
|
||||
frappe.msgprint(__("Not permitted to view {0}", [this.doctype]));
|
||||
return;
|
||||
frappe.throw(__("Not permitted to view {0}", [this.doctype]));
|
||||
}
|
||||
}
|
||||
|
||||
super.show();
|
||||
show_skeleton() {
|
||||
this.$list_skeleton = this.parent.page.container.find('.list-skeleton');
|
||||
if (!this.$list_skeleton.length) {
|
||||
this.$list_skeleton = $(`
|
||||
<div class="row list-skeleton">
|
||||
<div class="col-lg-2">
|
||||
<div class="list-skeleton-box"></div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="list-skeleton-box"></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
this.parent.page.container.find('.page-content').append(this.$list_skeleton);
|
||||
}
|
||||
this.parent.page.container.find('.layout-main').hide();
|
||||
this.$list_skeleton.show();
|
||||
}
|
||||
|
||||
hide_skeleton() {
|
||||
this.$list_skeleton && this.$list_skeleton.hide();
|
||||
this.parent.page.container.find('.layout-main').show();
|
||||
}
|
||||
|
||||
get view_name() {
|
||||
|
|
@ -548,6 +572,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
|
||||
render() {
|
||||
this.render_list();
|
||||
this.set_rows_as_checked();
|
||||
this.on_row_checked();
|
||||
this.render_count();
|
||||
}
|
||||
|
|
@ -583,9 +608,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
|
||||
const subject_field = this.columns[0].df;
|
||||
let subject_html = `
|
||||
<input class="level-item list-check-all hidden-xs" type="checkbox"
|
||||
<input class="level-item list-check-all" type="checkbox"
|
||||
title="${__("Select All")}">
|
||||
<span class="level-item list-liked-by-me">
|
||||
<span class="level-item list-liked-by-me hidden-xs">
|
||||
<span title="${__("Likes")}">${frappe.utils.icon('heart', 'sm', 'like-icon')}</span>
|
||||
</span>
|
||||
<span class="level-item">${__(subject_field.label)}</span>
|
||||
|
|
@ -622,7 +647,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
</div>
|
||||
<div class="level-left checkbox-actions">
|
||||
<div class="level list-subject">
|
||||
<input class="level-item list-check-all hidden-xs" type="checkbox"
|
||||
<input class="level-item list-check-all" type="checkbox"
|
||||
title="${__("Select All")}">
|
||||
<span class="level-item list-header-meta"></span>
|
||||
</div>
|
||||
|
|
@ -930,9 +955,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
|
||||
let subject_html = `
|
||||
<span class="level-item select-like">
|
||||
<input class="list-row-checkbox hidden-xs" type="checkbox"
|
||||
<input class="list-row-checkbox" type="checkbox"
|
||||
data-name="${escape(doc.name)}">
|
||||
<span class="list-row-like style="margin-bottom: 1px;">
|
||||
<span class="list-row-like hidden-xs style="margin-bottom: 1px;">
|
||||
${this.get_like_html(doc)}
|
||||
</span>
|
||||
</span>
|
||||
|
|
@ -1139,6 +1164,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
if (
|
||||
$target.hasClass("filterable") ||
|
||||
$target.hasClass("select-like") ||
|
||||
$target.hasClass("file-select") ||
|
||||
$target.hasClass("list-row-like") ||
|
||||
$target.is(":checkbox")
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
frappe.provide('frappe.model');
|
||||
|
||||
$.extend(frappe.model, {
|
||||
no_value_type: ['Section Break', 'Column Break', 'HTML', 'Table', 'Table MultiSelect',
|
||||
no_value_type: ['Section Break', 'Column Break', 'Tab Break', 'HTML', 'Table', 'Table MultiSelect',
|
||||
'Button', 'Image', 'Fold', 'Heading'],
|
||||
|
||||
layout_fields: ['Section Break', 'Column Break', 'Fold'],
|
||||
layout_fields: ['Section Break', 'Column Break', 'Tab Break', 'Fold'],
|
||||
|
||||
std_fields_list: ['name', 'owner', 'creation', 'modified', 'modified_by',
|
||||
'_user_tags', '_comments', '_assign', '_liked_by', 'docstatus',
|
||||
|
|
@ -131,6 +131,7 @@ $.extend(frappe.model, {
|
|||
with_doctype: function(doctype, callback, async) {
|
||||
if(locals.DocType[doctype]) {
|
||||
callback && callback();
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
let cached_timestamp = null;
|
||||
let cached_doc = null;
|
||||
|
|
@ -464,31 +465,31 @@ $.extend(frappe.model, {
|
|||
},
|
||||
|
||||
trigger: function(fieldname, value, doc) {
|
||||
let tasks = [];
|
||||
var runner = function(events, event_doc) {
|
||||
$.each(events || [], function(i, fn) {
|
||||
if(fn) {
|
||||
let _promise = fn(fieldname, value, event_doc || doc);
|
||||
const tasks = [];
|
||||
|
||||
function enqueue_events(events) {
|
||||
if (!events) return;
|
||||
|
||||
for (const fn of events) {
|
||||
if (!fn) continue;
|
||||
|
||||
tasks.push(() => {
|
||||
const return_value = fn(fieldname, value, doc);
|
||||
|
||||
// if the trigger returns a promise, return it,
|
||||
// or use the default promise frappe.after_ajax
|
||||
if (_promise && _promise.then) {
|
||||
return _promise;
|
||||
if (return_value && return_value.then) {
|
||||
return return_value;
|
||||
} else {
|
||||
return frappe.after_server_call();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if(frappe.model.events[doc.doctype]) {
|
||||
tasks.push(() => {
|
||||
return runner(frappe.model.events[doc.doctype][fieldname]);
|
||||
});
|
||||
|
||||
tasks.push(() => {
|
||||
return runner(frappe.model.events[doc.doctype]['*']);
|
||||
});
|
||||
enqueue_events(frappe.model.events[doc.doctype][fieldname]);
|
||||
enqueue_events(frappe.model.events[doc.doctype]['*']);
|
||||
}
|
||||
|
||||
return frappe.run_serially(tasks);
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
|
|||
|
||||
set_secondary_action(click) {
|
||||
this.footer.removeClass('hide');
|
||||
this.get_secondary_btn().removeClass('hide').on('click', click);
|
||||
this.get_secondary_btn().removeClass('hide').off('click').on('click', click);
|
||||
}
|
||||
|
||||
set_secondary_action_label(label) {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue