
Here the requirement is, on-click of the PDFbutton, it has to fetch the data from the database to get the account details and payment information, prepare the pdf containing payment information in tabular format, and trigger the file download so that the browser downloads the file to the user's local file system.
The screenshot shows the CAP application UI, with a custom button to trigger the PDF generation and download.
The downloaded PDF looks like the below screenshot.
Implementation Steps:
Define a Virtual Field
First, define a virtual field at the service level with the type LargeBinary and media type application/pdf. It will automatically create a hyperlink in the UI with the label Open File.
entity PDFDownload {
virtual null as pdf: LargeBinary @Core.MediaType: 'application/pdf',
}
Implementing the Read Handler
In the read handler of the entity, check if the URL path contains "pdf". (this is the name of the virtual field we defined in the entity, if you use a different name for the virtual field, check for the same in the url)
Example:
/dch/ui/payment/accountstatements/AccountStatements(${object.ID})/pdf
If it does, generate the PDF content, convert it to buffer data, push the buffered data into a Readable stream, and return it along with the media content type and disposition filename.
Import Necessary Modules
You will need the Buffer and Readable modules. Import them as shown below:
import { Buffer } from "buffer";
import { Readable } from "stream";
Here is the complete code:
import { Buffer } from "buffer";
import { Readable } from "stream";
srv.on('READ', 'PDFDownload', async (req, next) => {
const url = req["_path"];
let fileName, bufferData, contentType;
try {
if (url.includes("pdf")) {
// Provide the file name
fileName = 'your_file_name.pdf';
// Generate the buffer data
bufferData = await this.generatePdfBuffer(doc:PDFDocument)
contentType = "application/pdf";
}
} catch (error) {
// Handle error if needed
console.error('Error processing PDF download', error);
}
const readable = new Readable();
readable.push(bufferData);
readable.push(null);
return {
value: readable,
$mediaContentType: contentType,
$mediaContentDispositionFilename: fileName
};
});
Creating PDFs with Tables in Node.js using PDFKit
Here we will explore how to create PDFs with tables in Node.js using the pdfkit and pdfkit-table libraries. We will cover setting text properties, positioning text, adding tables, and ensuring table headers repeat on each page.
First, let's import the necessary libraries and initialize the PDF document with tables:
Run the commands npm install pdfkit and npm install pdfkit-table in the terminal, which will create a dependency in package.json as shown below:
"dependencies": {
"pdfkit": "^0.15.0",
"pdfkit-table": "^0.1.99",
}
Initializing the document with the library
import PDFDocument from "pdfkit";
import PDFDocumentWithTables from "pdfkit-table";
const doc = new PDFDocumentWithTables();
Setting Text Properties and Positioning Text
To set the font size and position text at specific coordinates, you can use the following code:
(doc as PDFDocument).fontSize(12); // Set the font size to 12
(doc as PDFDocument).text('Your_Field_Text', xPosition, yPosition); // Position the text at specified coordinates
(doc as PDFDocument).moveDown(1); // Move down one line
For further options to add contents, please refer to the pdfkit documentation.
Adding Tables to the PDF
To add a table, define the table structure with headers and rows:
const tableArray = {
headers: [
{ label: "Label_1", width: 60, renderer: null },
{ label: "Label_2", width: 70, renderer: null },
{ label: "Label_3", width: 80, renderer: null },
],
rows: [
["Value_For_Label_1", "Value_For_Label_2", "Value_For_Label_3"],
["Value_For_Label_1", "Value_For_Label_2", "Value_For_Label_3"],
["Value_For_Label_1", "Value_For_Label_2", "Value_For_Label_3"],
]};
await doc.table(tableArray);
Ensuring Table Headers Repeat on Each Page
If the table entries span multiple pages, you can ensure that the table headers repeat on each page by using a header hook function:
const headerHook = (pageNumber: number, doc: PDFDocument): void => {
if (pageNumber > 1) {
doc.table(tableArray, { startY: doc.y + 10 });
}
};
(doc as PDFDocument).hook = headerHook;
Full Example
Here’s the full example combining all the parts:
import PDFDocument from "pdfkit";
import PDFDocumentWithTables from "pdfkit-table";
const doc = new PDFDocumentWithTables();
// Set text properties and position
(doc as PDFDocument).fontSize(12); // Set the font size to 12
(doc as PDFDocument).text('Your_Field_Text', xPosition, yPosition); // Position the text at specified coordinates
(doc as PDFDocument).moveDown(1); // Move down one line
// Define table structure
const tableArray = {
headers: [
{ label: "Label_1", width: 60, renderer: null },
{ label: "Label_2", width: 70, renderer: null },
{ label: "Label_3", width: 80, renderer: null },
],
rows: [
["Value_For_Label_1", "Value_For_Label_2", "Value_For_Label_3"],
["Value_For_Label_1", "Value_For_Label_2", "Value_For_Label_3"],
["Value_For_Label_1", "Value_For_Label_2", "Value_For_Label_3"],
]
};
// Add table to document
await doc.table(tableArray);
// Header hook for multi-page tables
const headerHook = (pageNumber: number, doc: PDFDocument): void => {
if (pageNumber > 1) {
doc.table(tableArray, { startY: doc.y + 10 });
}};
(doc as PDFDocument).hook = headerHook;
With this setup, you can generate PDFs with well-formatted text and tables, ensuring that table headers repeat on each page if the table spans multiple pages.
Converting PDF Documents to Buffers:
The generated PDF document needs to be converted into buffer data and pushed into a readable stream. This stream will then send the data as a response to the HTTP request. The data is used as shown in the above complete code.
Import Necessary Modules:
Import the Writable stream class and the Buffer module from the stream and buffer packages, respectively.
import { Writable } from "stream";
import { Buffer } from "buffer";
Generate Buffer Function:
Create a function named generatePdfBuffer that takes a PDFDocument object as input and returns a Promise that resolves to a Buffer containing the PDF data.
private async generatePdfBuffer(doc: PDFDocument): Promise<Buffer> {
return new Promise((resolve) => {
const buffers = [];
doc.pipe(
new Writable({
write(chunk, encoding, callback) {
buffers.push(chunk);
callback();
},
final(callback) {
resolve(Buffer.concat(buffers));
callback();
},
})
);
});
}
How to change the link(Open file) to a button
Define a UI Fragment that contains a button with the handler in app/webapp/ext/fragment section.
<core:FragmentDefinition xmlns:core="sap.ui.core" xmlns="sap.m">
<Button
core:require="{ handler:'sap/erp4sme/c4b/payment/accountstatements/ext/fragment/DownloadPDF'}"
text="PDF"
press="handler.onPress" />
</core:FragmentDefinition>
Create a handler for the button that, when pressed, displays a toast message saying "A PDF is being downloaded" and then redirects the browser to the PDF URL.
sap.ui.define([
"sap/m/MessageToast"
], function (MessageToast) {
'use strict';
return {
onPress: function (oEvent) {
const view = this.editFlow.getView();
const i18nModelBundle = view.getModel("i18n").getResourceBundle();
MessageToast.show(i18nModelBundle.getText("DownloadingPdf"));
const object = oEvent.getSource().getBindingContext().getObject();
sap.m.URLHelper.redirect(`/dch/ui/payment/accountstatements/AccountStatements(${object.ID})/pdf`, true);
}};
});
Add the fragment in the columns section in manifest.json
"controlConfiguration": {
"@com.sap.vocabularies.UI.v1.LineItem": {
"columns": {
"DownloadPDF": {
"header": "{i18n>PDF}",
"position": {
"anchor": "DownloadPDF",
"placement": "After"
},
"template": "sap.erp4sme.c4b.payment.accountstatements.ext.fragment.DownloadPDF",
"width": "5em"
}
}}}
Limitations:
References:
crypto-for-business - srv/payment/accountStatements/ui-service-account-statements.ts
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
25 | |
24 | |
17 | |
14 | |
10 | |
9 | |
9 | |
7 | |
7 | |
7 |