From 3a9d078dc33489f2d6cbd29ad6cb3667e7b2cfee Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Wed, 31 Dec 2025 10:48:48 +0530 Subject: [PATCH 01/32] fix(report_view): enforce print permission for reports --- frappe/public/js/frappe/microtemplate.js | 3 +++ frappe/public/js/frappe/views/reports/report_view.js | 1 + frappe/utils/print_format.py | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/microtemplate.js b/frappe/public/js/frappe/microtemplate.js index f3041c677a..7a1558c949 100644 --- a/frappe/public/js/frappe/microtemplate.js +++ b/frappe/public/js/frappe/microtemplate.js @@ -187,6 +187,9 @@ frappe.render_pdf = function (html, opts = {}) { //Push the HTML content into an element formData.append("html", html); + if (opts.doctype) { + formData.append("doctype", opts.doctype); + } if (opts.orientation) { formData.append("orientation", opts.orientation); } diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index a34249c1d8..78ab8f59a9 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -1531,6 +1531,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { }, { label: __("Print"), + condition: () => frappe.model.can_print(this.doctype), action: () => { // prepare rows in their current state, sorted and filtered const rows_in_order = this.datatable.datamanager.rowViewOrder diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py index 59a94be98c..10afc78889 100644 --- a/frappe/utils/print_format.py +++ b/frappe/utils/print_format.py @@ -253,7 +253,9 @@ def download_pdf( @frappe.whitelist() -def report_to_pdf(html, orientation="Landscape"): +def report_to_pdf(html, orientation="Landscape", doctype=None): + if doctype: + frappe.has_permission(doctype, "print", throw=True) make_access_log(file_type="PDF", method="PDF", page=html) frappe.local.response.filename = "report.pdf" frappe.local.response.filecontent = get_pdf(html, {"orientation": orientation}) From 76a89b85ff137290b9a5d4528d361e50248b7db4 Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Tue, 13 Jan 2026 15:20:15 +0000 Subject: [PATCH 02/32] fix(calendar): show time slot views --- frappe/public/js/frappe/views/calendar/calendar.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index cd1f4c281c..21604b67a1 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -206,7 +206,7 @@ frappe.views.Calendar = class Calendar { } set_css() { const viewButtons = - ".fc-dayGridMonth-button, .fc-dayGridWeek-button, .fc-dayGridDay-button, .fc-today-button"; + ".fc-dayGridMonth-button, .fc-timeGridWeek-button, .fc-timeGridDay-button, .fc-today-button"; const fcViewButtonClasses = "fc-button fc-button-primary fc-button-active"; // remove fc-button styles @@ -253,13 +253,13 @@ frappe.views.Calendar = class Calendar { defaults.meridiem = "false"; this.cal_options = { plugins: frappe.FullCalendar.Plugins, - initialView: defaults.initialView || "dayGridMonth", + initialView: defaults.initialView || "timeGridWeek", locale: frappe.boot.lang, firstDay: 1, headerToolbar: { left: "prev,title,next", center: "", - right: "today,dayGridMonth,dayGridWeek,dayGridDay", + right: "today,dayGridMonth,timeGridWeek,timeGridDay", }, editable: true, droppable: true, From dcbbb12461ba49cdd5ffa93506a6464e5cc9af65 Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Tue, 13 Jan 2026 16:12:25 +0000 Subject: [PATCH 03/32] fix: update bind --- frappe/public/js/frappe/views/calendar/calendar.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index 21604b67a1..8482cfbc22 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -188,10 +188,10 @@ frappe.views.Calendar = class Calendar { const me = this; let btn_group = me.$wrapper.find(".fc-button-group"); btn_group.on("click", ".btn", function () { - let value = $(this).hasClass("fc-dayGridWeek-button") - ? "dayGridWeek" - : $(this).hasClass("fc-dayGridDay-button") - ? "dayGridDay" + let value = $(this).hasClass("fc-timeGridWeek-button") + ? "timeGridWeek" + : $(this).hasClass("fc-timeGridDay-button") + ? "timeGridDay" : "dayGridMonth"; me.set_localStorage_option("cal_initialView", value); }); From b663179bf8d94824dbe5f269ccd919c96b0f8838 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Tue, 13 Jan 2026 19:41:21 +0530 Subject: [PATCH 04/32] fix: copy releaserc from version-15 Also fix "branch" for future ease (cherry picked from commit ac4d4691849407a1018dbf26a1623fedf741e69b) Signed-off-by: Akhil Narang --- .releaserc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.releaserc b/.releaserc index 86f4f3cda0..ece1a68fa8 100644 --- a/.releaserc +++ b/.releaserc @@ -1,19 +1,22 @@ { - "branches": ["develop", {"name": "version-14-beta", "channel": "beta", "prerelease": true}], + "branches": ["version-17"], "plugins": [ "@semantic-release/commit-analyzer", { - "preset": "angular" + "preset": "angular", + "releaseRules": [ + {"breaking": true, "release": false} + ] }, "@semantic-release/release-notes-generator", [ "@semantic-release/exec", { - "prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" frappe/__init__.py' + "prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" frappe/__init__.py' } ], [ "@semantic-release/git", { "assets": ["frappe/__init__.py"], - "message": "chore(release): Bumped to Version ${nextRelease.version}" + "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}" } ], "@semantic-release/github" From e2cc46962eb14e228224907a59f797931e521410 Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Tue, 13 Jan 2026 23:49:25 +0000 Subject: [PATCH 05/32] revert: default initial view to dayGridMonth --- frappe/public/js/frappe/views/calendar/calendar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index 8482cfbc22..cef8b8a9c9 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -253,7 +253,7 @@ frappe.views.Calendar = class Calendar { defaults.meridiem = "false"; this.cal_options = { plugins: frappe.FullCalendar.Plugins, - initialView: defaults.initialView || "timeGridWeek", + initialView: defaults.initialView || "dayGridMonth", locale: frappe.boot.lang, firstDay: 1, headerToolbar: { From 14eaff022c2651380c681cedf12c81363871c27d Mon Sep 17 00:00:00 2001 From: sokumon Date: Wed, 14 Jan 2026 13:20:03 +0530 Subject: [PATCH 06/32] fix: use allowed pages to check perms --- frappe/boot.py | 6 +++--- .../desk/doctype/workspace_sidebar/workspace_sidebar.py | 9 ++------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index c1f572f995..b409e36b99 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -161,8 +161,8 @@ def load_desktop_data(bootinfo): from frappe.desk.desktop import get_workspace_sidebar_items bootinfo.workspaces = get_workspace_sidebar_items() - bootinfo.workspace_sidebar_item = get_sidebar_items() allowed_pages = [d.name for d in bootinfo.workspaces.get("pages")] + bootinfo.workspace_sidebar_item = get_sidebar_items(allowed_pages) bootinfo.module_wise_workspaces = get_controller("Workspace").get_module_wise_workspaces() bootinfo.dashboards = frappe.get_all("Dashboard") bootinfo.app_data = [] @@ -533,7 +533,7 @@ def get_sentry_dsn(): return os.getenv("FRAPPE_SENTRY_DSN") -def get_sidebar_items(): +def get_sidebar_items(allowed_workspaces): from frappe import _ from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import auto_generate_sidebar_from_module @@ -585,7 +585,7 @@ def get_sidebar_items(): if ( "My Workspaces" in sidebar_title or si.type == "Section Break" - or w.is_item_allowed(si.link_to, si.link_type) + or w.is_item_allowed(si.link_to, si.link_type, allowed_workspaces) ): sidebar_items[sidebar_title.lower()]["items"].append(workspace_sidebar) add_user_specific_sidebar(sidebar_items) diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py index d6390ca036..f3448894b9 100644 --- a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py +++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py @@ -77,7 +77,7 @@ class WorkspaceSidebar(Document): else: frappe.throw(_("You need to be Workspace Manager to delete a public workspace.")) - def is_item_allowed(self, name, item_type): + def is_item_allowed(self, name, item_type, allowed_workspaces): if frappe.session.user == "Administrator": return True @@ -100,12 +100,7 @@ class WorkspaceSidebar(Document): if item_type == "url": return True if item_type == "workspace": - try: - workspace = frappe.get_cached_doc("Workspace", name) - if workspace.module in self.allowed_modules: - return True - except frappe.DoesNotExistError: - return False + return name in allowed_workspaces def get_cached(self, cache_key, fallback_fn): value = frappe.cache.get_value(cache_key, user=frappe.session.user) From ea3b6a04a35413af463be87836d76d9f9b813592 Mon Sep 17 00:00:00 2001 From: sokumon Date: Wed, 14 Jan 2026 13:45:53 +0530 Subject: [PATCH 07/32] fix: close notifications correctly --- .../public/js/frappe/ui/notifications/notifications.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js index 808b48b592..8b6a5066ab 100644 --- a/frappe/public/js/frappe/ui/notifications/notifications.js +++ b/frappe/public/js/frappe/ui/notifications/notifications.js @@ -4,7 +4,11 @@ frappe.ui.Notifications = class Notifications { constructor(opts) { this.tabs = {}; this.notification_settings = frappe.boot.notification_settings; - this.full_height = opts?.full_height || true; + if (!opts?.full_height) { + this.full_height = true; + } + this.full_height = opts?.full_height; + this.wrapper = opts?.wrapper || $(".standard-items-sections"); this.make(); } @@ -52,8 +56,10 @@ frappe.ui.Notifications = class Notifications { ${frappe.utils.icon("x")} `) .on("click", (e) => { - if (!this.full_height) { + if (this.full_height) { this.dropdown.addClass("hidden"); + } else { + this.dropdown_list.addClass("hidden"); } }) .appendTo(this.header_actions) From 061b5579ef92329d1d317fc0254e1e0c76e41f96 Mon Sep 17 00:00:00 2001 From: "ALB.Leach" Date: Thu, 15 Jan 2026 13:57:21 +0700 Subject: [PATCH 08/32] fix: sidebar editor fixes (#35966) --- frappe/public/js/frappe/ui/sidebar/sidebar_editor.js | 1 + frappe/public/js/frappe/ui/sidebar/sidebar_item.js | 2 +- frappe/public/scss/desk/sidebar.scss | 7 ++++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_editor.js b/frappe/public/js/frappe/ui/sidebar/sidebar_editor.js index 9b781f7b9f..f952727da7 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar_editor.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_editor.js @@ -84,6 +84,7 @@ export class SidebarEditor { } prepare_data() { this.new_sidebar_items.forEach((item) => { + if (!item.nested_items) return; item.nested_items.forEach((nested_item) => { if (nested_item.parent) { delete nested_item.parent; diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js index 19e12a04be..f669d658ff 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js @@ -177,8 +177,8 @@ frappe.ui.sidebar_item.TypeSectionBreak = class SectionBreakSidebarItem extends this.full_template = $(this.wrapper); } make() { - if (this.item.nested_items.length == 0) return; super.make(); + if (!this.item.nested_items || this.item.nested_items.length == 0) return; this.add_items(); this.toggle_on_collapse(); this.enable_collapsible(this.item, this.full_template); diff --git a/frappe/public/scss/desk/sidebar.scss b/frappe/public/scss/desk/sidebar.scss index 0a0de5d260..0f6740c771 100644 --- a/frappe/public/scss/desk/sidebar.scss +++ b/frappe/public/scss/desk/sidebar.scss @@ -252,7 +252,6 @@ display: none; } .section-break { - flex: 0 0 auto !important; color: var(--ink-gray-5) !important; margin-left: 7px; gap: 0px !important; @@ -270,8 +269,7 @@ .standard-sidebar-item:hover { & .sidebar-item-edit-controls { visibility: visible; - display: flex; - gap: 6px; + width: auto; } } .collapse-sidebar-link { @@ -283,6 +281,9 @@ } .sidebar-item-edit-controls { visibility: hidden; + display: flex; + gap: 6px; + width: 0; } .standard-sidebar-item[data-name="add-sidebar-item"] { margin-top: 5px; From 03558f4848710d278afc66978ed6bc10a621752b Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Thu, 15 Jan 2026 12:31:01 +0530 Subject: [PATCH 09/32] chore(boilerplate): fixed db migration link (#35967) --- frappe/utils/boilerplate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 31fa50774c..89d91862c4 100644 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -818,7 +818,7 @@ jobs: patches_template = """[pre_model_sync] # Patches added in this section will be executed before doctypes are migrated -# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations +# Read docs to understand patches: https://docs.frappe.io/framework/user/en/database-migrations [post_model_sync] # Patches added in this section will be executed after doctypes are migrated""" From c63391b6a733c14a672a56bf41cb273a42f1281e Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Thu, 15 Jan 2026 13:03:07 +0530 Subject: [PATCH 10/32] fix: init proper empty session Signed-off-by: Akhil Narang --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 3cffd6ffda..11f48eaaa9 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -196,7 +196,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force: bool = local.cache = {} local.form_dict = _dict() local.preload_assets = {"style": [], "script": [], "icons": []} - local.session = _dict(user="Guest") + local.session = _dict(user="Guest", data=_dict()) local.dev_server = _dev_server # only for backwards compatibility local.qb = get_query_builder(local.conf.db_type) if not cache or not client_cache: From 740b65ff32a64a974392841d6eff9eeb229337fd Mon Sep 17 00:00:00 2001 From: Markus Lobedann Date: Thu, 15 Jan 2026 09:19:29 +0100 Subject: [PATCH 11/32] fix: update pyngrok dependency version to 7.5.0 6.0.0 doesn't work with unpaid accounts anymore --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 131071678c..5eb94c3b44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ Repository = "https://github.com/frappe/frappe.git" [project.optional-dependencies] dev = [ - "pyngrok~=6.0.0", + "pyngrok~=7.5.0", "watchdog~=6.0.0", "responses==0.23.1", # typechecking @@ -148,7 +148,7 @@ skip_namespaces = [ [tool.bench.dev-dependencies] coverage = "~=7.10.0" Faker = "~=18.10.1" -pyngrok = "~=6.0.0" +pyngrok = "~=7.5.0" unittest-xml-reporting = "~=3.2.0" watchdog = "~=6.0.0" hypothesis = "~=6.77.0" From 9af7187a72a1729214955174b0205fcf17f3582d Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Thu, 15 Jan 2026 15:13:05 +0530 Subject: [PATCH 12/32] fix(grid): border overflow issue --- frappe/public/scss/common/grid.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index d7fed36002..c9cca2dd86 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -8,6 +8,7 @@ color: var(--text-color); min-height: 75px; background-color: var(--subtle-accent); + overflow-y: hidden; } .form-grid.error { From ed48aa9b60f4c01c92d2f6fe29bbb86bcc3e77b0 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Thu, 15 Jan 2026 12:35:05 +0530 Subject: [PATCH 13/32] chore(hooks): develop_version bump --- frappe/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index 62801f33bb..a977f39529 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -8,7 +8,7 @@ app_publisher = "Frappe Technologies" app_description = "Full stack web framework with Python, Javascript, MariaDB, Redis, Node" app_license = "MIT" app_logo_url = "/assets/frappe/images/frappe-framework-logo.svg" -develop_version = "15.x.x-develop" +develop_version = "17.x.x-develop" app_home = "/app/build" app_email = "developers@frappe.io" From 137b94a4aad2dbae4c26df65dfa0ca3f6a1fd532 Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 15 Jan 2026 17:27:14 +0530 Subject: [PATCH 14/32] fix: show module sidebar --- frappe/public/js/frappe/ui/sidebar/sidebar.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js index 3a1ebcaf36..c12410c4d9 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js @@ -475,6 +475,8 @@ frappe.ui.Sidebar = class Sidebar { let sidebar = this.get_workspace_for_module(module); if (sidebars.includes(this.get_workspace_for_module(module))) { frappe.app.sidebar.setup(sidebar); + } else { + frappe.app.sidebar.setup(module); } } else if (module) { this.show_sidebar_for_module(module); From a98e94caf12a065cb71a89f7b7306339ce1ba7c4 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Thu, 15 Jan 2026 17:50:27 +0530 Subject: [PATCH 15/32] feat(doctype): generate controller_tree.js boilerplate for tree doctypes --- frappe/core/doctype/doctype/boilerplate/controller_tree.js | 5 +++++ frappe/core/doctype/doctype/doctype.py | 3 +++ frappe/modules/utils.py | 7 ++++++- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 frappe/core/doctype/doctype/boilerplate/controller_tree.js diff --git a/frappe/core/doctype/doctype/boilerplate/controller_tree.js b/frappe/core/doctype/doctype/boilerplate/controller_tree.js new file mode 100644 index 0000000000..9fc78986e0 --- /dev/null +++ b/frappe/core/doctype/doctype/boilerplate/controller_tree.js @@ -0,0 +1,5 @@ +// Copyright (c) {year}, {app_publisher} and contributors +// For license information, please see license.txt + +// frappe.treeview_settings["{doctype}"] = {{ +// }}; \ No newline at end of file diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index dc3a785a37..1b9f6b50f8 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -877,6 +877,9 @@ class DocType(Document): make_boilerplate("controller.js", self.as_dict()) # make_boilerplate("controller_list.js", self.as_dict()) + if self.is_tree: + make_boilerplate("controller_tree.js", self.as_dict()) + if self.has_web_view: templates_path = frappe.get_module_path( frappe.scrub(self.module), "doctype", frappe.scrub(self.name), "templates" diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 64d2463c2f..404540a32d 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -345,6 +345,7 @@ def get_app_publisher(module: str) -> str: def make_boilerplate(template: str, doc: "Document" | "frappe._dict", opts: dict | "frappe._dict" = None): + # print(template, "template_file_path \n\n\n") target_path = get_doc_path(doc.module, doc.doctype, doc.name) template_name = template.replace("controller", scrub(doc.name)) if template_name.endswith("._py"): @@ -354,6 +355,8 @@ def make_boilerplate(template: str, doc: "Document" | "frappe._dict", opts: dict get_module_path("core"), "doctype", scrub(doc.doctype), "boilerplate", template ) + + if os.path.exists(target_file_path): print(f"{target_file_path} already exists, skipping...") return @@ -400,7 +403,7 @@ def make_boilerplate(template: str, doc: "Document" | "frappe._dict", opts: dict """ controller_body = indent(dedent(controller_body), "\t") - + # print(source, "source \n\n\n") with open(target_file_path, "w") as target, open(template_file_path) as source: template = source.read() controller_file_content = cstr(template).format( @@ -413,6 +416,8 @@ def make_boilerplate(template: str, doc: "Document" | "frappe._dict", opts: dict **opts, custom_controller=controller_body, ) + print("template_file_path \n\n\n", controller_file_content) + # print(controller_file_content) target.write(frappe.as_unicode(controller_file_content)) From eda8edc80824ebef570e87d246f27f90ea913919 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Thu, 15 Jan 2026 20:12:02 +0530 Subject: [PATCH 16/32] refactor: utils.py --- frappe/modules/utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 404540a32d..cf2745e64d 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -345,7 +345,6 @@ def get_app_publisher(module: str) -> str: def make_boilerplate(template: str, doc: "Document" | "frappe._dict", opts: dict | "frappe._dict" = None): - # print(template, "template_file_path \n\n\n") target_path = get_doc_path(doc.module, doc.doctype, doc.name) template_name = template.replace("controller", scrub(doc.name)) if template_name.endswith("._py"): @@ -355,8 +354,6 @@ def make_boilerplate(template: str, doc: "Document" | "frappe._dict", opts: dict get_module_path("core"), "doctype", scrub(doc.doctype), "boilerplate", template ) - - if os.path.exists(target_file_path): print(f"{target_file_path} already exists, skipping...") return @@ -403,7 +400,7 @@ def make_boilerplate(template: str, doc: "Document" | "frappe._dict", opts: dict """ controller_body = indent(dedent(controller_body), "\t") - # print(source, "source \n\n\n") + with open(target_file_path, "w") as target, open(template_file_path) as source: template = source.read() controller_file_content = cstr(template).format( @@ -417,7 +414,6 @@ def make_boilerplate(template: str, doc: "Document" | "frappe._dict", opts: dict custom_controller=controller_body, ) print("template_file_path \n\n\n", controller_file_content) - # print(controller_file_content) target.write(frappe.as_unicode(controller_file_content)) From ec94433e6a0ceef593ae0b8900d142a903bffb0f Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 15 Jan 2026 21:04:19 +0530 Subject: [PATCH 17/32] fix: notification bell shouldn't flicker --- frappe/public/js/frappe/ui/notifications/notifications.js | 8 ++------ frappe/public/js/frappe/ui/sidebar/sidebar.js | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js index 8b6a5066ab..f2ca85decb 100644 --- a/frappe/public/js/frappe/ui/notifications/notifications.js +++ b/frappe/public/js/frappe/ui/notifications/notifications.js @@ -4,10 +4,7 @@ frappe.ui.Notifications = class Notifications { constructor(opts) { this.tabs = {}; this.notification_settings = frappe.boot.notification_settings; - if (!opts?.full_height) { - this.full_height = true; - } - this.full_height = opts?.full_height; + this.full_height = opts?.full_height || false; this.wrapper = opts?.wrapper || $(".standard-items-sections"); this.make(); @@ -155,9 +152,8 @@ frappe.ui.Notifications = class Notifications { const isInsideNotificationBtn = $(e.target).closest(".standard-items-sections .sidebar-notification").length > 0; const isInsideDropdown = $(e.target).closest(".notifications-list").length > 0; - if (!isInsideNotificationBtn && !isInsideDropdown) { - if (!full_height) { + if (full_height) { dropdown.addClass("hidden"); } } diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js index c12410c4d9..205f0766ef 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js @@ -332,7 +332,7 @@ frappe.ui.Sidebar = class Sidebar { } setup_notifications() { if (frappe.boot.desk_settings.notifications && frappe.session.user !== "Guest") { - this.notifications = new frappe.ui.Notifications(); + this.notifications = new frappe.ui.Notifications({ full_height: true }); } } add_item(container, item) { From 354f8337e9cc08262ab0b8662e31016e7e4ff970 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Thu, 15 Jan 2026 22:43:03 +0530 Subject: [PATCH 18/32] fix(doctype): remove debug print statement --- frappe/modules/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index cf2745e64d..64d2463c2f 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -413,7 +413,6 @@ def make_boilerplate(template: str, doc: "Document" | "frappe._dict", opts: dict **opts, custom_controller=controller_body, ) - print("template_file_path \n\n\n", controller_file_content) target.write(frappe.as_unicode(controller_file_content)) From 3f9957e2446e712a7a264db25539ff148eaa93d3 Mon Sep 17 00:00:00 2001 From: SID <158349177+0xsid0703@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:05:19 -0800 Subject: [PATCH 19/32] fix: Translate dashboard chart labels (fixes #35941) (#35952) * fix: translate dashboard chart labels (fixes #35941) - Translate chart_name when setting chart.label in dashboard_view.js - Translate chart name when adding existing charts to dashboard - Translate chart_name fallback in ChartDialog process_data This ensures dashboard chart labels are properly translated based on user's language preference. * fix: translate chart dataset names in backend (fixes #35941) - Translate chart.name when used as dataset name in get_chart_config - Translate chart.name when used as dataset name in get_group_by_chart_config This ensures chart dataset names (used in legends/tooltips) are also translated, complementing the frontend widget label translation fix. --- frappe/desk/doctype/dashboard_chart/dashboard_chart.py | 4 ++-- frappe/public/js/frappe/views/dashboard/dashboard_view.js | 4 ++-- frappe/public/js/frappe/widgets/widget_dialog.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 1152d9215f..64328dfc43 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -218,7 +218,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): else get_period(r[0], timegrain) for r in result ], - "datasets": [{"name": chart.name, "values": [r[1] for r in result]}], + "datasets": [{"name": _(chart.name), "values": [r[1] for r in result]}], } @@ -292,7 +292,7 @@ def get_group_by_chart_config(chart, filters) -> dict | None: if data: return { "labels": [item.get("name", "Not Specified") for item in data], - "datasets": [{"name": chart.name, "values": [item["count"] for item in data]}], + "datasets": [{"name": _(chart.name), "values": [item["count"] for item in data]}], } return None diff --git a/frappe/public/js/frappe/views/dashboard/dashboard_view.js b/frappe/public/js/frappe/views/dashboard/dashboard_view.js index 6b5b36a2ea..2adaee49fb 100644 --- a/frappe/public/js/frappe/views/dashboard/dashboard_view.js +++ b/frappe/public/js/frappe/views/dashboard/dashboard_view.js @@ -124,7 +124,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView { ? JSON.parse(settings.chart_config) : {}; this.charts.map((chart) => { - chart.label = chart.chart_name; + chart.label = __(chart.chart_name); chart.chart_settings = this.dashboard_chart_settings[chart.chart_name] || {}; }); this.render_dashboard_charts(); @@ -464,7 +464,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView { } else { this.chart_group.new_widget.on_create({ chart_name: chart.chart, - label: chart.chart, + label: __(chart.chart), name: chart.chart, }); } diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 0a5b4a24f3..6884cfef1e 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -147,7 +147,7 @@ class ChartDialog extends WidgetDialog { } process_data(data) { - data.label = data.label ? data.label : data.chart_name; + data.label = data.label ? data.label : __(data.chart_name); return data; } } From 0522c40501767ca88e998a4c92906a53bf15d286 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 15 Jan 2026 21:56:34 +0100 Subject: [PATCH 20/32] fix: return CommunicationComposer instance in email_doc method (#35992) --- frappe/public/js/frappe/form/form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 2eadf88417..1ce2a9be0f 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1400,7 +1400,7 @@ frappe.ui.form.Form = class FrappeForm { } email_doc(message) { - new frappe.views.CommunicationComposer({ + return new frappe.views.CommunicationComposer({ doc: this.doc, frm: this, subject: __(this.meta.name) + ": " + this.docname, From 345e9ed503ba4978457b6f80829c4d84dee0dfe4 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Fri, 16 Jan 2026 00:33:25 +0100 Subject: [PATCH 21/32] feat(version): add HTML diff view for multiline field changes (#35837) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- frappe/core/doctype/version/test_version.py | 129 +++++++++++++++++- frappe/core/doctype/version/version.js | 31 +++-- frappe/core/doctype/version/version.py | 53 +++++++ frappe/core/doctype/version/version_view.html | 63 +++++++++ frappe/public/scss/common/css_variables.scss | 5 +- frappe/public/scss/desk/dark.scss | 1 + 6 files changed, 268 insertions(+), 14 deletions(-) diff --git a/frappe/core/doctype/version/test_version.py b/frappe/core/doctype/version/test_version.py index b99dc6f046..e7f8829ef6 100644 --- a/frappe/core/doctype/version/test_version.py +++ b/frappe/core/doctype/version/test_version.py @@ -3,12 +3,137 @@ import copy import frappe -from frappe.core.doctype.version.version import get_diff -from frappe.tests import IntegrationTestCase +from frappe.core.doctype.version.version import ( + _as_string, + _generate_html_diff, + _should_generate_html_diff, + get_diff, +) +from frappe.tests import IntegrationTestCase, UnitTestCase from frappe.tests.utils import make_test_objects +class TestHTMLDiff(UnitTestCase): + def test_generate_html_diff_produces_table(self): + """Test HTML diff generates a table with content.""" + result = _generate_html_diff("line1\nline2", "line1\nmodified") + + self.assertIsNotNone(result) + self.assertIn("alert", result) + self.assertNotIn("
injected", result) + # Escaped versions should be present + self.assertIn("<script>", result) + self.assertIn("<div>", result) + + def test_should_generate_html_diff_multiline(self): + """Test should_generate_html_diff returns True for multiline text.""" + self.assertTrue(_should_generate_html_diff("line1\nline2", "line1\nmodified")) + self.assertTrue(_should_generate_html_diff("single", "multi\nline")) + self.assertTrue(_should_generate_html_diff("multi\nline", "single")) + + def test_should_generate_html_diff_long_text(self): + """Test should_generate_html_diff returns True for text > 80 characters.""" + self.assertTrue(_should_generate_html_diff("a" * 81, "b")) + self.assertTrue(_should_generate_html_diff("a", "b" * 81)) + self.assertTrue(_should_generate_html_diff("a" * 81, "b" * 81)) + + def test_should_generate_html_diff_short_text(self): + """Test should_generate_html_diff returns False for short single-line text.""" + self.assertFalse(_should_generate_html_diff("short", "text")) + self.assertFalse(_should_generate_html_diff("a" * 80, "b" * 80)) # Exactly 80 chars + + def test_should_generate_html_diff_empty_values(self): + """Test should_generate_html_diff returns False when either value is empty.""" + self.assertFalse(_should_generate_html_diff("", "short")) + self.assertFalse(_should_generate_html_diff("short", "")) + self.assertFalse(_should_generate_html_diff("", "")) + # Even long/multiline text returns False if the other value is empty + self.assertFalse(_should_generate_html_diff("", "a" * 81)) + self.assertFalse(_should_generate_html_diff("multi\nline", "")) + + def test_as_string_converts_values(self): + """Test _as_string converts values to strings correctly.""" + self.assertEqual(_as_string("text"), "text") + self.assertEqual(_as_string(None), "") + self.assertEqual(_as_string(""), "") + self.assertEqual(_as_string(0), "0") + + class TestVersion(IntegrationTestCase): + def test_onload_generates_html_diffs_for_multiline(self): + """Test onload generates HTML diffs for multiline changes.""" + version = frappe.get_doc( + doctype="Version", + ref_doctype="ToDo", + docname="test-doc", + data=frappe.as_json({"changed": [["description", "line1\nline2", "line1\nmodified"]]}), + ) + + version.onload() + + html_diffs = version.get_onload().get("html_diffs") + self.assertIsNotNone(html_diffs) + self.assertIn("description", html_diffs) + self.assertIn(" 80 characters.""" + version = frappe.get_doc( + doctype="Version", + ref_doctype="ToDo", + docname="test-doc", + data=frappe.as_json({"changed": [["notes", "x" * 81, "y" * 81]]}), + ) + + version.onload() + + html_diffs = version.get_onload().get("html_diffs") + self.assertIsNotNone(html_diffs) + self.assertIn("notes", html_diffs) + + def test_onload_no_html_diffs_for_simple_changes(self): + """Test onload doesn't generate HTML diffs for simple short changes.""" + version = frappe.get_doc( + doctype="Version", + ref_doctype="ToDo", + docname="test-doc", + data=frappe.as_json({"changed": [["status", "Open", "Closed"]]}), + ) + + version.onload() + + html_diffs = version.get_onload().get("html_diffs") + self.assertIsNone(html_diffs) + + def test_onload_handles_empty_data(self): + """Test onload handles empty or missing data gracefully.""" + version = frappe.get_doc( + doctype="Version", + ref_doctype="ToDo", + docname="test-doc", + data=None, + ) + + # Should not raise an error + version.onload() + self.assertIsNone(version.get_onload().get("html_diffs")) + + version.data = frappe.as_json({"changed": []}) + version.onload() + self.assertIsNone(version.get_onload().get("html_diffs")) + def test_get_diff(self): frappe.set_user("Administrator") test_records = make_test_objects("Event", reset=True) diff --git a/frappe/core/doctype/version/version.js b/frappe/core/doctype/version/version.js index 1e26e5f748..8fd83bc15f 100644 --- a/frappe/core/doctype/version/version.js +++ b/frappe/core/doctype/version/version.js @@ -1,12 +1,23 @@ -frappe.ui.form.on("Version", "refresh", function (frm) { - $( - frappe.render_template("version_view", { doc: frm.doc, data: JSON.parse(frm.doc.data) }) - ).appendTo(frm.fields_dict.table_html.$wrapper.empty()); - - frm.add_custom_button(__("Show all Versions"), function () { - frappe.set_route("List", "Version", { - ref_doctype: frm.doc.ref_doctype, - docname: frm.doc.docname, +frappe.ui.form.on("Version", { + refresh: function (frm) { + frm.add_custom_button(__("Show all Versions"), function () { + frappe.set_route("List", "Version", { + ref_doctype: frm.doc.ref_doctype, + docname: frm.doc.docname, + }); }); - }); + + frm.trigger("render_version_view"); + }, + + render_version_view: async function (frm) { + await frappe.model.with_doctype(frm.doc.ref_doctype); + + $( + frappe.render_template("version_view", { + doc: frm.doc, + data: JSON.parse(frm.doc.data), + }) + ).appendTo(frm.fields_dict.table_html.$wrapper.empty()); + }, }); diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py index d3ee8ca22d..7ea6f2f039 100644 --- a/frappe/core/doctype/version/version.py +++ b/frappe/core/doctype/version/version.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import difflib import json import frappe @@ -74,6 +75,29 @@ class Version(Document): def get_data(self): return json.loads(self.data) + def onload(self): + """Generate HTML diffs for multiline changes on document load.""" + if not self.data: + return + + data = self.get_data() + changed = data.get("changed", []) + if not changed: + return + + html_diffs = {} + for item in changed: + if len(item) >= 3: + fieldname, old_str, new_str = item[0], _as_string(item[1]), _as_string(item[2]) + if not _should_generate_html_diff(old_str, new_str): + continue + html_diff = _generate_html_diff(old_str, new_str) + if html_diff: + html_diffs[fieldname] = html_diff + + if html_diffs: + self.set_onload("html_diffs", html_diffs) + def get_diff(old, new, for_child=False, compare_cancelled=False): """Get diff between 2 document objects @@ -203,3 +227,32 @@ def get_diff(old, new, for_child=False, compare_cancelled=False): def on_doctype_update(): frappe.db.add_index("Version", ["ref_doctype", "docname"]) + + +def _generate_html_diff(old_str: str, new_str: str) -> str | None: + """Generate HTML diff for the given old and new strings.""" + old_lines = old_str.splitlines(keepends=True) + new_lines = new_str.splitlines(keepends=True) + + differ = difflib.HtmlDiff(wrapcolumn=80) + html_diff = differ.make_table( + old_lines, + new_lines, + fromdesc=frappe._("Original"), + todesc=frappe._("New"), + context=True, + numlines=3, + ) + return html_diff + + +def _should_generate_html_diff(old_str: str, new_str: str) -> bool: + """Determine if HTML diff should be generated for the given values.""" + return ( + old_str and new_str and ("\n" in old_str or "\n" in new_str or len(old_str) > 80 or len(new_str) > 80) + ) + + +def _as_string(value: str | None) -> str: + """Convert the given value to a string.""" + return cstr(value) if value is not None else "" diff --git a/frappe/core/doctype/version/version_view.html b/frappe/core/doctype/version/version_view.html index 7560118174..bbd63df849 100644 --- a/frappe/core/doctype/version/version_view.html +++ b/frappe/core/doctype/version/version_view.html @@ -1,3 +1,52 @@ +
{% if data.comment %}

{{ __("Comment") + " (" + data.comment_type }})

@@ -5,8 +54,19 @@ {% endif %} {% const getEscapedValue = (v) => v === null ? "null" : frappe.utils.escape_html(v) %} +{% const htmlDiffs = (doc.__onload && doc.__onload.html_diffs) || {} %} {% if data.changed && data.changed.length %}

{{ __("Values Changed") }}

+{% for item in data.changed %} + {% if htmlDiffs[item[0]] %} +
+
{{ frappe.meta.get_label(doc.ref_doctype, item[0]) }}
+
{{ htmlDiffs[item[0]] }}
+
+ {% endif %} +{% endfor %} +{% var hasSimpleChanges = data.changed.some(item => !htmlDiffs[item[0]]) %} +{% if hasSimpleChanges %} @@ -17,15 +77,18 @@ {% for item in data.changed %} + {% if !htmlDiffs[item[0]] %} + {% endif %} {% endfor %}
{{ frappe.meta.get_label(doc.ref_doctype, item[0]) }} {{ getEscapedValue(item[1]) }} {{ getEscapedValue(item[2]) }}
{% endif %} +{% endif %} {% var _keys = ["added", "removed"]; %} {% for key in _keys %} diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss index 1ae33c1a01..0fd3fb902c 100644 --- a/frappe/public/scss/common/css_variables.scss +++ b/frappe/public/scss/common/css_variables.scss @@ -150,8 +150,9 @@ $disabled-input-height: 22px; --switch-bg: var(--gray-300); // "diff" colors - --diff-added: var(--green-100); - --diff-removed: var(--red-100); + --diff-added: var(--green-200); + --diff-removed: var(--red-200); + --diff-changed: var(--blue-200); --right-arrow-svg: url("data: image/svg+xml;utf8, "); --left-arrow-svg: url("data: image/svg+xml;utf8, "); diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss index bb0cf9c2e5..aa4aabf48c 100644 --- a/frappe/public/scss/desk/dark.scss +++ b/frappe/public/scss/desk/dark.scss @@ -116,6 +116,7 @@ $check-icon-dark: url("data:image/svg+xml, Date: Fri, 16 Jan 2026 11:09:10 +0530 Subject: [PATCH 22/32] fix: Hungarian translations (#35934) --- frappe/locale/hu.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/locale/hu.po b/frappe/locale/hu.po index 2497e3c854..ffd24279ff 100644 --- a/frappe/locale/hu.po +++ b/frappe/locale/hu.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-12-21 09:35+0000\n" -"PO-Revision-Date: 2026-01-05 23:50\n" +"PO-Revision-Date: 2026-01-14 01:41\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Hungarian\n" "MIME-Version: 1.0\n" @@ -32,7 +32,7 @@ msgstr "\"Cégtörténet\"" #: frappe/core/doctype/data_export/exporter.py:202 msgid "\"Parent\" signifies the parent table in which this row must be added" -msgstr "\"Szülő\" jelenti azt a szülő táblát, amelyhez ezt a sort hozzá kell adni" +msgstr "\"Szülő\" jelenti azt a fő táblát, amelyhez ezt a sort hozzá kell adni" #. Description of the 'Team Members Heading' (Data) field in DocType 'About Us #. Settings' From 46daca0dd599d0a708ffe2cb24343e6c4a94a838 Mon Sep 17 00:00:00 2001 From: sokumon Date: Fri, 16 Jan 2026 02:54:56 +0530 Subject: [PATCH 23/32] fix(mobile): desktop modal design --- frappe/desk/page/desktop/desktop.css | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/frappe/desk/page/desktop/desktop.css b/frappe/desk/page/desktop/desktop.css index 005f98a956..a28dbe26e6 100644 --- a/frappe/desk/page/desktop/desktop.css +++ b/frappe/desk/page/desktop/desktop.css @@ -234,7 +234,7 @@ } .modal-body .icons{ margin-top: 0px; - place-self: start; + place-self: anchor-center; & .desktop-icon{ height: 126px; width: 127px; @@ -395,7 +395,9 @@ } .desktop-modal-body { - width: 90vw; + width: calc(100vw - 20px); + padding-left: 0px !important; + padding-right: 0px !important; > .icons-container { width: 100%; overflow: hidden !important; @@ -404,10 +406,8 @@ padding: 0; > .icons { - position: relative; - right: 6%; - column-gap: 4px; - row-gap: 8px; + column-gap: 6px; + row-gap: 6px; @media screen and (max-width: 380px) { --desktop-icon-container: 100px; @@ -463,4 +463,7 @@ [data-theme="dark"] .desktop-edit:hover{ opacity: 0.8; transition: opacity 0.3s; +} +.desktop-navbar-modal-search:hover{ + outline: 1px solid var(--surface-gray-3); } \ No newline at end of file From 04eb2517cd6aa243ef092dfa29ef7323c003a5f3 Mon Sep 17 00:00:00 2001 From: sokumon Date: Fri, 16 Jan 2026 14:42:15 +0530 Subject: [PATCH 24/32] fix: allow command k trigger inside input --- frappe/public/js/frappe/form/controls/base_input.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index beab7e96bd..a87155f37c 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -55,6 +55,18 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control // like links, currencies, HTMLs etc. this.disp_area = this.$wrapper.find(".control-value").get(0); } + this.setup_shortcut(); + } + setup_shortcut() { + $(this.input_area).on("keydown", function (event) { + if (event.originalEvent.ctrlKey || event.originalEvent.metaKey) { + if (event.originalEvent.key === "k" || event.originalEvent.key === "K") { + $("#navbar-modal-search").click(); + event.preventDefault(); + return false; + } + } + }); } set_max_width() { if (this.constructor.horizontal) { From bc590c084436eabb4aa19404dca554374a28664e Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:33:01 +0530 Subject: [PATCH 25/32] fix: ignore unsupported filter when querying dynamic link doctypes --- frappe/contacts/address_and_contact.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index ae712f7c5a..16ae1ca68d 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -116,6 +116,7 @@ def filter_dynamic_link_doctypes( txt = txt or "" filters = filters or {} + filters.pop("name", None) # ignore unsupported "name" filter - passed by validate_link_and_fetch _doctypes_from_df = frappe.get_all( "DocField", From cc498d3d54408efa9a318fc8d8ac01f9fa4886c5 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:52:45 +0530 Subject: [PATCH 26/32] fix: dont mutate filters for custom queries --- frappe/desk/search.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index d18ce76748..71483ff13c 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -109,28 +109,10 @@ def search_widget( if filters is None: filters = {} - are_filters_dict = isinstance(filters, dict) - include_disabled = False - if not query and are_filters_dict: - if "include_disabled" in filters: - if filters["include_disabled"] == 1: - include_disabled = True - filters.pop("include_disabled") - - filters = [make_filter_tuple(doctype, key, value) for key, value in filters.items()] - are_filters_dict = False - if for_link_validation: - if are_filters_dict: - # we add filter if possible, otherwise rely on txt - if "name" not in filters: - filters["name"] = txt - else: - filters.append([doctype, "name", "=", txt]) - as_dict = False - # for custom queries that don't respect filters but respect limit (rare) - # or for when we have to rely on txt + # for custom queries, we don't mutate filters + # we have to rely on txt # we want to match "A" with "A" only and not "A1", "BA" etc. page_length = PAGE_LENGTH_FOR_LINK_VALIDATION @@ -163,6 +145,19 @@ def search_widget( return [] meta = frappe.get_meta(doctype) + + include_disabled = False + if isinstance(filters, dict): + if "include_disabled" in filters: + if filters["include_disabled"] == 1: + include_disabled = True + filters.pop("include_disabled") + + filters = [make_filter_tuple(doctype, key, value) for key, value in filters.items()] + + if for_link_validation: + filters.append([doctype, "name", "=", txt]) + or_filters = [] # build from doctype From d4f1b51e98cf9eebcd1e0a9c6d57b569d74d8a0b Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:53:41 +0530 Subject: [PATCH 27/32] revert: "fix: ignore unsupported filter when querying dynamic link doctypes" This reverts commit bc590c084436eabb4aa19404dca554374a28664e. --- frappe/contacts/address_and_contact.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index 16ae1ca68d..ae712f7c5a 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -116,7 +116,6 @@ def filter_dynamic_link_doctypes( txt = txt or "" filters = filters or {} - filters.pop("name", None) # ignore unsupported "name" filter - passed by validate_link_and_fetch _doctypes_from_df = frappe.get_all( "DocField", From 190bb285c702f96c05925ef20e3102591690c823 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Sat, 17 Jan 2026 00:09:59 +0530 Subject: [PATCH 28/32] fix: args can be undefined --- frappe/public/js/frappe/form/controls/link.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 3439a290f7..eb3748a9c0 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -859,7 +859,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat } validate_link_and_fetch(value) { const args = this.get_search_args(value); - if (!args.doctype) return; + if (!args) return; const columns_to_fetch = Object.values(this.fetch_map); From b5e5a32baad2ad72dc04b73b78c0f06fb93b9698 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 17 Jan 2026 00:34:59 +0530 Subject: [PATCH 29/32] Revert "fix: per child level 'depends on' conditions" This reverts commit b308ee813ab6eafc3b00b74d720025b4755ab201. --- frappe/public/js/frappe/form/grid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 87c62aee55..c8b473de6a 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -552,7 +552,7 @@ export default class Grid { grid_row = new GridRow({ parent: $rows, parent_df: this.df, - docfields: JSON.parse(JSON.stringify(this.docfields)), + docfields: this.docfields, doc: d, frm: this.frm, grid: this, From 68d6588510024fbe1f734575de66cdeb1d01375e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 17 Jan 2026 00:35:10 +0530 Subject: [PATCH 30/32] Revert "fix: form indicators" This reverts commit 421dc48610938f1b2d34efde764844a5aef0675f. --- frappe/public/js/frappe/form/formatters.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index b2e6b3a22a..e2984e952a 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -433,9 +433,7 @@ frappe.format = function (value, df, options, doc) { df._options = doc ? doc[df.options] : null; } - var formatter = - frappe.meta.get_docfield(doc?.doctype, df.fieldname)?.formatter || - frappe.form.get_formatter(fieldtype); + var formatter = df.formatter || frappe.form.get_formatter(fieldtype); var formatted = formatter(value, df, options, doc); From d96f2fe2a4480dc1eaeefc76d14ae0447140779f Mon Sep 17 00:00:00 2001 From: sokumon Date: Sat, 17 Jan 2026 14:15:45 +0530 Subject: [PATCH 31/32] fix: dont consider virtual doctypes for sidebar generation --- frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py index f3448894b9..d21d79030a 100644 --- a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py +++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py @@ -334,7 +334,7 @@ def choose_top_doctypes(doctype_names): try: doctype_count_map = {} for doctype in doctype_names: - if not is_single_doctype(doctype): + if not is_single_doctype(doctype) and not frappe.get_meta(doctype).is_virtual: doctype_count_map[doctype] = frappe.db.count(doctype) top_doctypes = [ name From a0dfe6c7fb14067a4f3eb00c3c18a0afc83d46c0 Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Sat, 17 Jan 2026 20:24:45 +0000 Subject: [PATCH 32/32] fix: translate table headers in multi-select dialog --- frappe/public/js/frappe/form/multi_select_dialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index 1a3eb1359f..b83c405cbf 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -191,7 +191,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { get_child_datatable_columns() { const parent = this.doctype; return [parent, ...this.child_columns].map((d) => ({ - name: frappe.unscrub(d), + name: __(frappe.unscrub(d)), editable: false, })); }