diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index f6f0cad31a..93189d2b1f 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -17,6 +17,7 @@ if [ "$TYPE" == "server" ]; then
fi
if [ "$DB" == "mariadb" ];then
+ sudo apt install mariadb-client-10.3
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
@@ -58,4 +59,4 @@ cd ../..
bench start &
bench --site test_site reinstall --yes
if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi
-bench build --app frappe
\ No newline at end of file
+bench build --app frappe
diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml
index 0dd4cd51d8..3ac5cfa349 100644
--- a/.github/workflows/patch-mariadb-tests.yml
+++ b/.github/workflows/patch-mariadb-tests.yml
@@ -9,7 +9,7 @@ concurrency:
jobs:
test:
- runs-on: ubuntu-18.04
+ runs-on: ubuntu-latest
name: Patch Test
diff --git a/.github/workflows/server-mariadb-tests.yml b/.github/workflows/server-mariadb-tests.yml
index 57a7fa304d..0b187fc44c 100644
--- a/.github/workflows/server-mariadb-tests.yml
+++ b/.github/workflows/server-mariadb-tests.yml
@@ -13,7 +13,7 @@ concurrency:
jobs:
test:
- runs-on: ubuntu-18.04
+ runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -121,9 +121,10 @@ jobs:
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
- name: Upload coverage data
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: codecov/codecov-action@v2
with:
name: MariaDB
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
- verbose: true
+ verbose: true
\ No newline at end of file
diff --git a/.github/workflows/server-postgres-tests.yml b/.github/workflows/server-postgres-tests.yml
index 57ac9c6c60..a5630121a4 100644
--- a/.github/workflows/server-postgres-tests.yml
+++ b/.github/workflows/server-postgres-tests.yml
@@ -12,7 +12,7 @@ concurrency:
jobs:
test:
- runs-on: ubuntu-18.04
+ runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -124,6 +124,7 @@ jobs:
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
- name: Upload coverage data
+ if: ${{ steps.check-build.outputs.build == 'strawberry' }}
uses: codecov/codecov-action@v2
with:
name: Postgres
diff --git a/.github/workflows/translation_linter.yml b/.github/workflows/translation_linter.yml
deleted file mode 100644
index 4becaebd6b..0000000000
--- a/.github/workflows/translation_linter.yml
+++ /dev/null
@@ -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
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index 2a55546ec4..0727b06043 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -12,7 +12,7 @@ concurrency:
jobs:
test:
- runs-on: ubuntu-18.04
+ runs-on: ubuntu-latest
strategy:
fail-fast: false
diff --git a/codecov.yml b/codecov.yml
index eb81252b61..41b22001a5 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1,9 +1,13 @@
codecov:
require_ci_to_pass: yes
+
+coverage:
status:
project:
default:
+ target: auto
threshold: 0.5%
+
comment:
- layout: "diff, flags, files"
+ layout: "diff"
require_changes: true
diff --git a/cypress/integration/api.js b/cypress/integration/api.js
index 7a5b1611b0..e8c39e6e25 100644
--- a/cypress/integration/api.js
+++ b/cypress/integration/api.js
@@ -31,8 +31,13 @@ context('API Resources', () => {
});
it('Removes the Comments', () => {
- cy.get_list('Comment').then(body => body.data.forEach(comment => {
- cy.remove_doc('Comment', comment.name);
- }));
+ cy.get_list('Comment').then(body => {
+ let comment_names = [];
+ body.data.map(comment => comment_names.push(comment.name));
+ comment_names = [...new Set(comment_names)]; // remove duplicates
+ comment_names.forEach((comment_name) => {
+ cy.remove_doc('Comment', comment_name);
+ });
+ });
});
});
diff --git a/cypress/integration/control_float.js b/cypress/integration/control_float.js
new file mode 100644
index 0000000000..670d1fe73e
--- /dev/null
+++ b/cypress/integration/control_float.js
@@ -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"
+ }
+ ]
+ }
+ ];
+ }
+});
diff --git a/cypress/integration/datetime_field_form_validation.js b/cypress/integration/datetime_field_form_validation.js
index 66fdde6863..ef47a0fbf7 100644
--- a/cypress/integration/datetime_field_form_validation.js
+++ b/cypress/integration/datetime_field_form_validation.js
@@ -1,19 +1,19 @@
-context('Datetime Field Validation', () => {
- before(() => {
- cy.login();
- cy.visit('/app/communication');
- cy.window().its('frappe').then(frappe => {
- frappe.call("frappe.tests.ui_test_helpers.create_communication_records");
- });
- });
+// TODO: Enable this again
+// currently this is flaky possibly because of different timezone in CI
- // validating datetime field value when value is set from backend and get validated on form load.
- it('datetime field form validation', () => {
- cy.visit('/app/communication');
- cy.get('a[title="Test Form Communication 1"]').invoke('attr', 'data-name')
- .then((name) => {
- cy.visit(`/app/communication/${name}`);
- cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red');
- });
- });
-});
\ No newline at end of file
+// context('Datetime Field Validation', () => {
+// before(() => {
+// cy.login();
+// cy.visit('/app/communication');
+// });
+
+// it('datetime field form validation', () => {
+// // validating datetime field value when value is set from backend and get validated on form load.
+// cy.window().its('frappe').then(frappe => {
+// return frappe.xcall("frappe.tests.ui_test_helpers.create_communication_record");
+// }).then(doc => {
+// cy.visit(`/app/communication/${doc.name}`);
+// cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red');
+// });
+// });
+// });
\ No newline at end of file
diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js
index 633d1335ab..298bb20432 100644
--- a/cypress/integration/list_view.js
+++ b/cypress/integration/list_view.js
@@ -7,11 +7,11 @@ context('List View', () => {
});
});
it('enables "Actions" button', () => {
- const actions = ['Approve', 'Reject', 'Edit', '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.get('.list-row-container:contains("Pending") .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').should('have.length', 8).each((el, index) => {
+ cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 9).each((el, index) => {
cy.wrap(el).contains(actions[index]);
}).then((elements) => {
cy.intercept({
diff --git a/cypress/integration/multi_select_dialog.js b/cypress/integration/multi_select_dialog.js
new file mode 100644
index 0000000000..a45fba8d32
--- /dev/null
+++ b/cypress/integration/multi_select_dialog.js
@@ -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');
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/navigation.js b/cypress/integration/navigation.js
index 7e1426aa46..ba45137cbd 100644
--- a/cypress/integration/navigation.js
+++ b/cypress/integration/navigation.js
@@ -1,7 +1,6 @@
context('Navigation', () => {
before(() => {
cy.login();
- cy.visit('/app/website');
});
it('Navigate to route with hash in document name', () => {
cy.insert_doc('ToDo', {'__newname': 'ABC#123', 'description': 'Test this', 'ignore_duplicate': true});
@@ -11,4 +10,15 @@ context('Navigation', () => {
cy.go('back');
cy.title().should('eq', 'Website');
});
+
+ it.only('Navigate to previous page after login', () => {
+ cy.visit('/app/todo');
+ 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');
+ });
});
diff --git a/cypress/integration/sidebar.js b/cypress/integration/sidebar.js
index e05f1877bf..cd771430c6 100644
--- a/cypress/integration/sidebar.js
+++ b/cypress/integration/sidebar.js
@@ -6,12 +6,12 @@ context('Sidebar', () => {
});
it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => {
- cy.click_sidebar_button(0);
+ cy.click_sidebar_button("Assigned To");
//To check if no filter is available in "Assigned To" dropdown
cy.get('.empty-state').should('contain', 'No filters found');
- cy.click_sidebar_button(1);
+ cy.click_sidebar_button("Created By");
//To check if "Created By" dropdown contains filter
cy.get('.group-by-item > .dropdown-item').should('contain', 'Me');
@@ -22,7 +22,7 @@ context('Sidebar', () => {
cy.get_field('assign_to_me', 'Check').click();
cy.get('.modal-footer > .standard-actions > .btn-primary').click();
cy.visit('/app/doctype');
- cy.click_sidebar_button(0);
+ cy.click_sidebar_button("Assigned To");
//To check if filter is added in "Assigned To" dropdown after assignment
cy.get('.group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item').should('contain', '1');
@@ -38,20 +38,19 @@ context('Sidebar', () => {
cy.get('.fieldname-select-area > .awesomplete > .form-control').should('have.value', 'Assigned To');
cy.get('.condition').should('have.value', 'like');
cy.get('.filter-field > .form-group > .input-with-feedback').should('have.value', '%Administrator%');
+ cy.click_filter_button();
//To remove the applied filter
- cy.get('.filter-action-buttons > div > .btn-secondary').contains('Clear Filters').click();
- cy.click_filter_button();
- cy.get('.filter-selector > .btn').should('contain', 'Filter');
+ cy.clear_filters();
//To remove the assignment
cy.visit('/app/doctype');
cy.click_listview_row_item(0);
cy.get('.assignments > .avatar-group > .avatar > .avatar-frame').click();
cy.get('.remove-btn').click({force: true});
- cy.get('.modal.show > .modal-dialog > .modal-content > .modal-header > .modal-actions > .btn-modal-close').click();
+ cy.hide_dialog();
cy.visit('/app/doctype');
- cy.click_sidebar_button(0);
+ cy.click_sidebar_button("Assigned To");
cy.get('.empty-state').should('contain', 'No filters found');
});
});
\ No newline at end of file
diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js
index 7a8f3a159b..6387485220 100644
--- a/cypress/integration/timeline.js
+++ b/cypress/integration/timeline.js
@@ -4,11 +4,11 @@ context('Timeline', () => {
before(() => {
cy.visit('/login');
cy.login();
- cy.visit('/app/todo');
});
it('Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo', () => {
//Adding new ToDo
+ cy.visit('/app/todo');
cy.click_listview_primary_button('Add ToDo');
cy.findByRole('button', {name: 'Edit in full page'}).click();
cy.get('[data-fieldname="description"] .ql-editor').eq(0).type('Test ToDo', {force: true});
@@ -28,15 +28,15 @@ context('Timeline', () => {
cy.get('.timeline-content').should('contain', 'Testing Timeline');
//Editing comment
- cy.click_timeline_action_btn(0);
+ cy.click_timeline_action_btn("Edit");
cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(' 123');
- cy.click_timeline_action_btn(0);
+ cy.click_timeline_action_btn("Save");
//To check if the edited comment text is visible in timeline content
cy.get('.timeline-content').should('contain', 'Testing Timeline 123');
//Discarding comment
- cy.click_timeline_action_btn(0);
+ cy.click_timeline_action_btn("Edit");
cy.findByRole('button', {name: 'Dismiss'}).click();
//To check if after discarding the timeline content is same as previous
@@ -81,7 +81,7 @@ context('Timeline', () => {
cy.visit('/app/custom-submittable-doctype');
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click();
cy.findByRole('button', {name: 'Actions'}).click();
- cy.get('.actions-btn-group > .dropdown-menu > li > .grey-link').eq(7).click();
+ cy.get('.actions-btn-group > .dropdown-menu > li > .dropdown-item').contains("Delete").click();
cy.click_modal_primary_button('Yes', {force: true, delay: 700});
//Deleting the custom doctype
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index c941652487..47c37a56a0 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -187,7 +187,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
- cy.get('@input').type(value, {waitForAnimations: false, force: true});
+ cy.get('@input').type(value, {waitForAnimations: false, force: true, delay: 100});
}
return cy.get('@input');
});
@@ -252,7 +252,8 @@ Cypress.Commands.add('new_form', doctype => {
});
Cypress.Commands.add('go_to_list', doctype => {
- cy.visit(`/app/list/${doctype}/list`);
+ let dt_in_route = doctype.toLowerCase().replace(/ /g, '-');
+ cy.visit(`/app/${dt_in_route}`);
});
Cypress.Commands.add('clear_cache', () => {
@@ -316,7 +317,11 @@ Cypress.Commands.add('add_filter', () => {
});
Cypress.Commands.add('clear_filters', () => {
- cy.get('.filter-section .filter-button').click();
+ cy.intercept({
+ method: 'POST',
+ url: 'api/method/frappe.model.utils.user_settings.save'
+ }).as('filter-saved');
+ cy.get('.filter-section .filter-button').click({force: true});
cy.wait(300);
cy.get('.filter-popover').should('exist');
cy.get('.filter-popover').find('.clear-filters').click();
@@ -324,16 +329,15 @@ Cypress.Commands.add('clear_filters', () => {
cy.window().its('cur_list').then(cur_list => {
cur_list && cur_list.filter_area && cur_list.filter_area.clear();
});
-
-
+ cy.wait('@filter-saved');
});
Cypress.Commands.add('click_modal_primary_button', (btn_name) => {
cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).trigger('click', {force: true});
});
-Cypress.Commands.add('click_sidebar_button', (btn_no) => {
- cy.get('.list-group-by-fields > .group-by-field > .btn').eq(btn_no).click();
+Cypress.Commands.add('click_sidebar_button', (btn_name) => {
+ cy.get('.list-group-by-fields .list-link > a').contains(btn_name).click({force: true});
});
Cypress.Commands.add('click_listview_row_item', (row_no) => {
@@ -348,6 +352,6 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => {
cy.get('.primary-action').contains(btn_name).click({force: true});
});
-Cypress.Commands.add('click_timeline_action_btn', (btn_no) => {
- cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').eq(btn_no).first().click();
+Cypress.Commands.add('click_timeline_action_btn', (btn_name) => {
+ cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').contains(btn_name).click();
});
\ No newline at end of file
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 7c6005a350..38904c68d0 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -618,8 +618,6 @@ def read_only():
try:
retval = fn(*args, **get_newargs(fn, kwargs))
- except:
- raise
finally:
if local and hasattr(local, 'primary_db'):
local.db.close()
@@ -629,6 +627,29 @@ def read_only():
return wrapper_fn
return innfn
+def write_only():
+ # if replica connection exists, we have to replace it momentarily with the primary connection
+ def innfn(fn):
+ def wrapper_fn(*args, **kwargs):
+ primary_db = getattr(local, "primary_db", None)
+ replica_db = getattr(local, "replica_db", None)
+ in_read_only = getattr(local, "db", None) != primary_db
+
+ # switch to primary connection
+ if in_read_only and primary_db:
+ local.db = local.primary_db
+
+ try:
+ retval = fn(*args, **get_newargs(fn, kwargs))
+ finally:
+ # switch back to replica connection
+ if in_read_only and replica_db:
+ local.db = replica_db
+
+ return retval
+ return wrapper_fn
+ return innfn
+
def only_for(roles, message=False):
"""Raise `frappe.PermissionError` if the user does not have any of the given **Roles**.
diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py
index 6eccdac4fb..6fb33a51b7 100644
--- a/frappe/commands/__init__.py
+++ b/frappe/commands/__init__.py
@@ -104,7 +104,22 @@ def get_commands():
from .utils import commands as utils_commands
from .redis import commands as redis_commands
- all_commands = scheduler_commands + site_commands + translate_commands + utils_commands + redis_commands
- return list(set(all_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
+
commands = get_commands()
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index 9098e31738..2bd3110481 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -67,6 +67,9 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
validate_database_sql
)
+ site = get_site(context)
+ frappe.init(site=site)
+
force = context.force or force
decompressed_file_name = extract_sql_from_archive(sql_file_path)
@@ -85,9 +88,6 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
# check if valid SQL file
validate_database_sql(decompressed_file_name, _raise=not force)
- site = get_site(context)
- frappe.init(site=site)
-
# dont allow downgrading to older versions of frappe without force
if not force and is_downgrade(decompressed_file_name, verbose=True):
warn_message = (
@@ -474,7 +474,7 @@ def remove_from_installed_apps(context, app):
@click.command('uninstall-app')
@click.argument('app')
-@click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False, multiple=True)
+@click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False)
@click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False)
@click.option('--no-backup', help='Do not backup the site', is_flag=True, default=False)
@click.option('--force', help='Force remove app from site', is_flag=True, default=False)
@@ -738,6 +738,131 @@ def build_search_index(context):
finally:
frappe.destroy()
+@click.command('trim-database')
+@click.option('--dry-run', is_flag=True, default=False, help='Show what would be deleted')
+@click.option('--format', '-f', default='text', type=click.Choice(['json', 'text']), help='Output format')
+@click.option('--no-backup', is_flag=True, default=False, help='Do not backup the site')
+@pass_context
+def trim_database(context, dry_run, format, no_backup):
+ if not context.sites:
+ raise SiteNotSpecifiedError
+
+ from frappe.utils.backups import scheduled_backup
+
+ ALL_DATA = {}
+
+ for site in context.sites:
+ frappe.init(site=site)
+ frappe.connect()
+
+ TABLES_TO_DROP = []
+ STANDARD_TABLES = get_standard_tables()
+ information_schema = frappe.qb.Schema("information_schema")
+ table_name = frappe.qb.Field("table_name").as_("name")
+
+ queried_result = frappe.qb.from_(
+ information_schema.tables
+ ).select(table_name).where(
+ information_schema.tables.table_schema == frappe.conf.db_name
+ ).run()
+
+ database_tables = [x[0] for x in queried_result]
+ doctype_tables = frappe.get_all("DocType", pluck="name")
+
+ for x in database_tables:
+ doctype = x.lstrip("tab")
+ if not (doctype in doctype_tables or x.startswith("__") or x in STANDARD_TABLES):
+ TABLES_TO_DROP.append(x)
+
+ if not TABLES_TO_DROP:
+ if format == "text":
+ click.secho(f"No ghost tables found in {frappe.local.site}...Great!", fg="green")
+ else:
+ if not (no_backup or dry_run):
+ if format == "text":
+ print(f"Backing Up Tables: {', '.join(TABLES_TO_DROP)}")
+
+ odb = scheduled_backup(
+ ignore_conf=False,
+ include_doctypes=",".join(x.lstrip("tab") for x in TABLES_TO_DROP),
+ ignore_files=True,
+ force=True,
+ )
+ if format == "text":
+ odb.print_summary()
+ print("\nTrimming Database")
+
+ for table in TABLES_TO_DROP:
+ if format == "text":
+ print(f"* Dropping Table '{table}'...")
+ if not dry_run:
+ frappe.db.sql_ddl(f"drop table `{table}`")
+
+ ALL_DATA[frappe.local.site] = TABLES_TO_DROP
+ frappe.destroy()
+
+ if format == "json":
+ import json
+ print(json.dumps(ALL_DATA, indent=1))
+
+
+def get_standard_tables():
+ import re
+
+ tables = []
+ sql_file = os.path.join(
+ "..", "apps", "frappe", "frappe", "database", frappe.conf.db_type, f'framework_{frappe.conf.db_type}.sql'
+ )
+ content = open(sql_file).read().splitlines()
+
+ for line in content:
+ table_found = re.search(r"""CREATE TABLE ("|`)(.*)?("|`) \(""", line)
+ if table_found:
+ tables.append(table_found.group(2))
+
+ return tables
+
+@click.command('trim-tables')
+@click.option('--dry-run', is_flag=True, default=False, help='Show what would be deleted')
+@click.option('--format', '-f', default='table', type=click.Choice(['json', 'table']), help='Output format')
+@click.option('--no-backup', is_flag=True, default=False, help='Do not backup the site')
+@pass_context
+def trim_tables(context, dry_run, format, no_backup):
+ if not context.sites:
+ raise SiteNotSpecifiedError
+
+ from frappe.model.meta import trim_tables
+ from frappe.utils.backups import scheduled_backup
+
+ for site in context.sites:
+ frappe.init(site=site)
+ frappe.connect()
+
+ if not (no_backup or dry_run):
+ click.secho(f"Taking backup for {frappe.local.site}", fg="green")
+ odb = scheduled_backup(ignore_files=False, force=True)
+ odb.print_summary()
+
+ try:
+ trimmed_data = trim_tables(dry_run=dry_run, quiet=format == 'json')
+
+ if format == 'table' and not dry_run:
+ click.secho(f"The following data have been removed from {frappe.local.site}", fg='green')
+
+ handle_data(trimmed_data, format=format)
+ finally:
+ frappe.destroy()
+
+def handle_data(data: dict, format='json'):
+ if format == 'json':
+ import json
+ print(json.dumps({frappe.local.site: data}, indent=1, sort_keys=True))
+ else:
+ from frappe.utils.commands import render_table
+ data = [["DocType", "Fields"]] + [[table, ", ".join(columns)] for table, columns in data.items()]
+ render_table(data)
+
+
commands = [
add_system_manager,
backup,
@@ -766,5 +891,7 @@ commands = [
add_to_hosts,
start_ngrok,
build_search_index,
- partial_restore
+ partial_restore,
+ trim_tables,
+ trim_database,
]
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index b0151106db..90cd60c6ec 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -408,20 +408,47 @@ def bulk_rename(context, doctype, path):
frappe.destroy()
+@click.command('db-console')
+@pass_context
+def database(context):
+ """
+ Enter into the Database console for given site.
+ """
+ site = get_site(context)
+ if not site:
+ raise SiteNotSpecifiedError
+ frappe.init(site=site)
+ if not frappe.conf.db_type or frappe.conf.db_type == "mariadb":
+ _mariadb()
+ elif frappe.conf.db_type == "postgres":
+ _psql()
+
+
@click.command('mariadb')
@pass_context
def mariadb(context):
"""
Enter into mariadb console for a given site.
"""
- import os
-
site = get_site(context)
if not site:
raise SiteNotSpecifiedError
frappe.init(site=site)
+ _mariadb()
- # This is assuming you're within the bench instance.
+
+@click.command('postgres')
+@pass_context
+def postgres(context):
+ """
+ Enter into postgres console for a given site.
+ """
+ site = get_site(context)
+ frappe.init(site=site)
+ _psql()
+
+
+def _mariadb():
mysql = find_executable('mysql')
os.execv(mysql, [
mysql,
@@ -434,15 +461,7 @@ def mariadb(context):
"-A"])
-@click.command('postgres')
-@pass_context
-def postgres(context):
- """
- Enter into postgres console for a given site.
- """
- site = get_site(context)
- frappe.init(site=site)
- # This is assuming you're within the bench instance.
+def _psql():
psql = find_executable('psql')
subprocess.run([ psql, '-d', frappe.conf.db_name])
@@ -525,6 +544,74 @@ def console(context, autoreload=False):
terminal()
+@click.command('transform-database', help="Change tables' internal settings changing engine and row formats")
+@click.option('--table', required=True, help="Comma separated name of tables to convert. To convert all tables, pass 'all'")
+@click.option('--engine', default=None, type=click.Choice(["InnoDB", "MyISAM"]), help="Choice of storage engine for said table(s)")
+@click.option('--row_format', default=None, type=click.Choice(["DYNAMIC", "COMPACT", "REDUNDANT", "COMPRESSED"]), help="Set ROW_FORMAT parameter for said table(s)")
+@click.option('--failfast', is_flag=True, default=False, help="Exit on first failure occurred")
+@pass_context
+def transform_database(context, table, engine, row_format, failfast):
+ "Transform site database through given parameters"
+ site = get_site(context)
+ check_table = []
+ add_line = False
+ skipped = 0
+ frappe.init(site=site)
+
+ if frappe.conf.db_type and frappe.conf.db_type != "mariadb":
+ click.secho("This command only has support for MariaDB databases at this point", fg="yellow")
+ sys.exit(1)
+
+ if not (engine or row_format):
+ click.secho("Values for `--engine` or `--row_format` must be set")
+ sys.exit(1)
+
+ frappe.connect()
+
+ if table == "all":
+ information_schema = frappe.qb.Schema("information_schema")
+ queried_tables = frappe.qb.from_(
+ information_schema.tables
+ ).select("table_name").where(
+ (information_schema.tables.row_format != row_format)
+ & (information_schema.tables.table_schema == frappe.conf.db_name)
+ ).run()
+ tables = [x[0] for x in queried_tables]
+ else:
+ tables = [x.strip() for x in table.split(",")]
+
+ total = len(tables)
+
+ for current, table in enumerate(tables):
+ values_to_set = ""
+ if engine:
+ values_to_set += f" ENGINE={engine}"
+ if row_format:
+ values_to_set += f" ROW_FORMAT={row_format}"
+
+ try:
+ frappe.db.sql(f"ALTER TABLE `{table}`{values_to_set}")
+ update_progress_bar("Updating table schema", current - skipped, total)
+ add_line = True
+
+ except Exception as e:
+ check_table.append([table, e.args])
+ skipped += 1
+
+ if failfast:
+ break
+
+ if add_line:
+ print()
+
+ for errored_table in check_table:
+ table, err = errored_table
+ err_msg = f"{table}: ERROR {err[0]}: {err[1]}"
+ click.secho(err_msg, fg="yellow")
+
+ frappe.destroy()
+
+
@click.command('run-tests')
@click.option('--app', help="For App")
@click.option('--doctype', help="For DocType")
@@ -811,6 +898,8 @@ commands = [
build,
clear_cache,
clear_website_cache,
+ database,
+ transform_database,
jupyter,
console,
destroy_all_sessions,
diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py
index 5f0619d170..5d0ed18d5f 100644
--- a/frappe/contacts/doctype/address/address.py
+++ b/frappe/contacts/doctype/address/address.py
@@ -65,7 +65,7 @@ class Address(Document):
def has_link(self, doctype, name):
for link in self.links:
- if link.link_doctype==doctype and link.link_name== name:
+ if link.link_doctype == doctype and link.link_name == name:
return True
def has_common_link(self, doc):
diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py
index a1aa8408bf..dfb9ff2973 100644
--- a/frappe/contacts/doctype/contact/contact.py
+++ b/frappe/contacts/doctype/contact/contact.py
@@ -47,14 +47,14 @@ class Contact(Document):
def get_link_for(self, link_doctype):
'''Return the link name, if exists for the given link DocType'''
for link in self.links:
- if link.link_doctype==link_doctype:
+ if link.link_doctype == link_doctype:
return link.link_name
return None
def has_link(self, doctype, name):
for link in self.links:
- if link.link_doctype==doctype and link.link_name== name:
+ if link.link_doctype == doctype and link.link_name == name:
return True
def has_common_link(self, doc):
diff --git a/frappe/core/doctype/access_log/access_log.py b/frappe/core/doctype/access_log/access_log.py
index 0f5776ce2f..d93da02d25 100644
--- a/frappe/core/doctype/access_log/access_log.py
+++ b/frappe/core/doctype/access_log/access_log.py
@@ -9,6 +9,7 @@ class AccessLog(Document):
@frappe.whitelist()
+@frappe.write_only()
def make_access_log(doctype=None, document=None, method=None, file_type=None,
report_name=None, filters=None, page=None, columns=None):
diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json
index a7608c0fb1..6910d615d3 100644
--- a/frappe/core/doctype/docfield/docfield.json
+++ b/frappe/core/doctype/docfield/docfield.json
@@ -1,498 +1,543 @@
{
- "actions": [],
- "autoname": "hash",
- "creation": "2013-02-22 01:27:33",
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "label_and_type",
- "label",
- "fieldtype",
- "fieldname",
- "precision",
- "length",
- "non_negative",
- "hide_days",
- "hide_seconds",
- "reqd",
- "search_index",
- "in_list_view",
- "in_standard_filter",
- "in_global_search",
- "in_preview",
- "allow_in_quick_entry",
- "bold",
- "translatable",
- "collapsible",
- "collapsible_depends_on",
- "column_break_6",
- "options",
- "default",
- "fetch_from",
- "fetch_if_empty",
- "permissions",
- "depends_on",
- "hidden",
- "read_only",
- "unique",
- "set_only_once",
- "allow_bulk_edit",
- "column_break_13",
- "permlevel",
- "ignore_user_permissions",
- "allow_on_submit",
- "report_hide",
- "remember_last_selected_value",
- "ignore_xss_filter",
- "hide_border",
- "property_depends_on_section",
- "mandatory_depends_on",
- "column_break_38",
- "read_only_depends_on",
- "display",
- "in_filter",
- "no_copy",
- "print_hide",
- "print_hide_if_no_value",
- "print_width",
- "width",
- "columns",
- "column_break_22",
- "description",
- "oldfieldname",
- "oldfieldtype"
- ],
- "fields": [
- {
- "fieldname": "label_and_type",
- "fieldtype": "Section Break"
- },
- {
- "bold": 1,
- "fieldname": "label",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Label",
- "oldfieldname": "label",
- "oldfieldtype": "Data",
- "print_width": "163",
- "search_index": 1,
- "width": "163"
- },
- {
- "bold": 1,
- "default": "Data",
- "fieldname": "fieldtype",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Type",
- "oldfieldname": "fieldtype",
- "oldfieldtype": "Select",
- "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
- "reqd": 1,
- "search_index": 1
- },
- {
- "bold": 1,
- "fieldname": "fieldname",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Name",
- "oldfieldname": "fieldname",
- "oldfieldtype": "Data",
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
- "fieldname": "reqd",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Mandatory",
- "oldfieldname": "reqd",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
- "description": "Set non-standard precision for a Float or Currency field",
- "fieldname": "precision",
- "fieldtype": "Select",
- "label": "Precision",
- "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9",
- "print_hide": 1
- },
- {
- "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
- "fieldname": "length",
- "fieldtype": "Int",
- "label": "Length"
- },
- {
- "default": "0",
- "fieldname": "search_index",
- "fieldtype": "Check",
- "label": "Index",
- "oldfieldname": "search_index",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "in_list_view",
- "fieldtype": "Check",
- "label": "In List View",
- "print_width": "70px",
- "width": "70px"
- },
- {
- "default": "0",
- "fieldname": "in_standard_filter",
- "fieldtype": "Check",
- "label": "In 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",
- "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
- "fieldname": "in_preview",
- "fieldtype": "Check",
- "label": "In Preview"
- },
- {
- "default": "0",
- "fieldname": "allow_in_quick_entry",
- "fieldtype": "Check",
- "label": "Allow in Quick Entry"
- },
- {
- "default": "0",
- "fieldname": "bold",
- "fieldtype": "Check",
- "label": "Bold"
- },
- {
- "default": "0",
- "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
- "fieldname": "translatable",
- "fieldtype": "Check",
- "label": "Translatable"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype===\"Section Break\"",
- "fieldname": "collapsible",
- "fieldtype": "Check",
- "label": "Collapsible",
- "length": 255
- },
- {
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible_depends_on",
- "fieldtype": "Code",
- "label": "Collapsible Depends On",
- "options": "JS"
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Column Break"
- },
- {
- "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
- "fieldname": "options",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Options",
- "oldfieldname": "options",
- "oldfieldtype": "Text"
- },
- {
- "fieldname": "default",
- "fieldtype": "Small Text",
- "label": "Default",
- "oldfieldname": "default",
- "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": "permissions",
- "fieldtype": "Section Break",
- "label": "Permissions"
- },
- {
- "fieldname": "depends_on",
- "fieldtype": "Code",
- "label": "Display Depends On",
- "length": 255,
- "oldfieldname": "depends_on",
- "oldfieldtype": "Data",
- "options": "JS"
- },
- {
- "default": "0",
- "fieldname": "hidden",
- "fieldtype": "Check",
- "label": "Hidden",
- "oldfieldname": "hidden",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "read_only",
- "fieldtype": "Check",
- "label": "Read Only",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "unique",
- "fieldtype": "Check",
- "label": "Unique"
- },
- {
- "default": "0",
- "description": "Do not allow user to change after set the first time",
- "fieldname": "set_only_once",
- "fieldtype": "Check",
- "label": "Set Only Once"
- },
- {
- "default": "0",
- "depends_on": "eval: doc.fieldtype == \"Table\"",
- "fieldname": "allow_bulk_edit",
- "fieldtype": "Check",
- "label": "Allow Bulk Edit"
- },
- {
- "fieldname": "column_break_13",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "permlevel",
- "fieldtype": "Int",
- "label": "Perm Level",
- "oldfieldname": "permlevel",
- "oldfieldtype": "Int",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "description": "User permissions should not apply for this Link",
- "fieldname": "ignore_user_permissions",
- "fieldtype": "Check",
- "label": "Ignore User Permissions"
- },
- {
- "default": "0",
- "depends_on": "eval: parent.is_submittable",
- "fieldname": "allow_on_submit",
- "fieldtype": "Check",
- "label": "Allow on Submit",
- "oldfieldname": "allow_on_submit",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "report_hide",
- "fieldtype": "Check",
- "label": "Report Hide",
- "oldfieldname": "report_hide",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "depends_on": "eval:(doc.fieldtype == 'Link')",
- "fieldname": "remember_last_selected_value",
- "fieldtype": "Check",
- "label": "Remember Last Selected Value"
- },
- {
- "default": "0",
- "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
- "fieldname": "ignore_xss_filter",
- "fieldtype": "Check",
- "label": "Ignore XSS Filter"
- },
- {
- "fieldname": "display",
- "fieldtype": "Section Break",
- "label": "Display"
- },
- {
- "default": "0",
- "fieldname": "in_filter",
- "fieldtype": "Check",
- "label": "In Filter",
- "oldfieldname": "in_filter",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "no_copy",
- "fieldtype": "Check",
- "label": "No Copy",
- "oldfieldname": "no_copy",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "fieldname": "print_hide",
- "fieldtype": "Check",
- "label": "Print Hide",
- "oldfieldname": "print_hide",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "default": "0",
- "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
- "fieldname": "print_hide_if_no_value",
- "fieldtype": "Check",
- "label": "Print Hide If No Value"
- },
- {
- "fieldname": "print_width",
- "fieldtype": "Data",
- "label": "Print Width"
- },
- {
- "fieldname": "width",
- "fieldtype": "Data",
- "label": "Width",
- "oldfieldname": "width",
- "oldfieldtype": "Data",
- "print_width": "50px",
- "width": "50px"
- },
- {
- "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
- "fieldname": "columns",
- "fieldtype": "Int",
- "label": "Columns"
- },
- {
- "fieldname": "column_break_22",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "description",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "print_width": "300px",
- "width": "300px"
- },
- {
- "fieldname": "oldfieldname",
- "fieldtype": "Data",
- "hidden": 1,
- "oldfieldname": "oldfieldname",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "oldfieldtype",
- "fieldtype": "Data",
- "hidden": 1,
- "oldfieldname": "oldfieldtype",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "mandatory_depends_on",
- "fieldtype": "Code",
- "label": "Mandatory Depends On",
- "options": "JS"
- },
- {
- "fieldname": "read_only_depends_on",
- "fieldtype": "Code",
- "label": "Read Only Depends On",
- "options": "JS"
- },
- {
- "fieldname": "property_depends_on_section",
- "fieldtype": "Section Break",
- "label": "Property Depends On"
- },
- {
- "fieldname": "column_break_38",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Duration'",
- "fieldname": "hide_days",
- "fieldtype": "Check",
- "label": "Hide Days"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Duration'",
- "fieldname": "hide_seconds",
- "fieldtype": "Check",
- "label": "Hide Seconds"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Section Break'",
- "fieldname": "hide_border",
- "fieldtype": "Check",
- "label": "Hide Border"
- },
- {
- "default": "0",
- "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
- "fieldname": "non_negative",
- "fieldtype": "Check",
- "label": "Non Negative"
- }
- ],
- "idx": 1,
- "index_web_pages_for_search": 1,
- "istable": 1,
- "links": [],
- "modified": "2021-07-10 22:56:04.167745",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "DocField",
- "owner": "Administrator",
- "permissions": [],
- "sort_field": "modified",
- "sort_order": "ASC"
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2013-02-22 01:27:33",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "label_and_type",
+ "label",
+ "fieldtype",
+ "fieldname",
+ "precision",
+ "length",
+ "non_negative",
+ "hide_days",
+ "hide_seconds",
+ "reqd",
+ "search_index",
+ "column_break_18",
+ "options",
+ "defaults_section",
+ "default",
+ "column_break_6",
+ "fetch_from",
+ "fetch_if_empty",
+ "visibility_section",
+ "hidden",
+ "bold",
+ "allow_in_quick_entry",
+ "translatable",
+ "print_hide",
+ "print_hide_if_no_value",
+ "report_hide",
+ "column_break_28",
+ "depends_on",
+ "collapsible",
+ "collapsible_depends_on",
+ "hide_border",
+ "list__search_settings_section",
+ "in_list_view",
+ "in_standard_filter",
+ "in_preview",
+ "column_break_35",
+ "in_filter",
+ "in_global_search",
+ "permissions",
+ "read_only",
+ "allow_on_submit",
+ "ignore_user_permissions",
+ "allow_bulk_edit",
+ "column_break_13",
+ "permlevel",
+ "ignore_xss_filter",
+ "constraints_section",
+ "unique",
+ "no_copy",
+ "set_only_once",
+ "remember_last_selected_value",
+ "column_break_38",
+ "mandatory_depends_on",
+ "read_only_depends_on",
+ "display",
+ "print_width",
+ "width",
+ "max_height",
+ "columns",
+ "column_break_22",
+ "description",
+ "oldfieldname",
+ "oldfieldtype"
+ ],
+ "fields": [{
+ "fieldname": "label_and_type",
+ "fieldtype": "Section Break"
+ },
+ {
+ "bold": 1,
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label",
+ "oldfieldname": "label",
+ "oldfieldtype": "Data",
+ "print_width": "163",
+ "search_index": 1,
+ "width": "163"
+ },
+ {
+ "bold": 1,
+ "default": "Data",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Type",
+ "oldfieldname": "fieldtype",
+ "oldfieldtype": "Select",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Name",
+ "oldfieldname": "fieldname",
+ "oldfieldtype": "Data",
+ "search_index": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Mandatory",
+ "oldfieldname": "reqd",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
+ "description": "Set non-standard precision for a Float or Currency field",
+ "fieldname": "precision",
+ "fieldtype": "Select",
+ "label": "Precision",
+ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
+ "fieldname": "length",
+ "fieldtype": "Int",
+ "label": "Length"
+ },
+ {
+ "default": "0",
+ "fieldname": "search_index",
+ "fieldtype": "Check",
+ "label": "Index",
+ "oldfieldname": "search_index",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_list_view",
+ "fieldtype": "Check",
+ "label": "In List View",
+ "print_width": "70px",
+ "width": "70px"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_standard_filter",
+ "fieldtype": "Check",
+ "label": "In List Filter"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
+ "fieldname": "in_global_search",
+ "fieldtype": "Check",
+ "label": "In Global Search"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_in_quick_entry",
+ "fieldtype": "Check",
+ "label": "Allow in Quick Entry"
+ },
+ {
+ "default": "0",
+ "fieldname": "bold",
+ "fieldtype": "Check",
+ "label": "Bold"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
+ "fieldname": "translatable",
+ "fieldtype": "Check",
+ "label": "Translatable"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype===\"Section Break\"",
+ "fieldname": "collapsible",
+ "fieldtype": "Check",
+ "label": "Collapsible",
+ "length": 255
+ },
+ {
+ "depends_on": "eval:doc.fieldtype==\"Section Break\" && doc.collapsible",
+ "fieldname": "collapsible_depends_on",
+ "fieldtype": "Code",
+ "label": "Collapsible Depends On (JS)",
+ "max_height": "3rem",
+ "options": "JS"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
+ "fieldname": "options",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Options",
+ "oldfieldname": "options",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "default",
+ "fieldtype": "Small Text",
+ "label": "Default",
+ "max_height": "3rem",
+ "oldfieldname": "default",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "fetch_from",
+ "fieldtype": "Small Text",
+ "label": "Fetch From"
+ },
+ {
+ "default": "0",
+ "fieldname": "fetch_if_empty",
+ "fieldtype": "Check",
+ "label": "Fetch only if value is not set"
+ },
+ {
+ "fieldname": "permissions",
+ "fieldtype": "Section Break",
+ "label": "Permissions"
+ },
+ {
+ "fieldname": "depends_on",
+ "fieldtype": "Code",
+ "label": "Display Depends On (JS)",
+ "length": 255,
+ "max_height": "3rem",
+ "oldfieldname": "depends_on",
+ "oldfieldtype": "Data",
+ "options": "JS"
+ },
+ {
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden",
+ "oldfieldname": "hidden",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "Read Only",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "unique",
+ "fieldtype": "Check",
+ "label": "Unique"
+ },
+ {
+ "default": "0",
+ "fieldname": "set_only_once",
+ "fieldtype": "Check",
+ "label": "Set only once"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.fieldtype == \"Table\"",
+ "fieldname": "allow_bulk_edit",
+ "fieldtype": "Check",
+ "label": "Allow Bulk Edit"
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "permlevel",
+ "fieldtype": "Int",
+ "label": "Perm Level",
+ "oldfieldname": "permlevel",
+ "oldfieldtype": "Int",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "ignore_user_permissions",
+ "fieldtype": "Check",
+ "label": "Ignore User Permissions"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: parent.is_submittable",
+ "fieldname": "allow_on_submit",
+ "fieldtype": "Check",
+ "label": "Allow on Submit",
+ "oldfieldname": "allow_on_submit",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "report_hide",
+ "fieldtype": "Check",
+ "label": "Report Hide",
+ "oldfieldname": "report_hide",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:(doc.fieldtype == 'Link')",
+ "fieldname": "remember_last_selected_value",
+ "fieldtype": "Check",
+ "label": "Remember Last Selected Value"
+ },
+ {
+ "default": "0",
+ "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
+ "fieldname": "ignore_xss_filter",
+ "fieldtype": "Check",
+ "label": "Ignore XSS Filter"
+ },
+ {
+ "fieldname": "display",
+ "fieldtype": "Section Break",
+ "label": "Display"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_filter",
+ "fieldtype": "Check",
+ "label": "In Filter",
+ "oldfieldname": "in_filter",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "no_copy",
+ "fieldtype": "Check",
+ "label": "No Copy",
+ "oldfieldname": "no_copy",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "fieldname": "print_hide",
+ "fieldtype": "Check",
+ "label": "Print Hide",
+ "oldfieldname": "print_hide",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
+ "fieldname": "print_hide_if_no_value",
+ "fieldtype": "Check",
+ "label": "Print Hide If No Value"
+ },
+ {
+ "fieldname": "print_width",
+ "fieldtype": "Data",
+ "label": "Print Width",
+ "length": 10
+ },
+ {
+ "fieldname": "width",
+ "fieldtype": "Data",
+ "label": "Width",
+ "length": 10,
+ "oldfieldname": "width",
+ "oldfieldtype": "Data",
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
+ "fieldname": "columns",
+ "fieldtype": "Int",
+ "label": "Columns"
+ },
+ {
+ "fieldname": "column_break_22",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "print_width": "300px",
+ "width": "300px"
+ },
+ {
+ "fieldname": "oldfieldname",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "oldfieldname": "oldfieldname",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "oldfieldtype",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "oldfieldname": "oldfieldtype",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "mandatory_depends_on",
+ "fieldtype": "Code",
+ "label": "Mandatory Depends On (JS)",
+ "max_height": "3rem",
+ "options": "JS"
+ },
+ {
+ "fieldname": "read_only_depends_on",
+ "fieldtype": "Code",
+ "label": "Read Only Depends On (JS)",
+ "max_height": "3rem",
+ "options": "JS"
+ },
+ {
+ "fieldname": "column_break_38",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
+ "fieldtype": "Check",
+ "label": "Hide Days"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
+ "fieldtype": "Check",
+ "label": "Hide Seconds"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Section Break'",
+ "fieldname": "hide_border",
+ "fieldtype": "Check",
+ "label": "Hide Border"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
+ "fieldname": "non_negative",
+ "fieldtype": "Check",
+ "label": "Non Negative"
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "defaults_section",
+ "fieldtype": "Section Break",
+ "label": "Defaults",
+ "max_height": "2rem"
+ },
+ {
+ "fieldname": "visibility_section",
+ "fieldtype": "Section Break",
+ "label": "Visibility"
+ },
+ {
+ "fieldname": "column_break_28",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "constraints_section",
+ "fieldtype": "Section Break",
+ "label": "Constraints"
+ },
+ {
+ "fieldname": "max_height",
+ "fieldtype": "Data",
+ "label": "Max Height",
+ "length": 10
+ },
+ {
+ "fieldname": "list__search_settings_section",
+ "fieldtype": "Section Break",
+ "label": "List / Search Settings"
+ },
+ {
+ "fieldname": "column_break_35",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-04 19:41:23.684094",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "DocField",
+ "naming_rule": "Random",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "ASC"
}
\ No newline at end of file
diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js
index 3f1b5bb7ad..262a6efd90 100644
--- a/frappe/core/doctype/doctype/doctype.js
+++ b/frappe/core/doctype/doctype/doctype.js
@@ -61,9 +61,73 @@ frappe.ui.form.on('DocType', {
__('In Grid View') : __('In List View');
frm.events.autoname(frm);
+ frm.events.set_naming_rule_description(frm);
+ },
+
+ naming_rule: function(frm) {
+ // set the "autoname" property based on naming_rule
+ if (frm.doc.naming_rule && !frm.__from_autoname) {
+
+ // flag to avoid recursion
+ frm.__from_naming_rule = true;
+
+ if (frm.doc.naming_rule=='Set by user') {
+ frm.set_value('autoname', 'Prompt');
+ } else if (frm.doc.naming_rule=='By fieldname') {
+ frm.set_value('autoname', 'field:');
+ } else if (frm.doc.naming_rule=='By "Naming Series" field') {
+ frm.set_value('autoname', 'naming_series:');
+ } else if (frm.doc.naming_rule=='Expression') {
+ frm.set_value('autoname', 'format:');
+ } else if (frm.doc.naming_rule=='Expression (old style)') {
+ // pass
+ } else if (frm.doc.naming_rule=='Random') {
+ frm.set_value('autoname', 'hash');
+ }
+ setTimeout(() =>frm.__from_naming_rule = false, 500);
+
+ frm.events.set_naming_rule_description(frm);
+ }
+
+ },
+
+ set_naming_rule_description(frm) {
+ let naming_rule_description = {
+ 'Set by user': '',
+ 'By fieldname': 'Format: field:[fieldname]. Valid fieldname must exist',
+ 'By "Naming Series" field': 'Format: naming_series:[fieldname]. Fieldname called naming_series must exist',
+ 'Expression': 'Format: format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.',
+ 'Expression (old style)': 'Format: EXAMPLE-.##### Series by prefix (separated by a dot)',
+ 'Random': '',
+ 'By script': ''
+ };
+
+ if (frm.doc.naming_rule) {
+ frm.get_field('autoname').set_description(naming_rule_description[frm.doc.naming_rule]);
+ }
},
autoname: function(frm) {
+ // set naming_rule based on autoname (for old doctypes where its not been set)
+ if (frm.doc.autoname && !frm.doc.naming_rule && !frm.__from_naming_rule) {
+ // flag to avoid recursion
+ frm.__from_autoname = true;
+ if (frm.doc.autoname.toLowerCase() === 'prompt') {
+ frm.set_value('naming_rule', 'Set by user');
+ } else if (frm.doc.autoname.startsWith('field:')) {
+ frm.set_value('naming_rule', 'By fieldname');
+ } else if (frm.doc.autoname.startsWith('naming_series:')) {
+ frm.set_value('naming_rule', 'By "Naming Series" field');
+ } else if (frm.doc.autoname.startsWith('format:')) {
+ frm.set_value('naming_rule', 'Expression');
+ } else if (frm.doc.autoname.toLowerCase() === 'hash') {
+ frm.set_value('naming_rule', 'Random');
+ } else {
+ frm.set_value('naming_rule', 'Expression (old style)');
+ }
+ setTimeout(() => frm.__from_autoname = false, 500);
+ }
+
frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt');
}
});
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 63e0426eb3..18435f8873 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -26,6 +26,7 @@
"fields_section_break",
"fields",
"sb1",
+ "naming_rule",
"autoname",
"name_case",
"allow_rename",
@@ -554,6 +555,13 @@
"fieldname": "website_search_field",
"fieldtype": "Data",
"label": "Website Search Field"
+ },
+ {
+ "fieldname": "naming_rule",
+ "fieldtype": "Select",
+ "label": "Naming Rule",
+ "length": 40,
+ "options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
}
],
"icon": "fa fa-bolt",
@@ -635,7 +643,7 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2021-08-31 15:26:19.077164",
+ "modified": "2021-09-05 15:39:13.233403",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 63f222f1e9..8f8a8ed287 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -495,6 +495,9 @@ class DocType(Document):
# retain order of 'fields' table and change order in 'field_order'
docdict["field_order"] = [f.fieldname for f in self.fields]
+ if self.custom:
+ return
+
path = get_file_path(self.module, "DocType", self.name)
if os.path.exists(path):
try:
@@ -721,20 +724,20 @@ def validate_links_table_fieldnames(meta):
for index, link in enumerate(meta.links):
link_meta = frappe.get_meta(link.link_doctype)
if not link_meta.get_field(link.link_fieldname):
- message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
+ message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))
if link.is_child_table and not meta.get_field(link.table_fieldname):
- message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
+ message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname"))
if link.is_child_table:
if not link.parent_doctype:
- message = _("Row #{0}: Parent DocType is mandatory for internal links").format(index+1)
+ message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format(index+1)
frappe.throw(message, frappe.ValidationError, _("Parent Missing"))
if not link.table_fieldname:
- message = _("Row #{0}: Table Fieldname is mandatory for internal links").format(index+1)
+ message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index+1)
frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))
def validate_fields_for_doctype(doctype):
@@ -1029,6 +1032,9 @@ def validate_fields(meta):
frappe.throw(_('Option {0} for field {1} is not a child table')
.format(frappe.bold(doctype), frappe.bold(docfield.fieldname)), title=_("Invalid Option"))
+ def check_max_height(docfield):
+ if getattr(docfield, 'max_height', None) and (docfield.max_height[-2:] not in ('px', 'em')):
+ frappe.throw('Max for {} height must be in px, em, rem'.format(frappe.bold(docfield.fieldname)))
fields = meta.get("fields")
fieldname_list = [d.fieldname for d in fields]
@@ -1062,6 +1068,7 @@ def validate_fields(meta):
scrub_options_in_select(d)
scrub_fetch_from(d)
validate_data_field_type(d)
+ check_max_height(d)
check_fold(fields)
check_search_fields(meta, fields)
@@ -1218,8 +1225,14 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
if ("tabModule Def" in frappe.db.get_tables()
and not frappe.db.exists("Module Def", doc.module)):
m = frappe.get_doc({"doctype": "Module Def", "module_name": doc.module})
- m.app_name = frappe.local.module_app[frappe.scrub(doc.module)]
+ if frappe.scrub(doc.module) in frappe.local.module_app:
+ m.app_name = frappe.local.module_app[frappe.scrub(doc.module)]
+ else:
+ m.app_name = 'frappe'
m.flags.ignore_mandatory = m.flags.ignore_permissions = True
+ if frappe.flags.package:
+ m.package = frappe.flags.package.name
+ m.custom = 1
m.insert()
default_roles = ["Administrator", "Guest", "All"]
diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.json b/frappe/core/doctype/document_naming_rule/document_naming_rule.json
index 4a88e3be6e..4e6f3f3fd1 100644
--- a/frappe/core/doctype/document_naming_rule/document_naming_rule.json
+++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.json
@@ -41,6 +41,7 @@
"fieldname": "counter",
"fieldtype": "Int",
"label": "Counter",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -79,7 +80,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-11-04 14:38:14.836056",
+ "modified": "2021-09-13 20:07:47.617615",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Rule",
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index ebbcdcee17..d9ecd85533 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -254,10 +254,11 @@ class File(Document):
return
file_name = self.file_url.split('/')[-1]
try:
- with open(get_files_path(file_name, is_private=self.is_private), "rb") as f:
+ file_path = get_files_path(file_name, is_private=self.is_private)
+ with open(file_path, "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
- frappe.throw(_("File {0} does not exist").format(self.file_url))
+ frappe.throw(_("File {0} does not exist").format(file_path))
def on_trash(self):
if self.is_home_folder or self.is_attachments_folder:
diff --git a/frappe/core/doctype/module_def/module_def.js b/frappe/core/doctype/module_def/module_def.js
index c7a6cf85f9..73d2d6562c 100644
--- a/frappe/core/doctype/module_def/module_def.js
+++ b/frappe/core/doctype/module_def/module_def.js
@@ -5,6 +5,9 @@ frappe.ui.form.on('Module Def', {
refresh: function(frm) {
frappe.xcall('frappe.core.doctype.module_def.module_def.get_installed_apps').then(r => {
frm.set_df_property('app_name', 'options', JSON.parse(r));
+ if (!frm.doc.app_name) {
+ frm.set_value('app_name', 'frappe');
+ }
});
}
});
diff --git a/frappe/core/doctype/module_def/module_def.json b/frappe/core/doctype/module_def/module_def.json
index 4de046bbb6..7ddc55fce5 100644
--- a/frappe/core/doctype/module_def/module_def.json
+++ b/frappe/core/doctype/module_def/module_def.json
@@ -8,6 +8,7 @@
"field_order": [
"module_name",
"custom",
+ "package",
"app_name",
"restrict_to_domain"
],
@@ -23,6 +24,7 @@
"unique": 1
},
{
+ "depends_on": "eval:!doc.custom",
"fieldname": "app_name",
"fieldtype": "Select",
"in_list_view": 1,
@@ -41,24 +43,84 @@
"fieldname": "custom",
"fieldtype": "Check",
"label": "Custom"
+ },
+ {
+ "depends_on": "custom",
+ "fieldname": "package",
+ "fieldtype": "Link",
+ "label": "Package",
+ "options": "Package"
}
],
"icon": "fa fa-sitemap",
"idx": 1,
"links": [
{
+ "group": "DocType",
"link_doctype": "DocType",
"link_fieldname": "module"
},
{
+ "group": "DocType",
+ "link_doctype": "Client Script",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "DocType",
+ "link_doctype": "Server Script",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Website",
+ "link_doctype": "Web Page",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Website",
+ "link_doctype": "Web Template",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Website",
+ "link_doctype": "Website Theme",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Website",
+ "link_doctype": "Web Form",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
"link_doctype": "Workspace",
"link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Custom Field",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Property Setter",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Print Format",
+ "link_fieldname": "module"
+ },
+ {
+ "group": "Customization",
+ "link_doctype": "Notification",
+ "link_fieldname": "module"
}
],
- "modified": "2021-06-02 13:04:53.118716",
+ "modified": "2021-09-05 21:58:40.253909",
"modified_by": "Administrator",
"module": "Core",
"name": "Module Def",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/core/doctype/package/__init__.py b/frappe/core/doctype/package/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/package/licenses/GNU Affero General Public License.md b/frappe/core/doctype/package/licenses/GNU Affero General Public License.md
new file mode 100644
index 0000000000..c7f159aed8
--- /dev/null
+++ b/frappe/core/doctype/package/licenses/GNU Affero General Public License.md
@@ -0,0 +1,614 @@
+### GNU AFFERO GENERAL PUBLIC LICENSE
+
+Version 3, 19 November 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+### Preamble
+
+The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains
+free software for all its users.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing
+under this license.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+### TERMS AND CONDITIONS
+
+#### 0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public
+License.
+
+"Copyright" also means copyright-like laws that apply to other kinds
+of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of
+an exact copy. The resulting work is called a "modified version" of
+the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user
+through a computer network, with no transfer of a copy, is not
+conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to
+the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+#### 1. Source Code.
+
+The "source code" for a work means the preferred form of the work for
+making modifications to it. "Object code" means any non-source form of
+a work.
+
+A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can
+regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same
+work.
+
+#### 2. Basic Permissions.
+
+All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey,
+without conditions so long as your license otherwise remains in force.
+You may convey covered works to others for the sole purpose of having
+them make modifications exclusively for you, or provide you with
+facilities for running those works, provided that you comply with the
+terms of this License in conveying all material for which you do not
+control copyright. Those thus making or running the covered works for
+you must do so exclusively on your behalf, under your direction and
+control, on terms that prohibit them from making any copies of your
+copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the
+conditions stated below. Sublicensing is not allowed; section 10 makes
+it unnecessary.
+
+#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such
+circumvention is effected by exercising rights under this License with
+respect to the covered work, and you disclaim any intention to limit
+operation or modification of the work as a means of enforcing, against
+the work's users, your or third parties' legal rights to forbid
+circumvention of technological measures.
+
+#### 4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+#### 5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these
+conditions:
+
+- a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+- b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under
+ section 7. This requirement modifies the requirement in section 4
+ to "keep intact all notices".
+- c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+- d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+#### 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of
+sections 4 and 5, provided that you also convey the machine-readable
+Corresponding Source under the terms of this License, in one of these
+ways:
+
+- a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+- b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the Corresponding
+ Source from a network server at no charge.
+- c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+- d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+- e) Convey the object code using peer-to-peer transmission,
+ provided you inform other peers where the object code and
+ Corresponding Source of the work are being offered to the general
+ public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal,
+family, or household purposes, or (2) anything designed or sold for
+incorporation into a dwelling. In determining whether a product is a
+consumer product, doubtful cases shall be resolved in favor of
+coverage. For a particular product received by a particular user,
+"normally used" refers to a typical or common use of that class of
+product, regardless of the status of the particular user or of the way
+in which the particular user actually uses, or expects or is expected
+to use, the product. A product is a consumer product regardless of
+whether the product has substantial commercial, industrial or
+non-consumer uses, unless such uses represent the only significant
+mode of use of the product.
+
+"Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to
+install and execute modified versions of a covered work in that User
+Product from a modified version of its Corresponding Source. The
+information must suffice to ensure that the continued functioning of
+the modified object code is in no case prevented or interfered with
+solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or
+updates for a work that has been modified or installed by the
+recipient, or for the User Product in which it has been modified or
+installed. Access to a network may be denied when the modification
+itself materially and adversely affects the operation of the network
+or violates the rules and protocols for communication across the
+network.
+
+Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+#### 7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders
+of that material) supplement the terms of this License with terms:
+
+- a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+- b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+- c) Prohibiting misrepresentation of the origin of that material,
+ or requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+- d) Limiting the use for publicity purposes of names of licensors
+ or authors of the material; or
+- e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+- f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions
+ of it) with contractual assumptions of liability to the recipient,
+ for any liability that these contractual assumptions directly
+ impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions; the
+above requirements apply either way.
+
+#### 8. Termination.
+
+You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+However, if you cease all violation of this License, then your license
+from a particular copyright holder is reinstated (a) provisionally,
+unless and until the copyright holder explicitly and finally
+terminates your license, and (b) permanently, if the copyright holder
+fails to notify you of the violation by some reasonable means prior to
+60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+#### 9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run
+a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+#### 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+#### 11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned
+or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the
+scope of its coverage, prohibits the exercise of, or is conditioned on
+the non-exercise of one or more of the rights that are specifically
+granted under this License. You may not convey a covered work if you
+are a party to an arrangement with a third party that is in the
+business of distributing software, under which you make payment to the
+third party based on the extent of your activity of conveying the
+work, and under which the third party grants, to any of the parties
+who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by
+you (or copies made from those copies), or (b) primarily for and in
+connection with specific products or compilations that contain the
+covered work, unless you entered into that arrangement, or that patent
+license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+#### 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under
+this License and any other pertinent obligations, then as a
+consequence you may not convey it at all. For example, if you agree to
+terms that obligate you to collect a royalty for further conveying
+from those to whom you convey the Program, the only way you could
+satisfy both those terms and this License would be to refrain entirely
+from conveying the Program.
+
+#### 13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your
+version supports such interaction) an opportunity to receive the
+Corresponding Source of your version by providing access to the
+Corresponding Source from a network server at no charge, through some
+standard or customary means of facilitating copying of software. This
+Corresponding Source shall include the Corresponding Source for any
+work covered by version 3 of the GNU General Public License that is
+incorporated pursuant to the following paragraph.
+
+Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+#### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU Affero General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever
+published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions
+of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+#### 15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+#### 16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
+CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
+NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
+LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
+TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+#### 17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
diff --git a/frappe/core/doctype/package/licenses/GNU General Public License.md b/frappe/core/doctype/package/licenses/GNU General Public License.md
new file mode 100644
index 0000000000..c4580f2eb6
--- /dev/null
+++ b/frappe/core/doctype/package/licenses/GNU General Public License.md
@@ -0,0 +1,617 @@
+### GNU GENERAL PUBLIC LICENSE
+
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+### Preamble
+
+The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom
+to share and change all versions of a program--to make sure it remains
+free software for all its users. We, the Free Software Foundation, use
+the GNU General Public License for most of our software; it applies
+also to any other work released this way by its authors. You can apply
+it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you
+have certain responsibilities if you distribute copies of the
+software, or if you modify it: responsibilities to respect the freedom
+of others.
+
+For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the
+manufacturer can do so. This is fundamentally incompatible with the
+aim of protecting users' freedom to change the software. The
+systematic pattern of such abuse occurs in the area of products for
+individuals to use, which is precisely where it is most unacceptable.
+Therefore, we have designed this version of the GPL to prohibit the
+practice for those products. If such problems arise substantially in
+other domains, we stand ready to extend this provision to those
+domains in future versions of the GPL, as needed to protect the
+freedom of users.
+
+Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish
+to avoid the special danger that patents applied to a free program
+could make it effectively proprietary. To prevent this, the GPL
+assures that patents cannot be used to render the program non-free.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+### TERMS AND CONDITIONS
+
+#### 0. Definitions.
+
+"This License" refers to version 3 of the GNU General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds
+of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of
+an exact copy. The resulting work is called a "modified version" of
+the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user
+through a computer network, with no transfer of a copy, is not
+conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to
+the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+#### 1. Source Code.
+
+The "source code" for a work means the preferred form of the work for
+making modifications to it. "Object code" means any non-source form of
+a work.
+
+A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can
+regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same
+work.
+
+#### 2. Basic Permissions.
+
+All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey,
+without conditions so long as your license otherwise remains in force.
+You may convey covered works to others for the sole purpose of having
+them make modifications exclusively for you, or provide you with
+facilities for running those works, provided that you comply with the
+terms of this License in conveying all material for which you do not
+control copyright. Those thus making or running the covered works for
+you must do so exclusively on your behalf, under your direction and
+control, on terms that prohibit them from making any copies of your
+copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the
+conditions stated below. Sublicensing is not allowed; section 10 makes
+it unnecessary.
+
+#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such
+circumvention is effected by exercising rights under this License with
+respect to the covered work, and you disclaim any intention to limit
+operation or modification of the work as a means of enforcing, against
+the work's users, your or third parties' legal rights to forbid
+circumvention of technological measures.
+
+#### 4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+#### 5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these
+conditions:
+
+- a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+- b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under
+ section 7. This requirement modifies the requirement in section 4
+ to "keep intact all notices".
+- c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+- d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+#### 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of
+sections 4 and 5, provided that you also convey the machine-readable
+Corresponding Source under the terms of this License, in one of these
+ways:
+
+- a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+- b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the Corresponding
+ Source from a network server at no charge.
+- c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+- d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+- e) Convey the object code using peer-to-peer transmission,
+ provided you inform other peers where the object code and
+ Corresponding Source of the work are being offered to the general
+ public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal,
+family, or household purposes, or (2) anything designed or sold for
+incorporation into a dwelling. In determining whether a product is a
+consumer product, doubtful cases shall be resolved in favor of
+coverage. For a particular product received by a particular user,
+"normally used" refers to a typical or common use of that class of
+product, regardless of the status of the particular user or of the way
+in which the particular user actually uses, or expects or is expected
+to use, the product. A product is a consumer product regardless of
+whether the product has substantial commercial, industrial or
+non-consumer uses, unless such uses represent the only significant
+mode of use of the product.
+
+"Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to
+install and execute modified versions of a covered work in that User
+Product from a modified version of its Corresponding Source. The
+information must suffice to ensure that the continued functioning of
+the modified object code is in no case prevented or interfered with
+solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or
+updates for a work that has been modified or installed by the
+recipient, or for the User Product in which it has been modified or
+installed. Access to a network may be denied when the modification
+itself materially and adversely affects the operation of the network
+or violates the rules and protocols for communication across the
+network.
+
+Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+#### 7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders
+of that material) supplement the terms of this License with terms:
+
+- a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+- b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+- c) Prohibiting misrepresentation of the origin of that material,
+ or requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+- d) Limiting the use for publicity purposes of names of licensors
+ or authors of the material; or
+- e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+- f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions
+ of it) with contractual assumptions of liability to the recipient,
+ for any liability that these contractual assumptions directly
+ impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions; the
+above requirements apply either way.
+
+#### 8. Termination.
+
+You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+However, if you cease all violation of this License, then your license
+from a particular copyright holder is reinstated (a) provisionally,
+unless and until the copyright holder explicitly and finally
+terminates your license, and (b) permanently, if the copyright holder
+fails to notify you of the violation by some reasonable means prior to
+60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+#### 9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run
+a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+#### 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+#### 11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned
+or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the
+scope of its coverage, prohibits the exercise of, or is conditioned on
+the non-exercise of one or more of the rights that are specifically
+granted under this License. You may not convey a covered work if you
+are a party to an arrangement with a third party that is in the
+business of distributing software, under which you make payment to the
+third party based on the extent of your activity of conveying the
+work, and under which the third party grants, to any of the parties
+who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by
+you (or copies made from those copies), or (b) primarily for and in
+connection with specific products or compilations that contain the
+covered work, unless you entered into that arrangement, or that patent
+license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+#### 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under
+this License and any other pertinent obligations, then as a
+consequence you may not convey it at all. For example, if you agree to
+terms that obligate you to collect a royalty for further conveying
+from those to whom you convey the Program, the only way you could
+satisfy both those terms and this License would be to refrain entirely
+from conveying the Program.
+
+#### 13. Use with the GNU Affero General Public License.
+
+Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+#### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in
+detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies that a certain numbered version of the GNU General Public
+License "or any later version" applies to it, you have the option of
+following the terms and conditions either of that numbered version or
+of any later version published by the Free Software Foundation. If the
+Program does not specify a version number of the GNU General Public
+License, you may choose any version ever published by the Free
+Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions
+of the GNU General Public License can be used, that proxy's public
+statement of acceptance of a version permanently authorizes you to
+choose that version for the Program.
+
+Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+#### 15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+#### 16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
+CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
+NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
+LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
+TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+#### 17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
diff --git a/frappe/core/doctype/package/licenses/MIT License.md b/frappe/core/doctype/package/licenses/MIT License.md
new file mode 100644
index 0000000000..c038ee76ae
--- /dev/null
+++ b/frappe/core/doctype/package/licenses/MIT License.md
@@ -0,0 +1,17 @@
+### MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this
+software and associated documentation files (the "Software"), to deal in the Software
+without restriction, including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies
+or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/frappe/core/doctype/package/package.js b/frappe/core/doctype/package/package.js
new file mode 100644
index 0000000000..90e2eed1e3
--- /dev/null
+++ b/frappe/core/doctype/package/package.js
@@ -0,0 +1,17 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Package', {
+ validate: function(frm) {
+ if (!frm.doc.package_name) {
+ frm.set_value('package_name', frm.doc.name.toLowerCase().replace(' ', '-'));
+ }
+ },
+
+ license_type: function(frm) {
+ frappe.call('frappe.core.doctype.package.package.get_license_text',
+ {'license_type': frm.doc.license_type}).then(r => {
+ frm.set_value('license', r.message);
+ });
+ }
+});
diff --git a/frappe/core/doctype/package/package.json b/frappe/core/doctype/package/package.json
new file mode 100644
index 0000000000..285e17a5bb
--- /dev/null
+++ b/frappe/core/doctype/package/package.json
@@ -0,0 +1,76 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2021-09-04 11:54:35.155687",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "package_name",
+ "readme",
+ "license_type",
+ "license"
+ ],
+ "fields": [
+ {
+ "fieldname": "readme",
+ "fieldtype": "Markdown Editor",
+ "label": "Readme"
+ },
+ {
+ "fieldname": "package_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Package Name",
+ "reqd": 1
+ },
+ {
+ "fieldname": "license_type",
+ "fieldtype": "Select",
+ "label": "License Type",
+ "options": "\nMIT License\nGNU General Public License\nGNU Affero General Public License"
+ },
+ {
+ "fieldname": "license",
+ "fieldtype": "Markdown Editor",
+ "label": "License"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [
+ {
+ "group": "Modules",
+ "link_doctype": "Module Def",
+ "link_fieldname": "package"
+ },
+ {
+ "group": "Release",
+ "link_doctype": "Package Release",
+ "link_fieldname": "package"
+ }
+ ],
+ "modified": "2021-09-05 13:15:01.130982",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Package",
+ "naming_rule": "Set by user",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/package/package.py b/frappe/core/doctype/package/package.py
new file mode 100644
index 0000000000..aa9735c061
--- /dev/null
+++ b/frappe/core/doctype/package/package.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+import os
+from frappe.model.document import Document
+
+class Package(Document):
+ def validate(self):
+ if not self.package_name:
+ self.package_name = self.name.lower().replace(' ', '-')
+
+@frappe.whitelist()
+def get_license_text(license_type):
+ with open(os.path.join(os.path.dirname(__file__), 'licenses',
+ license_type + '.md'), 'r') as textfile:
+ return textfile.read()
+
diff --git a/frappe/core/doctype/package/test_package.py b/frappe/core/doctype/package/test_package.py
new file mode 100644
index 0000000000..3fb8d48274
--- /dev/null
+++ b/frappe/core/doctype/package/test_package.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+import frappe
+import os
+import json
+import unittest
+
+class TestPackage(unittest.TestCase):
+ def test_package_release(self):
+ make_test_package()
+ make_test_module()
+ make_test_doctype()
+ make_test_server_script()
+ make_test_web_page()
+
+ # make release
+ frappe.get_doc(dict(
+ doctype = 'Package Release',
+ package = 'Test Package',
+ publish = 1
+ )).insert()
+
+ self.assertTrue(os.path.exists(frappe.get_site_path('packages', 'test-package')))
+ self.assertTrue(os.path.exists(frappe.get_site_path('packages', 'test-package', 'test_module_for_package')))
+ self.assertTrue(os.path.exists(frappe.get_site_path('packages', 'test-package', 'test_module_for_package', 'doctype', 'test_doctype_for_package')))
+ with open(frappe.get_site_path('packages', 'test-package', 'test_module_for_package',
+ 'doctype', 'test_doctype_for_package', 'test_doctype_for_package.json')) as f:
+ doctype = json.loads(f.read())
+ self.assertEqual(doctype['doctype'], 'DocType')
+ self.assertEqual(doctype['name'], 'Test DocType for Package')
+ self.assertEqual(doctype['fields'][0]['fieldname'], 'test_field')
+
+
+def make_test_package():
+ if not frappe.db.exists('Package', 'Test Package'):
+ frappe.get_doc(dict(
+ doctype = 'Package',
+ name = 'Test Package',
+ package_name = 'test-package',
+ readme = '# Test Package'
+ )).insert()
+
+def make_test_module():
+ if not frappe.db.exists('Module Def', 'Test Module for Package'):
+ frappe.get_doc(dict(
+ doctype = 'Module Def',
+ module_name = 'Test Module for Package',
+ custom = 1,
+ app_name = 'frappe',
+ package = 'Test Package'
+ )).insert()
+
+def make_test_doctype():
+ if not frappe.db.exists('DocType', 'Test DocType for Package'):
+ frappe.get_doc(dict(
+ doctype = 'DocType',
+ name = 'Test DocType for Package',
+ custom = 1,
+ module = 'Test Module for Package',
+ autoname = 'Prompt',
+ fields = [dict(
+ fieldname = 'test_field',
+ fieldtype = 'Data',
+ label = 'Test Field'
+ )]
+ )).insert()
+
+def make_test_server_script():
+ if not frappe.db.exists('Server Script', 'Test Script for Package'):
+ frappe.get_doc(dict(
+ doctype = 'Server Script',
+ name = 'Test Script for Package',
+ module = 'Test Module for Package',
+ script_type = 'DocType Event',
+ reference_doctype = 'Test DocType for Package',
+ doctype_event = 'Before Save',
+ script = 'frappe.msgprint("Test")'
+ )).insert()
+
+def make_test_web_page():
+ if not frappe.db.exists('Web Page', 'test-web-page-for-package'):
+ frappe.get_doc(dict(
+ doctype = "Web Page",
+ module = 'Test Module for Package',
+ main_section = "Some content",
+ published = 1,
+ title = "Test Web Page for Package"
+ )).insert()
diff --git a/frappe/core/doctype/package_import/__init__.py b/frappe/core/doctype/package_import/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/package_import/package_import.js b/frappe/core/doctype/package_import/package_import.js
new file mode 100644
index 0000000000..c01a6266cc
--- /dev/null
+++ b/frappe/core/doctype/package_import/package_import.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Package Import', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/package_import/package_import.json b/frappe/core/doctype/package_import/package_import.json
new file mode 100644
index 0000000000..f3c6168f8d
--- /dev/null
+++ b/frappe/core/doctype/package_import/package_import.json
@@ -0,0 +1,65 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format:Package Import at {creation}",
+ "creation": "2021-09-05 16:36:46.680094",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "attach_package",
+ "activate",
+ "force",
+ "log"
+ ],
+ "fields": [
+ {
+ "fieldname": "attach_package",
+ "fieldtype": "Attach",
+ "label": "Attach Package"
+ },
+ {
+ "default": "0",
+ "fieldname": "activate",
+ "fieldtype": "Check",
+ "label": "Activate"
+ },
+ {
+ "fieldname": "log",
+ "fieldtype": "Code",
+ "label": "Log",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "force",
+ "fieldtype": "Check",
+ "label": "Force"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-05 21:30:04.796090",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Package Import",
+ "naming_rule": "Expression",
+ "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
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/package_import/package_import.py b/frappe/core/doctype/package_import/package_import.py
new file mode 100644
index 0000000000..f4a2d666dd
--- /dev/null
+++ b/frappe/core/doctype/package_import/package_import.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+import os
+import json
+import subprocess
+from frappe.model.document import Document
+from frappe.desk.form.load import get_attachments
+from frappe.model.sync import get_doc_files
+from frappe.modules.import_file import import_file_by_path, import_doc
+
+class PackageImport(Document):
+ def validate(self):
+ if self.activate:
+ self.import_package()
+
+ def import_package(self):
+ attachment = get_attachments(self.doctype, self.name)
+
+ if not attachment:
+ frappe.throw(frappe._('Please attach the package'))
+
+ attachment = attachment[0]
+
+ # get package_name from file (package_name-0.0.0.tar.gz)
+ package_name = attachment.file_name.split('.')[0].rsplit('-', 1)[0]
+ if not os.path.exists(frappe.get_site_path('packages')):
+ os.makedirs(frappe.get_site_path('packages'))
+
+ # extract
+ subprocess.check_output(['tar', 'xzf',
+ frappe.get_site_path(attachment.file_url.strip('/')), '-C',
+ frappe.get_site_path('packages')])
+
+ package_path = frappe.get_site_path('packages', package_name)
+
+ # import Package
+ with open(os.path.join(package_path, package_name + '.json'), 'r') as packagefile:
+ doc_dict = json.loads(packagefile.read())
+
+ frappe.flags.package = import_doc(doc_dict)
+
+ # collect modules
+ files = []
+ log = []
+ for module in os.listdir(package_path):
+ module_path = os.path.join(package_path, module)
+ if os.path.isdir(module_path):
+ get_doc_files(files, module_path)
+
+ # import files
+ for file in files:
+ import_file_by_path(file, force=self.force, ignore_version=True,
+ for_sync=True)
+ log.append('Imported {}'.format(file))
+
+ self.log = '\n'.join(log)
diff --git a/frappe/core/doctype/package_import/test_package_import.py b/frappe/core/doctype/package_import/test_package_import.py
new file mode 100644
index 0000000000..04628fed93
--- /dev/null
+++ b/frappe/core/doctype/package_import/test_package_import.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestPackageImport(unittest.TestCase):
+ pass
diff --git a/frappe/core/doctype/package_release/__init__.py b/frappe/core/doctype/package_release/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/package_release/package_release.js b/frappe/core/doctype/package_release/package_release.js
new file mode 100644
index 0000000000..9eabe36839
--- /dev/null
+++ b/frappe/core/doctype/package_release/package_release.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Package Release', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/package_release/package_release.json b/frappe/core/doctype/package_release/package_release.json
new file mode 100644
index 0000000000..b651d699c4
--- /dev/null
+++ b/frappe/core/doctype/package_release/package_release.json
@@ -0,0 +1,95 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-09-05 12:59:01.932327",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "package",
+ "publish",
+ "path",
+ "column_break_3",
+ "major",
+ "minor",
+ "patch",
+ "section_break_7",
+ "release_notes"
+ ],
+ "fields": [
+ {
+ "fieldname": "package",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Package",
+ "options": "Package",
+ "reqd": 1
+ },
+ {
+ "fieldname": "major",
+ "fieldtype": "Int",
+ "label": "Major"
+ },
+ {
+ "fieldname": "minor",
+ "fieldtype": "Int",
+ "label": "Minor"
+ },
+ {
+ "fieldname": "patch",
+ "fieldtype": "Int",
+ "label": "Patch",
+ "no_copy": 1
+ },
+ {
+ "fieldname": "path",
+ "fieldtype": "Small Text",
+ "label": "Path",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "release_notes",
+ "fieldtype": "Markdown Editor",
+ "label": "Release Notes"
+ },
+ {
+ "default": "0",
+ "fieldname": "publish",
+ "fieldtype": "Check",
+ "label": "Publish"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-05 16:04:32.860988",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Package Release",
+ "naming_rule": "By script",
+ "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
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/package_release/package_release.py b/frappe/core/doctype/package_release/package_release.py
new file mode 100644
index 0000000000..1fb8796882
--- /dev/null
+++ b/frappe/core/doctype/package_release/package_release.py
@@ -0,0 +1,81 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+from frappe.modules.export_file import export_doc
+import os
+import subprocess
+
+class PackageRelease(Document):
+ def set_version(self):
+ # set the next patch release by default
+ if not self.major:
+ self.major = frappe.db.max('Package Release', 'major', dict(package=self.package))
+ if not self.minor:
+ self.minor = frappe.db.max('Package Release', 'minor', dict(package=self.package))
+ if not self.patch:
+ self.patch = frappe.db.max('Package Release', 'patch', dict(package=self.package)) + 1
+
+ def autoname(self):
+ self.set_version()
+ self.name = '{}-{}.{}.{}'.format(
+ frappe.db.get_value('Package', self.package, 'package_name'),
+ self.major, self.minor, self.patch)
+
+ def validate(self):
+ if self.publish:
+ self.export_files()
+
+ def export_files(self):
+ '''Export all the documents in this package to site/packages folder'''
+ package = frappe.get_doc('Package', self.package)
+
+ self.export_modules()
+ self.export_package_files(package)
+ self.make_tarfile(package)
+
+ def export_modules(self):
+ for m in frappe.db.get_all('Module Def', dict(package=self.package)):
+ module = frappe.get_doc('Module Def', m.name)
+ for l in module.meta.links:
+ if l.link_doctype == 'Module Def':
+ continue
+ # all documents of the type in the module
+ for d in frappe.get_all(l.link_doctype, dict(module=m.name)):
+ export_doc(frappe.get_doc(l.link_doctype, d.name))
+
+ def export_package_files(self, package):
+ # write readme
+ with open(frappe.get_site_path('packages', package.package_name, 'README.md'), 'w') as readme:
+ readme.write(package.readme)
+
+ # write license
+ if package.license:
+ with open(frappe.get_site_path('packages', package.package_name, 'LICENSE.md'), 'w') as license:
+ license.write(package.license)
+
+ # write package.json as `frappe_package.json`
+ with open(frappe.get_site_path('packages', package.package_name, package.package_name + '.json'), 'w') as packagefile:
+ packagefile.write(frappe.as_json(package.as_dict(no_nulls=True)))
+
+ def make_tarfile(self, package):
+ # make tarfile
+ filename = '{}.tar.gz'.format(self.name)
+ subprocess.check_output(['tar', 'czf', filename, package.package_name],
+ cwd=frappe.get_site_path('packages'))
+
+ # move file
+ subprocess.check_output(['mv', frappe.get_site_path('packages', filename),
+ frappe.get_site_path('public', 'files')])
+
+ # make attachment
+ file = frappe.get_doc(dict(
+ doctype = 'File',
+ file_url = '/' + os.path.join('files', filename),
+ attached_to_doctype = self.doctype,
+ attached_to_name = self.name
+ ))
+
+ file.flags.ignore_duplicate_entry_error = True
+ file.insert()
diff --git a/frappe/core/doctype/package_release/test_package_release.py b/frappe/core/doctype/package_release/test_package_release.py
new file mode 100644
index 0000000000..6a15e8625b
--- /dev/null
+++ b/frappe/core/doctype/package_release/test_package_release.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestPackageRelease(unittest.TestCase):
+ pass
diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json
index b7e49673f8..520c0008c5 100644
--- a/frappe/core/doctype/server_script/server_script.json
+++ b/frappe/core/doctype/server_script/server_script.json
@@ -13,6 +13,7 @@
"api_method",
"allow_guest",
"column_break_3",
+ "module",
"disabled",
"section_break_8",
"script",
@@ -93,6 +94,12 @@
"label": "Event Frequency",
"mandatory_depends_on": "eval:doc.script_type == \"Scheduler Event\"",
"options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
}
],
"index_web_pages_for_search": 1,
@@ -102,7 +109,7 @@
"link_fieldname": "server_script"
}
],
- "modified": "2021-02-18 12:36:19.803425",
+ "modified": "2021-09-04 12:02:43.671240",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 036e3638af..79fe7a9140 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -27,6 +27,11 @@ class ServerScript(Document):
for job in self.scheduled_jobs:
frappe.delete_doc("Scheduled Job Type", job.name)
+ def get_code_fields(self):
+ return {
+ 'script': 'py'
+ }
+
@property
def scheduled_jobs(self) -> List[Dict[str, str]]:
return frappe.get_all(
diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py
index 2e5254b622..79a90933e7 100644
--- a/frappe/core/doctype/user_type/user_type.py
+++ b/frappe/core/doctype/user_type/user_type.py
@@ -36,8 +36,11 @@ class UserType(Document):
if not self.user_doctypes:
return
- modules = frappe.get_all('DocType', fields=['distinct module as module'],
- filters={'name': ('in', [d.document_type for d in self.user_doctypes])})
+ modules = frappe.get_all("DocType",
+ fields=["module"],
+ filters={"name": ("in", [d.document_type for d in self.user_doctypes])},
+ distinct=True,
+ )
self.set('user_type_modules', [])
for row in modules:
diff --git a/frappe/core/doctype/version/version.css b/frappe/core/doctype/version/version.css
deleted file mode 100644
index 769b352585..0000000000
--- a/frappe/core/doctype/version/version.css
+++ /dev/null
@@ -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;
-}
diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py
index 08d0456dff..fcb558650a 100644
--- a/frappe/core/doctype/version/version.py
+++ b/frappe/core/doctype/version/version.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-# License: MIT. See LICENSE
-
import frappe, json
from frappe.model.document import Document
diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json
index 464052ba39..8536c807d2 100644
--- a/frappe/core/workspace/build/build.json
+++ b/frappe/core/workspace/build/build.json
@@ -2,7 +2,7 @@
"cards_label": "Elements",
"category": "",
"charts": [],
- "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"DocType\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Workspace\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Report\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Elements\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Modules\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Models\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Views\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Scripting\", \"col\": 4}}]",
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]",
"creation": "2021-01-02 10:51:16.579957",
"developer_mode_only": 0,
"disable_user_customization": 0,
@@ -200,9 +200,37 @@
"onboard": 0,
"only_for": "",
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Packages",
+ "link_count": 2,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Package",
+ "link_count": 0,
+ "link_to": "Package",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Package Import",
+ "link_count": 0,
+ "link_to": "Package Import",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2021-08-05 12:15:55.793022",
+ "modified": "2021-09-05 21:14:52.384815",
"modified_by": "Administrator",
"module": "Core",
"name": "Build",
diff --git a/frappe/custom/doctype/client_script/client_script.json b/frappe/custom/doctype/client_script/client_script.json
index db02d8d4bc..50f6bf3cc4 100644
--- a/frappe/custom/doctype/client_script/client_script.json
+++ b/frappe/custom/doctype/client_script/client_script.json
@@ -9,7 +9,10 @@
"field_order": [
"dt",
"view",
+ "column_break_3",
+ "module",
"enabled",
+ "section_break_6",
"script",
"sample"
],
@@ -53,13 +56,27 @@
"label": "Apply To",
"options": "List\nForm",
"set_only_once": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-03-16 20:33:51.400191",
+ "modified": "2021-09-04 12:03:27.029815",
"modified_by": "Administrator",
"module": "Custom",
"name": "Client Script",
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index 6386d7eaf2..235f11aad8 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -1,453 +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",
- "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"
- }
- ],
- "icon": "fa fa-glass",
- "idx": 1,
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-07-12 06:54:13.042319",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Custom Field",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Administrator",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "search_fields": "dt,label,fieldtype,options",
- "sort_field": "modified",
- "sort_order": "ASC",
- "track_changes": 1
+ "actions": [],
+ "allow_import": 1,
+ "creation": "2013-01-10 16:34:01",
+ "description": "Adds a custom field to a DocType",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "dt",
+ "module",
+ "label",
+ "label_help",
+ "fieldname",
+ "insert_after",
+ "length",
+ "column_break_6",
+ "fieldtype",
+ "precision",
+ "hide_seconds",
+ "hide_days",
+ "options",
+ "fetch_from",
+ "fetch_if_empty",
+ "options_help",
+ "section_break_11",
+ "collapsible",
+ "collapsible_depends_on",
+ "default",
+ "depends_on",
+ "mandatory_depends_on",
+ "read_only_depends_on",
+ "properties",
+ "non_negative",
+ "reqd",
+ "unique",
+ "read_only",
+ "ignore_user_permissions",
+ "hidden",
+ "print_hide",
+ "print_hide_if_no_value",
+ "print_width",
+ "no_copy",
+ "allow_on_submit",
+ "in_list_view",
+ "in_standard_filter",
+ "in_global_search",
+ "in_preview",
+ "bold",
+ "report_hide",
+ "search_index",
+ "allow_in_quick_entry",
+ "ignore_xss_filter",
+ "translatable",
+ "hide_border",
+ "description",
+ "permlevel",
+ "width",
+ "columns"
+ ],
+ "fields": [{
+ "bold": 1,
+ "fieldname": "dt",
+ "fieldtype": "Link",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Document",
+ "oldfieldname": "dt",
+ "oldfieldtype": "Link",
+ "options": "DocType",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_filter": 1,
+ "label": "Label",
+ "no_copy": 1,
+ "oldfieldname": "label",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "label_help",
+ "fieldtype": "HTML",
+ "label": "Label Help",
+ "oldfieldtype": "HTML"
+ },
+ {
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Fieldname",
+ "no_copy": 1,
+ "oldfieldname": "fieldname",
+ "oldfieldtype": "Data",
+ "read_only": 1
+ },
+ {
+ "description": "Select the label after which you want to insert new field.",
+ "fieldname": "insert_after",
+ "fieldtype": "Select",
+ "label": "Insert After",
+ "no_copy": 1,
+ "oldfieldname": "insert_after",
+ "oldfieldtype": "Select"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "bold": 1,
+ "default": "Data",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Field Type",
+ "oldfieldname": "fieldtype",
+ "oldfieldtype": "Select",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
+ "description": "Set non-standard precision for a Float or Currency field",
+ "fieldname": "precision",
+ "fieldtype": "Select",
+ "label": "Precision",
+ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
+ },
+ {
+ "fieldname": "options",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Options",
+ "oldfieldname": "options",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "fetch_from",
+ "fieldtype": "Small Text",
+ "label": "Fetch From"
+ },
+ {
+ "default": "0",
+ "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
+ "fieldname": "fetch_if_empty",
+ "fieldtype": "Check",
+ "label": "Fetch If Empty"
+ },
+ {
+ "fieldname": "options_help",
+ "fieldtype": "HTML",
+ "label": "Options Help",
+ "oldfieldtype": "HTML"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible",
+ "fieldtype": "Check",
+ "label": "Collapsible"
+ },
+ {
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible_depends_on",
+ "fieldtype": "Code",
+ "label": "Collapsible Depends On"
+ },
+ {
+ "fieldname": "default",
+ "fieldtype": "Text",
+ "label": "Default Value",
+ "oldfieldname": "default",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "depends_on",
+ "fieldtype": "Code",
+ "label": "Depends On",
+ "length": 255
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Field Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "print_width": "300px",
+ "width": "300px"
+ },
+ {
+ "default": "0",
+ "fieldname": "permlevel",
+ "fieldtype": "Int",
+ "label": "Permission Level",
+ "oldfieldname": "permlevel",
+ "oldfieldtype": "Int"
+ },
+ {
+ "fieldname": "width",
+ "fieldtype": "Data",
+ "label": "Width",
+ "oldfieldname": "width",
+ "oldfieldtype": "Data"
+ },
+ {
+ "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
+ "fieldname": "columns",
+ "fieldtype": "Int",
+ "label": "Columns"
+ },
+ {
+ "fieldname": "properties",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "print_width": "50%",
+ "width": "50%"
+ },
+ {
+ "default": "0",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Is Mandatory Field",
+ "oldfieldname": "reqd",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "unique",
+ "fieldtype": "Check",
+ "label": "Unique"
+ },
+ {
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "Read Only"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype===\"Link\"",
+ "fieldname": "ignore_user_permissions",
+ "fieldtype": "Check",
+ "label": "Ignore User Permissions"
+ },
+ {
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden"
+ },
+ {
+ "default": "0",
+ "fieldname": "print_hide",
+ "fieldtype": "Check",
+ "label": "Print Hide",
+ "oldfieldname": "print_hide",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
+ "fieldname": "print_hide_if_no_value",
+ "fieldtype": "Check",
+ "label": "Print Hide If No Value"
+ },
+ {
+ "fieldname": "print_width",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Print Width",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "no_copy",
+ "fieldtype": "Check",
+ "label": "No Copy",
+ "oldfieldname": "no_copy",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_on_submit",
+ "fieldtype": "Check",
+ "label": "Allow on Submit",
+ "oldfieldname": "allow_on_submit",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_list_view",
+ "fieldtype": "Check",
+ "label": "In List View"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_standard_filter",
+ "fieldtype": "Check",
+ "label": "In Standard Filter"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
+ "fieldname": "in_global_search",
+ "fieldtype": "Check",
+ "label": "In Global Search"
+ },
+ {
+ "default": "0",
+ "fieldname": "bold",
+ "fieldtype": "Check",
+ "label": "Bold"
+ },
+ {
+ "default": "0",
+ "fieldname": "report_hide",
+ "fieldtype": "Check",
+ "label": "Report Hide",
+ "oldfieldname": "report_hide",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "search_index",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Index",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
+ "fieldname": "ignore_xss_filter",
+ "fieldtype": "Check",
+ "label": "Ignore XSS Filter"
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
+ "fieldname": "translatable",
+ "fieldtype": "Check",
+ "label": "Translatable"
+ },
+ {
+ "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
+ "fieldname": "length",
+ "fieldtype": "Int",
+ "label": "Length"
+ },
+ {
+ "fieldname": "mandatory_depends_on",
+ "fieldtype": "Code",
+ "label": "Mandatory Depends On",
+ "length": 255
+ },
+ {
+ "fieldname": "read_only_depends_on",
+ "fieldtype": "Code",
+ "label": "Read Only Depends On",
+ "length": 255
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_in_quick_entry",
+ "fieldtype": "Check",
+ "label": "Allow in Quick Entry"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
+ "fieldtype": "Check",
+ "label": "Hide Seconds"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
+ "fieldtype": "Check",
+ "label": "Hide Days"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Section Break'",
+ "fieldname": "hide_border",
+ "fieldtype": "Check",
+ "label": "Hide Border"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
+ "fieldname": "non_negative",
+ "fieldtype": "Check",
+ "label": "Non Negative"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
+ }
+ ],
+ "icon": "fa fa-glass",
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-04 12:45:23.810120",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Custom Field",
+ "owner": "Administrator",
+ "permissions": [{
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "dt,label,fieldtype,options",
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/custom/doctype/property_setter/property_setter.json b/frappe/custom/doctype/property_setter/property_setter.json
index b318d92c5a..fcb36637fe 100644
--- a/frappe/custom/doctype/property_setter/property_setter.json
+++ b/frappe/custom/doctype/property_setter/property_setter.json
@@ -13,6 +13,8 @@
"field_name",
"row_name",
"column_break0",
+ "module",
+ "section_break_9",
"property",
"property_type",
"value",
@@ -91,13 +93,23 @@
"fieldname": "row_name",
"fieldtype": "Data",
"label": "Row Name"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-09-24 14:42:38.599684",
+ "modified": "2021-09-04 12:46:17.860769",
"modified_by": "Administrator",
"module": "Custom",
"name": "Property Setter",
diff --git a/frappe/data/sample_site_config.json b/frappe/data/sample_site_config.json
deleted file mode 100644
index 715cd7b9fa..0000000000
--- a/frappe/data/sample_site_config.json
+++ /dev/null
@@ -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
-}
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 227530e415..c48e86d301 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -839,6 +839,30 @@ class Database(object):
return count
+ def sum(self, dt, fieldname, filters=None):
+ return self._get_aggregation('SUM', dt, fieldname, filters)
+
+ def avg(self, dt, fieldname, filters=None):
+ return self._get_aggregation('AVG', dt, fieldname, filters)
+
+ def min(self, dt, fieldname, filters=None):
+ return self._get_aggregation('MIN', dt, fieldname, filters)
+
+ def max(self, dt, fieldname, filters=None):
+ return self._get_aggregation('MAX', dt, fieldname, filters)
+
+ def _get_aggregation(self, function, dt, fieldname, filters=None):
+ if not self.has_column(dt, fieldname):
+ frappe.throw(frappe._('Invalid column'), self.InvalidColumnName)
+
+ query = f'SELECT {function}({fieldname}) AS value FROM `tab{dt}`'
+ values = ()
+ if filters:
+ conditions, values = self.build_conditions(filters)
+ query = f"{query} WHERE {conditions}"
+
+ return self.sql(query, values)[0][0] or 0
+
@staticmethod
def format_date(date):
return getdate(date).strftime("%Y-%m-%d")
@@ -895,13 +919,13 @@ class Database(object):
WHERE table_name = 'tab{0}' AND column_name = '{1}' '''.format(doctype, column))[0][0]
def has_index(self, table_name, index_name):
- pass
+ raise NotImplementedError
def add_index(self, doctype, fields, index_name=None):
- pass
+ raise NotImplementedError
def add_unique(self, doctype, fields, constraint_name=None):
- pass
+ raise NotImplementedError
@staticmethod
def get_index_name(fields):
@@ -927,7 +951,7 @@ class Database(object):
def escape(s, percent=True):
"""Excape quotes and percent in given string."""
# implemented in specific class
- pass
+ raise NotImplementedError
@staticmethod
def is_column_missing(e):
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index d4a119804b..5ed7991a82 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -135,8 +135,8 @@ class MariaDBDatabase(Database):
table_name = get_table_name(doctype)
return self.sql(f"DESC `{table_name}`")
- def change_column_type(self, table: str, column: str, type: str) -> Union[List, Tuple]:
- table_name = get_table_name(table)
+ def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} NOT NULL")
# exception types
@@ -195,7 +195,7 @@ class MariaDBDatabase(Database):
`password` TEXT NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`)
- ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
+ ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
def create_global_search_table(self):
if not '__global_search' in self.get_tables():
@@ -256,11 +256,11 @@ class MariaDBDatabase(Database):
index_name=index_name
))
- def add_index(self, doctype, fields, index_name=None):
+ def add_index(self, doctype: str, fields: List, index_name: str = None):
"""Creates an index with given fields if not already created.
Index name will be `fieldname1_fieldname2_index`"""
index_name = index_name or self.get_index_name(fields)
- table_name = 'tab' + doctype
+ table_name = get_table_name(doctype)
if not self.has_index(table_name, index_name):
self.commit()
self.sql("""ALTER TABLE `%s`
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index f8841e9417..670fb71aa2 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -61,6 +61,7 @@ CREATE TABLE `tabDocField` (
`in_preview` int(1) NOT NULL DEFAULT 0,
`read_only` int(1) NOT NULL DEFAULT 0,
`precision` varchar(255) DEFAULT NULL,
+ `max_height` varchar(10) DEFAULT NULL,
`length` int(11) NOT NULL DEFAULT 0,
`translatable` int(1) NOT NULL DEFAULT 0,
`hide_border` int(1) NOT NULL DEFAULT 0,
@@ -71,7 +72,7 @@ CREATE TABLE `tabDocField` (
KEY `label` (`label`),
KEY `fieldtype` (`fieldtype`),
KEY `fieldname` (`fieldname`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
@@ -108,7 +109,7 @@ CREATE TABLE `tabDocPerm` (
`email` int(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`name`),
KEY `parent` (`parent`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabDocType Action`
@@ -132,7 +133,7 @@ CREATE TABLE `tabDocType Action` (
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `modified` (`modified`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
--
-- Table structure for table `tabDocType Action`
@@ -155,7 +156,7 @@ CREATE TABLE `tabDocType Link` (
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `modified` (`modified`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
--
-- Table structure for table `tabDocType`
@@ -183,6 +184,7 @@ CREATE TABLE `tabDocType` (
`restrict_to_domain` varchar(255) DEFAULT NULL,
`app` varchar(255) DEFAULT NULL,
`autoname` varchar(255) DEFAULT NULL,
+ `naming_rule` varchar(40) DEFAULT NULL,
`name_case` varchar(255) DEFAULT NULL,
`title_field` varchar(255) DEFAULT NULL,
`image_field` varchar(255) DEFAULT NULL,
@@ -226,7 +228,7 @@ CREATE TABLE `tabDocType` (
`sender_field` varchar(255) DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `parent` (`parent`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabSeries`
@@ -237,7 +239,7 @@ CREATE TABLE `tabSeries` (
`name` varchar(100),
`current` int(10) NOT NULL DEFAULT 0,
PRIMARY KEY(`name`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
@@ -254,7 +256,7 @@ CREATE TABLE `tabSessions` (
`device` varchar(255) DEFAULT 'desktop',
`status` varchar(20) DEFAULT NULL,
KEY `sid` (`sid`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
@@ -267,7 +269,7 @@ CREATE TABLE `tabSingles` (
`field` varchar(255) DEFAULT NULL,
`value` text,
KEY `singles_doctype_field_index` (`doctype`, `field`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `__Auth`
@@ -281,7 +283,7 @@ CREATE TABLE `__Auth` (
`password` TEXT NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabFile`
@@ -309,7 +311,7 @@ CREATE TABLE `tabFile` (
KEY `parent` (`parent`),
KEY `attached_to_name` (`attached_to_name`),
KEY `attached_to_doctype` (`attached_to_doctype`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabDefaultValue`
@@ -332,4 +334,4 @@ CREATE TABLE `tabDefaultValue` (
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `defaultvalue_parent_defkey_index` (`parent`,`defkey`)
-) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index b40af59286..5768a2f23d 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -4,18 +4,22 @@ from frappe.database.schema import DBTable
class MariaDBTable(DBTable):
def create(self):
- add_text = ''
+ additional_definitions = ""
+ engine = self.meta.get("engine") or "InnoDB"
+ varchar_len = frappe.db.VARCHAR_LEN
# columns
column_defs = self.get_column_definitions()
- if column_defs: add_text += ',\n'.join(column_defs) + ',\n'
+ if column_defs:
+ additional_definitions += ',\n'.join(column_defs) + ',\n'
# index
index_defs = self.get_index_definitions()
- if index_defs: add_text += ',\n'.join(index_defs) + ',\n'
+ if index_defs:
+ additional_definitions += ',\n'.join(index_defs) + ',\n'
# create table
- frappe.db.sql("""create table `%s` (
+ query = f"""create table `{self.table_name}` (
name varchar({varchar_len}) not null primary key,
creation datetime(6),
modified datetime(6),
@@ -26,13 +30,15 @@ class MariaDBTable(DBTable):
parentfield varchar({varchar_len}),
parenttype varchar({varchar_len}),
idx int(8) not null default '0',
- %sindex parent(parent),
+ {additional_definitions}
+ index parent(parent),
index modified(modified))
ENGINE={engine}
- ROW_FORMAT=COMPRESSED
+ ROW_FORMAT=DYNAMIC
CHARACTER SET=utf8mb4
- COLLATE=utf8mb4_unicode_ci""".format(varchar_len=frappe.db.VARCHAR_LEN,
- engine=self.meta.get("engine") or 'InnoDB') % (self.table_name, add_text))
+ COLLATE=utf8mb4_unicode_ci"""
+
+ frappe.db.sql(query)
def alter(self):
for col in self.columns.values():
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index 00e60fb8d2..a06abb1013 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -4,6 +4,7 @@ from typing import List, Tuple, Union
import psycopg2
import psycopg2.extensions
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
+from psycopg2.errorcodes import STRING_DATA_RIGHT_TRUNCATION
import frappe
from frappe.database.database import Database
@@ -171,7 +172,7 @@ class PostgresDatabase(Database):
@staticmethod
def is_data_too_long(e):
- return e.pgcode == '22001'
+ return e.pgcode == STRING_DATA_RIGHT_TRUNCATION
def rename_table(self, old_name: str, new_name: str) -> Union[List, Tuple]:
old_name = get_table_name(old_name)
@@ -182,8 +183,8 @@ class PostgresDatabase(Database):
table_name = get_table_name(doctype)
return self.sql(f"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '{table_name}'")
- def change_column_type(self, table: str, column: str, type: str) -> Union[List, Tuple]:
- table_name = get_table_name(table)
+ def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
return self.sql(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}')
def create_auth_table(self):
@@ -258,14 +259,14 @@ class PostgresDatabase(Database):
return self.sql("""SELECT 1 FROM pg_indexes WHERE tablename='{table_name}'
and indexname='{index_name}' limit 1""".format(table_name=table_name, index_name=index_name))
- def add_index(self, doctype, fields, index_name=None):
+ def add_index(self, doctype: str, fields: List, index_name: str = None):
"""Creates an index with given fields if not already created.
Index name will be `fieldname1_fieldname2_index`"""
+ table_name = get_table_name(doctype)
index_name = index_name or self.get_index_name(fields)
- table_name = 'tab' + doctype
+ fields_str = '", "'.join(re.sub(r"\(.*\)", "", field) for field in fields)
- self.commit()
- self.sql("""CREATE INDEX IF NOT EXISTS "{}" ON `{}`("{}")""".format(index_name, table_name, '", "'.join(fields)))
+ self.sql_ddl(f'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}` ("{fields_str}")')
def add_unique(self, doctype, fields, constraint_name=None):
if isinstance(fields, str):
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index a4e94aa326..868f98fc98 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -61,6 +61,7 @@ CREATE TABLE "tabDocField" (
"in_preview" smallint NOT NULL DEFAULT 0,
"read_only" smallint NOT NULL DEFAULT 0,
"precision" varchar(255) DEFAULT NULL,
+ "max_height" varchar(10) DEFAULT NULL,
"length" bigint NOT NULL DEFAULT 0,
"translatable" smallint NOT NULL DEFAULT 0,
"hide_border" smallint NOT NULL DEFAULT 0,
@@ -188,6 +189,7 @@ CREATE TABLE "tabDocType" (
"restrict_to_domain" varchar(255) DEFAULT NULL,
"app" varchar(255) DEFAULT NULL,
"autoname" varchar(255) DEFAULT NULL,
+ "naming_rule" varchar(40) DEFAULT NULL,
"name_case" varchar(255) DEFAULT NULL,
"title_field" varchar(255) DEFAULT NULL,
"image_field" varchar(255) DEFAULT NULL,
diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js
index 48dd2ba108..0fe3932671 100644
--- a/frappe/desk/doctype/system_console/system_console.js
+++ b/frappe/desk/doctype/system_console/system_console.js
@@ -10,15 +10,95 @@ frappe.ui.form.on('System Console', {
description: __('Execute Console script'),
ignore_inputs: true,
});
+ frm.set_value("type", "Python");
},
refresh: function(frm) {
frm.disable_save();
frm.page.set_primary_action(__("Execute"), $btn => {
- $btn.text(__('Executing...'));
- return frm.execute_action("Execute").then(() => {
- $btn.text(__('Execute'));
- });
+ $btn.text(__("Executing..."));
+ return frm
+ .execute_action("Execute")
+ .then(() => frm.trigger("render_sql_output"))
+ .finally(() => $btn.text(__("Execute")));
+ });
+ },
+
+ type: function(frm) {
+ if (frm.doc.type == "Python") {
+ frm.set_value("output", "");
+ if (frm.sql_output) {
+ frm.sql_output.destroy();
+ frm.get_field("sql_output").html("");
+ }
+ }
+ },
+
+ render_sql_output: function(frm) {
+ if (frm.doc.type !== "SQL") return;
+ if (frm.sql_output) {
+ frm.sql_output.destroy();
+ frm.get_field("sql_output").html("");
+ }
+
+ if (frm.doc.output.startsWith("Traceback")) {
+ return;
+ }
+
+ let result = JSON.parse(frm.doc.output);
+ frm.set_value("output", `${result.length} ${result.length == 1 ? 'row' : 'rows'}`);
+
+ if (result.length) {
+ let columns = Object.keys(result[0]);
+ frm.sql_output = new DataTable(
+ frm.get_field("sql_output").$wrapper.get(0),
+ {
+ columns,
+ data: result
+ }
+ );
+ }
+ },
+
+ show_processlist: function(frm) {
+ if (frm.doc.show_processlist) {
+ // keep refreshing every 5 seconds
+ frm.events.refresh_processlist(frm);
+ frm.processlist_interval = setInterval(() => frm.events.refresh_processlist(frm), 5000);
+ } else {
+ if (frm.processlist_interval) {
+
+ // end it
+ clearInterval(frm.processlist_interval);
+ frm.get_field("processlist").html('');
+ }
+ }
+ },
+
+ refresh_processlist: function(frm) {
+ let timestamp = new Date();
+ frappe.call('frappe.desk.doctype.system_console.system_console.show_processlist').then(r => {
+ let rows = '';
+ for (let row of r.message) {
+ rows += `
+
${row.Id}
+
${row.Time}
+
${row.State}
+
${row.Info}
+
${row.Progress}
+
`
+ }
+ frm.get_field('processlist').html(`
+
Requested on: ${timestamp}
+
+
+
Id
+
Time
+
State
+
Info
+
Progress
+
+ ${rows}`);
});
}
});
diff --git a/frappe/desk/doctype/system_console/system_console.json b/frappe/desk/doctype/system_console/system_console.json
index 14e36e6fd3..657e9df89d 100644
--- a/frappe/desk/doctype/system_console/system_console.json
+++ b/frappe/desk/doctype/system_console/system_console.json
@@ -17,9 +17,15 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "execute_section",
+ "type",
"console",
"commit",
- "output"
+ "output",
+ "sql_output",
+ "database_processes_section",
+ "show_processlist",
+ "processlist"
],
"fields": [
{
@@ -40,13 +46,47 @@
"fieldname": "commit",
"fieldtype": "Check",
"label": "Commit"
+ },
+ {
+ "fieldname": "execute_section",
+ "fieldtype": "Section Break",
+ "label": "Execute"
+ },
+ {
+ "fieldname": "database_processes_section",
+ "fieldtype": "Section Break",
+ "label": "Database Processes"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_processlist",
+ "fieldtype": "Check",
+ "label": "Show Processlist"
+ },
+ {
+ "fieldname": "processlist",
+ "fieldtype": "HTML",
+ "label": "processlist"
+ },
+ {
+ "default": "Python",
+ "fieldname": "type",
+ "fieldtype": "Select",
+ "label": "Type",
+ "options": "Python\nSQL"
+ },
+ {
+ "depends_on": "eval:doc.type == 'SQL'",
+ "fieldname": "sql_output",
+ "fieldtype": "HTML",
+ "label": "SQL Output"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-08-21 14:44:35.296877",
+ "modified": "2021-09-15 17:17:44.844767",
"modified_by": "Administrator",
"module": "Desk",
"name": "System Console",
@@ -65,4 +105,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py
index f7f31cc3ba..107ab2f932 100644
--- a/frappe/desk/doctype/system_console/system_console.py
+++ b/frappe/desk/doctype/system_console/system_console.py
@@ -5,7 +5,7 @@
import json
import frappe
-from frappe.utils.safe_exec import safe_exec
+from frappe.utils.safe_exec import safe_exec, read_sql
from frappe.model.document import Document
class SystemConsole(Document):
@@ -13,8 +13,11 @@ class SystemConsole(Document):
frappe.only_for('System Manager')
try:
frappe.debug_log = []
- safe_exec(self.console)
- self.output = '\n'.join(frappe.debug_log)
+ if self.type == 'Python':
+ safe_exec(self.console)
+ self.output = '\n'.join(frappe.debug_log)
+ elif self.type == 'SQL':
+ self.output = frappe.as_json(read_sql(self.console, as_dict=1))
except: # noqa: E722
self.output = frappe.get_traceback()
@@ -33,4 +36,9 @@ class SystemConsole(Document):
def execute_code(doc):
console = frappe.get_doc(json.loads(doc))
console.run()
- return console.as_dict()
\ No newline at end of file
+ return console.as_dict()
+
+@frappe.whitelist()
+def show_processlist():
+ frappe.only_for('System Manager')
+ return frappe.db.sql('show full processlist', as_dict=1)
diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json
index 1e111b8d12..756a40da4b 100644
--- a/frappe/desk/doctype/workspace/workspace.json
+++ b/frappe/desk/doctype/workspace/workspace.json
@@ -165,8 +165,6 @@
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
- "in_list_view": 1,
- "in_standard_filter": 1,
"label": "Is Standard",
"search_index": 1
},
@@ -181,7 +179,6 @@
"depends_on": "eval:doc.extends_another_page == 1 || doc.for_user",
"fieldname": "extends",
"fieldtype": "Link",
- "in_standard_filter": 1,
"label": "Extends",
"options": "Workspace",
"search_index": 1
@@ -228,6 +225,8 @@
"default": "0",
"fieldname": "public",
"fieldtype": "Check",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
"label": "Public"
},
{
@@ -265,11 +264,13 @@
"label": "Roles"
}
],
+ "in_create": 1,
"links": [],
- "modified": "2021-08-30 18:47:18.227154",
+ "modified": "2021-09-16 12:01:06.450621",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index 25dd9b26d2..a0a22a43fc 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -208,17 +208,17 @@ def save_page(title, icon, parent, public, sb_public_items, sb_private_items, de
if loads(deleted_pages):
return delete_pages(loads(deleted_pages))
- return {"name": title, "public": public}
+ return {"name": title, "public": public, "label": doc.label}
def delete_pages(deleted_pages):
for page in deleted_pages:
if page.get("public") and "Workspace Manager" not in frappe.get_roles():
- return {"name": page.get("title"), "public": 1}
+ return {"name": page.get("title"), "public": 1, "label": page.get("label")}
if frappe.db.exists("Workspace", page.get("name")):
frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True)
- return {"name": "Home", "public": 1}
+ return {"name": "Home", "public": 1, "label": "Home"}
def sort_pages(sb_public_items, sb_private_items):
wspace_public_pages = get_page_list(['name', 'title'], {'public': 1})
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index c86efbcefd..d276a9707f 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
+from typing import Dict, List, Union
import frappe, json
import frappe.utils
import frappe.share
@@ -105,9 +106,10 @@ def get_docinfo(doc=None, doctype=None, name=None):
"assignment_logs": get_comments(doc.doctype, doc.name, 'assignment'),
"permissions": get_doc_permissions(doc),
"shared": frappe.share.get_users(doc.doctype, doc.name),
- "info_logs": get_comments(doc.doctype, doc.name, 'Info'),
+ "info_logs": get_comments(doc.doctype, doc.name, comment_type=['Info', 'Edit', 'Label']),
"share_logs": get_comments(doc.doctype, doc.name, 'share'),
"like_logs": get_comments(doc.doctype, doc.name, 'Like'),
+ "workflow_logs": get_comments(doc.doctype, doc.name, comment_type="Workflow"),
"views": get_view_logs(doc.doctype, doc.name),
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
@@ -138,10 +140,11 @@ def get_communications(doctype, name, start=0, limit=20):
return _get_communications(doctype, name, start, limit)
-def get_comments(doctype, name, comment_type='Comment'):
- comment_types = [comment_type]
+def get_comments(doctype: str, name: str, comment_type : Union[str, List[str]] = "Comment") -> List[frappe._dict]:
+ if isinstance(comment_type, list):
+ comment_types = comment_type
- if comment_type == 'share':
+ elif comment_type == 'share':
comment_types = ['Shared', 'Unshared']
elif comment_type == 'assignment':
@@ -150,15 +153,21 @@ def get_comments(doctype, name, comment_type='Comment'):
elif comment_type == 'attachment':
comment_types = ['Attachment', 'Attachment Removed']
- comments = frappe.get_all('Comment', fields = ['name', 'creation', 'content', 'owner', 'comment_type'], filters=dict(
- reference_doctype = doctype,
- reference_name = name,
- comment_type = ['in', comment_types]
- ))
+ else:
+ comment_types = [comment_type]
+
+ comments = frappe.get_all("Comment",
+ fields=["name", "creation", "content", "owner", "comment_type"],
+ filters={
+ "reference_doctype": doctype,
+ "reference_name": name,
+ "comment_type": ['in', comment_types],
+ }
+ )
# convert to markdown (legacy ?)
- if comment_type == 'Comment':
- for c in comments:
+ for c in comments:
+ if c.comment_type == "Comment":
c.content = frappe.utils.markdown(c.content)
return comments
diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py
index 704e5d8ed6..f40c135653 100644
--- a/frappe/desk/treeview.py
+++ b/frappe/desk/treeview.py
@@ -69,13 +69,11 @@ def make_tree_args(**kwarg):
doctype = kwarg['doctype']
parent_field = 'parent_' + doctype.lower().replace(' ', '_')
- name_field = kwarg.get('name_field', doctype.lower().replace(' ', '_') + '_name')
if kwarg['is_root'] == 'false': kwarg['is_root'] = False
if kwarg['is_root'] == 'true': kwarg['is_root'] = True
kwarg.update({
- name_field: kwarg[name_field],
parent_field: kwarg.get("parent") or kwarg.get(parent_field)
})
diff --git a/frappe/hooks.py b/frappe/hooks.py
index f3d25d6bf4..2ae5a59066 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -12,11 +12,11 @@ source_link = "https://github.com/frappe/frappe"
app_license = "MIT"
app_logo_url = '/assets/frappe/images/frappe-framework-logo.svg'
-develop_version = '13.x.x-develop'
+develop_version = '14.x.x-develop'
-app_email = "info@frappe.io"
+app_email = "developers@frappe.io"
-docs_app = "frappe_io"
+docs_app = "frappe_docs"
translator_url = "https://translate.erpnext.com"
@@ -164,7 +164,8 @@ doc_events = {
"after_rename": "frappe.desk.notifications.clear_doctype_notifications",
"on_cancel": [
"frappe.desk.notifications.clear_doctype_notifications",
- "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions"
+ "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
+ "frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers"
],
"on_trash": [
"frappe.desk.notifications.clear_doctype_notifications",
diff --git a/frappe/installer.py b/frappe/installer.py
index 23247046f6..f0bf0cb51c 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -445,9 +445,21 @@ def extract_sql_from_archive(sql_file_path):
else:
decompressed_file_name = sql_file_path
+ # convert archive sql to latest compatible
+ convert_archive_content(decompressed_file_name)
+
return decompressed_file_name
+def convert_archive_content(sql_file_path):
+ if frappe.conf.db_type == "mariadb":
+ # ever since mariaDB 10.6, row_format COMPRESSED has been deprecated and removed
+ # this step is added to ease restoring sites depending on older mariaDB servers
+ contents = open(sql_file_path).read()
+ with open(sql_file_path, "w") as f:
+ f.write(contents.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC"))
+
+
def extract_sql_gzip(sql_gz_path):
import subprocess
@@ -457,7 +469,7 @@ def extract_sql_gzip(sql_gz_path):
decompressed_file = original_file.rstrip(".gz")
cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file)
subprocess.check_call(cmd, shell=True)
- except:
+ except Exception:
raise
return decompressed_file
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index b9c8900839..5605ac61ed 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -307,7 +307,7 @@ class BaseDocument(object):
doc["doctype"] = self.doctype
for df in self.meta.get_table_fields():
children = self.get(df.fieldname) or []
- doc[df.fieldname] = [d.as_dict(convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls) for d in children]
+ doc[df.fieldname] = [d.as_dict(convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls, no_default_fields=no_default_fields) for d in children]
if no_nulls:
for k in list(doc):
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index ae159c1a69..fd74a8cfe4 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -2,6 +2,7 @@
# License: MIT. See LICENSE
"""build query for doclistview and return results"""
+from typing import List
import frappe.defaults
import frappe.share
from frappe import _
@@ -33,7 +34,7 @@ class DatabaseQuery(object):
join='left join', distinct=False, start=None, page_length=None, limit=None,
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
update=None, add_total_row=None, user_settings=None, reference_doctype=None,
- return_query=False, strict=True, pluck=None, ignore_ddl=False):
+ return_query=False, strict=True, pluck=None, ignore_ddl=False) -> List:
if not ignore_permissions and \
not frappe.has_permission(self.doctype, "select", user=user) and \
not frappe.has_permission(self.doctype, "read", user=user):
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index 207aca089b..cd0d8e0f3a 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -15,6 +15,7 @@ Example:
'''
from datetime import datetime
+import click
import frappe, json, os
from frappe.utils import cstr, cint, cast
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields
@@ -658,27 +659,48 @@ def get_default_df(fieldname):
fieldtype = "Data"
)
-def trim_tables(doctype=None):
+def trim_tables(doctype=None, dry_run=False, quiet=False):
"""
Removes database fields that don't exist in the doctype (json or custom field). This may be needed
as maintenance since removing a field in a DocType doesn't automatically
delete the db field.
"""
- ignore_fields = default_fields + optional_fields
-
- filters={ "issingle": 0 }
+ UPDATED_TABLES = {}
+ filters = {"issingle": 0}
if doctype:
filters["name"] = doctype
- for doctype in frappe.db.get_all("DocType", filters=filters):
- doctype = doctype.name
- columns = frappe.db.get_table_columns(doctype)
- fields = frappe.get_meta(doctype).get_fieldnames_with_value()
- columns_to_remove = [f for f in list(set(columns) - set(fields)) if f not in ignore_fields
- and not f.startswith("_")]
- if columns_to_remove:
- print(doctype, "columns removed:", columns_to_remove)
- columns_to_remove = ", ".join("drop `{0}`".format(c) for c in columns_to_remove)
- query = """alter table `tab{doctype}` {columns}""".format(
- doctype=doctype, columns=columns_to_remove)
- frappe.db.sql_ddl(query)
+ for doctype in frappe.db.get_all("DocType", filters=filters, pluck="name"):
+ try:
+ dropped_columns = trim_table(doctype, dry_run=dry_run)
+ if dropped_columns:
+ UPDATED_TABLES[doctype] = dropped_columns
+ except frappe.db.TableMissingError:
+ if quiet:
+ continue
+ click.secho(f"Ignoring missing table for DocType: {doctype}", fg="yellow", err=True)
+ click.secho(f"Consider removing record in the DocType table for {doctype}", fg="yellow", err=True)
+ except Exception as e:
+ if quiet:
+ continue
+ click.echo(e, err=True)
+
+ return UPDATED_TABLES
+
+
+def trim_table(doctype, dry_run=True):
+ frappe.cache().hdel('table_columns', f"tab{doctype}")
+ ignore_fields = default_fields + optional_fields
+ columns = frappe.db.get_table_columns(doctype)
+ fields = frappe.get_meta(doctype, cached=False).get_fieldnames_with_value()
+ is_internal = lambda f: f not in ignore_fields and not f.startswith("_")
+ columns_to_remove = [
+ f for f in list(set(columns) - set(fields)) if is_internal(f)
+ ]
+ DROPPED_COLUMNS = columns_to_remove[:]
+
+ if columns_to_remove and not dry_run:
+ columns_to_remove = ", ".join(f"DROP `{c}`" for c in columns_to_remove)
+ frappe.db.sql_ddl(f"ALTER TABLE `tab{doctype}` {columns_to_remove}")
+
+ return DROPPED_COLUMNS
diff --git a/frappe/model/naming.py b/frappe/model/naming.py
index 2e7f54938f..71ff281642 100644
--- a/frappe/model/naming.py
+++ b/frappe/model/naming.py
@@ -346,7 +346,7 @@ def _prompt_autoname(autoname, doc):
"""
# set from __newname in save.py
if not doc.name:
- frappe.throw(_("Name not set via prompt"))
+ frappe.throw(_("Please set the document name"))
def _format_autoname(autoname, doc):
"""
diff --git a/frappe/model/sync.py b/frappe/model/sync.py
index c2e3fcac08..138f9eaad4 100644
--- a/frappe/model/sync.py
+++ b/frappe/model/sync.py
@@ -80,9 +80,11 @@ def get_doc_files(files, start_path):
# load in sequence - warning for devs
document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
- 'website_theme', 'web_form', 'web_template', 'notification', 'print_style',
- 'data_migration_mapping', 'data_migration_plan', 'workspace',
- 'onboarding_step', 'module_onboarding', 'form_tour']
+ 'web_page', 'website_theme', 'web_form', 'web_template',
+ 'notification', 'print_style',
+ 'data_migration_mapping', 'data_migration_plan',
+ 'workspace', 'onboarding_step', 'module_onboarding', 'form_tour',
+ 'client_script', 'server_script', 'custom_field', 'property_setter']
for doctype in document_types:
doctype_path = os.path.join(start_path, doctype)
diff --git a/frappe/modules.txt b/frappe/modules.txt
index ae10c3ad55..1229116a2e 100644
--- a/frappe/modules.txt
+++ b/frappe/modules.txt
@@ -12,4 +12,4 @@ Data Migration
Chat
Social
Automation
-Event Streaming
\ No newline at end of file
+Event Streaming
diff --git a/frappe/modules/export_file.py b/frappe/modules/export_file.py
index 4a00295022..17e84ee488 100644
--- a/frappe/modules/export_file.py
+++ b/frappe/modules/export_file.py
@@ -6,7 +6,7 @@ import frappe.model
from frappe.modules import scrub, get_module_path, scrub_dt_dn
def export_doc(doc):
- export_to_files([[doc.doctype, doc.name]])
+ write_document_file(doc)
def export_to_files(record_list=None, record_module=None, verbose=0, create_init=None):
"""
@@ -21,16 +21,10 @@ def export_to_files(record_list=None, record_module=None, verbose=0, create_init
write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init, folder_name=folder_name)
def write_document_file(doc, record_module=None, create_init=True, folder_name=None):
- newdoc = doc.as_dict(no_nulls=True)
- doc.run_method("before_export", newdoc)
-
- # strip out default fields from children
- for df in doc.meta.get_table_fields():
- for d in newdoc.get(df.fieldname):
- for fieldname in frappe.model.default_fields:
- if fieldname in d:
- del d[fieldname]
+ doc_export = doc.as_dict(no_nulls=True)
+ doc.run_method("before_export", doc_export)
+ strip_default_fields(doc, doc_export)
module = record_module or get_module_name(doc)
# create folder
@@ -39,10 +33,33 @@ def write_document_file(doc, record_module=None, create_init=True, folder_name=N
else:
folder = create_folder(module, doc.doctype, doc.name, create_init)
- # write the data file
fname = scrub(doc.name)
+ write_code_files(folder, fname, doc, doc_export)
+
+ # write the data file
with open(os.path.join(folder, fname + ".json"), 'w+') as txtfile:
- txtfile.write(frappe.as_json(newdoc))
+ txtfile.write(frappe.as_json(doc_export))
+
+def strip_default_fields(doc, doc_export):
+ # strip out default fields from children
+ for df in doc.meta.get_table_fields():
+ for d in doc_export.get(df.fieldname):
+ for fieldname in frappe.model.default_fields:
+ if fieldname in d:
+ del d[fieldname]
+
+def write_code_files(folder, fname, doc, doc_export):
+ '''Export code files and strip from values'''
+ if hasattr(doc, 'get_code_fields'):
+ for key, extn in doc.get_code_fields().items():
+ if doc.get(key):
+ with open(os.path.join(folder, fname + "." + extn), 'w+') as txtfile:
+ txtfile.write(doc.get(key))
+
+ # remove from exporting
+ del doc_export[key]
+
+
def get_module_name(doc):
if doc.doctype == 'Module Def':
@@ -57,7 +74,10 @@ def get_module_name(doc):
return module
def create_folder(module, dt, dn, create_init):
- module_path = get_module_path(module)
+ if frappe.db.get_value('Module Def', module, 'custom'):
+ module_path = get_custom_module_path(module)
+ else:
+ module_path = get_module_path(module)
dt, dn = scrub_dt_dn(dt, dn)
@@ -72,6 +92,23 @@ def create_folder(module, dt, dn, create_init):
return folder
+def get_custom_module_path(module):
+ package = frappe.db.get_value('Module Def', module, 'package')
+ if not package:
+ frappe.throw('Package must be set for custom Module {module}'.format(module=module))
+
+ path = os.path.join(get_package_path(package), scrub(module))
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+ return path
+
+def get_package_path(package):
+ path = os.path.join(frappe.get_site_path('packages'), frappe.db.get_value('Package', package, 'package_name'))
+ if not os.path.exists(path):
+ os.makedirs(path)
+ return path
+
def create_init_py(module_path, dt, dn):
def create_if_not_exists(path):
initpy = os.path.join(path, '__init__.py')
diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py
index e398e384e3..e7a1f5f97c 100644
--- a/frappe/modules/import_file.py
+++ b/frappe/modules/import_file.py
@@ -54,31 +54,26 @@ def import_file_by_path(path, force=False, data_import=False, pre_process=None,
docs = [docs]
for doc in docs:
- if not force:
- # check if timestamps match
- db_modified = frappe.db.get_value(doc['doctype'], doc['name'], 'modified')
- if db_modified and doc.get('modified')==get_datetime_str(db_modified):
- return False
+ if not force and not is_changed(doc):
+ return False
original_modified = doc.get("modified")
- frappe.flags.in_import = True
import_doc(doc, force=force, data_import=data_import, pre_process=pre_process,
- ignore_version=ignore_version, reset_permissions=reset_permissions)
- frappe.flags.in_import = False
+ ignore_version=ignore_version, reset_permissions=reset_permissions, path=path)
if original_modified:
- # since there is a new timestamp on the file, update timestamp in
- if doc["doctype"] == doc["name"] and doc["name"]!="DocType":
- frappe.db.sql("""update tabSingles set value=%s where field="modified" and doctype=%s""",
- (original_modified, doc["name"]))
- else:
- frappe.db.sql("update `tab%s` set modified=%s where name=%s" % \
- (doc['doctype'], '%s', '%s'),
- (original_modified, doc['name']))
+ update_modified(original_modified, doc)
return True
+def is_changed(doc):
+ # check if timestamps match
+ db_modified = frappe.db.get_value(doc['doctype'], doc['name'], 'modified')
+ if db_modified and doc.get('modified')==get_datetime_str(db_modified):
+ return False
+ return True
+
def read_doc_from_file(path):
doc = None
if os.path.exists(path):
@@ -93,8 +88,17 @@ def read_doc_from_file(path):
return doc
+def update_modified(original_modified, doc):
+ # since there is a new timestamp on the file, update timestamp in
+ if doc["doctype"] == doc["name"] and doc["name"]!="DocType":
+ frappe.db.sql("""update tabSingles set value=%s where field="modified" and doctype=%s""",
+ (original_modified, doc["name"]))
+ else:
+ frappe.db.sql("update `tab%s` set modified=%s where name=%s" % (doc['doctype'],
+ '%s', '%s'), (original_modified, doc['name']))
+
def import_doc(docdict, force=False, data_import=False, pre_process=None,
- ignore_version=None, reset_permissions=False):
+ ignore_version=None, reset_permissions=False, path=None):
frappe.flags.in_import = True
docdict["__islocal"] = 1
@@ -104,14 +108,8 @@ def import_doc(docdict, force=False, data_import=False, pre_process=None,
doc = frappe.get_doc(docdict)
- # Note on Tree DocTypes:
- # The tree structure is maintained in the database via the fields "lft" and
- # "rgt". They are automatically set and kept up-to-date. Importing them
- # would destroy any existing tree structure.
- if getattr(doc.meta, 'is_tree', None) and any([doc.lft, doc.rgt]):
- print('Ignoring values of `lft` and `rgt` for {} "{}"'.format(doc.doctype, doc.name))
- doc.lft = None
- doc.rgt = None
+ reset_tree_properties(doc)
+ load_code_properties(doc, path)
doc.run_method("before_import")
@@ -119,27 +117,9 @@ def import_doc(docdict, force=False, data_import=False, pre_process=None,
if pre_process:
pre_process(doc)
- ignore = []
-
if frappe.db.exists(doc.doctype, doc.name):
+ delete_old_doc(doc, reset_permissions)
- old_doc = frappe.get_doc(doc.doctype, doc.name)
-
- if doc.doctype in ignore_values:
- # update ignore values
- for key in ignore_values.get(doc.doctype) or []:
- doc.set(key, old_doc.get(key))
-
- # update ignored docs into new doc
- for df in doc.meta.get_table_fields():
- if df.options in ignore_doctypes and not reset_permissions:
- doc.set(df.fieldname, [])
- ignore.append(df.options)
-
- # delete old
- frappe.delete_doc(doc.doctype, doc.name, force=1, ignore_doctypes=ignore, for_reload=True)
-
- doc.flags.ignore_children_type = ignore
doc.flags.ignore_links = True
if not data_import:
doc.flags.ignore_validate = True
@@ -149,3 +129,47 @@ def import_doc(docdict, force=False, data_import=False, pre_process=None,
doc.insert()
frappe.flags.in_import = False
+
+ return doc
+
+def load_code_properties(doc, path):
+ '''Load code files stored in separate files with extensions'''
+ if path:
+ if hasattr(doc, 'get_code_fields'):
+ dirname, filename = os.path.split(path)
+ for key, extn in doc.get_code_fields().items():
+ codefile = os.path.join(dirname, filename.split('.')[0]+'.'+extn)
+ if os.path.exists(codefile):
+ with open(codefile,'r') as txtfile:
+ doc.set(key, txtfile.read())
+
+
+def delete_old_doc(doc, reset_permissions):
+ ignore = []
+ old_doc = frappe.get_doc(doc.doctype, doc.name)
+
+ if doc.doctype in ignore_values:
+ # update ignore values
+ for key in ignore_values.get(doc.doctype) or []:
+ doc.set(key, old_doc.get(key))
+
+ # update ignored docs into new doc
+ for df in doc.meta.get_table_fields():
+ if df.options in ignore_doctypes and not reset_permissions:
+ doc.set(df.fieldname, [])
+ ignore.append(df.options)
+
+ # delete old
+ frappe.delete_doc(doc.doctype, doc.name, force=1, ignore_doctypes=ignore, for_reload=True)
+
+ doc.flags.ignore_children_type = ignore
+
+def reset_tree_properties(doc):
+ # Note on Tree DocTypes:
+ # The tree structure is maintained in the database via the fields "lft" and
+ # "rgt". They are automatically set and kept up-to-date. Importing them
+ # would destroy any existing tree structure.
+ if getattr(doc.meta, 'is_tree', None) and any([doc.lft, doc.rgt]):
+ print('Ignoring values of `lft` and `rgt` for {} "{}"'.format(doc.doctype, doc.name))
+ doc.lft = None
+ doc.rgt = None
diff --git a/frappe/patches/v13_0/increase_password_length.py b/frappe/patches/v13_0/increase_password_length.py
index 62ca2ed779..deb7d7e98a 100644
--- a/frappe/patches/v13_0/increase_password_length.py
+++ b/frappe/patches/v13_0/increase_password_length.py
@@ -1,4 +1,4 @@
import frappe
def execute():
- frappe.db.change_column_type(table="__Auth", column="password", type="TEXT")
+ frappe.db.change_column_type("__Auth", column="password", type="TEXT")
diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js
index 786f8f97ab..adc5e2363c 100644
--- a/frappe/printing/doctype/print_format/print_format.js
+++ b/frappe/printing/doctype/print_format/print_format.js
@@ -36,21 +36,23 @@ frappe.ui.form.on("Print Format", {
else if (frm.doc.custom_format && !frm.doc.raw_printing) {
frm.set_df_property("html", "reqd", 1);
}
- frappe.db.get_value('DocType', frm.doc.doc_type, 'default_print_format', (r) => {
- if (r.default_print_format != frm.doc.name) {
- frm.add_custom_button(__("Set as Default"), function () {
- frappe.call({
- method: "frappe.printing.doctype.print_format.print_format.make_default",
- args: {
- name: frm.doc.name
- },
- callback: function() {
- frm.refresh();
- }
+ if (frappe.perm.has_perm('DocType', 0, 'read', frm.doc.doc_type)) {
+ frappe.db.get_value('DocType', frm.doc.doc_type, 'default_print_format', (r) => {
+ if (r.default_print_format != frm.doc.name) {
+ frm.add_custom_button(__("Set as Default"), function () {
+ frappe.call({
+ method: "frappe.printing.doctype.print_format.print_format.make_default",
+ args: {
+ name: frm.doc.name
+ },
+ callback: function() {
+ frm.refresh();
+ }
+ });
});
- });
- }
- });
+ }
+ });
+ }
}
},
custom_format: function (frm) {
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index ca2a340661..da34dfda96 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -174,7 +174,7 @@ frappe.ui.form.PrintView = class {
});
}
- if (frappe.user.has_role('System Manager')) {
+ if (frappe.perm.has_perm('Print Format', 0, 'create')) {
this.page.add_menu_item(__('Customize'), () =>
this.edit_print_format()
);
diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg
index b2f1428967..b878f713e9 100644
--- a/frappe/public/icons/timeless/symbol-defs.svg
+++ b/frappe/public/icons/timeless/symbol-defs.svg
@@ -35,10 +35,13 @@
-
+
+
+
+
@@ -680,7 +683,7 @@
-
+
diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js
index d6c268a28a..6a14637f33 100644
--- a/frappe/public/js/frappe/form/controls/base_control.js
+++ b/frappe/public/js/frappe/form/controls/base_control.js
@@ -131,7 +131,7 @@ frappe.ui.form.Control = class BaseControl {
if (!this.doc.__islocal) {
new frappe.views.TranslationManager({
'df': this.df,
- 'source_text': value,
+ 'source_text': this.value,
'target_language': this.doc.language,
'doc': this.doc
});
diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js
index 5fbfa28073..60805b75de 100644
--- a/frappe/public/js/frappe/form/controls/code.js
+++ b/frappe/public/js/frappe/form/controls/code.js
@@ -14,11 +14,15 @@ frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlTex
this.ace_editor_target.addClass('border rounded');
this.ace_editor_target.css('height', 300);
+ if (this.df.max_height) {
+ this.ace_editor_target.css('max-height', this.df.max_height);
+ }
+
// initialize
const ace = window.ace;
this.editor = ace.edit(this.ace_editor_target.get(0));
- if (this.df.max_lines || this.df.min_lines) {
+ if (this.df.max_lines || this.df.min_lines || this.df.max_height) {
if (this.df.max_lines)
this.editor.setOption("maxLines", this.df.max_lines);
if (this.df.min_lines)
diff --git a/frappe/public/js/frappe/form/controls/comment.js b/frappe/public/js/frappe/form/controls/comment.js
index 7c10b61366..b9b2d6a987 100644
--- a/frappe/public/js/frappe/form/controls/comment.js
+++ b/frappe/public/js/frappe/form/controls/comment.js
@@ -104,8 +104,10 @@ frappe.ui.form.ControlComment = class ControlComment extends frappe.ui.form.Cont
return [
['bold', 'italic', 'underline'],
['blockquote', 'code-block'],
+ [{ 'direction': "rtl" }],
['link', 'image'],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
+ [{ 'align': [] }],
['clean']
];
}
diff --git a/frappe/public/js/frappe/form/controls/float.js b/frappe/public/js/frappe/form/controls/float.js
index 89f8f23cc5..e00f74238c 100644
--- a/frappe/public/js/frappe/form/controls/float.js
+++ b/frappe/public/js/frappe/form/controls/float.js
@@ -1,4 +1,17 @@
frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlInt {
+
+ make_input() {
+ super.make_input();
+ const change_handler = e => {
+ if (this.change) this.change(e);
+ else {
+ let value = this.get_input_value();
+ this.parse_validate_and_set_in_model(value, e);
+ }
+ };
+ // convert to number format on focusout since focus converts it to flt.
+ this.$input.on("focusout", change_handler);
+ }
parse(value) {
value = this.eval_expression(value);
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision());
diff --git a/frappe/public/js/frappe/form/controls/text.js b/frappe/public/js/frappe/form/controls/text.js
index 3156df6d1c..ccf9a85feb 100644
--- a/frappe/public/js/frappe/form/controls/text.js
+++ b/frappe/public/js/frappe/form/controls/text.js
@@ -8,6 +8,9 @@ frappe.ui.form.ControlText = class ControlText extends frappe.ui.form.ControlDat
make_input() {
super.make_input();
this.$input.css({'height': '300px'});
+ if (this.df.max_height) {
+ this.$input.css({'max-height': this.df.max_height});
+ }
}
};
diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js
index 99e87c5f21..2ce8fd1046 100644
--- a/frappe/public/js/frappe/form/controls/text_editor.js
+++ b/frappe/public/js/frappe/form/controls/text_editor.js
@@ -164,8 +164,11 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for
['bold', 'italic', 'underline', 'clean'],
[{ 'color': [] }, { 'background': [] }],
['blockquote', 'code-block'],
+ // Adding Direction tool to give the user the ability to change text direction.
+ [{ 'direction': "rtl" }],
['link', 'image'],
[{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }],
+ [{ 'align': [] }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{'table': [
'insert-table',
diff --git a/frappe/public/js/frappe/form/footer/base_timeline.js b/frappe/public/js/frappe/form/footer/base_timeline.js
index ab4ad95a81..702d964442 100644
--- a/frappe/public/js/frappe/form/footer/base_timeline.js
+++ b/frappe/public/js/frappe/form/footer/base_timeline.js
@@ -86,7 +86,7 @@ class BaseTimeline {
});
if (item.icon) {
timeline_item.append(`
-
`);
this.inner_toolbar.find('.inner-page-message').remove();
diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js
index 1c39f42ec5..32e3669caf 100644
--- a/frappe/public/js/frappe/utils/number_format.js
+++ b/frappe/public/js/frappe/utils/number_format.js
@@ -8,7 +8,12 @@ if (!window.frappe) window.frappe = {};
function flt(v, decimals, number_format) {
if (v == null || v == '') return 0;
- if (typeof v !== "number") {
+ if (!(typeof v === "number" || String(parseFloat(v)) == v)) {
+ // cases in which this block should not run
+ // 1. 'v' is already a number
+ // 2. v is already parsed but in string form
+ // if (typeof v !== "number") {
+
v = v + "";
// strip currency symbol if exists
@@ -25,6 +30,7 @@ function flt(v, decimals, number_format) {
v = 0;
}
+ v = parseFloat(v);
if (decimals != null)
return _round(v, decimals);
return v;
diff --git a/frappe/public/js/frappe/views/file/file_view.js b/frappe/public/js/frappe/views/file/file_view.js
index e020bff4dd..11204bb660 100644
--- a/frappe/public/js/frappe/views/file/file_view.js
+++ b/frappe/public/js/frappe/views/file/file_view.js
@@ -380,8 +380,10 @@ frappe.views.FileView = class FileView extends frappe.views.ListView {
return `