From e0606c9aa03362026ab4ddb23e127f3e15324e67 Mon Sep 17 00:00:00 2001 From: Shankarv19bcr Date: Mon, 26 Jan 2026 15:57:05 +0530 Subject: [PATCH 01/89] feat(file): add DocShare support to file permission checks --- frappe/core/doctype/file/file.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 3abf1d0c87..4135d9c369 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -885,6 +885,16 @@ def has_permission(doc, ptype=None, user=None, debug=False): if user != "Guest" and doc.owner == user: return True + if ( + user != "Guest" + and ptype in ("read", "write", "submit", "share") + and frappe.db.get_all( + "DocShare", + filters={"share_doctype": "File", "share_name": doc.name, ptype: 1}, + or_filters={"user": user, "everyone": 1}, + ) + ): + return True if doc.attached_to_doctype and doc.attached_to_name: attached_to_doctype = doc.attached_to_doctype From ab790b51bcb30322530a005d83869c1d8a093227 Mon Sep 17 00:00:00 2001 From: UmakanthKaspa Date: Sun, 15 Feb 2026 13:26:27 +0530 Subject: [PATCH 02/89] fix: pre-populate existing values in dynamic filters dialog --- .../desk/doctype/dashboard_chart/dashboard_chart.js | 8 +++++++- frappe/desk/doctype/number_card/number_card.js | 8 +++++++- frappe/desk/doctype/todo/todo.json | 11 +++++++++-- frappe/desk/doctype/todo/todo.py | 1 + 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index e11d496d69..1c3f0e8f1f 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -488,7 +488,13 @@ frappe.ui.form.on("Dashboard Chart", { }); dialog.show(); - dialog.set_values(frm.dynamic_filters); + if (frm.dynamic_filters) { + let filter_values = {}; + frm.dynamic_filters.forEach((f) => { + filter_values[f[0] + ":" + f[1]] = f[3]; + }); + dialog.set_values(filter_values); + } }); }, diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index c5621fe6a4..8f53d39700 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -405,7 +405,13 @@ frappe.ui.form.on("Number Card", { }); dialog.show(); - dialog.set_values(frm.dynamic_filters); + if (frm.dynamic_filters) { + let filter_values = {}; + frm.dynamic_filters.forEach((f) => { + filter_values[f[0] + ":" + f[1]] = f[3]; + }); + dialog.set_values(filter_values); + } }); }, diff --git a/frappe/desk/doctype/todo/todo.json b/frappe/desk/doctype/todo/todo.json index 3186a0a846..f897bbd1b2 100644 --- a/frappe/desk/doctype/todo/todo.json +++ b/frappe/desk/doctype/todo/todo.json @@ -10,6 +10,7 @@ "description_and_status", "status", "priority", + "test", "column_break_2", "color", "date", @@ -156,12 +157,17 @@ "in_standard_filter": 1, "label": "Allocated To", "options": "User" + }, + { + "fieldname": "test", + "fieldtype": "Data", + "label": "Test" } ], "icon": "fa fa-check", "idx": 2, "links": [], - "modified": "2024-03-23 16:03:58.758787", + "modified": "2026-02-15 13:06:18.389084", "modified_by": "Administrator", "module": "Desk", "name": "ToDo", @@ -191,6 +197,7 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "search_fields": "description, reference_type, reference_name", "sender_field": "sender", "sort_field": "creation", @@ -200,4 +207,4 @@ "title_field": "description", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py index 77cdfb90db..6c6f578f37 100644 --- a/frappe/desk/doctype/todo/todo.py +++ b/frappe/desk/doctype/todo/todo.py @@ -33,6 +33,7 @@ class ToDo(Document): role: DF.Link | None sender: DF.Data | None status: DF.Literal["Open", "Closed", "Cancelled"] + test: DF.Data | None # end: auto-generated types DocType = "ToDo" From b9f659ef7481407860df57f19d379fcad0239ecb Mon Sep 17 00:00:00 2001 From: Akash Tom Date: Wed, 18 Feb 2026 15:08:39 +0530 Subject: [PATCH 03/89] fix: apply margin for header and footer for ids header-html and footer-html --- frappe/utils/pdf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py index e93349f4db..9dad6463e6 100644 --- a/frappe/utils/pdf.py +++ b/frappe/utils/pdf.py @@ -369,6 +369,12 @@ def prepare_header_footer(soup: BeautifulSoup): # {"header-html": "/tmp/frappe-pdf-random.html"} options[html_id] = fname + + if html_id == "header-html": + options["margin-top"] = "25mm" + elif html_id == "footer-html": + options["margin-bottom"] = "25mm" + else: if html_id == "header-html": options["margin-top"] = "15mm" From 2293491aba79a3696af7b66dc14758d46a768846 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 20 Feb 2026 14:48:43 +0530 Subject: [PATCH 04/89] fix: default readonly values for datetime and time --- frappe/public/js/frappe/ui/field_group.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index 98ce569ab5..caa70877d4 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -35,6 +35,10 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { if (def_value == "Today" && field.df["fieldtype"] == "Date") { def_value = frappe.datetime.get_today(); + } else if (def_value == "Now" && field.df["fieldtype"] == "Datetime") { + def_value = frappe.datetime.now_datetime(); + } else if (def_value == "Now" && field.df["fieldtype"] == "Time") { + def_value = frappe.datetime.now_time(); } field.set_input(def_value); From e9730499d636591f0758a696696aafb1f4b017b6 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 20 Feb 2026 17:59:07 +0530 Subject: [PATCH 05/89] refactor: cleanup date default keyword util --- frappe/public/js/frappe/ui/field_group.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index caa70877d4..a379da66b1 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -18,6 +18,17 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } } + resolve_date_default_keywords(def_value, fieldtype) { + if (def_value == "Today" && fieldtype == "Date") { + return frappe.datetime.get_today(); + } else if (def_value == "Now" && fieldtype == "Datetime") { + return frappe.datetime.now_datetime(); + } else if (def_value == "Now" && fieldtype == "Time") { + return frappe.datetime.now_time(); + } + return def_value; + } + make() { let me = this; if (this.fields) { @@ -33,13 +44,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { ) return; - if (def_value == "Today" && field.df["fieldtype"] == "Date") { - def_value = frappe.datetime.get_today(); - } else if (def_value == "Now" && field.df["fieldtype"] == "Datetime") { - def_value = frappe.datetime.now_datetime(); - } else if (def_value == "Now" && field.df["fieldtype"] == "Time") { - def_value = frappe.datetime.now_time(); - } + def_value = me.resolve_date_default_keywords(def_value, field.df.fieldtype); field.set_input(def_value); // if default and has depends_on, render its fields. From 9972937b072afd1b575bd6efc2e5bb19877039bd Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 20 Feb 2026 18:11:48 +0530 Subject: [PATCH 06/89] fix: default string check ignore case --- frappe/public/js/frappe/ui/field_group.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index a379da66b1..8c09a96858 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -19,13 +19,23 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } resolve_date_default_keywords(def_value, fieldtype) { - if (def_value == "Today" && fieldtype == "Date") { + if (!def_value) return def_value; + + def_value = def_value.toLowerCase(); + + if (def_value == "today" && fieldtype == "Date") { return frappe.datetime.get_today(); - } else if (def_value == "Now" && fieldtype == "Datetime") { - return frappe.datetime.now_datetime(); - } else if (def_value == "Now" && fieldtype == "Time") { - return frappe.datetime.now_time(); } + + if (def_value == "now") { + if (fieldtype == "Datetime") { + return frappe.datetime.now_datetime(); + } + if (fieldtype == "Time") { + return frappe.datetime.now_time(); + } + } + return def_value; } From 6d79d703a0a0f317e902d90ad72dcd38234127c2 Mon Sep 17 00:00:00 2001 From: Safwan Samsudeen Date: Mon, 23 Feb 2026 11:18:51 +0530 Subject: [PATCH 07/89] fix: use standard method --- frappe/core/doctype/file/file.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 4135d9c369..dadacb4b5b 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -887,12 +887,8 @@ def has_permission(doc, ptype=None, user=None, debug=False): return True if ( user != "Guest" - and ptype in ("read", "write", "submit", "share") - and frappe.db.get_all( - "DocShare", - filters={"share_doctype": "File", "share_name": doc.name, ptype: 1}, - or_filters={"user": user, "everyone": 1}, - ) + and ptype + and frappe.share.get_shared("File", filters=[["share_name", "=", doc.name]], rights=[ptype]) ): return True From a2fdff9006ca426316ba488042b06131f2e3f43f Mon Sep 17 00:00:00 2001 From: Safwan <62411302+safwansamsudeen@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:34:37 +0530 Subject: [PATCH 08/89] fix: pass on user param Co-authored-by: Akhil Narang --- frappe/core/doctype/file/file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index dadacb4b5b..b98c4e3286 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -888,7 +888,7 @@ def has_permission(doc, ptype=None, user=None, debug=False): if ( user != "Guest" and ptype - and frappe.share.get_shared("File", filters=[["share_name", "=", doc.name]], rights=[ptype]) + and frappe.share.get_shared("File", filters=[["share_name", "=", doc.name]], rights=[ptype], user=user) ): return True From e15d4c837afdc6fcc6520435defd526e62ec7a0d Mon Sep 17 00:00:00 2001 From: Safwan Samsudeen Date: Mon, 23 Feb 2026 14:45:22 +0530 Subject: [PATCH 09/89] fix: validate ptype in file has_permission --- frappe/core/doctype/file/file.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index b98c4e3286..c0c5445957 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -887,8 +887,10 @@ def has_permission(doc, ptype=None, user=None, debug=False): return True if ( user != "Guest" - and ptype - and frappe.share.get_shared("File", filters=[["share_name", "=", doc.name]], rights=[ptype], user=user) + and ptype in ["read", "write", "share", "submit"] + and frappe.share.get_shared( + "File", filters=[["share_name", "=", doc.name]], rights=[ptype], user=user + ) ): return True From 0cdeee51ab682bf13f80113232de835294b533ca Mon Sep 17 00:00:00 2001 From: Aditya Patil Date: Mon, 23 Feb 2026 18:58:12 +0530 Subject: [PATCH 10/89] refactor: linked documents retrieval and UX --- frappe/desk/form/linked_with.py | 89 ++++++++++++--------- frappe/public/js/frappe/form/linked_with.js | 47 ++++++++--- frappe/public/scss/desk/list.scss | 21 +++++ 3 files changed, 105 insertions(+), 52 deletions(-) diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 4c54d1be4a..ccb681804f 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -437,37 +437,19 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di is_target_doctype_table = frappe.get_meta(doctype).istable for linked_doctype, link_context in linkinfo.items(): - # Don't try to fetch linked documents if the user can't read the doctype - if not frappe.has_permission(linked_doctype): - continue - linked_doctype_meta = frappe.get_meta(linked_doctype) if linked_doctype_meta.issingle: continue + has_permission = frappe.has_permission(linked_doctype) filters = [] + or_filters = [] ret = None parent_info = None - fields = [ - d.fieldname - for d in linked_doctype_meta.get( - "fields", - { - "in_list_view": 1, - "fieldtype": ["not in", ("Image", "HTML", "Button", *frappe.model.table_fields)], - }, - ) - ] + ["name", "modified", "docstatus"] - - if add_fields := link_context.get("add_fields"): - fields += add_fields - - fields = [sf.strip() for sf in fields if sf] - if filters_ctx := link_context.get("filters"): - ret = frappe.get_list(doctype=linked_doctype, fields=fields, filters=filters_ctx, order_by=None) + filters = filters_ctx elif link_context.get("get_parent"): # check for child table @@ -478,13 +460,10 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di doctype, name, ["parenttype", "parent"], as_dict=True, order_by=None ) - if parent_info and parent_info.parenttype == linked_doctype: - ret = frappe.get_list( - doctype=linked_doctype, - fields=fields, - filters=[[linked_doctype, "name", "=", parent_info.parent]], - order_by=None, - ) + if not (parent_info and parent_info.parenttype == linked_doctype): + continue + + filters = [[linked_doctype, "name", "=", parent_info.parent]] elif child_doctype := link_context.get("child_doctype"): or_filters = [ @@ -495,15 +474,6 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di if doctype_fieldname := link_context.get("doctype_fieldname"): filters.append([child_doctype, doctype_fieldname, "=", doctype]) - ret = frappe.get_list( - doctype=linked_doctype, - fields=fields, - filters=filters, - or_filters=or_filters, - distinct=True, - order_by=None, - ) - elif link_fieldnames := link_context.get("fieldname"): if isinstance(link_fieldnames, str): link_fieldnames = [link_fieldnames] @@ -518,12 +488,51 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di or frappe.db.exists(linked_doctype, {"parenttype": doctype, "parent": name}) ): continue + + total_count = len( + frappe.get_all( + linked_doctype, + filters=filters, + or_filters=or_filters, + fields=["name"], + order_by=None, + ) + ) + + if not total_count: + continue + + if has_permission: + fields = [ + d.fieldname + for d in linked_doctype_meta.get( + "fields", + { + "in_list_view": 1, + "fieldtype": ["not in", ("Image", "HTML", "Button", *frappe.model.table_fields)], + }, + ) + ] + ["name", "modified", "docstatus"] + + if add_fields := link_context.get("add_fields"): + fields += add_fields + + fields = [sf.strip() for sf in fields if sf] + ret = frappe.get_list( - doctype=linked_doctype, fields=fields, filters=filters, or_filters=or_filters, order_by=None + doctype=linked_doctype, + fields=fields, + filters=filters, + or_filters=or_filters, + distinct=True, + order_by=None, ) - if ret: - results[linked_doctype] = ret + permitted_count = len(ret or []) + results[linked_doctype] = { + "docs": ret or [], + "hidden_count": total_count - permitted_count, + } return results diff --git a/frappe/public/js/frappe/form/linked_with.js b/frappe/public/js/frappe/form/linked_with.js index 12c5dc4f6b..851976f29e 100644 --- a/frappe/public/js/frappe/form/linked_with.js +++ b/frappe/public/js/frappe/form/linked_with.js @@ -20,7 +20,8 @@ frappe.ui.form.LinkedWith = class LinkedWith { make_dialog() { this.dialog = new frappe.ui.Dialog({ - title: __("Linked With"), + title: __("Links"), + minimizable: true, }); this.dialog.on_page_show = () => { @@ -39,22 +40,34 @@ frappe.ui.form.LinkedWith = class LinkedWith { make_html() { let html = ""; const linked_docs = this.frm.__linked_docs; - const linked_doctypes = Object.keys(linked_docs); + const linked_doctypes = Object.keys(linked_docs).filter((dt) => { + const entry = linked_docs[dt]; + return (entry.docs && entry.docs.length) || entry.hidden_count > 0; + }); if (linked_doctypes.length === 0) { html = __("Not Linked to any record"); } else { - html = linked_doctypes - .map((doctype) => { - const docs = linked_docs[doctype]; - return ` -
- ${this.make_doc_head(doctype)} - ${docs.map((doc) => this.make_doc_row(doc, doctype)).join("")} + html = ` +
+ ${__("Following documents are linked to {0}", [frappe.utils.get_form_link(this.frm.doctype, this.frm.docname, true).bold()])}
- `; - }) - .join(""); + ${linked_doctypes + .map((doctype) => { + const { docs, hidden_count } = linked_docs[doctype]; + let rows = (docs || []).map((doc) => this.make_doc_row(doc, doctype)).join(""); + if (hidden_count > 0) { + rows += this.make_hidden_count_row(hidden_count); + } + return ` +
+ ${this.make_doc_head(doctype)} + ${rows} +
+ `; + }) + .join("")} + `; } $(this.dialog.body).html(html); @@ -68,6 +81,16 @@ frappe.ui.form.LinkedWith = class LinkedWith { `; } + make_hidden_count_row(count) { + return `
+
+
+ ${count == 1 ? __("{0} restricted document", [count]) : __("{0} restricted documents", [count])} +
+
+
`; + } + make_doc_row(doc, doctype) { return `
diff --git a/frappe/public/scss/desk/list.scss b/frappe/public/scss/desk/list.scss index 6ac02526f5..65aa1c5b94 100644 --- a/frappe/public/scss/desk/list.scss +++ b/frappe/public/scss/desk/list.scss @@ -371,6 +371,27 @@ input.list-header-checkbox { .list-item-table { border: 1px solid $border-color; border-radius: 3px; + + .list-row-head { + border-radius: unset; + } + + .list-row-container { + border-bottom: 1px solid $border-color; + border-radius: unset; + + &:last-child { + border-bottom: none; + } + } + + .list-row-container:hover { + border-radius: unset; + } + + .list-row-container .list-row { + border-bottom: none; + } } .list-item { From 2357c2c923beab9dc8a773d62bef9582f7923a77 Mon Sep 17 00:00:00 2001 From: Aditya Patil Date: Mon, 23 Feb 2026 19:14:55 +0530 Subject: [PATCH 11/89] fix: wording ('linked with' instead of 'linked to') --- frappe/public/js/frappe/form/linked_with.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/linked_with.js b/frappe/public/js/frappe/form/linked_with.js index 851976f29e..034202da1e 100644 --- a/frappe/public/js/frappe/form/linked_with.js +++ b/frappe/public/js/frappe/form/linked_with.js @@ -50,7 +50,7 @@ frappe.ui.form.LinkedWith = class LinkedWith { } else { html = `
- ${__("Following documents are linked to {0}", [frappe.utils.get_form_link(this.frm.doctype, this.frm.docname, true).bold()])} + ${__("Following documents are linked with {0}", [frappe.utils.get_form_link(this.frm.doctype, this.frm.docname, true).bold()])}
${linked_doctypes .map((doctype) => { From 8e97332029aa13d51405992700403c4b307e7145 Mon Sep 17 00:00:00 2001 From: Luis Mendoza Date: Fri, 6 Feb 2026 16:01:22 +0000 Subject: [PATCH 12/89] fix: resolve currency precision for child table rows in unsaved documents --- frappe/model/meta.py | 6 ++++++ frappe/public/js/frappe/model/meta.js | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 9e5624252d..5d430d2c5a 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -884,6 +884,12 @@ def get_field_currency(df, doc=None): if frappe.get_meta(doc.parenttype).has_field(df.get("options")): # only get_value if parent has currency field currency = frappe.db.get_value(doc.parenttype, doc.parent, df.get("options")) + if not currency: + # Parent may not be in DB yet (new document being saved). + # Use the in-memory parent document reference if available. + parent = getattr(doc, "parent_doc", None) + if parent: + currency = parent.get(df.get("options")) if currency: frappe.local.field_currency.setdefault((doc.doctype, ref_docname), frappe._dict()).setdefault( diff --git a/frappe/public/js/frappe/model/meta.js b/frappe/public/js/frappe/model/meta.js index d393f33cb7..7692dcfdfc 100644 --- a/frappe/public/js/frappe/model/meta.js +++ b/frappe/public/js/frappe/model/meta.js @@ -332,7 +332,8 @@ $.extend(frappe.meta, { } else if (df && df.fieldtype === "Currency") { precision = cint(frappe.defaults.get_default("currency_precision")); if (!precision) { - var number_format = get_number_format(); + var currency = frappe.meta.get_field_currency(df, doc); + var number_format = get_number_format(currency); var number_format_info = get_number_format_info(number_format); precision = number_format_info.precision; } From 72237e20b6680b865793080cebc994bf3e01807a Mon Sep 17 00:00:00 2001 From: Abdul Mannan Shaikh Date: Tue, 24 Feb 2026 07:09:51 +0000 Subject: [PATCH 13/89] fix(file): relink Attach fields in child tables during document rename --- frappe/core/doctype/file/utils.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/frappe/core/doctype/file/utils.py b/frappe/core/doctype/file/utils.py index 67abd82879..9d2a797ae8 100644 --- a/frappe/core/doctype/file/utils.py +++ b/frappe/core/doctype/file/utils.py @@ -427,6 +427,29 @@ def relink_mismatched_files(doc: "Document") -> None: for df in attach_fields: if doc.get(df.fieldname): relink_files(doc, df.fieldname, doc.__temporary_name) + + # Relink files in child table Attach fields + table_fields = doc.meta.get("fields", {"fieldtype": "Table"}) + for table_df in table_fields: + child_rows = doc.get(table_df.fieldname) or [] + if not child_rows: + continue + + child_meta = frappe.get_meta(table_df.options) + child_attach_fields = child_meta.get("fields", {"fieldtype": ["in", ["Attach", "Attach Image"]]}) + + if not child_attach_fields: + continue + + for child_row in child_rows: + for child_df in child_attach_fields: + file_url = child_row.get(child_df.fieldname) + if file_url: + frappe.db.set_value( + "File", + {"file_url": file_url, "attached_to_name": doc.__temporary_name}, + {"attached_to_name": doc.name}, + ) # delete temporary name after relinking is done doc.delete_key("__temporary_name") From 0580bb80feb61946e9c719906cc0e88fce866a0f Mon Sep 17 00:00:00 2001 From: Aditya Patil Date: Tue, 24 Feb 2026 12:44:52 +0530 Subject: [PATCH 14/89] fix: pre-commit --- frappe/public/js/frappe/form/linked_with.js | 26 +++++++++++++-------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/frappe/public/js/frappe/form/linked_with.js b/frappe/public/js/frappe/form/linked_with.js index 034202da1e..2aaa3dcb95 100644 --- a/frappe/public/js/frappe/form/linked_with.js +++ b/frappe/public/js/frappe/form/linked_with.js @@ -50,23 +50,29 @@ frappe.ui.form.LinkedWith = class LinkedWith { } else { html = `
- ${__("Following documents are linked with {0}", [frappe.utils.get_form_link(this.frm.doctype, this.frm.docname, true).bold()])} + ${__("Following documents are linked with {0}", [ + frappe.utils + .get_form_link(this.frm.doctype, this.frm.docname, true) + .bold(), + ])}
${linked_doctypes - .map((doctype) => { - const { docs, hidden_count } = linked_docs[doctype]; - let rows = (docs || []).map((doc) => this.make_doc_row(doc, doctype)).join(""); - if (hidden_count > 0) { - rows += this.make_hidden_count_row(hidden_count); - } - return ` + .map((doctype) => { + const { docs, hidden_count } = linked_docs[doctype]; + let rows = (docs || []) + .map((doc) => this.make_doc_row(doc, doctype)) + .join(""); + if (hidden_count > 0) { + rows += this.make_hidden_count_row(hidden_count); + } + return `
${this.make_doc_head(doctype)} ${rows}
`; - }) - .join("")} + }) + .join("")} `; } From a0b201d9a0de052f721d3def20f0b6b9280e6cb7 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 24 Feb 2026 15:19:50 +0530 Subject: [PATCH 15/89] fix: only check default keywords for valid fields --- frappe/public/js/frappe/ui/field_group.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index 8c09a96858..e482e32159 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -19,7 +19,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } resolve_date_default_keywords(def_value, fieldtype) { - if (!def_value) return def_value; + if (!def_value || typeof def_value !== "string") return def_value; def_value = def_value.toLowerCase(); @@ -54,7 +54,9 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { ) return; - def_value = me.resolve_date_default_keywords(def_value, field.df.fieldtype); + if (["Date", "Datetime", "Time"].includes(field.df.fieldtype)) { + def_value = me.resolve_date_default_keywords(def_value, field.df.fieldtype); + } field.set_input(def_value); // if default and has depends_on, render its fields. From 48f4ca2f7a8e40da0c407bd8d306bc2c4a34dd87 Mon Sep 17 00:00:00 2001 From: Akash Tom Date: Tue, 24 Feb 2026 17:28:54 +0530 Subject: [PATCH 16/89] refactor(Number Card): allow selection of fields in dynamic filter table --- .../desk/doctype/number_card/number_card.js | 269 +++++++++++++++--- 1 file changed, 225 insertions(+), 44 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index c5621fe6a4..160fc2a979 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -219,7 +219,9 @@ frappe.ui.form.on("Number Card", { `).appendTo(wrapper); if (frm.has_perm("write")) { - $(`

${__("Click table to edit")}

`).appendTo(wrapper); + $(`

${__("Click table to edit")}

`).appendTo( + wrapper + ); } let filters = JSON.parse(frm.doc.filters_json || "[]"); @@ -304,7 +306,7 @@ frappe.ui.form.on("Number Card", { frm.trigger("render_filters_table"); } }, - primary_action_label: __("Set"), + primary_action_label: __("Update"), }); if (is_document_type) { @@ -340,8 +342,6 @@ frappe.ui.form.on("Number Card", { frm.set_df_property("dynamic_filters_section", "hidden", 0); - let is_document_type = frm.doc.type == "Document Type"; - let wrapper = $(frm.get_field("dynamic_filters_json").wrapper).empty(); frm.dynamic_filter_table = $(` - - - + +
${__("Filter")}${__("Condition")}${__("Value")}${__("Filter")}${__("Expression")}
`).appendTo(wrapper); + if (frm.has_perm("write")) { + $(`

${__("Click table to edit")}

`).appendTo( + wrapper + ); + } + frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 ? JSON.parse(frm.doc.dynamic_filters_json) @@ -364,14 +369,6 @@ frappe.ui.form.on("Number Card", { frm.trigger("set_dynamic_filters_in_table"); - let filters = JSON.parse(frm.doc.filters_json || "[]"); - - let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog( - is_document_type, - filters, - frm.dynamic_filters - ); - frm.dynamic_filter_table.on("click", () => { if (!frm.has_perm("write")) { return; @@ -380,33 +377,220 @@ frappe.ui.form.on("Number Card", { if (!frappe.boot.developer_mode && frm.doc.is_standard) { frappe.throw(__("Cannot edit filters for standard number cards")); } - let dialog = new frappe.ui.Dialog({ - title: __("Set Dynamic Filters"), - fields: fields, - primary_action: () => { - let values = dialog.get_values(); - dialog.hide(); - let dynamic_filters = []; - for (let key of Object.keys(values)) { - if (is_document_type) { - let [doctype, fieldname] = key.split(":"); - dynamic_filters.push([doctype, fieldname, "=", values[key]]); - } - } - if (is_document_type) { - frm.set_value("dynamic_filters_json", JSON.stringify(dynamic_filters)); - } else { - frm.set_value("dynamic_filters_json", JSON.stringify(values)); - } - frm.trigger("set_dynamic_filters_in_table"); + frm.trigger("show_dynamic_filter_dialog"); + }); + }, + + show_dynamic_filter_dialog: function (frm) { + if (frm.doc.type === "Document Type") { + if (!frm.doc.document_type) { + frappe.msgprint(__("Please select a Document Type first")); + return; + } + frappe.model.with_doctype(frm.doc.document_type, () => { + frm.trigger("show_doctype_dynamic_filter_dialog"); + }); + return; + } + + if (!frm.doc.report_name) { + frappe.msgprint(__("Please select a Report first")); + return; + } + if (!frm.filters?.length) { + frappe.msgprint(__("No filters available for this report")); + return; + } + frm.trigger("show_report_dynamic_filter_dialog"); + }, + + show_doctype_dynamic_filter_dialog: function (frm) { + const dynamic_filters = + frm.doc.dynamic_filters_json?.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : []; + + const meta = frappe.get_meta(frm.doc.document_type); + const field_options = meta.fields + .filter((df) => df.fieldname && !frappe.model.no_value_type.includes(df.fieldtype)) + .map((df) => ({ label: df.label || df.fieldname, value: df.fieldname })); + + frappe.model.std_fields.forEach((df) => { + field_options.push({ label: df.label, value: df.fieldname }); + }); + + const dialog = new frappe.ui.Dialog({ + title: __("Set Dynamic Filters"), + fields: [ + { + fieldtype: "HTML", + fieldname: "help_text", + options: frm.events.get_dynamic_filter_help_text(), }, - primary_action_label: __("Set"), + { fieldtype: "HTML", fieldname: "filter_area" }, + ], + size: "large", + primary_action: () => { + const filters = []; + dialog.$wrapper.find(".dynamic-filter-row").each(function () { + const field = $(this).find(".filter-field").val(); + const expression = $(this).find(".filter-expression").val(); + if (field && expression) { + filters.push([frm.doc.document_type, field, "=", expression]); + } + }); + dialog.hide(); + frm.set_value("dynamic_filters_json", JSON.stringify(filters)); + frm.trigger("set_dynamic_filters_in_table"); + }, + primary_action_label: __("Update"), + }); + + const add_filter_row = frm.events.build_dynamic_filter_interface( + dialog.fields_dict.filter_area.$wrapper, + field_options, + "Field" + ); + + if (dynamic_filters?.length) { + dynamic_filters.forEach((filter) => add_filter_row(filter[1], filter[3])); + } else { + add_filter_row(); + } + + dialog.show(); + }, + + show_report_dynamic_filter_dialog: function (frm) { + const dynamic_filters = + frm.doc.dynamic_filters_json?.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : {}; + + const field_options = frm.filters + .filter((f) => f.fieldname) + .map((f) => ({ label: f.label || f.fieldname, value: f.fieldname })); + + const dialog = new frappe.ui.Dialog({ + title: __("Set Dynamic Filters"), + fields: [ + { + fieldtype: "HTML", + fieldname: "help_text", + options: frm.events.get_dynamic_filter_help_text(), + }, + { fieldtype: "HTML", fieldname: "filter_area" }, + ], + size: "large", + primary_action: () => { + const filters = {}; + dialog.$wrapper.find(".dynamic-filter-row").each(function () { + const field = $(this).find(".filter-field").val(); + const expression = $(this).find(".filter-expression").val(); + if (field && expression) { + filters[field] = expression; + } + }); + dialog.hide(); + frm.set_value("dynamic_filters_json", JSON.stringify(filters)); + frm.trigger("set_dynamic_filters_in_table"); + }, + primary_action_label: __("Update"), + }); + + const add_filter_row = frm.events.build_dynamic_filter_interface( + dialog.fields_dict.filter_area.$wrapper, + field_options, + "Filter" + ); + + if (dynamic_filters && Object.keys(dynamic_filters).length) { + Object.entries(dynamic_filters).forEach(([field, expression]) => + add_filter_row(field, expression) + ); + } else { + add_filter_row(); + } + + dialog.show(); + }, + + get_dynamic_filter_help_text: function () { + return `

+ ${__("Enter expressions that will be evaluated when the card is displayed. For example:")}
+ frappe.defaults.get_user_default("Company")
+ frappe.datetime.get_today()
+

`; + }, + + build_dynamic_filter_interface: function ($filter_area, field_options, field_label) { + const filter_html = ` +
+ + + + + + + + + +
${__(field_label)}${__("Expression")}
+
+ + +
+
+ `; + + $filter_area.html(filter_html); + + const add_filter_row = (field = "", expression = "") => { + let options_html = ''; + field_options.forEach((opt) => { + const selected = opt.value === field ? "selected" : ""; + options_html += ``; }); - dialog.show(); - dialog.set_values(frm.dynamic_filters); + const row_html = ` + + + + + + + + + + + + + + + + `; + + $filter_area.find(".filter-rows").append(row_html); + }; + + $filter_area.on("click", ".add-filter", () => add_filter_row()); + $filter_area.on("click", ".remove-filter", function () { + $(this).closest("tr").remove(); }); + $filter_area.on("click", ".clear-filters", () => { + $filter_area.find(".filter-rows").empty(); + add_filter_row(); + }); + + return add_filter_row; }, set_dynamic_filters_in_table: function (frm) { @@ -415,8 +599,8 @@ frappe.ui.form.on("Number Card", { ? JSON.parse(frm.doc.dynamic_filters_json) : null; - if (!frm.dynamic_filters) { - const filter_row = $(` + if (!frm.dynamic_filters || Object.keys(frm.dynamic_filters).length === 0) { + const filter_row = $(` ${__("Click to Set Dynamic Filters")}`); frm.dynamic_filter_table.find("tbody").html(filter_row); } else { @@ -425,17 +609,14 @@ frappe.ui.form.on("Number Card", { frm.dynamic_filters.forEach((filter) => { filter_rows += ` ${filter[1]} - ${filter[2] || ""} - ${filter[3]} + ${filter[3]} `; }); } else { - let condition = "="; for (let [key, val] of Object.entries(frm.dynamic_filters)) { filter_rows += ` ${key} - ${condition} - ${val || ""} + ${val || ""} `; } } From 2f5015c5d15dc8de4e2fcc5defc7a79e3019c533 Mon Sep 17 00:00:00 2001 From: Akash Tom Date: Tue, 24 Feb 2026 17:48:57 +0530 Subject: [PATCH 17/89] refactor: clean up deprecated code and eliminate duplication --- .../desk/doctype/number_card/number_card.js | 101 ++++-------------- 1 file changed, 19 insertions(+), 82 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index 160fc2a979..ddfbc89287 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -202,7 +202,6 @@ frappe.ui.form.on("Number Card", { render_filters_table: function (frm) { frm.set_df_property("filters_section", "hidden", 0); let is_document_type = frm.doc.type == "Document Type"; - let is_dynamic_filter = (f) => ["Date", "DateRange"].includes(f.fieldtype) && f.default; let wrapper = $(frm.get_field("filters_json").wrapper).empty(); let table = $(` { - if (is_dynamic_filter(f)) { - filters[f.fieldname] = f.default; - set_filters = true; - } - }); - set_filters && frm.set_value("filters_json", JSON.stringify(filters)); - } - let fields = []; if (is_document_type) { fields = [ @@ -292,7 +279,7 @@ frappe.ui.form.on("Number Card", { } let dialog = new frappe.ui.Dialog({ title: __("Set Filters"), - fields: fields.filter((f) => !is_dynamic_filter(f)), + fields, primary_action: function () { let values = this.get_values(); if (values) { @@ -406,11 +393,6 @@ frappe.ui.form.on("Number Card", { }, show_doctype_dynamic_filter_dialog: function (frm) { - const dynamic_filters = - frm.doc.dynamic_filters_json?.length > 2 - ? JSON.parse(frm.doc.dynamic_filters_json) - : []; - const meta = frappe.get_meta(frm.doc.document_type); const field_options = meta.fields .filter((df) => df.fieldname && !frappe.model.no_value_type.includes(df.fieldtype)) @@ -420,58 +402,23 @@ frappe.ui.form.on("Number Card", { field_options.push({ label: df.label, value: df.fieldname }); }); - const dialog = new frappe.ui.Dialog({ - title: __("Set Dynamic Filters"), - fields: [ - { - fieldtype: "HTML", - fieldname: "help_text", - options: frm.events.get_dynamic_filter_help_text(), - }, - { fieldtype: "HTML", fieldname: "filter_area" }, - ], - size: "large", - primary_action: () => { - const filters = []; - dialog.$wrapper.find(".dynamic-filter-row").each(function () { - const field = $(this).find(".filter-field").val(); - const expression = $(this).find(".filter-expression").val(); - if (field && expression) { - filters.push([frm.doc.document_type, field, "=", expression]); - } - }); - dialog.hide(); - frm.set_value("dynamic_filters_json", JSON.stringify(filters)); - frm.trigger("set_dynamic_filters_in_table"); - }, - primary_action_label: __("Update"), - }); - - const add_filter_row = frm.events.build_dynamic_filter_interface( - dialog.fields_dict.filter_area.$wrapper, - field_options, - "Field" - ); - - if (dynamic_filters?.length) { - dynamic_filters.forEach((filter) => add_filter_row(filter[1], filter[3])); - } else { - add_filter_row(); - } - - dialog.show(); + frm.events.show_dynamic_filter_dialog_common(frm, field_options); }, show_report_dynamic_filter_dialog: function (frm) { + const field_options = frm.filters + .filter((f) => f.fieldname) + .map((f) => ({ label: f.label || f.fieldname, value: f.fieldname })); + + frm.events.show_dynamic_filter_dialog_common(frm, field_options); + }, + + show_dynamic_filter_dialog_common: function (frm, field_options) { const dynamic_filters = frm.doc.dynamic_filters_json?.length > 2 ? JSON.parse(frm.doc.dynamic_filters_json) : {}; - const field_options = frm.filters - .filter((f) => f.fieldname) - .map((f) => ({ label: f.label || f.fieldname, value: f.fieldname })); - const dialog = new frappe.ui.Dialog({ title: __("Set Dynamic Filters"), fields: [ @@ -501,8 +448,7 @@ frappe.ui.form.on("Number Card", { const add_filter_row = frm.events.build_dynamic_filter_interface( dialog.fields_dict.filter_area.$wrapper, - field_options, - "Filter" + field_options ); if (dynamic_filters && Object.keys(dynamic_filters).length) { @@ -524,13 +470,13 @@ frappe.ui.form.on("Number Card", {

`; }, - build_dynamic_filter_interface: function ($filter_area, field_options, field_label) { + build_dynamic_filter_interface: function ($filter_area, field_options) { const filter_html = `
- + @@ -595,7 +541,7 @@ frappe.ui.form.on("Number Card", { set_dynamic_filters_in_table: function (frm) { frm.dynamic_filters = - frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + frm.doc.dynamic_filters_json?.length > 2 ? JSON.parse(frm.doc.dynamic_filters_json) : null; @@ -605,20 +551,11 @@ frappe.ui.form.on("Number Card", { frm.dynamic_filter_table.find("tbody").html(filter_row); } else { let filter_rows = ""; - if ($.isArray(frm.dynamic_filters)) { - frm.dynamic_filters.forEach((filter) => { - filter_rows += ` - - - `; - }); - } else { - for (let [key, val] of Object.entries(frm.dynamic_filters)) { - filter_rows += ` - - - `; - } + for (let [key, val] of Object.entries(frm.dynamic_filters)) { + filter_rows += ` + + + `; } frm.dynamic_filter_table.find("tbody").html(filter_rows); From 621ba403661e6148d0d6bdad75772c5e41125ece Mon Sep 17 00:00:00 2001 From: Aditya Patil Date: Tue, 24 Feb 2026 17:56:52 +0530 Subject: [PATCH 18/89] feat: loading state and spinner for dialog primary button --- .../js/frappe/file_uploader/FileUploader.vue | 19 ++---------- .../file_uploader/file_uploader.bundle.js | 1 + .../js/frappe/form/sidebar/assign_to.js | 7 +---- frappe/public/js/frappe/list/list_filter.js | 4 +-- frappe/public/js/frappe/ui/dialog.js | 31 ++++++++++++++++++- .../public/js/frappe/utils/dashboard_utils.js | 5 ++- .../js/frappe/views/reports/query_report.js | 2 +- frappe/public/scss/common/modal.scss | 5 +++ 8 files changed, 44 insertions(+), 30 deletions(-) diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue index 10012c82ad..f21d925efe 100644 --- a/frappe/public/js/frappe/file_uploader/FileUploader.vue +++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue @@ -514,22 +514,7 @@ function check_restrictions(file) { return is_correct_type && valid_file_size; } -function set_loading_state(dialog, loading) { - let $btn = dialog?.get_primary_btn(); - if (loading) { - $btn?.css("width", $btn.outerWidth()); - $btn?.html(``); - $btn?.prop("disabled", true); - dialog?.get_secondary_btn().prop("disabled", true); - } else { - $btn?.css("width", ""); - $btn?.html(__("Upload")); - $btn?.prop("disabled", false); - dialog?.get_secondary_btn().prop("disabled", false); - } -} -function upload_files(dialog) { - set_loading_state(dialog, true); +function upload_files() { if (show_file_browser.value) { promise = upload_via_file_browser(); } else if (show_web_link.value) { @@ -542,7 +527,7 @@ function upload_files(dialog) { } else { promise = frappe.run_serially(files.value.map((file, i) => () => upload_file(file, i))); } - return promise.finally(() => set_loading_state(dialog, false)); + return promise; } function upload_via_file_browser() { let selected_file = file_browser.value.selected_node; diff --git a/frappe/public/js/frappe/file_uploader/file_uploader.bundle.js b/frappe/public/js/frappe/file_uploader/file_uploader.bundle.js index 44e9d9add6..999cc675ea 100644 --- a/frappe/public/js/frappe/file_uploader/file_uploader.bundle.js +++ b/frappe/public/js/frappe/file_uploader/file_uploader.bundle.js @@ -151,6 +151,7 @@ class FileUploader { const dialog_opts = { title: title || __("Upload"), primary_action_label: __("Upload"), + primary_action_loading_label: __("Uploading"), primary_action: () => this.upload_files(), on_page_show: () => { this.uploader.wrapper_ready = true; diff --git a/frappe/public/js/frappe/form/sidebar/assign_to.js b/frappe/public/js/frappe/form/sidebar/assign_to.js index ce4e0ca995..1e9b1ee195 100644 --- a/frappe/public/js/frappe/form/sidebar/assign_to.js +++ b/frappe/public/js/frappe/form/sidebar/assign_to.js @@ -114,9 +114,7 @@ frappe.ui.form.AssignToDialog = class AssignToDialog { let args = me.dialog.get_values(); if (args && args.assign_to) { - me.dialog.set_message("Assigning..."); - - frappe.call({ + return frappe.call({ method: me.method, args: $.extend(args, { doctype: me.doctype, @@ -125,15 +123,12 @@ frappe.ui.form.AssignToDialog = class AssignToDialog { bulk_assign: me.bulk_assign || false, re_assign: me.re_assign || false, }), - btn: me.dialog.get_primary_btn(), callback: function (r) { if (!r.exc) { if (me.callback) { me.callback(r); } me.dialog && me.dialog.hide(); - } else { - me.dialog.clear_message(); } }, }); diff --git a/frappe/public/js/frappe/list/list_filter.js b/frappe/public/js/frappe/list/list_filter.js index e46c59eca9..f3ff15587b 100644 --- a/frappe/public/js/frappe/list/list_filter.js +++ b/frappe/public/js/frappe/list/list_filter.js @@ -120,7 +120,7 @@ export default class ListFilter { fields: fields, primary_action_label: __("Create"), primary_action: (values) => { - this.bind_save_filter(dialog, values.filter_name, values?.is_global); + return this.bind_save_filter(dialog, values.filter_name, values?.is_global); }, }); dialog.show(); @@ -138,7 +138,7 @@ export default class ListFilter { dialog.fields_dict.filter_name.set_description(__("Duplicate Filter Name")); return; } - this.save_filter(value, is_global).then(() => { + return this.save_filter(value, is_global).then(() => { this.refresh_list_filter(); dialog.hide(); }); diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js index 714edade37..8d2e978cf6 100644 --- a/frappe/public/js/frappe/ui/dialog.js +++ b/frappe/public/js/frappe/ui/dialog.js @@ -199,6 +199,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { this.has_primary_action = true; var me = this; const primary_btn = this.get_primary_btn().removeClass("hide").html(label); + const spinner = ``; if (typeof click == "function") { primary_btn.off("click").on("click", function () { me.primary_action_fulfilled = true; @@ -207,7 +208,35 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { // if no values then return var values = me.get_values(); if (!values) return; - click && click.apply(me, [values]); + const action = click.apply(me, [values]); + if (action && typeof action.then === "function") { + const loading_label = me.primary_action_loading_label; + primary_btn + .css({ + "min-width": primary_btn.outerWidth(), + "min-height": primary_btn.outerHeight(), + }) + .prop("disabled", true) + .addClass("btn-primary-dark") + .html( + `
+ ${spinner} + ${ + loading_label + ? `${loading_label}` + : "" + } +
` + ); + + Promise.resolve(action).finally(() => { + primary_btn + .css({ "min-width": "", "min-height": "" }) + .prop("disabled", false) + .removeClass("btn-primary-dark") + .html(label); + }); + } }); } return primary_btn; diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js index 46059db89e..991b9da65b 100644 --- a/frappe/public/js/frappe/utils/dashboard_utils.js +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -264,7 +264,7 @@ frappe.dashboard_utils = { primary_action: (values) => { values.name = docname; values.set_standard = frappe.boot.developer_mode; - frappe.xcall(method, { args: values }).then(() => { + return frappe.xcall(method, { args: values }).then(() => { let dashboard_route_html = `${values.dashboard}`; let message = __("{0} {1} added to Dashboard {2}", [ doctype, @@ -273,9 +273,8 @@ frappe.dashboard_utils = { ]); frappe.msgprint(message); + dialog.hide(); }); - - dialog.hide(); }, }); diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 8693fce7fb..f790aea680 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -2109,7 +2109,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { }, ], primary_action: (values) => { - frappe.call({ + return frappe.call({ method: "frappe.desk.query_report.save_report", args: { reference_report: this.report_name, diff --git a/frappe/public/scss/common/modal.scss b/frappe/public/scss/common/modal.scss index 3cb388a3fd..214688b4a7 100644 --- a/frappe/public/scss/common/modal.scss +++ b/frappe/public/scss/common/modal.scss @@ -103,6 +103,11 @@ body.modal-open[style^="padding-right"] { button:not(:last-child) { margin-right: var(--margin-xs); } + + .btn-primary-dark { + min-width: 80px; + max-width: 200px; + } } & > * { From 1b03eb1e078a5d59812f1222a69f6bb2e651a334 Mon Sep 17 00:00:00 2001 From: Akash Tom Date: Wed, 25 Feb 2026 13:28:39 +0530 Subject: [PATCH 19/89] fix: use same object-oriented format for dynamic filters everywhere --- .../desk/doctype/number_card/number_card.js | 38 +++++++++++++------ .../public/js/frappe/utils/dashboard_utils.js | 31 +++++++++------ 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index ddfbc89287..41f7d6a84f 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -349,11 +349,6 @@ frappe.ui.form.on("Number Card", { ); } - frm.dynamic_filters = - frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 - ? JSON.parse(frm.doc.dynamic_filters_json) - : null; - frm.trigger("set_dynamic_filters_in_table"); frm.dynamic_filter_table.on("click", () => { @@ -413,12 +408,28 @@ frappe.ui.form.on("Number Card", { frm.events.show_dynamic_filter_dialog_common(frm, field_options); }, + convert_legacy_dynamic_filters: function (dynamic_filters) { + if (Array.isArray(dynamic_filters)) { + const converted = {}; + dynamic_filters.forEach((filter) => { + if (filter.length >= 4) { + // Old format: [doctype, fieldname, operator, expression] + converted[filter[1]] = filter[3]; + } + }); + return converted; + } + return dynamic_filters; + }, + show_dynamic_filter_dialog_common: function (frm, field_options) { - const dynamic_filters = + let dynamic_filters = frm.doc.dynamic_filters_json?.length > 2 ? JSON.parse(frm.doc.dynamic_filters_json) : {}; + dynamic_filters = frm.events.convert_legacy_dynamic_filters(dynamic_filters); + const dialog = new frappe.ui.Dialog({ title: __("Set Dynamic Filters"), fields: [ @@ -511,8 +522,7 @@ frappe.ui.form.on("Number Card", {
`; - $filter_area.find(".filter-rows").append(row_html); + const $row = $(row_html); + $row.find(".filter-expression").val(expression); + $filter_area.find(".filter-rows").append($row); }; $filter_area.on("click", ".add-filter", () => add_filter_row()); @@ -540,18 +552,20 @@ frappe.ui.form.on("Number Card", { }, set_dynamic_filters_in_table: function (frm) { - frm.dynamic_filters = + let dynamic_filters = frm.doc.dynamic_filters_json?.length > 2 ? JSON.parse(frm.doc.dynamic_filters_json) : null; - if (!frm.dynamic_filters || Object.keys(frm.dynamic_filters).length === 0) { + dynamic_filters = frm.events.convert_legacy_dynamic_filters(dynamic_filters); + + if (!dynamic_filters || Object.keys(dynamic_filters).length === 0) { const filter_row = $(``); frm.dynamic_filter_table.find("tbody").html(filter_row); } else { let filter_rows = ""; - for (let [key, val] of Object.entries(frm.dynamic_filters)) { + for (let [key, val] of Object.entries(dynamic_filters)) { filter_rows += ` diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js index 46059db89e..33fbaf77b9 100644 --- a/frappe/public/js/frappe/utils/dashboard_utils.js +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -132,18 +132,27 @@ frappe.dashboard_utils = { remove_common_static_filter_values(static_filters, dynamic_filters) { if (dynamic_filters) { - if ($.isArray(static_filters)) { - static_filters = static_filters.filter((static_filter) => { - for (let dynamic_filter of dynamic_filters) { - if ( - static_filter[0] == dynamic_filter[0] && - static_filter[1] == dynamic_filter[1] - ) { - return false; + if (Array.isArray(static_filters)) { + if (Array.isArray(dynamic_filters)) { + static_filters = static_filters.filter((static_filter) => { + for (let dynamic_filter of dynamic_filters) { + if ( + static_filter[0] == dynamic_filter[0] && + static_filter[1] == dynamic_filter[1] + ) { + return false; + } } - } - return true; - }); + return true; + }); + } else { + static_filters = static_filters.filter((static_filter) => { + return !Object.prototype.hasOwnProperty.call( + dynamic_filters, + static_filter[1] + ); + }); + } } else { for (let key of Object.keys(dynamic_filters)) { delete static_filters[key]; From 332cad5b1815f6cd0236c2f3170c3428ad11b967 Mon Sep 17 00:00:00 2001 From: Aditya Patil Date: Wed, 25 Feb 2026 14:01:26 +0530 Subject: [PATCH 20/89] refactor: added `return` to pass the thenable back to primary_action in all the dialogs with async calls --- .../doctype/communication/communication.js | 4 ++-- frappe/core/doctype/doctype/doctype_list.js | 2 +- frappe/core/doctype/user/user.js | 19 ++++++++++--------- .../database_storage_usage_by_tables.js | 4 ++-- frappe/public/js/frappe/form/reminders.js | 5 ++--- .../public/js/frappe/list/bulk_operations.js | 4 +--- .../autocomplete_dialog.js | 6 ++---- frappe/public/js/frappe/ui/messages.js | 2 +- .../frappe/views/dashboard/dashboard_view.js | 5 +++-- frappe/public/js/frappe/views/interaction.js | 2 +- .../website_slideshow/website_slideshow.js | 2 +- 11 files changed, 26 insertions(+), 29 deletions(-) diff --git a/frappe/core/doctype/communication/communication.js b/frappe/core/doctype/communication/communication.js index 86ef59f994..d6103636e8 100644 --- a/frappe/core/doctype/communication/communication.js +++ b/frappe/core/doctype/communication/communication.js @@ -211,8 +211,7 @@ frappe.ui.form.on("Communication", { ], primary_action_label: __("Move"), primary_action(values) { - d.hide(); - frappe.call({ + return frappe.call({ method: "frappe.email.inbox.move_email", args: { communication: frm.doc.name, @@ -220,6 +219,7 @@ frappe.ui.form.on("Communication", { }, freeze: true, callback: function () { + d.hide(); window.history.back(); }, }); diff --git a/frappe/core/doctype/doctype/doctype_list.js b/frappe/core/doctype/doctype/doctype_list.js index 46b5e5b99d..3bb353c266 100644 --- a/frappe/core/doctype/doctype/doctype_list.js +++ b/frappe/core/doctype/doctype/doctype_list.js @@ -103,7 +103,7 @@ frappe.listview_settings["DocType"] = { primary_action_label: __("Create & Continue"), primary_action(values) { if (!values.istable) values.editable_grid = 0; - frappe.db + return frappe.db .insert({ doctype: "DocType", ...values, diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 4be1cfadec..3e9ff32d5e 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -201,18 +201,19 @@ frappe.ui.form.on("User", { }, ], primary_action: (values) => { - d.hide(); if (values.new_password !== values.confirm_password) { frappe.throw(__("Passwords do not match!")); } - frappe.call( - "frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", - { - user: frm.doc.email, - password: values.new_password, - logout: values.logout_sessions, - } - ); + return frappe + .call( + "frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", + { + user: frm.doc.email, + password: values.new_password, + logout: values.logout_sessions, + } + ) + .then(() => d.hide()); }, }); d.show(); 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 5a1ae6f87a..093016705e 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 @@ -25,7 +25,7 @@ frappe.query_reports["Database Storage Usage By Tables"] = { size: "small", primary_action_label: "Optimize", primary_action(values) { - frappe.call({ + return frappe.call({ method: "frappe.core.report.database_storage_usage_by_tables.database_storage_usage_by_tables.optimize_doctype", args: { doctype_name: values.doctype_name, @@ -38,9 +38,9 @@ frappe.query_reports["Database Storage Usage By Tables"] = { ) ); } + d.hide(); }, }); - d.hide(); }, }); d.show(); diff --git a/frappe/public/js/frappe/form/reminders.js b/frappe/public/js/frappe/form/reminders.js index 5c8fa16060..667ca805d6 100644 --- a/frappe/public/js/frappe/form/reminders.js +++ b/frappe/public/js/frappe/form/reminders.js @@ -48,8 +48,7 @@ export class ReminderManager { ], primary_action_label: __("Create"), primary_action: () => { - this.create_reminder(); - this.dialog.hide(); + return this.create_reminder().then(() => this.dialog.hide()); }, secondary_action_label: __("Cancel"), secondary_action: () => { @@ -84,7 +83,7 @@ export class ReminderManager { } create_reminder() { - frappe + return frappe .xcall("frappe.automation.doctype.reminder.reminder.create_new_reminder", { remind_at: this.dialog.get_value("remind_at"), description: this.dialog.get_value("description"), diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index dd85c3da0d..0b794a03c3 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -452,9 +452,7 @@ export default class BulkOperations { primary_action: () => { let args = dialog.get_values(); if (args && args.tags) { - dialog.set_message("Adding Tags..."); - - frappe.call({ + return frappe.call({ method: "frappe.desk.doctype.tag.tag.add_tags", args: { tags: args.tags, diff --git a/frappe/public/js/frappe/ui/address_autocomplete/autocomplete_dialog.js b/frappe/public/js/frappe/ui/address_autocomplete/autocomplete_dialog.js index 53d8405ceb..3fb854c15d 100644 --- a/frappe/public/js/frappe/ui/address_autocomplete/autocomplete_dialog.js +++ b/frappe/public/js/frappe/ui/address_autocomplete/autocomplete_dialog.js @@ -48,9 +48,6 @@ frappe.ui.AddressAutocompleteDialog = class AddressAutocompleteDialog { ], primary_action_label: __("Create Address"), primary_action: () => { - // Insert the address into the database - dialog.hide(); - const address = this.parse_selected_value(); address["doctype"] = "Address"; address["links"] = [ @@ -59,7 +56,8 @@ frappe.ui.AddressAutocompleteDialog = class AddressAutocompleteDialog { link_name: this.link_name, }, ]; - frappe.db.insert(address).then((doc) => { + return frappe.db.insert(address).then((doc) => { + dialog.hide(); this.after_insert && this.after_insert(doc); }); }, diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index 1e4661f76d..a7a7a2dd82 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -206,7 +206,7 @@ frappe.msgprint = function (msg, title, is_minimizable, re_route) { typeof data.primary_action.server_action === "string" ) { data.primary_action.action = () => { - frappe.call({ + return frappe.call({ method: data.primary_action.server_action, args: data.primary_action.args, callback() { diff --git a/frappe/public/js/frappe/views/dashboard/dashboard_view.js b/frappe/public/js/frappe/views/dashboard/dashboard_view.js index 2adaee49fb..9011902091 100644 --- a/frappe/public/js/frappe/views/dashboard/dashboard_view.js +++ b/frappe/public/js/frappe/views/dashboard/dashboard_view.js @@ -449,7 +449,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView { : chart.chart_type; chart.document_type = this.doctype; chart.filters_json = "[]"; - frappe + return frappe .xcall( "frappe.desk.doctype.dashboard_chart.dashboard_chart.create_dashboard_chart", { args: chart } @@ -460,6 +460,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView { name: doc.chart_name, label: chart.label, }); + dialog.hide(); }); } else { this.chart_group.new_widget.on_create({ @@ -467,8 +468,8 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView { label: __(chart.chart), name: chart.chart, }); + dialog.hide(); } - dialog.hide(); }, }); dialog.show(); diff --git a/frappe/public/js/frappe/views/interaction.js b/frappe/public/js/frappe/views/interaction.js index ba4ebe63d9..b6d6f80cfc 100644 --- a/frappe/public/js/frappe/views/interaction.js +++ b/frappe/public/js/frappe/views/interaction.js @@ -17,7 +17,7 @@ frappe.views.InteractionComposer = class InteractionComposer { fields: me.get_fields(), primary_action_label: __("Create"), primary_action: function () { - me.create_action(); + return me.create_action(); }, }); diff --git a/frappe/website/doctype/website_slideshow/website_slideshow.js b/frappe/website/doctype/website_slideshow/website_slideshow.js index 60a683eae8..7dea1370cb 100644 --- a/frappe/website/doctype/website_slideshow/website_slideshow.js +++ b/frappe/website/doctype/website_slideshow/website_slideshow.js @@ -31,7 +31,7 @@ frappe.ui.form.on("Website Slideshow", { ], primary_action_label: __("Add to table"), primary_action: ({ reference_doctype, reference_name }) => { - frappe.db + return frappe.db .get_list("File", { fields: ["file_url"], filters: { From 86154f0b227a699e77bb9617eb60ac0da2573323 Mon Sep 17 00:00:00 2001 From: Aditya Patil Date: Wed, 25 Feb 2026 14:26:32 +0530 Subject: [PATCH 21/89] fix: spinner styling --- frappe/public/js/frappe/ui/dialog.js | 2 +- frappe/public/scss/common/modal.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js index 8d2e978cf6..e544b48cc4 100644 --- a/frappe/public/js/frappe/ui/dialog.js +++ b/frappe/public/js/frappe/ui/dialog.js @@ -199,7 +199,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { this.has_primary_action = true; var me = this; const primary_btn = this.get_primary_btn().removeClass("hide").html(label); - const spinner = ``; + const spinner = ``; if (typeof click == "function") { primary_btn.off("click").on("click", function () { me.primary_action_fulfilled = true; diff --git a/frappe/public/scss/common/modal.scss b/frappe/public/scss/common/modal.scss index 214688b4a7..19f1c8dba5 100644 --- a/frappe/public/scss/common/modal.scss +++ b/frappe/public/scss/common/modal.scss @@ -2,6 +2,12 @@ h5.modal-title { margin: 0px !important; } +@keyframes spin { + to { + transform: rotate(360deg); + } +} + // Hack to fix incorrect padding applied by Bootstrap body.modal-open[style^="padding-right"] { padding-right: 12px !important; From 4956e34c9e40d661de10b60a1cf416e7653dd1ac Mon Sep 17 00:00:00 2001 From: Akash Tom Date: Wed, 25 Feb 2026 15:00:25 +0530 Subject: [PATCH 22/89] refactor: revert to using array based filtering --- .../desk/doctype/number_card/number_card.js | 50 +++++---------- .../public/js/frappe/utils/dashboard_utils.js | 62 ++++++++----------- 2 files changed, 41 insertions(+), 71 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index 41f7d6a84f..c9533c0a1d 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -397,7 +397,7 @@ frappe.ui.form.on("Number Card", { field_options.push({ label: df.label, value: df.fieldname }); }); - frm.events.show_dynamic_filter_dialog_common(frm, field_options); + frm.events.show_dynamic_filter_dialog_common(frm, field_options, frm.doc.document_type); }, show_report_dynamic_filter_dialog: function (frm) { @@ -405,30 +405,14 @@ frappe.ui.form.on("Number Card", { .filter((f) => f.fieldname) .map((f) => ({ label: f.label || f.fieldname, value: f.fieldname })); - frm.events.show_dynamic_filter_dialog_common(frm, field_options); + frm.events.show_dynamic_filter_dialog_common(frm, field_options, frm.doc.report_name); }, - convert_legacy_dynamic_filters: function (dynamic_filters) { - if (Array.isArray(dynamic_filters)) { - const converted = {}; - dynamic_filters.forEach((filter) => { - if (filter.length >= 4) { - // Old format: [doctype, fieldname, operator, expression] - converted[filter[1]] = filter[3]; - } - }); - return converted; - } - return dynamic_filters; - }, - - show_dynamic_filter_dialog_common: function (frm, field_options) { + show_dynamic_filter_dialog_common: function (frm, field_options, doctype_or_report) { let dynamic_filters = frm.doc.dynamic_filters_json?.length > 2 ? JSON.parse(frm.doc.dynamic_filters_json) - : {}; - - dynamic_filters = frm.events.convert_legacy_dynamic_filters(dynamic_filters); + : []; const dialog = new frappe.ui.Dialog({ title: __("Set Dynamic Filters"), @@ -442,12 +426,12 @@ frappe.ui.form.on("Number Card", { ], size: "large", primary_action: () => { - const filters = {}; + const filters = []; dialog.$wrapper.find(".dynamic-filter-row").each(function () { const field = $(this).find(".filter-field").val(); const expression = $(this).find(".filter-expression").val(); if (field && expression) { - filters[field] = expression; + filters.push([doctype_or_report, field, "=", expression]); } }); dialog.hide(); @@ -462,10 +446,10 @@ frappe.ui.form.on("Number Card", { field_options ); - if (dynamic_filters && Object.keys(dynamic_filters).length) { - Object.entries(dynamic_filters).forEach(([field, expression]) => - add_filter_row(field, expression) - ); + if (dynamic_filters?.length) { + dynamic_filters.forEach((filter) => { + add_filter_row(filter[1], filter[3]); + }); } else { add_filter_row(); } @@ -555,22 +539,20 @@ frappe.ui.form.on("Number Card", { let dynamic_filters = frm.doc.dynamic_filters_json?.length > 2 ? JSON.parse(frm.doc.dynamic_filters_json) - : null; + : []; - dynamic_filters = frm.events.convert_legacy_dynamic_filters(dynamic_filters); - - if (!dynamic_filters || Object.keys(dynamic_filters).length === 0) { + if (!dynamic_filters?.length) { const filter_row = $(``); frm.dynamic_filter_table.find("tbody").html(filter_row); } else { let filter_rows = ""; - for (let [key, val] of Object.entries(dynamic_filters)) { + dynamic_filters.forEach((filter) => { filter_rows += ` - - + + `; - } + }); frm.dynamic_filter_table.find("tbody").html(filter_rows); } diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js index 33fbaf77b9..3d425aae08 100644 --- a/frappe/public/js/frappe/utils/dashboard_utils.js +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -133,26 +133,17 @@ frappe.dashboard_utils = { remove_common_static_filter_values(static_filters, dynamic_filters) { if (dynamic_filters) { if (Array.isArray(static_filters)) { - if (Array.isArray(dynamic_filters)) { - static_filters = static_filters.filter((static_filter) => { - for (let dynamic_filter of dynamic_filters) { - if ( - static_filter[0] == dynamic_filter[0] && - static_filter[1] == dynamic_filter[1] - ) { - return false; - } + static_filters = static_filters.filter((static_filter) => { + for (let dynamic_filter of dynamic_filters) { + if ( + static_filter[0] == dynamic_filter[0] && + static_filter[1] == dynamic_filter[1] + ) { + return false; } - return true; - }); - } else { - static_filters = static_filters.filter((static_filter) => { - return !Object.prototype.hasOwnProperty.call( - dynamic_filters, - static_filter[1] - ); - }); - } + } + return true; + }); } else { for (let key of Object.keys(dynamic_filters)) { delete static_filters[key]; @@ -216,29 +207,26 @@ frappe.dashboard_utils = { ? JSON.parse(doc.dynamic_filters_json) : null; - if (!dynamic_filters || !Object.keys(dynamic_filters).length) { + if (!dynamic_filters?.length) { return filters; } - if (Array.isArray(dynamic_filters)) { - dynamic_filters.forEach((f) => { - try { - f[3] = eval(f[3]); - } catch (e) { - frappe.throw(__("Invalid expression set in filter {0} ({1})", [f[1], f[0]])); - } - }); + dynamic_filters.forEach((f) => { + try { + f[3] = eval(f[3]); + } catch (e) { + frappe.throw(__("Invalid expression set in filter {0} ({1})", [f[1], f[0]])); + } + }); + + if (!filters) { + filters = dynamic_filters; + } else if (Array.isArray(filters)) { filters = [...filters, ...dynamic_filters]; } else { - for (let key of Object.keys(dynamic_filters)) { - try { - const val = eval(dynamic_filters[key]); - dynamic_filters[key] = val; - } catch (e) { - frappe.throw(__("Invalid expression set in filter {0}", [key])); - } - } - Object.assign(filters, dynamic_filters); + dynamic_filters.forEach((f) => { + filters[f[1]] = f[3]; + }); } return filters; From d699aa2c145597d284371eeda3950f3db4a5e1ac Mon Sep 17 00:00:00 2001 From: Akash Tom Date: Wed, 25 Feb 2026 15:49:09 +0530 Subject: [PATCH 23/89] refactor: use fieldselect component instead of select element --- .../desk/doctype/number_card/number_card.js | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index c9533c0a1d..c69b96bf89 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -428,10 +428,11 @@ frappe.ui.form.on("Number Card", { primary_action: () => { const filters = []; dialog.$wrapper.find(".dynamic-filter-row").each(function () { - const field = $(this).find(".filter-field").val(); - const expression = $(this).find(".filter-expression").val(); - if (field && expression) { - filters.push([doctype_or_report, field, "=", expression]); + const $row = $(this); + const fieldname = $row.data("selected_fieldname"); + const expression = $row.find(".filter-expression").val(); + if (fieldname && expression) { + filters.push([doctype_or_report, fieldname, "=", expression]); } }); dialog.hide(); @@ -443,7 +444,8 @@ frappe.ui.form.on("Number Card", { const add_filter_row = frm.events.build_dynamic_filter_interface( dialog.fields_dict.filter_area.$wrapper, - field_options + field_options, + doctype_or_report ); if (dynamic_filters?.length) { @@ -465,7 +467,7 @@ frappe.ui.form.on("Number Card", {

`; }, - build_dynamic_filter_interface: function ($filter_area, field_options) { + build_dynamic_filter_interface: function ($filter_area, field_options, doctype_or_report) { const filter_html = `
${__(field_label)}${__("Field")} ${__("Expression")}
${filter[1]}${filter[3]}
${key}${val || ""}
${key}${val || ""}
- + @@ -524,7 +534,9 @@ frappe.ui.form.on("Number Card", {
${__("Click to Set Dynamic Filters")}
${key} ${val || ""}
${__("Click to Set Dynamic Filters")}
${key}${val || ""}${filter[1]}${filter[3] || ""}
@@ -491,20 +493,16 @@ frappe.ui.form.on("Number Card", { $filter_area.html(filter_html); - const add_filter_row = (field = "", expression = "") => { - let options_html = ''; - field_options.forEach((opt) => { - const selected = opt.value === field ? "selected" : ""; - options_html += ``; - }); + const filter_fields = field_options.map((opt) => ({ + fieldname: opt.value, + label: opt.label, + parent: doctype_or_report, + })); + const add_filter_row = (fieldname = "", expression = "") => { const row_html = ` - + @@ -519,7 +517,24 @@ frappe.ui.form.on("Number Card", { `; const $row = $(row_html); + + const field_select = new frappe.ui.FieldSelect({ + parent: $row.find(".fieldname-select-area"), + doctype: doctype_or_report, + filter_fields: filter_fields, + input_class: "input-xs", + select: (_, selected_fieldname) => { + $row.data("selected_fieldname", selected_fieldname); + }, + }); + + if (fieldname) { + field_select.set_value(doctype_or_report, fieldname); + $row.data("selected_fieldname", fieldname); + } + $row.find(".filter-expression").val(expression); + $row.data("field_select", field_select); $filter_area.find(".filter-rows").append($row); }; From c0ba3d9fabd24552a3d47db65f9c146cb10a0e06 Mon Sep 17 00:00:00 2001 From: Aditya Patil Date: Wed, 25 Feb 2026 15:51:41 +0530 Subject: [PATCH 24/89] fix: spinner size and spacing to match frappe-ui --- frappe/public/js/frappe/ui/dialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js index e544b48cc4..1129ed3aca 100644 --- a/frappe/public/js/frappe/ui/dialog.js +++ b/frappe/public/js/frappe/ui/dialog.js @@ -199,7 +199,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { this.has_primary_action = true; var me = this; const primary_btn = this.get_primary_btn().removeClass("hide").html(label); - const spinner = ``; + const spinner = ``; if (typeof click == "function") { primary_btn.off("click").on("click", function () { me.primary_action_fulfilled = true; @@ -219,7 +219,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { .prop("disabled", true) .addClass("btn-primary-dark") .html( - `
+ `
${spinner} ${ loading_label From f005bf5b02a6e51f6ceefeb3d9b9f3814648e92f Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 25 Feb 2026 17:33:48 +0530 Subject: [PATCH 25/89] fix: calculate scroll position wrt correct container in jump to field --- frappe/public/js/frappe/utils/utils.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index fb888880f3..f64a4f56dc 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -328,7 +328,7 @@ Object.assign(frappe.utils, { scroll_top = typeof element == "number" ? element - cint(additional_offset) - : this.get_scroll_position(element, additional_offset); + : this.get_scroll_position(element, element_to_be_scrolled, additional_offset); } if (scroll_top < 0) { @@ -366,10 +366,26 @@ Object.assign(frappe.utils, { element_to_be_scrolled.scrollTop(scroll_top); } }, - get_scroll_position: function (element, additional_offset) { - let header_offset = - $(".navbar").height() + $(".page-head:visible").height() || $(".navbar").height(); - return $(element).offset().top - header_offset - cint(additional_offset); + get_scroll_position: function (element, element_to_be_scrolled, additional_offset) { + function getOffsetRelativeToContainer() { + let offset = 0; + + let el = element instanceof HTMLElement ? element : element[0]; + const container = element_to_be_scrolled ? element_to_be_scrolled[0] : null; + + while (el && el !== container && el.offsetParent) { + offset += el.offsetTop; + el = el.offsetParent; + } + + return offset; + } + + const navbar_height = $(".navbar").height() || 0; + const page_head_height = $(".page-head:visible").height() || 0; + const header_offset = navbar_height + page_head_height; + const element_offset_top = getOffsetRelativeToContainer(); + return element_offset_top - header_offset - cint(additional_offset); }, filter_dict: function (dict, filters) { var ret = []; From f3f4a688f9be75b2ae7378b7d9e7ba1af992d3f7 Mon Sep 17 00:00:00 2001 From: Akash Tom Date: Wed, 25 Feb 2026 17:49:48 +0530 Subject: [PATCH 26/89] fix(Number Card): renaming based on label field --- frappe/desk/doctype/number_card/number_card.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json index 089786fbdc..4382211485 100644 --- a/frappe/desk/doctype/number_card/number_card.json +++ b/frappe/desk/doctype/number_card/number_card.json @@ -1,6 +1,7 @@ { "actions": [], "allow_rename": 1, + "autoname": "field:label", "creation": "2020-04-15 18:06:39.444683", "doctype": "DocType", "editable_grid": 1, @@ -72,7 +73,8 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Label", - "reqd": 1 + "reqd": 1, + "unique": 1 }, { "fieldname": "color", @@ -229,10 +231,11 @@ } ], "links": [], - "modified": "2025-09-17 21:00:11.351605", + "modified": "2026-02-25 16:33:09.032056", "modified_by": "Administrator", "module": "Desk", "name": "Number Card", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { From 7e344f95f2e5be3be143c7bfd9191b7a673a0a89 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 25 Feb 2026 18:24:42 +0530 Subject: [PATCH 27/89] fix: pass scrollable container from form --- 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 cefd7e1527..325fcdd1e5 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -2083,7 +2083,7 @@ frappe.ui.form.Form = class FrappeForm { } // scroll to input - frappe.utils.scroll_to($el, true, 15); + frappe.utils.scroll_to($el, true, 15, $(".main-section")); // focus if text field if (focus) { From d862716267d9489fdc180eeab9d831ac9c3ddc56 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 25 Feb 2026 18:25:15 +0530 Subject: [PATCH 28/89] fix: avoid clipping highlight shadow for grid --- frappe/public/scss/common/grid.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index fa03f4d84f..00a967a46e 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -779,7 +779,7 @@ .data-row.row { flex-wrap: nowrap; } -.frappe-control[data-fieldtype="Table"].form-group:has(.column-limit-reached) { +.frappe-control[data-fieldtype="Table"].form-group:has(.column-limit-reached):not(.highlight) { overflow-x: clip; } .column-limit-reached { From 8a2dca535e4cbbad502c6190959e1cd81fcfbd32 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 25 Feb 2026 18:39:04 +0530 Subject: [PATCH 29/89] fix: consider fixed tabs list height for correct vertical offset --- frappe/public/js/frappe/utils/utils.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index f64a4f56dc..c4e4d0718f 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -328,7 +328,7 @@ Object.assign(frappe.utils, { scroll_top = typeof element == "number" ? element - cint(additional_offset) - : this.get_scroll_position(element, element_to_be_scrolled, additional_offset); + : this.get_scroll_position(element, additional_offset, element_to_be_scrolled); } if (scroll_top < 0) { @@ -366,8 +366,8 @@ Object.assign(frappe.utils, { element_to_be_scrolled.scrollTop(scroll_top); } }, - get_scroll_position: function (element, element_to_be_scrolled, additional_offset) { - function getOffsetRelativeToContainer() { + get_scroll_position: function (element, additional_offset, element_to_be_scrolled) { + const get_offset_relative_to_container = () => { let offset = 0; let el = element instanceof HTMLElement ? element : element[0]; @@ -379,12 +379,19 @@ Object.assign(frappe.utils, { } return offset; - } + }; + + const get_header_offset = () => { + const navbar_height = $(".navbar").height() || 0; + const page_head_height = $(".page-head:visible").height() || 0; + const tabs_container_height = $(".form-tabs-list:visible").height() || 0; + + return navbar_height + page_head_height + tabs_container_height; + }; + + const element_offset_top = get_offset_relative_to_container(); + const header_offset = get_header_offset(); - const navbar_height = $(".navbar").height() || 0; - const page_head_height = $(".page-head:visible").height() || 0; - const header_offset = navbar_height + page_head_height; - const element_offset_top = getOffsetRelativeToContainer(); return element_offset_top - header_offset - cint(additional_offset); }, filter_dict: function (dict, filters) { From e9b1017d466f2a27409cbe9220a93419b94c5f27 Mon Sep 17 00:00:00 2001 From: Sumit Jain Date: Wed, 25 Feb 2026 20:49:06 +0530 Subject: [PATCH 30/89] fix: Add translation for labels in print format templates --- frappe/templates/print_format/macros/AttachImage.html | 2 +- frappe/templates/print_format/macros/Data.html | 2 +- frappe/templates/print_format/macros/Signature.html | 2 +- frappe/templates/print_format/macros/Table.html | 4 ++-- frappe/templates/print_format/print_format.html | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frappe/templates/print_format/macros/AttachImage.html b/frappe/templates/print_format/macros/AttachImage.html index 796662f67a..53d173a2a9 100644 --- a/frappe/templates/print_format/macros/AttachImage.html +++ b/frappe/templates/print_format/macros/AttachImage.html @@ -2,6 +2,6 @@ {%- block value -%}
- {{ df.label }} + {{ _(df.label) }}
{%- endblock -%} diff --git a/frappe/templates/print_format/macros/Data.html b/frappe/templates/print_format/macros/Data.html index 722c42ce1a..d04593f330 100644 --- a/frappe/templates/print_format/macros/Data.html +++ b/frappe/templates/print_format/macros/Data.html @@ -1,7 +1,7 @@ {% if value %}
{%- block label -%} -
{{ df.label }}
+
{{ _(df.label) }}
{%- endblock -%} {%- block value -%}
{{ doc.get_formatted(df.fieldname) }}
diff --git a/frappe/templates/print_format/macros/Signature.html b/frappe/templates/print_format/macros/Signature.html index 128ff2a927..909e49f0ab 100644 --- a/frappe/templates/print_format/macros/Signature.html +++ b/frappe/templates/print_format/macros/Signature.html @@ -2,6 +2,6 @@ {%- block value -%}
- {{ df.label }} + {{ _(df.label) }}
{%- endblock -%} diff --git a/frappe/templates/print_format/macros/Table.html b/frappe/templates/print_format/macros/Table.html index 27c0be961c..a967c923d3 100644 --- a/frappe/templates/print_format/macros/Table.html +++ b/frappe/templates/print_format/macros/Table.html @@ -1,7 +1,7 @@ {% if doc.get(df.fieldname) %}
- {{ df.label }} + {{ _(df.label) }}
- -
{% set columns = df.table_columns %} @@ -9,7 +9,7 @@ {% for column in columns %} {% endfor %} diff --git a/frappe/templates/print_format/print_format.html b/frappe/templates/print_format/print_format.html index b9fb95a9d3..2e494b4321 100644 --- a/frappe/templates/print_format/print_format.html +++ b/frappe/templates/print_format/print_format.html @@ -21,7 +21,7 @@ {% for section in layout.sections %}
{% if section.label %} - + {% endif %}
From 15313e8747d70bcf4cdb27ff7b06910bef9bc55e Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 25 Feb 2026 15:20:03 +0530 Subject: [PATCH 31/89] fix(UI): specify px unit to image height and width labels --- frappe/printing/doctype/letter_head/letter_head.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/printing/doctype/letter_head/letter_head.json b/frappe/printing/doctype/letter_head/letter_head.json index 4ddafc47db..782f8d9484 100644 --- a/frappe/printing/doctype/letter_head/letter_head.json +++ b/frappe/printing/doctype/letter_head/letter_head.json @@ -127,12 +127,12 @@ { "fieldname": "image_height", "fieldtype": "Float", - "label": "Image Height" + "label": "Image Height (px)" }, { "fieldname": "image_width", "fieldtype": "Float", - "label": "Image Width" + "label": "Image Width (px)" }, { "depends_on": "eval:doc.footer_source==='Image' && doc.letter_head_name", @@ -148,12 +148,12 @@ { "fieldname": "footer_image_height", "fieldtype": "Float", - "label": "Image Height" + "label": "Image Height (px)" }, { "fieldname": "footer_image_width", "fieldtype": "Float", - "label": "Image Width" + "label": "Image Width (px)" }, { "fieldname": "footer_align", @@ -203,7 +203,7 @@ "links": [], "make_attachments_public": 1, "max_attachments": 3, - "modified": "2026-02-24 20:53:14.297567", + "modified": "2026-02-25 14:37:56.061516", "modified_by": "Administrator", "module": "Printing", "name": "Letter Head", From ae12ea072a31df7d4e5ca78776422e97616ac57d Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Wed, 25 Feb 2026 23:52:25 +0530 Subject: [PATCH 32/89] chore: linters check --- frappe/printing/doctype/letter_head/letter_head.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/printing/doctype/letter_head/letter_head.json b/frappe/printing/doctype/letter_head/letter_head.json index 782f8d9484..aa40d0a751 100644 --- a/frappe/printing/doctype/letter_head/letter_head.json +++ b/frappe/printing/doctype/letter_head/letter_head.json @@ -203,7 +203,7 @@ "links": [], "make_attachments_public": 1, "max_attachments": 3, - "modified": "2026-02-25 14:37:56.061516", + "modified": "2026-02-25 14:37:57.061516", "modified_by": "Administrator", "module": "Printing", "name": "Letter Head", From b58592f30cad342936c3ff68776e86f9541d3583 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 26 Feb 2026 01:15:50 +0530 Subject: [PATCH 33/89] fix: scroll to route field correctly in test --- cypress/integration/web_form.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cypress/integration/web_form.js b/cypress/integration/web_form.js index 36f65a80bc..cbcd9e5bcf 100644 --- a/cypress/integration/web_form.js +++ b/cypress/integration/web_form.js @@ -27,7 +27,9 @@ context("Web Form", () => { cy.wait("@save_form"); + cy.get('.frappe-control[data-fieldname="route"]').scrollIntoView(); cy.get_field("route").should("have.value", "note"); + cy.get(".title-area .indicator-pill") .should("contain.text", "Published") .should("have.class", "green"); From c554641e58b570c54f47c74f77e5521536dad995 Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 26 Feb 2026 01:58:41 +0530 Subject: [PATCH 34/89] chore: update pypdf --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d0bf7066b3..306aab4d3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ # We depend on internal attributes, # do NOT add loose requirements on PyMySQL versions. "PyMySQL==1.1.2", - "pypdf==6.7.1", + "pypdf==6.7.2", "PyPika @ git+https://github.com/frappe/pypika@2c50e6142b2d61d2d243e466fdd5dc03b3d918f2", "mysqlclient==2.2.7", "PyQRCode~=1.2.1", From 85464f7031e7d7e8005144334e540569df099bc8 Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 26 Feb 2026 01:37:23 +0530 Subject: [PATCH 35/89] fix: business hours in popup,theme and apps for which to show --- frappe/public/js/billing.bundle.js | 10 +++++++--- frappe/public/js/frappe/utils/utils.js | 12 ++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/billing.bundle.js b/frappe/public/js/billing.bundle.js index c9bf512844..79e0609c58 100644 --- a/frappe/public/js/billing.bundle.js +++ b/frappe/public/js/billing.bundle.js @@ -89,13 +89,17 @@ function openFrappeCloudDashboard() { } function addChatBubble() { - if (checkBusinessHours()) { + const all_apps = frappe.utils.get_installed_apps(); + const desk_apps = ["erpnext", "hrms"]; + + const apps_allowed = frappe.utils.is_sub_array(all_apps, desk_apps); + if (checkBusinessHours && apps_allowed) { let chat_banner = document.createElement("script"); chat_banner.innerHTML = '(function(d,t){var BASE_URL="https://chat.frappe.cloud";var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src=BASE_URL+"/packs/js/sdk.js";g.async=true;s.parentNode.insertBefore(g,s);g.onload=function(){window.chatwootSDK.run({websiteToken:"LdmfJzftdJGEcFjoTqk8CrSq",baseUrl:BASE_URL})}})(document,"script");'; document.body.append(chat_banner); const root = document.documentElement; - root.style.setProperty("--s-700", "var(--gray-50)"); + root.style.setProperty("--s-700", "var(--gray-500)"); } } @@ -103,5 +107,5 @@ function checkBusinessHours() { let currentTime = new Date(); const istTime = new Date(currentTime.toLocaleString("en-US", { timeZone: "Asia/Kolkata" })); - return istTime.getHours() >= 11 && istTime.getHours() <= 18; + return istTime.getHours() >= 11 && istTime.getHours() < 18; } diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index fb888880f3..e61e0f3b78 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -2229,4 +2229,16 @@ Object.assign(frappe.utils, { } return value; }, + get_installed_apps() { + return frappe.boot.app_data.map((app) => { + return app.app_name; + }); + }, + is_sub_array(big, small) { + let i = 0; + for (let num of big) { + if (num === small[i]) i++; + } + return i === small.length; + }, }); From 4ae1db0d6b7c9370838af5f6262a5e443ec215c8 Mon Sep 17 00:00:00 2001 From: MochaMind Date: Thu, 26 Feb 2026 04:44:41 +0530 Subject: [PATCH 36/89] fix: Swedish translations --- frappe/locale/sv.po | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/locale/sv.po b/frappe/locale/sv.po index 0b77050332..7ad41225a0 100644 --- a/frappe/locale/sv.po +++ b/frappe/locale/sv.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2026-02-22 09:42+0000\n" -"PO-Revision-Date: 2026-02-23 22:07\n" +"PO-Revision-Date: 2026-02-25 23:14\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Swedish\n" "MIME-Version: 1.0\n" @@ -2868,7 +2868,7 @@ msgstr "Tilldelning Klar" #. Label of the assignment_days (Table) field in DocType 'Assignment Rule' #: frappe/automation/doctype/assignment_rule/assignment_rule.json msgid "Assignment Days" -msgstr "Automation Dagar" +msgstr "Tilldelning Dagar" #. Name of a DocType #. Label of the assignment_rule (Link) field in DocType 'ToDo' @@ -2876,7 +2876,7 @@ msgstr "Automation Dagar" #: frappe/automation/doctype/assignment_rule/assignment_rule.json #: frappe/desk/doctype/todo/todo.json frappe/workspace_sidebar/automation.json msgid "Assignment Rule" -msgstr "Automation Regel" +msgstr "Tilldelning Regel" #. Name of a DocType #: frappe/automation/doctype/assignment_rule_day/assignment_rule_day.json @@ -2890,13 +2890,13 @@ msgstr "Automation Regel Användare" #: frappe/automation/doctype/assignment_rule/assignment_rule.py:55 msgid "Assignment Rule is not allowed on document type {0}" -msgstr "Automation Regel är ej tillåten på dokument typ {0}" +msgstr "Tilldelning Regel är ej tillåten på dokument typ {0}" #. Label of the assignment_rules_section (Section Break) field in DocType #. 'Assignment Rule' #: frappe/automation/doctype/assignment_rule/assignment_rule.json msgid "Assignment Rules" -msgstr "Automation Regler" +msgstr "Tilldelning Regler" #: frappe/desk/doctype/notification_log/notification_log.py:153 msgid "Assignment Update on {0}" From 431ffa074b9a5a9e8abcc09275fac3786970e9d3 Mon Sep 17 00:00:00 2001 From: MochaMind Date: Thu, 26 Feb 2026 04:44:53 +0530 Subject: [PATCH 37/89] fix: Persian translations --- frappe/locale/fa.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/locale/fa.po b/frappe/locale/fa.po index cd633e0b2d..8862e8e299 100644 --- a/frappe/locale/fa.po +++ b/frappe/locale/fa.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2026-02-22 09:42+0000\n" -"PO-Revision-Date: 2026-02-23 22:07\n" +"PO-Revision-Date: 2026-02-25 23:14\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Persian\n" "MIME-Version: 1.0\n" @@ -6314,7 +6314,7 @@ msgstr "سفارشی‌سازی" #: frappe/custom/doctype/customize_form/customize_form.js:89 msgid "Customize Child Table" -msgstr "سفارشی کردن جدول فرزند" +msgstr "سفارشی‌سازی جدول فرزند" #: frappe/public/js/frappe/views/dashboard/dashboard_view.js:38 msgid "Customize Dashboard" @@ -6339,7 +6339,7 @@ msgstr "سفارشی‌سازی فرم - {0}" #. Name of a DocType #: frappe/custom/doctype/customize_form_field/customize_form_field.json msgid "Customize Form Field" -msgstr "سفارشی کردن فیلد فرم" +msgstr "سفارشی‌سازی فیلد فرم" #: frappe/public/js/frappe/list/list_view.js:1994 msgctxt "Customize qucik filters of List View" @@ -18808,7 +18808,7 @@ msgstr "" #: frappe/core/doctype/doctype/doctype.py:1699 msgid "Options for Rating field can range from 3 to 10" -msgstr "گزینه‌های فیلد رتبه بندی می‌تواند از 3 تا 10 باشد" +msgstr "گزینه‌های فیلد رتبه‌بندی می‌تواند از 3 تا 10 باشد" #: frappe/custom/doctype/custom_field/custom_field.js:96 msgid "Options for select. Each option on a new line." From f1991ef1270a7e13a485a59937bdaf6f47142df7 Mon Sep 17 00:00:00 2001 From: MochaMind Date: Thu, 26 Feb 2026 04:44:58 +0530 Subject: [PATCH 38/89] fix: Croatian translations --- frappe/locale/hr.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/locale/hr.po b/frappe/locale/hr.po index 9f534aaf36..8851d800e9 100644 --- a/frappe/locale/hr.po +++ b/frappe/locale/hr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2026-02-22 09:42+0000\n" -"PO-Revision-Date: 2026-02-23 22:07\n" +"PO-Revision-Date: 2026-02-25 23:14\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Croatian\n" "MIME-Version: 1.0\n" @@ -2870,7 +2870,7 @@ msgstr "Dodjela je Završena" #. Label of the assignment_days (Table) field in DocType 'Assignment Rule' #: frappe/automation/doctype/assignment_rule/assignment_rule.json msgid "Assignment Days" -msgstr "Dani Dodjeljivanja" +msgstr "Dani Dodjele" #. Name of a DocType #. Label of the assignment_rule (Link) field in DocType 'ToDo' From 20f20444f0592d433b949cb07371357cf9918bed Mon Sep 17 00:00:00 2001 From: MochaMind Date: Thu, 26 Feb 2026 04:45:02 +0530 Subject: [PATCH 39/89] fix: Bosnian translations --- frappe/locale/bs.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/locale/bs.po b/frappe/locale/bs.po index c0467055e8..f05ddd2f43 100644 --- a/frappe/locale/bs.po +++ b/frappe/locale/bs.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2026-02-22 09:42+0000\n" -"PO-Revision-Date: 2026-02-23 22:07\n" +"PO-Revision-Date: 2026-02-25 23:15\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Bosnian\n" "MIME-Version: 1.0\n" @@ -2870,7 +2870,7 @@ msgstr "Dodjela je Završena" #. Label of the assignment_days (Table) field in DocType 'Assignment Rule' #: frappe/automation/doctype/assignment_rule/assignment_rule.json msgid "Assignment Days" -msgstr "Dani Dodjeljivanja" +msgstr "Dani Dodjele" #. Name of a DocType #. Label of the assignment_rule (Link) field in DocType 'ToDo' @@ -2888,7 +2888,7 @@ msgstr "Dan Dodjele Pravila" #. Name of a DocType #: frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json msgid "Assignment Rule User" -msgstr "Korisnik Dodjele Pravila" +msgstr "Korisnik Pravila Dodjele" #: frappe/automation/doctype/assignment_rule/assignment_rule.py:55 msgid "Assignment Rule is not allowed on document type {0}" From d15a6d6da0d32bd14db910d59c56d8e117d32000 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 26 Feb 2026 11:20:50 +0530 Subject: [PATCH 40/89] fix: Only show "Edit Values" button if template has something to edit Closes: https://github.com/frappe/frappe/issues/18612 --- .../website_settings/website_settings.js | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/frappe/website/doctype/website_settings/website_settings.js b/frappe/website/doctype/website_settings/website_settings.js index 07051588bd..a09101b062 100644 --- a/frappe/website/doctype/website_settings/website_settings.js +++ b/frappe/website/doctype/website_settings/website_settings.js @@ -16,6 +16,10 @@ frappe.ui.form.on("Website Settings", { frm.add_custom_button(__("View Website"), () => { window.open("/", "_blank"); }); + + // Check if templates have fields and show/hide edit button + frm.events.check_template_has_fields(frm, "navbar_template"); + frm.events.check_template_has_fields(frm, "footer_template"); }, set_banner_from_image: function (frm) { @@ -100,11 +104,36 @@ frappe.ui.form.on("Website Settings", { frappe.show_alert(__("Please select {0}", [frm.get_docfield(template_field).label])); return; } + let values = JSON.parse(frm.doc[values_field] || "{}"); open_web_template_values_editor(template, values).then((new_values) => { frm.set_value(values_field, JSON.stringify(new_values)); }); }, + + check_template_has_fields(frm, template_field) { + let template = frm.doc[template_field]; + let button_field = "edit_" + template_field + "_values"; + + if (!template || template === "Standard Navbar" || template === "Standard Footer") { + frm.toggle_display(button_field, false); + return; + } + + frappe.model.with_doc("Web Template", template, () => { + let doc = frappe.model.get_doc("Web Template", template); + let has_fields = doc.fields && doc.fields.length > 0; + frm.toggle_display(button_field, has_fields); + }); + }, + + navbar_template(frm) { + frm.events.check_template_has_fields(frm, "navbar_template"); + }, + + footer_template(frm) { + frm.events.check_template_has_fields(frm, "footer_template"); + }, }); frappe.ui.form.on("Top Bar Item", { From 9c0cb33267fa53f443935e8c6536ec90dad48ebb Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 26 Feb 2026 11:42:33 +0530 Subject: [PATCH 41/89] fix: show if any of the app is installed --- frappe/desk/page/desktop/desktop.js | 1 - frappe/public/js/billing.bundle.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/desk/page/desktop/desktop.js b/frappe/desk/page/desktop/desktop.js index 514af59374..ba76bfb88f 100644 --- a/frappe/desk/page/desktop/desktop.js +++ b/frappe/desk/page/desktop/desktop.js @@ -548,7 +548,6 @@ class DesktopPage { frappe.router.on("change", function () { if (frappe.get_route()[0] == "desktop" || frappe.get_route()[0] == "") { me.setup_navbar(); - me.setup_edit_button(); } else { $(".navbar").show(); frappe.desktop_utils.close_desktop_modal(); diff --git a/frappe/public/js/billing.bundle.js b/frappe/public/js/billing.bundle.js index 79e0609c58..da8b52be7a 100644 --- a/frappe/public/js/billing.bundle.js +++ b/frappe/public/js/billing.bundle.js @@ -92,7 +92,7 @@ function addChatBubble() { const all_apps = frappe.utils.get_installed_apps(); const desk_apps = ["erpnext", "hrms"]; - const apps_allowed = frappe.utils.is_sub_array(all_apps, desk_apps); + const apps_allowed = desk_apps.some((app) => all_apps.includes(app)); if (checkBusinessHours && apps_allowed) { let chat_banner = document.createElement("script"); chat_banner.innerHTML = From 372ce74ca94e3996cfae4f7f5744606d8b88fcf5 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 26 Feb 2026 12:53:24 +0530 Subject: [PATCH 42/89] fix: use scroll_to param in url strictly for fields --- frappe/public/js/frappe/form/form.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 2570864ee3..c56c10fe09 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1574,15 +1574,10 @@ frappe.ui.form.Form = class FrappeForm { var scroll_to = frappe.route_options.scroll_to; delete frappe.route_options.scroll_to; - var selector = []; - for (var key in scroll_to) { - var value = scroll_to[key]; - selector.push(repl('[data-%(key)s="%(value)s"]', { key: key, value: value })); - } - - selector = $(selector.join(" ")); - if (selector.length) { - frappe.utils.scroll_to(selector); + if (this.scroll_to_field(scroll_to)) { + const url = new URL(window.location); + url.searchParams.delete("scroll_to"); + history.replaceState(null, null, url); } } else if (window.location.hash) { if ($(window.location.hash).length) { From 14a003eeb72633bb0c9337ef030a2fd11bf73568 Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 26 Feb 2026 13:09:45 +0530 Subject: [PATCH 43/89] fix: disable chat popup --- frappe/public/js/billing.bundle.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frappe/public/js/billing.bundle.js b/frappe/public/js/billing.bundle.js index da8b52be7a..c3771f4ce9 100644 --- a/frappe/public/js/billing.bundle.js +++ b/frappe/public/js/billing.bundle.js @@ -32,9 +32,6 @@ $(document).ready(function () { !!frappe.boot.setup_complete && !frappe.is_mobile() && frappe.user.has_role("System Manager"); - if (visiblity_condition && isFCUser) { - addChatBubble(); - } if (isFCUser) { $.extend(card_args, { primary_action_label: "Upgrade", From cf2ec23f2739f0e2c4c96a3696e4988276a72d94 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 26 Feb 2026 13:27:46 +0530 Subject: [PATCH 44/89] fix: cannot scroll in folders --- frappe/desk/page/desktop/desktop.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/desk/page/desktop/desktop.css b/frappe/desk/page/desktop/desktop.css index ef5e4e6062..4c294eb722 100644 --- a/frappe/desk/page/desktop/desktop.css +++ b/frappe/desk/page/desktop/desktop.css @@ -87,12 +87,17 @@ } } .modal -.modal-body .icons-container,.folder-icon .icons-container { +.modal-body .icons-container, .folder-icon .icons-container { padding:0px; margin: 0px; height: 100%; + overflow: auto; +} + +.folder-icon .icons-container { overflow: hidden; } + .icons{ gap: 16px; display: grid; From 92e3d1272774d5d2f26ac07ae0fc5ed1bb717616 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 26 Feb 2026 13:53:08 +0530 Subject: [PATCH 45/89] fix: text muted for skipped steps --- frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue b/frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue index 9ee883cf31..6104e4d7a3 100644 --- a/frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue +++ b/frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue @@ -306,7 +306,7 @@ function markReset(step) {
{{ __(step.action_label) }} From e737289c24d2b0b02de760cc15429fc42dbb0cfd Mon Sep 17 00:00:00 2001 From: Akash Tom Date: Thu, 26 Feb 2026 14:35:17 +0530 Subject: [PATCH 46/89] fix: prevent focus on empty mandatory field on save --- frappe/public/js/frappe/form/save.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index e3c2cd1e4e..4a182d47e2 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -231,7 +231,7 @@ frappe.ui.form.check_mandatory = function (frm) { } function scroll_to(fieldname) { - if (frm.scroll_to_field(fieldname)) { + if (frm.scroll_to_field(fieldname, false)) { frm.scroll_set = true; } } From 2526dbd99aef194b74877536e0fd467b5786d75f Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 26 Feb 2026 14:40:01 +0530 Subject: [PATCH 47/89] fix: show chat popup only in the desktop page --- frappe/public/js/billing.bundle.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/billing.bundle.js b/frappe/public/js/billing.bundle.js index c3771f4ce9..2c11bc75af 100644 --- a/frappe/public/js/billing.bundle.js +++ b/frappe/public/js/billing.bundle.js @@ -32,6 +32,16 @@ $(document).ready(function () { !!frappe.boot.setup_complete && !frappe.is_mobile() && frappe.user.has_role("System Manager"); + if (visiblity_condition && isFCUser) { + frappe.router.on("change", function () { + if (frappe.get_route()[0] == "") { + addChatBubble(); + toggleChatBubble(true); + } else { + toggleChatBubble(false); + } + }); + } if (isFCUser) { $.extend(card_args, { primary_action_label: "Upgrade", @@ -89,9 +99,10 @@ function addChatBubble() { const all_apps = frappe.utils.get_installed_apps(); const desk_apps = ["erpnext", "hrms"]; - const apps_allowed = desk_apps.some((app) => all_apps.includes(app)); + const apps_allowed = frappe.utils.is_sub_array(all_apps, desk_apps); if (checkBusinessHours && apps_allowed) { let chat_banner = document.createElement("script"); + chat_banner.setAttribute("id", "chat_widget_trigger"); chat_banner.innerHTML = '(function(d,t){var BASE_URL="https://chat.frappe.cloud";var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src=BASE_URL+"/packs/js/sdk.js";g.async=true;s.parentNode.insertBefore(g,s);g.onload=function(){window.chatwootSDK.run({websiteToken:"LdmfJzftdJGEcFjoTqk8CrSq",baseUrl:BASE_URL})}})(document,"script");'; document.body.append(chat_banner); @@ -106,3 +117,13 @@ function checkBusinessHours() { return istTime.getHours() >= 11 && istTime.getHours() < 18; } + +function toggleChatBubble(toggle) { + if (toggle) { + $(".woot-widget-holder").show(); + $("#cw-bubble-holder").show(); + } else { + $(".woot-widget-holder").hide(); + $("#cw-bubble-holder").hide(); + } +} From bc569c97b170f54887abaaaf5739afcc4f245117 Mon Sep 17 00:00:00 2001 From: Vibhuti Garachh Date: Thu, 26 Feb 2026 14:30:42 +0530 Subject: [PATCH 48/89] fix: improve grid row layout using flex for proper alignment --- frappe/public/js/frappe/form/grid_row.js | 6 ++++-- frappe/public/scss/common/grid.scss | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 348bd13840..616c794936 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -1033,7 +1033,7 @@ export default class GridRow { let is_focused = false; var $col = $( - `
` + `
` ) .attr("data-fieldname", df.fieldname) .attr("data-fieldtype", df.fieldtype) @@ -1095,7 +1095,9 @@ export default class GridRow { return out; }); - $col.field_area = $('
').appendTo($col).toggle(false); + $col.field_area = $('
') + .appendTo($col) + .toggle(false); $col.static_area = $('
').appendTo($col).html(txt); // set title attribute to see full label for columns in the heading row diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index fa03f4d84f..9c6e17a44b 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -309,8 +309,8 @@ border-radius: 0px; border: 0px; padding-top: 10px; - padding-bottom: calc(var(--padding-md) - 3px); - height: auto; + padding-bottom: 10px; + height: 100%; } .link-btn { @@ -430,6 +430,7 @@ .frappe-control { margin-bottom: 0px !important; position: relative; + flex-grow: 1; } .col-sm-6 { From 6a7343e54327a0faff4564efb48f9f8e84bd7f6e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 26 Feb 2026 15:27:41 +0530 Subject: [PATCH 49/89] fix: empty space in report view --- frappe/public/scss/desk/report.scss | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/frappe/public/scss/desk/report.scss b/frappe/public/scss/desk/report.scss index c476bbe5c0..42f922eeae 100644 --- a/frappe/public/scss/desk/report.scss +++ b/frappe/public/scss/desk/report.scss @@ -93,6 +93,41 @@ border-radius: var(--border-radius); } } + +.report-view { + .layout-main-section { + height: calc(100vh - var(--page-head-height)); + display: flex; + flex-direction: column; + overflow: hidden; + + .page-form { + flex-shrink: 0; + } + + .frappe-list { + flex-grow: 1; + display: flex; + flex-direction: column; + overflow: hidden; + + .result, + .no-result { + flex-grow: 1; + overflow: auto; + } + + .comparison-message { + margin: 4px 0px; + } + + .no-result + .comparison-message { + display: none; + } + } + } +} + @include media-breakpoint-up(sm) { .report-view { width: calc(100% - 220px); From 3fbbe235825635410b685635b3546df6c298b52b Mon Sep 17 00:00:00 2001 From: Akash Tom Date: Thu, 26 Feb 2026 15:29:08 +0530 Subject: [PATCH 50/89] fix(Number Card): remove restriction on Currency based aggregation --- frappe/desk/doctype/number_card/number_card.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index c5621fe6a4..37ddd727bc 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -124,11 +124,6 @@ frappe.ui.form.on("Number Card", { frappe.model.with_doctype(doctype, () => { frappe.get_meta(doctype).fields.map((df) => { if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) { - if (df.fieldtype == "Currency") { - if (!df.options || df.options !== "Company:company:default_currency") { - return; - } - } aggregate_based_on_fields.push({ label: df.label, value: df.fieldname }); } }); From a2eca18bd6aee9c16405d2e0318139969c0d6716 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 26 Feb 2026 15:53:55 +0530 Subject: [PATCH 51/89] fix: hide unnecessary comparison message --- .../js/frappe/views/reports/report_view.js | 35 +++++++++++++------ frappe/public/scss/desk/report.scss | 34 +++++++++++++++--- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 68d523b0fa..9bedbf0e11 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -99,16 +99,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { }); } - setup_paging_area() { - super.setup_paging_area(); - const message = __( - "For comparison, use >5, <10 or =324. For ranges, use 5:10 (for values between 5 & 10)." - ); - this.$paging_area.before( - `${message}` - ); - } - setup_sort_selector() { this.sort_selector = new frappe.ui.SortSelector({ parent: this.filter_area.$filter_list_wrapper, @@ -430,6 +420,8 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { } setup_inline_filter_observer() { + this.setup_inline_filter_help_icons(); + this.$datatable_wrapper.on( "keyup", ".dt-filter", @@ -439,6 +431,29 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { ); } + setup_inline_filter_help_icons() { + const message = __( + "For comparison, use >5, <10 or =324.\nFor ranges, use 5:10 (for values between 5 & 10)." + ); + + this.$datatable_wrapper.find(".dt-filter").each((_, input) => { + const $input = $(input); + + if ($input.siblings(".comparison-help-icon").length) { + return; + } + + const $icon = $( + `${frappe.utils.icon( + "info", + "xs" + )}` + ); + + $input.after($icon); + }); + } + update_count_for_inline_filter() { if (!this.datatable) return; diff --git a/frappe/public/scss/desk/report.scss b/frappe/public/scss/desk/report.scss index 42f922eeae..a84b4719c3 100644 --- a/frappe/public/scss/desk/report.scss +++ b/frappe/public/scss/desk/report.scss @@ -118,10 +118,6 @@ } .comparison-message { - margin: 4px 0px; - } - - .no-result + .comparison-message { display: none; } } @@ -164,6 +160,36 @@ @include get_textstyle("base", "regular"); } +.report-view { + .datatable .dt-row-filter .dt-cell__content { + position: relative; + } + + .datatable .dt-row-filter .dt-filter.dt-input { + padding-inline-end: 1.5rem; + } + + .datatable .dt-row-filter .comparison-help-icon { + position: absolute; + top: 50%; + right: 10px; + transform: translateY(-50%); + display: inline-flex; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + + .icon { + stroke: currentColor; + } + } + + .datatable .dt-row-filter .dt-filter.dt-input:focus + .comparison-help-icon { + opacity: 1; + pointer-events: auto; + } +} + .list-count { margin-right: var(--margin-sm); @include get_textstyle("base", "regular"); From c4442a3a5d617ae8fc68c9cfccf13b9b4767f283 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 26 Feb 2026 16:28:39 +0530 Subject: [PATCH 52/89] fix: don't replace # in web link fragments for attachment links --- frappe/public/js/frappe/form/sidebar/attachments.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js index bb36a2f65b..894856dcec 100644 --- a/frappe/public/js/frappe/form/sidebar/attachments.js +++ b/frappe/public/js/frappe/form/sidebar/attachments.js @@ -180,8 +180,18 @@ frappe.ui.form.Attachments = class Attachments { file_url = "/files/" + attachment.file_name; } } + + const is_web_url = /^(https?:)?\/\//i.test(file_url); + + file_url = encodeURI(file_url); + // hash is not escaped, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI - return encodeURI(file_url).replace(/#/g, "%23"); + // only encode hash if it's a local file path, not a web URL + if (!is_web_url) { + file_url = file_url.replace(/#/g, "%23"); + } + + return file_url; } get_file_id_from_file_url(file_url) { var fid; From 46c04d03e846241032d43de57993beb715303031 Mon Sep 17 00:00:00 2001 From: Safwan <62411302+safwansamsudeen@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:38:48 +0530 Subject: [PATCH 53/89] fix: save issue with paginated grid (#37588) --- frappe/public/js/frappe/form/layout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 893d8abe3e..d1dc39a231 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -745,7 +745,7 @@ frappe.ui.form.Layout = class Layout { if (f.df.fieldtype === "Table") { for (const row of f.grid?.grid_rows || []) { - row.refresh_dependency(); + row?.refresh_dependency(); } } } From c0f772e71a588f70382090051c687c48d8000882 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 26 Feb 2026 17:11:34 +0530 Subject: [PATCH 54/89] fix: Limit storage usage report to current schema (#37595) For deployments where permissions somehow allow reading other schemas. --- .../database_storage_usage_by_tables.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py index df0a0c9470..df8dff27ce 100644 --- a/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py +++ b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py @@ -22,6 +22,7 @@ def execute(filters=None): round((data_length / 1024 / 1024), 2) as data_size, round((index_length / 1024 / 1024), 2) as index_size FROM information_schema.TABLES + WHERE table_schema = DATABASE() ORDER BY (data_length + index_length) DESC; """, "postgres": """ From d5067ce684b07045011c53f8426e51f88c262330 Mon Sep 17 00:00:00 2001 From: Akash Tom Date: Thu, 26 Feb 2026 17:46:39 +0530 Subject: [PATCH 55/89] fix(Chart Widget): recreate chart on applying filter to render legend properly --- frappe/public/js/frappe/widgets/chart_widget.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index f9c1d049cc..eda2048a67 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -570,8 +570,14 @@ export default class ChartWidget extends Widget { let setup_dashboard_chart = () => { const chart_args = this.get_chart_args(); + const is_circular_chart = ["Pie", "Donut", "Percentage"].includes(this.chart_doc.type); + if (!this.dashboard_chart) { this.dashboard_chart = frappe.utils.make_chart(this.chart_wrapper[0], chart_args); + } else if (is_circular_chart) { + this.chart_wrapper.empty(); + delete this.dashboard_chart; + this.dashboard_chart = frappe.utils.make_chart(this.chart_wrapper[0], chart_args); } else { this.dashboard_chart.update(this.data); } @@ -619,6 +625,7 @@ export default class ChartWidget extends Widget { colors: colors, height: this.height, maxSlices: this.chart_doc.number_of_groups || max_slices, + truncateLegends: 0, axisOptions: { xIsSeries: this.chart_doc.timeseries, shortenYAxisNumbers: 1, From d4e6fea1ce03e9f00f9a2c530518c4bf82a91ccc Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Thu, 26 Feb 2026 14:12:27 +0000 Subject: [PATCH 56/89] fix: hide print format builder after route change --- .../printing/page/print_format_builder/print_format_builder.js | 2 ++ 1 file changed, 2 insertions(+) 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 bae82ba040..a40101ba87 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -35,6 +35,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { this.show_start(); } else { this.page.set_title(this.print_format.name); + this.page.sidebar.toggle(true); this.setup_print_format(); } } @@ -65,6 +66,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { this.page.main.html(frappe.render_template("print_format_builder_start", {})); this.page.clear_actions(); this.page.set_title(__("Print Format Builder")); + this.page.sidebar.toggle(false); this.start_edit_print_format(); this.start_new_print_format(); } From 675b23c47cd6eb2d784815a491193cfcfbeff288 Mon Sep 17 00:00:00 2001 From: Shubh Doshi Date: Wed, 18 Feb 2026 13:41:28 +0530 Subject: [PATCH 57/89] fix: validate doc_status for non-submittable doctypes in workflow --- .../components/Properties.vue | 13 ++++ frappe/public/js/workflow_builder/store.js | 8 ++- .../doctype/workflow/test_workflow.py | 62 +++++++++++++++++-- frappe/workflow/doctype/workflow/workflow.js | 24 ++++++- frappe/workflow/doctype/workflow/workflow.py | 23 ++++++- 5 files changed, 118 insertions(+), 12 deletions(-) diff --git a/frappe/public/js/workflow_builder/components/Properties.vue b/frappe/public/js/workflow_builder/components/Properties.vue index 12b3f4371e..51ca48f317 100644 --- a/frappe/public/js/workflow_builder/components/Properties.vue +++ b/frappe/public/js/workflow_builder/components/Properties.vue @@ -4,6 +4,11 @@ import { useStore } from "../store"; let store = useStore(); +const is_doc_status_readonly = computed(() => { + if (!store.workflow.selected || !("state" in store.workflow.selected.data)) return false; + return !store.is_submittable(); +}); + let title = ref("Workflow Details"); let doc = computed(() => { @@ -30,6 +35,13 @@ let properties = computed(() => { ); store.statefields.splice(2, 0, allow_edit); + const submittable = store.is_submittable(); + + // Auto-reset doc_status to "Draft" for non-submittable doctypes + if (!submittable && store.workflow.selected.data.doc_status !== "Draft") { + store.workflow.selected.data.doc_status = "Draft"; + } + return store.statefields.filter((df) => { if (df.fieldname == "doc_status") { df.options = ["Draft", "Submitted", "Cancelled"]; @@ -61,6 +73,7 @@ let properties = computed(() => { v-model="doc[df.fieldname]" :data-fieldname="df.fieldname" :data-fieldtype="df.fieldtype" + :read_only="df.fieldname === 'doc_status' ? is_doc_status_readonly : false" />
diff --git a/frappe/public/js/workflow_builder/store.js b/frappe/public/js/workflow_builder/store.js index bbb4925ff5..4c08a58b91 100644 --- a/frappe/public/js/workflow_builder/store.js +++ b/frappe/public/js/workflow_builder/store.js @@ -135,13 +135,18 @@ export const useStore = defineStore("workflow-builder-store", () => { frappe.breadcrumbs.$breadcrumbs.append(breadcrumbs); } + function is_submittable() { + if (!workflow_doc.value?.document_type) return true; + return frappe.get_meta(workflow_doc.value.document_type)?.is_submittable; + } + function get_state_df(data) { let doc_status_map = { Draft: 0, Submitted: 1, Cancelled: 2, }; - data.doc_status = doc_status_map[data.doc_status]; + data.doc_status = is_submittable() ? doc_status_map[data.doc_status] : 0; return data; } @@ -234,5 +239,6 @@ export const useStore = defineStore("workflow-builder-store", () => { reset_changes, save_changes, setup_undo_redo, + is_submittable, }; }); diff --git a/frappe/workflow/doctype/workflow/test_workflow.py b/frappe/workflow/doctype/workflow/test_workflow.py index 7146f299fd..5bab3d043d 100644 --- a/frappe/workflow/doctype/workflow/test_workflow.py +++ b/frappe/workflow/doctype/workflow/test_workflow.py @@ -108,14 +108,15 @@ class TestWorkflow(IntegrationTestCase): self.assertEqual(workflow_actions[0].status, "Completed") def test_if_workflow_set_on_action(self): + self.workflow, doc = create_new_submittable_doctype_with_workflow() self.workflow._update_state_docstatus = True self.workflow.states[1].doc_status = 1 self.workflow.save() - todo = create_new_todo() - self.assertEqual(todo.docstatus, 0) - todo.submit() - self.assertEqual(todo.docstatus, 1) - self.assertEqual(todo.workflow_state, "Approved") + + self.assertEqual(doc.docstatus, 0) + doc.submit() + self.assertEqual(doc.docstatus, 1) + self.assertEqual(doc.workflow_state, "Approved") self.workflow.states[1].doc_status = 0 self.workflow.save() @@ -350,6 +351,57 @@ def create_new_todo(): return frappe.get_doc(doctype="ToDo", description="workflow " + random_string(10)).insert() +def create_new_submittable_doctype_with_workflow(): + submittable_dt = frappe.get_doc( + { + "doctype": "DocType", + "module": "Core", + "name": "Test Submittable Doc", + "custom": 1, + "is_submittable": 1, + "fields": [ + {"label": "Field", "fieldname": "test_field", "fieldtype": "Data"}, + { + "label": "Workflow State", + "fieldname": "workflow_state", + "fieldtype": "Link", + "options": "Workflow State", + }, + ], + "permissions": [{"role": "System Manager", "read": 1, "write": 1, "submit": 1, "cancel": 1}], + } + ).insert(ignore_if_duplicate=True) + + workflow = None + if not frappe.db.exists("Workflow", "Submittable Workflow"): + workflow = frappe.new_doc("Workflow") + workflow.workflow_name = "Submittable Workflow" + workflow.document_type = submittable_dt.name + workflow.workflow_state_field = "workflow_state" + workflow.is_active = 1 + workflow.append("states", dict(state="Pending", allow_edit="All")) + workflow.append( + "states", + dict(state="Approved", allow_edit="System Manager", doc_status=0), + ) + workflow.append( + "transitions", + dict( + state="Pending", + action="Approve", + next_state="Approved", + allowed="System Manager", + allow_self_approval=1, + ), + ) + workflow.insert(ignore_permissions=True) + else: + workflow = frappe.get_doc("Workflow", "Submittable Workflow") + + doc = frappe.get_doc({"doctype": submittable_dt.name, "test_field": "test"}).insert() + return workflow, doc + + def create_new_note(doc): note = frappe.new_doc("Note") note.title = "workflow - " + doc.name diff --git a/frappe/workflow/doctype/workflow/workflow.js b/frappe/workflow/doctype/workflow/workflow.js index 3346f6f519..243483f085 100644 --- a/frappe/workflow/doctype/workflow/workflow.js +++ b/frappe/workflow/doctype/workflow/workflow.js @@ -109,9 +109,10 @@ frappe.ui.form.on("Workflow", { return; } frappe.model.with_doctype(doc.document_type, () => { - const fieldnames = frappe - .get_meta(doc.document_type) - .fields.filter((field) => !frappe.model.no_value_type.includes(field.fieldtype)) + const meta = frappe.get_meta(doc.document_type); + const is_submittable = meta.is_submittable; + const fieldnames = meta.fields + .filter((field) => !frappe.model.no_value_type.includes(field.fieldtype)) .map((field) => field.fieldname); frm.fields_dict.states.grid.update_docfield_property( @@ -119,6 +120,23 @@ frappe.ui.form.on("Workflow", { "options", [""].concat(fieldnames) ); + + frm.fields_dict.states.grid.update_docfield_property( + "doc_status", + "read_only", + !is_submittable + ); + + if (!is_submittable) { + let changed = false; + frm.doc.states.forEach((row) => { + if (parseInt(row.doc_status || 0) !== 0) { + row.doc_status = "0"; + changed = true; + } + }); + if (changed) frm.refresh_field("states"); + } }); }, create_warning_dialog: function (frm) { diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py index 710c153f0c..c5c6320f00 100644 --- a/frappe/workflow/doctype/workflow/workflow.py +++ b/frappe/workflow/doctype/workflow/workflow.py @@ -90,23 +90,40 @@ class Workflow(Document): frappe.throw(frappe._("{0} not a valid State").format(state)) + # Check if doctype is submittable + meta = frappe.get_meta(self.document_type) + is_submittable = meta.is_submittable + + # Validate that non-submittable doctypes only have doc_status 0 + if not is_submittable: + for state in self.states: + if cint(state.doc_status or 0) != 0: + frappe.throw( + frappe._( + "Workflow State '{0}' has Document Status {1}, but DocType '{2}' is not submittable. " + "Only Document Status 0 (Draft) is allowed for non-submittable DocTypes." + ).format(state.state, state.doc_status, self.document_type) + ) + for t in self.transitions: state = get_state(t.state) next_state = get_state(t.next_state) + state_docstatus = cint(state.doc_status or 0) + next_state_docstatus = cint(next_state.doc_status or 0) - if state.doc_status == "2": + if state_docstatus == 2: frappe.throw( frappe._("Cannot change state of Cancelled Document. Transition row {0}").format(t.idx) ) - if state.doc_status == "1" and next_state.doc_status == "0": + if state_docstatus == 1 and next_state_docstatus == 0: frappe.throw( frappe._( "Submitted Document cannot be converted back to draft. Transition row {0}" ).format(t.idx) ) - if state.doc_status == "0" and next_state.doc_status == "2": + if state_docstatus == 0 and next_state_docstatus == 2: frappe.throw(frappe._("Cannot cancel before submitting. See Transition {0}").format(t.idx)) def set_active(self): From 9dcc97cb2611e4cbef6535d68cb648a72a19e4ea Mon Sep 17 00:00:00 2001 From: Shubh Doshi Date: Thu, 19 Feb 2026 10:04:40 +0530 Subject: [PATCH 58/89] refactor: updated is_doc_status_readonly condition --- frappe/public/js/workflow_builder/components/Properties.vue | 3 +-- frappe/workflow/doctype/workflow/workflow.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/frappe/public/js/workflow_builder/components/Properties.vue b/frappe/public/js/workflow_builder/components/Properties.vue index 51ca48f317..b55b3cf51e 100644 --- a/frappe/public/js/workflow_builder/components/Properties.vue +++ b/frappe/public/js/workflow_builder/components/Properties.vue @@ -5,7 +5,7 @@ import { useStore } from "../store"; let store = useStore(); const is_doc_status_readonly = computed(() => { - if (!store.workflow.selected || !("state" in store.workflow.selected.data)) return false; + if (!store.workflow.selected || !(store.workflow.selected.type === "state")) return false; return !store.is_submittable(); }); @@ -37,7 +37,6 @@ let properties = computed(() => { const submittable = store.is_submittable(); - // Auto-reset doc_status to "Draft" for non-submittable doctypes if (!submittable && store.workflow.selected.data.doc_status !== "Draft") { store.workflow.selected.data.doc_status = "Draft"; } diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py index c5c6320f00..1ee0cc09ec 100644 --- a/frappe/workflow/doctype/workflow/workflow.py +++ b/frappe/workflow/doctype/workflow/workflow.py @@ -90,11 +90,9 @@ class Workflow(Document): frappe.throw(frappe._("{0} not a valid State").format(state)) - # Check if doctype is submittable meta = frappe.get_meta(self.document_type) is_submittable = meta.is_submittable - # Validate that non-submittable doctypes only have doc_status 0 if not is_submittable: for state in self.states: if cint(state.doc_status or 0) != 0: From 26cb0c1622075dc89ad9f7b30c75b55317ceeefc Mon Sep 17 00:00:00 2001 From: Shubh Doshi Date: Thu, 26 Feb 2026 11:57:05 +0530 Subject: [PATCH 59/89] fix: check all states at once in workflow-builder --- .../components/Properties.vue | 25 ++++--- frappe/public/js/workflow_builder/store.js | 42 +++++++++-- .../doctype/workflow/test_workflow.py | 71 ++++++++++--------- frappe/workflow/doctype/workflow/workflow.js | 16 ++++- frappe/workflow/doctype/workflow/workflow.py | 6 +- 5 files changed, 102 insertions(+), 58 deletions(-) diff --git a/frappe/public/js/workflow_builder/components/Properties.vue b/frappe/public/js/workflow_builder/components/Properties.vue index b55b3cf51e..f6a9df1285 100644 --- a/frappe/public/js/workflow_builder/components/Properties.vue +++ b/frappe/public/js/workflow_builder/components/Properties.vue @@ -1,16 +1,21 @@
- {{ column.label }} + {{ _(column.label) }}