From 6662d4a26311973e36de5ebb908f4d3db9126203 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Fri, 30 May 2025 19:15:08 +0530 Subject: [PATCH 1/5] feat: add download as csv option in prepared report --- .../prepared_report/prepared_report.js | 26 +++++++ .../prepared_report/prepared_report.py | 69 ++++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/prepared_report/prepared_report.js b/frappe/core/doctype/prepared_report/prepared_report.js index aa55de9bbe..607323b279 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.js +++ b/frappe/core/doctype/prepared_report/prepared_report.js @@ -44,6 +44,32 @@ frappe.ui.form.on("Prepared Report", { frappe.route_options = { prepared_report_name: frm.doc.name }; frappe.set_route("query-report", frm.doc.report_name); }); + let csv_attached = (frm.get_files() || []).some((f) => f.file_url.endsWith(".csv")); + if (!csv_attached) { + frm.add_custom_button(__("Download as CSV"), function () { + frappe.call({ + method: "frappe.core.doctype.prepared_report.prepared_report.enqueue_json_to_csv_conversion", + args: { + prepared_report_name: frm.doc.name, + }, + callback: function () { + frappe.msgprint( + __( + "Your CSV file is being generated and will appear in the Attachments section once ready. Additionally, you will get notified when the file is available for download." + ) + ); + + frappe.realtime.on("csv_ready_notification", (data) => { + frappe.msgprint( + __( + `${data.message} Download CSV` + ) + ); + }); + }, + }); + }); + } } }, }); diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index 32e5f3ea40..ff69ae3b5b 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -85,7 +85,13 @@ class PreparedReport(Document): def get_prepared_data(self, with_file_name=False): if attachments := get_attachments(self.doctype, self.name): - attachment = attachments[0] + attachment = None + for f in attachments or []: + print(f.file_url.endswith(".gz"), "ends with .json.gz") + if f.file_url.endswith(".gz"): + attachment = f + break + attached_file = frappe.get_doc("File", attachment.name) if with_file_name: @@ -297,3 +303,64 @@ def has_permission(doc, user): return True return doc.report_name in user.get_all_reports().keys() + + +@frappe.whitelist() +def enqueue_json_to_csv_conversion(prepared_report_name): + """Call this to enqueue the conversion in background.""" + enqueue(method=convert_json_to_csv, queue="long", prepared_report_name=prepared_report_name) + + +def convert_json_to_csv(prepared_report_name): + """Background job: Fetch JSON file, convert to CSV, attach CSV to Prepared Report.""" + + import csv + from io import StringIO + + try: + doc = frappe.get_doc("Prepared Report", prepared_report_name) + json_content, file_name = doc.get_prepared_data(with_file_name=True) + + if not json_content: + frappe.log_error(f"No JSON content found for {prepared_report_name}", "CSV Conversion") + return + + parsed = json.loads(json_content) + + columns = parsed.get("columns", []) + result = parsed.get("result", []) + + if not columns or not result: + frappe.log_error("Columns or result is empty", "CSV Conversion") + return + + fieldnames = [col.get("fieldname") for col in columns if col.get("fieldname")] + + output = StringIO() + writer = csv.DictWriter(output, fieldnames=fieldnames) + writer.writeheader() + for row in result: + writer.writerow({key: row.get(key, "") for key in fieldnames}) + + csv_content = output.getvalue().encode("utf-8") + + _file = frappe.get_doc( + { + "doctype": "File", + "file_name": f"csv_{file_name[:-8]}.csv", + "attached_to_doctype": "Prepared Report", + "attached_to_name": prepared_report_name, + "content": csv_content, + "is_private": 1, + } + ) + _file.save(ignore_permissions=True) + + frappe.publish_realtime( + event="csv_ready_notification", + message={"message": "Your CSV file is ready. Click to download.", "file_url": _file.file_url}, + user=frappe.session.user, + ) + + except Exception: + frappe.log_error(frappe.get_traceback(), f"Failed CSV conversion for {prepared_report_name}") From e641483493cfff0e96c85f4c4ced57885806e888 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Fri, 30 May 2025 19:21:11 +0530 Subject: [PATCH 2/5] chore: remove debugging statement --- frappe/core/doctype/prepared_report/prepared_report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index ff69ae3b5b..a4189b8963 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -87,7 +87,6 @@ class PreparedReport(Document): if attachments := get_attachments(self.doctype, self.name): attachment = None for f in attachments or []: - print(f.file_url.endswith(".gz"), "ends with .json.gz") if f.file_url.endswith(".gz"): attachment = f break From 59827412337b39a2ee789fc8d6d0f027d26aa41d Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Fri, 30 May 2025 20:20:18 +0530 Subject: [PATCH 3/5] refactor: send system notification instead of realtime --- .../doctype/prepared_report/prepared_report.js | 8 -------- .../doctype/prepared_report/prepared_report.py | 18 +++++++++++++----- .../notification_log/notification_log.json | 4 ++-- .../notification_log/notification_log.py | 5 +++-- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/frappe/core/doctype/prepared_report/prepared_report.js b/frappe/core/doctype/prepared_report/prepared_report.js index 607323b279..66265fbf74 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.js +++ b/frappe/core/doctype/prepared_report/prepared_report.js @@ -58,14 +58,6 @@ frappe.ui.form.on("Prepared Report", { "Your CSV file is being generated and will appear in the Attachments section once ready. Additionally, you will get notified when the file is available for download." ) ); - - frappe.realtime.on("csv_ready_notification", (data) => { - frappe.msgprint( - __( - `${data.message} Download CSV` - ) - ); - }); }, }); }); diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index a4189b8963..d6a96baf4a 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -11,6 +11,7 @@ from rq import get_current_job import frappe from frappe.database.utils import dangerously_reconnect_on_connection_abort from frappe.desk.form.load import get_attachments +from frappe.desk.notifications import enqueue_create_notification from frappe.desk.query_report import generate_report_result from frappe.model.document import Document from frappe.monitor import add_data_to_monitor @@ -355,11 +356,18 @@ def convert_json_to_csv(prepared_report_name): ) _file.save(ignore_permissions=True) - frappe.publish_realtime( - event="csv_ready_notification", - message={"message": "Your CSV file is ready. Click to download.", "file_url": _file.file_url}, - user=frappe.session.user, - ) + frappe.get_doc( + { + "doctype": "Notification Log", + "subject": "Your CSV file is ready for download", + "email_content": f'Click here to download the file.', + "for_user": frappe.session.user, + "type": "Alert", + "document_type": "File", + "document_name": _file.name, + "link": _file.file_url, + } + ).insert(ignore_permissions=True) except Exception: frappe.log_error(frappe.get_traceback(), f"Failed CSV conversion for {prepared_report_name}") diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json index 3228392a92..4411780a9e 100644 --- a/frappe/desk/doctype/notification_log/notification_log.json +++ b/frappe/desk/doctype/notification_log/notification_log.json @@ -96,7 +96,7 @@ }, { "fieldname": "link", - "fieldtype": "Data", + "fieldtype": "Small Text", "hidden": 1, "label": "Link" }, @@ -109,7 +109,7 @@ "hide_toolbar": 1, "in_create": 1, "links": [], - "modified": "2025-05-10 23:58:54.717673", + "modified": "2025-05-30 20:17:44.969738", "modified_by": "Administrator", "module": "Desk", "name": "Notification Log", diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index ec09e01fd6..bfae0f69a9 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -24,12 +24,13 @@ class NotificationLog(Document): document_name: DF.Data | None document_type: DF.Link | None email_content: DF.TextEditor | None + email_header: DF.Data | None for_user: DF.Link | None from_user: DF.Link | None - link: DF.Data | None + link: DF.SmallText | None read: DF.Check subject: DF.Text | None - type: DF.Literal["", "Mention", "Assignment", "Share", "Alert"] + type: DF.Literal["", "Mention", "Energy Point", "Assignment", "Share", "Alert"] # end: auto-generated types def after_insert(self): From 6bb2981329e1c2faf2cee4975ceb74e1f6fbec37 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Fri, 30 May 2025 20:29:07 +0530 Subject: [PATCH 4/5] refactor: remove unused import --- frappe/core/doctype/prepared_report/prepared_report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index d6a96baf4a..29b321808c 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -11,7 +11,6 @@ from rq import get_current_job import frappe from frappe.database.utils import dangerously_reconnect_on_connection_abort from frappe.desk.form.load import get_attachments -from frappe.desk.notifications import enqueue_create_notification from frappe.desk.query_report import generate_report_result from frappe.model.document import Document from frappe.monitor import add_data_to_monitor From 7d8c90dc41879926bc507a25a7ed95f777d29163 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Fri, 30 May 2025 21:08:20 +0530 Subject: [PATCH 5/5] refactor: remove unnecessay try catch --- .../prepared_report/prepared_report.py | 86 +++++++++---------- .../notification_log/notification_log.py | 2 +- 2 files changed, 42 insertions(+), 46 deletions(-) diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index 29b321808c..2c8fdc2e05 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -316,57 +316,53 @@ def convert_json_to_csv(prepared_report_name): import csv from io import StringIO - try: - doc = frappe.get_doc("Prepared Report", prepared_report_name) - json_content, file_name = doc.get_prepared_data(with_file_name=True) + doc = frappe.get_doc("Prepared Report", prepared_report_name) + json_content, file_name = doc.get_prepared_data(with_file_name=True) - if not json_content: - frappe.log_error(f"No JSON content found for {prepared_report_name}", "CSV Conversion") - return + if not json_content: + frappe.log_error(f"No JSON content found for {prepared_report_name}", "CSV Conversion") + return - parsed = json.loads(json_content) + parsed = json.loads(json_content) - columns = parsed.get("columns", []) - result = parsed.get("result", []) + columns = parsed.get("columns", []) + result = parsed.get("result", []) - if not columns or not result: - frappe.log_error("Columns or result is empty", "CSV Conversion") - return + if not columns or not result: + frappe.log_error("Columns or result is empty", "CSV Conversion") + return - fieldnames = [col.get("fieldname") for col in columns if col.get("fieldname")] + fieldnames = [col.get("fieldname") for col in columns if col.get("fieldname")] - output = StringIO() - writer = csv.DictWriter(output, fieldnames=fieldnames) - writer.writeheader() - for row in result: - writer.writerow({key: row.get(key, "") for key in fieldnames}) + output = StringIO() + writer = csv.DictWriter(output, fieldnames=fieldnames) + writer.writeheader() + for row in result: + writer.writerow({key: row.get(key, "") for key in fieldnames}) - csv_content = output.getvalue().encode("utf-8") + csv_content = output.getvalue().encode("utf-8") - _file = frappe.get_doc( - { - "doctype": "File", - "file_name": f"csv_{file_name[:-8]}.csv", - "attached_to_doctype": "Prepared Report", - "attached_to_name": prepared_report_name, - "content": csv_content, - "is_private": 1, - } - ) - _file.save(ignore_permissions=True) + _file = frappe.get_doc( + { + "doctype": "File", + "file_name": f"csv_{file_name[:-8]}.csv", + "attached_to_doctype": "Prepared Report", + "attached_to_name": prepared_report_name, + "content": csv_content, + "is_private": 1, + } + ) + _file.save(ignore_permissions=True) - frappe.get_doc( - { - "doctype": "Notification Log", - "subject": "Your CSV file is ready for download", - "email_content": f'Click here to download the file.', - "for_user": frappe.session.user, - "type": "Alert", - "document_type": "File", - "document_name": _file.name, - "link": _file.file_url, - } - ).insert(ignore_permissions=True) - - except Exception: - frappe.log_error(frappe.get_traceback(), f"Failed CSV conversion for {prepared_report_name}") + frappe.get_doc( + { + "doctype": "Notification Log", + "subject": "Your CSV file is ready for download", + "email_content": f'Click here to download the file.', + "for_user": frappe.session.user, + "type": "Alert", + "document_type": "File", + "document_name": _file.name, + "link": _file.file_url, + } + ).insert(ignore_permissions=True) diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index bfae0f69a9..dc54833b5a 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -30,7 +30,7 @@ class NotificationLog(Document): link: DF.SmallText | None read: DF.Check subject: DF.Text | None - type: DF.Literal["", "Mention", "Energy Point", "Assignment", "Share", "Alert"] + type: DF.Literal["", "Mention", "Assignment", "Share", "Alert"] # end: auto-generated types def after_insert(self):