kintoneで「見積書をPDFで出したい」という要望は、帳票まわりでおそらく最も多いものです。多くの場合は帳票プラグインを導入するのが正解ですが、要件がシンプルで、ランニングコストをかけたくない、あるいはレイアウトを完全に自前で制御したいというケースでは、JavaScriptカスタマイズで作ってしまうという選択肢があります。
今回は、オープンソースのPDF生成ライブラリ pdfmake を使って、レコード詳細画面に「見積書PDF出力」ボタンを追加し、日本語の見積書PDFをブラウザ上だけで生成するところまで実際に作ってみます。サーバーは一切立てません。
■なぜkintoneは「見積書をPDFで」がつまずくのか
まず前提の整理です。kintoneには自社フォーマットの帳票を出力する機能が標準では存在しません。
「レコード印刷はあるじゃないか」と思われるかもしれませんが、あれは画面レイアウトをそのまま紙に落とすだけの機能です。ロゴも押印欄も入らず、明細行の体裁も整わず、複数レコードをまとめて出すこともできません。社内の控えとしては使えても、取引先に送れる見た目にはならないというのが実際のところです。
そのため実務では、次の3つのいずれかを選ぶことになります。
- 標準のレコード印刷 ― 無料。社内控えなど、体裁を問わない用途に限られる
- 帳票プラグイン・連携サービス ― 月額7,000円程度から。社外送付する見積書・請求書、既存のExcel様式を流用したい場合、運用を情シス以外に任せたい場合
- JavaScriptカスタマイズで自作 ― 開発工数のみ。帳票が1〜2種類で固定され、レイアウトを細かく制御したい場合
正直に書いておくと、業務の中心が帳票発行であるなら、素直に帳票サービスを使うべきです。保守を自社で抱え込まずに済み、インボイス制度や電子帳簿保存法への追随もベンダー側が対応してくれます。
一方で「見積書1種類だけ」「社内で完結する」「レイアウトはこちらで決めたい」という条件なら、自作は十分に現実的です。今回はその前提で進めます。
■完成イメージ ― ボタンひとつで、日本語の見積書PDF
先にゴールをお見せします。レコード詳細画面のヘッダーに追加されたボタンを押すと、その場でPDFが生成され、モーダルでプレビューが開きます。
◆ 出力(生成された見積書PDF):

動作の流れは4ステップです。
- レコード詳細画面のヘッダーに「見積書PDF出力」ボタンが表示される
- ボタンを押すと、その場でPDFが生成される
- モーダルウィンドウでPDFプレビューが開く
- 「PDFを保存」でファイルとしてダウンロードできる
■構成 ― pdfmake本体+日本語フォントVFS+カスタマイズ本体
仕組みは3つのファイルでできています。アプリにアップロードするのはこれだけです。
- pdfmake.min.js ― PDF生成ライブラリ本体(今回は 0.2.23)
- vfs_fonts.js ― Noto Sans JP を組み込んだ仮想ファイルシステム(自作)
- report.js ― 見積書PDFを出力するカスタマイズ本体
pdfmake(npm)は、JavaScriptのオブジェクトで帳票レイアウトを宣言的に記述してPDFを生成するライブラリです。ライセンスはMITで商用利用も可能です。
kintoneカスタマイズと相性が良い理由は3点あります。
- ブラウザだけで完結する ― PDF生成用のサーバーを別途立てる必要がなく、レコードのデータからその場で生成できる
- 表組みが得意 ― 見積書の明細行のような可変長テーブルを、配列の入れ子として素直に書ける
- レイアウトがコードとして残る ― テンプレートファイルの管理が不要で、変更履歴をGitで追える
■サンプルアプリのフィールド構成
検証に使ったアプリのフィールドは次のとおりです。ラベルではなくフィールドコードがコードと対応する点に注意してください。
- 顧客名(customer_name/文字列1行・必須)
- 見積番号(estimate_number/文字列1行・必須・重複禁止)
- 発行日(issue_date/日付・必須・レコード作成時の日付を自動入力)
- 件名(subject/文字列1行)
- 有効期限(valid_until/日付)
- 発行元会社名・住所・電話番号(issuer_name/issuer_address/issuer_phone・初期値あり)
- 明細(items/テーブル)― 品目、数量、単価、金額(数量×単価の計算フィールド)
- 小計(subtotal/計算・明細の金額を合計)
- 消費税率(tax_rate/数値・初期値10)
- 消費税額(tax_amount/計算・小計×税率を四捨五入)
- 合計金額(total/計算・小計+消費税額)
- 備考(notes/文字列複数行)
ここでの設計上のポイントは、金額計算をすべてkintoneの計算フィールド側に寄せていることです。小計・消費税額・合計をJavaScript側で再計算すると、画面の表示金額とPDFの金額がズレるリスクが生まれます。計算はkintoneに任せ、JavaScriptは「読んで並べるだけ」にしておくのが安全です。

■日本語フォントをVFSに組み込む
pdfmakeの配布物に同梱されている標準フォントは Roboto(欧文のみ) です。そのまま日本語を出力すると、文字化けするか、何も表示されない空白のPDFになります。
日本語を出すには、日本語フォントをBase64に変換し、pdfmakeの仮想ファイルシステム(VFS)に組み込む必要があります。手順は3ステップです。
1. フォントを入手する
日本語サブセット版のOTFを Noto CJK リポジトリ(Sans/SubsetOTF/JP) から取得します。使うのは Regular と Bold の2ファイルです。ライセンスは SIL Open Font License 1.1 で、商用利用は可能ですがライセンス表記が必要になります。
2. VFSを生成する
npmでpdfmakeを取得し、フォントを配置したディレクトリを引数に、同梱のVFSビルドスクリプト(build-vfs.js)を実行します。生成された vfs_fonts.js が、Base64化したフォントをpdfmakeへ登録するファイルになります。詳しい仕様は pdfmake 公式ドキュメント「Custom fonts via Virtual file system」 に記載があります。
3. フォントファミリーを登録する
カスタマイズ側で、VFSに含まれるファイル名を指定してフォントを登録します。normal に Regular、bold に Bold を割り当て、italics と bolditalics にも同じファイルを指定しておけば、斜体指定が混ざっても落ちません。
そして帳票定義の defaultStyle で font を「NotoSansJP」に指定します。この一行を忘れると日本語は出ません。筆者はここで一度つまずきました。フォントをVFSに入れただけでは、pdfmakeは既定のRobotoを使い続けます。
なお日本語フォントはサブセットでも、Regular/Boldの2書体で数MB規模になります。kintoneヘルプ「制限値一覧」のとおり、カスタマイズ用JavaScriptは1ファイル20MBまで、1アプリ30ファイルまでなので容量上の問題はありません。ただし初回のページ読み込みは体感で重くなります。太字が不要ならRegularのみに絞ると軽量化できます。
■コードの全体像 ― 4つのブロックでできている
ここからカスタマイズ本体(report.js)の全文を掲載します。全体で約150行ですが、長さに身構える必要はありません。やっていることは「ボタンを置く」「レコードを読む」「レイアウトを組む」「表示する」の4つだけで、残りは数値や日付を整形する小さな関数です。
- ① イベント登録とボタン生成 ― レコード詳細画面にボタンを追加し、クリック時の処理を組み立てる
- ② 検証と値の取り出し ― 必須フィールドの存在を確認し、レコードから表示用の値を集める
- ③ 帳票定義の組み立て ― pdfmakeに渡すレイアウト定義を作る。ここが実質的な「テンプレート」
- ④ プレビュー表示とユーティリティ ― 生成したPDFをモーダルで表示し、数値・日付・ファイル名を整形する
帳票の見た目を変えたいときに触るのは③だけです。①②④は基本的にそのまま使い回せます。
■① イベント登録とボタン生成
まずは入り口です。全体を即時実行関数で包み、グローバルスコープを汚さないようにしています。
(() => {
'use strict';
const BUTTON_ID = 'pdfmake-estimate-output-button';
const FONT_NAME = 'NotoSansJP';
const REQUIRED_FIELDS = [
'customer_name', 'estimate_number', 'issue_date', 'issuer_name',
'items', 'subtotal', 'tax_rate', 'tax_amount', 'total'
];
kintone.events.on('app.record.detail.show', (event) => {
if (document.getElementById(BUTTON_ID)) return event;
const headerSpace = kintone.app.record.getHeaderMenuSpaceElement();
if (!headerSpace) return event;
const button = document.createElement('button');
button.id = BUTTON_ID;
button.type = 'button';
button.textContent = '見積書PDF出力';
button.className = 'kintoneplugin-button-normal';
button.style.marginLeft = '8px';
button.addEventListener('click', () => {
button.disabled = true;
try {
if (!window.pdfMake || typeof window.pdfMake.createPdf !== 'function') {
throw new Error('pdfmakeが読み込まれていません。JavaScriptの読み込み順を確認してください。');
}
window.pdfMake.addFonts({
[FONT_NAME]: {
normal: 'NotoSansJP-Regular.otf',
bold: 'NotoSansJP-Bold.otf',
italics: 'NotoSansJP-Regular.otf',
bolditalics: 'NotoSansJP-Bold.otf'
}
});
validateFields(event.record);
const values = resolveValues(event.record);
const fileName = `見積書_${safeName(values.customer)}_${safeName(values.number)}.pdf`;
window.pdfMake.createPdf(buildDocument(event.record, values))
.getDataUrl((dataUrl) => showPreview(dataUrl, fileName));
} catch (error) {
console.error('[pdfmake estimate sample]', error);
alert(`PDFの生成に失敗しました。\n${error.message}`);
} finally {
button.disabled = false;
}
});
headerSpace.appendChild(button);
return event;
});ここで押さえてほしい点が4つあります。
1. 冒頭のID重複チェックは省略できません。kintoneは画面遷移の仕方によって同じイベントが複数回発火することがあり、このチェックがないとボタンが2つ3つと増えていきます。実際、最初の実装ではこれで増殖しました。
2. ボタンにはkintone標準のクラスを当てます。「kintoneplugin-button-normal」を指定すると、自作ボタンでも標準機能と同じ見た目になり、後から見た人が違和感を持ちません。
3. pdfmakeの存在確認を最初に行います。読み込み順を間違えた場合、ここで原因が特定できるエラーメッセージが出ます。「undefined のプロパティを読めません」という汎用エラーで悩む時間を減らせます。
4. try / catch / finally で必ずボタンを戻します。処理中はボタンを無効化して二重クリックを防ぎ、成功・失敗にかかわらず finally で元に戻します。エラーはコンソールに詳細を出し、利用者にはメッセージだけを見せる形です。
■② 検証と値の取り出し
次は、レコードから必要な値を集めるブロックです。
function validateFields(record) {
const missing = REQUIRED_FIELDS.filter((code) => !record[code]);
if (missing.length) throw new Error(`必要なフィールドがありません:${missing.join(', ')}`);
}
function resolveValues(record) {
return {
customer: valueOf(record.customer_name, '顧客名未入力'),
number: valueOf(record.estimate_number, '番号なし'),
total: numberOf(record.total, 0),
subject: valueOf(record.subject, ''),
issueDate: formatDate(valueOf(record.issue_date, '')),
validUntil: formatDate(valueOf(record.valid_until, '')),
issuerName: valueOf(record.issuer_name, ''),
issuerAddress: valueOf(record.issuer_address, ''),
issuerPhone: valueOf(record.issuer_phone, ''),
notes: valueOf(record.notes, '')
};
}
function getItems(record) {
const rows = record.items && Array.isArray(record.items.value) ? record.items.value : [];
return rows.map((row) => {
const item = row.value || {};
const quantity = numberOf(item.quantity, 0);
const unitPrice = numberOf(item.unit_price, 0);
return {
name: valueOf(item.item_name, ''),
quantity,
unitPrice,
amount: numberOf(item.amount, quantity * unitPrice)
};
});
}validateFields は、あえて処理を止めるための関数です。フィールドコードが1つでも足りなければ、その場で例外を投げてPDFを生成しません。
ここは設計判断が分かれるところです。初期の実装では「フィールドが見つからなければ、最初の文字列フィールドで代用する」というフォールバックを入れていました。動作としては親切ですが、間違った項目が印字されたPDFが取引先に届くほうが、何も出ないより明らかに危険です。止まれば人が気づきますが、それらしい見た目で間違っている帳票には誰も気づきません。最終的にフォールバックは廃止し、不足しているフィールドコードを名指しでエラーに出す方式にしました。
getItems では、サブテーブルの二段構造に注意してください。明細テーブルは値が配列になっており、さらに各行の実データがもう一段下の階層に入っています。ここを見落とすと明細が空のまま出力されます。
■③ 帳票定義の組み立て ― ここが実質的なテンプレート
pdfmakeに渡すレイアウト定義です。見た目を変えたいときに触るのはこの関数だけなので、少し丁寧に読んでください。
function buildDocument(record, values) {
const items = getItems(record);
const subtotal = numberOf(record.subtotal, 0);
const taxRate = numberOf(record.tax_rate, 10);
const taxAmount = numberOf(record.tax_amount, 0);
const total = values.total;
const body = [[
header('品目'), header('数量', 'center'), header('単価', 'right'), header('金額', 'right')
]];
if (items.length) {
items.forEach((item) => body.push([
cell(item.name || '(未入力)'),
cell(formatNumber(item.quantity), 'center'),
cell(`${formatCurrency(item.unitPrice)}円`, 'right'),
cell(`${formatCurrency(item.amount)}円`, 'right')
]));
} else {
body.push([
{text: '明細は登録されていません', colSpan: 4, color: '#666666', margin: [4, 8, 4, 8]},
{}, {}, {}
]);
}
return {
pageSize: 'A4',
pageMargins: [42, 48, 42, 52],
info: {title: `見積書 ${values.number}`, subject: values.subject, author: values.issuerName},
content: [
// タイトル
{text: '御 見 積 書', style: 'title'},
// 左に宛名、右に発行情報と発行元
{
columns: [
{
width: '*',
stack: [
{text: `${values.customer} 御中`, style: 'customer'},
values.subject ? {text: `件名:${values.subject}`, margin: [0, 12, 0, 0]} : {}
]
},
{
width: 210,
stack: [
{text: `見積番号:${values.number}`},
{text: `発行日:${values.issueDate || '-'}`, margin: [0, 3, 0, 0]},
values.validUntil ? {text: `有効期限:${values.validUntil}`, margin: [0, 3, 0, 0]} : {},
{text: values.issuerName, bold: true, margin: [0, 14, 0, 0]},
values.issuerAddress ? {text: values.issuerAddress, margin: [0, 2, 0, 0]} : {},
values.issuerPhone ? {text: `TEL:${values.issuerPhone}`, margin: [0, 2, 0, 0]} : {}
]
}
],
columnGap: 24,
margin: [0, 0, 0, 28]
},
// 合計金額の帯
{
table: {widths: ['*'], body: [[{
text: `合計金額 ${formatCurrency(total)}円(税込)`,
alignment: 'center', bold: true, fontSize: 17,
color: '#17365d', fillColor: '#eaf2f8', margin: [8, 10, 8, 10]
}]]},
layout: {hLineColor: () => '#6c8ebf', vLineColor: () => '#6c8ebf'},
margin: [0, 0, 0, 22]
},
// 明細テーブル
{
table: {headerRows: 1, widths: ['*', 55, 80, 85], body, dontBreakRows: true},
layout: {
fillColor: (rowIndex) => rowIndex === 0 ? '#d9e7f5' : null,
hLineColor: () => '#a9b7c6',
vLineColor: () => '#a9b7c6'
}
},
// 小計・消費税・合計(右寄せ)
{
columns: [
{width: '*', text: ''},
{
width: 235,
table: {widths: ['*', 100], body: [
summary('小計', subtotal),
summary(`消費税(${formatNumber(taxRate)}%)`, taxAmount),
summary('合計', total, true)
]},
layout: {hLineColor: () => '#a9b7c6', vLineColor: () => '#a9b7c6'}
}
],
margin: [0, 14, 0, 0]
},
// 備考(入力があるときだけ出す)
values.notes ? {
stack: [
{text: '備考', bold: true, margin: [0, 24, 0, 5]},
{
table: {widths: ['*'], body: [[{text: values.notes, margin: [6, 6, 6, 6]}]]},
layout: {hLineColor: () => '#a9b7c6', vLineColor: () => '#a9b7c6'}
}
]
} : {}
],
footer: (page, pages) => ({
text: `${page} / ${pages}`, alignment: 'center',
color: '#777777', fontSize: 8, margin: [0, 16, 0, 0]
}),
defaultStyle: {font: FONT_NAME, fontSize: 9.5, color: '#222222', lineHeight: 1.25},
styles: {
title: {fontSize: 24, bold: true, alignment: 'center', characterSpacing: 4, margin: [0, 0, 0, 32]},
customer: {fontSize: 14, bold: true, decoration: 'underline'}
}
};
}読み解くポイントを挙げます。
金額はkintoneの計算フィールドから読むだけ。小計・消費税額・合計はJavaScriptで再計算していません。第2引数のフォールバックはあくまで保険で、通常はkintoneが確定させた値がそのまま印字されます。画面とPDFで数字が食い違う事故は、この設計にしておけば構造的に起きません。
defaultStyle の font 指定が日本語表示の生命線です。ここで「NotoSansJP」を指定しないと、VFSにフォントを組み込んでいてもpdfmakeは既定のRobotoを使い続け、日本語がまるごと空白になります。
headerRows を1にすると、ページをまたいでもヘッダー行が繰り返されます。明細が20行を超えるような見積書では効いてきます。あわせて dontBreakRows を有効にしておくと、1行が途中で分断されるのを防げます。
空の値は空オブジェクトで飛ばします。件名・有効期限・備考などは、未入力なら要素そのものを出力しない書き方にしています。pdfmakeは空オブジェクトを無視するため、条件分岐を素直に三項演算子で書けます。
レイアウト調整の勘所。A4を横にしたければ pageSize の隣に pageOrientation を追加、余白を変えたければ pageMargins(左・上・右・下の順)、罫線の色は layout の hLineColor と vLineColor、明細の列幅は widths の配列を触ります。アスタリスクは「残りの幅を埋める」指定なので、品目欄だけ可変にしてあります。
■④ プレビュー表示とユーティリティ
生成したPDFを画面に出す部分と、細かい整形関数です。
function showPreview(dataUrl, fileName) {
const existing = document.getElementById('pdfmake-preview-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.id = 'pdfmake-preview-overlay';
Object.assign(overlay.style, {
position: 'fixed', inset: '20px', zIndex: '100000', background: '#fff',
border: '1px solid #999', boxShadow: '0 4px 18px rgba(0,0,0,.35)',
display: 'flex', flexDirection: 'column'
});
const toolbar = document.createElement('div');
Object.assign(toolbar.style, {
display: 'flex', alignItems: 'center', gap: '12px',
padding: '10px 14px', background: '#f5f5f5', borderBottom: '1px solid #ccc'
});
const title = document.createElement('strong');
title.textContent = '見積書PDFプレビュー';
const save = document.createElement('a');
save.href = dataUrl;
save.download = fileName;
save.textContent = 'PDFを保存';
save.className = 'kintoneplugin-button-normal';
const close = document.createElement('button');
close.type = 'button';
close.textContent = '閉じる';
close.className = 'kintoneplugin-button-dialog-cancel';
close.style.marginLeft = 'auto';
close.addEventListener('click', () => overlay.remove());
const frame = document.createElement('iframe');
frame.title = '見積書PDFプレビュー';
frame.src = dataUrl;
Object.assign(frame.style, {width: '100%', flex: '1', border: '0'});
toolbar.appendChild(title);
toolbar.appendChild(save);
toolbar.appendChild(close);
overlay.appendChild(toolbar);
overlay.appendChild(frame);
document.body.appendChild(overlay);
}
function valueOf(field, fallback) {
return !field || field.value === null || field.value === undefined || field.value === ''
? fallback : String(field.value);
}
function numberOf(field, fallback) {
if (!field || field.value === null || field.value === undefined || field.value === '') return fallback;
const number = Number(String(field.value).replace(/,/g, ''));
return Number.isFinite(number) ? number : fallback;
}
function header(text, alignment) {
return {text, bold: true, alignment: alignment || 'left', margin: [4, 5, 4, 5]};
}
function cell(text, alignment) {
return {text: String(text), alignment: alignment || 'left', margin: [4, 5, 4, 5]};
}
function summary(label, amount, bold) {
return [
{text: label, bold: Boolean(bold), margin: [5, 4, 5, 4]},
{text: `${formatCurrency(amount)}円`, bold: Boolean(bold), alignment: 'right', margin: [5, 4, 5, 4]}
];
}
function formatNumber(value) {
return Number(value || 0).toLocaleString('ja-JP', {maximumFractionDigits: 4});
}
function formatCurrency(value) {
return Math.round(Number(value || 0)).toLocaleString('ja-JP');
}
function formatDate(value) {
const match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2})/);
return match ? `${match[1]}年${Number(match[2])}月${Number(match[3])}日` : String(value || '');
}
function safeName(value) {
return String(value || '未入力').replace(/[\\/:*?"<>|]/g, '_').slice(0, 50);
}
})();ここも4点だけ補足します。
プレビューは新規タブではなくモーダルにしています。pdfmakeには新規タブで開くメソッドもありますが、ブラウザのポップアップブロックに引っかかることがあるため、Data URLをiframeに流し込む方式のほうが安定します。
numberOf ではカンマを除去しています。kintoneの数値フィールドは桁区切り設定によって「175,000」のような文字列で返ってくることがあり、そのままNumberに渡すとNaNになります。
formatDate は正規表現で分解するだけです。kintoneの日付フィールドは年月日をハイフンで区切った文字列で返ってくるので、「年」「月」「日」に組み替えれば日本の帳票の体裁になります。
safeName は地味ですが必須です。顧客名をファイル名に使う場合、Windowsで使用できない記号を除去しないとダウンロードが失敗します。取引先名にスラッシュが入るケースは実際にあります。
■導入手順 ― 3ファイルをkintoneに登録する
実際にアプリに組み込むます。
1. 対象アプリを開き、右上の歯車アイコンから「アプリの設定」へ進みます。
2. 「設定」タブの「JavaScript / CSSでカスタマイズ」を開きます。
3. 「PC用のJavaScriptファイル」で「ファイルを追加」を選び、次の順番でアップロードします。
- 1番目:pdfmake.min.js(ライブラリ本体)
- 2番目:vfs_fonts.js(日本語フォントVFS)
- 3番目:report.js(上で掲載したカスタマイズ本体)

4. 画面下部の「保存」を押します。
5. 続けて「アプリを更新」を実行します。この操作を忘れると設定が本番に反映されません。「保存」だけで満足して「動かない」と悩むのは、kintoneカスタマイズで最も多いつまずきです。
この順番は必須です。理由は依存関係にあります。vfs_fonts.js は読み込み時点でpdfmake本体がグローバルに存在していることを前提にVFSを登録し、report.js はpdfmakeとVFSの両方が揃っていることを前提に動きます。順番が違うと、「pdfmakeが読み込まれていません」というエラーになるか、日本語が空白のPDFが出力されます。カスタマイズ画面ではドラッグで並べ替えられるので、保存前に必ず確認してください。
アップロード方法にはREST APIやコマンドラインツールを使う選択肢もあります。運用に載せる段階では cybozu developer network「kintoneカスタマイズファイルのアップロード方法まとめ」 が参考になります。
なお今回のコードは、掲載したフィールドコードをそのまま使用します。既存アプリに適用する場合は、アプリ側のフィールドコードを合わせるか、REQUIRED_FIELDS と各参照名をアプリの構成に書き換えてください。
■動作確認
アプリを更新したら、レコード詳細画面を開いて確認して「見積書PDF出力」をクリックすると帳票が表示されます。

■まとめ ― 150行で、月額費用のかからない帳票ができる
今回のコードで確認できたことを整理します。
- 実装は「ボタンを置く」「レコードを読む」「レイアウトを組む」「表示する」の4ブロックで、見た目を変えるときに触るのは1つだけ
- 必須フィールドの検証は入れる。フォールバックで代用するより、止めて名指しでエラーを出すほうが安全
- 金額はkintoneの計算フィールドから読むだけにし、JavaScriptでは再計算しない
- 登録は本体 → VFS → カスタマイズ本体の順、そして「保存」のあとに「アプリを更新」
- ロゴ・押印・配色は現実的な範囲。自動保存や一括出力が要件に入った時点で、帳票サービスとの比較に戻る
約150行のJavaScriptで、月額費用のかからない見積書PDFができました。目新しい技術はひとつも使っていません。kintoneがデータと計算を持ち、pdfmakeがそれを紙面に落とすという、それだけの構成です。
ただし前回も書いたとおり、安く作れることと、長く運用できることは別の話です。このコードは自社で保守することになります。帳票の種類が増える、様式変更が頻繁に発生する、法令要件が絡む ― そのいずれかに該当し始めたら、いったん立ち止まって「買う」選択肢に戻る。その判断ができることが、自作を選ぶうえでの前提だと考えています。
■参考情報(一次情報)
- pdfmake 公式ドキュメント
- Document definition object(pdfmake 公式ドキュメント)
- Tables(pdfmake 公式ドキュメント)
- Custom fonts (client-side) via Virtual file system(pdfmake 公式ドキュメント)
- pdfmake 0.2.23(npm)
- JavaScript API(cybozu developer network)
- kintoneカスタマイズファイルのアップロード方法まとめ(cybozu developer network)
- 制限値一覧(kintone ヘルプ)
※本記事は2026年8月時点の情報をもとに作成しています。検証環境は pdfmake 0.2.23 と Noto Sans JP(SubsetOTF Regular・Bold)の組み合わせで、JavaScript構文チェック、日本語PDFの生成テスト、kintone上でのプレビュー表示まで確認済みです。掲載したコードは検証環境で動作したものですが、kintoneのバージョン・ブラウザ・ライブラリのバージョンによって動作が異なる場合があります。本番アプリへ適用する前に、必ず検証環境で動作を確認してください。記事中の顧客名・見積番号・金額はすべて架空のサンプルです。コードの利用は自己責任でお願いします。ライブラリおよびフォントのライセンス条件は変更される可能性があるため、商用利用の際は最新の一次情報をご確認ください。