From 45443104195c7338a4147000cb68f868f410e483 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 2 Feb 2026 17:39:30 +0530 Subject: [PATCH 001/163] feat: evaluate virtual docfield value in get method --- frappe/model/base_document.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 6c88dfefd6..44e4371e40 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -311,7 +311,7 @@ class BaseDocument: def get_db_value(self, key): return frappe.db.get_value(self.doctype, self.name, key) - def get(self, key, filters=None, limit=None, default=None): + def get(self, key, filters=None, limit=None, default=None, ignore_virtual=False): if isinstance(key, dict): return _filter(self.get_all_children(), key, limit=limit) @@ -327,6 +327,20 @@ class BaseDocument: if limit and isinstance(value, list | tuple) and len(value) > limit: value = value[:limit] + if not value: + df = self.meta.get_field(key) + is_virtual_field = getattr(df, "is_virtual", False) + + if is_virtual_field: + if ignore_virtual or key not in self.permitted_fieldnames: + return value + + if (prop := getattr(type(self), key, None)) and is_a_property(prop): + value = getattr(self, key) + + elif options := getattr(df, "options", None): + value = self._evaluate_virtual_field_options(options) + return value def getone(self, key, filters=None): From dbfa0495ab0872c6d1ee8571953dcfd173aa6d20 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 2 Feb 2026 18:07:52 +0530 Subject: [PATCH 002/163] refactor: common util for fetching virtual field value --- frappe/model/base_document.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 44e4371e40..bd8699c0d1 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -311,6 +311,15 @@ class BaseDocument: def get_db_value(self, key): return frappe.db.get_value(self.doctype, self.name, key) + def get_virtual_field_value(self, df): + fieldname = df.fieldname + + if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop): + return getattr(self, fieldname) + + elif options := getattr(df, "options", None): + return self._evaluate_virtual_field_options(options) + def get(self, key, filters=None, limit=None, default=None, ignore_virtual=False): if isinstance(key, dict): return _filter(self.get_all_children(), key, limit=limit) @@ -329,17 +338,12 @@ class BaseDocument: if not value: df = self.meta.get_field(key) + is_virtual_field = getattr(df, "is_virtual", False) + ignore_virtual = ignore_virtual or key not in self.permitted_fieldnames - if is_virtual_field: - if ignore_virtual or key not in self.permitted_fieldnames: - return value - - if (prop := getattr(type(self), key, None)) and is_a_property(prop): - value = getattr(self, key) - - elif options := getattr(df, "options", None): - value = self._evaluate_virtual_field_options(options) + if is_virtual_field and not ignore_virtual: + value = self.get_virtual_field_value(df, ignore_virtual) return value @@ -530,12 +534,7 @@ class BaseDocument: if is_virtual_field: if ignore_virtual or fieldname not in self.permitted_fieldnames: continue - - if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop): - value = getattr(self, fieldname) - - elif options := getattr(df, "options", None): - value = self._evaluate_virtual_field_options(options) + value = self.get_virtual_field_value(df) fieldtype = df.fieldtype if isinstance(value, list) and fieldtype not in table_fields: From 7e9236289290ba120ea86a978028f089d63429dd Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 2 Feb 2026 22:12:01 +0530 Subject: [PATCH 003/163] fix: don't return virtual values before save --- frappe/model/base_document.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index bd8699c0d1..a9abec60a5 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -311,16 +311,7 @@ class BaseDocument: def get_db_value(self, key): return frappe.db.get_value(self.doctype, self.name, key) - def get_virtual_field_value(self, df): - fieldname = df.fieldname - - if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop): - return getattr(self, fieldname) - - elif options := getattr(df, "options", None): - return self._evaluate_virtual_field_options(options) - - def get(self, key, filters=None, limit=None, default=None, ignore_virtual=False): + def get(self, key, filters=None, limit=None, default=None): if isinstance(key, dict): return _filter(self.get_all_children(), key, limit=limit) @@ -336,15 +327,6 @@ class BaseDocument: if limit and isinstance(value, list | tuple) and len(value) > limit: value = value[:limit] - if not value: - df = self.meta.get_field(key) - - is_virtual_field = getattr(df, "is_virtual", False) - ignore_virtual = ignore_virtual or key not in self.permitted_fieldnames - - if is_virtual_field and not ignore_virtual: - value = self.get_virtual_field_value(df, ignore_virtual) - return value def getone(self, key, filters=None): @@ -512,6 +494,15 @@ class BaseDocument: eval_locals={"doc": self}, ) + def get_virtual_field_value(self, df): + fieldname = df.fieldname + + if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop): + return getattr(self, fieldname) + + elif options := getattr(df, "options", None): + return self._evaluate_virtual_field_options(options) + def get_valid_dict( self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False ) -> _dict: From 36d471b98d7e7d56507a5c35e0a088eb888e21b0 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 2 Feb 2026 22:14:13 +0530 Subject: [PATCH 004/163] fix: show title field values in link for virtual fields --- frappe/desk/search.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 0f9c86cf8e..0cf31ad770 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -337,7 +337,12 @@ def build_for_autosuggest(res: list[tuple], doctype: str) -> list[LinkSearchResu for item in res: item = list(item) if len(item) == 1: - item = [item[0], item[0]] + title_field = meta.title_field + docfield = meta.get_field(title_field) + if docfield and docfield.is_virtual: + doc = frappe.get_doc(meta.name, item[0]) + title_value = doc.get_virtual_field_value(docfield) + item = [item[0], title_value or item[0]] label = _(item[1]) if meta.translated_doctype else item[1] item[1] = item[0] From f1731981a82ca12eea1d927b601857da3cb0e1b2 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 2 Feb 2026 22:16:21 +0530 Subject: [PATCH 005/163] fix: query filters breaking for title virtual fields --- frappe/desk/search.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 0cf31ad770..ebb827f036 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -167,7 +167,9 @@ def search_widget( } search_fields = ["name"] if meta.title_field: - search_fields.append(meta.title_field) + is_virtual_field = getattr(meta.get_field(meta.title_field), "is_virtual", False) + if not is_virtual_field: + search_fields.append(meta.title_field) if meta.search_fields: search_fields.extend(meta.get_search_fields()) From a252e7e2650e861e555483764858b02ec2d70528 Mon Sep 17 00:00:00 2001 From: prathameshkurunkar7 Date: Thu, 19 Feb 2026 14:53:32 +0530 Subject: [PATCH 006/163] fix(sendmail): respect inline_images parameter in sendmail --- frappe/email/email_body.py | 32 +++++++++++++++++++++++++++++--- frappe/email/test_email_body.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 5dc3054843..4ef17acef6 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -210,7 +210,18 @@ class EMail: if has_inline_images: # process inline images - message, _inline_images = replace_filename_with_cid(message) + provided_images = {} + if inline_images: + for img in inline_images: + if img.get("filename") and img.get("filecontent"): + # index by full path and basename for flexible matching + provided_images[img["filename"]] = img["filecontent"] + basename = img["filename"].rsplit("/", 1)[-1] + if basename not in provided_images: + provided_images[basename] = img["filecontent"] + + # process inline images while preferring provided_images over disk reads + message, _inline_images = replace_filename_with_cid(message, provided_images) # prepare parts msg_related = MIMEMultipart("related", policy=policy.SMTP) @@ -552,11 +563,22 @@ def get_footer(email_account, footer=None): return footer -def replace_filename_with_cid(message): +def replace_filename_with_cid(message, provided_images=None): """Replaces with and return the modified message and a list of inline_images with {filename, filecontent, content_id} + + Args: + message: The HTML message to process + provided_images: A dictionary of images to use instead of reading from disk + Example: + { + "assets/frappe/images/filename.jpg": filecontent, + "filename.jpg": filecontent, + } """ + if provided_images is None: + provided_images = {} inline_images = [] @@ -571,7 +593,11 @@ def replace_filename_with_cid(message): img_path_escaped = frappe.utils.html_utils.unescape_html(img_path) filename = img_path_escaped.rsplit("/")[-1] - filecontent = get_filecontent_from_path(img_path_escaped) + # check if the image is provided in the provided_images(by checking full path and basename) + filecontent = provided_images.get(img_path_escaped) or provided_images.get(filename) + if not filecontent: + filecontent = get_filecontent_from_path(img_path_escaped) + if not filecontent: message = re.sub(f"""embed=['"]{re.escape(img_path)}['"]""", "", message) continue diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index 2152c50917..bd10cc1632 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -137,6 +137,39 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> """.format(inline_images[0].get("content_id")) self.assertEqual(message, processed_message) + def test_sendmail_inline_images_parameter_respected(self): + """Test that inline_images parameter works through sendmail.""" + + test_image_content = b"FAKE_PNG_BINARY_CONTENT_FOR_TESTING" + + html_content = '
Logo
' + + inline_images = [ + { + "filename": "files/nonexistent_test_image.png", + "filecontent": test_image_content, + } + ] + + # Use QueueBuilder directly (what sendmail uses internally) + from frappe.email.doctype.email_queue.email_queue import QueueBuilder + + builder = QueueBuilder( + recipients=["test@example.com"], + sender="me@example.com", + subject="Test Inline Images", + message=html_content, + inline_images=inline_images, + ) + + # Get the email content that would be sent + mail = builder.prepare_email_content() + email_string = mail.as_string() + + # Assertions + self.assertIn("cid:", email_string) + self.assertNotIn('embed="files/nonexistent_test_image.png"', email_string) + def test_inline_styling(self): html = """

Hi John

From 90615ea4df94c4c9bc0b9b4ae97b70545cdbf33e Mon Sep 17 00:00:00 2001 From: prathameshkurunkar7 Date: Thu, 19 Feb 2026 15:04:26 +0530 Subject: [PATCH 007/163] docs(test_email_body): clarify test docs --- frappe/email/test_email_body.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index bd10cc1632..27e061d0b0 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -138,7 +138,13 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> self.assertEqual(message, processed_message) def test_sendmail_inline_images_parameter_respected(self): - """Test that inline_images parameter works through sendmail.""" + """ + Test that inline_images parameter works through sendmail. + Earlier this was ignored and the image was read from disk instead of using the provided content. + The way to check this is essentially checking if the image is embedded with cid: + -> Correct behavior + If the image is not embedded with cid: -> Incorrect behavior + """ test_image_content = b"FAKE_PNG_BINARY_CONTENT_FOR_TESTING" @@ -151,7 +157,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> } ] - # Use QueueBuilder directly (what sendmail uses internally) + # use QueueBuilder to send the email (sendmail uses this internally) from frappe.email.doctype.email_queue.email_queue import QueueBuilder builder = QueueBuilder( @@ -162,11 +168,9 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> inline_images=inline_images, ) - # Get the email content that would be sent mail = builder.prepare_email_content() email_string = mail.as_string() - # Assertions self.assertIn("cid:", email_string) self.assertNotIn('embed="files/nonexistent_test_image.png"', email_string) From fd40eef2d3220bb0a56d8e3d4da57bc4460d859f Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Thu, 26 Feb 2026 10:45:47 +0530 Subject: [PATCH 008/163] fix(user): send mail to user to indicate that their password has been updated Send an e-mail to user to indicate that their password has been changed, fixes a security flaw where user would just be logged out and have no clue as to what occurred Co-authored-by: Ankush Menat --- frappe/core/doctype/user/user.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index b1831d59f9..8e65573474 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -376,9 +376,23 @@ class User(Document): toggle_notifications(self.name, enable=cint(self.enabled), ignore_permissions=True) self.disable_email_fields_if_user_disabled() - def email_new_password(self, new_password=None): + def set_new_password(self, new_password=None): + """Set New Password for user""" if new_password and not self.flags.in_insert: _update_password(user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions) + outgoing_email_exists = frappe.db.exists( + "Email Account", {"default_outgoing": 1, "awaiting_password": 0} + ) + if outgoing_email_exists: + email_message = _( + "Your password has been changed and you might have been logged out of all systems.
Please contact the Administrator for further assistance." + ) + user_email = frappe.db.get_value("User", self.name, "email") + frappe.sendmail( + recipients=[user_email], + subject=_("Security Alert: Your password has been changed."), + content=email_message, + ) def set_system_user(self): """For the standard users like admin and guest, the user type is fixed.""" @@ -451,7 +465,7 @@ class User(Document): msgprint(_("Welcome email sent")) return else: - self.email_new_password(new_password) + self.set_new_password(new_password) except frappe.OutgoingEmailError: frappe.clear_last_message() From 97c3ce64080d02e70f85230facbd26041e36940c Mon Sep 17 00:00:00 2001 From: Safwan Samsudeen Date: Fri, 27 Feb 2026 17:19:28 +0530 Subject: [PATCH 009/163] fix: render barcodes in print view --- frappe/printing/page/print/print.js | 11 +++++- .../print_formats/standard_macros.html | 3 ++ frappe/www/printview.html | 35 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index 0e5d68c165..a46299a371 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -499,7 +499,16 @@ frappe.ui.form.PrintView = class { this.$print_format_body .find("body") .html(``); - + const iframeDoc = this.$print_format_body[0]; + const script = iframeDoc.createElement("script"); + script.src = "https://cdn.jsdelivr.net/npm/jsbarcode@3/dist/JsBarcode.all.min.js"; + script.onload = () => { + iframeDoc.querySelectorAll("svg[data-barcode-value]").forEach((el) => { + JsBarcode(el, el.dataset.barcodeValue, { width: 3, height: 50, fontSize: 16 }); + el.setAttribute("width", "100%"); + }); + }; + iframeDoc.head.appendChild(script); this.show_footer(); this.$print_format_body.find(".print-format").css({ diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 4aeb657edf..1f4d2fab96 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -157,6 +157,9 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" {% elif df.fieldtype=="Signature" %} + {% elif df.fieldtype=="Barcode" %} + + {% elif df.fieldtype in ("Attach", "Attach Image") and frappe.utils.is_image(doc[df.fieldname]) %} diff --git a/frappe/www/printview.html b/frappe/www/printview.html index 17e6380baa..f599388137 100644 --- a/frappe/www/printview.html +++ b/frappe/www/printview.html @@ -6,6 +6,41 @@ {{ title }} {{ include_style('print.bundle.css') }} + + {% if print_style %}