diff --git a/.eslintrc b/.eslintrc index dd9e350b1b..c5e7d6831a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,65 +2,32 @@ "env": { "browser": true, "node": true, - "es6": true + "es2022": true }, "parserOptions": { - "ecmaVersion": 11, "sourceType": "module" }, "extends": "eslint:recommended", "rules": { - "indent": [ - "error", - "tab", - { "SwitchCase": 1 } - ], - "brace-style": [ - "error", - "1tbs" - ], - "space-unary-ops": [ - "error", - { "words": true } - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "off" - ], - "semi": [ - "warn", - "always" - ], - "camelcase": [ - "off" - ], - "no-unused-vars": [ - "warn" - ], - "no-redeclare": [ - "warn" - ], - "no-console": [ - "warn" - ], - "no-extra-boolean-cast": [ - "off" - ], - "no-control-regex": [ - "off" - ], - "space-before-blocks": "warn", - "keyword-spacing": "warn", - "comma-spacing": "warn", - "key-spacing": "warn", + "indent": "off", + "brace-style": "off", + "no-mixed-spaces-and-tabs": "off", + "no-useless-escape": "off", + "space-unary-ops": ["error", { "words": true }], + "linebreak-style": "off", + "quotes": ["off"], + "semi": "off", + "camelcase": "off", + "no-unused-vars": "off", + "no-console": ["warn"], + "no-extra-boolean-cast": ["off"], + "no-control-regex": ["off"], }, "root": true, "globals": { "frappe": true, "Vue": true, + "SetVueGlobals": true, "__": true, "repl": true, "Class": true, @@ -76,8 +43,10 @@ "is_null": true, "in_list": true, "has_common": true, + "posthog": true, "has_words": true, "validate_email": true, + "open_web_template_values_editor": true, "validate_name": true, "validate_phone": true, "validate_url": true, diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 481041ed68..1be4c1ef97 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -62,6 +62,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.10' + cache: pip - uses: pre-commit/action@v3.0.0 - name: Download Semgrep rules @@ -69,7 +70,7 @@ jobs: - name: Run Semgrep rules run: | - pip install semgrep==0.97.0 + pip install semgrep semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness deps-vulnerable-check: diff --git a/.github/workflows/release_notes.yml b/.github/workflows/release_notes.yml index bf761f23ba..9e5719d78b 100644 --- a/.github/workflows/release_notes.yml +++ b/.github/workflows/release_notes.yml @@ -29,10 +29,14 @@ jobs: steps: - name: Update notes run: | - NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' ) + NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/generate-notes -f tag_name=$RELEASE_TAG \ + | jq -r '.body' \ + | sed -E '/^\* (chore|ci|test|docs|style)/d' \ + | sed -E 's/by @mergify //' + ) RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/tags/$RELEASE_TAG | jq -r '.id') - gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/$RELEASE_ID -f body=$NEW_NOTES + gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/$RELEASE_ID -f body="$NEW_NOTES" env: - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c6bbe8ec9..7443bde6a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: - id: debug-statements - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v3.9.0 hooks: - id: pyupgrade args: ['--py310-plus'] @@ -48,13 +48,31 @@ repos: )$ + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.44.0 + hooks: + - id: eslint + types_or: [javascript] + args: ['--quiet'] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + frappe/public/dist/.*| + cypress/.*| + .*node_modules.*| + .*boilerplate.*| + frappe/www/website_script.js| + frappe/templates/includes/.*| + frappe/public/js/lib/.* + )$ + - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.0.0 hooks: - id: flake8 additional_dependencies: ['flake8-bugbear',] diff --git a/cypress/integration/form_builder.js b/cypress/integration/form_builder.js index b298abdbe7..bf7ab86008 100644 --- a/cypress/integration/form_builder.js +++ b/cypress/integration/form_builder.js @@ -9,38 +9,23 @@ context("Form Builder", () => { it("Open Form Builder for Web Form Doctype/Customize Form", () => { // doctype - cy.visit("/app/form-builder/Web Form"); + cy.visit("/app/doctype/Web Form"); + cy.findByRole("tab", { name: "Form" }).click(); cy.get(".form-builder-container").should("exist"); // customize form - cy.visit("/app/form-builder/Web Form/customize"); + cy.visit("/app/customize-form?doc_type=Web%20Form"); + cy.findByRole("tab", { name: "Form" }).click(); cy.get(".form-builder-container").should("exist"); }); - it("Change Doctype using page title dialog", () => { - cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link"); - - cy.visit(`/app/form-builder/Web Form`); - cy.get(".form-builder-container").should("exist"); - - cy.get(".page-title").click(); - - cy.get(".frappe-control[data-fieldname='doctype'] input").click().as("input"); - cy.get("@input").type("{rightArrow}Web Form Field", { delay: 200 }); - cy.wait("@search_link"); - cy.get("@input").type("{enter}").blur(); - - cy.click_modal_primary_button("Edit"); - - cy.get(".page-title .title-text").should("have.text", "Web Form Field"); - }); - - it("Save without change, check form dirty and reset changes", () => { - cy.visit(`/app/form-builder/${doctype_name}`); + it("Save without change, check form dirty", () => { + cy.visit(`/app/doctype/${doctype_name}`); + cy.findByRole("tab", { name: "Form" }).click(); // Save without change cy.click_doc_primary_button("Save"); - cy.get(".desk-alert.orange .alert-message").should("have.text", "No changes to save"); + cy.get(".desk-alert.orange .alert-message").should("have.text", "No changes in document"); // Check form dirty cy.get(".tab-content.active .section-columns-container:first .column:first .field:first") @@ -48,14 +33,11 @@ context("Form Builder", () => { .dblclick() .type("Dirty"); cy.get(".title-area .indicator-pill.orange").should("have.text", "Not Saved"); - - // Reset changes - cy.get(".page-actions .custom-actions .btn").contains("Reset Changes").click(); - cy.get(".title-area .indicator-pill.orange").should("not.exist"); }); it("Add empty section and save", () => { - cy.visit(`/app/form-builder/${doctype_name}`); + cy.visit(`/app/doctype/${doctype_name}`); + cy.findByRole("tab", { name: "Form" }).click(); let first_section = ".tab-content.active .form-section-container:first"; @@ -71,7 +53,8 @@ context("Form Builder", () => { it("Add Table field and check if columns are rendered", () => { cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link"); - cy.visit(`/app/form-builder/${doctype_name}`); + cy.visit(`/app/doctype/${doctype_name}`); + cy.findByRole("tab", { name: "Form" }).click(); let first_field = ".tab-content.active .section-columns-container:first .column:first .field:first"; @@ -126,20 +109,23 @@ context("Form Builder", () => { }); it("Drag Field/Column/Section & Tab", () => { - cy.visit(`/app/form-builder/${doctype_name}`); + cy.visit(`/app/doctype/${doctype_name}`); + cy.findByRole("tab", { name: "Form" }).click(); let first_column = ".tab-content.active .section-columns-container:first .column:first"; let first_field = first_column + " .field:first"; let label = "div[title='Double click to edit label'] span:first"; + cy.get(".tab-header .tabs .tab:first").click(); + // drag first tab to second position - cy.get(".tabs .tab:first").drag(".tabs .tab:nth-child(2)", { + cy.get(".tab-header .tabs .tab:first").drag(".tab-header .tabs .tab:nth-child(2)", { target: { x: 10, y: 10 }, force: true, }); - cy.get(".tabs .tab:first").find(label).should("have.text", "Tab 2"); + cy.get(".tab-header .tabs .tab:first").find(label).should("have.text", "Tab 2"); - cy.get(".tabs .tab:first").click(); + cy.get(".tab-header .tabs .tab:first").click(); cy.get(".sidebar-container .tab:first").click(); // drag check field to first column @@ -151,7 +137,7 @@ context("Form Builder", () => { cy.get(first_field) .find("div[title='Double click to edit label']") .dblclick() - .type("Test Check{enter}"); + .type("Test Check"); cy.get(first_field).find(label).should("have.text", "Test Check"); // drag the first field to second position @@ -184,13 +170,14 @@ context("Form Builder", () => { }); it("Add New Tab/Section/Column to Form", () => { - cy.visit(`/app/form-builder/${doctype_name}`); + cy.visit(`/app/doctype/${doctype_name}`); + cy.findByRole("tab", { name: "Form" }).click(); let first_section = ".tab-content.active .form-section-container:first"; // add new tab cy.get(".tab-header").realHover().find(".tab-actions .new-tab-btn").click(); - cy.get(".tabs .tab").should("have.length", 3); + cy.get(".tab-header .tabs .tab").should("have.length", 3); // add new section cy.get(first_section).click(15, 10); @@ -218,11 +205,12 @@ context("Form Builder", () => { // remove tab cy.get(".tab-header").realHover().find(".tab-actions .remove-tab-btn").click(); - cy.get(".tabs .tab").should("have.length", 2); + cy.get(".tab-header .tabs .tab").should("have.length", 2); }); it("Update Title field Label to New Title through Customize Form", () => { - cy.visit(`/app/form-builder/${doctype_name}`); + cy.visit(`/app/doctype/${doctype_name}`); + cy.findByRole("tab", { name: "Form" }).click(); let first_field = ".tab-content.active .section-columns-container:first .column:first .field:first"; @@ -239,7 +227,8 @@ context("Form Builder", () => { }); it("Validate Duplicate Name & reqd + hidden without default logic", () => { - cy.visit(`/app/form-builder/${doctype_name}`); + cy.visit(`/app/doctype/${doctype_name}`); + cy.findByRole("tab", { name: "Form" }).click(); let first_field = ".tab-content.active .section-columns-container:first .column:first .field:first"; @@ -275,10 +264,11 @@ context("Form Builder", () => { }); it("Undo/Redo", () => { - cy.visit(`/app/form-builder/${doctype_name}`); + cy.visit(`/app/doctype/${doctype_name}`); + cy.findByRole("tab", { name: "Form" }).click(); // click on second tab - cy.get(".tabs .tab:last").click(); + cy.get(".tab-header .tabs .tab:last").click(); let first_column = ".tab-content.active .section-columns-container:first .column:first"; let first_field = first_column + " .field:first"; diff --git a/cypress/integration/grid_configuration.js b/cypress/integration/grid_configuration.js index 9112d7023e..3d49ed1503 100644 --- a/cypress/integration/grid_configuration.js +++ b/cypress/integration/grid_configuration.js @@ -4,6 +4,8 @@ context("Grid Configuration", () => { cy.visit("/app/doctype/User"); }); it("Set user wise grid settings", () => { + cy.findByRole("tab", { name: "Form" }).click(); + cy.get('.form-section[data-fieldname="fields_section"]').click(); cy.wait(100); cy.get('.frappe-control[data-fieldname="fields"]').as("table"); cy.get("@table").find(".icon-sm").click(); diff --git a/cypress/integration/sidebar.js b/cypress/integration/sidebar.js index 0f97cdc7fe..91c38ef6ce 100644 --- a/cypress/integration/sidebar.js +++ b/cypress/integration/sidebar.js @@ -27,60 +27,70 @@ context("Sidebar", () => { }); it("Verify attachment visibility config", () => { - verify_attachment_visibility("doctype/Blog Post", true); + cy.call("frappe.tests.ui_test_helpers.create_todo", { + description: "Sidebar Attachment ToDo", + }).then((todo) => { + verify_attachment_visibility(`todo/${todo.message.name}`, true); + }); verify_attachment_visibility("blog-post/test-blog-attachment-post", false); }); it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => { - cy.visit("/app/doctype"); - cy.click_sidebar_button("Assigned To"); + cy.call("frappe.tests.ui_test_helpers.create_todo", { + description: "Sidebar Attachment ToDo", + }).then((todo) => { + let todo_name = todo.message.name; + cy.visit("/app/todo"); + 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"); + //To check if no filter is available in "Assigned To" dropdown + cy.get(".empty-state").should("contain", "No filters found"); - //Assigning a doctype to a user - cy.visit("/app/doctype/ToDo"); - cy.get(".form-assignments > .flex > .text-muted").click(); - 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("Assigned To"); + //Assigning a doctype to a user + cy.visit(`/app/todo/${todo_name}`); + cy.get(".form-assignments > .flex > .text-muted").click(); + cy.get_field("assign_to_me", "Check").click(); + cy.get(".modal-footer > .standard-actions > .btn-primary").click(); + cy.visit("/app/todo"); + 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" - ); + //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"); - //To check if there is no filter added to the listview - cy.get(".filter-button").should("contain", "Filter"); + //To check if there is no filter added to the listview + cy.get(".filter-button").should("contain", "Filter"); - //To add a filter to display data into the listview - cy.get(".group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item").click(); + //To add a filter to display data into the listview + cy.get( + ".group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item" + ).click(); - //To check if filter is applied - cy.click_filter_button().should("contain", "1 filter"); - 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", - `%${cy.config("testUser")}%` - ); - cy.click_filter_button(); + //To check if filter is applied + cy.click_filter_button().should("contain", "1 filter"); + 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", + `%${cy.config("testUser")}%` + ); + cy.click_filter_button(); - //To remove the applied filter - cy.clear_filters(); + //To remove the applied filter + cy.clear_filters(); - //To remove the assignment - cy.visit("/app/doctype/ToDo"); - cy.get(".assignments > .avatar-group > .avatar > .avatar-frame").click(); - cy.get(".remove-btn").click({ force: true }); - cy.hide_dialog(); - cy.visit("/app/doctype"); - cy.click_sidebar_button("Assigned To"); - cy.get(".empty-state").should("contain", "No filters found"); + //To remove the assignment + cy.visit(`/app/todo/${todo_name}`); + cy.get(".assignments > .avatar-group > .avatar > .avatar-frame").click(); + cy.get(".remove-btn").click({ force: true }); + cy.hide_dialog(); + cy.visit("/app/todo"); + cy.click_sidebar_button("Assigned To"); + cy.get(".empty-state").should("contain", "No filters found"); + }); }); }); diff --git a/esbuild/build-cleanup.js b/esbuild/build-cleanup.js index 023fce08c5..77b3c0c1c8 100644 --- a/esbuild/build-cleanup.js +++ b/esbuild/build-cleanup.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ const path = require("path"); const fs = require("fs"); const glob = require("fast-glob"); diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js index 1476db3c20..8937a03216 100644 --- a/esbuild/esbuild.js +++ b/esbuild/esbuild.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ const path = require("path"); const fs = require("fs"); const glob = require("fast-glob"); diff --git a/esbuild/utils.js b/esbuild/utils.js index 3edccfd024..3326c2d39b 100644 --- a/esbuild/utils.js +++ b/esbuild/utils.js @@ -94,16 +94,16 @@ function get_cli_arg(name) { function log_error(message, badge = "ERROR") { badge = chalk.white.bgRed(` ${badge} `); - console.error(`${badge} ${message}`); // eslint-disable-line no-console + console.error(`${badge} ${message}`); } function log_warn(message, badge = "WARN") { badge = chalk.black.bgYellowBright(` ${badge} `); - console.warn(`${badge} ${message}`); // eslint-disable-line no-console + console.warn(`${badge} ${message}`); } function log(...args) { - console.log(...args); // eslint-disable-line no-console + console.log(...args); } function get_redis_subscriber(kind) { diff --git a/frappe/__init__.py b/frappe/__init__.py index 88b995d17b..44e1a24137 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -19,7 +19,8 @@ import os import re import unicodedata import warnings -from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, TypeAlias, overload +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Literal, Optional, TypeAlias, overload import click from werkzeug.local import Local, release_local @@ -243,7 +244,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False) local.preload_assets = {"style": [], "script": []} local.session = _dict() local.dev_server = _dev_server - local.qb = get_query_builder(local.conf.db_type or "mariadb") + local.qb = get_query_builder(local.conf.db_type) local.qb.get_query = get_query setup_redis_cache_connection() setup_module_map() @@ -274,9 +275,12 @@ def connect( set_user("Administrator") -def connect_replica(): +def connect_replica() -> bool: from frappe.database import get_db + if local and hasattr(local, "replica_db") and hasattr(local, "primary_db"): + return False + user = local.conf.db_name password = local.conf.db_password port = local.conf.replica_db_port @@ -291,6 +295,8 @@ def connect_replica(): local.primary_db = local.db local.db = local.replica_db + return True + def get_site_config(sites_path: str | None = None, site_path: str | None = None) -> dict[str, Any]: """Returns `site_config.json` combined with `sites/common_site_config.json`. @@ -320,6 +326,27 @@ def get_site_config(sites_path: str | None = None, site_path: str | None = None) elif local.site and not local.flags.new_site: raise IncorrectSitePath(f"{local.site} does not exist") + # Generalized env variable overrides and defaults + def db_default_ports(db_type): + from frappe.database.mariadb.database import MariaDBDatabase + + return { + "mariadb": MariaDBDatabase.default_port, # 3306 + "postgres": 5432, + }[db_type] + + config["redis_queue"] = ( + os.environ.get("FRAPPE_REDIS_QUEUE") or config.get("redis_queue") or "redis://127.0.0.1:11311" + ) + config["redis_cache"] = ( + os.environ.get("FRAPPE_REDIS_CACHE") or config.get("redis_cache") or "redis://127.0.0.1:13311" + ) + config["db_type"] = os.environ.get("FRAPPE_DB_TYPE") or config.get("db_type") or "mariadb" + config["db_host"] = os.environ.get("FRAPPE_DB_HOST") or config.get("db_host") or "127.0.0.1" + config["db_port"] = ( + os.environ.get("FRAPPE_DB_PORT") or config.get("db_port") or db_default_ports(config["db_type"]) + ) + return _dict(config) @@ -360,7 +387,7 @@ def setup_redis_cache_connection(): if not cache: from frappe.utils.redis_wrapper import RedisWrapper - cache = RedisWrapper.from_url(conf.get("redis_cache") or "redis://localhost:11311") + cache = RedisWrapper.from_url(conf.get("redis_cache")) def get_traceback(with_context: bool = False) -> str: @@ -788,13 +815,17 @@ def is_whitelisted(method): def read_only(): def innfn(fn): def wrapper_fn(*args, **kwargs): + + # frappe.read_only could be called from nested functions, in such cases don't swap the + # connection again. + switched_connection = False if conf.read_from_replica: - connect_replica() + switched_connection = connect_replica() try: retval = fn(*args, **get_newargs(fn, kwargs)) finally: - if local and hasattr(local, "primary_db"): + if switched_connection and local and hasattr(local, "primary_db"): local.db.close() local.db = local.primary_db diff --git a/frappe/app.py b/frappe/app.py index dd7f28f0cc..cdb5d6ad21 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -284,6 +284,8 @@ def handle_exception(e): or (frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text")) ) + allow_traceback = frappe.get_system_settings("allow_error_traceback") if frappe.db else False + if not frappe.session.user: # If session creation fails then user won't be unset. This causes a lot of code that # assumes presence of this to fail. Session creation fails => guest or expired login @@ -338,7 +340,7 @@ def handle_exception(e): else: traceback = "
" + sanitize_html(frappe.get_traceback()) + "" # disable traceback in production if flag is set - if frappe.local.flags.disable_traceback and not frappe.local.dev_server: + if frappe.local.flags.disable_traceback or not allow_traceback and not frappe.local.dev_server: traceback = "" frappe.respond_as_web_page( diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 1bb3f7c51f..19952bdc9b 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -12,6 +12,8 @@ from frappe.contacts.doctype.contact.contact import ( get_contacts_linked_from, get_contacts_linking_to, ) +from frappe.core.doctype.communication.email import make +from frappe.desk.form import assign_to from frappe.model.document import Document from frappe.utils import ( add_days, @@ -372,14 +374,14 @@ class AutoRepeat(Document): elif "{" in self.message: message = frappe.render_template(self.message, {"doc": new_doc}) - frappe.sendmail( - reference_doctype=new_doc.doctype, - reference_name=new_doc.name, + make( + doctype=new_doc.doctype, + name=new_doc.name, recipients=self.recipients, subject=subject, content=message, attachments=attachments, - expose_recipients="header", + send_email=1, ) @frappe.whitelist() diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index e77376b693..bdddad8cf6 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -469,7 +469,7 @@ def database(context, extra_args): if not site: raise SiteNotSpecifiedError frappe.init(site=site) - if not frappe.conf.db_type or frappe.conf.db_type == "mariadb": + if frappe.conf.db_type == "mariadb": _mariadb(extra_args=extra_args) elif frappe.conf.db_type == "postgres": _psql(extra_args=extra_args) @@ -505,19 +505,17 @@ def postgres(context, extra_args): def _mariadb(extra_args=None): - from frappe.database.mariadb.database import MariaDBDatabase - mysql = which("mysql") command = [ mysql, "--port", - str(frappe.conf.db_port or MariaDBDatabase.default_port), + str(frappe.conf.db_port), "-u", frappe.conf.db_name, f"-p{frappe.conf.db_password}", frappe.conf.db_name, "-h", - frappe.conf.db_host or "localhost", + frappe.conf.db_host, "--pager=less -SFX", "--safe-updates", "-A", @@ -530,8 +528,8 @@ def _mariadb(extra_args=None): def _psql(extra_args=None): psql = which("psql") - host = frappe.conf.db_host or "127.0.0.1" - port = frappe.conf.db_port or "5432" + host = frappe.conf.db_host + port = frappe.conf.db_port env = os.environ.copy() env["PGPASSWORD"] = frappe.conf.db_password conn_string = f"postgresql://{frappe.conf.db_name}@{host}:{port}/{frappe.conf.db_name}" @@ -666,7 +664,7 @@ def transform_database(context, table, engine, row_format, failfast): skipped = 0 frappe.init(site=site) - if frappe.conf.db_type and frappe.conf.db_type != "mariadb": + if frappe.conf.db_type != "mariadb": click.secho("This command only has support for MariaDB databases at this point", fg="yellow") sys.exit(1) diff --git a/frappe/contacts/doctype/contact/test_contact.py b/frappe/contacts/doctype/contact/test_contact.py index e91e132258..18f0d78732 100644 --- a/frappe/contacts/doctype/contact/test_contact.py +++ b/frappe/contacts/doctype/contact/test_contact.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import frappe from frappe.contacts.doctype.contact.contact import get_full_name +from frappe.email import get_contact_list from frappe.tests.utils import FrappeTestCase test_dependencies = ["Contact", "Salutation"] @@ -44,6 +45,17 @@ class TestContact(FrappeTestCase): "John Jane Doe", ) + def test_get_contact_list(self): + # First time from database + results = get_contact_list("_Test Supplier") + self.assertEqual(results[0].label, "test_contact@example.com") + self.assertEqual(results[0].description, "_Test Contact For _Test Supplier") + + # Second time from cache + results = get_contact_list("_Test Supplier") + self.assertEqual(results[0].label, "test_contact@example.com") + self.assertEqual(results[0].description, "_Test Contact For _Test Supplier") + def create_contact(name, salutation, emails=None, phones=None, save=True): doc = frappe.get_doc( diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index 22c47be692..0df62080be 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -7,11 +7,14 @@ import frappe from frappe import _ from frappe.core.doctype.data_import.exporter import Exporter from frappe.core.doctype.data_import.importer import Importer +from frappe.model import core_doctypes_list from frappe.model.document import Document from frappe.modules.import_file import import_file_by_path from frappe.utils.background_jobs import enqueue, is_job_enqueued from frappe.utils.csvutils import validate_google_sheets_url +BLOCKED_DOCTYPES = set(core_doctypes_list) - {"User", "Role"} + class DataImport(Document): def validate(self): @@ -24,10 +27,15 @@ class DataImport(Document): self.template_options = "" self.template_warnings = "" + self.validate_doctype() self.validate_import_file() self.validate_google_sheets_url() self.set_payload_count() + def validate_doctype(self): + if self.reference_doctype in BLOCKED_DOCTYPES: + frappe.throw(_("Importing {0} is not allowed.").format(self.reference_doctype)) + def validate_import_file(self): if self.import_file: # validate template diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index bb2af5cec0..650729aef6 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -2,6 +2,26 @@ // MIT License. See license.txt frappe.ui.form.on("DocType", { + before_save: function (frm) { + let form_builder = frappe.form_builder; + if (form_builder?.store) { + let fields = form_builder.store.update_fields(); + + // if fields is a string, it means there is an error + if (typeof fields === "string") { + frappe.throw(fields); + } + } + }, + after_save: function (frm) { + if ( + frappe.form_builder && + frappe.form_builder.doctype === frm.doc.name && + frappe.form_builder.store + ) { + frappe.form_builder.store.fetch(); + } + }, refresh: function (frm) { frm.set_query("role", "permissions", function (doc) { if (doc.custom && frappe.session.user != "Administrator") { @@ -21,8 +41,6 @@ frappe.ui.form.on("DocType", { frm.toggle_enable("beta", 0); } - render_form_builder_message(frm); - if (!frm.is_new() && !frm.doc.istable) { if (frm.doc.issingle) { frm.add_custom_button(__("Go to {0}", [__(frm.doc.name)]), () => { @@ -72,6 +90,8 @@ frappe.ui.form.on("DocType", { frm.cscript.autoname(frm); frm.cscript.set_naming_rule_description(frm); frm.trigger("setup_default_views"); + + render_form_builder(frm); }, istable: (frm) => { @@ -142,4 +162,30 @@ function render_form_builder_message(frm) { } } +function render_form_builder(frm) { + if (frappe.form_builder && frappe.form_builder.doctype === frm.doc.name) { + frappe.form_builder.setup_page_actions(); + frappe.form_builder.store.fetch(); + return; + } + + if (frappe.form_builder) { + frappe.form_builder.wrapper = $(frm.fields_dict["form_builder"].wrapper); + frappe.form_builder.frm = frm; + frappe.form_builder.doctype = frm.doc.name; + frappe.form_builder.customize = false; + frappe.form_builder.init(true); + frappe.form_builder.store.fetch(); + } else { + frappe.require("form_builder.bundle.js").then(() => { + frappe.form_builder = new frappe.ui.FormBuilder({ + wrapper: $(frm.fields_dict["form_builder"].wrapper), + frm: frm, + doctype: frm.doc.name, + customize: false, + }); + }); + } +} + extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({ frm: cur_frm })); diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 5209a408eb..d42fa62802 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -25,9 +25,6 @@ "beta", "is_virtual", "queue_in_background", - "fields_section_break", - "try_form_builder_html", - "fields", "sb1", "naming_rule", "autoname", @@ -35,6 +32,32 @@ "column_break_15", "description", "documentation", + "sb2", + "permissions", + "restrict_to_domain", + "read_only", + "in_create", + "actions_section", + "actions", + "links_section", + "links", + "document_states_section", + "states", + "web_view", + "has_web_view", + "allow_guest_to_view", + "index_web_pages_for_search", + "route", + "is_published_field", + "website_search_field", + "advanced", + "engine", + "migration_hash", + "form_builder_tab", + "form_builder", + "fields_section", + "fields", + "settings_tab", "form_settings_section", "image_field", "timeline_field", @@ -68,28 +91,7 @@ "column_break_51", "email_append_to", "sender_field", - "subject_field", - "sb2", - "permissions", - "restrict_to_domain", - "read_only", - "in_create", - "actions_section", - "actions", - "links_section", - "links", - "document_states_section", - "states", - "web_view", - "has_web_view", - "allow_guest_to_view", - "index_web_pages_for_search", - "route", - "is_published_field", - "website_search_field", - "advanced", - "engine", - "migration_hash" + "subject_field" ], "fields": [ { @@ -195,12 +197,6 @@ "fieldtype": "Check", "label": "Beta" }, - { - "fieldname": "fields_section_break", - "fieldtype": "Section Break", - "label": "Fields", - "oldfieldtype": "Section Break" - }, { "fieldname": "fields", "fieldtype": "Table", @@ -633,9 +629,25 @@ "label": "Is Calendar and Gantt" }, { - "fieldname": "try_form_builder_html", + "fieldname": "settings_tab", + "fieldtype": "Tab Break", + "label": "Settings" + }, + { + "fieldname": "form_builder_tab", + "fieldtype": "Tab Break", + "label": "Form" + }, + { + "fieldname": "form_builder", "fieldtype": "HTML", - "label": "Try Form Builder HTML" + "label": "Form Builder" + }, + { + "collapsible": 1, + "fieldname": "fields_section", + "fieldtype": "Section Break", + "label": "Fields" } ], "icon": "fa fa-bolt", @@ -718,7 +730,7 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2023-05-15 14:07:51.526257", + "modified": "2023-07-12 13:56:26.185637", "modified_by": "Administrator", "module": "Core", "name": "DocType", @@ -755,4 +767,4 @@ "states": [], "track_changes": 1, "translated_doctype": 1 -} +} \ No newline at end of file diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 6a1887f14a..c127335b16 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -1496,6 +1496,7 @@ def get_fields_not_allowed_in_list_view(meta) -> list[str]: not_allowed_in_list_view.append("Attach Image") if meta.istable: not_allowed_in_list_view.remove("Button") + not_allowed_in_list_view.remove("HTML") return not_allowed_in_list_view diff --git a/frappe/core/doctype/doctype/doctype_list.js b/frappe/core/doctype/doctype/doctype_list.js deleted file mode 100644 index f4811fa01d..0000000000 --- a/frappe/core/doctype/doctype/doctype_list.js +++ /dev/null @@ -1,28 +0,0 @@ -frappe.listview_settings["DocType"] = { - onload: function (me) { - me.page.btn_primary.addClass("hidden"); - this.setup_select_primary_button(me); - }, - - setup_select_primary_button: function (me) { - let actions = [ - { - label: __("Add DocType (Form Builder)"), - description: __("Use the form builder to create a new DocType"), - action: () => frappe.set_route("form-builder", "new-doctype"), - }, - { - label: __("Add DocType"), - description: __("Create a new DocType"), - action: () => frappe.new_doc("DocType"), - }, - ]; - - frappe.utils.add_select_group_button( - me.page.btn_primary.parent(), - actions, - "btn-primary", - "add" - ); - }, -}; diff --git a/frappe/core/doctype/file/utils.py b/frappe/core/doctype/file/utils.py index 932fce9a03..b106ea67c5 100644 --- a/frappe/core/doctype/file/utils.py +++ b/frappe/core/doctype/file/utils.py @@ -308,7 +308,7 @@ def attach_files_to_document(doc: "Document", event) -> None: # we dont want the update to fail if file cannot be attached for some reason value = doc.get(df.fieldname) if not (value or "").startswith(("/files", "/private/files")): - return + continue if frappe.db.exists( "File", @@ -319,7 +319,7 @@ def attach_files_to_document(doc: "Document", event) -> None: "attached_to_field": df.fieldname, }, ): - return + continue unattached_file = frappe.db.exists( "File", @@ -341,7 +341,7 @@ def attach_files_to_document(doc: "Document", event) -> None: "attached_to_field": df.fieldname, }, ) - return + continue file: "File" = frappe.get_doc( doctype="File", diff --git a/frappe/core/doctype/report/boilerplate/controller.js b/frappe/core/doctype/report/boilerplate/controller.js index 9cf71a8c09..b7a53df088 100644 --- a/frappe/core/doctype/report/boilerplate/controller.js +++ b/frappe/core/doctype/report/boilerplate/controller.js @@ -1,6 +1,5 @@ // Copyright (c) {year}, {app_publisher} and contributors // For license information, please see license.txt -/* eslint-disable */ frappe.query_reports["{name}"] = {{ "filters": [ diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index e389a73cbb..7a72918f35 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -271,7 +271,7 @@ frappe.ui.form.on("User", { if (frappe.route_flags.unsaved === 1) { delete frappe.route_flags.unsaved; - for (var i = 0; i < frm.doc.user_emails.length; i++) { + for (let i = 0; i < frm.doc.user_emails.length; i++) { frm.doc.user_emails[i].idx = frm.doc.user_emails[i].idx + 1; } frm.dirty(); @@ -308,7 +308,7 @@ frappe.ui.form.on("User", { enable_incoming: 1, }; frappe.model.with_doctype("Email Account", function (doc) { - var doc = frappe.model.get_new_doc("Email Account"); + doc = frappe.model.get_new_doc("Email Account"); frappe.route_flags.linked_user = frm.doc.name; frappe.route_flags.delete_user_from_locals = true; frappe.set_route("Form", "Email Account", doc.name); diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 9bcc9ebd3d..062d3f349f 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1,7 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +from collections.abc import Sequence from datetime import timedelta -from typing import Optional, Sequence +from typing import Optional import frappe import frappe.defaults diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index 57214b82e2..805242325f 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -60,6 +60,10 @@ class UserPermission(Document): frappe.throw(_("{0} has already assigned default value for {1}.").format(ref_link, self.allow)) +def send_user_permissions(bootinfo): + bootinfo.user["user_permissions"] = get_user_permissions() + + @frappe.whitelist() def get_user_permissions(user=None): """Get all users permissions for the user as a dict of doctype""" diff --git a/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js index b2cf268b36..8a4dbefc45 100644 --- a/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js +++ b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js @@ -1,6 +1,5 @@ // Copyright (c) 2022, Frappe Technologies and contributors // For license information, please see license.txt -/* eslint-disable */ frappe.query_reports["Database Storage Usage By Tables"] = { filters: [], diff --git a/frappe/core/report/transaction_log_report/transaction_log_report.js b/frappe/core/report/transaction_log_report/transaction_log_report.js index 3c7261306d..f8f132a6d1 100644 --- a/frappe/core/report/transaction_log_report/transaction_log_report.js +++ b/frappe/core/report/transaction_log_report/transaction_log_report.js @@ -1,6 +1,5 @@ // Copyright (c) 2019, Frappe Technologies and contributors // For license information, please see license.txt -/* eslint-disable */ frappe.query_reports["Transaction Log Report"] = { onload: function (query_report) { diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index bd711c169d..c2e2e83d7f 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -100,8 +100,6 @@ frappe.ui.form.on("Customize Form", { frm.page.set_title(__("Customize Form - {0}", [frm.doc.doc_type])); frappe.customize_form.set_primary_action(frm); - render_form_builder_message(frm); - frm.add_custom_button( __("Go to {0} List", [__(frm.doc.doc_type)]), function () { @@ -149,6 +147,8 @@ frappe.ui.form.on("Customize Form", { ["queue_in_background"], frappe.get_meta(frm.doc.doc_type).is_submittable || 0 ); + + render_form_builder(frm); }); } @@ -334,37 +334,6 @@ frappe.ui.form.on("DocType State", { }, }); -frappe.customize_form.validate_fieldnames = async function (frm) { - for (let i = 0; i < frm.doc.fields.length; i++) { - let field = frm.doc.fields[i]; - - let fieldname = field.label && frappe.model.scrub(field.label).toLowerCase(); - if ( - field.label && - !field.fieldname && - in_list(frappe.model.restricted_fields, fieldname) - ) { - let message = __( - "For field {0} in row {1}, fieldname {2} is restricted it will be renamed as {2}1. Do you want to continue?", - [field.label, field.idx, fieldname] - ); - await pause_to_confirm(message); - } - } - - function pause_to_confirm(message) { - return new Promise((resolve) => { - frappe.confirm( - message, - () => resolve(), - () => { - frm.page.btn_primary.prop("disabled", false); - } - ); - }); - } -}; - frappe.customize_form.save_customization = function (frm) { if (frm.doc.doc_type) { return frm.call({ @@ -383,9 +352,22 @@ frappe.customize_form.save_customization = function (frm) { } }; +frappe.customize_form.update_fields_from_form_builder = function (frm) { + let form_builder = frappe.form_builder; + if (form_builder?.store) { + let fields = form_builder.store.update_fields(); + + // if fields is a string, it means there is an error + if (typeof fields === "string") { + frappe.throw(fields); + } + frm.refresh_fields(); + } +}; + frappe.customize_form.set_primary_action = function (frm) { - frm.page.set_primary_action(__("Update"), async () => { - await this.validate_fieldnames(frm); + frm.page.set_primary_action(__("Update"), () => { + this.update_fields_from_form_builder(frm); this.save_customization(frm); }); }; @@ -433,30 +415,29 @@ frappe.customize_form.clear_locals_and_refresh = function (frm) { frm.refresh(); }; -function render_form_builder_message(frm) { - $(frm.fields_dict["try_form_builder_html"].wrapper).empty(); - if (!frm.is_new() && frm.fields_dict["try_form_builder_html"]) { - let title = __("Use Form Builder to visually customize your form layout"); - let msg = __( - "You can drag and drop fields to create your form layout, add tabs, sections and columns to organize your form and update field properties all from one screen." - ); +function render_form_builder(frm) { + if (frappe.form_builder && frappe.form_builder.doctype === frm.doc.doc_type) { + frappe.form_builder.setup_page_actions(); + frappe.form_builder.store.fetch(); + return; + } - let message = ` - - `; - - $(frm.fields_dict["try_form_builder_html"].wrapper).html(message); + if (frappe.form_builder) { + frappe.form_builder.wrapper = $(frm.fields_dict["form_builder"].wrapper); + frappe.form_builder.frm = frm; + frappe.form_builder.doctype = frm.doc.doc_type; + frappe.form_builder.customize = true; + frappe.form_builder.init(true); + frappe.form_builder.store.fetch(); + } else { + frappe.require("form_builder.bundle.js").then(() => { + frappe.form_builder = new frappe.ui.FormBuilder({ + wrapper: $(frm.fields_dict["form_builder"].wrapper), + frm: frm, + doctype: frm.doc.doc_type, + customize: true, + }); + }); } } diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index e0d822eb61..d3c8c661e7 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -21,12 +21,20 @@ "allow_auto_repeat", "allow_import", "queue_in_background", - "fields_section_break", - "try_form_builder_html", - "fields", "naming_section", "naming_rule", "autoname", + "document_actions_section", + "actions", + "document_links_section", + "links", + "document_states_section", + "states", + "form_tab", + "form_builder", + "fields_section_break", + "fields", + "settings_tab", "form_settings_section", "image_field", "max_attachments", @@ -48,12 +56,6 @@ "email_append_to", "sender_field", "subject_field", - "document_actions_section", - "actions", - "document_links_section", - "links", - "document_states_section", - "states", "section_break_8", "sort_field", "column_break_10", @@ -174,8 +176,8 @@ "options": "ASC\nDESC" }, { + "collapsible": 1, "depends_on": "doc_type", - "description": "Customize Label, Print Hide, Default etc.", "fieldname": "fields_section_break", "fieldtype": "Section Break", "label": "Fields" @@ -369,9 +371,19 @@ "label": "Is Calendar and Gantt" }, { - "fieldname": "try_form_builder_html", + "fieldname": "settings_tab", + "fieldtype": "Tab Break", + "label": "Settings" + }, + { + "fieldname": "form_builder", "fieldtype": "HTML", - "label": "Try Form Builder HTML" + "label": "Form Builder" + }, + { + "fieldname": "form_tab", + "fieldtype": "Tab Break", + "label": "Form" } ], "hide_toolbar": 1, @@ -380,7 +392,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-05-15 16:03:19.872532", + "modified": "2023-07-16 13:25:46.201184", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.js b/frappe/custom/doctype/doctype_layout/doctype_layout.js index b212b79a5b..9c803e40f6 100644 --- a/frappe/custom/doctype/doctype_layout/doctype_layout.js +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.js @@ -31,7 +31,7 @@ frappe.ui.form.on("DocType Layout", { await frm.events.sync_fields(frm, false); if (frm.is_new()) { - frm.doc.__newname = document_name; + frm.doc.__newname = document_name; // eslint-disable-line frm.refresh_field("__newname"); } } diff --git a/frappe/custom/report/audit_system_hooks/audit_system_hooks.js b/frappe/custom/report/audit_system_hooks/audit_system_hooks.js index a78464f3da..b1db3894f1 100644 --- a/frappe/custom/report/audit_system_hooks/audit_system_hooks.js +++ b/frappe/custom/report/audit_system_hooks/audit_system_hooks.js @@ -1,6 +1,5 @@ // Copyright (c) 2023, Frappe Technologies and contributors // For license information, please see license.txt -/* eslint-disable */ frappe.query_reports["Audit System Hooks"] = { filters: [], diff --git a/frappe/database/database.py b/frappe/database/database.py index a264f39d47..a6254a0242 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -8,9 +8,10 @@ import random import re import string import traceback +from collections.abc import Iterable, Sequence from contextlib import contextmanager, suppress from time import time -from typing import Any, Iterable, Sequence +from typing import Any from pypika.dialects import MySQLQueryBuilder, PostgreSQLQueryBuilder from pypika.terms import Criterion, NullValue @@ -87,8 +88,8 @@ class Database: port=None, ): self.setup_type_map() - self.host = host or frappe.conf.db_host or "127.0.0.1" - self.port = port or frappe.conf.db_port or "" + self.host = host or frappe.conf.db_host + self.port = port or frappe.conf.db_port self.user = user or frappe.conf.db_name self.db_name = frappe.conf.db_name self._conn = None diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py index 5840158fa1..381abdc3d4 100644 --- a/frappe/database/db_manager.py +++ b/frappe/database/db_manager.py @@ -68,11 +68,7 @@ class DbManager: if pipe: print("Restoring Database file...") - command = ( - "{pipe} mysql -u {user} -p{password} -h{host} " - + ("-P{port}" if frappe.db.port else "") - + " {target} {source}" - ) + command = "{pipe} mysql -u {user} -p{password} -h{host} -P{port} {target} {source}" command = command.format( pipe=pipe, user=esc(user), diff --git a/frappe/database/operator_map.py b/frappe/database/operator_map.py index 2c8b53dae3..d98f46d758 100644 --- a/frappe/database/operator_map.py +++ b/frappe/database/operator_map.py @@ -2,7 +2,7 @@ # MIT License. See license.txt import operator -from typing import Callable +from collections.abc import Callable import frappe from frappe.database.utils import NestedSetHierarchy diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index ff14510c9c..200bae892f 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -54,7 +54,7 @@ def import_db_from_sql(source_sql=None, verbose=False): _command = ( f"psql {frappe.conf.db_name} " - f"-h {frappe.conf.db_host or 'localhost'} -p {str(frappe.conf.db_port or '5432')} " + f"-h {frappe.conf.db_host} -p {str(frappe.conf.db_port)} " f"-U {frappe.conf.db_name}" ) diff --git a/frappe/desk/doctype/console_log/console_log.json b/frappe/desk/doctype/console_log/console_log.json index a9ae9717fd..b8ccf8c9b5 100644 --- a/frappe/desk/doctype/console_log/console_log.json +++ b/frappe/desk/doctype/console_log/console_log.json @@ -6,8 +6,7 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "script", - "output" + "script" ], "fields": [ { @@ -16,20 +15,15 @@ "in_list_view": 1, "label": "Script", "read_only": 1 - }, - { - "fieldname": "output", - "fieldtype": "Code", - "label": "Output", - "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-18 20:07:57.587344", + "modified": "2023-07-05 22:16:02.823955", "modified_by": "Administrator", "module": "Desk", "name": "Console Log", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { @@ -48,5 +42,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json index 5ca49f3831..3a4f192656 100644 --- a/frappe/desk/doctype/event/event.json +++ b/frappe/desk/doctype/event/event.json @@ -75,7 +75,7 @@ "label": "Event Type", "oldfieldname": "event_type", "oldfieldtype": "Select", - "options": "Private\nPublic\nCancelled", + "options": "Private\nPublic", "reqd": 1, "search_index": 1 }, @@ -223,7 +223,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "options": "Open\nCompleted\nClosed" + "options": "Open\nCompleted\nClosed\nCancelled" }, { "collapsible": 1, @@ -295,7 +295,7 @@ "icon": "fa fa-calendar", "idx": 1, "links": [], - "modified": "2022-08-12 19:24:34.794098", + "modified": "2023-06-23 10:33:15.685368", "modified_by": "Administrator", "module": "Desk", "name": "Event", diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 390f519367..068457c2c6 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -9,6 +9,7 @@ frappe.ui.form.on("Form Tour", { frm.set_query("reference_doctype", () => { return { filters: { istable: 0 } }; }); + frm.trigger("reference_doctype"); frm.set_query("report_name", () => { if (frm.doc.reference_doctype) { return { diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py index 993af6e753..b79fd515b8 100644 --- a/frappe/desk/doctype/system_console/system_console.py +++ b/frappe/desk/doctype/system_console/system_console.py @@ -26,7 +26,7 @@ class SystemConsole(Document): else: frappe.db.rollback() - frappe.get_doc(dict(doctype="Console Log", script=self.console, output=self.output)).insert() + frappe.get_doc(dict(doctype="Console Log", script=self.console)).insert() frappe.db.commit() diff --git a/frappe/desk/page/form_builder/__init__.py b/frappe/desk/page/form_builder/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/desk/page/form_builder/form_builder.js b/frappe/desk/page/form_builder/form_builder.js deleted file mode 100644 index a7a25b5c6c..0000000000 --- a/frappe/desk/page/form_builder/form_builder.js +++ /dev/null @@ -1,211 +0,0 @@ -frappe.pages["form-builder"].on_page_load = function (wrapper) { - frappe.ui.make_app_page({ - parent: wrapper, - title: __("Form Builder"), - single_column: true, - }); - - // hot reload in development - if (frappe.boot.developer_mode) { - frappe.hot_update = frappe.hot_update || []; - frappe.hot_update.push(() => load_form_builder(wrapper)); - } -}; - -frappe.pages["form-builder"].on_page_show = function (wrapper) { - load_form_builder(wrapper); -}; - -function load_form_builder(wrapper) { - let route = frappe.get_route(); - route = route.filter((a) => a); - - if (route.length > 1 && route[1] === "new-doctype") { - frappe.pages["form-builder"].new_doctype(route[2]); - } else if (route.length > 1) { - let doctype = route[1]; - let is_customize_form = route[2] === "customize"; - - if (frappe.form_builder?.doctype) { - frappe.form_builder.doctype = frappe.form_builder.store.doctype = doctype; - frappe.form_builder.customize = frappe.form_builder.store.is_customize_form = - is_customize_form; - frappe.form_builder.init(true); - frappe.form_builder.store.fetch(); - return; - } - - let $parent = $(wrapper).find(".layout-main-section"); - $parent.empty(); - - frappe.require("form_builder.bundle.js").then(() => { - frappe.form_builder = new frappe.ui.FormBuilder({ - wrapper: $parent, - page: wrapper.page, - doctype: doctype, - customize: is_customize_form, - }); - }); - } else { - frappe.pages["form-builder"].select_doctype(); - } -} - -frappe.pages["form-builder"].select_doctype = function () { - let d = new frappe.ui.Dialog({ - title: __("Select DocType"), - fields: [ - { - label: __("Select DocType"), - fieldname: "doctype", - fieldtype: "Link", - options: "DocType", - only_select: 1, - }, - { - label: __("Customize"), - fieldname: "customize", - fieldtype: "Check", - }, - ], - primary_action_label: __("Edit"), - primary_action({ doctype, customize }) { - if (customize) { - frappe.model.with_doctype(doctype).then(() => { - let meta = frappe.get_meta(doctype); - if (in_list(frappe.model.core_doctypes_list, this.doctype)) - frappe.throw(__("Core DocTypes cannot be customized.")); - - if (meta.issingle) frappe.throw(__("Single DocTypes cannot be customized.")); - - if (meta.custom) - frappe.throw( - __( - "Only standard DocTypes are allowed to be customized from Customize Form." - ) - ); - frappe.set_route("form-builder", doctype, "customize"); - }); - } else { - frappe.set_route("form-builder", doctype); - } - }, - secondary_action_label: __("Create New DocType"), - secondary_action() { - let doctype = d.get_value("doctype") || ""; - d.hide(); - frappe.set_route("form-builder", "new-doctype", doctype); - }, - }); - - d.show(); -}; - -frappe.pages["form-builder"].new_doctype = function (doctype) { - let non_developer = frappe.session.user !== "Administrator" || !frappe.boot.developer_mode; - let new_d = new frappe.ui.Dialog({ - title: __("Create New DocType"), - fields: [ - { - label: __("DocType Name"), - fieldname: "doctype_name", - fieldtype: "Data", - default: doctype, - reqd: 1, - }, - { fieldtype: "Column Break" }, - { - label: __("Module"), - fieldname: "module", - fieldtype: "Link", - options: "Module Def", - reqd: 1, - }, - { fieldtype: "Section Break" }, - { - label: __("Is Submittable"), - fieldname: "is_submittable", - fieldtype: "Check", - description: __( - "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended." - ), - depends_on: "eval:!doc.istable && !doc.issingle", - }, - { - label: __("Is Child Table"), - fieldname: "istable", - fieldtype: "Check", - description: __("Child Tables are shown as a Grid in other DocTypes"), - depends_on: "eval:!doc.is_submittable && !doc.issingle", - }, - { - label: __("Editable Grid"), - fieldname: "editable_grid", - fieldtype: "Check", - depends_on: "istable", - default: 1, - }, - { - label: __("Is Single"), - fieldname: "issingle", - fieldtype: "Check", - description: __( - "Single Types have only one record no tables associated. Values are stored in tabSingles" - ), - depends_on: "eval:!doc.istable && !doc.is_submittable", - }, - { - label: __("Custom?"), - fieldname: "custom", - fieldtype: "Check", - default: non_developer, - read_only: non_developer, - }, - ], - primary_action_label: __("Create & Continue"), - primary_action(values) { - if (!values.istable) values.editable_grid = 0; - frappe.db - .insert({ - doctype: "DocType", - name: values.doctype_name, - module: values.module, - istable: values.istable, - editable_grid: values.editable_grid, - issingle: values.issingle, - custom: values.custom, - is_submittable: values.is_submittable, - permissions: [ - { - create: 1, - delete: 1, - email: 1, - export: 1, - print: 1, - read: 1, - report: 1, - role: "System Manager", - share: 1, - write: 1, - }, - ], - fields: [ - { - label: "Title", - fieldname: "title", - fieldtype: "Data", - }, - ], - }) - .then((doc) => { - frappe.set_route("form-builder", doc.name); - }); - }, - secondary_action_label: __("Back"), - secondary_action() { - new_d.hide(); - window.history.back(); - }, - }); - new_d.show(); -}; diff --git a/frappe/desk/page/form_builder/form_builder.json b/frappe/desk/page/form_builder/form_builder.json deleted file mode 100644 index afeacecd90..0000000000 --- a/frappe/desk/page/form_builder/form_builder.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "content": null, - "creation": "2022-10-10 22:42:53.597423", - "docstatus": 0, - "doctype": "Page", - "idx": 0, - "modified": "2022-10-10 22:42:53.597423", - "modified_by": "Administrator", - "module": "Desk", - "name": "form-builder", - "owner": "Administrator", - "page_name": "form-builder", - "roles": [], - "script": null, - "standard": "Yes", - "style": null, - "system_page": 0, - "title": "Form Builder" -} \ No newline at end of file diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index e5c268b24c..decf540097 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -642,7 +642,7 @@ function guess_country(country_info) { try { const system_timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - for ([country, info] of Object.entries(country_info)) { + for (let [country, info] of Object.entries(country_info)) { let possible_timezones = (info.timezones || []).filter((t) => t == system_timezone); if (possible_timezones.length) return country; } diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index a50588bdca..d89a15ee8e 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -377,7 +377,7 @@ def email_setup_wizard_exception(traceback, args): traceback=traceback, args="\n".join(pretty_args), user=frappe.session.user, - headers=frappe.request.headers, + headers=frappe.request.headers if frappe.request else "[no request]", ) frappe.sendmail( diff --git a/frappe/desk/page/user_profile/user_profile_controller.js b/frappe/desk/page/user_profile/user_profile_controller.js index 5103bd8a19..fd38f09ddd 100644 --- a/frappe/desk/page/user_profile/user_profile_controller.js +++ b/frappe/desk/page/user_profile/user_profile_controller.js @@ -145,7 +145,6 @@ class UserProfile { }); } - // eslint-disable-next-line no-unused-vars render_percentage_chart(field, title) { frappe .xcall( diff --git a/frappe/desk/report/todo/todo.js b/frappe/desk/report/todo/todo.js index 52fee62afd..f67a366418 100644 --- a/frappe/desk/report/todo/todo.js +++ b/frappe/desk/report/todo/todo.js @@ -1,6 +1,5 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -/* eslint-disable */ frappe.query_reports["ToDo"] = { filters: [], diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 071b6e7e61..c6252250fb 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -525,47 +525,47 @@ def get_stats(stats, doctype, filters=None): if filters is None: filters = [] - tags = json.loads(stats) + columns = json.loads(stats) if filters: filters = json.loads(filters) - stats = {} + results = {} try: - columns = frappe.db.get_table_columns(doctype) + db_columns = frappe.db.get_table_columns(doctype) except (frappe.db.InternalError, frappe.db.ProgrammingError): # raised when _user_tags column is added on the fly # raised if its a virtual doctype - columns = [] + db_columns = [] - for tag in tags: - if tag not in columns: + for column in columns: + if column not in db_columns: continue try: tag_count = frappe.get_list( doctype, - fields=[tag, "count(*)"], - filters=filters + [[tag, "!=", ""]], - group_by=tag, + fields=[column, "count(*)"], + filters=filters + [[column, "!=", ""]], + group_by=column, as_list=True, distinct=1, ) - if tag == "_user_tags": - stats[tag] = scrub_user_tags(tag_count) + if column == "_user_tags": + results[column] = scrub_user_tags(tag_count) no_tag_count = frappe.get_list( doctype, - fields=[tag, "count(*)"], - filters=filters + [[tag, "in", ("", ",")]], + fields=[column, "count(*)"], + filters=filters + [[column, "in", ("", ",")]], as_list=True, - group_by=tag, - order_by=tag, + group_by=column, + order_by=column, ) no_tag_count = no_tag_count[0][1] if no_tag_count else 0 - stats[tag].append([_("No Tags"), no_tag_count]) + results[column].append([_("No Tags"), no_tag_count]) else: - stats[tag] = tag_count + results[column] = tag_count except frappe.db.SQLError: pass @@ -573,7 +573,7 @@ def get_stats(stats, doctype, filters=None): # raised when _user_tags column is added on the fly pass - return stats + return results @frappe.whitelist() diff --git a/frappe/desk/utils.py b/frappe/desk/utils.py index 77edf88d7a..d638bb822a 100644 --- a/frappe/desk/utils.py +++ b/frappe/desk/utils.py @@ -53,6 +53,8 @@ def get_csv_bytes(data: list[list], csv_params: dict) -> bytes: def provide_binary_file(filename: str, extension: str, content: bytes) -> None: """Provide a binary file to the client.""" + from frappe import _ + frappe.response["type"] = "binary" frappe.response["filecontent"] = content - frappe.response["filename"] = f"{filename}.{extension}" + frappe.response["filename"] = f"{_(filename)}.{extension}" diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py index 5c4d6f4c72..463f54d7e0 100644 --- a/frappe/email/__init__.py +++ b/frappe/email/__init__.py @@ -2,7 +2,6 @@ # License: MIT. See LICENSE import frappe -from frappe.desk.reportview import build_match_conditions def sendmail_to_system_managers(subject, content): @@ -12,31 +11,38 @@ def sendmail_to_system_managers(subject, content): @frappe.whitelist() def get_contact_list(txt, page_length=20) -> list[dict]: """Return email ids for a multiselect field.""" + from frappe.contacts.doctype.contact.contact import get_full_name if cached_contacts := get_cached_contacts(txt): return cached_contacts[:page_length] - reportview_conditions = build_match_conditions("Contact") - match_conditions = f"and {reportview_conditions}" if reportview_conditions else "" + fields = ["name", "first_name", "middle_name", "last_name", "company_name"] + contacts = frappe.get_list( + "Contact", + fields=fields + ["`tabContact Email`.email_id"], + filters=[ + ["Contact Email", "email_id", "is", "set"], + ], + or_filters=[[field, "like", f"%{txt}%"] for field in fields] + + [["Contact Email", "email_id", "like", f"%{txt}%"]], + limit_page_length=page_length, + ) # The multiselect field will store the `label` as the selected value. # The `value` is just used as a unique key to distinguish between the options. # https://github.com/frappe/frappe/blob/6c6a89bcdd9454060a1333e23b855d0505c9ebc2/frappe/public/js/frappe/form/controls/autocomplete.js#L29-L35 - out = frappe.db.sql( - f"""select name as value, email_id as label, - concat(first_name, ifnull(concat(' ',last_name), '' )) as description - from tabContact - where (name like %(txt)s or email_id like %(txt)s) and email_id != '' - {match_conditions} - limit %(page_length)s""", - {"txt": f"%{txt}%", "page_length": page_length}, - as_dict=True, - ) - out = list(filter(None, out)) + result = [ + frappe._dict( + value=d.name, + label=d.email_id, + description=get_full_name(d.first_name, d.middle_name, d.last_name, d.company_name), + ) + for d in contacts + ] - update_contact_cache(out) + update_contact_cache(result) - return out + return result def get_system_managers(): diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 525703c8a2..5ddd71a4f6 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -857,7 +857,7 @@ class InboundMail(Email): """Remove Prefixes like 'fw', FWD', 're' etc from subject.""" # Match strings like "fw:", "re :" etc. regex = r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*" - return frappe.as_unicode(strip(re.sub(regex, "", subject, 0, flags=re.IGNORECASE))) + return frappe.as_unicode(strip(re.sub(regex, "", subject, count=0, flags=re.IGNORECASE))) @staticmethod def get_email_fields(doctype): diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 26c323352d..8dbd778a7d 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -11,6 +11,10 @@ class SiteNotSpecifiedError(Exception): super(Exception, self).__init__(self.message) +class UrlSchemeNotSupported(Exception): + pass + + class ValidationError(Exception): http_status_code = 417 diff --git a/frappe/hooks.py b/frappe/hooks.py index 740e9f3415..c088a5153a 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -427,6 +427,11 @@ after_job = [ extend_bootinfo = [ "frappe.utils.telemetry.add_bootinfo", + "frappe.core.doctype.user_permission.user_permission.send_user_permissions", ] +naming_series_variables = { + "PM": "frappe.tests.test_naming.parse_naming_series_variable", +} + get_changelog_feed = "frappe.desk.doctype.changelog_feed.changelog_feed.get_feed" diff --git a/frappe/installer.py b/frappe/installer.py index 775e5b9b02..a8646f480b 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -133,7 +133,7 @@ def install_db( from frappe.database import setup_database if not db_type: - db_type = frappe.conf.db_type or "mariadb" + db_type = frappe.conf.db_type if not root_login and db_type == "mariadb": root_login = "root" @@ -772,7 +772,7 @@ def is_downgrade(sql_file_path, verbose=False): # This function is only tested with mariadb # TODO: Add postgres support - if frappe.conf.db_type not in (None, "mariadb"): + if frappe.conf.db_type != "mariadb": return False from semantic_version import Version @@ -824,7 +824,7 @@ def is_partial(sql_file_path): def partial_restore(sql_file_path, verbose=False): sql_file = extract_sql_from_archive(sql_file_path) - if frappe.conf.db_type in (None, "mariadb"): + if frappe.conf.db_type == "mariadb": from frappe.database.mariadb.setup_db import import_db_from_sql elif frappe.conf.db_type == "postgres": import warnings diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py index 136e39e3a3..2593a21e51 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.py +++ b/frappe/integrations/doctype/google_calendar/google_calendar.py @@ -481,7 +481,7 @@ def update_event_in_google_calendar(doc, method=None): event["description"] = doc.description event["recurrence"] = repeat_on_to_google_calendar_recurrence_rule(doc) event["status"] = ( - "cancelled" if doc.event_type == "Cancelled" or doc.status == "Closed" else event.get("status") + "cancelled" if doc.status == "Cancelled" or doc.status == "Closed" else event.get("status") ) event.update( format_date_according_to_google_calendar( diff --git a/frappe/model/document.py b/frappe/model/document.py index e2a55065bb..0968dea0f0 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -3,7 +3,8 @@ import hashlib import json import time -from typing import Any, Generator, Iterable +from collections.abc import Generator, Iterable +from typing import Any from werkzeug.exceptions import NotFound diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 431a5c9879..3d8845382b 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -3,7 +3,8 @@ import datetime import re -from typing import TYPE_CHECKING, Callable, Optional +from collections.abc import Callable +from typing import TYPE_CHECKING, Optional import frappe from frappe import _ @@ -338,11 +339,11 @@ def parse_naming_series( part = determine_consecutive_week_number(today) elif e == "timestamp": part = str(today) - elif e == "FY": - part = frappe.defaults.get_user_default("fiscal_year") elif doc and (e.startswith("{") or doc.get(e, _sentinel) is not _sentinel): e = e.replace("{", "").replace("}", "") part = doc.get(e) + elif method := has_custom_parser(e): + part = frappe.get_attr(method[0])(doc, e) else: part = e @@ -354,6 +355,11 @@ def parse_naming_series( return name +def has_custom_parser(e): + """Returns true if the naming series part has a custom parser""" + return frappe.get_hooks("naming_series_variables", {}).get(e) + + def determine_consecutive_week_number(datetime): """Determines the consecutive calendar week""" m = datetime.month diff --git a/frappe/patches.txt b/frappe/patches.txt index ebdda9b220..054fe9b946 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -226,3 +226,4 @@ frappe.desk.doctype.form_tour.patches.introduce_ui_tours execute:frappe.delete_doc_if_exists("Workspace", "Customization") execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_naming", "Amend Counter") execute:frappe.delete_doc_if_exists("DocType", "Error Snapshot") +frappe.patches.v15_0.move_event_cancelled_to_status diff --git a/frappe/patches/v15_0/move_event_cancelled_to_status.py b/frappe/patches/v15_0/move_event_cancelled_to_status.py new file mode 100644 index 0000000000..3cb63a46fd --- /dev/null +++ b/frappe/patches/v15_0/move_event_cancelled_to_status.py @@ -0,0 +1,12 @@ +import frappe + + +def execute(): + Event = frappe.qb.DocType("Event") + query = ( + frappe.qb.update(Event) + .set(Event.event_type, "Private") + .set(Event.status, "Cancelled") + .where(Event.event_type == "Cancelled") + ) + query.run() diff --git a/frappe/permissions.py b/frappe/permissions.py index e8ca0ecb3c..ae26b56b2e 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -93,10 +93,10 @@ def has_permission( doc = frappe.get_doc(meta.name, doc) perm = get_doc_permissions(doc, user=user, ptype=ptype).get(ptype) if not perm: - push_perm_check_log( - _("User {0} does not have access to this document").format(frappe.bold(user)) - + f": {_(doc.doctype)} - {doc.name}" - ) + msg = _("User {0} does not have access to this document").format(frappe.bold(user)) + if frappe.has_permission(doc.doctype): + msg += f": {_(doc.doctype)} - {doc.name}" + push_perm_check_log(msg) else: if ptype == "submit" and not cint(meta.is_submittable): push_perm_check_log(_("Document Type is not submittable")) diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index 657904f32e..46313ef992 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -372,10 +372,11 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { if (!$item.hasClass("print-format-builder-field")) { var fieldname = $item.attr("data-fieldname"); + let field; if (fieldname === "_custom_html") { - var field = me.get_custom_html_field(); + field = me.get_custom_html_field(); } else { - var field = frappe.meta.get_docfield(me.print_format.doc_type, fieldname); + field = frappe.meta.get_docfield(me.print_format.doc_type, fieldname); } var html = frappe.render_template("print_format_builder_field", { @@ -561,7 +562,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { resize(); } else if (new_no_of_columns > no_of_columns) { // add empty column and resize old columns - for (var i = no_of_columns; i < new_no_of_columns; i++) { + for (let i = no_of_columns; i < new_no_of_columns; i++) { var col = $( '