Quellcode durchsuchen

Merge branch 'develop' into coverage

version-14
Suraj Shetty vor 3 Jahren
committed by GitHub
Ursprung
Commit
4d435e271e
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden GPG-Schlüssel-ID: 4AEE18F83AFDEB23
100 geänderte Dateien mit 3373 neuen und 2579 gelöschten Zeilen
  1. +2
    -1
      .github/helper/install.sh
  2. +1
    -1
      .github/workflows/docs-checker.yml
  3. +1
    -1
      .github/workflows/patch-mariadb-tests.yml
  4. +1
    -1
      .github/workflows/publish-assets-develop.yml
  5. +1
    -1
      .github/workflows/publish-assets-releases.yml
  6. +2
    -2
      .github/workflows/server-mariadb-tests.yml
  7. +1
    -1
      .github/workflows/server-postgres-tests.yml
  8. +0
    -22
      .github/workflows/translation_linter.yml
  9. +1
    -1
      .github/workflows/ui-tests.yml
  10. +59
    -0
      cypress/fixtures/doctype_with_tab_break.js
  11. +93
    -0
      cypress/integration/control_float.js
  12. +8
    -5
      cypress/integration/dashboard_links.js
  13. +1
    -1
      cypress/integration/folder_navigation.js
  14. +4
    -1
      cypress/integration/form.js
  15. +31
    -0
      cypress/integration/form_tab_break.js
  16. +20
    -2
      cypress/integration/list_view.js
  17. +58
    -0
      cypress/integration/multi_select_dialog.js
  18. +12
    -1
      cypress/integration/navigation.js
  19. +1
    -0
      cypress/integration/timeline.js
  20. +6
    -2
      cypress/integration/timeline_email.js
  21. +2
    -1
      frappe/__init__.py
  22. +78
    -56
      frappe/build.py
  23. +18
    -3
      frappe/commands/__init__.py
  24. +1
    -1
      frappe/commands/redis_utils.py
  25. +132
    -5
      frappe/commands/site.py
  26. +101
    -12
      frappe/commands/utils.py
  27. +1
    -1
      frappe/contacts/address_and_contact.py
  28. +1
    -1
      frappe/contacts/doctype/address/address.py
  29. +2
    -2
      frappe/contacts/doctype/contact/contact.py
  30. +32
    -16
      frappe/core/doctype/access_log/access_log.py
  31. +541
    -542
      frappe/core/doctype/docfield/docfield.json
  32. +2
    -0
      frappe/core/doctype/doctype/doctype.py
  33. +2
    -1
      frappe/core/doctype/document_naming_rule/document_naming_rule.json
  34. +66
    -224
      frappe/core/doctype/sms_settings/sms_settings.json
  35. +1
    -1
      frappe/core/doctype/user/user.py
  36. +0
    -21
      frappe/core/doctype/version/version.css
  37. +0
    -2
      frappe/core/doctype/version/version.py
  38. +2
    -1
      frappe/core/page/background_jobs/background_jobs.py
  39. +456
    -458
      frappe/custom/doctype/custom_field/custom_field.json
  40. +1
    -1
      frappe/custom/doctype/custom_field/custom_field.py
  41. +2
    -2
      frappe/custom/doctype/customize_form_field/customize_form_field.json
  42. +1
    -1
      frappe/custom/doctype/property_setter/property_setter.py
  43. +0
    -45
      frappe/data/sample_site_config.json
  44. +5
    -5
      frappe/database/database.py
  45. +7
    -7
      frappe/database/mariadb/database.py
  46. +11
    -11
      frappe/database/mariadb/framework_mariadb.sql
  47. +14
    -8
      frappe/database/mariadb/schema.py
  48. +7
    -9
      frappe/database/mariadb/setup_db.py
  49. +8
    -7
      frappe/database/postgres/database.py
  50. +2
    -0
      frappe/database/schema.py
  51. +105
    -321
      frappe/desk/doctype/note/note.json
  52. +84
    -4
      frappe/desk/doctype/system_console/system_console.js
  53. +43
    -3
      frappe/desk/doctype/system_console/system_console.json
  54. +12
    -4
      frappe/desk/doctype/system_console/system_console.py
  55. +22
    -33
      frappe/desk/doctype/tag/tag.py
  56. +14
    -1
      frappe/desk/doctype/tag_link/tag_link.json
  57. +5
    -4
      frappe/desk/doctype/workspace/workspace.json
  58. +3
    -3
      frappe/desk/doctype/workspace/workspace.py
  59. +1
    -1
      frappe/desk/reportview.py
  60. +4
    -4
      frappe/email/doctype/email_account/email_account.json
  61. +1
    -0
      frappe/email/doctype/notification/notification.py
  62. +5
    -0
      frappe/email/doctype/notification/test_notification.py
  63. +3
    -2
      frappe/event_streaming/doctype/event_producer/event_producer.py
  64. +4
    -1
      frappe/handler.py
  65. +5
    -4
      frappe/hooks.py
  66. +13
    -1
      frappe/installer.py
  67. +2
    -0
      frappe/model/__init__.py
  68. +1
    -1
      frappe/model/base_document.py
  69. +7
    -2
      frappe/model/db_query.py
  70. +38
    -16
      frappe/model/meta.py
  71. +1
    -1
      frappe/patches/v13_0/increase_password_length.py
  72. +0
    -0
      frappe/printing/doctype/network_printer_settings/__init__.py
  73. +29
    -0
      frappe/printing/doctype/network_printer_settings/network_printer_settings.js
  74. +66
    -0
      frappe/printing/doctype/network_printer_settings/network_printer_settings.json
  75. +37
    -0
      frappe/printing/doctype/network_printer_settings/network_printer_settings.py
  76. +8
    -0
      frappe/printing/doctype/network_printer_settings/test_network_printer_settings.py
  77. +0
    -22
      frappe/printing/doctype/print_settings/print_settings.js
  78. +4
    -25
      frappe/printing/doctype/print_settings/print_settings.json
  79. +0
    -20
      frappe/printing/doctype/print_settings/print_settings.py
  80. +102
    -63
      frappe/printing/page/print/print.js
  81. +3
    -3
      frappe/printing/page/print_format_builder/print_format_builder.js
  82. +1
    -1
      frappe/printing/page/print_format_builder/print_format_builder_sidebar.html
  83. +3
    -0
      frappe/public/js/desk.bundle.js
  84. +49
    -0
      frappe/public/js/frappe/form/column.js
  85. +13
    -0
      frappe/public/js/frappe/form/controls/float.js
  86. +50
    -154
      frappe/public/js/frappe/form/dashboard.js
  87. +4
    -2
      frappe/public/js/frappe/form/footer/form_timeline.js
  88. +29
    -11
      frappe/public/js/frappe/form/form.js
  89. +14
    -12
      frappe/public/js/frappe/form/grid.js
  90. +6
    -4
      frappe/public/js/frappe/form/grid_row_form.js
  91. +173
    -240
      frappe/public/js/frappe/form/layout.js
  92. +282
    -72
      frappe/public/js/frappe/form/multi_select_dialog.js
  93. +146
    -0
      frappe/public/js/frappe/form/section.js
  94. +75
    -0
      frappe/public/js/frappe/form/tab.js
  95. +1
    -1
      frappe/public/js/frappe/form/toolbar.js
  96. +30
    -3
      frappe/public/js/frappe/list/base_list.js
  97. +22
    -26
      frappe/public/js/frappe/list/list_factory.js
  98. +34
    -8
      frappe/public/js/frappe/list/list_view.js
  99. +19
    -18
      frappe/public/js/frappe/model/model.js
  100. +1
    -1
      frappe/public/js/frappe/ui/dialog.js

+ 2
- 1
.github/helper/install.sh Datei anzeigen

@@ -17,6 +17,7 @@ if [ "$TYPE" == "server" ]; then
fi fi


if [ "$DB" == "mariadb" ];then 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 character_set_server = 'utf8mb4'";
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; 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 start &
bench --site test_site reinstall --yes bench --site test_site reinstall --yes
if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi
bench build --app frappe
bench build --app frappe

+ 1
- 1
.github/workflows/docs-checker.yml Datei anzeigen

@@ -12,7 +12,7 @@ jobs:
- name: 'Setup Environment' - name: 'Setup Environment'
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.6
python-version: 3.7


- name: 'Clone repo' - name: 'Clone repo'
uses: actions/checkout@v2 uses: actions/checkout@v2


+ 1
- 1
.github/workflows/patch-mariadb-tests.yml Datei anzeigen

@@ -9,7 +9,7 @@ concurrency:


jobs: jobs:
test: test:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest


name: Patch Test name: Patch Test




+ 1
- 1
.github/workflows/publish-assets-develop.yml Datei anzeigen

@@ -18,7 +18,7 @@ jobs:
node-version: 14 node-version: 14
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: '3.6'
python-version: '3.7'
- name: Set up bench and build assets - name: Set up bench and build assets
run: | run: |
npm install -g yarn npm install -g yarn


+ 1
- 1
.github/workflows/publish-assets-releases.yml Datei anzeigen

@@ -21,7 +21,7 @@ jobs:
python-version: '12.x' python-version: '12.x'
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: '3.6'
python-version: '3.7'
- name: Set up bench and build assets - name: Set up bench and build assets
run: | run: |
npm install -g yarn npm install -g yarn


+ 2
- 2
.github/workflows/server-mariadb-tests.yml Datei anzeigen

@@ -13,7 +13,7 @@ concurrency:


jobs: jobs:
test: test:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest


strategy: strategy:
fail-fast: false fail-fast: false
@@ -127,4 +127,4 @@ jobs:
name: MariaDB name: MariaDB
fail_ci_if_error: true fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true
verbose: true

+ 1
- 1
.github/workflows/server-postgres-tests.yml Datei anzeigen

@@ -12,7 +12,7 @@ concurrency:


jobs: jobs:
test: test:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest


strategy: strategy:
fail-fast: false fail-fast: false


+ 0
- 22
.github/workflows/translation_linter.yml Datei anzeigen

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

+ 1
- 1
.github/workflows/ui-tests.yml Datei anzeigen

@@ -12,7 +12,7 @@ concurrency:


jobs: jobs:
test: test:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest


strategy: strategy:
fail-fast: false fail-fast: false


+ 59
- 0
cypress/fixtures/doctype_with_tab_break.js Datei anzeigen

@@ -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
- 0
cypress/integration/control_float.js Datei anzeigen

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

+ 8
- 5
cypress/integration/dashboard_links.js Datei anzeigen

@@ -9,17 +9,20 @@ context('Dashboard links', () => {
cy.clear_filters(); cy.clear_filters();


cy.visit('/app/user'); 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 //To check if initially the dashboard contains only the "Contact" link and there is no counter
cy.get('[data-doctype="Contact"]').should('contain', 'Contact'); cy.get('[data-doctype="Contact"]').should('contain', 'Contact');


//Adding a new 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.get('[data-doctype="Contact"][data-fieldname="first_name"]').type('Admin');
cy.findByRole('button', {name: 'Save'}).click(); cy.findByRole('button', {name: 'Save'}).click();
cy.visit('/app/user'); 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 //To check if the counter for contact doc is "1" after adding the contact
cy.get('[data-doctype="Contact"] > .count').should('contain', '1'); cy.get('[data-doctype="Contact"] > .count').should('contain', '1');
@@ -27,7 +30,7 @@ context('Dashboard links', () => {


//Deleting the newly created contact //Deleting the newly created contact
cy.visit('/app/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.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click(); cy.get('.actions-btn-group [data-label="Delete"]').click();
cy.findByRole('button', {name: 'Yes'}).click({delay: 700}); 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 //To check if the counter from the "Contact" doc link is removed
cy.wait(700); cy.wait(700);
cy.visit('/app/user'); 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'); cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
}); });




+ 1
- 1
cypress/integration/folder_navigation.js Datei anzeigen

@@ -71,7 +71,7 @@ context('Folder Navigation', () => {
it('Deleting Test Folder from the home', () => { it('Deleting Test Folder from the home', () => {
//Deleting the Test Folder added in the home directory //Deleting the Test Folder added in the home directory
cy.visit('/app/file/view/home'); 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.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click(); cy.get('.actions-btn-group [data-label="Delete"]').click();
cy.findByRole('button', {name: 'Yes'}).click(); cy.findByRole('button', {name: 'Yes'}).click();


+ 4
- 1
cypress/integration/form.js Datei anzeigen

@@ -8,7 +8,10 @@ context('Form', () => {
}); });
it('create a new form', () => { it('create a new form', () => {
cy.visit('/app/todo/new'); 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.wait(300);
cy.get('.page-title').should('contain', 'Not Saved'); cy.get('.page-title').should('contain', 'Not Saved');
cy.intercept({ cy.intercept({


+ 31
- 0
cypress/integration/form_tab_break.js Datei anzeigen

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

});
});

+ 20
- 2
cypress/integration/list_view.js Datei anzeigen

@@ -6,6 +6,23 @@ context('List View', () => {
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow"); 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', () => { it('enables "Actions" button', () => {
const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete'];
cy.go_to_list('ToDo'); cy.go_to_list('ToDo');
@@ -24,10 +41,11 @@ context('List View', () => {
}).as('real-time-update'); }).as('real-time-update');
cy.wrap(elements).contains('Approve').click(); cy.wrap(elements).contains('Approve').click();
cy.wait(['@bulk-approval', '@real-time-update']); 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.clear_filters();
cy.get('.list-row-container:visible').should('contain', 'Approved'); cy.get('.list-row-container:visible').should('contain', 'Approved');
}); });
}); });
}); });


+ 58
- 0
cypress/integration/multi_select_dialog.js Datei anzeigen

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

+ 12
- 1
cypress/integration/navigation.js Datei anzeigen

@@ -1,7 +1,6 @@
context('Navigation', () => { context('Navigation', () => {
before(() => { before(() => {
cy.login(); cy.login();
cy.visit('/app/website');
}); });
it('Navigate to route with hash in document name', () => { it('Navigate to route with hash in document name', () => {
cy.insert_doc('ToDo', {'__newname': 'ABC#123', 'description': 'Test this', 'ignore_duplicate': true}); cy.insert_doc('ToDo', {'__newname': 'ABC#123', 'description': 'Test this', 'ignore_duplicate': true});
@@ -11,4 +10,16 @@ context('Navigation', () => {
cy.go('back'); cy.go('back');
cy.title().should('eq', 'Website'); 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');
});
}); });

+ 1
- 0
cypress/integration/timeline.js Datei anzeigen

@@ -11,6 +11,7 @@ context('Timeline', () => {
cy.visit('/app/todo'); cy.visit('/app/todo');
cy.click_listview_primary_button('Add ToDo'); cy.click_listview_primary_button('Add ToDo');
cy.findByRole('button', {name: 'Edit in full page'}).click(); 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.get('[data-fieldname="description"] .ql-editor').eq(0).type('Test ToDo', {force: true});
cy.wait(200); cy.wait(200);
cy.findByRole('button', {name: 'Save'}).click(); cy.findByRole('button', {name: 'Save'}).click();


+ 6
- 2
cypress/integration/timeline_email.js Datei anzeigen

@@ -5,14 +5,16 @@ context('Timeline Email', () => {
cy.visit('/app/todo'); 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.click_listview_primary_button('Add ToDo');
cy.get('.custom-actions:visible > .btn').contains("Edit in full page").click({delay: 500}); cy.get('.custom-actions:visible > .btn').contains("Edit in full page").click({delay: 500});
cy.fill_field("description", "Test ToDo", "Text Editor"); cy.fill_field("description", "Test ToDo", "Text Editor");
cy.wait(500); cy.wait(500);
cy.get('.primary-action').contains('Save').click({force: true}); cy.get('.primary-action').contains('Save').click({force: true});
cy.wait(700); cy.wait(700);
});

it('Adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => {
cy.visit('/app/todo'); cy.visit('/app/todo');
cy.get('.list-row > .level-left > .list-subject').eq(0).click(); 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 > .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('#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.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click();

cy.visit('/app/todo'); cy.visit('/app/todo');
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click(); cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();


//Removing the added attachment //Removing the added attachment
cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click(); 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(); cy.get('.modal-footer:visible > .standard-actions > .btn-primary').contains('Yes').click();


//To check if the removed attachment is shown in the timeline content //To check if the removed attachment is shown in the timeline content


+ 2
- 1
frappe/__init__.py Datei anzeigen

@@ -235,12 +235,13 @@ def connect_replica():
from frappe.database import get_db from frappe.database import get_db
user = local.conf.db_name user = local.conf.db_name
password = local.conf.db_password password = local.conf.db_password
port = local.conf.replica_db_port


if local.conf.different_credentials_for_replica: if local.conf.different_credentials_for_replica:
user = local.conf.replica_db_name user = local.conf.replica_db_name
password = local.conf.replica_db_password 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 # swap db connections
local.primary_db = local.db local.primary_db = local.db


+ 78
- 56
frappe/build.py Datei anzeigen

@@ -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 # License: MIT. See LICENSE
import os import os
import re import re
import json import json
import shutil import shutil
import subprocess import subprocess
from subprocess import getoutput
from io import StringIO from io import StringIO
from tempfile import mkdtemp, mktemp from tempfile import mkdtemp, mktemp
from distutils.spawn import find_executable from distutils.spawn import find_executable
@@ -17,6 +18,8 @@ import psutil
from urllib.parse import urlparse from urllib.parse import urlparse
from simple_chalk import green from simple_chalk import green
from semantic_version import Version from semantic_version import Version
from requests import head
from requests.exceptions import HTTPError




timestamps = {} timestamps = {}
@@ -24,6 +27,12 @@ app_paths = None
sites_path = os.path.abspath(os.getcwd()) sites_path = os.path.abspath(os.getcwd())




class AssetsNotDownloadedError(Exception):
pass

class AssetsDontExistError(HTTPError):
pass

def download_file(url, prefix): def download_file(url, prefix):
from requests import get from requests import get


@@ -70,81 +79,94 @@ def build_missing_files():
bundle(build_mode, apps="frappe") 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( 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:
# if tag exists, download assets from github release # 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: 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): 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 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): def download_frappe_assets(verbose=True):
"""Downloads and sets up Frappe assets if they exist based on the current """Downloads and sets up Frappe assets if they exist based on the current
commit HEAD. commit HEAD.
Returns True if correctly setup else returns False. 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") 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: 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: 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

return assets_setup
pass

return False




def symlink(target, link_name, overwrite=False): def symlink(target, link_name, overwrite=False):


+ 18
- 3
frappe/commands/__init__.py Datei anzeigen

@@ -102,9 +102,24 @@ def get_commands():
from .site import commands as site_commands from .site import commands as site_commands
from .translate import commands as translate_commands from .translate import commands as translate_commands
from .utils import commands as utils_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() commands = get_commands()

frappe/commands/redis.py → frappe/commands/redis_utils.py Datei anzeigen

@@ -3,7 +3,7 @@ import os
import click import click


import frappe import frappe
from frappe.utils.rq import RedisQueue
from frappe.utils.redis_queue import RedisQueue
from frappe.installer import update_site_config from frappe.installer import update_site_config


@click.command('create-rq-users') @click.command('create-rq-users')

+ 132
- 5
frappe/commands/site.py Datei anzeigen

@@ -67,6 +67,9 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
validate_database_sql validate_database_sql
) )


site = get_site(context)
frappe.init(site=site)

force = context.force or force force = context.force or force
decompressed_file_name = extract_sql_from_archive(sql_file_path) 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 # check if valid SQL file
validate_database_sql(decompressed_file_name, _raise=not force) 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 # dont allow downgrading to older versions of frappe without force
if not force and is_downgrade(decompressed_file_name, verbose=True): if not force and is_downgrade(decompressed_file_name, verbose=True):
warn_message = ( warn_message = (
@@ -474,7 +474,7 @@ def remove_from_installed_apps(context, app):


@click.command('uninstall-app') @click.command('uninstall-app')
@click.argument('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('--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('--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) @click.option('--force', help='Force remove app from site', is_flag=True, default=False)
@@ -738,6 +738,131 @@ def build_search_index(context):
finally: finally:
frappe.destroy() 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 = [ commands = [
add_system_manager, add_system_manager,
backup, backup,
@@ -766,5 +891,7 @@ commands = [
add_to_hosts, add_to_hosts,
start_ngrok, start_ngrok,
build_search_index, build_search_index,
partial_restore
partial_restore,
trim_tables,
trim_database,
] ]

+ 101
- 12
frappe/commands/utils.py Datei anzeigen

@@ -408,20 +408,47 @@ def bulk_rename(context, doctype, path):
frappe.destroy() 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') @click.command('mariadb')
@pass_context @pass_context
def mariadb(context): def mariadb(context):
""" """
Enter into mariadb console for a given site. Enter into mariadb console for a given site.
""" """
import os

site = get_site(context) site = get_site(context)
if not site: if not site:
raise SiteNotSpecifiedError raise SiteNotSpecifiedError
frappe.init(site=site) frappe.init(site=site)
_mariadb()


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


# This is assuming you're within the bench instance.

def _mariadb():
mysql = find_executable('mysql') mysql = find_executable('mysql')
os.execv(mysql, [ os.execv(mysql, [
mysql, mysql,
@@ -434,15 +461,7 @@ def mariadb(context):
"-A"]) "-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') psql = find_executable('psql')
subprocess.run([ psql, '-d', frappe.conf.db_name]) subprocess.run([ psql, '-d', frappe.conf.db_name])


@@ -525,6 +544,74 @@ def console(context, autoreload=False):
terminal() 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.command('run-tests')
@click.option('--app', help="For App") @click.option('--app', help="For App")
@click.option('--doctype', help="For DocType") @click.option('--doctype', help="For DocType")
@@ -814,6 +901,8 @@ commands = [
build, build,
clear_cache, clear_cache,
clear_website_cache, clear_website_cache,
database,
transform_database,
jupyter, jupyter,
console, console,
destroy_all_sessions, destroy_all_sessions,


+ 1
- 1
frappe/contacts/address_and_contact.py Datei anzeigen

@@ -178,4 +178,4 @@ def set_link_title(doc):
for link in doc.links: for link in doc.links:
if not link.link_title: if not link.link_title:
linked_doc = frappe.get_doc(link.link_doctype, link.link_name) linked_doc = frappe.get_doc(link.link_doctype, link.link_name)
link.link_title = linked_doc.get("title_field") or linked_doc.get("name")
link.link_title = linked_doc.get_title() or link.link_name

+ 1
- 1
frappe/contacts/doctype/address/address.py Datei anzeigen

@@ -65,7 +65,7 @@ class Address(Document):


def has_link(self, doctype, name): def has_link(self, doctype, name):
for link in self.links: 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 return True


def has_common_link(self, doc): def has_common_link(self, doc):


+ 2
- 2
frappe/contacts/doctype/contact/contact.py Datei anzeigen

@@ -47,14 +47,14 @@ class Contact(Document):
def get_link_for(self, link_doctype): def get_link_for(self, link_doctype):
'''Return the link name, if exists for the given link DocType''' '''Return the link name, if exists for the given link DocType'''
for link in self.links: for link in self.links:
if link.link_doctype==link_doctype:
if link.link_doctype == link_doctype:
return link.link_name return link.link_name


return None return None


def has_link(self, doctype, name): def has_link(self, doctype, name):
for link in self.links: 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 return True


def has_common_link(self, doc): def has_common_link(self, doc):


+ 32
- 16
frappe/core/doctype/access_log/access_log.py Datei anzeigen

@@ -1,6 +1,7 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe import frappe
from tenacity import retry, retry_if_exception_type, stop_after_attempt
from frappe.model.document import Document from frappe.model.document import Document




@@ -10,25 +11,40 @@ class AccessLog(Document):


@frappe.whitelist() @frappe.whitelist()
@frappe.write_only() @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 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) doc.insert(ignore_permissions=True)


# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` # `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() frappe.db.commit()

+ 541
- 542
frappe/core/doctype/docfield/docfield.json
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 2
- 0
frappe/core/doctype/doctype/doctype.py Datei anzeigen

@@ -274,6 +274,8 @@ class DocType(Document):
d.fieldname = d.fieldname + '_section' d.fieldname = d.fieldname + '_section'
elif d.fieldtype=='Column Break': elif d.fieldtype=='Column Break':
d.fieldname = d.fieldname + '_column' d.fieldname = d.fieldname + '_column'
elif d.fieldtype=='Tab Break':
d.fieldname = d.fieldname + '_tab'
else: else:
d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx) d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx)
else: else:


+ 2
- 1
frappe/core/doctype/document_naming_rule/document_naming_rule.json Datei anzeigen

@@ -41,6 +41,7 @@
"fieldname": "counter", "fieldname": "counter",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Counter", "label": "Counter",
"no_copy": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -79,7 +80,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-11-04 14:38:14.836056",
"modified": "2021-09-13 20:07:47.617615",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "Document Naming Rule", "name": "Document Naming Rule",


+ 66
- 224
frappe/core/doctype/sms_settings/sms_settings.json Datei anzeigen

@@ -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": [ "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%" "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": [ "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 "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
}

+ 1
- 1
frappe/core/doctype/user/user.py Datei anzeigen

@@ -788,7 +788,7 @@ def sign_up(email, full_name, redirect_to):
return 2, _("Please ask your administrator to verify your sign-up") return 2, _("Please ask your administrator to verify your sign-up")


@frappe.whitelist(allow_guest=True) @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): def reset_password(user):
if user=="Administrator": if user=="Administrator":
return 'not allowed' return 'not allowed'


+ 0
- 21
frappe/core/doctype/version/version.css Datei anzeigen

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

+ 0
- 2
frappe/core/doctype/version/version.py Datei anzeigen

@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE


# License: MIT. See LICENSE

import frappe, json import frappe, json


from frappe.model.document import Document from frappe.model.document import Document


+ 2
- 1
frappe/core/page/background_jobs/background_jobs.py Datei anzeigen

@@ -67,7 +67,8 @@ def get_info(show_failed=False) -> List[Dict]:
fail_registry = queue.failed_job_registry fail_registry = queue.failed_job_registry
for job_id in fail_registry.get_job_ids(): for job_id in fail_registry.get_job_ids():
job = queue.fetch_job(job_id) job = queue.fetch_job(job_id)
add_job(job, queue.name)
if job:
add_job(job, queue.name)


return jobs return jobs




+ 456
- 458
frappe/custom/doctype/custom_field/custom_field.json Datei anzeigen

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

+ 1
- 1
frappe/custom/doctype/custom_field/custom_field.py Datei anzeigen

@@ -18,7 +18,7 @@ class CustomField(Document):
if not self.fieldname: if not self.fieldname:
label = self.label label = self.label
if not 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) label = self.fieldtype + "_" + str(self.idx)
else: else:
frappe.throw(_("Label is mandatory")) frappe.throw(_("Label is mandatory"))


+ 2
- 2
frappe/custom/doctype/customize_form_field/customize_form_field.json Datei anzeigen

@@ -82,7 +82,7 @@
"label": "Type", "label": "Type",
"oldfieldname": "fieldtype", "oldfieldname": "fieldtype",
"oldfieldtype": "Select", "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, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
@@ -428,7 +428,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-07-10 21:57:24.479749",
"modified": "2021-07-11 21:57:24.479749",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Custom", "module": "Custom",
"name": "Customize Form Field", "name": "Customize Form Field",


+ 1
- 1
frappe/custom/doctype/property_setter/property_setter.py Datei anzeigen

@@ -34,7 +34,7 @@ class PropertySetter(Document):
fields=['fieldname', 'label', 'fieldtype'], fields=['fieldname', 'label', 'fieldtype'],
filters={ filters={
'parent': dt, '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': ['!=', ''] 'fieldname': ['!=', '']
}, },
order_by='label asc', order_by='label asc',


+ 0
- 45
frappe/data/sample_site_config.json Datei anzeigen

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

+ 5
- 5
frappe/database/database.py Datei anzeigen

@@ -332,7 +332,7 @@ class Database(object):
values[key] = value values[key] = value
if isinstance(value, (list, tuple)): if isinstance(value, (list, tuple)):
# value is a tuple like ("!=", 0) # value is a tuple like ("!=", 0)
_operator = value[0]
_operator = value[0].lower()
values[key] = value[1] values[key] = value[1]
if isinstance(value[1], (tuple, list)): if isinstance(value[1], (tuple, list)):
# value is a list in tuple ("in", ("A", "B")) # 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] WHERE table_name = 'tab{0}' AND column_name = '{1}' '''.format(doctype, column))[0][0]


def has_index(self, table_name, index_name): def has_index(self, table_name, index_name):
pass
raise NotImplementedError


def add_index(self, doctype, fields, index_name=None): def add_index(self, doctype, fields, index_name=None):
pass
raise NotImplementedError


def add_unique(self, doctype, fields, constraint_name=None): def add_unique(self, doctype, fields, constraint_name=None):
pass
raise NotImplementedError


@staticmethod @staticmethod
def get_index_name(fields): def get_index_name(fields):
@@ -951,7 +951,7 @@ class Database(object):
def escape(s, percent=True): def escape(s, percent=True):
"""Excape quotes and percent in given string.""" """Excape quotes and percent in given string."""
# implemented in specific class # implemented in specific class
pass
raise NotImplementedError


@staticmethod @staticmethod
def is_column_missing(e): def is_column_missing(e):


+ 7
- 7
frappe/database/mariadb/database.py Datei anzeigen

@@ -22,11 +22,11 @@ class MariaDBDatabase(Database):
def setup_type_map(self): def setup_type_map(self):
self.db_type = 'mariadb' self.db_type = 'mariadb'
self.type_map = { self.type_map = {
'Currency': ('decimal', '18,6'),
'Currency': ('decimal', '21,9'),
'Int': ('int', '11'), 'Int': ('int', '11'),
'Long Int': ('bigint', '20'), 'Long Int': ('bigint', '20'),
'Float': ('decimal', '18,6'),
'Percent': ('decimal', '18,6'),
'Float': ('decimal', '21,9'),
'Percent': ('decimal', '21,9'),
'Check': ('int', '1'), 'Check': ('int', '1'),
'Small Text': ('text', ''), 'Small Text': ('text', ''),
'Long Text': ('longtext', ''), 'Long Text': ('longtext', ''),
@@ -51,7 +51,7 @@ class MariaDBDatabase(Database):
'Color': ('varchar', self.VARCHAR_LEN), 'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('longtext', ''), 'Barcode': ('longtext', ''),
'Geolocation': ('longtext', ''), 'Geolocation': ('longtext', ''),
'Duration': ('decimal', '18,6'),
'Duration': ('decimal', '21,9'),
'Icon': ('varchar', self.VARCHAR_LEN) 'Icon': ('varchar', self.VARCHAR_LEN)
} }


@@ -135,8 +135,8 @@ class MariaDBDatabase(Database):
table_name = get_table_name(doctype) table_name = get_table_name(doctype)
return self.sql(f"DESC `{table_name}`") 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") return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} NOT NULL")


# exception types # exception types
@@ -195,7 +195,7 @@ class MariaDBDatabase(Database):
`password` TEXT NOT NULL, `password` TEXT NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0, `encrypted` INT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`) 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): def create_global_search_table(self):
if not '__global_search' in self.get_tables(): if not '__global_search' in self.get_tables():


+ 11
- 11
frappe/database/mariadb/framework_mariadb.sql Datei anzeigen

@@ -72,7 +72,7 @@ CREATE TABLE `tabDocField` (
KEY `label` (`label`), KEY `label` (`label`),
KEY `fieldtype` (`fieldtype`), KEY `fieldtype` (`fieldtype`),
KEY `fieldname` (`fieldname`) 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, `email` int(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`name`), PRIMARY KEY (`name`),
KEY `parent` (`parent`) 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` -- Table structure for table `tabDocType Action`
@@ -133,7 +133,7 @@ CREATE TABLE `tabDocType Action` (
PRIMARY KEY (`name`), PRIMARY KEY (`name`),
KEY `parent` (`parent`), KEY `parent` (`parent`),
KEY `modified` (`modified`) 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` -- Table structure for table `tabDocType Action`
@@ -156,7 +156,7 @@ CREATE TABLE `tabDocType Link` (
PRIMARY KEY (`name`), PRIMARY KEY (`name`),
KEY `parent` (`parent`), KEY `parent` (`parent`),
KEY `modified` (`modified`) 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` -- Table structure for table `tabDocType`
@@ -228,7 +228,7 @@ CREATE TABLE `tabDocType` (
`sender_field` varchar(255) DEFAULT NULL, `sender_field` varchar(255) DEFAULT NULL,
PRIMARY KEY (`name`), PRIMARY KEY (`name`),
KEY `parent` (`parent`) 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` -- Table structure for table `tabSeries`
@@ -239,7 +239,7 @@ CREATE TABLE `tabSeries` (
`name` varchar(100), `name` varchar(100),
`current` int(10) NOT NULL DEFAULT 0, `current` int(10) NOT NULL DEFAULT 0,
PRIMARY KEY(`name`) 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', `device` varchar(255) DEFAULT 'desktop',
`status` varchar(20) DEFAULT NULL, `status` varchar(20) DEFAULT NULL,
KEY `sid` (`sid`) 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, `field` varchar(255) DEFAULT NULL,
`value` text, `value` text,
KEY `singles_doctype_field_index` (`doctype`, `field`) 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` -- Table structure for table `__Auth`
@@ -283,7 +283,7 @@ CREATE TABLE `__Auth` (
`password` TEXT NOT NULL, `password` TEXT NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0, `encrypted` INT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`) 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` -- Table structure for table `tabFile`
@@ -311,7 +311,7 @@ CREATE TABLE `tabFile` (
KEY `parent` (`parent`), KEY `parent` (`parent`),
KEY `attached_to_name` (`attached_to_name`), KEY `attached_to_name` (`attached_to_name`),
KEY `attached_to_doctype` (`attached_to_doctype`) 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` -- Table structure for table `tabDefaultValue`
@@ -334,4 +334,4 @@ CREATE TABLE `tabDefaultValue` (
PRIMARY KEY (`name`), PRIMARY KEY (`name`),
KEY `parent` (`parent`), KEY `parent` (`parent`),
KEY `defaultvalue_parent_defkey_index` (`parent`,`defkey`) 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;

+ 14
- 8
frappe/database/mariadb/schema.py Datei anzeigen

@@ -4,18 +4,22 @@ from frappe.database.schema import DBTable


class MariaDBTable(DBTable): class MariaDBTable(DBTable):
def create(self): def create(self):
add_text = ''
additional_definitions = ""
engine = self.meta.get("engine") or "InnoDB"
varchar_len = frappe.db.VARCHAR_LEN


# columns # columns
column_defs = self.get_column_definitions() 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
index_defs = self.get_index_definitions() 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 # create table
frappe.db.sql("""create table `%s` (
query = f"""create table `{self.table_name}` (
name varchar({varchar_len}) not null primary key, name varchar({varchar_len}) not null primary key,
creation datetime(6), creation datetime(6),
modified datetime(6), modified datetime(6),
@@ -26,13 +30,15 @@ class MariaDBTable(DBTable):
parentfield varchar({varchar_len}), parentfield varchar({varchar_len}),
parenttype varchar({varchar_len}), parenttype varchar({varchar_len}),
idx int(8) not null default '0', idx int(8) not null default '0',
%sindex parent(parent),
{additional_definitions}
index parent(parent),
index modified(modified)) index modified(modified))
ENGINE={engine} ENGINE={engine}
ROW_FORMAT=COMPRESSED
ROW_FORMAT=DYNAMIC
CHARACTER SET=utf8mb4 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): def alter(self):
for col in self.columns.values(): for col in self.columns.values():


+ 7
- 9
frappe/database/mariadb/setup_db.py Datei anzeigen

@@ -34,25 +34,23 @@ def setup_database(force, source_sql, verbose, no_mariadb_socket=False):
db_name = frappe.local.conf.db_name db_name = frappe.local.conf.db_name
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password) root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
dbman = DbManager(root_conn) dbman = DbManager(root_conn)
dbman_kwargs = {}
if no_mariadb_socket:
dbman_kwargs["host"] = "%"

if force or (db_name not in dbman.get_database_list()): 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) dbman.drop_database(db_name)
else: else:
raise Exception("Database %s already exists" % (db_name,)) 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) if verbose: print("Created user %s" % db_name)


dbman.create_database(db_name) dbman.create_database(db_name)
if verbose: print("Created database %s" % 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() dbman.flush_privileges()
if verbose: print("Granted privileges to user %s and database %s" % (db_name, db_name)) if verbose: print("Granted privileges to user %s and database %s" % (db_name, db_name))




+ 8
- 7
frappe/database/postgres/database.py Datei anzeigen

@@ -4,6 +4,7 @@ from typing import List, Tuple, Union
import psycopg2 import psycopg2
import psycopg2.extensions import psycopg2.extensions
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
from psycopg2.errorcodes import STRING_DATA_RIGHT_TRUNCATION


import frappe import frappe
from frappe.database.database import Database from frappe.database.database import Database
@@ -31,11 +32,11 @@ class PostgresDatabase(Database):
def setup_type_map(self): def setup_type_map(self):
self.db_type = 'postgres' self.db_type = 'postgres'
self.type_map = { self.type_map = {
'Currency': ('decimal', '18,6'),
'Currency': ('decimal', '21,9'),
'Int': ('bigint', None), 'Int': ('bigint', None),
'Long 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), 'Check': ('smallint', None),
'Small Text': ('text', ''), 'Small Text': ('text', ''),
'Long Text': ('text', ''), 'Long Text': ('text', ''),
@@ -60,7 +61,7 @@ class PostgresDatabase(Database):
'Color': ('varchar', self.VARCHAR_LEN), 'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('text', ''), 'Barcode': ('text', ''),
'Geolocation': ('text', ''), 'Geolocation': ('text', ''),
'Duration': ('decimal', '18,6'),
'Duration': ('decimal', '21,9'),
'Icon': ('varchar', self.VARCHAR_LEN) 'Icon': ('varchar', self.VARCHAR_LEN)
} }


@@ -171,7 +172,7 @@ class PostgresDatabase(Database):


@staticmethod @staticmethod
def is_data_too_long(e): 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]: def rename_table(self, old_name: str, new_name: str) -> Union[List, Tuple]:
old_name = get_table_name(old_name) old_name = get_table_name(old_name)
@@ -182,8 +183,8 @@ class PostgresDatabase(Database):
table_name = get_table_name(doctype) table_name = get_table_name(doctype)
return self.sql(f"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '{table_name}'") 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}') return self.sql(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}')


def create_auth_table(self): def create_auth_table(self):


+ 2
- 0
frappe/database/schema.py Datei anzeigen

@@ -303,6 +303,8 @@ def get_definition(fieldtype, precision=None, length=None):
size = d[1] if d[1] else None size = d[1] if d[1] else None


if size: 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: if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6:
size = '21,9' size = '21,9'




+ 105
- 321
frappe/desk/doctype/note/note.json Datei anzeigen

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

+ 84
- 4
frappe/desk/doctype/system_console/system_console.js Datei anzeigen

@@ -10,15 +10,95 @@ frappe.ui.form.on('System Console', {
description: __('Execute Console script'), description: __('Execute Console script'),
ignore_inputs: true, ignore_inputs: true,
}); });
frm.set_value("type", "Python");
}, },


refresh: function(frm) { refresh: function(frm) {
frm.disable_save(); frm.disable_save();
frm.page.set_primary_action(__("Execute"), $btn => { 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>`);
}); });
} }
}); });

+ 43
- 3
frappe/desk/doctype/system_console/system_console.json Datei anzeigen

@@ -17,9 +17,15 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"execute_section",
"type",
"console", "console",
"commit", "commit",
"output"
"output",
"sql_output",
"database_processes_section",
"show_processlist",
"processlist"
], ],
"fields": [ "fields": [
{ {
@@ -40,13 +46,47 @@
"fieldname": "commit", "fieldname": "commit",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Commit" "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, "hide_toolbar": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-08-21 14:44:35.296877",
"modified": "2021-09-15 17:17:44.844767",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Desk", "module": "Desk",
"name": "System Console", "name": "System Console",
@@ -65,4 +105,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
}
}

+ 12
- 4
frappe/desk/doctype/system_console/system_console.py Datei anzeigen

@@ -5,7 +5,7 @@
import json import json


import frappe 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 from frappe.model.document import Document


class SystemConsole(Document): class SystemConsole(Document):
@@ -13,8 +13,11 @@ class SystemConsole(Document):
frappe.only_for('System Manager') frappe.only_for('System Manager')
try: try:
frappe.debug_log = [] 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 except: # noqa: E722
self.output = frappe.get_traceback() self.output = frappe.get_traceback()


@@ -33,4 +36,9 @@ class SystemConsole(Document):
def execute_code(doc): def execute_code(doc):
console = frappe.get_doc(json.loads(doc)) console = frappe.get_doc(json.loads(doc))
console.run() 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)

+ 22
- 33
frappe/desk/doctype/tag/tag.py Datei anzeigen

@@ -128,46 +128,35 @@ def delete_tags_for_document(doc):
}) })


def update_tags(doc, tags): 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} 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={ existing_tags = [tag.tag for tag in frappe.get_list("Tag Link", filters={
"document_type": doc.doctype, "document_type": doc.doctype,
"document_name": doc.name "document_name": doc.name
}, fields=["tag"])] }, fields=["tag"])]


deleted_tags = get_deleted_tags(new_tags, existing_tags)

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

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() @frappe.whitelist()
def get_documents_for_tag(tag): def get_documents_for_tag(tag):


+ 14
- 1
frappe/desk/doctype/tag_link/tag_link.json Datei anzeigen

@@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2019-09-24 13:25:36.435685", "creation": "2019-09-24 13:25:36.435685",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -44,7 +45,8 @@
"read_only": 1 "read_only": 1
} }
], ],
"modified": "2019-10-03 16:42:35.932409",
"links": [],
"modified": "2021-09-20 16:53:37.217998",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Desk", "module": "Desk",
"name": "Tag Link", "name": "Tag Link",
@@ -61,6 +63,17 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
} }
], ],
"read_only": 1, "read_only": 1,


+ 5
- 4
frappe/desk/doctype/workspace/workspace.json Datei anzeigen

@@ -165,8 +165,6 @@
"default": "0", "default": "0",
"fieldname": "is_standard", "fieldname": "is_standard",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Is Standard", "label": "Is Standard",
"search_index": 1 "search_index": 1
}, },
@@ -181,7 +179,6 @@
"depends_on": "eval:doc.extends_another_page == 1 || doc.for_user", "depends_on": "eval:doc.extends_another_page == 1 || doc.for_user",
"fieldname": "extends", "fieldname": "extends",
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1,
"label": "Extends", "label": "Extends",
"options": "Workspace", "options": "Workspace",
"search_index": 1 "search_index": 1
@@ -228,6 +225,8 @@
"default": "0", "default": "0",
"fieldname": "public", "fieldname": "public",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Public" "label": "Public"
}, },
{ {
@@ -265,11 +264,13 @@
"label": "Roles" "label": "Roles"
} }
], ],
"in_create": 1,
"links": [], "links": [],
"modified": "2021-08-30 18:47:18.227154",
"modified": "2021-09-16 12:01:06.450621",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Desk", "module": "Desk",
"name": "Workspace", "name": "Workspace",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {


+ 3
- 3
frappe/desk/doctype/workspace/workspace.py Datei anzeigen

@@ -208,17 +208,17 @@ def save_page(title, icon, parent, public, sb_public_items, sb_private_items, de
if loads(deleted_pages): if loads(deleted_pages):
return delete_pages(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): def delete_pages(deleted_pages):
for page in deleted_pages: for page in deleted_pages:
if page.get("public") and "Workspace Manager" not in frappe.get_roles(): 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")): if frappe.db.exists("Workspace", page.get("name")):
frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True) 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): def sort_pages(sb_public_items, sb_private_items):
wspace_public_pages = get_page_list(['name', 'title'], {'public': 1}) wspace_public_pages = get_page_list(['name', 'title'], {'public': 1})


+ 1
- 1
frappe/desk/reportview.py Datei anzeigen

@@ -121,7 +121,7 @@ def validate_filters(data, filters):


def setup_group_by(data): def setup_group_by(data):
'''Add columns for aggregated values e.g. count(name)''' '''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'): if data.aggregate_function.lower() not in ('count', 'sum', 'avg'):
frappe.throw(_('Invalid aggregate function')) frappe.throw(_('Invalid aggregate function'))




+ 4
- 4
frappe/email/doctype/email_account/email_account.json Datei anzeigen

@@ -226,7 +226,7 @@
}, },
{ {
"default": "UNSEEN", "default": "UNSEEN",
"depends_on": "eval: doc.enable_incoming",
"depends_on": "eval: doc.enable_incoming && doc.use_imap",
"fieldname": "email_sync_option", "fieldname": "email_sync_option",
"fieldtype": "Select", "fieldtype": "Select",
"hide_days": 1, "hide_days": 1,
@@ -236,7 +236,7 @@
}, },
{ {
"default": "250", "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 ", "description": "Total number of emails to sync in initial sync process ",
"fieldname": "initial_sync_count", "fieldname": "initial_sync_count",
"fieldtype": "Select", "fieldtype": "Select",
@@ -567,7 +567,7 @@
"icon": "fa fa-inbox", "icon": "fa fa-inbox",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-08-31 15:23:25.714366",
"modified": "2021-09-21 16:44:25.728637",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Email", "module": "Email",
"name": "Email Account", "name": "Email Account",
@@ -589,4 +589,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
}
}

+ 1
- 0
frappe/email/doctype/notification/notification.py Datei anzeigen

@@ -146,6 +146,7 @@ def get_context(context):
if doc.meta.get_field(fieldname).fieldtype in frappe.model.numeric_fieldtypes: if doc.meta.get_field(fieldname).fieldtype in frappe.model.numeric_fieldtypes:
value = frappe.utils.cint(value) value = frappe.utils.cint(value)


doc.reload()
doc.set(fieldname, value) doc.set(fieldname, value)
doc.flags.updater_reference = { doc.flags.updater_reference = {
'doctype': self.doctype, 'doctype': self.doctype,


+ 5
- 0
frappe/email/doctype/notification/test_notification.py Datei anzeigen

@@ -20,6 +20,8 @@ class TestNotification(unittest.TestCase):
notification.event = 'Value Change' notification.event = 'Value Change'
notification.value_changed = 'status' notification.value_changed = 'status'
notification.send_to_all_assignees = 1 notification.send_to_all_assignees = 1
notification.set_property_after_alert = 'description'
notification.property_value = 'Changed by Notification'
notification.save() notification.save()


if not frappe.db.exists('Notification', {'name': 'Contact Status Update'}, 'name'): if not frappe.db.exists('Notification', {'name': 'Contact Status Update'}, 'name'):
@@ -237,6 +239,9 @@ class TestNotification(unittest.TestCase):


self.assertTrue(email_queue) 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] recipients = [d.recipient for d in email_queue.recipients]
self.assertTrue('test2@example.com' in recipients) self.assertTrue('test2@example.com' in recipients)
self.assertTrue('test1@example.com' in recipients) self.assertTrue('test1@example.com' in recipients)


+ 3
- 2
frappe/event_streaming/doctype/event_producer/event_producer.py Datei anzeigen

@@ -408,8 +408,9 @@ def sync_dependencies(document, producer_site):
child_table = doc.get(df.fieldname) child_table = doc.get(df.fieldname)
for entry in child_table: for entry in child_table:
child_doc = producer_site.get_doc(entry.doctype, entry.name) 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): def sync_link_dependencies(doc, link_fields, producer_site):
set_dependencies(doc, link_fields, producer_site) set_dependencies(doc, link_fields, producer_site)


+ 4
- 1
frappe/handler.py Datei anzeigen

@@ -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) doc = frappe.get_doc(dt, dn)


else: 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._original_modified = doc.modified
doc.check_if_latest() doc.check_if_latest()




+ 5
- 4
frappe/hooks.py Datei anzeigen

@@ -12,11 +12,11 @@ source_link = "https://github.com/frappe/frappe"
app_license = "MIT" app_license = "MIT"
app_logo_url = '/assets/frappe/images/frappe-framework-logo.svg' 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" translator_url = "https://translate.erpnext.com"


@@ -164,7 +164,8 @@ doc_events = {
"after_rename": "frappe.desk.notifications.clear_doctype_notifications", "after_rename": "frappe.desk.notifications.clear_doctype_notifications",
"on_cancel": [ "on_cancel": [
"frappe.desk.notifications.clear_doctype_notifications", "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": [ "on_trash": [
"frappe.desk.notifications.clear_doctype_notifications", "frappe.desk.notifications.clear_doctype_notifications",


+ 13
- 1
frappe/installer.py Datei anzeigen

@@ -445,9 +445,21 @@ def extract_sql_from_archive(sql_file_path):
else: else:
decompressed_file_name = sql_file_path decompressed_file_name = sql_file_path


# convert archive sql to latest compatible
convert_archive_content(decompressed_file_name)

return 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): def extract_sql_gzip(sql_gz_path):
import subprocess import subprocess


@@ -457,7 +469,7 @@ def extract_sql_gzip(sql_gz_path):
decompressed_file = original_file.rstrip(".gz") decompressed_file = original_file.rstrip(".gz")
cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file) cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file)
subprocess.check_call(cmd, shell=True) subprocess.check_call(cmd, shell=True)
except:
except Exception:
raise raise


return decompressed_file return decompressed_file


+ 2
- 0
frappe/model/__init__.py Datei anzeigen

@@ -41,6 +41,7 @@ data_fieldtypes = (
no_value_fields = ( no_value_fields = (
'Section Break', 'Section Break',
'Column Break', 'Column Break',
'Tab Break',
'HTML', 'HTML',
'Table', 'Table',
'Table MultiSelect', 'Table MultiSelect',
@@ -53,6 +54,7 @@ no_value_fields = (
display_fieldtypes = ( display_fieldtypes = (
'Section Break', 'Section Break',
'Column Break', 'Column Break',
'Tab Break',
'HTML', 'HTML',
'Button', 'Button',
'Image', 'Image',


+ 1
- 1
frappe/model/base_document.py Datei anzeigen

@@ -307,7 +307,7 @@ class BaseDocument(object):
doc["doctype"] = self.doctype doc["doctype"] = self.doctype
for df in self.meta.get_table_fields(): for df in self.meta.get_table_fields():
children = self.get(df.fieldname) or [] 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: if no_nulls:
for k in list(doc): for k in list(doc):


+ 7
- 2
frappe/model/db_query.py Datei anzeigen

@@ -4,6 +4,7 @@


from typing import List from typing import List
import frappe.defaults import frappe.defaults
from frappe.query_builder.utils import Column
import frappe.share import frappe.share
from frappe import _ from frappe import _
import frappe.permissions import frappe.permissions
@@ -491,7 +492,7 @@ class DatabaseQuery(object):
f.value = date_range f.value = date_range
fallback = "'0001-01-01 00:00:00'" 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) value = cstr(f.value)
fallback = "NULL" fallback = "NULL"


@@ -547,8 +548,12 @@ class DatabaseQuery(object):
value = flt(f.value) value = flt(f.value)
fallback = 0 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 # 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)}" value = f"{frappe.db.escape(value, percent=False)}"


if ( if (


+ 38
- 16
frappe/model/meta.py Datei anzeigen

@@ -15,6 +15,7 @@ Example:


''' '''
from datetime import datetime from datetime import datetime
import click
import frappe, json, os import frappe, json, os
from frappe.utils import cstr, cint, cast from frappe.utils import cstr, cint, cast
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields 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" 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 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 as maintenance since removing a field in a DocType doesn't automatically
delete the db field. delete the db field.
""" """
ignore_fields = default_fields + optional_fields

filters={ "issingle": 0 }
UPDATED_TABLES = {}
filters = {"issingle": 0}
if doctype: if doctype:
filters["name"] = 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
- 1
frappe/patches/v13_0/increase_password_length.py Datei anzeigen

@@ -1,4 +1,4 @@
import frappe import frappe


def execute(): 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
frappe/printing/doctype/network_printer_settings/__init__.py Datei anzeigen


+ 29
- 0
frappe/printing/doctype/network_printer_settings/network_printer_settings.js Datei anzeigen

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

+ 66
- 0
frappe/printing/doctype/network_printer_settings/network_printer_settings.json Datei anzeigen

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

+ 37
- 0
frappe/printing/doctype/network_printer_settings/network_printer_settings.py Datei anzeigen

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

+ 8
- 0
frappe/printing/doctype/network_printer_settings/test_network_printer_settings.py Datei anzeigen

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt

# import frappe
import unittest

class TestNetworkPrinterSettings(unittest.TestCase):
pass

+ 0
- 22
frappe/printing/doctype/print_settings/print_settings.js Datei anzeigen

@@ -15,27 +15,5 @@ frappe.ui.form.on('Print Settings', {
}, },
onload: function(frm) { onload: function(frm) {
frm.script_manager.trigger("print_style"); 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)
});
}
} }
}); });

+ 4
- 25
frappe/printing/doctype/print_settings/print_settings.json Datei anzeigen

@@ -19,9 +19,6 @@
"allow_print_for_cancelled", "allow_print_for_cancelled",
"server_printer", "server_printer",
"enable_print_server", "enable_print_server",
"server_ip",
"printer_name",
"port",
"raw_printing_section", "raw_printing_section",
"enable_raw_printing", "enable_raw_printing",
"print_style_section", "print_style_section",
@@ -107,29 +104,11 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "enable_print_server",
"fieldname": "enable_print_server", "fieldname": "enable_print_server",
"fieldtype": "Check", "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", "fieldname": "raw_printing_section",
@@ -183,7 +162,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-02-15 14:16:18.474254",
"modified": "2021-09-17 12:59:14.783694",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Printing", "module": "Printing",
"name": "Print Settings", "name": "Print Settings",


+ 0
- 20
frappe/printing/doctype/print_settings/print_settings.py Datei anzeigen

@@ -12,26 +12,6 @@ class PrintSettings(Document):
def on_update(self): def on_update(self):
frappe.clear_cache() 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() @frappe.whitelist()
def is_print_server_enabled(): def is_print_server_enabled():
if not hasattr(frappe.local, 'enable_print_server'): if not hasattr(frappe.local, 'enable_print_server'):


+ 102
- 63
frappe/printing/page/print/print.js Datei anzeigen

@@ -165,10 +165,7 @@ frappe.ui.form.PrintView = class {
frappe.set_route('Form', 'Print Settings'); 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.page.add_menu_item(__('Raw Printing Setting'), () => {
this.printer_setting_dialog(); this.printer_setting_dialog();
}); });
@@ -179,6 +176,12 @@ frappe.ui.form.PrintView = class {
this.edit_print_format() 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) { show(frm) {
@@ -460,72 +463,108 @@ frappe.ui.form.PrintView = class {


printit() { printit() {
let me = this; 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) { render_page(method, printit = false) {
let w = window.open( let w = window.open(
frappe.urllib.get_full_url( frappe.urllib.get_full_url(


+ 3
- 3
frappe/printing/page/print_format_builder/print_format_builder.js Datei anzeigen

@@ -261,7 +261,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
} else if(f.fieldtype==="Column Break") { } else if(f.fieldtype==="Column Break") {
set_column(); 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) { && f.label) {
if(!column) set_column(); if(!column) set_column();


@@ -298,7 +298,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
init_visible_columns(f) { init_visible_columns(f) {
f.visible_columns = [] f.visible_columns = []
$.each(frappe.get_meta(f.options).fields, function(i, _f) { $.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) { !_f.print_hide && f.label) {


// column names set as fieldname|width // column names set as fieldname|width
@@ -606,7 +606,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
// add remaining fields // add remaining fields
$.each(doc_fields, function(j, f) { $.each(doc_fields, function(j, f) {
if (f && !in_list(column_names, f.fieldname) 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); fields.push(f);
} }
}) })


+ 1
- 1
frappe/printing/page/print_format_builder/print_format_builder_sidebar.html Datei anzeigen

@@ -4,7 +4,7 @@
</div> </div>
<div class="print-format-builder-sidebar-fields"> <div class="print-format-builder-sidebar-fields">
{% for (var i=0, l=fields.length; i < l; i++) { var f = fields[i]; %} {% 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" <div class="print-format-builder-field-placeholder"
data-fieldname="{%= f.fieldname %}"> data-fieldname="{%= f.fieldname %}">
<div title="{{f.label}}" class="field-label btn btn-default btn-sm sidebar-field ellipsis <div title="{{f.label}}" class="field-label btn btn-default btn-sm sidebar-field ellipsis


+ 3
- 0
frappe/public/js/desk.bundle.js Datei anzeigen

@@ -30,6 +30,9 @@ import "./frappe/ui/slides.js";
import "./frappe/ui/find.js"; import "./frappe/ui/find.js";
import "./frappe/ui/iconbar.js"; import "./frappe/ui/iconbar.js";
import "./frappe/form/layout.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/ui/field_group.js";
import "./frappe/form/link_selector.js"; import "./frappe/form/link_selector.js";
import "./frappe/form/multi_select_dialog.js"; import "./frappe/form/multi_select_dialog.js";


+ 49
- 0
frappe/public/js/frappe/form/column.js Datei anzeigen

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

+ 13
- 0
frappe/public/js/frappe/form/controls/float.js Datei anzeigen

@@ -1,4 +1,17 @@
frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlInt { 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) { parse(value) {
value = this.eval_expression(value); value = this.eval_expression(value);
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision()); return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision());


+ 50
- 154
frappe/public/js/frappe/form/dashboard.js Datei anzeigen

@@ -1,61 +1,65 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt // MIT License. See license.txt


import Section from "./section.js";

frappe.ui.form.Dashboard = class FormDashboard { frappe.ui.form.Dashboard = class FormDashboard {
constructor(opts) {
$.extend(this, opts);
constructor(parent, frm) {
this.parent = parent;
this.frm = frm;
this.setup_dashboard_sections(); this.setup_dashboard_sections();
} }


setup_dashboard_sections() { setup_dashboard_sections() {
this.progress_area = new Section(this.parent, {
this.progress_area = this.make_section({
css_class: 'progress-area', css_class: 'progress-area',
hidden: 1, 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', css_class: 'form-heatmap',
hidden: 1, hidden: 1,
collapsible: 1,
is_dashboard_section: 1,
body_html: ` body_html: `
<div id="heatmap-${frappe.model.scrub(this.frm.doctype)}" class="heatmap"></div> <div id="heatmap-${frappe.model.scrub(this.frm.doctype)}" class="heatmap"></div>
<div class="text-muted small heatmap-message hidden"></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', css_class: 'form-graph',
hidden: 1, hidden: 1,
collapsible: 1
is_dashboard_section: 1
}); });


this.stats_area_row = $(`<div class="row"></div>`); 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', css_class: 'form-stats',
hidden: 1, hidden: 1,
collapsible: 1,
is_dashboard_section: 1,
body_html: this.stats_area_row body_html: this.stats_area_row
}); });


this.transactions_area = $(`<div class="transactions"></div`); 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', css_class: 'form-links',
hidden: 1, hidden: 1,
collapsible: 1,
is_dashboard_section: 1,
body_html: this.transactions_area body_html: this.transactions_area
}); });
}



make_section(df) {
return new Section(this.parent, df);
} }


reset() { reset() {
this.hide();

// clear progress // clear progress
this.progress_area.body.empty(); this.progress_area.body.empty();
this.progress_area.hide(); this.progress_area.hide();
@@ -70,19 +74,19 @@ frappe.ui.form.Dashboard = class FormDashboard {


// clear custom // clear custom
this.parent.find('.custom').remove(); 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 = { let options = {
title,
label,
css_class, css_class,
hidden, hidden,
body_html, body_html,
make_card: true, 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) { add_progress(title, percent, message) {
@@ -154,7 +158,7 @@ frappe.ui.form.Dashboard = class FormDashboard {


make_progress_chart(title) { make_progress_chart(title) {
this.progress_area.show(); 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); .appendTo(this.progress_area.body);
return progress_chart; return progress_chart;
} }
@@ -169,7 +173,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
this.init_data(); this.init_data();
} }


var show = false;
let show = false;


if (this.data && ((this.data.transactions || []).length if (this.data && ((this.data.transactions || []).length
|| (this.data.reports || []).length)) { || (this.data.reports || []).length)) {
@@ -197,11 +201,10 @@ frappe.ui.form.Dashboard = class FormDashboard {
} }


after_refresh() { after_refresh() {
var me = this;
// show / hide new buttons (if allowed) // 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(); !this.frm.is_new() && this.set_open_count();
@@ -269,7 +272,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
} }


render_links() { render_links() {
var me = this;
let me = this;
this.links_area.show(); this.links_area.show();
this.links_area.body.find('.btn-new').addClass('hidden'); this.links_area.body.find('.btn-new').addClass('hidden');
if (this.data_rendered) { if (this.data_rendered) {
@@ -329,7 +332,7 @@ frappe.ui.form.Dashboard = class FormDashboard {


open_document_list($link, show_open) { open_document_list($link, show_open) {
// show document list with filters // show document list with filters
var doctype = $link.attr('data-doctype'),
let doctype = $link.attr('data-doctype'),
names = $link.attr('data-names') || []; names = $link.attr('data-names') || [];


if (this.data.internal_links[doctype]) { if (this.data.internal_links[doctype]) {
@@ -351,8 +354,8 @@ frappe.ui.form.Dashboard = class FormDashboard {
get_document_filter(doctype) { get_document_filter(doctype) {
// return the default filter for the given document // return the default filter for the given document
// like {"customer": frm.doc.name} // 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.non_standard_fieldnames[doctype] || this.data.fieldname)
: this.data.fieldname; : this.data.fieldname;


@@ -371,7 +374,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
} }


// list all items from the transaction list // list all items from the transaction list
var items = [],
let items = [],
me = this; me = this;


this.data.transactions.forEach(function(group) { 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({ frappe.call({
type: "GET", type: "GET",
method: method, method: method,
@@ -429,7 +432,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
} }


set_badge_count(doctype, open_count, count, names) { set_badge_count(doctype, open_count, count, names) {
var $link = $(this.transactions_area)
let $link = $(this.transactions_area)
.find('.document-link[data-doctype="'+doctype+'"]'); .find('.document-link[data-doctype="'+doctype+'"]');


if (open_count) { if (open_count) {
@@ -476,7 +479,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
this.heatmap_area.body.find('svg').css({'margin': 'auto'}); this.heatmap_area.body.find('svg').css({'margin': 'auto'});


// message // 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) { if (this.data.heatmap_message) {
heatmap_message.removeClass('hidden').html(this.data.heatmap_message); heatmap_message.removeClass('hidden').html(this.data.heatmap_message);
} else { } else {
@@ -491,9 +494,9 @@ frappe.ui.form.Dashboard = class FormDashboard {




// set colspan // 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) { if (n_indicators > 4) {
colspan = 3; colspan = 3;
} else { } else {
@@ -505,7 +508,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
indicators.removeClass().addClass('col-sm-'+colspan).addClass('indicator-column'); 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); +label+'</span></div>').appendTo(this.stats_area_row);


return indicator; return indicator;
@@ -513,9 +516,9 @@ frappe.ui.form.Dashboard = class FormDashboard {


// graphs // graphs
setup_graph() { 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, doctype: this.frm.doctype,
docname: this.frm.doc.name, docname: this.frm.doc.name,
}; };
@@ -579,11 +582,10 @@ frappe.ui.form.Dashboard = class FormDashboard {
} }


add_comment(text, alert_class, permanent) { add_comment(text, alert_class, permanent) {
var me = this;
this.set_headline_alert(text, alert_class); this.set_headline_alert(text, alert_class);
if (!permanent) { if (!permanent) {
setTimeout(function() {
me.clear_headline();
setTimeout(() => {
this.clear_headline();
}, 10000); }, 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();
}
}

+ 4
- 2
frappe/public/js/frappe/form/footer/form_timeline.js Datei anzeigen

@@ -147,7 +147,9 @@ class FormTimeline extends BaseTimeline {
} }


get_user_link(user) { 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); return frappe.utils.get_form_link('User', user, true, user_display_text);
} }


@@ -353,7 +355,7 @@ class FormTimeline extends BaseTimeline {
icon: 'branch', icon: 'branch',
icon_size: 'sm', icon_size: 'sm',
creation: workflow_log.creation, creation: workflow_log.creation,
content: __(workflow_log.content),
content: `${this.get_user_link(workflow_log.owner)} ${__(workflow_log.content)}`,
title: "Workflow", title: "Workflow",
}); });
}); });


+ 29
- 11
frappe/public/js/frappe/form/form.js Datei anzeigen

@@ -94,6 +94,11 @@ frappe.ui.form.Form = class FrappeForm {
this.watch_model_updates(); this.watch_model_updates();


if (!this.meta.hide_toolbar && frappe.boot.desk_settings.timeline) { 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({ this.footer = new frappe.ui.form.Footer({
frm: this, frm: this,
parent: $('<div>').appendTo(this.page.main.parent()) parent: $('<div>').appendTo(this.page.main.parent())
@@ -128,8 +133,8 @@ frappe.ui.form.Form = class FrappeForm {
} }


setup_std_layout() { 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 // only tray
this.meta.section_style='Simple'; // always simple! this.meta.section_style='Simple'; // always simple!
@@ -141,17 +146,19 @@ frappe.ui.form.Form = class FrappeForm {
doctype_layout: this.doctype_layout, doctype_layout: this.doctype_layout,
frm: this, frm: this,
with_dashboard: true, with_dashboard: true,
card_layout: true,
card_layout: true
}); });

this.layout.make(); this.layout.make();


this.fields_dict = this.layout.fields_dict; this.fields_dict = this.layout.fields_dict;
this.fields = this.layout.fields_list; 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({ this.tour = new frappe.ui.form.FormTour({
frm: this frm: this
@@ -181,8 +188,7 @@ frappe.ui.form.Form = class FrappeForm {


me.layout.refresh_dependency(); me.layout.refresh_dependency();
me.layout.refresh_sections(); 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) { if(doc.parent===me.docname && doc.parentfield===df.fieldname) {
me.dirty(); me.dirty();
me.fields_dict[df.fieldname].grid.set_value(fieldname, value, doc); 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.cscript.is_onload && this.is_new() && this.focus_on_first_input(),
() => this.run_after_load_hook(), () => this.run_after_load_hook(),
() => this.dashboard.after_refresh()
() => this.dashboard.after_refresh(),
]); ]);


} else { } else {
@@ -468,6 +474,8 @@ frappe.ui.form.Form = class FrappeForm {


this.$wrapper.trigger('render_complete'); this.$wrapper.trigger('render_complete');


this.cscript.is_onload && this.set_first_tab_as_active();

if(!this.hidden) { if(!this.hidden) {
this.layout.show_empty_form_message(); this.layout.show_empty_form_message();
} }
@@ -475,6 +483,11 @@ frappe.ui.form.Form = class FrappeForm {
this.scroll_to_element(); this.scroll_to_element();
} }


set_first_tab_as_active() {
this.layout.tabs[0]
&& this.layout.tabs[0].set_active();
}

focus_on_first_input() { focus_on_first_input() {
let first = this.form_wrapper.find('.form-layout :input:visible:first'); let first = this.form_wrapper.find('.form-layout :input:visible:first');
if (!in_list(["Date", "Datetime"], first.attr("data-fieldtype"))) { if (!in_list(["Date", "Datetime"], first.attr("data-fieldtype"))) {
@@ -1605,6 +1618,11 @@ frappe.ui.form.Form = class FrappeForm {


let $el = field.$wrapper; let $el = field.$wrapper;


// set tab as active
if (field.tab && !field.tab.is_active()) {
field.tab.set_active();
}

// uncollapse section // uncollapse section
if (field.section.is_collapsed()) { if (field.section.is_collapsed()) {
field.section.collapse(false); field.section.collapse(false);


+ 14
- 12
frappe/public/js/frappe/form/grid.js Datei anzeigen

@@ -212,13 +212,12 @@ export default class Grid {


delete_all_rows() { delete_all_rows() {
frappe.confirm(__("Are you sure you want to 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.refresh();
this.frm && this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
this.frm && this.frm.dirty();
this.scroll_to_top(); this.scroll_to_top();
}); });
} }
@@ -244,8 +243,10 @@ export default class Grid {


this.remove_rows_button.toggleClass('hidden', this.remove_rows_button.toggleClass('hidden',
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); 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() { get_selected() {
@@ -835,10 +836,11 @@ export default class Grid {
$.each(row, (ci, value) => { $.each(row, (ci, value) => {
var fieldname = fieldnames[ci]; var fieldname = fieldnames[ci];
var df = frappe.meta.get_docfield(me.df.options, fieldname); 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;
}
}); });
} }
} }


+ 6
- 4
frappe/public/js/frappe/form/grid_row_form.js Datei anzeigen

@@ -123,10 +123,12 @@ export default class GridRowForm {
.toggle(this.row.grid.is_editable()); .toggle(this.row.grid.is_editable());
} }
refresh_field(fieldname) { 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() { set_focus() {
// wait for animation and then focus on the first row // wait for animation and then focus on the first row


+ 173
- 240
frappe/public/js/frappe/form/layout.js Datei anzeigen

@@ -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 { frappe.ui.form.Layout = class Layout {
constructor (opts) { constructor (opts) {
this.views = {}; this.views = {};
this.pages = []; this.pages = [];
this.tabs = [];
this.sections = []; this.sections = [];
this.fields_list = []; this.fields_list = [];
this.fields_dict = {}; this.fields_dict = {};


$.extend(this, opts); $.extend(this, opts);
} }

make() { make() {
if (!this.parent && this.body) { if (!this.parent && this.body) {
this.parent = this.body; this.parent = this.body;
} }
this.wrapper = $('<div class="form-layout">').appendTo(this.parent); this.wrapper = $('<div class="form-layout">').appendTo(this.parent);
this.message = $('<div class="form-message hidden"></div>').appendTo(this.wrapper); this.message = $('<div class="form-message hidden"></div>').appendTo(this.wrapper);
this.page = $('<div class="form-page"></div>').appendTo(this.wrapper);

if (!this.fields) { if (!this.fields) {
this.fields = this.get_doctype_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(); 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() { show_empty_form_message() {
if (!(this.wrapper.find(".frappe-control:visible").length || this.wrapper.find(".section-head.collapsed").length)) { 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")); 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'); 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.section = null;
this.column = 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) { switch (df.fieldtype) {
case "Fold": case "Fold":
me.make_page(df);
this.make_page(df);
break; break;
case "Section Break": case "Section Break":
me.make_section(df);
this.make_section(df);
break; break;
case "Column Break": case "Column Break":
me.make_column(df);
this.make_column(df);
break;
case "Tab Break":
this.make_tab(df);
break; break;
default: 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; 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 df.fieldname = fieldname; // change of fieldname is avoided
if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) { if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) {
const fieldobj = this.init_field(df, render); 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.section && this.make_section();
!this.column && this.make_column(); !this.column && this.make_column();


@@ -159,9 +191,15 @@ frappe.ui.form.Layout = class Layout {
this.section.fields_list.push(fieldobj); this.section.fields_list.push(fieldobj);
this.section.fields_dict[df.fieldname] = fieldobj; this.section.fields_dict[df.fieldname] = fieldobj;
fieldobj.section = this.section; 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({ const fieldobj = frappe.ui.form.make_control({
df: df, df: df,
doctype: this.doctype, doctype: this.doctype,
@@ -176,8 +214,8 @@ frappe.ui.form.Layout = class Layout {
return fieldobj; 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">\ head = $('<div class="form-clickable-section text-center">\
<a class="btn-fold h6 text-muted">' + __("Show more details") + '</a>\ <a class="btn-fold h6 text-muted">' + __("Show more details") + '</a>\
</div>').appendTo(this.wrapper); </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.page = $('<div class="form-page second-page hide"></div>').appendTo(this.wrapper);


this.fold_btn = head.find(".btn-fold").on("click", function () { this.fold_btn = head.find(".btn-fold").on("click", function () {
var page = $(this).parent().next();
let page = $(this).parent().next();
if (page.hasClass("hide")) { if (page.hasClass("hide")) {
$(this).removeClass("btn-fold").html(__("Hide details")); $(this).removeClass("btn-fold").html(__("Hide details"));
page.removeClass("hide"); page.removeClass("hide");
@@ -202,12 +240,12 @@ frappe.ui.form.Layout = class Layout {
this.folded = true; this.folded = true;
} }


unfold () {
unfold() {
this.fold_btn.trigger('click'); 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 // append to layout fields
if (df) { if (df) {
@@ -218,15 +256,23 @@ frappe.ui.form.Layout = class Layout {
this.column = null; 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) { if (df && df.fieldname) {
this.fields_list.push(this.column); 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 (doc) this.doc = doc;


if (this.frm) { 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 // 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) { if (this.frm && this.frm.wrapper) {
$(this.frm.wrapper).trigger("refresh-fields"); $(this.frm.wrapper).trigger("refresh-fields");
@@ -246,6 +292,9 @@ frappe.ui.form.Layout = class Layout {
// refresh sections // refresh sections
this.refresh_sections(); this.refresh_sections();


// refresh tabs
this.tabbed_layout && this.refresh_tabs();

if (this.frm) { if (this.frm) {
// collapse sections // collapse sections
this.refresh_section_collapse(); 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) => { let fieldnames = fields.map((field) => {
if (field.fieldname) return field.fieldname; 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.render(fields);
this.refresh_fields(fields); this.refresh_fields(fields);
} }
@@ -300,11 +372,11 @@ frappe.ui.form.Layout = class Layout {
refresh_section_collapse () { refresh_section_collapse () {
if (!(this.sections && this.sections.length)) return; 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) { if (df && df.collapsible) {
var collapse = true;
let collapse = true;


if (df.collapsible_depends_on) { if (df.collapsible_depends_on) {
collapse = !this.evaluate_depends_on_value(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) { if (me.doc) {
fieldobj.doc = me.doc; fieldobj.doc = me.doc;
fieldobj.doctype = me.doc.doctype; 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.wrapper.find(".section-count-label:visible").each(function (i) {
$(this).html(i + 1); $(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) { 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, prev = null,
fields = me.fields_list,
in_grid = false,
fields = this.fields_list,
focused = false; focused = false;


// in grid // 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) { if (!grid_row || !grid_row.layout) {
return; return;
} }
fields = grid_row.layout.fields_list; 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 (fields[i].df.fieldname == fieldname) {
if (shift) { if (shift) {
if (prev) { if (prev) {
@@ -384,7 +464,7 @@ frappe.ui.form.Layout = class Layout {
break; break;
} }
if (i < len - 1) { if (i < len - 1) {
focused = me.focus_on_next_field(i, fields);
focused = this.focus_on_next_field(i, fields);
} }


if (focused) { if (focused) {
@@ -408,17 +488,19 @@ frappe.ui.form.Layout = class Layout {
// next row // next row
grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true); grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true);
} }
} else {
} else if (!shift) {
// End of tab navigation
$(this.primary_button).focus(); $(this.primary_button).focus();
} }
} }


return false; return false;
} }
focus_on_next_field (start_idx, fields) {

focus_on_next_field(start_idx, fields) {
// loop to find next eligible 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 (this.is_visible(field)) {
if (field.df.fieldtype === "Table") { if (field.df.fieldtype === "Table") {
// open table grid // 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 // next is table, show the table
if (field.df.fieldtype=="Table") { if (field.df.fieldtype=="Table") {
if (!field.grid.grid_rows.length) { if (!field.grid.grid_rows.length) {
@@ -454,18 +541,19 @@ frappe.ui.form.Layout = class Layout {
field.$input.focus(); field.$input.focus();
} }
} }
get_open_grid_row () {

get_open_grid_row() {
return $(".grid-row-open").data("grid_row"); return $(".grid-row-open").data("grid_row");
} }
refresh_dependency () {

refresh_dependency() {
// Resolve "depends_on" and show / hide accordingly // Resolve "depends_on" and show / hide accordingly
var me = this;


// build dependants' dictionary // 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; f.dependencies_clear = true;
if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) { if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) {
has_dep = true; has_dep = true;
@@ -475,8 +563,8 @@ frappe.ui.form.Layout = class Layout {
if (!has_dep) return; if (!has_dep) return;


// show / hide based on values // 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; f.guardian_has_value = true;
if (f.df.depends_on) { if (f.df.depends_on) {
// evaluate guardian // evaluate guardian
@@ -508,7 +596,8 @@ frappe.ui.form.Layout = class Layout {


this.refresh_section_count(); 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 set_property = this.evaluate_depends_on_value(condition);
let value = set_property ? 1 : 0; let value = set_property ? 1 : 0;
let form_obj; 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) { if (!doc && this.get_values) {
var doc = this.get_values(true);
doc = this.get_values(true);
} }


if (!doc) { if (!doc) {
return; 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') { if (typeof (expression) === 'boolean') {
out = expression; out = expression;
@@ -574,160 +664,3 @@ frappe.ui.form.Layout = class Layout {
return out; 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();
}
};

+ 282
- 72
frappe/public/js/frappe/form/multi_select_dialog.js Datei anzeigen

@@ -2,86 +2,191 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
constructor(opts) { constructor(opts) {
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */ /* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */
Object.assign(this, opts); 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 { } else {
this.make();
this.init();
} }
} }


make() {
let me = this;
init() {
this.page_length = 20; this.page_length = 20;
this.start = 0; this.start = 0;
let fields = this.get_primary_filters();
this.fields = this.get_fields();

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


// Make results area
fields = fields.concat([
{ fieldtype: "HTML", fieldname: "results_area" },
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 doctype_plural = this.doctype.plural();
let title = __("Select {0}", [this.for_select ? __("value") : __(doctype_plural)]);


this.dialog = new frappe.ui.Dialog({ 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"), 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) { if (this.add_filters_group) {
this.make_filter_area(); 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.$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>`); style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`);


this.$results = this.$wrapper.find('.results'); this.$results = this.$wrapper.find('.results');
this.$results.append(this.make_list_row()); 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() { get_primary_filters() {
@@ -94,7 +199,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
columns[0] = [ columns[0] = [
{ {
fieldtype: "Data", fieldtype: "Data",
label: __("Search"),
label: __("Name"),
fieldname: "search_term" fieldname: "search_term"
} }
]; ];
@@ -127,6 +232,16 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
// now a is a fixed-size array with mutable entries // 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 = [ fields = [
...columns[0], ...columns[0],
{ fieldtype: "Column Break" }, { fieldtype: "Column Break" },
@@ -156,6 +271,9 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
this.get_results(); 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() { get_custom_filters() {
@@ -166,7 +284,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
}); });
}, {}); }, {});
} else { } 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() { get_checked_values() {
// Return name of checked value. // Return name of checked value.
return this.$results.find('.list-item-container').map(function () { 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)); me.$results.append(me.make_list_row(result));
}); });


this.$results.find(".list-item--head").css("z-index", 0);

if (frappe.flags.auto_scroll) { if (frappe.flags.auto_scroll) {
this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500); 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); this.render_result_list(checked, 0, false);
} }


get_results() {
get_filters_from_setters() {
let me = this; let me = this;
let filters = this.get_query ? this.get_query().filters : {} || {}; let filters = this.get_query ? this.get_query().filters : {} || {};
let filter_fields = []; 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];
}

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


let args = {
doctype: me.doctype,
txt: me.dialog.fields_dict["search_term"].get_value(),
return {
doctype: this.doctype,
txt: this.dialog.fields_dict["search_term"].get_value(),
filters: filters, filters: filters,
filter_fields: filter_fields, filter_fields: filter_fields,
start: this.start, start: this.start,
@@ -334,25 +488,81 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
query: this.get_query ? this.get_query().query : '', query: this.get_query ? this.get_query().query : '',
as_dict: 1 as_dict: 1
}; };
frappe.call({
}

async perform_search(args) {
const res = await frappe.call({
type: "GET", type: "GET",
method: 'frappe.desk.search.search_widget', method: 'frappe.desk.search.search_widget',
no_spinner: true, no_spinner: true,
args: args, 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
- 0
frappe/public/js/frappe/form/section.js Datei anzeigen

@@ -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
- 0
frappe/public/js/frappe/form/tab.js Datei anzeigen

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

+ 1
- 1
frappe/public/js/frappe/form/toolbar.js Datei anzeigen

@@ -545,7 +545,7 @@ frappe.ui.form.Toolbar = class Toolbar {


show_jump_to_field_dialog() { show_jump_to_field_dialog() {
let visible_fields_filter = f => 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.df.hidden
&& f.disp_status !== 'None'; && f.disp_status !== 'None';




+ 30
- 3
frappe/public/js/frappe/list/base_list.js Datei anzeigen

@@ -6,7 +6,11 @@ frappe.views.BaseList = class BaseList {
} }


show() { show() {
frappe.run_serially([
return frappe.run_serially([
() => this.show_skeleton(),
() => this.fetch_meta(),
() => this.hide_skeleton(),
() => this.check_permissions(),
() => this.init(), () => this.init(),
() => this.before_refresh(), () => this.before_refresh(),
() => this.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() { setup_page() {
this.page = this.parent.page; this.page = this.parent.page;
this.$page = $(this.parent); 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() { setup_view() {
// for child classes // for child classes
} }
@@ -417,6 +445,7 @@ frappe.views.BaseList = class BaseList {
start: this.start, start: this.start,
page_length: this.page_length, page_length: this.page_length,
view: this.view, view: this.view,
group_by: this.get_group_by()
}; };
} }


@@ -463,8 +492,6 @@ frappe.views.BaseList = class BaseList {
} else { } else {
this.data = this.data.concat(data); this.data = this.data.concat(data);
} }

this.data = this.data.uniqBy((d) => d.name);
} }


freeze() { freeze() {


+ 22
- 26
frappe/public/js/frappe/list/list_factory.js Datei anzeigen

@@ -9,35 +9,31 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory {
var me = this; var me = this;
var doctype = route[1]; 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() { show() {


+ 34
- 8
frappe/public/js/frappe/list/list_view.js Datei anzeigen

@@ -33,14 +33,38 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


show() { show() {
this.parent.disable_scroll_to_top = true; this.parent.disable_scroll_to_top = true;
super.show();
}


check_permissions() {
if (!this.has_permissions()) { if (!this.has_permissions()) {
frappe.set_route(''); 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() { get view_name() {
@@ -548,6 +572,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


render() { render() {
this.render_list(); this.render_list();
this.set_rows_as_checked();
this.on_row_checked(); this.on_row_checked();
this.render_count(); this.render_count();
} }
@@ -583,9 +608,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


const subject_field = this.columns[0].df; const subject_field = this.columns[0].df;
let subject_html = ` 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")}"> 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 title="${__("Likes")}">${frappe.utils.icon('heart', 'sm', 'like-icon')}</span>
</span> </span>
<span class="level-item">${__(subject_field.label)}</span> <span class="level-item">${__(subject_field.label)}</span>
@@ -622,7 +647,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
</div> </div>
<div class="level-left checkbox-actions"> <div class="level-left checkbox-actions">
<div class="level list-subject"> <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")}"> title="${__("Select All")}">
<span class="level-item list-header-meta"></span> <span class="level-item list-header-meta"></span>
</div> </div>
@@ -930,9 +955,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {


let subject_html = ` let subject_html = `
<span class="level-item select-like"> <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)}"> 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)} ${this.get_like_html(doc)}
</span> </span>
</span> </span>
@@ -1139,6 +1164,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
if ( if (
$target.hasClass("filterable") || $target.hasClass("filterable") ||
$target.hasClass("select-like") || $target.hasClass("select-like") ||
$target.hasClass("file-select") ||
$target.hasClass("list-row-like") || $target.hasClass("list-row-like") ||
$target.is(":checkbox") $target.is(":checkbox")
) { ) {


+ 19
- 18
frappe/public/js/frappe/model/model.js Datei anzeigen

@@ -4,10 +4,10 @@
frappe.provide('frappe.model'); frappe.provide('frappe.model');


$.extend(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'], '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', std_fields_list: ['name', 'owner', 'creation', 'modified', 'modified_by',
'_user_tags', '_comments', '_assign', '_liked_by', 'docstatus', '_user_tags', '_comments', '_assign', '_liked_by', 'docstatus',
@@ -131,6 +131,7 @@ $.extend(frappe.model, {
with_doctype: function(doctype, callback, async) { with_doctype: function(doctype, callback, async) {
if(locals.DocType[doctype]) { if(locals.DocType[doctype]) {
callback && callback(); callback && callback();
return Promise.resolve();
} else { } else {
let cached_timestamp = null; let cached_timestamp = null;
let cached_doc = null; let cached_doc = null;
@@ -464,31 +465,31 @@ $.extend(frappe.model, {
}, },


trigger: function(fieldname, value, doc) { 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, // if the trigger returns a promise, return it,
// or use the default promise frappe.after_ajax // or use the default promise frappe.after_ajax
if (_promise && _promise.then) {
return _promise;
if (return_value && return_value.then) {
return return_value;
} else { } else {
return frappe.after_server_call(); return frappe.after_server_call();
} }
}
});
});
}
}; };


if(frappe.model.events[doc.doctype]) { 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); return frappe.run_serially(tasks);


+ 1
- 1
frappe/public/js/frappe/ui/dialog.js Datei anzeigen

@@ -153,7 +153,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {


set_secondary_action(click) { set_secondary_action(click) {
this.footer.removeClass('hide'); 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) { set_secondary_action_label(label) {


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.

Laden…
Abbrechen
Speichern