diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 19a7c68e19..246bdbe096 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -50,7 +50,9 @@ if [ "$TYPE" == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; f if [ "$TYPE" == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi if [ "$TYPE" == "ui" ]; then bench setup requirements --node; fi -if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi +bench setup requirements --dev + +if [ "$TYPE" == "ui" ]; then sed -i 's/^web: bench serve/web: bench serve --with-coverage/g' Procfile; fi # install node-sass which is required for website theme test cd ./apps/frappe || exit @@ -60,4 +62,4 @@ cd ../.. bench start & bench --site test_site reinstall --yes if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi -CI=Yes bench build --app frappe +if [ "$TYPE" == "server" ]; then CI=Yes bench build --app frappe; fi diff --git a/.github/helper/roulette.py b/.github/helper/roulette.py index 9831df7f30..90f4608a22 100644 --- a/.github/helper/roulette.py +++ b/.github/helper/roulette.py @@ -41,6 +41,7 @@ if __name__ == "__main__": # this is a push build, run all builds if not pr_number: os.system('echo "::set-output name=build::strawberry"') + os.system('echo "::set-output name=build-server::strawberry"') sys.exit(0) files_list = files_list or get_files_list(pr_number=pr_number, repo=repo) @@ -52,7 +53,8 @@ if __name__ == "__main__": ci_files_changed = any(f for f in files_list if is_ci(f)) only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list) only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list) - only_py_changed = len(list(filter(is_py, files_list))) == len(files_list) + updated_py_file_count = len(list(filter(is_py, files_list))) + only_py_changed = updated_py_file_count == len(files_list) if ci_files_changed: print("CI related files were updated, running all build processes.") @@ -65,8 +67,12 @@ if __name__ == "__main__": print("Only Frontend code was updated; Stopping Python build process.") sys.exit(0) - elif only_py_changed and build_type == "ui": - print("Only Python code was updated, stopping Cypress build process.") - sys.exit(0) + elif build_type == "ui": + if only_py_changed: + print("Only Python code was updated, stopping Cypress build process.") + sys.exit(0) + elif updated_py_file_count > 0: + # both frontend and backend code were updated + os.system('echo "::set-output name=build-server::strawberry"') os.system('echo "::set-output name=build::strawberry"') diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 3eefd1ce82..fc8093444e 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -141,6 +141,12 @@ jobs: env: CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb + - name: Stop server + if: ${{ steps.check-build.outputs.build-server == 'strawberry' }} + run: | + ps -ef | grep "frappe serve" | awk '{print $2}' | xargs kill -s SIGINT 2> /dev/null || true + sleep 5 + - name: Check If Coverage Report Exists id: check_coverage uses: andstor/file-existence-action@v1 @@ -156,3 +162,13 @@ jobs: directory: /home/runner/frappe-bench/apps/frappe/.cypress-coverage/ verbose: true flags: ui-tests + + - name: Upload Server Coverage Data + if: ${{ steps.check-build.outputs.build-server == 'strawberry' }} + uses: codecov/codecov-action@v2 + with: + name: MariaDB + fail_ci_if_error: true + files: /home/runner/frappe-bench/sites/coverage.xml + verbose: true + flags: server diff --git a/cypress/fixtures/child_table_doctype.js b/cypress/fixtures/child_table_doctype.js new file mode 100644 index 0000000000..f65e5d1765 --- /dev/null +++ b/cypress/fixtures/child_table_doctype.js @@ -0,0 +1,30 @@ +export default { + name: "Child Table Doctype", + actions: [], + custom: 1, + autoname: "field:title", + creation: "2022-02-09 20:15:21.242213", + doctype: "DocType", + editable_grid: 1, + engine: "InnoDB", + fields: [ + { + fieldname: "title", + fieldtype: "Data", + in_list_view: 1, + label: "Title", + unique: 1 + } + ], + links: [], + istable: 1, + modified: "2022-02-10 12:03:12.603763", + modified_by: "Administrator", + module: "Custom", + naming_rule: "By fieldname", + owner: "Administrator", + permissions: [], + sort_field: 'modified', + sort_order: 'ASC', + track_changes: 1 +}; \ No newline at end of file diff --git a/cypress/fixtures/doctype_to_link.js b/cypress/fixtures/doctype_to_link.js new file mode 100644 index 0000000000..f5335b1755 --- /dev/null +++ b/cypress/fixtures/doctype_to_link.js @@ -0,0 +1,45 @@ +export default { + name: "Doctype to Link", + actions: [], + custom: 1, + naming_rule: "By fieldname", + autoname: "field:title", + creation: "2022-02-09 20:15:21.242213", + doctype: "DocType", + editable_grid: 1, + engine: "InnoDB", + fields: [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "unique": 1 + } + ], + links: [ + { + "group": "Child Doctype", + "link_doctype": "Doctype With Child Table", + "link_fieldname": "title" + } + ], + modified: "2022-02-10 12:03:12.603763", + modified_by: "Administrator", + module: "Custom", + owner: "Administrator", + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: 'System Manager', + share: 1, + write: 1 + } + ], + sort_field: 'modified', + sort_order: 'ASC', + track_changes: 1 +}; \ No newline at end of file diff --git a/cypress/fixtures/doctype_with_child_table.js b/cypress/fixtures/doctype_with_child_table.js new file mode 100644 index 0000000000..bbb2127448 --- /dev/null +++ b/cypress/fixtures/doctype_with_child_table.js @@ -0,0 +1,46 @@ +export default { + name: "Doctype With Child Table", + actions: [], + custom: 1, + autoname: "field:title", + creation: "2022-02-09 20:15:21.242213", + doctype: "DocType", + editable_grid: 1, + engine: "InnoDB", + fields: [ + { + fieldname: "title", + fieldtype: "Data", + label: "Title", + unique: 1 + }, + { + fieldname: "child_table", + fieldtype: "Table", + label: "Child Table", + options: "Child Table Doctype", + reqd: 1 + } + ], + links: [], + modified: "2022-02-10 12:03:12.603763", + modified_by: "Administrator", + module: "Custom", + naming_rule: "By fieldname", + owner: "Administrator", + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: 'System Manager', + share: 1, + write: 1 + } + ], + sort_field: 'modified', + sort_order: 'ASC', + track_changes: 1 +}; diff --git a/cypress/integration/dashboard_links.js b/cypress/integration/dashboard_links.js index 16ffd41cf4..93d10cf1fd 100644 --- a/cypress/integration/dashboard_links.js +++ b/cypress/integration/dashboard_links.js @@ -1,7 +1,21 @@ +import doctype_with_child_table from '../fixtures/doctype_with_child_table'; +import child_table_doctype from '../fixtures/child_table_doctype'; +import doctype_to_link from '../fixtures/doctype_to_link'; +const doctype_to_link_name = doctype_to_link.name; +const child_table_doctype_name = child_table_doctype.name; + context('Dashboard links', () => { before(() => { cy.visit('/login'); cy.login(); + cy.insert_doc('DocType', child_table_doctype, true); + cy.insert_doc('DocType', doctype_with_child_table, true); + cy.insert_doc('DocType', doctype_to_link, true); + return cy.window().its('frappe').then(frappe => { + return frappe.xcall("frappe.tests.ui_test_helpers.update_child_table", { + name: child_table_doctype_name + }); + }); }); it('Adding a new contact, checking for the counter on the dashboard and deleting the created contact', () => { @@ -62,4 +76,14 @@ context('Dashboard links', () => { cy.findByText('Website Analytics'); }); }); + + it('check if child table is populated with linked field on creation from dashboard link', () => { + cy.new_form(doctype_to_link_name); + cy.fill_field("title", "Test Linking"); + cy.findByRole("button", {name: "Save"}).click(); + + cy.get('.document-link .btn-new').click(); + cy.get('.frappe-control[data-fieldname="child_table"] .rows .data-row .col[data-fieldname="doctype_to_link"]') + .should('contain.text', 'Test Linking'); + }); }); diff --git a/cypress/integration/depends_on.js b/cypress/integration/depends_on.js index 9aa6b5d89d..12f54f2b6e 100644 --- a/cypress/integration/depends_on.js +++ b/cypress/integration/depends_on.js @@ -55,10 +55,31 @@ context('Depends On', () => { 'read_only_depends_on': "eval:doc.test_field=='Some Other Value'", 'options': "Child Test Depends On" }, + { + "label": "Dependent Tab", + "fieldname": "dependent_tab", + "fieldtype": "Tab Break", + "depends_on": "eval:doc.test_field=='Show Tab'" + }, + { + "fieldname": "tab_section", + "fieldtype": "Section Break", + }, + { + "label": "Field in Tab", + "fieldname": "field_in_tab", + "fieldtype": "Data", + } ] }); }); }); + it('should show the tab on other setting field value', () => { + cy.new_form('Test Depends On'); + cy.fill_field('test_field', 'Show Tab'); + cy.get('body').click(); + cy.findByRole("tab", {name: "Dependent Tab"}).should('be.visible'); + }); it('should set the field as mandatory depending on other fields value', () => { cy.new_form('Test Depends On'); cy.fill_field('test_field', 'Some Value'); diff --git a/cypress/integration/grid.js b/cypress/integration/grid.js new file mode 100644 index 0000000000..4fa52712cf --- /dev/null +++ b/cypress/integration/grid.js @@ -0,0 +1,92 @@ +context('Grid', () => { + beforeEach(() => { + cy.login(); + cy.visit('/app/website'); + }); + before(() => { + cy.login(); + cy.visit('/app/website'); + return cy.window().its('frappe').then(frappe => { + return frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records"); + }); + }); + it('update docfield property using update_docfield_property', () => { + cy.visit('/app/contact/Test Contact'); + cy.window().its("cur_frm").then(frm => { + cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); + let field = frm.get_field("phone_nos"); + field.grid.update_docfield_property("is_primary_phone", "hidden", true); + + cy.get('@table').find('[data-idx="1"] .edit-grid-row').click(); + cy.get('.grid-row-open').as('table-form'); + cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_phone"]').should("be.hidden"); + cy.get('@table-form').find('.grid-footer-toolbar').click(); + + cy.get('@table').find('[data-idx="2"] .edit-grid-row').click(); + cy.get('.grid-row-open').as('table-form'); + cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_phone"]').should("be.hidden"); + cy.get('@table-form').find('.grid-footer-toolbar').click(); + }); + }); + it('update docfield property using toggle_display', () => { + cy.visit('/app/contact/Test Contact'); + cy.window().its("cur_frm").then(frm => { + cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); + let field = frm.get_field("phone_nos"); + field.grid.toggle_display("is_primary_mobile_no", false); + + cy.get('@table').find('[data-idx="1"] .edit-grid-row').click(); + cy.get('.grid-row-open').as('table-form'); + cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_mobile_no"]').should("be.hidden"); + cy.get('@table-form').find('.grid-footer-toolbar').click(); + + cy.get('@table').find('[data-idx="2"] .edit-grid-row').click(); + cy.get('.grid-row-open').as('table-form'); + cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_mobile_no"]').should("be.hidden"); + cy.get('@table-form').find('.grid-footer-toolbar').click(); + }); + }); + it('update docfield property using toggle_enable', () => { + cy.visit('/app/contact/Test Contact'); + cy.window().its("cur_frm").then(frm => { + cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); + let field = frm.get_field("phone_nos"); + field.grid.toggle_enable("phone", false); + + + cy.get('@table').find('[data-idx="1"] .edit-grid-row').click(); + cy.get('.grid-row-open').as('table-form'); + cy.get('@table-form').find('.frappe-control[data-fieldname="phone"] .control-value').should('have.class', 'like-disabled-input'); + cy.get('@table-form').find('.grid-footer-toolbar').click(); + + cy.get('@table').find('[data-idx="2"] .edit-grid-row').click(); + cy.get('.grid-row-open').as('table-form'); + cy.get('@table-form').find('.frappe-control[data-fieldname="phone"] .control-value').should('have.class', 'like-disabled-input'); + cy.get('@table-form').find('.grid-footer-toolbar').click(); + }); + }); + it('update docfield property using toggle_reqd', () => { + cy.visit('/app/contact/Test Contact'); + cy.window().its("cur_frm").then(frm => { + cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); + let field = frm.get_field("phone_nos"); + field.grid.toggle_reqd("phone", false); + + cy.get('@table').find('[data-idx="1"] .edit-grid-row').click(); + cy.get('.grid-row-open').as('table-form'); + cy.get_field("phone").as('phone-field'); + cy.get('@phone-field').focus().clear().wait(500).blur(); + cy.get('@phone-field').should("not.have.class", "has-error"); + cy.get('@table-form').find('.grid-footer-toolbar').click(); + + cy.get('@table').find('[data-idx="2"] .edit-grid-row').click(); + cy.get('.grid-row-open').as('table-form'); + cy.get_field("phone").as('phone-field'); + cy.get('@phone-field').focus().clear().wait(500).blur(); + cy.get('@phone-field').should("not.have.class", "has-error"); + cy.get('@table-form').find('.grid-footer-toolbar').click(); + + }); + }); +}); + diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js index 5a0b13a3a0..4bc5784a53 100644 --- a/cypress/integration/report_view.js +++ b/cypress/integration/report_view.js @@ -46,7 +46,6 @@ context('Report View', () => { it('test load more with count selection buttons', () => { cy.visit('/app/contact/view/report'); - cy.clear_filters(); cy.get('.list-paging-area .list-count').should('contain.text', '20 of'); cy.get('.list-paging-area .btn-more').click(); cy.get('.list-paging-area .list-count').should('contain.text', '40 of'); @@ -61,6 +60,10 @@ context('Report View', () => { cy.get('.list-paging-area .btn-more').click(); cy.get('.list-paging-area .list-count').should('contain.text', '300 of'); + // check if refresh works after load more + cy.get('.page-head .standard-actions [data-original-title="Refresh"]').click(); + cy.get('.list-paging-area .list-count').should('contain.text', '300 of'); + cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click(); cy.get('.list-paging-area .list-count').should('contain.text', '500 of'); diff --git a/frappe/build.py b/frappe/build.py index 6b93b8b93a..7a06ee3a22 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -1,25 +1,21 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import os -import re -import json import shutil +import re import subprocess +from distutils.spawn import find_executable from subprocess import getoutput -from io import StringIO from tempfile import mkdtemp, mktemp -from distutils.spawn import find_executable - -import frappe -from frappe.utils.minify import JavascriptMinify +from urllib.parse import urlparse import click import psutil -from urllib.parse import urlparse -from semantic_version import Version from requests import head from requests.exceptions import HTTPError +from semantic_version import Version +import frappe timestamps = {} app_paths = None @@ -32,6 +28,7 @@ class AssetsNotDownloadedError(Exception): class AssetsDontExistError(HTTPError): pass + def download_file(url, prefix): from requests import get @@ -277,12 +274,14 @@ def check_node_executable(): click.echo(f"{warn} Please install yarn using below command and try again.\nnpm install -g yarn") click.echo() + def get_node_env(): node_env = { "NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}" } return node_env + def get_safe_max_old_space_size(): safe_max_old_space_size = 0 try: @@ -296,6 +295,7 @@ def get_safe_max_old_space_size(): return safe_max_old_space_size + def generate_assets_map(): symlinks = {} @@ -344,7 +344,6 @@ def clear_broken_symlinks(): os.remove(path) - def unstrip(message: str) -> str: """Pads input string on the right side until the last available column in the terminal """ @@ -397,94 +396,6 @@ def link_assets_dir(source, target, hard_link=False): symlink(source, target, overwrite=True) -def build(no_compress=False, verbose=False): - for target, sources in get_build_maps().items(): - pack(os.path.join(assets_path, target), sources, no_compress, verbose) - - -def get_build_maps(): - """get all build.jsons with absolute paths""" - # framework js and css files - - build_maps = {} - for app_path in app_paths: - path = os.path.join(app_path, "public", "build.json") - if os.path.exists(path): - with open(path) as f: - try: - for target, sources in (json.loads(f.read() or "{}")).items(): - # update app path - source_paths = [] - for source in sources: - if isinstance(source, list): - s = frappe.get_pymodule_path(source[0], *source[1].split("/")) - else: - s = os.path.join(app_path, source) - source_paths.append(s) - - build_maps[target] = source_paths - except ValueError as e: - print(path) - print("JSON syntax error {0}".format(str(e))) - return build_maps - - -def pack(target, sources, no_compress, verbose): - outtype, outtxt = target.split(".")[-1], "" - jsm = JavascriptMinify() - - for f in sources: - suffix = None - if ":" in f: - f, suffix = f.split(":") - if not os.path.exists(f) or os.path.isdir(f): - print("did not find " + f) - continue - timestamps[f] = os.path.getmtime(f) - try: - with open(f, "r") as sourcefile: - data = str(sourcefile.read(), "utf-8", errors="ignore") - - extn = f.rsplit(".", 1)[1] - - if ( - outtype == "js" - and extn == "js" - and (not no_compress) - and suffix != "concat" - and (".min." not in f) - ): - tmpin, tmpout = StringIO(data.encode("utf-8")), StringIO() - jsm.minify(tmpin, tmpout) - minified = tmpout.getvalue() - if minified: - outtxt += str(minified or "", "utf-8").strip("\n") + ";" - - if verbose: - print("{0}: {1}k".format(f, int(len(minified) / 1024))) - elif outtype == "js" and extn == "html": - # add to frappe.templates - outtxt += html_to_js_template(f, data) - else: - outtxt += "\n/*\n *\t%s\n */" % f - outtxt += "\n" + data + "\n" - - except Exception: - print("--Error in:" + f + "--") - print(frappe.get_traceback()) - - with open(target, "w") as f: - f.write(outtxt.encode("utf-8")) - - print("Wrote %s - %sk" % (target, str(int(os.path.getsize(target) / 1024)))) - - -def html_to_js_template(path, content): - """returns HTML template content as Javascript code, adding it to `frappe.templates`""" - return """frappe.templates["{key}"] = '{content}';\n""".format( - key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content)) - - def scrub_html_template(content): """Returns HTML content with removed whitespace and comments""" # remove whitespace to a single space @@ -496,37 +407,7 @@ def scrub_html_template(content): return content.replace("'", "\'") -def files_dirty(): - for target, sources in get_build_maps().items(): - for f in sources: - if ":" in f: - f, suffix = f.split(":") - if not os.path.exists(f) or os.path.isdir(f): - continue - if os.path.getmtime(f) != timestamps.get(f): - print(f + " dirty") - return True - else: - return False - - -def compile_less(): - if not find_executable("lessc"): - return - - for path in app_paths: - less_path = os.path.join(path, "public", "less") - if os.path.exists(less_path): - for fname in os.listdir(less_path): - if fname.endswith(".less") and fname != "variables.less": - fpath = os.path.join(less_path, fname) - mtime = os.path.getmtime(fpath) - if fpath in timestamps and mtime == timestamps[fpath]: - continue - - timestamps[fpath] = mtime - - print("compiling {0}".format(fpath)) - - css_path = os.path.join(path, "public", "css", fname.rsplit(".", 1)[0] + ".css") - os.system("lessc {0} > {1}".format(fpath, css_path)) +def html_to_js_template(path, content): + """returns HTML template content as Javascript code, adding it to `frappe.templates`""" + return """frappe.templates["{key}"] = '{content}';\n""".format( + key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content)) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 62488525b0..1684f26d49 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -447,21 +447,17 @@ def disable_user(context, email): @pass_context def migrate(context, skip_failing=False, skip_search_index=False): "Run patches, sync schema and rebuild files/translations" - from frappe.migrate import migrate + from frappe.migrate import SiteMigration for site in context.sites: click.secho(f"Migrating {site}", fg="green") - frappe.init(site=site) - frappe.connect() try: - migrate( - context.verbose, + SiteMigration( skip_failing=skip_failing, - skip_search_index=skip_search_index - ) + skip_search_index=skip_search_index, + ).run(site=site) finally: print() - frappe.destroy() if not context.sites: raise SiteNotSpecifiedError diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index e3379a43aa..7246df8aa7 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -742,8 +742,9 @@ def run_ui_tests(context, app, headless=False, parallel=True, with_coverage=Fals @click.option('--profile', is_flag=True, default=False) @click.option('--noreload', "no_reload", is_flag=True, default=False) @click.option('--nothreading', "no_threading", is_flag=True, default=False) +@click.option('--with-coverage', is_flag=True, default=False) @pass_context -def serve(context, port=None, profile=False, no_reload=False, no_threading=False, sites_path='.', site=None): +def serve(context, port=None, profile=False, no_reload=False, no_threading=False, sites_path='.', site=None, with_coverage=False): "Start development web server" import frappe.app @@ -751,8 +752,12 @@ def serve(context, port=None, profile=False, no_reload=False, no_threading=False site = None else: site = context.sites[0] - - frappe.app.serve(port=port, profile=profile, no_reload=no_reload, no_threading=no_threading, site=site, sites_path='.') + with CodeCoverage(with_coverage, 'frappe'): + if with_coverage: + # unable to track coverage with threading enabled + no_threading = True + no_reload = True + frappe.app.serve(port=port, profile=profile, no_reload=no_reload, no_threading=no_threading, site=site, sites_path='.') @click.command('request') diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 2808a2710b..50a7b31bca 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -745,7 +745,7 @@ def delete_file(path): """Delete file from `public folder`""" if path: if ".." in path.split("/"): - frappe.msgprint(_("It is risky to delete this file: {0}. Please contact your System Manager.").format(path)) + frappe.throw(_("It is risky to delete this file: {0}. Please contact your System Manager.").format(path)) parts = os.path.split(path.strip("/")) if parts[0]=="files": diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index 5a22304f32..bf63afa5c5 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -3,7 +3,7 @@ import frappe, json, os import unittest -from frappe.desk.query_report import run, save_report +from frappe.desk.query_report import run, save_report, add_total_row from frappe.desk.reportview import delete_report, save_report as _save_report from frappe.custom.doctype.customize_form.customize_form import reset_customization from frappe.core.doctype.user_permission.test_user_permission import create_user @@ -282,3 +282,55 @@ result = [ # Set user back to administrator frappe.set_user('Administrator') + + def test_add_total_row_for_tree_reports(self): + report_settings = { + 'tree': True, + 'parent_field': 'parent_value' + } + + columns = [ + { + "fieldname": "parent_column", + "label": "Parent Column", + "fieldtype": "Data", + "width": 10 + }, + { + "fieldname": "column_1", + "label": "Column 1", + "fieldtype": "Float", + "width": 10 + }, + { + "fieldname": "column_2", + "label": "Column 2", + "fieldtype": "Float", + "width": 10 + } + ] + + result = [ + { + "parent_column": "Parent 1", + "column_1": 200, + "column_2": 150.50 + }, + { + "parent_column": "Child 1", + "column_1": 100, + "column_2": 75.25, + "parent_value": "Parent 1" + }, + { + "parent_column": "Child 2", + "column_1": 100, + "column_2": 75.25, + "parent_value": "Parent 1" + } + ] + + result = add_total_row(result, columns, meta=None, report_settings=report_settings) + self.assertEqual(result[-1][0], "Total") + self.assertEqual(result[-1][1], 200) + self.assertEqual(result[-1][2], 150.50) diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index 6b427fdebf..cb218b2eae 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -347,6 +347,7 @@ frappe.PermissionEngine = class PermissionEngine { } add_check_events() { + let me = this; this.body.on("click", ".show-user-permissions", () => { frappe.route_options = { allow: this.get_doctype() || "" }; frappe.set_route('List', 'User Permission'); @@ -373,7 +374,7 @@ frappe.PermissionEngine = class PermissionEngine { // exception: reverse chk.prop("checked", !chk.prop("checked")); } else { - this.get_perm(args.role)[args.ptype] = args.value; + me.get_perm(args.role)[args.ptype] = args.value; } } }); diff --git a/frappe/coverage.py b/frappe/coverage.py index 1969cae141..5f89800deb 100644 --- a/frappe/coverage.py +++ b/frappe/coverage.py @@ -29,6 +29,7 @@ FRAPPE_EXCLUSIONS = [ "*/commands/*", "*/frappe/change_log/*", "*/frappe/exceptions*", + "*/frappe/coverage.py", "*frappe/setup.py", "*/doctype/*/*_dashboard.py", "*/patches/*", diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 97bceeb725..9ed956e986 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -73,7 +73,7 @@ def get_report_result(report, filters): return res @frappe.read_only() -def generate_report_result(report, filters=None, user=None, custom_columns=None): +def generate_report_result(report, filters=None, user=None, custom_columns=None, report_settings=None): user = user or frappe.session.user filters = filters or [] @@ -108,7 +108,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) result = get_filtered_data(report.ref_doctype, columns, result, user) if cint(report.add_total_row) and result and not skip_total_row: - result = add_total_row(result, columns) + result = add_total_row(result, columns, report_settings=report_settings) return { "result": result, @@ -210,7 +210,7 @@ def get_script(report_name): @frappe.whitelist() @frappe.read_only() -def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None): +def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, report_settings=None): report = get_report_doc(report_name) if not user: user = frappe.session.user @@ -238,7 +238,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False, cust dn = "" result = get_prepared_report_result(report, filters, dn, user) else: - result = generate_report_result(report, filters, user, custom_columns) + result = generate_report_result(report, filters, user, custom_columns, report_settings) result["add_total_row"] = report.add_total_row and not result.get( "skip_total_row", False @@ -435,9 +435,19 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi return result, column_widths -def add_total_row(result, columns, meta=None): +def add_total_row(result, columns, meta=None, report_settings=None): total_row = [""] * len(columns) has_percent = [] + is_tree = False + parent_field = '' + + if report_settings: + if isinstance(report_settings, (str,)): + report_settings = json.loads(report_settings) + + is_tree = report_settings.get('tree') + parent_field = report_settings.get('parent_field') + for i, col in enumerate(columns): fieldtype, options, fieldname = None, None, None if isinstance(col, str): @@ -464,12 +474,12 @@ def add_total_row(result, columns, meta=None): for row in result: if i >= len(row): continue - cell = row.get(fieldname) if isinstance(row, dict) else row[i] if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt( cell ): - total_row[i] = flt(total_row[i]) + flt(cell) + if not (is_tree and row.get(parent_field)): + total_row[i] = flt(total_row[i]) + flt(cell) if fieldtype == "Percent" and i not in has_percent: has_percent.append(i) diff --git a/frappe/installer.py b/frappe/installer.py index d892ff4ddc..0948620ab0 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -529,10 +529,9 @@ def extract_sql_gzip(sql_gz_path): import subprocess try: - # dvf - decompress, verbose, force original_file = sql_gz_path decompressed_file = original_file.rstrip(".gz") - cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file) + cmd = 'gzip --decompress --force < {0} > {1}'.format(original_file, decompressed_file) subprocess.check_call(cmd, shell=True) except Exception: raise diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index 7c9c64ba3c..3d29feebac 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -45,8 +45,8 @@ class LDAPSettings(Document): title=_("Misconfigured")) if self.ldap_directory_server.lower() == 'custom': - if not self.ldap_group_member_attribute or not self.ldap_group_mappings_section: - frappe.throw(_("Custom LDAP Directoy Selected, please ensure 'LDAP Group Member attribute' and 'LDAP Group Mappings' are entered"), + if not self.ldap_group_member_attribute or not self.ldap_group_objectclass: + frappe.throw(_("Custom LDAP Directoy Selected, please ensure 'LDAP Group Member attribute' and 'Group Object Class' are entered"), title=_("Misconfigured")) else: diff --git a/frappe/migrate.py b/frappe/migrate.py index d13fe858f7..eabd0ff3e0 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -1,30 +1,54 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import json import os -import sys +from textwrap import dedent + import frappe -import frappe.translate -import frappe.modules.patch_handler import frappe.model.sync -from frappe.utils.fixtures import sync_fixtures -from frappe.utils.connections import check_connection -from frappe.utils.dashboard import sync_dashboards +import frappe.modules.patch_handler +import frappe.translate from frappe.cache_manager import clear_global_cache -from frappe.desk.notifications import clear_notifications -from frappe.website.utils import clear_website_cache from frappe.core.doctype.language.language import sync_languages -from frappe.modules.utils import sync_customizations from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs -from frappe.search.website_search import build_index_for_all_routes from frappe.database.schema import add_column +from frappe.desk.notifications import clear_notifications from frappe.modules.patch_handler import PatchType +from frappe.modules.utils import sync_customizations +from frappe.search.website_search import build_index_for_all_routes +from frappe.utils.connections import check_connection +from frappe.utils.dashboard import sync_dashboards +from frappe.utils.fixtures import sync_fixtures +from frappe.website.utils import clear_website_cache + +BENCH_START_MESSAGE = dedent( + """ + Cannot run bench migrate without the services running. + If you are running bench in development mode, make sure that bench is running: + + $ bench start + Otherwise, check the server logs and ensure that all the required services are running. + """ +) -def migrate(verbose=True, skip_failing=False, skip_search_index=False): - '''Migrate all apps to the current version, will: +def atomic(method): + def wrapper(*args, **kwargs): + try: + ret = method(*args, **kwargs) + frappe.db.commit() + return ret + except Exception: + frappe.db.rollback() + raise + + return wrapper + + +class SiteMigration: + """Migrate all apps to the current version, will: - run before migrate hooks - run patches - sync doctypes (schema) @@ -35,70 +59,117 @@ def migrate(verbose=True, skip_failing=False, skip_search_index=False): - sync languages - sync web pages (from /www) - run after migrate hooks - ''' - - service_status = check_connection(redis_services=["redis_cache"]) - if False in service_status.values(): - for service in service_status: - if not service_status.get(service, True): - print("{} service is not running.".format(service)) - print("""Cannot run bench migrate without the services running. -If you are running bench in development mode, make sure that bench is running: + """ -$ bench start + def __init__(self, skip_failing: bool = False, skip_search_index: bool = False) -> None: + self.skip_failing = skip_failing + self.skip_search_index = skip_search_index -Otherwise, check the server logs and ensure that all the required services are running.""") - sys.exit(1) + def setUp(self): + """Complete setup required for site migration + """ + frappe.flags.touched_tables = set() + self.touched_tables_file = frappe.get_site_path("touched_tables.json") + add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data") + clear_global_cache() - touched_tables_file = frappe.get_site_path('touched_tables.json') - if os.path.exists(touched_tables_file): - os.remove(touched_tables_file) + if os.path.exists(self.touched_tables_file): + os.remove(self.touched_tables_file) - try: - add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data") - frappe.flags.touched_tables = set() frappe.flags.in_migrate = True - clear_global_cache() + def tearDown(self): + """Run operations that should be run post schema updation processes + This should be executed irrespective of outcome + """ + frappe.translate.clear_cache() + clear_website_cache() + clear_notifications() + + with open(self.touched_tables_file, "w") as f: + json.dump(list(frappe.flags.touched_tables), f, sort_keys=True, indent=4) + + if not self.skip_search_index: + print(f"Building search index for {frappe.local.site}") + build_index_for_all_routes() + + frappe.publish_realtime("version-update") + frappe.flags.touched_tables.clear() + frappe.flags.in_migrate = False + @atomic + def pre_schema_updates(self): + """Executes `before_migrate` hooks + """ for app in frappe.get_installed_apps(): - for fn in frappe.get_hooks('before_migrate', app_name=app): + for fn in frappe.get_hooks("before_migrate", app_name=app): frappe.get_attr(fn)() - frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.pre_model_sync) + @atomic + def run_schema_updates(self): + """Run patches as defined in patches.txt, sync schema changes as defined in the {doctype}.json files + """ + frappe.modules.patch_handler.run_all(skip_failing=self.skip_failing, patch_type=PatchType.pre_model_sync) frappe.model.sync.sync_all() - frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.post_model_sync) - frappe.translate.clear_cache() + frappe.modules.patch_handler.run_all(skip_failing=self.skip_failing, patch_type=PatchType.post_model_sync) + + @atomic + def post_schema_updates(self): + """Execute pending migration tasks post patches execution & schema sync + This includes: + * Sync `Scheduled Job Type` and scheduler events defined in hooks + * Sync fixtures & custom scripts + * Sync in-Desk Module Dashboards + * Sync customizations: Custom Fields, Property Setters, Custom Permissions + * Sync Frappe's internal language master + * Sync Portal Menu Items + * Sync Installed Applications Version History + * Execute `after_migrate` hooks + """ sync_jobs() sync_fixtures() sync_dashboards() sync_customizations() sync_languages() - frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu() - - # syncs static files - clear_website_cache() - - # updating installed applications data - frappe.get_single('Installed Applications').update_versions() + frappe.get_single("Portal Settings").sync_menu() + frappe.get_single("Installed Applications").update_versions() for app in frappe.get_installed_apps(): - for fn in frappe.get_hooks('after_migrate', app_name=app): + for fn in frappe.get_hooks("after_migrate", app_name=app): frappe.get_attr(fn)() - if not skip_search_index: - # Run this last as it updates the current session - print('Building search index for {}'.format(frappe.local.site)) - build_index_for_all_routes() - - frappe.db.commit() - - clear_notifications() - - frappe.publish_realtime("version-update") - frappe.flags.in_migrate = False - finally: - with open(touched_tables_file, 'w') as f: - json.dump(list(frappe.flags.touched_tables), f, sort_keys=True, indent=4) - frappe.flags.touched_tables.clear() + def required_services_running(self) -> bool: + """Returns True if all required services are running. Returns False and prints + instructions to stdout when required services are not available. + """ + service_status = check_connection(redis_services=["redis_cache"]) + are_services_running = all(service_status.values()) + + if not are_services_running: + for service in service_status: + if not service_status.get(service, True): + print(f"Service {service} is not running.") + print(BENCH_START_MESSAGE) + + return are_services_running + + def run(self, site: str): + """Run Migrate operation on site specified. This method initializes + and destroys connections to the site database. + """ + if not self.required_services_running(): + raise SystemExit(1) + + if site: + frappe.init(site=site) + frappe.connect() + + self.setUp() + try: + self.pre_schema_updates() + self.run_schema_updates() + finally: + self.post_schema_updates() + self.tearDown() + frappe.destroy() diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index f4a4916a9f..8a81aa5610 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -33,13 +33,12 @@ def get_controller(doctype): module_name, custom = frappe.db.get_value( "DocType", doctype, ("module", "custom"), cache=True - ) or ["Core", False] + ) or ("Core", False) if custom: - if frappe.db.field_exists("DocType", "is_tree"): - is_tree = frappe.db.get_value("DocType", doctype, "is_tree", cache=True) - else: - is_tree = False + is_tree = frappe.db.get_value( + "DocType", doctype, "is_tree", ignore=True, cache=True + ) _class = NestedSet if is_tree else Document else: class_overrides = frappe.get_hooks('override_doctype_class') @@ -106,13 +105,9 @@ class BaseDocument(object): }) """ - # QUESTION: why do we need the 1st for loop? - # we're essentially setting the values in d, in the 2nd for loop (?) - - # first set default field values of base document - for key in default_fields: - if key in d: - self.set(key, d[key]) + # set name first, as it is used a reference in child document + if "name" in d: + self.name = d["name"] for key, value in d.items(): self.set(key, value) @@ -196,6 +191,7 @@ class BaseDocument(object): if isinstance(value, (dict, BaseDocument)): if not self.__dict__.get(key): self.__dict__[key] = [] + value = self._init_child(value, key) self.__dict__[key].append(value) @@ -232,11 +228,11 @@ class BaseDocument(object): def _init_child(self, value, key): if not self.doctype: return value + if not isinstance(value, BaseDocument): - if "doctype" not in value or value['doctype'] is None: - value["doctype"] = self.get_table_field_doctype(key) - if not value["doctype"]: - raise AttributeError(key) + value["doctype"] = self.get_table_field_doctype(key) + if not value["doctype"]: + raise AttributeError(key) value = get_controller(value["doctype"])(value) value.init_valid_columns() diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 77d4de466f..a3d167fb9b 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -14,16 +14,28 @@ Example: ''' +import json +import os 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, child_table_fields -from frappe.model.document import Document + +import frappe +from frappe import _ +from frappe.model import ( + child_table_fields, + data_fieldtypes, + default_fields, + no_value_fields, + optional_fields, + table_fields, +) from frappe.model.base_document import BaseDocument -from frappe.modules import load_doctype_module +from frappe.model.document import Document from frappe.model.workflow import get_workflow_name -from frappe import _ +from frappe.modules import load_doctype_module +from frappe.utils import cast, cint, cstr + def get_meta(doctype, cached=True): if cached: @@ -553,7 +565,7 @@ class Meta(Document): # For internal links parent doctype will be the key doctype = link.parent_doctype or link.link_doctype # group found - if link.group and group.label == link.group: + if link.group and _(group.label) == _(link.group): if doctype not in group.get('items'): group.get('items').append(doctype) link.added = True diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index c0c7ce8b4e..a11ddce520 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -334,7 +334,7 @@ frappe.ui.form.Form = class FrappeForm { this.doc = frappe.get_doc(this.doctype, this.docname); // check permissions - if(!this.has_read_permission()) { + if (!this.has_read_permission()) { frappe.show_not_permitted(__(this.doctype) + " " + __(this.docname)); return; } @@ -1363,6 +1363,7 @@ frappe.ui.form.Form = class FrappeForm { set_df_property(fieldname, property, value, docname, table_field, table_row_name=null) { let df; + if (!docname || !table_field) { df = this.get_docfield(fieldname); } else { @@ -1372,8 +1373,10 @@ frappe.ui.form.Form = class FrappeForm { df = frappe.meta.get_docfield(filtered_fields[0].parent, table_field, table_row_name); } } + if (df && df[property] != value) { df[property] = value; + if (table_field && table_row_name) { if (this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name]) { this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name].refresh_field(fieldname); @@ -1508,7 +1511,9 @@ frappe.ui.form.Form = class FrappeForm { // update child doc opts.child = locals[opts.child.doctype][opts.child.name]; - var std_field_list = ["doctype"].concat(frappe.model.std_fields_list); + var std_field_list = ["doctype"] + .concat(frappe.model.std_fields_list) + .concat(frappe.model.child_table_field_list); for (var key in r.message) { if (std_field_list.indexOf(key)===-1) { opts.child[key] = r.message[key]; @@ -1659,23 +1664,17 @@ frappe.ui.form.Form = class FrappeForm { // make new doctype from the current form // will handover to `make_methods` if defined // or will create and match link fields - var me = this; + let me = this; if(this.make_methods && this.make_methods[doctype]) { return this.make_methods[doctype](this); } else if(this.custom_make_buttons && this.custom_make_buttons[doctype]) { this.custom_buttons[__(this.custom_make_buttons[doctype])].trigger('click'); } else { frappe.model.with_doctype(doctype, function() { - var new_doc = frappe.model.get_new_doc(doctype); + let new_doc = frappe.model.get_new_doc(doctype, null, null, true); // set link fields (if found) - frappe.get_meta(doctype).fields.forEach(function(df) { - if(df.fieldtype==='Link' && df.options===me.doctype) { - new_doc[df.fieldname] = me.doc.name; - } else if (['Link', 'Dynamic Link'].includes(df.fieldtype) && me.doc[df.fieldname]) { - new_doc[df.fieldname] = me.doc[df.fieldname]; - } - }); + me.set_link_field(doctype, new_doc); frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); // frappe.set_route('Form', doctype, new_doc.name); @@ -1683,6 +1682,20 @@ frappe.ui.form.Form = class FrappeForm { } } + set_link_field(doctype, new_doc) { + let me = this; + frappe.get_meta(doctype).fields.forEach(function(df) { + if (df.fieldtype === 'Link' && df.options === me.doctype) { + new_doc[df.fieldname] = me.doc.name; + } else if (['Link', 'Dynamic Link'].includes(df.fieldtype) && me.doc[df.fieldname]) { + new_doc[df.fieldname] = me.doc[df.fieldname]; + } else if (df.fieldtype === 'Table' && df.options && df.reqd) { + let row = new_doc[df.fieldname][0]; + me.set_link_field(df.options, row); + } + }); + } + update_in_all_rows(table_fieldname, fieldname, value) { // update the child value in all tables where it is missing if(!value) return; diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 8b615f3c59..ea90387922 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -502,10 +502,9 @@ export default class Grid { set_column_disp(fieldname, show) { if ($.isArray(fieldname)) { - for (var i = 0, l = fieldname.length; i < l; i++) { - var fname = fieldname[i]; - this.get_docfield(fname).hidden = show ? 0 : 1; - this.set_editable_grid_column_disp(fname, show); + for (let field of fieldname) { + this.update_docfield_property(field, "hidden", show); + this.set_editable_grid_column_disp(field, show); } } else { this.get_docfield(fieldname).hidden = show ? 0 : 1; @@ -555,17 +554,17 @@ export default class Grid { } toggle_reqd(fieldname, reqd) { - this.get_docfield(fieldname).reqd = reqd; + this.update_docfield_property(fieldname, "reqd", reqd); this.debounced_refresh(); } toggle_enable(fieldname, enable) { - this.get_docfield(fieldname).read_only = enable ? 0 : 1; + this.update_docfield_property(fieldname, "read_only", enable ? 0 : 1); this.debounced_refresh(); } toggle_display(fieldname, show) { - this.get_docfield(fieldname).hidden = show ? 0 : 1; + this.update_docfield_property(fieldname, "hidden", show ? 0 : 1); this.debounced_refresh(); } @@ -747,7 +746,7 @@ export default class Grid { var df = this.visible_columns[i][0]; var colsize = this.visible_columns[i][1]; if (colsize > 1 && colsize < 11 - && !in_list(frappe.model.std_fields_list, df.fieldname)) { + && frappe.model.is_non_std_field(df.fieldname)) { if (passes < 3 && ["Int", "Currency", "Float", "Check", "Percent"].indexOf(df.fieldtype) !== -1) { // don't increase col size of these fields in first 3 passes diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index a40f428969..0ee5a180e0 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -5,11 +5,7 @@ export default class GridRow { this.on_grid_fields_dict = {}; this.on_grid_fields = []; $.extend(this, opts); - if (this.doc && this.parent_df.options) { - frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields); - const docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name); - this.docfields = docfields.length ? docfields : opts.docfields; - } + this.set_docfields(); this.columns = {}; this.columns_list = []; this.row_check_html = ''; @@ -41,6 +37,22 @@ export default class GridRow { this.set_data(); } } + + set_docfields(update=false) { + if (this.doc && this.parent_df.options) { + frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields); + const docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name); + if (update) { + // to maintain references + this.docfields.forEach(df => { + Object.assign(df, docfields.find(d => d.fieldname === df.fieldname)); + }); + } else { + this.docfields = docfields; + } + } + } + set_data() { this.wrapper.data({ "doc": this.doc @@ -148,6 +160,11 @@ export default class GridRow { }, __('Move To'), 'Update'); } refresh() { + // update docfields for new record + if (this.frm && this.doc && this.doc.__islocal) { + this.set_docfields(true); + } + if(this.frm && this.doc) { this.doc = locals[this.doc.doctype][this.doc.name]; } diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 0de6b1db0d..8c0f8953c5 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -547,24 +547,28 @@ frappe.ui.form.Layout = class Layout { } refresh_dependency() { - // Resolve "depends_on" and show / hide accordingly + /** + Resolve "depends_on" and show / hide accordingly + build dependants' dictionary + */ - // build dependants' dictionary let has_dep = false; - for (let fkey in this.fields_list) { - let f = this.fields_list[fkey]; - f.dependencies_clear = true; + const fields = this.fields_list.concat(this.tabs); + + for (let fkey in fields) { + let f = fields[fkey]; if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) { has_dep = true; + break; } } if (!has_dep) return; // show / hide based on values - for (let i = this.fields_list.length - 1; i >= 0; i--) { - let f = this.fields_list[i]; + for (let i = fields.length - 1; i >= 0; i--) { + let f = fields[i]; f.guardian_has_value = true; if (f.df.depends_on) { // evaluate guardian diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index bc5f7a9b52..61922a2422 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -1,6 +1,6 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { constructor(opts) { - /* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */ + /* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label, columns */ Object.assign(this, opts); this.for_select = this.doctype == "[Select]"; if (!this.for_select) { @@ -400,23 +400,22 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { return this.results.filter(res => checked_values.includes(res.name)); } + get_datatable_columns() { + if (this.get_query && this.get_query().query && this.columns) return this.columns; + + if (Array.isArray(this.setters)) + return ["name", ...this.setters.map(df => df.fieldname)]; + + return ["name", ...Object.keys(this.setters)]; + } + make_list_row(result = {}) { var me = this; // Make a head row by default (if result not passed) let head = Object.keys(result).length === 0; let contents = ``; - let columns = ["name"]; - - if ($.isArray(this.setters)) { - for (let df of this.setters) { - columns.push(df.fieldname); - } - } else { - columns = columns.concat(Object.keys(this.setters)); - } - - columns.forEach(function (column) { + this.get_datatable_columns().forEach(function (column) { contents += `