diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index 69ccd59600..464c90a8e8 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -10,6 +10,7 @@ export default class BulkOperations { const is_submittable = frappe.model.is_submittable(this.doctype); const allow_print_for_cancelled = cint(print_settings.allow_print_for_cancelled); const letterheads = this.get_letterhead_options(); + const MAX_PRINT_LIMIT = 500; const valid_docs = docs .filter((doc) => { @@ -35,8 +36,10 @@ export default class BulkOperations { return; } - if (valid_docs.length > 50) { - frappe.msgprint(__("You can only print upto 50 documents at a time")); + if (valid_docs.length > MAX_PRINT_LIMIT) { + frappe.msgprint( + __("You can only print upto {0} documents at a time", [MAX_PRINT_LIMIT]) + ); return; } @@ -102,28 +105,34 @@ export default class BulkOperations { pdf_options = JSON.stringify({ "page-size": args.page_size }); } - const w = window.open( - "/api/method/frappe.utils.print_format.download_multi_pdf?" + - "doctype=" + - encodeURIComponent(this.doctype) + - "&name=" + - encodeURIComponent(json_string) + - "&format=" + - encodeURIComponent(print_format) + - "&no_letterhead=" + - (with_letterhead ? "0" : "1") + - "&letterhead=" + - encodeURIComponent(letterhead) + - "&options=" + - encodeURIComponent(pdf_options) - ); - - if (!w) { - frappe.msgprint(__("Please enable pop-ups")); - return; - } + frappe + .call("frappe.utils.print_format.download_multi_pdf_async", { + doctype: this.doctype, + name: json_string, + format: print_format, + no_letterhead: with_letterhead ? "0" : "1", + letterhead: letterhead, + options: pdf_options, + }) + .then((response) => { + let task_id = response.message.task_id; + frappe.realtime.task_subscribe(task_id); + frappe.realtime.on(`task_complete:${task_id}`, (data) => { + frappe.msgprint({ + title: __("Bulk PDF Export"), + message: __("Your PDF is ready for download"), + primary_action: { + label: __("Download PDF"), + client_action: "window.open", + args: data.file_url, + }, + }); + frappe.realtime.task_unsubscribe(task_id); + frappe.realtime.off(`task_complete:${task_id}`); + }); + dialog.hide(); + }); }); - dialog.show(); } diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py index 29c76d7615..fde49db3a0 100644 --- a/frappe/utils/print_format.py +++ b/frappe/utils/print_format.py @@ -1,4 +1,7 @@ +import http +import json import os +import uuid from io import BytesIO from pypdf import PdfWriter @@ -19,14 +22,68 @@ from frappe.www.printview import validate_print_permission @frappe.whitelist() -def download_multi_pdf(doctype, name, format=None, no_letterhead=False, letterhead=None, options=None): +def download_multi_pdf( + doctype: str | dict[str, list[str]], + name: str | list[str], + format: str | None = None, + no_letterhead: bool = False, + letterhead: str | None = None, + options: str | None = None, +): + """ + Calls _download_multi_pdf with the given parameters and returns the response + """ + return _download_multi_pdf(doctype, name, format, no_letterhead, options) + + +@frappe.whitelist() +def download_multi_pdf_async( + doctype: str | dict[str, list[str]], + name: str | list[str], + format: str | None = None, + no_letterhead: bool = False, + letterhead: str | None = None, + options: str | None = None, +): + """ + Calls _download_multi_pdf with the given parameters in a background job, returns task ID + """ + task_id = str(uuid.uuid4()) + if isinstance(doctype, dict): + doc_count = sum([len(doctype[dt]) for dt in doctype]) + else: + doc_count = len(json.loads(name)) + + frappe.enqueue( + _download_multi_pdf, + doctype=doctype, + name=name, + task_id=task_id, + format=format, + no_letterhead=no_letterhead, + letterhead=letterhead, + options=options, + queue="long" if doc_count > 20 else "short", + ) + frappe.local.response["http_status_code"] = http.HTTPStatus.CREATED + return {"task_id": task_id} + + +def _download_multi_pdf( + doctype: str | dict[str, list[str]], + name: str | list[str], + format: str | None = None, + no_letterhead: bool = False, + letterhead: str | None = None, + options: str | None = None, + task_id: str | None = None, +): """Return a PDF compiled by concatenating multiple documents. The documents can be from a single DocType or multiple DocTypes. - Note: The design may seem a little weird, but it exists exists to - ensure backward compatibility. The correct way to use this function is to - pass a dict to doctype as described below + Note: The design may seem a little weird, but it exists to ensure backward compatibility. + The correct way to use this function is to pass a dict to doctype as described below NEW FUNCTIONALITY ================= @@ -51,10 +108,9 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, letterhe Print Format to be used Returns: - PDF: A PDF generated by the concatenation of the mentioned input docs + Publishes a link to the PDF to the given task ID """ - - import json + filename = "" pdf_writer = PdfWriter() @@ -63,24 +119,47 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, letterhe if not isinstance(doctype, dict): result = json.loads(name) + total_docs = len(result) + filename = f"{doctype}_" # Concatenating pdf files - for ss in result: - pdf_writer = frappe.get_print( - doctype, - ss, - format, - as_pdf=True, - output=pdf_writer, - no_letterhead=no_letterhead, - letterhead=letterhead, - pdf_options=options, + for idx, ss in enumerate(result): + try: + pdf_writer = frappe.get_print( + doctype, + ss, + format, + as_pdf=True, + output=pdf_writer, + no_letterhead=no_letterhead, + letterhead=letterhead, + pdf_options=options, + ) + except Exception: + if task_id: + frappe.publish_realtime(task_id=task_id, message={"message": "Failed"}) + + # Publish progress + if task_id: + frappe.publish_progress( + percent=(idx + 1) / total_docs * 100, + title=_("PDF Generation in Progress"), + description=_( + f"{idx + 1}/{total_docs} complete | Please leave this tab open until completion." + ), + task_id=task_id, + ) + + if task_id is None: + frappe.local.response.filename = "{doctype}.pdf".format( + doctype=doctype.replace(" ", "-").replace("/", "-") ) - frappe.local.response.filename = "{doctype}.pdf".format( - doctype=doctype.replace(" ", "-").replace("/", "-") - ) + else: + total_docs = sum([len(doctype[dt]) for dt in doctype]) + count = 1 for doctype_name in doctype: + filename += f"{doctype_name}_" for doc_name in doctype[doctype_name]: try: pdf_writer = frappe.get_print( @@ -94,19 +173,45 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, letterhe pdf_options=options, ) except Exception: + if task_id: + frappe.publish_realtime(task_id=task_id, message="Failed") frappe.log_error( title="Error in Multi PDF download", message=f"Permission Error on doc {doc_name} of doctype {doctype_name}", reference_doctype=doctype_name, reference_name=doc_name, ) - frappe.local.response.filename = f"{name}.pdf" + + count += 1 + + if task_id: + frappe.publish_progress( + percent=count / total_docs * 100, + title=_("PDF Generation in Progress"), + description=_( + f"{count}/{total_docs} complete | Please leave this tab open until completion." + ), + task_id=task_id, + ) + if task_id is None: + frappe.local.response.filename = f"{name}.pdf" with BytesIO() as merged_pdf: pdf_writer.write(merged_pdf) - frappe.local.response.filecontent = merged_pdf.getvalue() - - frappe.local.response.type = "pdf" + if task_id: + _file = frappe.get_doc( + { + "doctype": "File", + "file_name": f"{filename}{task_id}.pdf", + "content": merged_pdf.getvalue(), + "is_private": 1, + } + ) + _file.save() + frappe.publish_realtime(f"task_complete:{task_id}", message={"file_url": _file.unique_url}) + else: + frappe.local.response.filecontent = merged_pdf.getvalue() + frappe.local.response.type = "pdf" @deprecated