Salesforce で「帳票を PDF で出力したい」という要件はありませんか?
本記事では、JavaScript ライブラリの「jsPDF-AutoTable」を使った方法をご紹介いたします。
PDF 出力の方法
PDF 出力には代表的な 3 つの方法があります。
特徴をふまえて選ぶと良いでしょう。
1. Visualforce
Visualforce ページに <apex:page renderAs=”pdf”> タグを指定する。
特徴
- PDF はサーバー側で生成する
- Visualforce タグ、HTML、CSSでデザインする
- 利用可能なフォントが決まっている(日本語は「Arial Unicode MS」のみ)
2. 外部 PDF 生成サービス
AppExchange を利用、または外部PDF生成サービスを開発する。
特徴
- ほぼ制約がない
- AppExchange であれば設定のみで導入できる事が多い
- 運用コストがかかる
3. JavaScript ライブラリ
jsPDF などの JavaScript ライブラリを利用する。
特徴
- PDF はクライアント側で生成する
- JavaScript でデザインする
- フォントは自由に設定できるがクライアントにダウンロードさせる必要がある
- 難易度が高い(公式ドキュメントに記載なし、ブラウザ依存など)
JavaScript ライブラリ(jsPDF-AutoTable)で PDF 出力
JavaScript ライブラリの「jsPDF-AutoTable」を使って、クイックアクションで PDF 出力してみましょう。
静的リソース
JavaScript ライブラリと日本語フォントをダウンロードします。
日本語フォントはモリサワの BIZ UD 明朝を使ってみました。
| jsPDF | https://cdnjs.com/libraries/jspdf |
| jsPDF-AutoTable | https://cdnjs.com/libraries/jspdf-autotable |
| BIZ UD 明朝 | https://fonts.google.com/specimen/BIZ+UDMincho |
以下のファイルを ZIP ファイルにします(フォルダは作りません)。
フォントはライセンスに注意する必要があります。
念の為、同梱されている OFL.txt も含めておきます。
- jspdf.umd.min.js
- jspdf.plugin.autotable.min.js
- BIZUDMincho-Regular.ttf
- OFL.txt
設定 > 静的リソースから「新規」をクリックします。
名前を「jsPDF」にして、作成した ZIP ファイルをアップロードします。
静的リソースの容量に注意する必要があります。
1つの静的リソース: 最大 5 MB
組織全体の合計: 最大 250 MB
容量が足りない場合は、フォントファイルを分割、サブセット化する、
または、コンテンツバージョンにアップロードするという方法もあります。
オブジェクト
請求カスタムオブジェクトを作成します。
| 表示ラベル | API 参照名 |
|---|---|
| 請求 | Invoice__c |
請求オブジェクトのカスタム項目を作成します。
| 表示ラベル | API 参照名 | データ型 |
|---|---|---|
| 取引先 | Account__c | 参照関係 (取引先) |
| 請求日 | InvoiceDate__c | 日付 |
| 合計金額 | TotalAmount__c | 積み上げ集計 (合計 請求明細.金額) |
請求明細カスタムオブジェクトを作成します。
| 表示ラベル | API 参照名 |
|---|---|
| 請求明細 | InvoiceLine__c |
請求明細オブジェクトのカスタム項目を作成します。
請求の子リレーション名は InvoiceLines とします。
| 表示ラベル | API 参照名 | データ型 |
|---|---|---|
| 請求 | Invoice__c | 主従関係 (請求) |
| 品名 | ProductName__c | テキスト (255) |
| 単価 | UnitPrice__c | 通貨 (18、0) |
| 数量 | Quantity__c | 数値 (18、0) |
| 金額 | Amount__c | 数式 (UnitPrice__c * Quantity__c) |
Apex クラス
請求データを取得する Apex クラスを作成します。
/**
* 請求PDFコントローラー
*/
public with sharing class InvoicePdfController {
@AuraEnabled(cacheable=true)
public static Invoice__c getInvoiceData(Id invoiceId) {
List<Invoice__c> invoices = [
SELECT Id, Name, Account__r.Name, InvoiceDate__c, TotalAmount__c,
(
SELECT Id, Name, ProductName__c, UnitPrice__c, Quantity__c, Amount__c
FROM InvoiceLines__r
ORDER BY Name ASC
)
FROM Invoice__c
WHERE Id = :invoiceId
WITH USER_MODE
LIMIT 1
];
return invoices.isEmpty() ? null : invoices.get(0);
}
}Lightning Web コンポーネント
PDF を生成する Lightning Web コンポーネントを作成します。
ヘッドレス(画面無し)アクションにするので js、js-meta.xml の 2 つのファイルを作成します。
/**
* 請求PDF
*/
import { LightningElement, api } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { loadScript } from 'lightning/platformResourceLoader';
import JSPDF_RESOURCE from '@salesforce/resourceUrl/jsPDF';
import getInvoiceData from '@salesforce/apex/InvoicePdfController.getInvoiceData';
const FONT_FILE_NAME = 'BIZUDMincho-Regular.ttf';
const FONT_NAME = 'BIZUDMincho';
export default class InvoicePdfGenerator extends LightningElement {
@api recordId;
isGenerating = false;
@api async invoke() {
if (this.isGenerating) return;
this.isGenerating = true;
this.dispatchEvent(new ShowToastEvent({ title: 'PDF生成中...' }));
try {
// jsPDF読み込み
await loadScript(this, `${JSPDF_RESOURCE}/jspdf.umd.min.js`);
await loadScript(this, `${JSPDF_RESOURCE}/jspdf.plugin.autotable.min.js`);
// フォント読み込み
const response = await fetch(`${JSPDF_RESOURCE}/${FONT_FILE_NAME}`);
if (!response.ok) {
throw new Error(`フォント読み込みエラー (${response.status})`);
}
const font = await response.bytes();
const base64font = font.toBase64();
// 請求データ取得
const invoice = await getInvoiceData({ invoiceId: this.recordId });
const invoiceLines = invoice.InvoiceLines__r || [];
const accountName = invoice.Account__r?.Name || '';
// jsPDF初期化
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
// フォント登録
pdf.addFileToVFS(FONT_FILE_NAME, base64font);
pdf.addFont(FONT_FILE_NAME, FONT_NAME, 'normal');
pdf.setFont(FONT_NAME);
const pageWidth = pdf.internal.pageSize.width; // ページ横幅
const horizontalMargin = 15; // 水平マージン
let startY = 20; // 描画開始Y座標
// ヘッダー
pdf.setFontSize(18);
pdf.text('請求書', pageWidth/2, startY, { align: 'center' });
pdf.setFontSize(10);
startY += 15;
pdf.text(`${accountName} 御中`, horizontalMargin, startY, { align: 'left' });
pdf.text(`${this.formatDate(invoice.InvoiceDate__c)}`, pageWidth-horizontalMargin, startY, { align: 'right' });
// 明細テーブル
startY += 20;
pdf.autoTable({
startY: startY,
tableWidth: 180,
margin: { horizontal: 'center' },
head: [['内容', '単価', '数量', '金額']],
body: invoiceLines.map((line) => [
line.ProductName__c,
this.formatCurrency(line.UnitPrice__c),
this.formatNumber(line.Quantity__c),
this.formatCurrency(line.Amount__c),
]),
theme: 'grid',
styles: {
font: FONT_NAME,
fontStyle: 'normal',
fontSize: 10,
lineWidth: 0.2,
cellPadding: 3,
},
headStyles: {
font: FONT_NAME,
fontStyle: 'normal',
fillColor: [230, 230, 230],
textColor: [0, 0, 0],
halign: 'center',
},
columnStyles: {
0: { cellWidth: 80 },
1: { cellWidth: 40, halign: 'right' },
2: { cellWidth: 20, halign: 'right' },
3: { cellWidth: 40, halign: 'right' },
}
});
// 合計金額
startY = pdf.lastAutoTable.finalY + 10;
pdf.setFontSize(10);
pdf.text(`合計金額 ${this.formatCurrency(invoice.TotalAmount__c)}`, pageWidth-horizontalMargin, startY, { align: 'right' });
// ファイル名
const today = new Date();
const [yyyy, mm, dd] = [
today.getFullYear(),
String(today.getMonth() + 1).padStart(2, '0'),
String(today.getDate()).padStart(2, '0'),
];
const fileName = `${invoice.Name}-${yyyy}${mm}${dd}.pdf`;
// PDF保存
pdf.save(fileName);
} catch (error) {
const errorMessage = error?.body?.message || error?.message || '予期せぬエラーが発生しました。';
this.dispatchEvent(new ShowToastEvent({ title: 'エラーが発生しました', message: errorMessage, variant: 'error' }));
} finally {
this.isGenerating = false;
}
}
formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
const [yyyy, mm, dd] = [
d.getFullYear(),
String(d.getMonth() + 1).padStart(2, '0'),
String(d.getDate()).padStart(2, '0'),
];
return `${yyyy}年${mm}月${dd}日`;
}
formatNumber(value) {
if (!value) return '0';
return Number(value).toLocaleString('ja-JP');
}
formatCurrency(value) {
return `${this.formatNumber(value)} 円`;
}
}LWR サイトで fetch が失敗する場合は、フォントを JS 化して、loadScript で読み込むと良いでしょう。
メタデータファイルで、ヘッドレス(画面無し)アクションの設定をします。
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>67.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordAction</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordAction">
<actionType>Action</actionType>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>クイックアクション
クイックアクションを作成し、レコード詳細ページに配置します。
設定手順は以下の通りです。
クイックアクションの作成
- 設定 > オブジェクトマネージャーから請求オブジェクトを開きます。
- 「ボタン、リンク、およびアクション」を選び、「新規アクション」をクリックします。
- アクション種別で「Lightning Web コンポーネント」を選択します。
- Lightning Web コンポーネントで作成した LWC を選択します。
- 表示ラベルと名前(例:請求PDF / InvoicePdf)を入力します。
- 「保存」をクリックします。
ページレイアウトへの配置
- 請求オブジェクトの「ページレイアウト」を開きます。
- 上部のメニューから「モバイルおよび Lightning のアクション」を選びます。
- 作成したクイックアクションを、「Salesforce モバイルおよび Lightning Experience のアクション」エリアにドラッグ&ドロップで配置します。
- 「保存」をクリックします。
PDF への出力
請求データを作成して、請求レコード詳細画面にクイックアクションが表示されるか確認してみましょう。

請求明細データを作成してから、クイックアクションを実行してみましょう。
以下のようなPDFをダウンロードできます。

まとめ
本記事では、PDF 出力の代表的な 3 つの方法と特徴、JavaScript ライブラリの「jsPDF-AutoTable」を使って PDF 出力する方法をご紹介いたしました。
PDF 出力方法の検討に役立てていただければ幸いです。
Salesforce活用について、ご不明な点やご相談がありましたらお気軽にサンブリッジへお問い合わせください。



