Skip to content

Commit

Permalink
feat: move bulk print operation to the background (backport #25358) (#…
Browse files Browse the repository at this point in the history
…25396)

* feat: move bulk print operation to the background

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
(cherry picked from commit bf6cb1a)

# Conflicts:
#	frappe/utils/print_format.py

* fix: open PDF in new tab

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
(cherry picked from commit 5d0db0c)

* fix: update message

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
(cherry picked from commit 9bf22b7)

* fix: add back a limitation to number of the documents

Don't allow printing more than 500 documents

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
(cherry picked from commit 1caae03)

* fix: let backend generate task ID

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
(cherry picked from commit 5a6bff9)

* fix: make filename more user-friendly

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
(cherry picked from commit 508e4d9)

* refactor(bulk_print): choose queue dynamically

Update docstrings and type hints a bit

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
(cherry picked from commit 5e37ac7)

* refactor: add in a new endpoint for background printing

Let the original one stay as-is for backward compatibility

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
(cherry picked from commit 0ec3e4a)

# Conflicts:
#	frappe/utils/print_format.py

* fix: unsubscribe from task after completion

Also update event name to be more logical

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
(cherry picked from commit 6a5af14)

* refactor: make download button a primary action, update text

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
(cherry picked from commit 5f087ed)

* chore: fix conflicts

Signed-off-by: Akhil Narang <me@akhilnarang.dev>

---------

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
Co-authored-by: Akhil Narang <me@akhilnarang.dev>
  • Loading branch information
mergify[bot] and akhilnarang committed Mar 13, 2024
1 parent 08e8d8c commit 4508239
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 48 deletions.
55 changes: 32 additions & 23 deletions frappe/public/js/frappe/list/bulk_operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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;
}

Expand Down Expand Up @@ -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();
}

Expand Down
154 changes: 129 additions & 25 deletions frappe/utils/print_format.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import http
import json
import os
import uuid
from io import BytesIO

from PyPDF2 import PdfWriter
Expand All @@ -19,16 +22,69 @@


@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,
):
"""
Concatenate multiple docs as PDF .
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.
Returns 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
=================
Expand Down Expand Up @@ -56,10 +112,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()

Expand All @@ -68,24 +123,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 _i, ss in enumerate(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(
Expand All @@ -99,19 +177,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
Expand Down

0 comments on commit 4508239

Please sign in to comment.