Quip終了に伴うデータエクスポートでお困りの方へ

Lightning Web コンポーネント(LWC)を使って帳票出力してみた

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 明朝を使ってみました。

jsPDFhttps://cdnjs.com/libraries/jspdf
jsPDF-AutoTablehttps://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>

クイックアクション

クイックアクションを作成し、レコード詳細ページに配置します。
設定手順は以下の通りです。

クイックアクションの作成

  1. 設定 > オブジェクトマネージャーから請求オブジェクトを開きます。
  2. 「ボタン、リンク、およびアクション」を選び、「新規アクション」をクリックします。
  3. アクション種別で「Lightning Web コンポーネント」を選択します。
  4. Lightning Web コンポーネントで作成した LWC を選択します。
  5. 表示ラベルと名前(例:請求PDF / InvoicePdf)を入力します。
  6. 「保存」をクリックします。

ページレイアウトへの配置

  1. 請求オブジェクトの「ページレイアウト」を開きます。
  2. 上部のメニューから「モバイルおよび Lightning のアクション」を選びます。
  3. 作成したクイックアクションを、「Salesforce モバイルおよび Lightning Experience のアクション」エリアにドラッグ&ドロップで配置します。
  4. 「保存」をクリックします。

PDF への出力

請求データを作成して、請求レコード詳細画面にクイックアクションが表示されるか確認してみましょう。

請求レコード詳細画面にクイックアクションが表示されるか確認
クイックアクションの表示ラベルに「請求PDF」を指定した場合の表示例

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

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

まとめ

本記事では、PDF 出力の代表的な 3 つの方法と特徴、JavaScript ライブラリの「jsPDF-AutoTable」を使って PDF 出力する方法をご紹介いたしました。
PDF 出力方法の検討に役立てていただければ幸いです。

Salesforce活用について、ご不明な点やご相談がありましたらお気軽にサンブリッジへお問い合わせください。

Salesforce活用支援

Salesforce活用支援のお問い合わせ

サンブリッジでは、Salesforceの定着から使い方のレクチャーを含めた伴走支援など、導入後につまづきがちなポイントを押さえてご支援します。

サービス・ソリューションに関するお問い合わせ

Salesforce・AWSなど、ご支援に関してご不明な点やご相談はこちらからお問い合わせください。

貴社のビジネス課題を発見し、解決にむけてコンサルタントがご提案いたします。

この記事を書いた人

開発、保守を担当。
Salesforce の Apex や Visualforce、LWC など実務で得たノウハウを発信していきます。 

<保有資格>
Salesforce 認定 Platform デベロッパー

目次