From a4fbe0160e9e933ea3d346b9b2add1ce39551979 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Tue, 27 May 2025 13:16:27 +0530 Subject: [PATCH 001/144] feat: show mask data in form, list and report view --- .../custom_docperm/custom_docperm.json | 18 ++++++- .../doctype/custom_docperm/custom_docperm.py | 1 + frappe/core/doctype/docfield/docfield.json | 14 +++++- frappe/core/doctype/docfield/docfield.py | 15 ++++++ frappe/core/doctype/docperm/docperm.json | 14 +++++- frappe/core/doctype/docperm/docperm.py | 1 + .../permission_manager/permission_manager.js | 3 +- frappe/model/__init__.py | 2 + frappe/model/db_query.py | 50 +++++++++++++++++++ frappe/model/document.py | 47 +++++++++++++++++ frappe/model/meta.py | 3 ++ frappe/public/js/frappe/form/form.js | 14 ++++++ frappe/public/js/frappe/form/layout.js | 6 +++ frappe/public/js/frappe/list/base_list.js | 2 + frappe/public/js/frappe/list/list_view.js | 7 +++ frappe/public/js/frappe/model/meta.js | 5 ++ 16 files changed, 195 insertions(+), 7 deletions(-) diff --git a/frappe/core/doctype/custom_docperm/custom_docperm.json b/frappe/core/doctype/custom_docperm/custom_docperm.json index eb9dcdfe0a..00a47a0113 100644 --- a/frappe/core/doctype/custom_docperm/custom_docperm.json +++ b/frappe/core/doctype/custom_docperm/custom_docperm.json @@ -23,6 +23,7 @@ "submit", "cancel", "amend", + "mask", "additional_permissions", "report", "export", @@ -153,6 +154,16 @@ "print_width": "32px", "width": "32px" }, + { + "default": "0", + "fieldname": "mask", + "fieldtype": "Check", + "label": "Mask", + "oldfieldname": "mask", + "oldfieldtype": "Check", + "print_width": "32px", + "width": "32px" + }, { "fieldname": "additional_permissions", "fieldtype": "Section Break", @@ -214,11 +225,13 @@ "label": "Select" } ], + "grid_page_length": 50, "links": [], - "modified": "2024-03-23 16:02:14.726078", + "modified": "2025-05-22 16:59:35.484376", "modified_by": "Administrator", "module": "Core", "name": "Custom DocPerm", + "naming_rule": "Random", "owner": "Administrator", "permissions": [ { @@ -235,8 +248,9 @@ } ], "read_only": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "ASC", "states": [], "title_field": "parent" -} \ No newline at end of file +} diff --git a/frappe/core/doctype/custom_docperm/custom_docperm.py b/frappe/core/doctype/custom_docperm/custom_docperm.py index 77f2524159..485e187c5e 100644 --- a/frappe/core/doctype/custom_docperm/custom_docperm.py +++ b/frappe/core/doctype/custom_docperm/custom_docperm.py @@ -21,6 +21,7 @@ class CustomDocPerm(Document): email: DF.Check export: DF.Check if_owner: DF.Check + mask: DF.Check parent: DF.Data | None permlevel: DF.Int print: DF.Check diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index e04006f472..9455b5855c 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -20,6 +20,7 @@ "is_virtual", "search_index", "not_nullable", + "mask", "column_break_18", "options", "sort_options", @@ -607,20 +608,29 @@ "fieldname": "sticky", "fieldtype": "Check", "label": "Sticky" + }, + { + "default": "0", + "depends_on": "eval:[\"Select\", \"Read Only\", \"Phone\", \"Percent\", \"Password\", \"Link\", \"Int\", \"Float\", \"Dynamic Link\", \"Duration\", \"Datetime\", \"Currency\", \"Data\", \"Date\"].includes(doc.fieldtype)", + "fieldname": "mask", + "fieldtype": "Check", + "label": "Mask" } ], + "grid_page_length": 50, "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-01-30 14:58:19.746600", + "modified": "2025-05-17 00:48:20.359702", "modified_by": "Administrator", "module": "Core", "name": "DocField", "naming_rule": "Random", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "ASC", "states": [] -} \ No newline at end of file +} diff --git a/frappe/core/doctype/docfield/docfield.py b/frappe/core/doctype/docfield/docfield.py index 43542427f6..00f0a80b00 100644 --- a/frappe/core/doctype/docfield/docfield.py +++ b/frappe/core/doctype/docfield/docfield.py @@ -90,6 +90,7 @@ class DocField(Document): link_filters: DF.JSON | None make_attachment_public: DF.Check mandatory_depends_on: DF.Code | None + mask: DF.Check max_height: DF.Data | None no_copy: DF.Check non_negative: DF.Check @@ -158,3 +159,17 @@ class DocField(Document): parent = f" parent={self.parent}" if getattr(self, "parent", None) else "" return f"<{self.fieldtype}{doctype}: {self.fieldname}{docstatus}{parent}{unsaved}>" + + +# TODO: remove this function when all usages are removed +def get_masked_fields(doctype): + return frappe.db.get_values( + doctype="DocField", + filters={ + "parent": doctype, + "parentfield": "fields", + "mask": 1, + }, + fieldname="fieldname", + as_dict=True, + ) diff --git a/frappe/core/doctype/docperm/docperm.json b/frappe/core/doctype/docperm/docperm.json index 1b6e7fffc7..3c6591f3d4 100644 --- a/frappe/core/doctype/docperm/docperm.json +++ b/frappe/core/doctype/docperm/docperm.json @@ -22,6 +22,7 @@ "submit", "cancel", "amend", + "mask", "additional_permissions", "report", "export", @@ -205,18 +206,27 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Select" + }, + { + "default": "0", + "fieldname": "mask", + "fieldtype": "Check", + "label": "Mask" } ], + "grid_page_length": 50, "idx": 1, + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-03-23 16:02:18.443496", + "modified": "2025-05-20 16:50:32.679113", "modified_by": "Administrator", "module": "Core", "name": "DocPerm", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "ASC", "states": [] -} \ No newline at end of file +} diff --git a/frappe/core/doctype/docperm/docperm.py b/frappe/core/doctype/docperm/docperm.py index d014d7dae1..8aa54845c2 100644 --- a/frappe/core/doctype/docperm/docperm.py +++ b/frappe/core/doctype/docperm/docperm.py @@ -20,6 +20,7 @@ class DocPerm(Document): email: DF.Check export: DF.Check if_owner: DF.Check + mask: DF.Check parent: DF.Data parentfield: DF.Data parenttype: DF.Data diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index 47e0fcc865..b216bcfd3a 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -280,7 +280,7 @@ frappe.PermissionEngine = class PermissionEngine { add_check(cell, d, fieldname, label, description = "") { if (!label) label = toTitle(fieldname.replace(/_/g, " ")); - if (d.permlevel > 0 && ["read", "write"].indexOf(fieldname) == -1) { + if (d.permlevel > 0 && ["read", "write", "mask"].indexOf(fieldname) == -1) { return; } @@ -331,6 +331,7 @@ frappe.PermissionEngine = class PermissionEngine { "import", "export", "share", + "mask", ]; } diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 994624608c..f6eedf5801 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -241,6 +241,8 @@ def get_permitted_fields( with_virtual_fields=not ignore_virtual, ) + # print(doctype, " : In permitted fields, \n valid columns: ", permitted_fields, "\n\n\n") + if permission_type == "select": return permitted_fields diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 5f65ce50e4..0d8e848758 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -218,8 +218,57 @@ class DatabaseQuery: if pluck: return [d[pluck] for d in result] + if self.doctype and result: + result = self.mask_fields(result) + return result + def mask_fields(self, result): + """Mask fields in the result based on the doctype's masked fields""" + masked_fields = self.get_masked_fields() + if not masked_fields: + return result + + for row in result: + for field in masked_fields: + if field.fieldname in row: + fieldtype = field.fieldtype + val = row[field.fieldname] + if not val: + continue + + if fieldtype == "Data" and field.options == "Phone": + row[field.fieldname] = val[:3] + "********" + elif fieldtype == "Data" and field.options == "Email": + email = val.split("@") + row[field.fieldname] = "********@" + email[1] + elif fieldtype == "Date": + row[field.fieldname] = "xx-xx-xxxx" + elif fieldtype == "Time": + row[field.fieldname] = "xx-xx-xxxx" + else: + row[field.fieldname] = "********" + + return result + + def get_masked_fields(self): + """Get masked fields for the doctype""" + # TODO: store in session to avoid multiple calls + if not self.doctype: + return [] + + meta = self.get_meta(self.doctype) + + if not meta: + return [] + + mask_fields = [] + for field in meta.get_masked_fields(): + if not meta.has_permlevel_access_to(fieldname=field.fieldname, df=field, permission_type="mask"): + mask_fields.append(field) + + return mask_fields + def build_and_run(self): args = self.prepare_args() args.limit = self.add_limit() @@ -650,6 +699,7 @@ from {tables} if "." in column: table, column = column.split(".", 1) + print(i, "field", column, "permitted_fields") doctype = self.linked_table_aliases[table] if table in self.linked_table_aliases else table doctype = doctype.replace("`", "").removeprefix("tab") diff --git a/frappe/model/document.py b/frappe/model/document.py index c37bb4b7a0..fda529e1a7 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -269,8 +269,55 @@ class Document(BaseDocument, DocRef): if hasattr(self, "__setup__"): self.__setup__() + if not is_doctype: + self.mask_fields() + + print("inside document, \n\n\n\nn\n") + return self + def mask_fields(self): + mask_fields = frappe.get_meta(self.doctype).get_masked_fields() + if mask_fields: + # loop through masked fields and check it they have mask permissions on field level else mask value + for field in mask_fields: + if self.has_permlevel_access_to(fieldname=field.fieldname, permission_type="mask"): + # if user has access to mask field then skip masking + continue + + already_masked = False + field.read_only = 1 + field.mask_readonly = 1 + field.old_fieldtype = field.fieldtype + field.fieldtype = "Data" + + # if field type is Data and option is Phone the mask all value except last 3 + if field.old_fieldtype == "Data" and field.options == "Phone": + already_masked = True + self.set(field.fieldname, self.get(field.fieldname)[0:3] + "********") + + if field.old_fieldtype == "Data" and field.options == "Email": + already_masked = True + email = self.get(field.fieldname) + if email: + email = email.split("@") + self.set(field.fieldname, "********" + "@" + email[1]) + + if field.old_fieldtype == "Date": + already_masked = True + date = self.get(field.fieldname) + if date: + self.set(field.fieldname, "xx-xx-xxxx") + + if field.old_fieldtype == "Time": + already_masked = True + date = self.get(field.fieldname) + if date: + self.set(field.fieldname, "xx-xx-xxxx") + + if not already_masked: + self.set(field.fieldname, "********") + def load_children_from_db(self): is_doctype = self.doctype == "DocType" diff --git a/frappe/model/meta.py b/frappe/model/meta.py index f12557d537..d39686147c 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -213,6 +213,9 @@ class Meta(Document): def get_dynamic_link_fields(self): return self._dynamic_link_fields + def get_masked_fields(self): + return [df for df in self.fields if df.get("mask")] + @cached_property def _dynamic_link_fields(self): return self.get("fields", {"fieldtype": "Dynamic Link"}) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index cd69726fe0..194c5cd025 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -67,6 +67,7 @@ frappe.ui.form.Form = class FrappeForm { Cancel: "cancel", Amend: "amend", Delete: "delete", + Mask: "mask", }; } @@ -464,6 +465,8 @@ frappe.ui.form.Form = class FrappeForm { this.show_conflict_message(); this.show_submission_queue_banner(); + this.mark_mask_fields_readonly(); + if (frappe.boot.read_only) { this.disable_form(); } @@ -1170,6 +1173,16 @@ frappe.ui.form.Form = class FrappeForm { this.disable_save(); } + mark_mask_fields_readonly() { + this.fields.forEach((field) => { + if (field.df.mask && field.df.mask_readonly) { + // console.log(field.df); + this.set_df_property(field.df.fieldname, "disabled", "1"); + this.set_df_property(field.df.fieldname, "fieldtype", "Data"); + } + }); + } + handle_save_fail(btn, on_error) { $(btn).prop("disabled", false); if (on_error) { @@ -1834,6 +1847,7 @@ frappe.ui.form.Form = class FrappeForm { share: p.share, print: p.print, email: p.email, + mask: p.mask, }; }); this.refresh_fields(); diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 6caa979497..1738d23863 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -246,6 +246,12 @@ frappe.ui.form.Layout = class Layout { } init_field(df, parent, render = false) { + if (df.mask && df.mask_readonly) { + if (df.fieldtype !== "Data") { + df.original_fieldtype = df.fieldtype; + df.fieldtype = "Data"; + } + } const fieldobj = frappe.ui.form.make_control({ df: df, doctype: this.doctype, diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index bfcd95c131..91639c173c 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -526,6 +526,8 @@ frappe.views.BaseList = class BaseList { this.freeze(true); // fetch data from server return frappe.call(args).then((r) => { + // console.log(r, "list view response"); + // render this.prepare_data(r); this.toggle_result_area(); diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 763b1b7466..4d5bbf58f3 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -395,6 +395,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } const fields_in_list_view = this.get_fields_in_list_view(); + + // console.log(fields_in_list_view, "fields_in_list_view"); + // Add rest from in_list_view docfields this.columns = this.columns.concat( fields_in_list_view @@ -851,6 +854,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { _value = _value * out_of_ratings; } + if (df.fieldname == "company") { + console.log(df); + } + if (df.fieldtype === "Image") { html = df.options ? ` Date: Tue, 10 Jun 2025 19:42:49 +0530 Subject: [PATCH 002/144] feat: export encrypted data and add system setting --- .../system_settings/system_settings.json | 9 +- .../system_settings/system_settings.py | 1 + frappe/desk/form/meta.py | 19 +++- frappe/desk/reportview.py | 8 +- frappe/model/db_query.py | 30 +++--- frappe/model/document.py | 91 ++++++++++++------- frappe/model/meta.py | 30 +++++- .../js/frappe/form/controls/base_control.js | 2 +- frappe/public/js/frappe/form/form.js | 11 ++- frappe/public/js/frappe/list/list_view.js | 10 +- frappe/public/js/frappe/model/meta.js | 5 - 11 files changed, 143 insertions(+), 73 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 6c38d7f302..2061c27a12 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -27,6 +27,7 @@ "rounding_method", "permissions", "apply_strict_user_permissions", + "enable_data_masking", "column_break_21", "allow_older_web_view_links", "security_tab", @@ -707,12 +708,18 @@ "fieldname": "max_report_rows", "fieldtype": "Int", "label": "Max Report Rows" + }, + { + "default": "0", + "fieldname": "enable_data_masking", + "fieldtype": "Check", + "label": "Enable Data Masking" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2025-05-19 14:17:40.748786", + "modified": "2025-06-10 14:54:41.151334", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index 5caf06536c..f1fe86b8bd 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -49,6 +49,7 @@ class SystemSettings(Document): dormant_days: DF.Int email_footer_address: DF.SmallText | None email_retry_limit: DF.Int + enable_data_masking: DF.Check enable_onboarding: DF.Check enable_password_policy: DF.Check enable_scheduler: DF.Check diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index b592edefd0..db8f67ee53 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -5,7 +5,7 @@ import os import frappe from frappe import _ from frappe.build import scrub_html_template -from frappe.model.meta import Meta +from frappe.model.meta import Meta, is_data_masking_enabled from frappe.model.utils import render_include from frappe.modules import get_module_path, load_doctype_module, scrub from frappe.utils import get_bench_path, get_html_format @@ -48,6 +48,23 @@ def get_meta(doctype, cached=True) -> "FormMeta": # In prod don't use cached meta when explicitly requesting from DB. meta = FormMeta(doctype, cached=frappe.conf.developer_mode) + if (meta.name not in meta.special_doctypes) and is_data_masking_enabled(): + meta = mask_protected_fields(meta) + + return meta + + +def mask_protected_fields(meta): + for df in meta.fields: + if df.mask and not meta.has_permlevel_access_to( + fieldname=df.fieldname, df=df, permission_type="mask" + ): + # store orignal fieldtype and change fieldtype to Data + df.read_only = 1 + df.mask_readonly = 1 + df.set("old_fieldtype", df.get("old_fieldtype") or df.fieldtype) + if df.fieldtype != "Data": + df.fieldtype = "Data" return meta diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 43c83d0187..c8c3550178 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -375,7 +375,9 @@ def export_query(): form_params = get_form_params() form_params["limit_page_length"] = None - form_params["as_list"] = True + + # remove as_list param because key is needed for data masking + # form_params["as_list"] = True doctype = form_params.pop("doctype") if isinstance(form_params["fields"], list): form_params["fields"].append("owner") @@ -419,6 +421,10 @@ def export_query(): data = [[_("Sr"), *labels]] processed_data = [] + # convert ret to a list of lists if it is a list of dicts + if isinstance(ret, list) and ret and isinstance(ret[0], dict): + ret = [[value for value in row.values()] for row in ret] + if frappe.local.lang == "en" or not translate_values: data.extend([i + 1, *list(row)] for i, row in enumerate(ret)) elif translate_values: diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index a257bbba0c..b34c9c5c20 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -226,29 +226,28 @@ class DatabaseQuery: def mask_fields(self, result): """Mask fields in the result based on the doctype's masked fields""" masked_fields = self.get_masked_fields() + if not masked_fields: return result for row in result: for field in masked_fields: if field.fieldname in row: - fieldtype = field.fieldtype val = row[field.fieldname] if not val: continue - if fieldtype == "Data" and field.options == "Phone": - row[field.fieldname] = val[:3] + "********" - elif fieldtype == "Data" and field.options == "Email": + if field.old_fieldtype == "Data" and field.options == "Phone": + row[field.fieldname] = val[:3] + "XXXXXX" + elif field.old_fieldtype == "Data" and field.options == "Email": email = val.split("@") - row[field.fieldname] = "********@" + email[1] - elif fieldtype == "Date": - row[field.fieldname] = "xx-xx-xxxx" - elif fieldtype == "Time": - row[field.fieldname] = "xx-xx-xxxx" + row[field.fieldname] = "XXXXXX@" + email[1] + elif field.old_fieldtype == "Date": + row[field.fieldname] = "XX-XX-XXXX" + elif field.old_fieldtype == "Time": + row[field.fieldname] = "XX:XX" else: - row[field.fieldname] = "********" - + row[field.fieldname] = "XXXXXXXX" return result def get_masked_fields(self): @@ -262,12 +261,7 @@ class DatabaseQuery: if not meta: return [] - mask_fields = [] - for field in meta.get_masked_fields(): - if not meta.has_permlevel_access_to(fieldname=field.fieldname, df=field, permission_type="mask"): - mask_fields.append(field) - - return mask_fields + return meta.get_masked_fields() def build_and_run(self): args = self.prepare_args() @@ -699,7 +693,7 @@ from {tables} if "." in column: table, column = column.split(".", 1) - print(i, "field", column, "permitted_fields") + # print(i, "field", column, "permitted_fields") doctype = self.linked_table_aliases[table] if table in self.linked_table_aliases else table doctype = doctype.replace("`", "").removeprefix("tab") diff --git a/frappe/model/document.py b/frappe/model/document.py index c184089f5f..e81524bc84 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -273,51 +273,72 @@ class Document(BaseDocument, DocRef): if not is_doctype: self.mask_fields() - print("inside document, \n\n\n\nn\n") - return self + def mask_field_value(self, field): + # TODO: use this method to mask value and remove other code + already_masked = False + + if field.fieldtype == "Data" and field.options == "Phone": + already_masked = True + self.set(field.fieldname, self.get(field.fieldname)[0:3] + "XXXXXXX") + + if field.fieldtype == "Data" and field.options == "Email": + already_masked = True + email = self.get(field.fieldname) + if email: + email = email.split("@") + self.set(field.fieldname, "XXXXXXXX" + "@" + email[1]) + + if field.fieldtype == "Date": + already_masked = True + date = self.get(field.fieldname) + if date: + self.set(field.fieldname, "XX-XX-XXXX") + + if field.fieldtype == "Time": + already_masked = True + date = self.get(field.fieldname) + if date: + self.set(field.fieldname, "XX:XX") + + if not already_masked: + self.set(field.fieldname, "XXXXXXXX") + def mask_fields(self): mask_fields = frappe.get_meta(self.doctype).get_masked_fields() - if mask_fields: - # loop through masked fields and check it they have mask permissions on field level else mask value - for field in mask_fields: - if self.has_permlevel_access_to(fieldname=field.fieldname, permission_type="mask"): - # if user has access to mask field then skip masking - continue - already_masked = False - field.read_only = 1 - field.mask_readonly = 1 - field.old_fieldtype = field.fieldtype - field.fieldtype = "Data" + if not mask_fields: + return + for field in mask_fields: + already_masked = False - # if field type is Data and option is Phone the mask all value except last 3 - if field.old_fieldtype == "Data" and field.options == "Phone": - already_masked = True - self.set(field.fieldname, self.get(field.fieldname)[0:3] + "********") + # if field type is Data and option is Phone the mask all value except last 3 + if field.old_fieldtype == "Data" and field.options == "Phone": + already_masked = True + self.set(field.fieldname, self.get(field.fieldname)[0:3] + "XXXXXXX") - if field.old_fieldtype == "Data" and field.options == "Email": - already_masked = True - email = self.get(field.fieldname) - if email: - email = email.split("@") - self.set(field.fieldname, "********" + "@" + email[1]) + if field.old_fieldtype == "Data" and field.options == "Email": + already_masked = True + email = self.get(field.fieldname) + if email: + email = email.split("@") + self.set(field.fieldname, "XXXXXXXX" + "@" + email[1]) - if field.old_fieldtype == "Date": - already_masked = True - date = self.get(field.fieldname) - if date: - self.set(field.fieldname, "xx-xx-xxxx") + if field.old_fieldtype == "Date": + already_masked = True + date = self.get(field.fieldname) + if date: + self.set(field.fieldname, "XX-XX-XXXX") - if field.old_fieldtype == "Time": - already_masked = True - date = self.get(field.fieldname) - if date: - self.set(field.fieldname, "xx-xx-xxxx") + if field.old_fieldtype == "Time": + already_masked = True + date = self.get(field.fieldname) + if date: + self.set(field.fieldname, "XX:XXXX") - if not already_masked: - self.set(field.fieldname, "********") + if not already_masked: + self.set(field.fieldname, "XXXXXXXX") def load_children_from_db(self): is_doctype = self.doctype == "DocType" diff --git a/frappe/model/meta.py b/frappe/model/meta.py index d39686147c..3943507999 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -42,6 +42,7 @@ from frappe.model.workflow import get_workflow_name from frappe.modules import load_doctype_module from frappe.types import DocRef from frappe.utils import cached_property, cast, cint, cstr +from frappe.utils.caching import request_cache from frappe.utils.data import add_to_date, get_datetime DEFAULT_FIELD_LABELS = { @@ -83,8 +84,16 @@ def get_meta(doctype: "str | DocType", cached: bool = True) -> "_Meta": return meta meta = Meta(doctype) + key = f"doctype_meta::{meta.name}" frappe.client_cache.set_value(key, meta) + + if meta.name not in meta.special_doctypes: + if is_data_masking_enabled(): + from frappe.desk.form.meta import mask_protected_fields + + meta = mask_protected_fields(meta) + return meta @@ -96,6 +105,10 @@ def clear_meta_cache(doctype: str = "*"): frappe.client_cache.delete_value(key) +def is_data_masking_enabled(): + return frappe.db.get_single_value("System Settings", "enable_data_masking") + + def load_meta(doctype): return Meta(doctype) @@ -214,7 +227,22 @@ class Meta(Document): return self._dynamic_link_fields def get_masked_fields(self): - return [df for df in self.fields if df.get("mask")] + # print(self.fields, "Indise the meta yes \n\n\n") + # return [df for df in self.fields if df.get("mask")] + # print("inside mask fields: ", self.get("fields", {"mask": 1}), "\n\n\n") + # return self.get("fields", {"mask": 1}) + return self.get("fields", {"mask_readonly": 1}) + # fields = self.get("fields", {"mask_readonly": 1}) + # change fieldtype to Data for masked fields + # print("fields: ", fields, "\n\n\n") + # for df in fields: + # print("df: ", df, "\n\n\n") + # if df.get("fieldtype") != "Data": + # df.old_fieldtype = df.fieldtype + # df.fieldtype = "Data" + # return fields + + # return [df for df in self.get("fields", {"mask": 1}) if df.get("fieldtype") != "Data"] @cached_property def _dynamic_link_fields(self): diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index d56f269f80..dfb241ca95 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -9,7 +9,7 @@ frappe.ui.form.Control = class BaseControl { make() { this.make_wrapper(); this.$wrapper - .attr("data-fieldtype", this.df.fieldtype) + .attr("data-fieldtype", this.df?.old_fieldtype || this.df.fieldtype) .attr("data-fieldname", this.df.fieldname); this.wrapper = this.$wrapper.get(0); this.wrapper.fieldobj = this; // reference for event handlers diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 194c5cd025..673f01cd81 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -465,7 +465,9 @@ frappe.ui.form.Form = class FrappeForm { this.show_conflict_message(); this.show_submission_queue_banner(); - this.mark_mask_fields_readonly(); + if (!this.is_new()) { + this.mark_mask_fields_readonly(); + } if (frappe.boot.read_only) { this.disable_form(); @@ -1176,11 +1178,12 @@ frappe.ui.form.Form = class FrappeForm { mark_mask_fields_readonly() { this.fields.forEach((field) => { if (field.df.mask && field.df.mask_readonly) { - // console.log(field.df); - this.set_df_property(field.df.fieldname, "disabled", "1"); - this.set_df_property(field.df.fieldname, "fieldtype", "Data"); + console.log(field.df); + // this.set_df_property(field.df.fieldname, "disabled", "1"); + this.set_df_property(field.df.fieldname, "fieldtype", "Date"); } }); + // this.refresh(); } handle_save_fail(btn, on_error) { diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 4d5bbf58f3..ab9149f2d2 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -854,9 +854,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { _value = _value * out_of_ratings; } - if (df.fieldname == "company") { - console.log(df); - } + let filterable = df?.mask_readonly ? "no-underline" : " filterable"; if (df.fieldtype === "Image") { html = df.options @@ -866,14 +864,14 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { ${frappe.utils.icon("restriction")} `; } else if (df.fieldtype === "Select") { - html = ` ${__(_value)} `; } else if (df.fieldtype === "Link") { - html = ` ${_value} `; @@ -882,7 +880,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { ${_value} `; } else { - html = ` ${format()} `; diff --git a/frappe/public/js/frappe/model/meta.js b/frappe/public/js/frappe/model/meta.js index fca8500e9a..9724b0729f 100644 --- a/frappe/public/js/frappe/model/meta.js +++ b/frappe/public/js/frappe/model/meta.js @@ -17,11 +17,6 @@ frappe.get_meta = function (doctype) { $.extend(frappe.meta, { sync: function (doc) { $.each(doc.fields, function (i, df) { - if (df.mask) { - // console.log("inside meta", frappe.meta.get_masked_fields()); - // df.mask_readonly = 1; - console.log(df); - } frappe.meta.add_field(df); }); From aba7f29aa68f2b349fbeeb39b7b393d8f3964678 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Tue, 10 Jun 2025 19:54:13 +0530 Subject: [PATCH 003/144] refactor: remove debugging statement --- frappe/core/doctype/docfield/docfield.py | 14 -------------- frappe/model/__init__.py | 2 -- frappe/model/db_query.py | 1 - frappe/model/meta.py | 15 --------------- frappe/public/js/frappe/form/form.js | 4 +--- frappe/public/js/frappe/list/base_list.js | 2 -- frappe/public/js/frappe/list/list_view.js | 3 --- 7 files changed, 1 insertion(+), 40 deletions(-) diff --git a/frappe/core/doctype/docfield/docfield.py b/frappe/core/doctype/docfield/docfield.py index 00f0a80b00..3f6d642e55 100644 --- a/frappe/core/doctype/docfield/docfield.py +++ b/frappe/core/doctype/docfield/docfield.py @@ -159,17 +159,3 @@ class DocField(Document): parent = f" parent={self.parent}" if getattr(self, "parent", None) else "" return f"<{self.fieldtype}{doctype}: {self.fieldname}{docstatus}{parent}{unsaved}>" - - -# TODO: remove this function when all usages are removed -def get_masked_fields(doctype): - return frappe.db.get_values( - doctype="DocField", - filters={ - "parent": doctype, - "parentfield": "fields", - "mask": 1, - }, - fieldname="fieldname", - as_dict=True, - ) diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index f6eedf5801..994624608c 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -241,8 +241,6 @@ def get_permitted_fields( with_virtual_fields=not ignore_virtual, ) - # print(doctype, " : In permitted fields, \n valid columns: ", permitted_fields, "\n\n\n") - if permission_type == "select": return permitted_fields diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 27563323fa..3fc3de7e83 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -694,7 +694,6 @@ from {tables} if "." in column: table, column = column.split(".", 1) - # print(i, "field", column, "permitted_fields") doctype = self.linked_table_aliases[table] if table in self.linked_table_aliases else table doctype = doctype.replace("`", "").removeprefix("tab") diff --git a/frappe/model/meta.py b/frappe/model/meta.py index c182a74e22..b137db862d 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -227,22 +227,7 @@ class Meta(Document): return self._dynamic_link_fields def get_masked_fields(self): - # print(self.fields, "Indise the meta yes \n\n\n") - # return [df for df in self.fields if df.get("mask")] - # print("inside mask fields: ", self.get("fields", {"mask": 1}), "\n\n\n") - # return self.get("fields", {"mask": 1}) return self.get("fields", {"mask_readonly": 1}) - # fields = self.get("fields", {"mask_readonly": 1}) - # change fieldtype to Data for masked fields - # print("fields: ", fields, "\n\n\n") - # for df in fields: - # print("df: ", df, "\n\n\n") - # if df.get("fieldtype") != "Data": - # df.old_fieldtype = df.fieldtype - # df.fieldtype = "Data" - # return fields - - # return [df for df in self.get("fields", {"mask": 1}) if df.get("fieldtype") != "Data"] @cached_property def _dynamic_link_fields(self): diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 673f01cd81..b9c3794d24 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1178,12 +1178,10 @@ frappe.ui.form.Form = class FrappeForm { mark_mask_fields_readonly() { this.fields.forEach((field) => { if (field.df.mask && field.df.mask_readonly) { - console.log(field.df); - // this.set_df_property(field.df.fieldname, "disabled", "1"); + this.set_df_property(field.df.fieldname, "disabled", "1"); this.set_df_property(field.df.fieldname, "fieldtype", "Date"); } }); - // this.refresh(); } handle_save_fail(btn, on_error) { diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index b58bfef3b4..f1b54301ad 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -526,8 +526,6 @@ frappe.views.BaseList = class BaseList { this.freeze(true); // fetch data from server return frappe.call(args).then((r) => { - // console.log(r, "list view response"); - // render this.prepare_data(r); this.toggle_result_area(); diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 7ba60a23a9..96214e182c 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -395,9 +395,6 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } const fields_in_list_view = this.get_fields_in_list_view(); - - // console.log(fields_in_list_view, "fields_in_list_view"); - // Add rest from in_list_view docfields this.columns = this.columns.concat( fields_in_list_view From f32f9f1d83d07f1dc1618f3acc8c45390ac1247c Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Wed, 11 Jun 2025 10:48:09 +0530 Subject: [PATCH 004/144] fix(minor): typo of data --- 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 b9c3794d24..418744ef46 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1179,7 +1179,7 @@ frappe.ui.form.Form = class FrappeForm { this.fields.forEach((field) => { if (field.df.mask && field.df.mask_readonly) { this.set_df_property(field.df.fieldname, "disabled", "1"); - this.set_df_property(field.df.fieldname, "fieldtype", "Date"); + this.set_df_property(field.df.fieldname, "fieldtype", "Data"); } }); } From 76f2221b1fa301fcab41df915baa2277d6b13453 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Wed, 11 Jun 2025 20:25:13 +0530 Subject: [PATCH 005/144] fix: don't mask fields in patch, install or in migrate state --- frappe/desk/form/meta.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index db8f67ee53..ed7ec8b2f7 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -55,6 +55,14 @@ def get_meta(doctype, cached=True) -> "FormMeta": def mask_protected_fields(meta): + if ( + frappe.flags.in_patch + or frappe.flags.in_install + or frappe.flags.in_migrate + or frappe.flags.in_setup_wizard + ): + return meta + for df in meta.fields: if df.mask and not meta.has_permlevel_access_to( fieldname=df.fieldname, df=df, permission_type="mask" From 4613129c4c6095a1030b8a3671ca0196271ce37d Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Wed, 11 Jun 2025 20:38:29 +0530 Subject: [PATCH 006/144] fix: don't mask fields in patch, install or in migrate state --- frappe/desk/form/meta.py | 29 +++++++++++++++++------------ frappe/model/meta.py | 9 ++------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index ed7ec8b2f7..eda9672b17 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -5,7 +5,7 @@ import os import frappe from frappe import _ from frappe.build import scrub_html_template -from frappe.model.meta import Meta, is_data_masking_enabled +from frappe.model.meta import Meta from frappe.model.utils import render_include from frappe.modules import get_module_path, load_doctype_module, scrub from frappe.utils import get_bench_path, get_html_format @@ -48,7 +48,7 @@ def get_meta(doctype, cached=True) -> "FormMeta": # In prod don't use cached meta when explicitly requesting from DB. meta = FormMeta(doctype, cached=frappe.conf.developer_mode) - if (meta.name not in meta.special_doctypes) and is_data_masking_enabled(): + if meta.name not in meta.special_doctypes: meta = mask_protected_fields(meta) return meta @@ -63,19 +63,24 @@ def mask_protected_fields(meta): ): return meta - for df in meta.fields: - if df.mask and not meta.has_permlevel_access_to( - fieldname=df.fieldname, df=df, permission_type="mask" - ): - # store orignal fieldtype and change fieldtype to Data - df.read_only = 1 - df.mask_readonly = 1 - df.set("old_fieldtype", df.get("old_fieldtype") or df.fieldtype) - if df.fieldtype != "Data": - df.fieldtype = "Data" + if is_data_masking_enabled(): + for df in meta.fields: + if df.mask and not meta.has_permlevel_access_to( + fieldname=df.fieldname, df=df, permission_type="mask" + ): + # store orignal fieldtype and change fieldtype to Data + df.read_only = 1 + df.mask_readonly = 1 + df.set("old_fieldtype", df.get("old_fieldtype") or df.fieldtype) + if df.fieldtype != "Data": + df.fieldtype = "Data" return meta +def is_data_masking_enabled(): + return frappe.db.get_single_value("System Settings", "enable_data_masking") + + class FormMeta(Meta): def __init__(self, doctype, *, cached=True): self.__dict__.update(frappe.get_meta(doctype, cached=cached).__dict__) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index b137db862d..054c963b29 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -89,10 +89,9 @@ def get_meta(doctype: "str | DocType", cached: bool = True) -> "_Meta": frappe.client_cache.set_value(key, meta) if meta.name not in meta.special_doctypes: - if is_data_masking_enabled(): - from frappe.desk.form.meta import mask_protected_fields + from frappe.desk.form.meta import mask_protected_fields - meta = mask_protected_fields(meta) + meta = mask_protected_fields(meta) return meta @@ -105,10 +104,6 @@ def clear_meta_cache(doctype: str = "*"): frappe.client_cache.delete_value(key) -def is_data_masking_enabled(): - return frappe.db.get_single_value("System Settings", "enable_data_masking") - - def load_meta(doctype): return Meta(doctype) From 4a866ca3700bc13698e43749c0dc9ff8dfb4937c Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Thu, 26 Jun 2025 13:00:27 +0530 Subject: [PATCH 007/144] refactor: remove useless conditions --- frappe/model/db_query.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 3fc3de7e83..80e61b9abc 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -253,15 +253,9 @@ class DatabaseQuery: def get_masked_fields(self): """Get masked fields for the doctype""" - # TODO: store in session to avoid multiple calls - if not self.doctype: - return [] meta = self.get_meta(self.doctype) - if not meta: - return [] - return meta.get_masked_fields() def build_and_run(self): From cbcf16440aeb5c776d8956e62a2ba97edcbae1ae Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Thu, 26 Jun 2025 13:06:44 +0530 Subject: [PATCH 008/144] refactor: remove masking setting from System Settings --- .../system_settings/system_settings.json | 9 +------ .../system_settings/system_settings.py | 1 - frappe/desk/form/meta.py | 25 ++++++++----------- frappe/model/db_query.py | 10 ++++++++ frappe/model/document.py | 1 + 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index b9a78e78a1..619894f153 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -28,7 +28,6 @@ "show_absolute_datetime_in_timeline", "permissions", "apply_strict_user_permissions", - "enable_data_masking", "column_break_21", "allow_older_web_view_links", "security_tab", @@ -718,12 +717,6 @@ "fieldtype": "Check", "label": "Show Absolute Datetime in Timeline" }, - { - "default": "0", - "fieldname": "enable_data_masking", - "fieldtype": "Check", - "label": "Enable Data Masking" - }, { "fieldname": "api_logging_section", "fieldtype": "Section Break", @@ -739,7 +732,7 @@ "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2025-06-10 14:54:41.151334", + "modified": "2025-06-26 13:03:49.011134", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index 70efbf4a1e..323c4a3f54 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -49,7 +49,6 @@ class SystemSettings(Document): dormant_days: DF.Int email_footer_address: DF.SmallText | None email_retry_limit: DF.Int - enable_data_masking: DF.Check enable_onboarding: DF.Check enable_password_policy: DF.Check enable_scheduler: DF.Check diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index d51a4a27b0..b6ca3f31d5 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -60,24 +60,19 @@ def mask_protected_fields(meta): ): return meta - if is_data_masking_enabled(): - for df in meta.fields: - if df.mask and not meta.has_permlevel_access_to( - fieldname=df.fieldname, df=df, permission_type="mask" - ): - # store orignal fieldtype and change fieldtype to Data - df.read_only = 1 - df.mask_readonly = 1 - df.set("old_fieldtype", df.get("old_fieldtype") or df.fieldtype) - if df.fieldtype != "Data": - df.fieldtype = "Data" + for df in meta.fields: + if df.mask and not meta.has_permlevel_access_to( + fieldname=df.fieldname, df=df, permission_type="mask" + ): + # store orignal fieldtype and change fieldtype to Data + df.read_only = 1 + df.mask_readonly = 1 + df.set("old_fieldtype", df.get("old_fieldtype") or df.fieldtype) + if df.fieldtype != "Data": + df.fieldtype = "Data" return meta -def is_data_masking_enabled(): - return frappe.db.get_single_value("System Settings", "enable_data_masking") - - class FormMeta(Meta): def __init__(self, doctype, *, cached=True): self.__dict__.update(frappe.get_meta(doctype, cached=cached).__dict__) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 80e61b9abc..0689689016 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -659,6 +659,16 @@ from {tables} ignore_virtual=True, ) ) + + # get_permitted_field = get_permitted_fields( + # doctype=self.doctype, + # parenttype=self.parent_doctype, + # permission_type=self.permission_map.get(self.doctype), + # ignore_virtual=True, + # ) + + print(self.doctype, self.permission_map.get(self.doctype), "permitted_fields \n\n\n\n") + # print(get_permitted_field, "get_permitted_field \n\n\n\n") permitted_child_table_fields = {} for i, field in enumerate(self.fields): diff --git a/frappe/model/document.py b/frappe/model/document.py index 9b581d022e..698b01718f 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -979,6 +979,7 @@ class Document(BaseDocument): return # check for child tables + print(high_permlevel_fields, "high_permlevel_fields \n\n\n") for df in self.meta.get_table_fields(): high_permlevel_fields = frappe.get_meta(df.options).get_high_permlevel_fields() if high_permlevel_fields: From c2544f9096db2c34fd43040d4c9526a17470efe7 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Mon, 18 Aug 2025 23:38:18 +0530 Subject: [PATCH 009/144] refactor: change approach of masking fields --- frappe/desk/form/meta.py | 12 ---- frappe/desk/reportview.py | 7 +- frappe/model/db_query.py | 72 ++++++++++++------- frappe/model/document.py | 63 ++-------------- frappe/model/meta.py | 5 +- .../js/frappe/form/controls/base_control.js | 2 +- frappe/public/js/frappe/form/formatters.js | 2 +- frappe/public/js/frappe/form/layout.js | 2 +- 8 files changed, 56 insertions(+), 109 deletions(-) diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index b6ca3f31d5..fc65fc49b3 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -52,24 +52,12 @@ def get_meta(doctype, cached=True) -> "FormMeta": def mask_protected_fields(meta): - if ( - frappe.flags.in_patch - or frappe.flags.in_install - or frappe.flags.in_migrate - or frappe.flags.in_setup_wizard - ): - return meta - for df in meta.fields: if df.mask and not meta.has_permlevel_access_to( fieldname=df.fieldname, df=df, permission_type="mask" ): # store orignal fieldtype and change fieldtype to Data - df.read_only = 1 df.mask_readonly = 1 - df.set("old_fieldtype", df.get("old_fieldtype") or df.fieldtype) - if df.fieldtype != "Data": - df.fieldtype = "Data" return meta diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 9621b44402..3832052e87 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -376,8 +376,7 @@ def export_query(): form_params = get_form_params() form_params["limit_page_length"] = None - # remove as_list param because key is needed for data masking - # form_params["as_list"] = True + form_params["as_list"] = True doctype = form_params.pop("doctype") if isinstance(form_params["fields"], list): form_params["fields"].append("owner") @@ -421,10 +420,6 @@ def export_query(): data = [[_("Sr"), *labels]] processed_data = [] - # convert ret to a list of lists if it is a list of dicts - if isinstance(ret, list) and ret and isinstance(ret[0], dict): - ret = [[value for value in row.values()] for row in ret] - if frappe.local.lang == "en" or not translate_values: data.extend([i + 1, *list(row)] for i, row in enumerate(ret)) elif translate_values: diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 3ba56ee575..1180fd1332 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -231,24 +231,36 @@ class DatabaseQuery: if not masked_fields: return result - for row in result: - for field in masked_fields: - if field.fieldname in row: - val = row[field.fieldname] - if not val: - continue + if self.as_list: + masked_result = [] + field_index_map = {} + for idx, field in enumerate(self.fields): + # handle aliases (e.g. `tabSI`.`posting_date` as posting_date) + if " as " in field.lower(): + alias = field.split(" as ")[1].strip(" '") + field_index_map[alias] = idx + else: + # extract last part after `.` + col = field.split(".")[-1].strip("`") + field_index_map[col] = idx + # if as_list then we don't have field names in the result so we need to mask by position + for row in result: + row = list(row) # convert tuple to list mutable + for field in masked_fields: + if field.fieldname in field_index_map: + idx = field_index_map[field.fieldname] + val = row[idx] + row[idx] = mask_field_value(field, val) + + masked_result.append(tuple(row)) # convert back to tuple + result = masked_result + else: + for row in result: + for field in masked_fields: + if field.fieldname in row: + val = row[field.fieldname] + row[field.fieldname] = mask_field_value(field, val) - if field.old_fieldtype == "Data" and field.options == "Phone": - row[field.fieldname] = val[:3] + "XXXXXX" - elif field.old_fieldtype == "Data" and field.options == "Email": - email = val.split("@") - row[field.fieldname] = "XXXXXX@" + email[1] - elif field.old_fieldtype == "Date": - row[field.fieldname] = "XX-XX-XXXX" - elif field.old_fieldtype == "Time": - row[field.fieldname] = "XX:XX" - else: - row[field.fieldname] = "XXXXXXXX" return result def get_masked_fields(self): @@ -660,15 +672,6 @@ from {tables} ) ) - # get_permitted_field = get_permitted_fields( - # doctype=self.doctype, - # parenttype=self.parent_doctype, - # permission_type=self.permission_map.get(self.doctype), - # ignore_virtual=True, - # ) - - print(self.doctype, self.permission_map.get(self.doctype), "permitted_fields \n\n\n\n") - # print(get_permitted_field, "get_permitted_field \n\n\n\n") permitted_child_table_fields = {} for i, field in enumerate(self.fields): @@ -1239,6 +1242,23 @@ from {tables} update_user_settings(self.doctype, user_settings) +def mask_field_value(field, val): + if not val: + return val + + if field.fieldtype == "Data" and field.options == "Phone": + return val[:3] + "XXXXXX" + elif field.fieldtype == "Data" and field.options == "Email": + email = val.split("@") + return "XXXXXX@" + email[1] if len(email) > 1 else "XXXXXX" + elif field.fieldtype == "Date": + return "XX-XX-XXXX" + elif field.fieldtype == "Time": + return "XX:XX" + else: + return "XXXXXXXX" + + def cast_name(column: str) -> str: """Casts name field to varchar for postgres diff --git a/frappe/model/document.py b/frappe/model/document.py index 9e342e7381..1825543ff2 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -273,70 +273,16 @@ class Document(BaseDocument): return self - def mask_field_value(self, field): - # TODO: use this method to mask value and remove other code - already_masked = False - - if field.fieldtype == "Data" and field.options == "Phone": - already_masked = True - self.set(field.fieldname, self.get(field.fieldname)[0:3] + "XXXXXXX") - - if field.fieldtype == "Data" and field.options == "Email": - already_masked = True - email = self.get(field.fieldname) - if email: - email = email.split("@") - self.set(field.fieldname, "XXXXXXXX" + "@" + email[1]) - - if field.fieldtype == "Date": - already_masked = True - date = self.get(field.fieldname) - if date: - self.set(field.fieldname, "XX-XX-XXXX") - - if field.fieldtype == "Time": - already_masked = True - date = self.get(field.fieldname) - if date: - self.set(field.fieldname, "XX:XX") - - if not already_masked: - self.set(field.fieldname, "XXXXXXXX") - def mask_fields(self): + from frappe.model.db_query import mask_field_value + mask_fields = frappe.get_meta(self.doctype).get_masked_fields() if not mask_fields: return for field in mask_fields: - already_masked = False - - # if field type is Data and option is Phone the mask all value except last 3 - if field.old_fieldtype == "Data" and field.options == "Phone": - already_masked = True - self.set(field.fieldname, self.get(field.fieldname)[0:3] + "XXXXXXX") - - if field.old_fieldtype == "Data" and field.options == "Email": - already_masked = True - email = self.get(field.fieldname) - if email: - email = email.split("@") - self.set(field.fieldname, "XXXXXXXX" + "@" + email[1]) - - if field.old_fieldtype == "Date": - already_masked = True - date = self.get(field.fieldname) - if date: - self.set(field.fieldname, "XX-XX-XXXX") - - if field.old_fieldtype == "Time": - already_masked = True - date = self.get(field.fieldname) - if date: - self.set(field.fieldname, "XX:XXXX") - - if not already_masked: - self.set(field.fieldname, "XXXXXXXX") + val = self.get(field.fieldname) + self.set(field.fieldname, mask_field_value(field, val)) def load_children_from_db(self): is_doctype = self.doctype == "DocType" @@ -981,7 +927,6 @@ class Document(BaseDocument): return # check for child tables - print(high_permlevel_fields, "high_permlevel_fields \n\n\n") for df in self.meta.get_table_fields(): high_permlevel_fields = frappe.get_meta(df.options).get_high_permlevel_fields() if high_permlevel_fields: diff --git a/frappe/model/meta.py b/frappe/model/meta.py index c2405dd960..11dc18b7df 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -88,14 +88,13 @@ def get_meta(doctype: "str | DocType", cached: bool = True) -> "_Meta": meta = Meta(doctype) - key = f"doctype_meta::{meta.name}" - frappe.client_cache.set_value(key, meta) - if meta.name not in meta.special_doctypes: from frappe.desk.form.meta import mask_protected_fields meta = mask_protected_fields(meta) + key = f"doctype_meta::{meta.name}" + frappe.client_cache.set_value(key, meta) return meta diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index dfb241ca95..d56f269f80 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -9,7 +9,7 @@ frappe.ui.form.Control = class BaseControl { make() { this.make_wrapper(); this.$wrapper - .attr("data-fieldtype", this.df?.old_fieldtype || this.df.fieldtype) + .attr("data-fieldtype", this.df.fieldtype) .attr("data-fieldname", this.df.fieldname); this.wrapper = this.$wrapper.get(0); this.wrapper.fieldobj = this; // reference for event handlers diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index c068502fc7..b28d7cdfec 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -413,7 +413,7 @@ frappe.form.get_formatter = function (fieldtype) { }; frappe.format = function (value, df, options, doc) { - if (!df) df = { fieldtype: "Data" }; + if (!df || df?.mask_readonly) df = { fieldtype: "Data" }; if (df.fieldname == "_user_tags") df = { ...df, fieldtype: "Tag" }; var fieldtype = df.fieldtype || "Data"; diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 1738d23863..46ee93f645 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -248,7 +248,7 @@ frappe.ui.form.Layout = class Layout { init_field(df, parent, render = false) { if (df.mask && df.mask_readonly) { if (df.fieldtype !== "Data") { - df.original_fieldtype = df.fieldtype; + df.read_only = 1; df.fieldtype = "Data"; } } From d8e0d8b232a178527b643a613d8b7a710c7f34c9 Mon Sep 17 00:00:00 2001 From: octex Date: Tue, 19 Aug 2025 12:18:05 -0300 Subject: [PATCH 010/144] fix: Fixed issue with translated options for dynamic link in list view filter --- frappe/public/js/frappe/form/controls/dynamic_link.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/dynamic_link.js b/frappe/public/js/frappe/form/controls/dynamic_link.js index 8f64a167dc..04092b0be2 100644 --- a/frappe/public/js/frappe/form/controls/dynamic_link.js +++ b/frappe/public/js/frappe/form/controls/dynamic_link.js @@ -7,17 +7,12 @@ frappe.ui.form.ControlDynamicLink = class ControlDynamicLink extends frappe.ui.f //for dialog box options = cur_dialog.get_value(this.df.options); } else if (!cur_frm) { - const selector = `input[data-fieldname="${this.df.options}"]`; - let input = null; if (cur_list) { // for list page - input = cur_list.filter_area.standard_filters_wrapper.find(selector); + options = cur_list.page.fields_dict[this.df.options].get_input_value(); } if (cur_page) { - input = $(cur_page.page).find(selector); - } - if (input) { - options = input.val(); + options = cur_page.page.fields_dict[this.df.options].get_input_value(); } } else { options = frappe.model.get_value(this.df.parent, this.docname, this.df.options); From d579a6e26c790b145b1ea8c738e0e5c31de9db26 Mon Sep 17 00:00:00 2001 From: octex Date: Tue, 19 Aug 2025 16:08:02 -0300 Subject: [PATCH 011/144] fix: fields_dict does not exists in cur_page --- frappe/public/js/frappe/form/controls/dynamic_link.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/dynamic_link.js b/frappe/public/js/frappe/form/controls/dynamic_link.js index 04092b0be2..a08996d319 100644 --- a/frappe/public/js/frappe/form/controls/dynamic_link.js +++ b/frappe/public/js/frappe/form/controls/dynamic_link.js @@ -11,8 +11,11 @@ frappe.ui.form.ControlDynamicLink = class ControlDynamicLink extends frappe.ui.f // for list page options = cur_list.page.fields_dict[this.df.options].get_input_value(); } - if (cur_page) { - options = cur_page.page.fields_dict[this.df.options].get_input_value(); + else if (cur_page) { + const selector = `input[data-fieldname="${this.df.options}"]`; + let input = null; + input = $(cur_page.page).find(selector); + options = input.val(); } } else { options = frappe.model.get_value(this.df.parent, this.docname, this.df.options); From 09134f44cb6d93620454463a28046cf13755dce1 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Tue, 2 Sep 2025 10:58:40 +0530 Subject: [PATCH 012/144] fix: access attribute using get method --- frappe/desk/form/meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index fc65fc49b3..151a9b935f 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -53,7 +53,7 @@ def get_meta(doctype, cached=True) -> "FormMeta": def mask_protected_fields(meta): for df in meta.fields: - if df.mask and not meta.has_permlevel_access_to( + if df.get("mask") and not meta.has_permlevel_access_to( fieldname=df.fieldname, df=df, permission_type="mask" ): # store orignal fieldtype and change fieldtype to Data From a13bf7246f1045a2d31f2317c55cbe2d4edf91d1 Mon Sep 17 00:00:00 2001 From: sokumon Date: Wed, 30 Jul 2025 19:57:51 +0530 Subject: [PATCH 013/144] fix: prevent row-size limit error --- frappe/database/schema.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frappe/database/schema.py b/frappe/database/schema.py index cb0afb4281..40702427d0 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -436,6 +436,8 @@ def get_definition(fieldtype, precision=None, length=None, *, options=None): if length: if coltype == "varchar": + if length < 64: + length = 64 size = length elif coltype == "int" and length < 11: # allow setting custom length for int if length provided is less than 11 @@ -470,5 +472,9 @@ def add_column(doctype, column_name, fieldtype, precision=None, length=None, def query += " not null" if default: query += f" default '{default}'" - - frappe.db.sql(query) + try: + frappe.db.sql(query) + except Exception as err: + # 1118 is error code for the row size limit exceeded error + if hasattr(err, "args") and err.args[0] == 1118: + frappe.db.rollback() From 3cb061bacb212c11c4408549405e38d2b1e7e36a Mon Sep 17 00:00:00 2001 From: sokumon Date: Mon, 4 Aug 2025 19:32:34 +0530 Subject: [PATCH 014/144] fix: add reference and test_case --- frappe/database/schema.py | 9 +++------ frappe/tests/test_db_update.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 40702427d0..a3d1d83a0b 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -436,6 +436,7 @@ def get_definition(fieldtype, precision=None, length=None, *, options=None): if length: if coltype == "varchar": + # Reference: https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-row-formats/troubleshooting-row-size-too-large-errors-with-innodb if length < 64: length = 64 size = length @@ -472,9 +473,5 @@ def add_column(doctype, column_name, fieldtype, precision=None, length=None, def query += " not null" if default: query += f" default '{default}'" - try: - frappe.db.sql(query) - except Exception as err: - # 1118 is error code for the row size limit exceeded error - if hasattr(err, "args") and err.args[0] == 1118: - frappe.db.rollback() + + frappe.db.sql(query) diff --git a/frappe/tests/test_db_update.py b/frappe/tests/test_db_update.py index dc150b1be8..dd9a985ba6 100644 --- a/frappe/tests/test_db_update.py +++ b/frappe/tests/test_db_update.py @@ -176,6 +176,20 @@ class TestDBUpdate(IntegrationTestCase): self.assertEqual(frappe.db.get_column_type(referring_doctype.name, link), "uuid") + @run_only_if(db_type_is.MARIADB) + def test_row_size(self): + from frappe.database.schema import add_column + from frappe.utils import get_table_name + + test_doc = new_doctype().insert() + try: + for i in range(400): + add_column(test_doc.name, fieldtype="Data", column_name=f"col{i}", length=63) + except Exception as e: + print(e) + finally: + frappe.db.sql_ddl(f"drop table `{get_table_name(test_doc.name)}`") + class TestDBUpdateSanityChecks(IntegrationTestCase): @run_only_if(db_type_is.MARIADB) From d80e41cca3f64279012b6dd7bbdf49fd9a9fd05a Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Wed, 17 Sep 2025 17:15:41 +0530 Subject: [PATCH 015/144] feat: mask queery report data --- frappe/desk/query_report.py | 13 ++++++++- .../js/frappe/views/reports/query_report.js | 27 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index b20cc92292..5c3f20c1e3 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -674,9 +674,20 @@ def get_filtered_data(ref_doctype, columns, data, user): shared = frappe.share.get_shared(ref_doctype, user) columns_dict = get_columns_dict(columns) - role_permissions = get_role_permissions(frappe.get_meta(ref_doctype), user) + ref_doctype_meta = frappe.get_meta(ref_doctype) + + role_permissions = get_role_permissions(ref_doctype_meta, user) if_owner = role_permissions.get("if_owner", {}).get("report") + if ref_doctype_meta.get_masked_fields(): + from frappe.model.db_query import mask_field_value + + # Apply masking to the fields + for field in ref_doctype_meta.get_masked_fields(): + for row in data: + val = row.get(field.fieldname) + row[field.fieldname] = mask_field_value(field, val) + if match_filters_per_doctype: for row in data: # Why linked_doctypes.get(ref_doctype)? because if column is empty, linked_doctypes[ref_doctype] is removed diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 437a4179d1..e0a9270b54 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1030,6 +1030,10 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { let data = this.data; let columns = this.columns.filter((col) => !col.hidden); + if (this.report_doc?.ref_doctype) { + columns = this.update_masked_fields_in_columns(columns, this.report_doc?.ref_doctype); + } + if (data.length > (cint(frappe.boot.sysdefaults.max_report_rows) || 100000)) { let msg = __( "This report contains {0} rows and is too big to display in browser, you can {1} this report instead.", @@ -1084,6 +1088,29 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } + update_masked_fields_in_columns(columns) { + const meta_fields = frappe.get_meta(this.report_doc?.ref_doctype).fields; + + const masked_field_map = Object.fromEntries( + meta_fields + .filter((field) => field.mask && field.mask_readonly) + .map((field) => [field.fieldname, field]) + ); + + // return updated columns with masked field metadata applied + return columns.map((col) => { + const masked_field = masked_field_map[col.fieldname]; + if (masked_field) { + return { + ...col, + fieldtype: "Data", + options: masked_field.options, + }; + } + return col; + }); + } + show_loading_screen() { const loading_state = `
From e69b607aabd7a8fde1005b53a2a0a6fb190dc2a3 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Fri, 19 Sep 2025 11:21:40 +0530 Subject: [PATCH 016/144] feat: add validation to prevent changing values on save --- frappe/model/base_document.py | 45 ++++++++++++++++++---------- frappe/model/document.py | 6 ++-- frappe/public/js/frappe/form/form.js | 4 +-- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 3a5057595e..c8d62453bf 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -1399,7 +1399,7 @@ class BaseDocument: else: return True - def reset_values_if_no_permlevel_access(self, has_access_to, high_permlevel_fields): + def reset_values_if_no_permlevel_access(self, has_access_to, high_permlevel_fields, mask_fields): """If the user does not have permissions at permlevel > 0, then reset the values to original / default""" to_reset = [ df @@ -1411,22 +1411,35 @@ class BaseDocument: ) ] - if to_reset: - if self.is_new(): - # if new, set default value - ref_doc = frappe.new_doc(self.doctype) - else: - # get values from old doc - if self.parent_doc: - parent_doc = self.parent_doc.get_latest() - child_docs = [d for d in parent_doc.get(self.parentfield) if d.name == self.name] - if not child_docs: - return - ref_doc = child_docs[0] - else: - ref_doc = self.get_latest() + to_reset = to_reset + mask_fields - for df in to_reset: + if not to_reset: + return + + if self.is_new(): + # if new, set default value + ref_doc = frappe.new_doc(self.doctype) + else: + # get values from old doc + if self.parent_doc: + parent_doc = self.parent_doc.get_latest() + child_docs = [d for d in parent_doc.get(self.parentfield) if d.name == self.name] + if not child_docs: + return + ref_doc = child_docs[0] + else: + ref_doc = self.get_latest() + + masked_fieldnames = [df.fieldname for df in to_reset if df.get("mask_readonly")] + ref_values = {} + if not self.is_new() and masked_fieldnames: + ref_values = frappe.db.get_value(self.doctype, self.name, masked_fieldnames, as_dict=True) or {} + + for df in to_reset: + if df.get("mask_readonly") and not self.is_new(): + if df.fieldname in ref_values: + self.set(df.fieldname, ref_values[df.fieldname]) + else: self.set(df.fieldname, ref_doc.get(df.fieldname)) def get_value(self, fieldname): diff --git a/frappe/model/document.py b/frappe/model/document.py index 916a2771fc..e5ed785341 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -923,8 +923,10 @@ class Document(BaseDocument): has_access_to = self.get_permlevel_access() high_permlevel_fields = self.meta.get_high_permlevel_fields() - if high_permlevel_fields: - self.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields) + mask_fields = self.meta.get_masked_fields() + + if high_permlevel_fields or mask_fields: + self.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields, mask_fields) # If new record then don't reset the values for child table if self.is_new(): diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 260d4647f0..c3effffc2a 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -465,9 +465,7 @@ frappe.ui.form.Form = class FrappeForm { this.show_conflict_message(); this.show_submission_queue_banner(); - if (!this.is_new()) { - this.mark_mask_fields_readonly(); - } + this.mark_mask_fields_readonly(); if (frappe.boot.read_only) { this.disable_form(); From 4942cdc289b65666061b8896837b47b936076587 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Fri, 19 Sep 2025 14:43:45 +0530 Subject: [PATCH 017/144] fix: add default value none --- frappe/model/base_document.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index c8d62453bf..ae33642aac 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -1399,7 +1399,7 @@ class BaseDocument: else: return True - def reset_values_if_no_permlevel_access(self, has_access_to, high_permlevel_fields, mask_fields): + def reset_values_if_no_permlevel_access(self, has_access_to, high_permlevel_fields, mask_fields=None): """If the user does not have permissions at permlevel > 0, then reset the values to original / default""" to_reset = [ df @@ -1411,6 +1411,9 @@ class BaseDocument: ) ] + if not mask_fields: + mask_fields = [] + to_reset = to_reset + mask_fields if not to_reset: From 355876724cfe062ac838ed66398f133a47e2212a Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Fri, 19 Sep 2025 17:05:07 +0530 Subject: [PATCH 018/144] fix: translation separator should be `::` not `:` Signed-off-by: Akhil Narang --- frappe/website/doctype/web_form/web_form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index f875f7cab4..455e02e649 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -287,7 +287,7 @@ def get_context(context): "Are you sure you want to discard the changes?", "Mandatory fields required::Error message in web form", "Invalid values for fields::Error message in web form", - "Error:Title of error message in web form", + "Error::Title of error message in web form", "Page {0} of {1}", "Couldn't save, please check the data you have entered", "Validation Error", From 03ac6e2f75b3906bddd74956984b83283af74db5 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Sun, 21 Sep 2025 19:09:32 +0530 Subject: [PATCH 019/144] refactor: fetch mask field from cache instead of meta --- frappe/desk/form/meta.py | 16 +++--------- frappe/model/meta.py | 26 ++++++++++++++----- frappe/public/js/frappe/form/form.js | 10 +++---- frappe/public/js/frappe/form/formatters.js | 8 +++++- frappe/public/js/frappe/form/layout.js | 6 ----- frappe/public/js/frappe/list/list_view.js | 5 +++- .../js/frappe/views/reports/query_report.js | 14 +++------- 7 files changed, 42 insertions(+), 43 deletions(-) diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index 151a9b935f..d2022215c7 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -45,19 +45,6 @@ def get_meta(doctype, cached=True) -> "FormMeta": # In prod don't use cached meta when explicitly requesting from DB. meta = FormMeta(doctype, cached=frappe.conf.developer_mode) - if meta.name not in meta.special_doctypes: - meta = mask_protected_fields(meta) - - return meta - - -def mask_protected_fields(meta): - for df in meta.fields: - if df.get("mask") and not meta.has_permlevel_access_to( - fieldname=df.fieldname, df=df, permission_type="mask" - ): - # store orignal fieldtype and change fieldtype to Data - df.mask_readonly = 1 return meta @@ -89,6 +76,9 @@ class FormMeta(Meta): for k in ASSET_KEYS: d[k] = __dict.get(k) + # add masked fields (per-user, per-meta) + d["masked_fields"] = [df.fieldname for df in self.get_masked_fields()] + return d def add_code(self): diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 11dc18b7df..b95ebfa5b9 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -88,11 +88,6 @@ def get_meta(doctype: "str | DocType", cached: bool = True) -> "_Meta": meta = Meta(doctype) - if meta.name not in meta.special_doctypes: - from frappe.desk.form.meta import mask_protected_fields - - meta = mask_protected_fields(meta) - key = f"doctype_meta::{meta.name}" frappe.client_cache.set_value(key, meta) return meta @@ -200,7 +195,26 @@ class Meta(Document): return self._dynamic_link_fields def get_masked_fields(self): - return self.get("fields", {"mask_readonly": 1}) + import copy + + if frappe.session.user == "Administrator": + return [] + cache_key = f"masked_fields::{self.name}::{frappe.session.user}" + masked_fields = frappe.cache.get_value(cache_key) + + if masked_fields is None: + masked_fields = [] + for df in self.fields: + if df.get("mask") and not self.has_permlevel_access_to( + fieldname=df.fieldname, df=df, permission_type="mask" + ): + # work on a copy instead of original df + df_copy = copy.deepcopy(df) + df_copy.mask_readonly = 1 + masked_fields.append(df_copy) + frappe.cache.set_value(cache_key, masked_fields) + + return masked_fields @cached_property def _dynamic_link_fields(self): diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index c3effffc2a..8eab942aa1 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1174,11 +1174,11 @@ frappe.ui.form.Form = class FrappeForm { } mark_mask_fields_readonly() { - this.fields.forEach((field) => { - if (field.df.mask && field.df.mask_readonly) { - this.set_df_property(field.df.fieldname, "disabled", "1"); - this.set_df_property(field.df.fieldname, "fieldtype", "Data"); - } + const masked_fields = this.meta.masked_fields || []; + + masked_fields.forEach((fieldname) => { + this.set_df_property(fieldname, "read_only", 1); + this.set_df_property(fieldname, "fieldtype", "Data"); }); } diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 8a5b60f174..da916be9eb 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -417,7 +417,13 @@ frappe.form.get_formatter = function (fieldtype) { }; frappe.format = function (value, df, options, doc) { - if (!df || df?.mask_readonly) df = { fieldtype: "Data" }; + let mask_readonly = false; + if (df.parent) { + const mask_fields = frappe.get_meta(df.parent)?.masked_fields; + mask_readonly = mask_fields?.includes(df.fieldname); + } + + if (!df || mask_readonly) df = { fieldtype: "Data" }; if (df.fieldname == "_user_tags") df = { ...df, fieldtype: "Tag" }; var fieldtype = df.fieldtype || "Data"; diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index cb5445691a..fc50b9bfcd 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -245,12 +245,6 @@ frappe.ui.form.Layout = class Layout { } init_field(df, parent, render = false) { - if (df.mask && df.mask_readonly) { - if (df.fieldtype !== "Data") { - df.read_only = 1; - df.fieldtype = "Data"; - } - } const fieldobj = frappe.ui.form.make_control({ df: df, doctype: this.doctype, diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 16d75604ec..343f44b8f8 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -922,7 +922,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { _value = _value * out_of_ratings; } - let filterable = df?.mask_readonly ? "no-underline" : " filterable"; + let masked_fields = frappe.get_meta(this.doctype).masked_fields || []; + let is_masked = masked_fields.includes(df.fieldname); + + let filterable = is_masked ? "no-underline" : " filterable"; if (df.fieldtype === "Image") { html = df.options diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 75859d0dab..8f5b881e1c 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1090,22 +1090,14 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } update_masked_fields_in_columns(columns) { - const meta_fields = frappe.get_meta(this.report_doc?.ref_doctype).fields; + const masked_fields = frappe.get_meta(this.report_doc?.ref_doctype).masked_fields; - const masked_field_map = Object.fromEntries( - meta_fields - .filter((field) => field.mask && field.mask_readonly) - .map((field) => [field.fieldname, field]) - ); - - // return updated columns with masked field metadata applied return columns.map((col) => { - const masked_field = masked_field_map[col.fieldname]; - if (masked_field) { + if (masked_fields.includes(col.fieldname)) { return { ...col, fieldtype: "Data", - options: masked_field.options, + options: [], }; } return col; From 0b2d0d6458f918ba7d30fd7f5fe0c58819b8e34a Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Tue, 23 Sep 2025 16:23:27 +0530 Subject: [PATCH 020/144] refactor: update default value for rows_threshold_for_grid_search to 20 --- frappe/core/doctype/doctype/doctype.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 6a035986b3..95f85fd9e8 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -702,7 +702,7 @@ "label": "Protect Attached Files" }, { - "default": "0", + "default": "20", "depends_on": "istable", "fieldname": "rows_threshold_for_grid_search", "fieldtype": "Int", @@ -792,7 +792,7 @@ "link_fieldname": "document_type" } ], - "modified": "2025-07-19 12:23:16.296416", + "modified": "2025-09-23 06:48:13.555017", "modified_by": "Administrator", "module": "Core", "name": "DocType", From 886ee5a57b7d3177e4435681d9232b02e84659c8 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Tue, 23 Sep 2025 16:32:13 +0530 Subject: [PATCH 021/144] refactor: use variable instead of hard coded value for default search row threshold --- frappe/public/js/frappe/form/grid_row.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 10b18171e7..76dae51c14 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -13,6 +13,7 @@ export default class GridRow { read_only: [], }; this.row_check_html = '
`); } else { $body.append(` - - - - - - - ${frappe.perm.rights.map((p) => ``).join("")} - - - -
${__("Document Type")} ${__("Level")} ${__("If Owner")} ${__(frappe.unscrub(p))}
+
+ + + + + + + ${frappe.perm.rights + .map( + (p) => + `` + ) + .join("")} + + + +
${__("Document Type")} ${__("Level")} ${__("If Owner")} ${__( + frappe.unscrub(p) + )}
+
`); permissions.forEach((perm) => { $body.find("tbody").append(` From 86499a3c9a69e167ded4e90497503652c63af29b Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Tue, 23 Sep 2025 18:47:59 +0000 Subject: [PATCH 025/144] fix(roles-editor): update header background color based on theme --- frappe/public/js/frappe/roles_editor.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/roles_editor.js b/frappe/public/js/frappe/roles_editor.js index 2e0b9c3aa6..72ce5e3575 100644 --- a/frappe/public/js/frappe/roles_editor.js +++ b/frappe/public/js/frappe/roles_editor.js @@ -54,6 +54,8 @@ frappe.RoleEditor = class { this.make_perm_dialog(); } $(this.perm_dialog.body).empty(); + let is_dark = document.documentElement.getAttribute("data-theme") === "dark"; + let header_bg_color = is_dark ? "bg-dark text-white" : "bg-light"; return frappe .xcall("frappe.core.doctype.user.user.get_perm_info", { role }) .then((permissions) => { @@ -68,13 +70,13 @@ frappe.RoleEditor = class { - - - + + + ${frappe.perm.rights .map( (p) => - `` ) From 9057b508b68e4fc3667317b6cdca0ef8b40f777e Mon Sep 17 00:00:00 2001 From: MochaMind Date: Wed, 24 Sep 2025 02:03:20 +0530 Subject: [PATCH 026/144] fix: Norwegian Bokmal translations --- frappe/locale/nb.po | 146 ++++++++++++++++++++++---------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/frappe/locale/nb.po b/frappe/locale/nb.po index eb26d49a46..6cb7bfd6c9 100644 --- a/frappe/locale/nb.po +++ b/frappe/locale/nb.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-21 09:33+0000\n" -"PO-Revision-Date: 2025-09-22 20:25\n" +"PO-Revision-Date: 2025-09-23 20:33\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Norwegian Bokmal\n" "MIME-Version: 1.0\n" @@ -4608,7 +4608,7 @@ msgstr "Velg autentiseringsmetode som skal brukes av alle brukere" #: frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py:39 #: frappe/website/doctype/contact_us_settings/contact_us_settings.json msgid "City" -msgstr "By" +msgstr "Poststed" #. Label of the city (Data) field in DocType 'Address' #: frappe/contacts/doctype/address/address.json @@ -8838,7 +8838,7 @@ msgstr "Utsending av e-post er slått av." #. Description of the 'Send Email Alert' (Check) field in DocType 'Workflow' #: frappe/workflow/doctype/workflow/workflow.json msgid "Emails will be sent with next possible workflow actions" -msgstr "E-postmeldinger vil bli sendt med neste mulige handlinger i arbeidsflyten" +msgstr "Det vil bli sendt e-post med informasjon om neste mulige arbeidsflythandlinger" #: frappe/website/doctype/web_form/web_form.js:34 msgid "Embed code copied" @@ -24814,7 +24814,7 @@ msgstr "Egenskaper for tilstand" #: frappe/contacts/doctype/address/address.json #: frappe/website/doctype/contact_us_settings/contact_us_settings.json msgid "State/Province" -msgstr "Stat/provins" +msgstr "Delstat/provins" #. Label of the document_states_section (Tab Break) field in DocType 'DocType' #. Label of the states (Table) field in DocType 'Customize Form' @@ -29094,12 +29094,12 @@ msgstr "Meta-tagg for nettstedet" #: frappe/website/doctype/website_route_meta/website_route_meta.json #: frappe/website/workspace/website/website.json msgid "Website Route Meta" -msgstr "" +msgstr "Sti-metadata for nettsted" #. Name of a DocType #: frappe/website/doctype/website_route_redirect/website_route_redirect.json msgid "Website Route Redirect" -msgstr "" +msgstr "Sti-viderekobling for nettsted" #. Name of a DocType #. Label of a Link in the Website Workspace @@ -29133,24 +29133,24 @@ msgstr "Innstillinger for nettsted" #: frappe/website/doctype/website_sidebar/website_sidebar.json #: frappe/website/workspace/website/website.json msgid "Website Sidebar" -msgstr "" +msgstr "Nettstedets sidefelt" #. Name of a DocType #: frappe/website/doctype/website_sidebar_item/website_sidebar_item.json msgid "Website Sidebar Item" -msgstr "" +msgstr "Element i nettstedets sidefelt" #. Name of a DocType #. Label of a Link in the Website Workspace #: frappe/website/doctype/website_slideshow/website_slideshow.json #: frappe/website/workspace/website/website.json msgid "Website Slideshow" -msgstr "" +msgstr "Lysbildefremvisning på nettstedet" #. Name of a DocType #: frappe/website/doctype/website_slideshow_item/website_slideshow_item.json msgid "Website Slideshow Item" -msgstr "" +msgstr "Element i lysbildefremvisning på nettstedet" #. Label of the website_theme (Link) field in DocType 'Website Settings' #. Name of a DocType @@ -29164,18 +29164,18 @@ msgstr "Nettstedstema" #. Name of a DocType #: frappe/website/doctype/website_theme_ignore_app/website_theme_ignore_app.json msgid "Website Theme Ignore App" -msgstr "" +msgstr "Ignorer app for nettstedstema" #. Label of the website_theme_image (Image) field in DocType 'Website Settings' #: frappe/website/doctype/website_settings/website_settings.json msgid "Website Theme Image" -msgstr "" +msgstr "Bilde­ for nettstedstema" #. Label of the website_theme_image_link (Code) field in DocType 'Website #. Settings' #: frappe/website/doctype/website_settings/website_settings.json msgid "Website Theme image link" -msgstr "" +msgstr "Bilde­lenke for nettstedstema" #. Option for the 'SocketIO Transport Mode' (Select) field in DocType 'System #. Health Report' @@ -29201,7 +29201,7 @@ msgstr "Onsdag" #: frappe/public/js/frappe/views/calendar/calendar.js:276 msgid "Week" -msgstr "" +msgstr "Uke" #. Option for the 'Frequency' (Select) field in DocType 'Auto Email Report' #: frappe/email/doctype/auto_email_report/auto_email_report.json @@ -29239,7 +29239,7 @@ msgstr "Ukeslang" #: frappe/desk/page/setup_wizard/setup_wizard.js:384 msgid "Welcome" -msgstr "" +msgstr "Velkommen" #. Label of the welcome_email_template (Link) field in DocType 'System #. Settings' @@ -29247,12 +29247,12 @@ msgstr "" #: frappe/core/doctype/system_settings/system_settings.json #: frappe/email/doctype/email_group/email_group.json msgid "Welcome Email Template" -msgstr "" +msgstr "Mal for velkomst-e-post" #. Label of the welcome_url (Data) field in DocType 'Email Group' #: frappe/email/doctype/email_group/email_group.json msgid "Welcome URL" -msgstr "" +msgstr "Velkomst-URL" #. Name of a Workspace #: frappe/core/workspace/welcome_workspace/welcome_workspace.json @@ -29261,11 +29261,11 @@ msgstr "Velkomst og introduksjon" #: frappe/core/doctype/user/user.py:416 msgid "Welcome email sent" -msgstr "" +msgstr "Velkomst-e-post sendt" #: frappe/core/doctype/user/user.py:477 msgid "Welcome to {0}" -msgstr "" +msgstr "Velkommen til {0}" #: frappe/public/js/frappe/ui/notifications/notifications.js:62 msgid "What's New" @@ -29332,7 +29332,7 @@ msgstr "Vil legge til «%» før og etter spørringen" #: frappe/desk/page/setup_wizard/setup_wizard.js:485 msgid "Will be your login ID" -msgstr "" +msgstr "Vil være din innloggings-ID" #: frappe/printing/page/print_format_builder/print_format_builder.js:424 msgid "Will only be shown if section headings are enabled" @@ -29342,7 +29342,7 @@ msgstr "Vil bare vises hvis seksjonsoverskrifter er aktivert" #. in DocType 'System Settings' #: frappe/core/doctype/system_settings/system_settings.json msgid "Will run scheduled jobs only once a day for inactive sites. Set it to 0 to avoid automatically disabling the scheduler." -msgstr "" +msgstr "Kjører planlagte jobber bare én gang om dagen for inaktive nettsteder. Sett den til 0 for å unngå automatisk deaktivering av planleggeren." #: frappe/public/js/frappe/form/print_utils.js:45 msgid "With Letter head" @@ -29357,7 +29357,7 @@ msgstr "Info om bakgrunnsprosesser" #. Label of the worker_name (Data) field in DocType 'RQ Worker' #: frappe/core/doctype/rq_worker/rq_worker.json msgid "Worker Name" -msgstr "" +msgstr "Prosessnavn" #. Option for the 'Comment Type' (Select) field in DocType 'Comment' #. Group in DocType's connections @@ -29373,30 +29373,30 @@ msgstr "Arbeidsflyt" #: frappe/workflow/doctype/workflow_action/workflow_action.json #: frappe/workflow/doctype/workflow_action/workflow_action.py:444 msgid "Workflow Action" -msgstr "" +msgstr "Arbeidsflythandling" #. Name of a DocType #. Description of a DocType #: frappe/workflow/doctype/workflow_action_master/workflow_action_master.json msgid "Workflow Action Master" -msgstr "" +msgstr "Mal for arbeidsflythandling" #. Label of the workflow_action_name (Data) field in DocType 'Workflow Action #. Master' #: frappe/workflow/doctype/workflow_action_master/workflow_action_master.json msgid "Workflow Action Name" -msgstr "" +msgstr "Navn på arbeidsflythandling" #. Name of a DocType #: frappe/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.json msgid "Workflow Action Permitted Role" -msgstr "" +msgstr "Tillatt rolle for arbeidsflythandling" #. Description of the 'Is Optional State' (Check) field in DocType 'Workflow #. Document State' #: frappe/workflow/doctype/workflow_document_state/workflow_document_state.json msgid "Workflow Action is not created for optional states" -msgstr "Arbeidsflythandlingen er ikke opprettet for valgfrie tilstander" +msgstr "Det opprettes ikke arbeidsflythandling for valgfrie tilstander" #: frappe/public/js/workflow_builder/store.js:129 #: frappe/workflow/doctype/workflow/workflow.js:25 @@ -29420,7 +29420,7 @@ msgstr "Med Arbeidsflytbygger kan du lage arbeidsflyter visuelt. Du kan dra og s #. Label of the workflow_data (JSON) field in DocType 'Workflow' #: frappe/workflow/doctype/workflow/workflow.json msgid "Workflow Data" -msgstr "" +msgstr "Arbeidsflytdata" #: frappe/public/js/workflow_builder/components/Properties.vue:44 msgid "Workflow Details" @@ -29429,40 +29429,40 @@ msgstr "Arbeidsflytdetaljer" #. Name of a DocType #: frappe/workflow/doctype/workflow_document_state/workflow_document_state.json msgid "Workflow Document State" -msgstr "" +msgstr "Tilstand for arbeidsflytdokument" #. Label of the workflow_name (Data) field in DocType 'Workflow' #: frappe/workflow/doctype/workflow/workflow.json msgid "Workflow Name" -msgstr "" +msgstr "Navn på arbeidsflyt" #. Label of the workflow_state (Data) field in DocType 'Workflow Action' #. Name of a DocType #: frappe/workflow/doctype/workflow_action/workflow_action.json #: frappe/workflow/doctype/workflow_state/workflow_state.json msgid "Workflow State" -msgstr "" +msgstr "Arbeidsflyttilstand" #. Label of the workflow_state_field (Data) field in DocType 'Workflow' #: frappe/workflow/doctype/workflow/workflow.json msgid "Workflow State Field" -msgstr "" +msgstr "Felt for arbeidsflyttilstand" #: frappe/model/workflow.py:64 msgid "Workflow State not set" -msgstr "" +msgstr "Arbeidsflyttilstand ikke angitt" #: frappe/model/workflow.py:260 frappe/model/workflow.py:268 msgid "Workflow State transition not allowed from {0} to {1}" -msgstr "" +msgstr "Overgang mellom arbeidsflyttilstander er ikke tillatt fra {0} til {1}" #: frappe/workflow/doctype/workflow/workflow.js:140 msgid "Workflow States Don't Exist" -msgstr "" +msgstr "Arbeidsflyttilstander finnes ikke" #: frappe/model/workflow.py:384 msgid "Workflow Status" -msgstr "" +msgstr "Arbeidsflyttilstand" #. Option for the 'Script Type' (Select) field in DocType 'Server Script' #: frappe/core/doctype/server_script/server_script.json @@ -29472,26 +29472,26 @@ msgstr "Oppgaver i arbeidsflyt" #. Name of a DocType #: frappe/workflow/doctype/workflow_transition/workflow_transition.json msgid "Workflow Transition" -msgstr "" +msgstr "Arbeidsflytovergang" #. Name of a DocType #: frappe/workflow/doctype/workflow_transition_task/workflow_transition_task.json msgid "Workflow Transition Task" -msgstr "" +msgstr "Oppgave ved arbeidsflytovergang" #. Name of a DocType #: frappe/workflow/doctype/workflow_transition_tasks/workflow_transition_tasks.json msgid "Workflow Transition Tasks" -msgstr "" +msgstr "Oppgaver ved arbeidsflytovergang" #. Description of a DocType #: frappe/workflow/doctype/workflow_state/workflow_state.json msgid "Workflow state represents the current state of a document." -msgstr "" +msgstr "Arbeidsflyttilstand representerer den nåværende tilstanden til et dokument." #: frappe/public/js/workflow_builder/store.js:83 msgid "Workflow updated successfully" -msgstr "Arbeidsflyten ble vellykket oppdatert" +msgstr "Arbeidsflyten ble oppdatert" #. Label of the workspace_section (Section Break) field in DocType 'User' #. Label of a Link in the Build Workspace @@ -29711,27 +29711,27 @@ msgstr "I går" #: frappe/public/js/frappe/utils/user.js:33 msgctxt "Name of the current user. For example: You edited this 5 hours ago." msgid "You" -msgstr "" +msgstr "Du" #: frappe/public/js/frappe/form/footer/form_timeline.js:463 msgid "You Liked" -msgstr "" +msgstr "Du likte" #: frappe/public/js/frappe/form/footer/version_timeline_content_builder.js:266 msgid "You added 1 row to {0}" -msgstr "" +msgstr "Du la til 1 rad til {0}" #: frappe/public/js/frappe/form/footer/version_timeline_content_builder.js:244 msgid "You added {0} rows to {1}" -msgstr "" +msgstr "Du la til {0} rader til {1}" #: frappe/public/js/frappe/dom.js:438 msgid "You are connected to internet." -msgstr "" +msgstr "Du er koblet til Internett." #: frappe/public/js/frappe/ui/toolbar/navbar.html:20 msgid "You are impersonating as another user." -msgstr "" +msgstr "Du utgir deg for å være en annen bruker." #: frappe/integrations/frappe_providers/frappecloud_billing.py:28 msgid "You are not allowed to access this resource" @@ -29739,27 +29739,27 @@ msgstr "Du har ikke tilgang til denne ressursen" #: frappe/permissions.py:431 msgid "You are not allowed to access this {0} record because it is linked to {1} '{2}' in field {3}" -msgstr "" +msgstr "Du har ikke tilgang til denne {0} oppføringen fordi den er lenket til {1} '{2}' i felt {3}" #: frappe/permissions.py:420 msgid "You are not allowed to access this {0} record because it is linked to {1} '{2}' in row {3}, field {4}" -msgstr "" +msgstr "Du har ikke tilgang til denne {0} -posten fordi den er knyttet til {1} '{2}' i rad {3}, felt {4}" #: frappe/public/js/frappe/views/kanban/kanban_board.bundle.js:68 msgid "You are not allowed to create columns" -msgstr "" +msgstr "Du har ikke rettigheter til å opprette kolonner" #: frappe/core/doctype/report/report.py:97 msgid "You are not allowed to delete Standard Report" -msgstr "" +msgstr "Du har ikke rettigheter til å slette standardrapporten" #: frappe/website/doctype/website_theme/website_theme.py:73 msgid "You are not allowed to delete a standard Website Theme" -msgstr "" +msgstr "Du har ikke rettigheter til å slette et standard nettstedstema" #: frappe/core/doctype/report/report.py:391 msgid "You are not allowed to edit the report." -msgstr "" +msgstr "Du har ikke rettigheter til å redigere rapporten." #: frappe/core/doctype/data_import/exporter.py:121 #: frappe/core/doctype/data_import/exporter.py:125 @@ -29770,39 +29770,39 @@ msgstr "Du har ikke rettigheter til å eksportere {} dokumenttype (DocType)" #: frappe/public/js/frappe/views/treeview.js:448 msgid "You are not allowed to print this report" -msgstr "Du har ikke tillatelse til å skrive ut denne rapporten" +msgstr "Du har ikke rettigheter til å skrive ut denne rapporten" #: frappe/public/js/frappe/views/communication.js:787 msgid "You are not allowed to send emails related to this document" -msgstr "" +msgstr "Du har ikke rettigheter til å sende e-poster om dette dokumentet" #: frappe/website/doctype/web_form/web_form.py:605 msgid "You are not allowed to update this Web Form Document" -msgstr "" +msgstr "Du har ikke rettigheter til å oppdatere dette nettskjemadokumentet" #: frappe/public/js/frappe/request.js:37 msgid "You are not connected to Internet. Retry after sometime." -msgstr "" +msgstr "Du er ikke koblet til Internett. Prøv på nytt etter en stund." #: frappe/public/js/frappe/web_form/webform_script.js:22 msgid "You are not permitted to access this page without login." -msgstr "" +msgstr "Du har ikke tilgang til denne siden uten å være logget inn." #: frappe/www/app.py:27 msgid "You are not permitted to access this page." -msgstr "" +msgstr "Ditt rettighetsnivå hindrer visning av denne siden." #: frappe/__init__.py:465 msgid "You are not permitted to access this resource. Login to access" -msgstr "" +msgstr "Du må være innlogget for å få tilgang til denne ressursen." #: frappe/public/js/frappe/form/sidebar/document_follow.js:131 msgid "You are now following this document. You will receive daily updates via email. You can change this in User Settings." -msgstr "" +msgstr "Du følger nå dette dokumentet. Du vil motta daglige oppdateringer via e-post. Du kan endre dette i brukerinnstillingene." #: frappe/core/doctype/installed_applications/installed_applications.py:117 msgid "You are only allowed to update order, do not remove or add apps." -msgstr "" +msgstr "Du kan bare endre rekkefølgen på appene, ikke legge til eller fjerne dem." #: frappe/email/doctype/email_account/email_account.js:284 msgid "You are selecting Sync Option as ALL, It will resync all read as well as unread message from server. This may also cause the duplication of Communication (emails)." @@ -29811,7 +29811,7 @@ msgstr "Du velger Synkroniseringsalternativet ALLE. Dette vil synkronisere alle #: frappe/public/js/frappe/form/footer/form_timeline.js:414 msgctxt "Form timeline" msgid "You attached {0}" -msgstr "" +msgstr "Du la ved {0}" #: frappe/printing/page/print_format_builder/print_format_builder.js:749 msgid "You can add dynamic properties from the document by using Jinja templating." @@ -29831,11 +29831,11 @@ msgstr "Du kan også kopiere og lime inn dette" #: frappe/templates/emails/delete_data_confirmation.html:11 msgid "You can also copy-paste this {0} to your browser" -msgstr "" +msgstr "Du kan også kopiere og lime inn denne {0} i nettleseren din" #: frappe/templates/emails/user_invitation_expired.html:8 msgid "You can ask your team to resend the invitation if you'd still like to join." -msgstr "" +msgstr "Du kan be teamet ditt om å sende invitasjonen på nytt hvis du fortsatt ønsker å bli med." #: frappe/core/page/permission_manager/permission_manager_help.html:17 msgid "You can change Submitted documents by cancelling them and then, amending them." @@ -29851,19 +29851,19 @@ msgstr "Du kan fortsette med onboarding-prosessen etter å ha utforsket denne si #: frappe/model/delete_doc.py:137 msgid "You can disable this {0} instead of deleting it." -msgstr "" +msgstr "Du kan deaktivere denne {0} i stedet for å slette den." #: frappe/core/doctype/file/file.py:758 msgid "You can increase the limit from System Settings." -msgstr "" +msgstr "Du kan øke grensen fra systeminnstillingene." #: frappe/utils/synchronization.py:48 msgid "You can manually remove the lock if you think it's safe: {}" -msgstr "" +msgstr "Du kan manuelt fjerne låsen hvis du tror det er trygt: {}" #: frappe/public/js/frappe/form/controls/markdown_editor.js:75 msgid "You can only insert images in Markdown fields" -msgstr "" +msgstr "Du kan bare sette inn bilder i felter som støtter Markdown" #: frappe/public/js/frappe/list/bulk_operations.js:42 msgid "You can only print upto {0} documents at a time" @@ -29889,7 +29889,7 @@ msgstr "Du kan velge ett av følgende," #. 'System Settings' #: frappe/core/doctype/system_settings/system_settings.json msgid "You can set a high value here if multiple users will be logging in from the same network." -msgstr "" +msgstr "Du kan angi en høy verdi her hvis flere brukere skal logge seg på fra samme nettverk." #: frappe/desk/query_report.py:382 msgid "You can try changing the filters of your report." @@ -30858,7 +30858,7 @@ msgstr "" #: frappe/public/js/frappe/widgets/number_card_widget.js:309 msgid "since last week" -msgstr "" +msgstr "siden forrige uke" #: frappe/public/js/frappe/widgets/number_card_widget.js:311 msgid "since last year" @@ -31410,7 +31410,7 @@ msgstr "" #: frappe/model/workflow.py:245 msgid "{0} is not a valid Workflow State. Please update your Workflow and try again." -msgstr "" +msgstr "{0} er ikke en gyldig arbeidsflyttilstand. Oppdater arbeidsflyten din og prøv på nytt." #: frappe/permissions.py:809 msgid "{0} is not a valid parent DocType for {1}" @@ -31704,7 +31704,7 @@ msgstr "" #: frappe/public/js/frappe/utils/pretty_date.js:64 msgid "{0} weeks ago" -msgstr "" +msgstr "{0} uker siden" #: frappe/public/js/frappe/utils/pretty_date.js:39 msgid "{0} y" From dc0b5792ba3850b1eb3348b550592c1302a97b3c Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Wed, 24 Sep 2025 13:06:41 +0530 Subject: [PATCH 027/144] fix(db_query): improve function checking Signed-off-by: Akhil Narang --- frappe/model/db_query.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 41451035a6..91e9d32164 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -394,8 +394,6 @@ from {tables} "concat", "concat_ws", "if", - "ifnull", - "nullif", "coalesce", "connection_id", "current_user", @@ -425,16 +423,19 @@ from {tables} if SUB_QUERY_PATTERN.match(field): # Check for subquery anywhere in the field, not just at the beginning if "(" in lower_field: - location = lower_field.index("(") - subquery_token = lower_field[location + 1 :].lstrip().split(" ", 1)[0] - if any(keyword in subquery_token for keyword in blacklisted_keywords): - _raise_exception() - - function = lower_field.split("(", 1)[0].rstrip() - if function in blacklisted_functions: - frappe.throw( - _("Use of function {0} in field is restricted").format(function), exc=frappe.DataError - ) + # Check all parentheses pairs, not just the first one + paren_start = 0 + while True: + location = lower_field.find("(", paren_start) + if location == -1: + break + token = lower_field[location + 1 :].lstrip().split(" ", 1)[0] + if any( + re.search(r"\b" + re.escape(keyword) + r"\b", token) + for keyword in blacklisted_keywords + blacklisted_functions + ): + _raise_exception() + paren_start = location + 1 if "@" in lower_field: # prevent access to global variables From dc2422ebde8019cedb21230d0847d0cb6285777e Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Wed, 24 Sep 2025 18:20:09 +0530 Subject: [PATCH 028/144] fix: don't call `file.check_content()` twice This first call was sometimes done when `_content` wasn't set, resulting in: ``` File "apps/frappe/frappe/core/doctype/file/file.py", line 138, in validate self.check_content() File "apps/frappe/frappe/core/doctype/file/file.py", line 381, in check_content if self.file_type == "PDF" and self._content and pdf_contains_js(self._content): ^^^^^^^^^^^^^ AttributeError: 'File' object has no attribute '_content' ``` Just calling it in `write_file()` seems good enough Signed-off-by: Akhil Narang --- frappe/core/doctype/file/file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index b49ae51036..6fed582df7 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -141,7 +141,6 @@ class File(Document): self.validate_file_url() self.validate_file_on_disk() self.file_size = frappe.form_dict.file_size or self.file_size - self.check_content() def validate_attachment_references(self): if not self.attached_to_doctype: From 7743c90783eb09aa1e9045a147c350fbedbcb6ba Mon Sep 17 00:00:00 2001 From: Corentin Forler Date: Wed, 24 Sep 2025 16:06:28 +0200 Subject: [PATCH 029/144] feat: Optionally show a warning when opening an external link --- .../system_settings/system_settings.json | 10 +- .../system_settings/system_settings.py | 1 + frappe/public/js/frappe/router.js | 104 ++++++++++++++++++ frappe/sessions.py | 1 + 4 files changed, 115 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 987ec66dde..13f4ee83f5 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -30,6 +30,7 @@ "apply_strict_user_permissions", "column_break_21", "allow_older_web_view_links", + "show_external_link_warning", "security_tab", "security", "session_expiry", @@ -744,12 +745,19 @@ "fieldtype": "Int", "label": "Max signups allowed per hour", "non_negative": 1 + }, + { + "default": "Never", + "fieldname": "show_external_link_warning", + "fieldtype": "Select", + "label": "Show External Link Warning", + "options": "Never\nAsk\nAlways" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2025-09-03 10:52:38.096662", + "modified": "2025-09-24 16:04:02.016562", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index 5e0e57f096..b78395ce5e 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -97,6 +97,7 @@ class SystemSettings(Document): session_expiry: DF.Data | None setup_complete: DF.Check show_absolute_datetime_in_timeline: DF.Check + show_external_link_warning: DF.Literal["Never", "Ask", "Always"] store_attached_pdf_document: DF.Check strip_exif_metadata_from_uploaded_images: DF.Check time_format: DF.Literal["HH:mm:ss", "HH:mm"] diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index 8c6effa2fc..01d983d493 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -28,6 +28,11 @@ $("body").on("click", "a", function (e) { const href = target_element.getAttribute("href"); const is_on_same_host = target_element.hostname === window.location.hostname; + if (frappe.router.show_external_link_warning_if_needed(target_element)) { + e.preventDefault(); + return; // warning shown + } + if (target_element.getAttribute("target") === "_blank") { return; } @@ -570,6 +575,105 @@ frappe.router = { slug(name) { return name.toLowerCase().replace(/ /g, "-"); }, + + show_external_link_warning_if_needed(/** @type {HTMLAnchorElement} */ aElement) { + try { + if (!aElement?.href) { + return false; // not a true link + } + + // Get the external link handling type + /** @type {'Always' | 'Ask' | 'Never' | null} */ + const showWarningWhen = frappe.boot.show_external_link_warning || "Never"; + if (showWarningWhen == "Never") { + return false; // the feature is disabled + } + + // Check that the origin is external (does not prevent self-clickjacking on GET endpoints) + const url = new URL(aElement.href); + const hostname = url.hostname; + if (hostname === window.location.hostname) { + return false; // self-linking is allowed + } + + // Check if the origin was ignored by the user + const localStorageKey = `skip-external-link-warning:${hostname}`; + if (showWarningWhen == "Ask" && localStorage.getItem(localStorageKey)) { + return false; // user chose to skip warning forever + } + + // Check if the link if inside the confirmation popup + const incominSkipToken = aElement.getAttribute("data-skip-link-warning"); + if (incominSkipToken && sessionStorage.getItem(incominSkipToken) == "1") { + return false; // anchor is the confirmation itself + } + + // Finally, show the warning + const dialog = new frappe.ui.Dialog({ + title: __("Warning"), + primary_action: null, + fields: [ + { + fieldname: "warning_html", + fieldtype: "HTML", + }, + { + fieldname: "confirm_checkbox", + fieldtype: "Check", + label: __("Do not warn me again about {0}", [ + frappe.utils.escape_html(hostname).bold(), + ]), + default: 0, + hidden: showWarningWhen == "Always", + change() { + if (dialog.get_value("confirm_checkbox")) { + localStorage.setItem(localStorageKey, "1"); + } else { + localStorage.removeItem(localStorageKey); + } + }, + }, + ], + }); + + const warningElement = dialog.fields_dict.warning_html.$wrapper.get(0); + + const introElement = document.createElement("p"); + introElement.textContent = __( + "You are about to open an external link. To confirm, click the link again." + ); + warningElement.appendChild(introElement); + + const boxElement = document.createElement("div"); + boxElement.classList.add("border", "rounded-lg", "p-3", "mt-6", "mb-6", "text-center"); + warningElement.appendChild(boxElement); + + const hintElement = document.createElement("p"); + hintElement.classList.add("text-sm", "mb-1"); + hintElement.textContent = __("You will be redirected to:"); + boxElement.appendChild(hintElement); + + const confirmElement = document.createElement("a"); + confirmElement.classList.add("text-sm", "font-mono"); + confirmElement.style.wordBreak = "break-all"; + confirmElement.textContent = aElement.href; + confirmElement.href = aElement.href; + confirmElement.target = aElement.target; + confirmElement.addEventListener("click", () => dialog.hide(), { capture: true }); + + // Add a token to skip the warning when clicking inside the confirmation dialog + const skipToken = frappe.utils.get_random(16); + confirmElement.setAttribute("data-skip-link-warning", skipToken); + sessionStorage.setItem(skipToken, "1"); + boxElement.appendChild(confirmElement); + + dialog.show(); + return true; // prevent default handling + } catch (e) { + console.error(e); + } + return false; + }, }; // global functions for backward compatibility diff --git a/frappe/sessions.py b/frappe/sessions.py index 5e9fdb43b9..701e140ae5 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -181,6 +181,7 @@ def get(): bootinfo["user"]["impersonated_by"] = frappe.session.data.get("impersonated_by") bootinfo["navbar_settings"] = frappe.client_cache.get_doc("Navbar Settings") bootinfo.has_app_updates = has_app_update_notifications() + bootinfo.show_external_link_warning = frappe.get_system_settings("show_external_link_warning") return bootinfo From 348e1299bc26853cb1cc16adff10a3d70c58db57 Mon Sep 17 00:00:00 2001 From: MochaMind Date: Thu, 25 Sep 2025 02:05:25 +0530 Subject: [PATCH 030/144] fix: Serbian (Cyrillic) translations --- frappe/locale/sr.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frappe/locale/sr.po b/frappe/locale/sr.po index bbbb059968..53ee9fb11a 100644 --- a/frappe/locale/sr.po +++ b/frappe/locale/sr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-21 09:33+0000\n" -"PO-Revision-Date: 2025-09-21 19:57\n" +"PO-Revision-Date: 2025-09-24 20:35\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Serbian (Cyrillic)\n" "MIME-Version: 1.0\n" @@ -19602,7 +19602,7 @@ msgstr "Прецизност" #: frappe/core/doctype/doctype/doctype.py:1670 msgid "Precision ({0}) for {1} cannot be greater than its length ({2})." -msgstr "" +msgstr "Прецизност ({0}) за {1} не може бити већа од његове дужине ({2})." #: frappe/core/doctype/doctype/doctype.py:1401 msgid "Precision should be between 1 and 6" @@ -23652,7 +23652,7 @@ msgstr "Поставите нестандардну прецизност за п #. Description of the 'Precision' (Select) field in DocType 'DocField' #: frappe/core/doctype/docfield/docfield.json msgid "Set non-standard precision for a Float, Currency or Percent field" -msgstr "" +msgstr "Подесите нестандардну прецизност за поља врсте децимални број, валута или проценат" #. Label of the set_only_once (Check) field in DocType 'DocField' #: frappe/core/doctype/docfield/docfield.json @@ -26110,7 +26110,7 @@ msgstr "Број пројекта добијен путем Google Cloud кон #: frappe/desk/utils.py:106 msgid "The report you requested has been generated.

Click here to download:
{0}

This link will expire in {1} hours." -msgstr "" +msgstr "Извештај који сте затражили је генерисан.

Кликните овде за преузимање:
{0}

Овај линк истиче за {1} сата." #: frappe/core/doctype/user/user.py:1000 msgid "The reset password link has been expired" @@ -26335,7 +26335,7 @@ msgstr "Ово се не може опозвати" #: frappe/desk/doctype/number_card/number_card.js:480 msgctxt "Number Card" msgid "This card is visible only to Administrator and System Managers by default. Set a DocType to share with users who have read access." -msgstr "" +msgstr "Ова картица је подразумевано видљива само администратору и систем менаџерима. Подесите DocType да је делите са корисницима који имају право читања." #. Description of the 'Is Public' (Check) field in DocType 'Number Card' #: frappe/desk/doctype/number_card/number_card.json @@ -30284,7 +30284,7 @@ msgstr "Ваш упит је примљен. Одговорићемо Вам у #: frappe/desk/query_report.py:342 frappe/desk/reportview.py:396 msgid "Your report is being generated in the background. You will receive an email on {0} with a download link once it is ready." -msgstr "" +msgstr "Ваш извештај се генерише у позадини. Добићете имејл на {0} са линком за преузимање када буде спреман." #: frappe/app.py:374 msgid "Your session has expired, please login again to continue." From 09fd341fd1bd9b337bf129be0457ce16a6b0db1d Mon Sep 17 00:00:00 2001 From: MochaMind Date: Thu, 25 Sep 2025 02:05:38 +0530 Subject: [PATCH 031/144] fix: Persian translations --- frappe/locale/fa.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/locale/fa.po b/frappe/locale/fa.po index 5adda8ab94..e5a799f43b 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: 2025-09-21 09:33+0000\n" -"PO-Revision-Date: 2025-09-22 20:24\n" +"PO-Revision-Date: 2025-09-24 20:35\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Persian\n" "MIME-Version: 1.0\n" @@ -3533,7 +3533,7 @@ msgstr "ساخت" #. Description of a Card Break in the Build Workspace #: frappe/core/workspace/build/build.json msgid "Build your own reports, print formats, and dashboards. Create personalized workspaces for easier navigation" -msgstr "" +msgstr "گزارش‌ها، قالب‌های چاپ و داشبوردهای خودتان را بسازید. برای پیمایش آسان‌تر، فضاهای کاری شخصی‌سازی‌شده ایجاد کنید" #: frappe/workflow/doctype/workflow/workflow_list.js:18 msgid "Build {0}" @@ -4061,7 +4061,7 @@ msgstr "محتویات فایل یک پوشه را نمی‌توان دریاف #: frappe/printing/page/print/print.js:884 msgid "Cannot have multiple printers mapped to a single print format." -msgstr "نمی‌توان چندین چاپگر را به یک قالب چاپی نگاشت کرد." +msgstr "نمی‌توان چندین چاپگر را به یک قالب چاپ واحد نگاشت کرد." #: frappe/public/js/frappe/form/grid.js:1133 msgid "Cannot import table with more than 5000 rows." From f3de13deee145bab501257b64de2d62f57a9b74f Mon Sep 17 00:00:00 2001 From: MochaMind Date: Thu, 25 Sep 2025 02:05:46 +0530 Subject: [PATCH 032/144] fix: Norwegian Bokmal translations --- frappe/locale/nb.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frappe/locale/nb.po b/frappe/locale/nb.po index 6cb7bfd6c9..0f30e40e50 100644 --- a/frappe/locale/nb.po +++ b/frappe/locale/nb.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-21 09:33+0000\n" -"PO-Revision-Date: 2025-09-23 20:33\n" +"PO-Revision-Date: 2025-09-24 20:35\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Norwegian Bokmal\n" "MIME-Version: 1.0\n" @@ -16392,7 +16392,7 @@ msgstr "Navneregel" #. Settings' #: frappe/core/doctype/document_naming_settings/document_naming_settings.json msgid "Naming Series" -msgstr "Navneserie" +msgstr "Nummerserie" #: frappe/model/naming.py:268 msgid "Naming Series mandatory" @@ -29577,15 +29577,15 @@ msgstr "Arbeidsområder" #: frappe/public/js/frappe/form/footer/form_timeline.js:757 msgid "Would you like to publish this comment? This means it will become visible to website/portal users." -msgstr "" +msgstr "Ønsker du å publisere denne kommentaren? Dette betyr at den blir synlig for brukere av nettstedet/portalen." #: frappe/public/js/frappe/form/footer/form_timeline.js:761 msgid "Would you like to unpublish this comment? This means it will no longer be visible to website/portal users." -msgstr "" +msgstr "Ønsker du å avpublisere denne kommentaren? Dette betyr at den ikke lenger vil være synlig for brukere av nettstedet/portalen." #: frappe/desk/page/setup_wizard/setup_wizard.py:41 msgid "Wrapping up" -msgstr "" +msgstr "Oppsummerer" #. Label of the write (Check) field in DocType 'Custom DocPerm' #. Label of the write (Check) field in DocType 'DocPerm' @@ -29604,7 +29604,7 @@ msgstr "" #: frappe/public/js/frappe/views/reports/report_view.js:495 msgid "X Axis Field" -msgstr "" +msgstr "Felt i X-akse" #. Label of the x_field (Select) field in DocType 'Dashboard Chart' #: frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -29623,7 +29623,7 @@ msgstr "Y-akse" #: frappe/public/js/frappe/views/reports/report_view.js:502 msgid "Y Axis Fields" -msgstr "" +msgstr "Felt for Y-akse" #. Label of the y_field (Select) field in DocType 'Dashboard Chart Field' #: frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.json From b5dadc33fa39903737c52c2c47e596f0e53f36f5 Mon Sep 17 00:00:00 2001 From: MochaMind Date: Thu, 25 Sep 2025 02:05:49 +0530 Subject: [PATCH 033/144] fix: Serbian (Latin) translations --- frappe/locale/sr_CS.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frappe/locale/sr_CS.po b/frappe/locale/sr_CS.po index 4d57d2a98c..ebdcb82ce4 100644 --- a/frappe/locale/sr_CS.po +++ b/frappe/locale/sr_CS.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-21 09:33+0000\n" -"PO-Revision-Date: 2025-09-21 19:58\n" +"PO-Revision-Date: 2025-09-24 20:35\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Serbian (Latin)\n" "MIME-Version: 1.0\n" @@ -19603,7 +19603,7 @@ msgstr "Preciznost" #: frappe/core/doctype/doctype/doctype.py:1670 msgid "Precision ({0}) for {1} cannot be greater than its length ({2})." -msgstr "" +msgstr "Preciznost ({0}) za {1} ne može biti veća od njegove dužine ({2})." #: frappe/core/doctype/doctype/doctype.py:1401 msgid "Precision should be between 1 and 6" @@ -23653,7 +23653,7 @@ msgstr "Postavite nestandardnu preciznost za polje sa decimalnim brojem ili valu #. Description of the 'Precision' (Select) field in DocType 'DocField' #: frappe/core/doctype/docfield/docfield.json msgid "Set non-standard precision for a Float, Currency or Percent field" -msgstr "" +msgstr "Podesite nestandardnu preciznost za polja vrste decimalni broj, valuta ili procenat" #. Label of the set_only_once (Check) field in DocType 'DocField' #: frappe/core/doctype/docfield/docfield.json @@ -26111,7 +26111,7 @@ msgstr "Broj projekta dobijen putem Google Cloud konzole, u odeljku
Click here to download:
{0}

This link will expire in {1} hours." -msgstr "" +msgstr "Izveštaj koji ste zatražili je generisan.

Kliknite ovde za preuzimanje:
{0}

Ovaj link ističe za {1} sata." #: frappe/core/doctype/user/user.py:1000 msgid "The reset password link has been expired" @@ -26336,7 +26336,7 @@ msgstr "Ovo se ne može opozvati" #: frappe/desk/doctype/number_card/number_card.js:480 msgctxt "Number Card" msgid "This card is visible only to Administrator and System Managers by default. Set a DocType to share with users who have read access." -msgstr "" +msgstr "Ova kartica je podrazumevano vidljiva samo administratoru i sistem menadžerima. Podesite DocType da je delite sa korisnicima koji imaju pravo čitanja." #. Description of the 'Is Public' (Check) field in DocType 'Number Card' #: frappe/desk/doctype/number_card/number_card.json @@ -30284,7 +30284,7 @@ msgstr "Vaš upit je primljen. Odgovorićemo Vam uskoro, ukoliko imate dodatne i #: frappe/desk/query_report.py:342 frappe/desk/reportview.py:396 msgid "Your report is being generated in the background. You will receive an email on {0} with a download link once it is ready." -msgstr "" +msgstr "Vaš izveštaj se generiše u pozadini. Dobićete imejl na {0} sa linkom za preuzimanje kada bude spreman." #: frappe/app.py:374 msgid "Your session has expired, please login again to continue." From a0e4386c7a90efbbcd8c001a13aed22fc89a2279 Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 25 Sep 2025 04:04:00 +0530 Subject: [PATCH 034/144] fix: let focus jump directly inside the first input of the first row --- .../public/js/frappe/form/controls/table.js | 28 ------------------- frappe/public/js/frappe/form/grid.js | 13 ++++++++- frappe/public/js/frappe/form/grid_row.js | 26 ++++++++++++++++- frappe/public/scss/common/grid.scss | 7 ++--- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js index 62d31a3f7a..e056fe9e07 100644 --- a/frappe/public/js/frappe/form/controls/table.js +++ b/frappe/public/js/frappe/form/controls/table.js @@ -16,25 +16,6 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control this.frm.grids[this.frm.grids.length] = this; } const me = this; - this.$wrapper.on("keydown", (e) => { - if (e.which == 9) { - if (e.shiftKey) { - let row_idx = me.set_current_row(e.target); - if (row_idx) { - this.grid.grid_rows[row_idx - 1].toggle_editable_row(true); - } - } else { - if (this.grid.grid_rows.length > 0) { - this.grid.grid_rows[this.grid.grid_rows.length - 1].toggle_editable_row( - true - ); - } else { - this.grid.add_new_row(null, null, true, null, true); - this.grid.grid_rows[0].toggle_editable_row(true); - } - } - } - }); this.$wrapper.on("paste", ":text", (e) => { const table_field = this.df.fieldname; const grid = this.grid; @@ -173,13 +154,4 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control check_all_rows() { this.$wrapper.find(".grid-row-check")[0].click(); } - set_current_row(target) { - let current_row = null; - for (let i = 0; i < this.grid.grid_rows.length; i++) { - if (this.grid.grid_rows[i].wrapper.get(0).contains(target)) { - current_row = i + 1; - } - } - return current_row; - } }; diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 0b3c4623d0..959e720806 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -70,7 +70,7 @@ export default class Grid {

-
+
@@ -939,6 +939,7 @@ export default class Grid { } setTimeout(() => { + this.grid_rows[idx].toggle_editable_row(true); this.grid_rows[idx].row .find('input[type="Text"],textarea,select') .filter(":visible:first") @@ -1276,4 +1277,14 @@ export default class Grid { this.debounced_refresh(); } + + get_current_row(target) { + let current_row = null; + for (let i = 0; i < this.grid_rows.length; i++) { + if (this.grid_rows[i].wrapper.get(0).contains(target)) { + current_row = i; + } + } + return current_row; + } } diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 10b18171e7..dcfae333a7 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -1101,7 +1101,17 @@ export default class GridRow { this.columns[df.fieldname] = $col; this.columns_list.push($col); - + if (ci == 0 && !this.header_row) { + $col.attr("tabIndex", 0); + $col.on("focus", function () { + if (me.grid.grid_rows.length == 0) { + me.grid.add_new_row(); + } + me.grid.grid_rows[me.grid.grid_rows.length - 1].toggle_editable_row(true); + me.grid.set_focus_on_row(); + $col.attr("tabIndex", ""); + }); + } return $col; } @@ -1199,6 +1209,8 @@ export default class GridRow { // flag list input if (this.columns_list && this.columns_list.slice(-1)[0] === column) { field.$input.attr("data-last-input", 1); + } else if (this.columns_list && this.columns_list.slice(0)[0] === column) { + field.$input.attr("data-first-input", 1); } } @@ -1288,6 +1300,18 @@ export default class GridRow { return false; } } + } else if (e.which === TAB && e.shiftKey) { + var first_column = me.wrapper + .find("input:enabled:not([type='checkbox'])") + .first() + .get(0); + var is_first_column = + $(this).attr("data-first-input") || first_column === this; + if (is_first_column) { + let ri = me.grid.get_current_row(e.target); + if (ri == 0) return; + me.grid.grid_rows[ri - 1].toggle_editable_row(true); + } } }); } diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index 817d337789..63a07df18e 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -8,9 +8,6 @@ color: var(--text-color); min-height: 150px; background-color: var(--subtle-accent); - &:focus-visible { - @include grid-focus(); - } } .form-grid.error { @@ -165,7 +162,9 @@ display: flex; vertical-align: middle; } - +.grid-static-col:focus-visible { + @include grid-focus(); +} .grid-static-col, .row-index { // height: 38px; From 92459e24b50259bd5810e51304656a8898881cc2 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Thu, 25 Sep 2025 11:23:20 +0530 Subject: [PATCH 035/144] refactor: add dynamic max height --- frappe/public/js/frappe/roles_editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/roles_editor.js b/frappe/public/js/frappe/roles_editor.js index 72ce5e3575..afd8e57d56 100644 --- a/frappe/public/js/frappe/roles_editor.js +++ b/frappe/public/js/frappe/roles_editor.js @@ -66,7 +66,7 @@ frappe.RoleEditor = class {
`); } else { $body.append(` -
+
${__("Document Type")} ${__("Level")} ${__("If Owner")} ${__("Document Type")} ${__("Level")} ${__("If Owner")} ${__( + `${__( frappe.unscrub(p) )}
From 5dd94922b1dd2cd11c509a2f68a717c63022c5a7 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Thu, 25 Sep 2025 14:48:44 +0530 Subject: [PATCH 036/144] fix: preserve this context in add_discard --- frappe/public/js/frappe/form/toolbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 13b083f280..f93486cc5a 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -336,7 +336,7 @@ frappe.ui.form.Toolbar = class Toolbar { ) { this.page.add_menu_item( __("Discard"), - function () { + () => { this.frm._discard(); }, true From e124936a71faf88037a7a8ecc82cac6ccc3d202d Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Thu, 25 Sep 2025 16:55:18 +0530 Subject: [PATCH 037/144] fix(dynamic_links): skip virtual docfields in dynamic link map Signed-off-by: Akhil Narang --- frappe/model/dynamic_links.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/model/dynamic_links.py b/frappe/model/dynamic_links.py index a054406c2d..de6b9b0f33 100644 --- a/frappe/model/dynamic_links.py +++ b/frappe/model/dynamic_links.py @@ -13,7 +13,7 @@ dynamic_link_queries = [ `tabDocField`.fieldname, `tabDocField`.options from `tabDocField`, `tabDocType` where `tabDocField`.fieldtype='Dynamic Link' and - `tabDocType`.`name`=`tabDocField`.parent and `tabDocType`.is_virtual = 0 + `tabDocType`.`name`=`tabDocField`.parent and `tabDocType`.is_virtual = 0 and `tabDocField`.is_virtual = 0 order by `tabDocType`.read_only, `tabDocType`.in_create""", """select `tabCustom Field`.dt as parent, `tabDocType`.read_only, `tabDocType`.in_create, From ae5708f9be52cf83da16cf6792b1ba769ea395ea Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 25 Sep 2025 22:16:13 +0530 Subject: [PATCH 038/144] fix: center align checkboxes --- frappe/public/scss/desk/print_preview.scss | 4 ---- frappe/public/scss/desk/role_editor.scss | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/frappe/public/scss/desk/print_preview.scss b/frappe/public/scss/desk/print_preview.scss index d048e453bf..ba236ac0de 100644 --- a/frappe/public/scss/desk/print_preview.scss +++ b/frappe/public/scss/desk/print_preview.scss @@ -49,10 +49,6 @@ align-items: unset; } - .input-area { - margin-top: 0.2rem; - } - .label-area { white-space: unset; } diff --git a/frappe/public/scss/desk/role_editor.scss b/frappe/public/scss/desk/role_editor.scss index aba9d9aba8..da67a4afe8 100644 --- a/frappe/public/scss/desk/role_editor.scss +++ b/frappe/public/scss/desk/role_editor.scss @@ -28,6 +28,7 @@ table.user-perm { margin-bottom: var(--margin-sm); label { position: relative; + align-items: center; } input[type="checkbox"] { margin-left: 0; From 878c0896795ec98cf49fadce91ccad9f383b1db5 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:45:29 +0200 Subject: [PATCH 039/144] fix: special operators in compare util (#34145) --- frappe/tests/test_utils.py | 106 +++++++++++++++++++++++++++++++++++++ frappe/utils/data.py | 34 ++++++++++-- 2 files changed, 136 insertions(+), 4 deletions(-) diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 83a653528e..bd4ebf411c 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -59,6 +59,7 @@ from frappe.utils.data import ( cint, comma_and, comma_or, + compare, cstr, duration_to_seconds, evaluate_filters, @@ -236,6 +237,111 @@ class TestFilters(IntegrationTestCase): } self.assertFalse(evaluate_filters(doc, [("last_password_reset_date", "Timespan", "today")])) + def test_is_operator(self): + """Test 'is' operator for checking if values are set or not set.""" + # Test "is set" with different fieldtypes and values + self.assertTrue(compare("1", "is", "set", "Int")) + self.assertTrue(compare(1, "is", "set", "Int")) + self.assertTrue(compare(0, "is", "set", "Int")) # 0 is considered "set" + self.assertTrue(compare("hello", "is", "set", "Data")) + self.assertTrue(compare(0.0, "is", "set", "Float")) + + # Test "is set" with unset values - None should always be "not set" regardless of fieldtype + self.assertFalse(compare(None, "is", "set", "Int")) + self.assertFalse(compare(None, "is", "set", "Float")) + self.assertFalse(compare(None, "is", "set", "Check")) + self.assertFalse(compare(None, "is", "set", "Data")) + self.assertFalse(compare("", "is", "set")) + self.assertFalse(compare("", "is", "set", "Data")) + self.assertFalse(compare(None, "is", "set")) + + # Test "is not set" with set values + self.assertFalse(compare("1", "is", "not set", "Int")) + self.assertFalse(compare(1, "is", "not set", "Int")) + self.assertFalse(compare(0, "is", "not set", "Int")) + self.assertFalse(compare("hello", "is", "not set", "Data")) + self.assertFalse(compare(0.0, "is", "not set", "Float")) + + # Test "is not set" with unset values - None should always be "not set" regardless of fieldtype + self.assertTrue(compare(None, "is", "not set", "Int")) + self.assertTrue(compare(None, "is", "not set", "Float")) + self.assertTrue(compare(None, "is", "not set", "Check")) + self.assertTrue(compare(None, "is", "not set", "Data")) + self.assertTrue(compare("", "is", "not set")) + self.assertTrue(compare("", "is", "not set", "Data")) + self.assertTrue(compare(None, "is", "not set")) + + def test_in_operators(self): + """Test 'in' and 'not in' operators with and without fieldtype casting.""" + test_list = ["a", "b", "c"] + + # Test "in" operator without fieldtype + self.assertTrue(compare("a", "in", test_list)) + self.assertFalse(compare("", "in", test_list)) + self.assertFalse(compare("d", "in", test_list)) + self.assertFalse(compare(None, "in", test_list)) + + # Test "not in" operator without fieldtype + self.assertFalse(compare("a", "not in", test_list)) + self.assertTrue(compare("", "not in", test_list)) + self.assertTrue(compare("d", "not in", test_list)) + self.assertTrue(compare(None, "not in", test_list)) + + # Test "in" operator with fieldtype casting - only first value should be cast + string_list = ["1", "2", "3"] + self.assertTrue(compare(1, "in", string_list, "Data")) + self.assertTrue(compare("2", "in", string_list, "Data")) + self.assertFalse(compare(4, "in", string_list, "Data")) + + # Test type mismatch: Int fieldtype with string list (val2 is NOT cast) + mixed_list = ["1", "2", "3"] + self.assertFalse(compare("1", "in", mixed_list, "Int")) + self.assertFalse(compare(1, "in", mixed_list, "Int")) + + # Test with matching types: Int fieldtype with int list + int_list = [1, 2, 3] + self.assertTrue(compare("1", "in", int_list, "Int")) + self.assertTrue(compare(2, "in", int_list, "Int")) + self.assertFalse(compare("4", "in", int_list, "Int")) + + # Test "not in" operator with fieldtype casting + self.assertFalse(compare(1, "not in", string_list, "Data")) + self.assertFalse(compare("2", "not in", string_list, "Data")) + self.assertTrue(compare(4, "not in", string_list, "Data")) + + # Test "not in" with type mismatch + self.assertTrue(compare("1", "not in", mixed_list, "Int")) + self.assertFalse(compare("1", "not in", int_list, "Int")) + + # Test with Float fieldtype + float_list = [1.5, 2.5, 3.5] + self.assertTrue(compare("1.5", "in", float_list, "Float")) + self.assertFalse(compare("4.5", "in", float_list, "Float")) + + # Test None with "in"/"not in" operators - None should not be cast + self.assertFalse(compare(None, "in", [""], "Data")) + self.assertFalse(compare(None, "in", [0], "Int")) + self.assertFalse(compare(None, "in", [0.0], "Float")) + self.assertFalse(compare(None, "in", ["", "test"], "Data")) + self.assertTrue(compare(None, "in", [None, "test"], "Data")) + + # Test "not in" with None + self.assertTrue(compare(None, "not in", [""], "Data")) + self.assertTrue(compare(None, "not in", [0], "Int")) + self.assertTrue(compare(None, "not in", [0.0], "Float")) + self.assertTrue(compare(None, "not in", ["", "test"], "Data")) + self.assertFalse(compare(None, "not in", [None, "test"], "Data")) + + def test_is_operator_case_insensitive(self): + """Test that 'is' operator patterns are case insensitive.""" + self.assertTrue(compare("value", "is", "SET")) + self.assertTrue(compare("value", "is", "Set")) + self.assertTrue(compare("value", "is", "set")) + + self.assertTrue(compare(None, "is", "NOT SET")) + self.assertTrue(compare(None, "is", "Not Set")) + self.assertTrue(compare(None, "is", "not set")) + class TestMoney(IntegrationTestCase): def test_money_in_words(self): diff --git a/frappe/utils/data.py b/frappe/utils/data.py index e9d8c3881b..e0bdf17de8 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -2021,7 +2021,7 @@ def sql_like(value: str, pattern: str) -> bool: return pattern in value -def filter_operator_is(value: str, pattern: str) -> bool: +def filter_operator_is(value: str | None, pattern: str) -> bool: """Operator `is` can have two values: 'set' or 'not set'.""" pattern = pattern.lower() @@ -2082,11 +2082,37 @@ def evaluate_filters(doc: "Mapping", filters: FilterSignature): return True -def compare(val1: Any, condition: str, val2: Any, fieldtype: str | None = None): +def compare(val1: Any, condition: str, val2: Any, fieldtype: str | None = None) -> bool: + """Compare two values using the specified operator with optional fieldtype casting. + + Args: + val1: The left operand value to compare + condition: The comparison operator (e.g., "=", ">", "is", "in", "like") + val2: The right operand value to compare against + fieldtype: Optional fieldtype for casting val1 (and val2 for most operators) + + Returns: + bool: True if the comparison evaluates to True, False otherwise + + Note: + - For "is" operator: No casting is performed to preserve None values + - For "in"/"not in" operators: Only val1 is cast (if not None), val2 remains unchanged + - For "Timespan" operator: No casting is performed + - For other operators: Both val1 and val2 are cast to the specified fieldtype + """ if fieldtype: - val1 = cast(fieldtype, val1) - if condition != "Timespan": + if condition in {"is", "Timespan"}: + # No casting to preserve original values + pass + elif condition in {"in", "not in"}: + # Cast only val1 (if not None), preserve val2 container + if val1 is not None: + val1 = cast(fieldtype, val1) + else: + # Cast both values for comparison operators (=, !=, >, <, >=, <=, like, etc.) + val1 = cast(fieldtype, val1) val2 = cast(fieldtype, val2) + if condition in operator_map: return operator_map[condition](val1, val2) From 59b7fc19d759763e80fb1278ead9f00e4301c6cd Mon Sep 17 00:00:00 2001 From: MochaMind Date: Fri, 26 Sep 2025 02:09:34 +0530 Subject: [PATCH 040/144] fix: Norwegian Bokmal translations --- frappe/locale/nb.po | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/frappe/locale/nb.po b/frappe/locale/nb.po index 0f30e40e50..1f5edf9369 100644 --- a/frappe/locale/nb.po +++ b/frappe/locale/nb.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-21 09:33+0000\n" -"PO-Revision-Date: 2025-09-24 20:35\n" +"PO-Revision-Date: 2025-09-25 20:39\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Norwegian Bokmal\n" "MIME-Version: 1.0\n" @@ -3811,7 +3811,7 @@ msgstr "Skygger for knapp" #: frappe/core/doctype/doctype/doctype.json #: frappe/custom/doctype/customize_form/customize_form.json msgid "By \"Naming Series\" field" -msgstr "Via feltet \"Navngiving av serier\"" +msgstr "Via feltet \"Nummerserie\"" #: frappe/website/doctype/web_page/web_page.js:111 #: frappe/website/doctype/web_page/web_page.js:118 @@ -5335,7 +5335,7 @@ msgstr "Konfigurer hvordan endrede dokumenter skal navngis.
\n\n" #. Description of a DocType #: frappe/core/doctype/document_naming_settings/document_naming_settings.json msgid "Configure various aspects of how document naming works like naming series, current counter." -msgstr "Konfigurer ulike aspekter av hvordan dokumentnavngivning fungerer, for eksempel navngivning av serier, gjeldende teller." +msgstr "Konfigurer ulike aspekter av hvordan dokumentnavngivning fungerer, for eksempel nummerserie, gjeldende teller." #: frappe/core/doctype/user/user.js:412 frappe/public/js/frappe/dom.js:345 #: frappe/www/update-password.html:66 @@ -12403,7 +12403,7 @@ msgstr "Hvis du oppdaterer, velg «Overskriv», ellers slettes ikke eksisterende #: frappe/core/doctype/data_export/exporter.py:188 msgid "If you are uploading new records, \"Naming Series\" becomes mandatory, if present." -msgstr "Hvis du laster opp nye poster, blir «Navneserie» obligatorisk, hvis den finnes." +msgstr "Hvis du laster opp nye poster, blir «Nummerserie» påkrevet, hvis den finnes." #: frappe/core/doctype/data_export/exporter.py:186 msgid "If you are uploading new records, leave the \"name\" (ID) column blank." @@ -13273,7 +13273,7 @@ msgstr "Ugyldig e-postserver. Vennligst rett opp og prøv igjen." #: frappe/model/naming.py:109 msgid "Invalid Naming Series: {}" -msgstr "Ugyldig navngivningsserie: {}" +msgstr "Ugyldig nummerserie: {}" #: frappe/core/doctype/rq_job/rq_job.py:113 #: frappe/core/doctype/rq_job/rq_job.py:122 @@ -13466,11 +13466,11 @@ msgstr "" #: frappe/model/naming.py:62 msgid "Invalid naming series {}: dot (.) missing" -msgstr "Ugyldig navneserie {}: punktum (.) mangler" +msgstr "Ugyldig nummerserie {}: punktum (.) mangler" #: frappe/model/naming.py:76 msgid "Invalid naming series {}: dot (.) missing before the numeric placeholders. Kindly use a format like ABCD.#####." -msgstr "Ugyldig navngivningsserie {}: punktum (.) mangler før de numeriske plassholderne. Vennligst bruk et format som ABCD.#####." +msgstr "Ugyldig nummerserie {}: punktum (.) mangler før de numeriske plassholderne. Vennligst bruk et format som ABCD.#####.." #: frappe/core/doctype/data_import/importer.py:453 msgid "Invalid or corrupted content for import" @@ -16377,9 +16377,9 @@ msgstr "Navngivning" msgid "Naming Options:\n" "
  1. field:[fieldname] - By Field
  2. naming_series: - By Naming Series (field called naming_series must be present)
  3. Prompt - Prompt user for a name
  4. [series] - Series by prefix (separated by a dot); for example PRE.#####
  5. \n" "
  6. format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
" -msgstr "Alternativer for navngivning:\n" -"
  1. field:[fieldname] - Etter felt
  2. naming_series: - Etter navngivningsserie (feltet kalt naming_series må være til stede)
  3. Spørsmål - Spør brukeren om et navn
  4. [serie] - Serie etter prefiks (atskilt med punktum); for eksempel PRE.#####
  5. \n" -"
  6. format: EKSEMPEL-{MM}flere ord{fieldname1}-{fieldname2}-{#####} - Erstatt alle ord med parenteser (feltnavn, datoord (DD, MM, ÅÅ), serier) med verdien deres. Utenfor parenteser kan alle tegn brukes.
" +msgstr "Alternativer for nummerserie:\n" +"
  1. field:[fieldname] - Etter felt
  2. naming_series: - Etter nummerserie (feltet kalt naming_series må være til stede)
  3. Spørsmål - Spør brukeren om et navn
  4. [serie] - Serie etter prefiks (atskilt med punktum); for eksempel PRE.#####
  5. \n" +"
  6. format: EKSEMPEL-{MM}flere ord{fieldname1}-{fieldname2}-{#####} - Erstatt alle ord med parenteser (feltnavn, datoord (DD, MM, YY), serier) med verdien deres. Utenfor parenteser kan alle tegn brukes.
" #. Label of the naming_rule (Select) field in DocType 'DocType' #. Label of the naming_rule (Select) field in DocType 'Customize Form' @@ -16396,7 +16396,7 @@ msgstr "Nummerserie" #: frappe/model/naming.py:268 msgid "Naming Series mandatory" -msgstr "Navneserie påkrevet" +msgstr "Nummerserie påkrevet" #. Option for the 'Type' (Select) field in DocType 'Web Template' #. Label of the top_bar (Section Break) field in DocType 'Website Settings' @@ -23561,7 +23561,7 @@ msgstr "Angi begrensning" #. DocType 'Document Naming Settings' #: frappe/core/doctype/document_naming_settings/document_naming_settings.json msgid "Set Naming Series options on your transactions." -msgstr "Angi alternativer for navneserier på transaksjoner" +msgstr "Angi alternativer for nummerserier på transaksjoner." #. Label of the new_password (Password) field in DocType 'User' #: frappe/core/doctype/user/user.json @@ -24611,7 +24611,7 @@ msgstr "Spesialtegn er ikke tillatt" #: frappe/model/naming.py:68 msgid "Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}" -msgstr "Spesialtegn unntatt '-', '#', '.', '/', '{{' and '}}' er ikke tillatt i navneserier {0}" +msgstr "Spesialtegn unntatt '-', '#', '.', '/', '{{' and '}}' er ikke tillatt i nummerserier {0}" #. Description of the 'Timeout (In Seconds)' (Int) field in DocType 'Report' #: frappe/core/doctype/report/report.json @@ -27359,7 +27359,7 @@ msgstr "Prøv igjen" #. Settings' #: frappe/core/doctype/document_naming_settings/document_naming_settings.json msgid "Try a Naming Series" -msgstr "Prøv en navneserie" +msgstr "Prøv en nummerserie" #: frappe/printing/page/print/print.js:202 #: frappe/printing/page/print/print.js:208 @@ -27915,7 +27915,7 @@ msgstr "Oppdaterer globale innstillinger" #: frappe/core/doctype/document_naming_settings/document_naming_settings.js:59 msgid "Updating naming series options" -msgstr "Oppdaterer alternativer for navneserier" +msgstr "Oppdaterer alternativer for nummerserier" #: frappe/public/js/frappe/form/toolbar.js:136 msgid "Updating related fields..." From 6cf52ac7ce31e024966f3338f360781d770381ac Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Fri, 26 Sep 2025 00:54:16 +0200 Subject: [PATCH 041/144] refactor: add type hints and docstring to `delete_doc` (#34006) --- frappe/model/delete_doc.py | 64 +++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 6b3914527d..c8096f3aad 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -3,6 +3,7 @@ import os import shutil +from typing import Any import frappe import frappe.defaults @@ -20,19 +21,58 @@ from frappe.utils.password import delete_all_passwords_for def delete_doc( - doctype=None, - name=None, - force=0, - ignore_doctypes=None, - for_reload=False, - ignore_permissions=False, - flags=None, - ignore_on_trash=False, - ignore_missing=True, - delete_permanently=False, -): + doctype: str | None = None, + name: str | int | list[str | int] | None = None, + force: int | bool = 0, + ignore_doctypes: list[str] | None = None, + for_reload: bool = False, + ignore_permissions: bool = False, + flags: dict[str, Any] | None = None, + ignore_on_trash: bool = False, + ignore_missing: bool = True, + delete_permanently: bool = False, +) -> bool | None: """ - Deletes a doc(dt, dn) and validates if it is not submitted and not linked in a live record + Deletes a document and validates if it is not submitted and not linked in a live record. + + Args: + doctype (str, optional): The document type to delete. If not provided, + retrieved from frappe.form_dict.get("dt"). Defaults to None. + name (str | int | list, optional): The name/ID of the document(s) to delete. + Can be a single name or a list of names. If not provided, + retrieved from frappe.form_dict.get("dn"). Defaults to None. + force (bool, optional): When True, bypasses link existence checks and allows + deletion of documents that are linked to other records. Also allows + deletion of standard DocTypes. Defaults to 0 (False). + ignore_doctypes (list, optional): A list of child doctypes to ignore when + deleting child table records associated with the document. Defaults to None. + for_reload (bool, optional): When True, indicates the deletion is for reloading + purposes (like during doctype updates). Skips certain validations like + permissions and on_trash methods, and automatically sets delete_permanently=True. + Defaults to False. + ignore_permissions (bool, optional): When True, bypasses permission checks + during deletion. Useful for system operations. Defaults to False. + flags (dict, optional): Additional flags to set on the document during the + deletion process. These flags affect document behavior during deletion. + Defaults to None. + ignore_on_trash (bool, optional): When True, skips calling the document's + on_trash method, which typically contains cleanup logic. Defaults to False. + ignore_missing (bool, optional): When True, doesn't raise an error if the + document doesn't exist and returns False. When False, raises + frappe.DoesNotExistError if document is missing. Defaults to True. + delete_permanently (bool, optional): When True, permanently deletes the document + without adding it to the "Deleted Document" table for recovery purposes. + When False, the document is soft-deleted and can be recovered. Defaults to False. + + Raises: + frappe.DoesNotExistError: When document doesn't exist and ignore_missing=False. + frappe.LinkExistsError: When document is linked to other records and force=False. + frappe.PermissionError: When user doesn't have delete permissions and ignore_permissions=False. + frappe.ValidationError: When trying to delete a submitted document. + frappe.QueryTimeoutError: When document is locked by another user. + + Returns: + bool: False if document doesn't exist and ignore_missing=True, otherwise None. """ if not ignore_doctypes: ignore_doctypes = [] From 3dd7466c6614f021ec6b71ec06801d5f4a9c7a04 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Fri, 26 Sep 2025 12:26:51 +0530 Subject: [PATCH 042/144] fix: try setting request IP from request.remote_addr if possible Some misconfigured setups don't have the IP set in the headers Signed-off-by: Akhil Narang --- frappe/auth.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/auth.py b/frappe/auth.py index f667138ea1..67c005f560 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -66,6 +66,9 @@ class HTTPRequest: elif frappe.get_request_header("REMOTE_ADDR"): frappe.local.request_ip = frappe.get_request_header("REMOTE_ADDR") + elif frappe.request and getattr(frappe.request, "remote_addr", None): + frappe.local.request_ip = frappe.request.remote_addr + else: frappe.local.request_ip = "127.0.0.1" From 0a08418947b92cd6641a8b13bd8969c1d312668a Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Fri, 26 Sep 2025 13:16:21 +0530 Subject: [PATCH 043/144] build(deps): bump ruff Signed-off-by: Akhil Narang --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef3d02e220..7e3c8257ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: exclude: ^frappe/tests/classes/context_managers\.py$ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.1 + rev: v0.13.2 hooks: - id: ruff name: "Run ruff import sorter" From 6ca4d4d167a1a009d99062747711de7a994aa633 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Fri, 26 Sep 2025 13:16:43 +0530 Subject: [PATCH 044/144] refactor(treewide): ruff format Signed-off-by: Akhil Narang --- frappe/auth.py | 2 +- frappe/commands/redis_utils.py | 2 +- frappe/commands/site.py | 2 +- frappe/commands/test_commands.py | 2 +- frappe/commands/utils.py | 3 +-- frappe/core/doctype/audit_trail/test_audit_trail.py | 4 ++-- frappe/core/doctype/communication/communication.py | 6 +++--- frappe/database/postgres/schema.py | 2 +- frappe/deprecation_dumpster.py | 2 +- frappe/desk/form/linked_with.py | 3 +-- frappe/desk/listview.py | 2 +- frappe/desk/reportview.py | 2 +- frappe/desk/treeview.py | 6 ++---- frappe/email/doctype/email_queue/test_email_queue.py | 2 +- frappe/email/doctype/notification/notification.py | 4 +++- frappe/email/email_body.py | 6 +++--- frappe/email/receive.py | 6 +++--- .../doctype/geolocation_settings/providers/here.py | 2 +- .../doctype/geolocation_settings/providers/nomatim.py | 2 +- .../doctype/ldap_settings/test_ldap_settings.py | 4 ++-- frappe/integrations/oauth2.py | 8 ++++---- frappe/tests/test_api.py | 2 +- frappe/tests/test_hooks.py | 2 +- frappe/tests/test_patches.py | 4 ++-- frappe/tests/test_query_report.py | 2 +- frappe/tests/test_utils.py | 8 ++++---- frappe/tests/utils/generators.py | 5 ++--- frappe/translate.py | 4 ++-- frappe/utils/__init__.py | 2 +- frappe/utils/background_jobs.py | 2 +- frappe/utils/backups.py | 2 +- frappe/utils/data.py | 2 +- frappe/utils/goal.py | 2 +- frappe/utils/weasyprint.py | 2 +- frappe/www/printview.py | 4 ++-- 35 files changed, 56 insertions(+), 59 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index f667138ea1..b1e0c0b764 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -666,7 +666,7 @@ def validate_oauth(authorization_header): required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split( get_url_delimiter() ) - valid, oauthlib_request = get_oauth_server().verify_request( + valid, _oauthlib_request = get_oauth_server().verify_request( uri, http_method, body, headers, required_scopes ) if valid: diff --git a/frappe/commands/redis_utils.py b/frappe/commands/redis_utils.py index 884c4400ff..e4e83a87d9 100644 --- a/frappe/commands/redis_utils.py +++ b/frappe/commands/redis_utils.py @@ -61,7 +61,7 @@ def create_rq_users(set_admin_password=False, use_rq_auth=False): ) click.secho(f"`export {env_key}={user_credentials['default'][1]}`") click.secho( - "NOTE: Please save the admin password as you " "can not access redis server without the password", + "NOTE: Please save the admin password as you can not access redis server without the password", fg="yellow", ) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 7a2a690033..3e920cc0c2 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -336,7 +336,7 @@ def restore_backup( # Check if the backup is of an older version of frappe and the user hasn't specified force if is_downgrade(sql_file_path, verbose=True) and not force: warn_message = ( - "This is not recommended and may lead to unexpected behaviour. " "Do you want to continue anyway?" + "This is not recommended and may lead to unexpected behaviour. Do you want to continue anyway?" ) click.confirm(warn_message, abort=True) diff --git a/frappe/commands/test_commands.py b/frappe/commands/test_commands.py index de9f154cd4..cde28456ca 100644 --- a/frappe/commands/test_commands.py +++ b/frappe/commands/test_commands.py @@ -297,7 +297,7 @@ class TestCommands(BaseTestCommands): self.execute("bench --site {test_site} backup --exclude 'ToDo'", site_data) site_data.update({"kw": "\"{'partial':True}\""}) self.execute( - "bench --site {test_site} execute" " frappe.utils.backups.fetch_latest_backups --kwargs {kw}", + "bench --site {test_site} execute frappe.utils.backups.fetch_latest_backups --kwargs {kw}", site_data, ) site_data.update({"database": json.loads(self.stdout)["database"]}) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index f09bc49474..b9b730fd90 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -435,8 +435,7 @@ def import_doc(context: CliCtxObj, path, force=False): type=click.Path(exists=True, dir_okay=False, resolve_path=True), required=True, help=( - "Path to import file (.csv, .xlsx)." - "Consider that relative paths will resolve from 'sites' directory" + "Path to import file (.csv, .xlsx).Consider that relative paths will resolve from 'sites' directory" ), ) @click.option("--doctype", type=str, required=True) diff --git a/frappe/core/doctype/audit_trail/test_audit_trail.py b/frappe/core/doctype/audit_trail/test_audit_trail.py index 41dadc92b5..820651e751 100644 --- a/frappe/core/doctype/audit_trail/test_audit_trail.py +++ b/frappe/core/doctype/audit_trail/test_audit_trail.py @@ -25,7 +25,7 @@ class TestAuditTrail(IntegrationTestCase): re_amended_doc = amend_document(amended_doc, changed_fields, {}, 1) comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", re_amended_doc.name) - documents, results = comparator.compare_document() + _documents, results = comparator.compare_document() test_field_values = results["changed"]["Field"] self.check_expected_values(test_field_values, ["first value", "second value", "third value"]) @@ -41,7 +41,7 @@ class TestAuditTrail(IntegrationTestCase): amended_doc = amend_document(doc, {}, rows_updated, 1) comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", amended_doc.name) - documents, results = comparator.compare_document() + _documents, results = comparator.compare_document() results = frappe._dict(results) self.check_rows_updated(results.row_changed) diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 9e6907edd6..2c5cf4889a 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -565,11 +565,11 @@ def parse_email(email_strings): for email in email_string.split(","): local_part = email.split("@", 1)[0].strip('"') - user, detail = None, None + _user, detail = None, None if "+" in local_part: - user, detail = local_part.split("+", 1) + _user, detail = local_part.split("+", 1) elif "--" in local_part: - detail, user = local_part.rsplit("--", 1) + detail, _user = local_part.rsplit("--", 1) if not detail: continue diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py index 926f9e0edd..75b944f74a 100644 --- a/frappe/database/postgres/schema.py +++ b/frappe/database/postgres/schema.py @@ -147,7 +147,7 @@ class PostgresTable(DBTable): if isinstance(default, str): default = frappe.db.escape(default) change_nullability.append( - f"ALTER COLUMN \"{col.fieldname}\" {'SET' if col.not_nullable else 'DROP'} NOT NULL" + f'ALTER COLUMN "{col.fieldname}" {"SET" if col.not_nullable else "DROP"} NOT NULL' ) change_nullability.append(f'ALTER COLUMN "{col.fieldname}" SET DEFAULT {default}') diff --git a/frappe/deprecation_dumpster.py b/frappe/deprecation_dumpster.py index 6f6e743620..bdf91e9a38 100644 --- a/frappe/deprecation_dumpster.py +++ b/frappe/deprecation_dumpster.py @@ -899,7 +899,7 @@ def tests_utils_get_dependencies(doctype): import frappe from frappe.tests.utils.generators import get_modules - module, test_module = get_modules(doctype) + _module, test_module = get_modules(doctype) meta = frappe.get_meta(doctype) link_fields = meta.get_link_fields() diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 7dbea54d95..072dcc8433 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -622,8 +622,7 @@ def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False): "DocField", fields=["parent", "options"], filters=child_filters, as_list=1 ): ret[parent] = {"child_doctype": options, "fieldname": links_dict[options]} - if options in ret: - del ret[options] + ret.pop(options, None) virtual_doctypes = frappe.get_all("DocType", {"is_virtual": 1}, pluck="name") for dt in virtual_doctypes: diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py index 9c91026aa6..94af0a06aa 100644 --- a/frappe/desk/listview.py +++ b/frappe/desk/listview.py @@ -83,7 +83,7 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d break if owner_idx: - data = [data.pop(owner_idx)] + data[0:49] + data = [data.pop(owner_idx), *data[0:49]] else: data = data[0:50] else: diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index b51753bb2b..35761a58e1 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -547,7 +547,7 @@ def get_field_info(fields, doctype): if parenttype != doctype: # If the column is from a child table, append the child doctype. # For example, "Item Code (Sales Invoice Item)". - label += f" ({ _(parenttype) })" + label += f" ({_(parenttype)})" field_info.append( {"name": name, "label": label, "fieldtype": fieldtype, "translatable": translatable} diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index 2f315924b5..3e73db2806 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -9,8 +9,7 @@ from frappe import _ def get_all_nodes(doctype, label, parent, tree_method, **filters): """Recursively gets all data from tree nodes""" - if "cmd" in filters: - del filters["cmd"] + filters.pop("cmd", None) filters.pop("data", None) tree_method = frappe.get_attr(tree_method) @@ -20,8 +19,7 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters): data = tree_method(doctype, parent, **filters) out = [dict(parent=label, data=data)] - if "is_root" in filters: - del filters["is_root"] + filters.pop("is_root", None) to_check = [d.get("value") for d in data if d.get("expandable")] while to_check: diff --git a/frappe/email/doctype/email_queue/test_email_queue.py b/frappe/email/doctype/email_queue/test_email_queue.py index 5dc76096f4..bcbf91d249 100644 --- a/frappe/email/doctype/email_queue/test_email_queue.py +++ b/frappe/email/doctype/email_queue/test_email_queue.py @@ -54,7 +54,7 @@ class TestEmailQueue(IntegrationTestCase): Subject: {subject} From: Test To: - Date: {frappe.utils.now_datetime().strftime('%a, %d %b %Y %H:%M:%S %z')} + Date: {frappe.utils.now_datetime().strftime("%a, %d %b %Y %H:%M:%S %z")} Reply-To: test@example.com X-Frappe-Site: {frappe.local.site} """ diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index cc9222301e..a094c3f1b5 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -352,7 +352,9 @@ def get_context(context): To queue a notification from a server script: ```python - notification = frappe.get_doc("Notification", "My Notification", ignore_permissions=True) + notification = frappe.get_doc( + "Notification", "My Notification", ignore_permissions=True + ) notification.queue_send(customer) ``` diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 7a2256fdda..e02ae105b0 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -237,7 +237,7 @@ class EMail: """Append the message with MIME content to the root node (as attachment)""" from email.mime.text import MIMEText - maintype, subtype = mime_type.split("/") + _maintype, subtype = mime_type.split("/") part = MIMEText(message, _subtype=subtype, policy=policy.SMTP) if as_attachment: @@ -445,7 +445,7 @@ def add_attachment(fname, fcontent, content_type=None, parent=None, content_id=N from email.mime.text import MIMEText if not content_type: - content_type, encoding = mimetypes.guess_type(fname) + content_type, _encoding = mimetypes.guess_type(fname) if not parent: return @@ -597,7 +597,7 @@ def get_header(header=None): if not title: title = frappe.get_hooks("app_title")[-1] - email_header, text = get_email_from_template( + email_header, _text = get_email_from_template( "email_header", {"header_title": title, "indicator": indicator} ) diff --git a/frappe/email/receive.py b/frappe/email/receive.py index a2598546e2..6733c2d246 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -205,7 +205,7 @@ class EmailServer: readonly = self.settings.email_sync_rule != "UNSEEN" self.imap.select(folder, readonly=readonly) - response, message = self.imap.uid("search", None, self.settings.email_sync_rule) + _response, message = self.imap.uid("search", None, self.settings.email_sync_rule) if message[0]: email_list = message[0].split() else: @@ -217,7 +217,7 @@ class EmailServer: # compare the UIDVALIDITY of email account and imap server uid_validity = self.settings.uid_validity - response, message = self.imap.status(folder, "(UIDVALIDITY UIDNEXT)") + _response, message = self.imap.status(folder, "(UIDVALIDITY UIDNEXT)") current_uid_validity = self.parse_imap_response("UIDVALIDITY", message[0]) or 0 uidnext = int(self.parse_imap_response("UIDNEXT", message[0]) or "1") @@ -270,7 +270,7 @@ class EmailServer: def retrieve_message(self, uid, msg_num, folder): try: if cint(self.settings.use_imap): - status, message = self.imap.uid("fetch", uid, "(BODY.PEEK[] BODY.PEEK[HEADER] FLAGS)") + _status, message = self.imap.uid("fetch", uid, "(BODY.PEEK[] BODY.PEEK[HEADER] FLAGS)") raw = message[0] self.get_email_seen_status(uid, raw[0]) diff --git a/frappe/integrations/doctype/geolocation_settings/providers/here.py b/frappe/integrations/doctype/geolocation_settings/providers/here.py index 0ecac02d9c..9234d528d7 100644 --- a/frappe/integrations/doctype/geolocation_settings/providers/here.py +++ b/frappe/integrations/doctype/geolocation_settings/providers/here.py @@ -34,7 +34,7 @@ class Here: "label": address["label"], "value": json.dumps( { - "address_line1": f'{address.get("street", "")} {address.get("houseNumber", "")}'.strip(), + "address_line1": f"{address.get('street', '')} {address.get('houseNumber', '')}".strip(), "city": address.get("city", ""), "state": address.get("state", ""), "pincode": address.get("postalCode", ""), diff --git a/frappe/integrations/doctype/geolocation_settings/providers/nomatim.py b/frappe/integrations/doctype/geolocation_settings/providers/nomatim.py index c02e14ad68..c7d74b9510 100644 --- a/frappe/integrations/doctype/geolocation_settings/providers/nomatim.py +++ b/frappe/integrations/doctype/geolocation_settings/providers/nomatim.py @@ -37,7 +37,7 @@ class Nomatim: "label": result["display_name"], "value": json.dumps( { - "address_line1": f'{address.get("road")} {address.get("house_number", "")}'.strip(), + "address_line1": f"{address.get('road')} {address.get('house_number', '')}".strip(), "city": address.get("city") or address.get("town") or address.get("village"), "state": address.get("state"), "pincode": address.get("postcode"), diff --git a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py index 85bf3b4af9..ef22e1ff6f 100644 --- a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py @@ -240,7 +240,7 @@ class LDAP_TestCase: function_return = self.test_class.connect_to_ldap( base_dn=self.base_dn, password=self.base_password ) - args, kwargs = ldap3_connection_method.call_args + _args, kwargs = ldap3_connection_method.call_args for connection_arg in kwargs: if ( @@ -305,7 +305,7 @@ class LDAP_TestCase: base_dn=self.base_dn, password=self.base_password, read_only=False ) - args, kwargs = ldap3_connection_method.call_args + _args, kwargs = ldap3_connection_method.call_args self.assertFalse( kwargs["read_only"], diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index 96224f4c5a..063bd2a3bc 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -72,7 +72,7 @@ def approve(*args, **kwargs): frappe.flags.oauth_credentials, ) = get_oauth_server().validate_authorization_request(r.url, r.method, r.get_data(), r.headers) - headers, body, status = get_oauth_server().create_authorization_response( + headers, _body, _status = get_oauth_server().create_authorization_response( uri=frappe.flags.oauth_credentials["redirect_uri"], body=r.get_data(), headers=r.headers, @@ -144,7 +144,7 @@ def authorize(**kwargs): def get_token(*args, **kwargs): try: r = frappe.request - headers, body, status = get_oauth_server().create_token_response( + _headers, body, _status = get_oauth_server().create_token_response( r.url, r.method, r.form, r.headers, frappe.flags.oauth_credentials ) body = frappe._dict(json.loads(body)) @@ -165,7 +165,7 @@ def get_token(*args, **kwargs): def revoke_token(*args, **kwargs): try: r = frappe.request - headers, body, status = get_oauth_server().create_revocation_response( + _headers, _body, status = get_oauth_server().create_revocation_response( r.url, headers=r.headers, body=r.form, @@ -184,7 +184,7 @@ def revoke_token(*args, **kwargs): def openid_profile(*args, **kwargs): try: r = frappe.request - headers, body, status = get_oauth_server().create_userinfo_response( + _headers, body, _status = get_oauth_server().create_userinfo_response( r.url, headers=r.headers, body=r.form, diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py index 4518fdc52d..789fb1e15f 100644 --- a/frappe/tests/test_api.py +++ b/frappe/tests/test_api.py @@ -471,7 +471,7 @@ class TestResponse(FrappeAPITestCase): } for redirect, expected_redirect in expected_redirects.items(): - response = self.get(f"/login?{urlencode({'redirect-to':redirect})}", {"sid": self.sid}) + response = self.get(f"/login?{urlencode({'redirect-to': redirect})}", {"sid": self.sid}) self.assertEqual(response.location, expected_redirect) diff --git a/frappe/tests/test_hooks.py b/frappe/tests/test_hooks.py index 8c84f5e4a0..5860f5e860 100644 --- a/frappe/tests/test_hooks.py +++ b/frappe/tests/test_hooks.py @@ -205,7 +205,7 @@ def custom_has_permission(doc, ptype, user): def custom_auth(): - auth_type, token = frappe.get_request_header("Authorization", "Bearer ").split(" ") + _auth_type, token = frappe.get_request_header("Authorization", "Bearer ").split(" ") if token == "set_test_example_user": frappe.set_user("test@example.com") diff --git a/frappe/tests/test_patches.py b/frappe/tests/test_patches.py index fc6e466bed..9cddbb7001 100644 --- a/frappe/tests/test_patches.py +++ b/frappe/tests/test_patches.py @@ -121,7 +121,7 @@ class TestPatchReader(IntegrationTestCase): @patch("builtins.open", new_callable=mock_open, read_data=EDGE_CASES) def test_new_style_edge_cases(self, _file): - all, pre, post = self.get_patches() + _all, pre, _post = self.get_patches() self.assertEqual( pre, [ @@ -134,7 +134,7 @@ class TestPatchReader(IntegrationTestCase): @patch("builtins.open", new_callable=mock_open, read_data=COMMENTED_OUT) def test_ignore_comments(self, _file): - all, pre, post = self.get_patches() + _all, pre, _post = self.get_patches() self.assertEqual(pre, ["app.module.patch1", "app.module.patch3"]) def test_verify_patch_txt(self): diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index 7114a5dc99..2ad356c924 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -53,7 +53,7 @@ class TestQueryReport(IntegrationTestCase): visible_idx = [0, 2, 3] # Build the result - xlsx_data, column_widths = build_xlsx_data( + xlsx_data, _column_widths = build_xlsx_data( data, visible_idx, include_indentation=False, include_filters=True ) diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 83a653528e..79fa026b92 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -362,7 +362,7 @@ class TestMathUtils(IntegrationTestCase): self.assertEqual(floor(22.7330), 22) self.assertEqual(floor("24.7"), 24) self.assertEqual(floor("26.7"), 26) - self.assertEqual(floor(Decimal(29.45)), 29) + self.assertEqual(floor(Decimal("29.45")), 29) def test_ceil(self): from decimal import Decimal @@ -372,7 +372,7 @@ class TestMathUtils(IntegrationTestCase): self.assertEqual(ceil(22.7330), 23) self.assertEqual(ceil("24.7"), 25) self.assertEqual(ceil("26.7"), 27) - self.assertEqual(ceil(Decimal(29.45)), 30) + self.assertEqual(ceil(Decimal("29.45")), 30) class TestHTMLUtils(IntegrationTestCase): @@ -800,7 +800,7 @@ class TestResponse(IntegrationTestCase): timedelta(days=10, hours=12, minutes=120, seconds=10), ], "float": [ - Decimal(29.21), + Decimal("29.21"), ], "doc": [ frappe.get_doc("System Settings"), @@ -1071,7 +1071,7 @@ class TestMiscUtils(IntegrationTestCase): self.assertIsInstance(get_file_timestamp(__file__), str) def test_execute_in_shell(self): - err, out = execute_in_shell("ls") + _err, out = execute_in_shell("ls") self.assertIn("apps", cstr(out)) def test_get_all_sites(self): diff --git a/frappe/tests/utils/generators.py b/frappe/tests/utils/generators.py index b92364ea50..93b72edc2d 100644 --- a/frappe/tests/utils/generators.py +++ b/frappe/tests/utils/generators.py @@ -63,7 +63,7 @@ def get_missing_records_doctypes(doctype, visited=None) -> list[str]: # Mark as visited visited.add(doctype) - module, test_module = get_modules(doctype) + _module, test_module = get_modules(doctype) meta = frappe.get_meta(doctype) link_fields = meta.get_link_fields() @@ -158,12 +158,11 @@ def _generate_records_for( index_doctype: str, reset: bool = False, commit: bool = False, initial_doctype: str | None = None ) -> Generator[tuple[str, "Document"], None, None]: """Create and yield test records for a specific doctype.""" - module: str test_module: ModuleType logstr = f" {index_doctype} via {initial_doctype}" - module, test_module = get_modules(index_doctype) + _module, test_module = get_modules(index_doctype) # First prioriry: module's _make_test_records as an escape hatch # to completely bypass the standard loading and create test records diff --git a/frappe/translate.py b/frappe/translate.py index ead55ab67e..d29485a586 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -695,9 +695,9 @@ def write_csv_file(path, app_messages, lang_dict): if len(app_message) == 2: path, message = app_message elif len(app_message) == 3: - path, message, lineno = app_message + path, message, _lineno = app_message elif len(app_message) == 4: - path, message, context, lineno = app_message + path, message, context, _lineno = app_message else: continue diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index bad08b3565..c9975d643c 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -610,7 +610,7 @@ def get_disk_usage(): files_path = get_files_path() if not os.path.exists(files_path): return 0 - err, out = execute_in_shell(f"du -hsm {files_path}") + _err, out = execute_in_shell(f"du -hsm {files_path}") return cint(out.split("\n")[-2].split("\t")[0]) diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index 39fff78bde..34ac14d463 100644 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -592,7 +592,7 @@ def get_redis_conn(username=None, password=None): return RedisQueue.get_connection(**cred) except redis.exceptions.AuthenticationError: log( - f'Wrong credentials used for {cred.username or "default user"}. ' + f"Wrong credentials used for {cred.username or 'default user'}. " "You can reset credentials using `bench create-rq-users` CLI and restart the server", colour="red", ) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index b8d4b2b2d9..1e74b9b8d6 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -298,7 +298,7 @@ class BackupGenerator: def zip_files(self): # For backwards compatibility - pre v13 click.secho( - "BackupGenerator.zip_files has been deprecated in favour of" " BackupGenerator.backup_files", + "BackupGenerator.zip_files has been deprecated in favour of BackupGenerator.backup_files", fg="yellow", ) return self.backup_files() diff --git a/frappe/utils/data.py b/frappe/utils/data.py index e9d8c3881b..ab5a0f3c8d 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1633,7 +1633,7 @@ def get_thumbnail_base64_for_image(src: str) -> dict[str, str] | None: return try: - image, unused_filename, extn = get_local_image(src) + image, _unused_filename, extn = get_local_image(src) except OSError: return diff --git a/frappe/utils/goal.py b/frappe/utils/goal.py index 01cd9d835e..3ba7c128ba 100644 --- a/frappe/utils/goal.py +++ b/frappe/utils/goal.py @@ -116,7 +116,7 @@ def get_monthly_goal_graph_data( { "title": _("Completed"), "color": "#28a745", - "value": f"{int(round(flt(current_month_value) / flt(goal) * 100))}%", + "value": f"{round(flt(current_month_value) / flt(goal) * 100)}%", }, ] y_markers = {"yMarkers": [{"label": _("Goal"), "lineType": "dashed", "value": flt(goal)}]} diff --git a/frappe/utils/weasyprint.py b/frappe/utils/weasyprint.py index af9778ed49..4703fc807c 100644 --- a/frappe/utils/weasyprint.py +++ b/frappe/utils/weasyprint.py @@ -100,7 +100,7 @@ class PrintFormatGenerator: def render_pdf(self): """Return a bytes sequence of the rendered PDF.""" - HTML, CSS = import_weasyprint() + HTML, _CSS = import_weasyprint() self._make_header_footer() diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 32bfd013ed..ebe5837843 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -229,7 +229,7 @@ def get_rendered_template( if letter_head.header_script: letter_head.content += f""" """ @@ -238,7 +238,7 @@ def get_rendered_template( if letter_head.footer_script: letter_head.footer += f""" """ From 898742cdb61501e8d42c549fc5a05b1cf18912b7 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Fri, 26 Sep 2025 13:22:29 +0530 Subject: [PATCH 045/144] chore: manual fixups Signed-off-by: Akhil Narang --- frappe/commands/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index b9b730fd90..bfbd1b1052 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -435,7 +435,7 @@ def import_doc(context: CliCtxObj, path, force=False): type=click.Path(exists=True, dir_okay=False, resolve_path=True), required=True, help=( - "Path to import file (.csv, .xlsx).Consider that relative paths will resolve from 'sites' directory" + "Path to import file (.csv, .xlsx). Consider that relative paths will resolve from 'sites' directory" ), ) @click.option("--doctype", type=str, required=True) From 1ae276d44163de6dae4ccd6ca168918bd3e9c5d5 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Fri, 26 Sep 2025 13:15:01 +0200 Subject: [PATCH 046/144] fix: do not show debug message when traceback is disallowed --- frappe/utils/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/response.py b/frappe/utils/response.py index bf36b4faa9..082190529e 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -199,7 +199,7 @@ def _make_logs_v1(): [orjson.dumps(d).decode() for d in frappe.local.message_log] ).decode() - if frappe.debug_log: + if frappe.debug_log and is_traceback_allowed(): response["_debug_messages"] = orjson.dumps(frappe.local.debug_log).decode() if frappe.flags.error_message: From 9e5e5ca1ea79140b72ce332943a628f35ae788e8 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Fri, 26 Sep 2025 16:51:18 +0530 Subject: [PATCH 047/144] feat: add Make 'name' searchable in Global Search in custom form --- frappe/custom/doctype/customize_form/customize_form.json | 7 +++++++ frappe/custom/doctype/customize_form/customize_form.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 4eb0593966..c41de205a5 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -44,6 +44,7 @@ "force_re_route_to_default_view", "column_break_29", "show_preview_popup", + "show_name_in_global_search", "email_settings_section", "default_email_template", "column_break_26", @@ -430,6 +431,12 @@ "fieldtype": "Int", "label": "Rows Threshold for Grid Search", "non_negative": 1 + }, + { + "default": "0", + "fieldname": "show_name_in_global_search", + "fieldtype": "Check", + "label": "Make \"name\" searchable in Global Search" } ], "hide_toolbar": 1, diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index f5061f3f52..1fa7a1c4aa 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -79,6 +79,7 @@ class CustomizeForm(Document): search_fields: DF.Data | None sender_field: DF.Data | None sender_name_field: DF.Data | None + show_name_in_global_search: DF.Check show_preview_popup: DF.Check show_title_field_in_link: DF.Check sort_field: DF.Literal[None] @@ -307,6 +308,8 @@ class CustomizeForm(Document): ) def set_property_setters_for_doctype(self, meta): + if self.get("show_name_in_global_search") != meta.get("show_name_in_global_search"): + self.flags.rebuild_doctype_for_global_search = True for prop, prop_type in doctype_properties.items(): if self.get(prop) != meta.get(prop): self.make_property_setter(prop, self.get(prop), prop_type) @@ -736,6 +739,7 @@ doctype_properties = { "track_views": "Check", "allow_auto_repeat": "Check", "allow_import": "Check", + "show_name_in_global_search": "Check", "show_preview_popup": "Check", "default_email_template": "Data", "email_append_to": "Check", From 2558a7b0bfb832b6b54461121e3e459148681419 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:22:43 +0200 Subject: [PATCH 048/144] feat(Form Builder): show default value for "Select" and "Check" fields (#34050) --- frappe/public/js/form_builder/FormBuilder.vue | 8 ++------ .../components/controls/CheckControl.vue | 17 +++++++++++++---- .../components/controls/SelectControl.vue | 14 ++++++++++++-- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/frappe/public/js/form_builder/FormBuilder.vue b/frappe/public/js/form_builder/FormBuilder.vue index 63ed4ba25d..c826f255ab 100644 --- a/frappe/public/js/form_builder/FormBuilder.vue +++ b/frappe/public/js/form_builder/FormBuilder.vue @@ -144,7 +144,7 @@ onMounted(() => store.fetch()); } .editable { - input, + input:not([type="checkbox"]), textarea, select, .ace_editor, @@ -258,7 +258,7 @@ onMounted(() => store.fetch()); border-color: transparent; } - input, + input:not([type="checkbox"]), textarea, select, .ace_editor, @@ -269,10 +269,6 @@ onMounted(() => store.fetch()); .ql-editor { background-color: var(--control-bg) !important; } - - input[type="checkbox"] { - background-color: var(--fg-bg) !important; - } } .form-main > :deep(div:first-child:not(.tab-header)) { diff --git a/frappe/public/js/form_builder/components/controls/CheckControl.vue b/frappe/public/js/form_builder/components/controls/CheckControl.vue index 0b35344805..38c49de355 100644 --- a/frappe/public/js/form_builder/components/controls/CheckControl.vue +++ b/frappe/public/js/form_builder/components/controls/CheckControl.vue @@ -1,8 +1,18 @@