【GAS】請求書・納品書を自動作成&メール送信!コピペOK
/ 初心者向け
GASで請求書・納品書を自動生成&メール送信!もう手作業は卒業
「請求書や納品書の作成、毎回時間がかかって大変…」
「メールで送るのも手間がかかる…」
そんな悩みを抱えていませんか?
この記事では、Google Apps Script(GAS)を使って、Googleスプレッドシートに蓄積されたデータから請求書や納品書を自動で生成し、顧客へメール送信する具体的な方法を、コピペできるコード付きで解説します。
この記事で解決できること
- 請求書・納品書作成の工数削減: 手作業でのデータ入力や転記作業をなくし、大幅な時間短縮を実現します。
- メール送信の自動化: 生成した書類を顧客ごとに自動でメール送信できます。
- ヒューマンエラーの削減: データ入力ミスや書類作成ミスを減らし、正確性を向上させます。
- GoogleスプレッドシートとGASの連携: 身近なツールで高度な業務自動化を体験できます。
準備するもの
1. Googleアカウント: GmailとGoogleドライブがあればOKです。
2. Googleスプレッドシート: 請求書・納品書の元データ(顧客情報、商品情報、数量、単価など)を入力するシート。
3. Googleドキュメント(テンプレート): 請求書・納品書のテンプレートとなるドキュメント。
実装手順
ステップ1:請求書・納品書テンプレートの準備
Googleドキュメントで、請求書または納品書のテンプレートを作成します。テンプレートには、後ほどGASで差し込むための「プレースホルダー」を設定します。
プレースホルダーの例:
{{会社名}}{{氏名}}{{請求日}}{{請求番号}}{{商品名}}{{数量}}{{単価}}{{金額}}{{合計金額}}
テンプレートができたら、ファイル名を分かりやすいもの(例:「請求書テンプレート」)にして、Googleドライブに保存してください。
ステップ2:元データ用スプレッドシートの準備
Googleスプレッドシートで、請求書・納品書の元データとなるシートを作成します。各列には、テンプレートのプレースホルダーに対応する項目を設定します。
スプレッドシートの例:
| 顧客ID | 会社名 | 氏名 | 請求日 | 請求番号 | 商品名 | 数量 | 単価 | 金額 | 状況 | メール送信フラグ |
|---|---|---|---|---|---|---|---|---|---|---|
| 001 | 株式会社〇〇 | 田中 一郎 | 2023/10/27 | INV-001 | 商品A | 2 | 1000 | 2000 | 未送信 | |
| 002 | △△商事 | 佐藤 花子 | 2023/10/27 | INV-002 | 商品B | 1 | 5000 | 5000 | 未送信 | |
状況列は、作成済みか未作成かなどのステータス管理に便利です。メール送信フラグ列は、メール送信済みかどうかを管理するために使用します。
ステップ3:GASコードの記述
「拡張機能」>「Apps Script」からスクリプトエディタを開き、以下のコードを記述します。
function generateAndSendInvoices() {
// ★★★ 設定項目 ★★★
const TEMPLATE_DOC_ID = 'YOUR_TEMPLATE_DOCUMENT_ID'; // 請求書テンプレートのドキュメントID
const SHEET_NAME = '請求データ'; // 元データが入力されているシート名
const EMAIL_SUBJECT_PREFIX = '【請求書】'; // メール件名のプレフィックス
const SENDER_NAME = 'あなたの会社名'; // 送信者名(Gmailの表示名)
const ATTACHMENT_FOLDER_NAME = '作成済み請求書'; // 作成した請求書を保存するフォルダ名
// ★★★ テンプレートのプレースホルダーとスプレッドシートの列の対応 ★★★
// ドキュメントのプレースホルダー名: スプレッドシートの列名
const PLACEHOLDER_MAP = {
'{{会社名}}': '会社名',
'{{氏名}}': '氏名',
'{{請求日}}': '請求日',
'{{請求番号}}': '請求番号',
// 商品明細はループで処理するため、ここでは例として合計金額のみ
'{{合計金額}}': '金額' // ここはスプレッドシートの集計結果の列名に合わせる
};
// 商品明細のテンプレート部分(例:1行分の商品明細)
// ※ ここはテンプレートの構造に合わせて調整が必要です。
// 例:<table><tbody><tr><td>{{商品名}}</td><td>{{数量}}</td><td>{{単価}}</td><td>{{金額}}</td></tr></tbody></table>
// GAS側では、この<tbody>...</tbody>の部分を動的に生成・置換します。
const ITEM_TEMPLATE_ROW = '<tr><td>{{商品名}}</td><td>{{数量}}</td><td>{{単価}}</td><td>{{金額}}</td></tr>';
// ★★★ スプレッドシートの列インデックス(0から始まる) ★★★
const COL_EMAIL_SENT_FLAG = 10; // メール送信フラグの列インデックス
const COL_STATUS = 9; // 状況の列インデックス
const COL_TOTAL_AMOUNT = 8; // 合計金額の列インデックス (Placeholder Mapの'{{合計金額}}'に対応)
// --------------------------------------------------------------------
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_NAME);
const dataRange = sheet.getDataRange();
const values = dataRange.getValues();
const headerRow = values[0]; // ヘッダー行
const dataRows = values.slice(1); // データ行
// テンプレートドキュメントを取得
const templateDoc = DocumentApp.openById(TEMPLATE_DOC_ID);
const templateBody = templateDoc.getBody();
// 作成済み請求書を保存するフォルダを取得または作成
let saveFolder = DriveApp.getFoldersByName(ATTACHMENT_FOLDER_NAME).next();
if (!saveFolder) {
saveFolder = DriveApp.createFolder(ATTACHMENT_FOLDER_NAME);
}
dataRows.forEach((row, index) => {
const rowIndex = index + 1; // シート上の行番号(1から始まる)
const emailSentFlag = row[COL_EMAIL_SENT_FLAG];
const status = row[COL_STATUS];
// メール送信済み、または「作成済」の場合はスキップ
if (emailSentFlag === '送信済' || status === '作成済') {
return;
}
const invoiceNumber = row[headerRow.indexOf('請求番号')]; // 請求番号
const recipientName = row[headerRow.indexOf('氏名')];
const recipientEmail = row[headerRow.indexOf('メールアドレス')]; // ★★★ メールアドレスの列名に合わせてください ★★★
// PDFとして保存するためのドキュメントコピーを作成
const newDoc = templateDoc.makeCopy(invoiceNumber + '_請求書');
const newDocBody = newDoc.getBody();
// プレースホルダーをデータで置換
for (const placeholder in PLACEHOLDER_MAP) {
const columnName = PLACEHOLDER_MAP[placeholder];
const columnIndex = headerRow.indexOf(columnName);
if (columnIndex !== -1) {
newDocBody.replaceText(placeholder, row[columnIndex]);
}
}
// 商品明細を動的に生成・置換
// ★★★ 商品明細のデータがあるシートの範囲を正しく指定してください ★★★
// 例:商品明細が別シートにある場合や、1行に複数商品がカンマ区切りなどで入っている場合など、
// 実際のデータ構造に合わせてこの部分を実装してください。
// ここでは、簡略化のため、1行に1つの商品明細が入っていると仮定します。
// より複雑な明細構造の場合は、別途商品明細用のループ処理が必要です。
// 例:templateBody.replaceText('{{商品明細テーブル}}', generatedItemRows);
// 例: templateBody.replaceText('{{商品名}}', row[headerRow.indexOf('商品名')]);
// templateBody.replaceText('{{数量}}', row[headerRow.indexOf('数量')]);
// templateBody.replaceText('{{単価}}', row[headerRow.indexOf('単価')]);
// templateBody.replaceText('{{金額}}', row[headerRow.indexOf('金額')]);
// TODO: 実際のデータ構造に合わせて、商品明細の生成ロジックを実装してください。
// 例:商品明細が別シートにある場合
// const itemSheet = ss.getSheetByName('商品明細');
// const itemData = itemSheet.getDataRange().getValues();
// const itemHeader = itemData[0];
// let generatedItemRows = '';
// itemData.slice(1).forEach(itemRow => {
// if (itemRow[itemHeader.indexOf('請求番号')] === invoiceNumber) {
// let itemRowHtml = ITEM_TEMPLATE_ROW;
// itemRowHtml = itemRowHtml.replace('{{商品名}}', itemRow[itemHeader.indexOf('商品名')]);
// itemRowHtml = itemRowHtml.replace('{{数量}}', itemRow[itemHeader.indexOf('数量')]);
// itemRowHtml = itemRowHtml.replace('{{単価}}', itemRow[itemHeader.indexOf('単価')]);
// itemRowHtml = itemRowHtml.replace('{{金額}}', itemRow[itemHeader.indexOf('金額')]);
// generatedItemRows += itemRowHtml;
// }
// });
// newDocBody.replaceText('<tbody>{{商品明細}}</tbody>', '<tbody>' + generatedItemRows + '</tbody>'); // {{商品明細}}を置換するプレースホルダーをテンプレートに用意
// ドキュメントを保存
newDoc.saveAndClose();
// PDFに変換
const pdfBlob = newDoc.getAs('application/pdf');
pdfBlob.setName(invoiceNumber + '_請求書.pdf');
// 作成したPDFをフォルダに保存
const uploadedFile = saveFolder.createFile(pdfBlob);
// メール送信
try {
const emailBody = `
${recipientName}様
いつもお世話になっております。
${EMAIL_SUBJECT_PREFIX}${invoiceNumber}の請求書をお送りいたします。
ご確認ください。
よろしくお願いいたします。
${SENDER_NAME}
`;
GmailApp.sendEmail(recipientEmail, `${EMAIL_SUBJECT_PREFIX}${invoiceNumber}`, emailBody, {
attachments: [pdfBlob],
name: SENDER_NAME
});
// スプレッドシートの状況を更新
sheet.getRange(rowIndex + 1, COL_STATUS + 1).setValue('作成済');
sheet.getRange(rowIndex + 1, COL_EMAIL_SENT_FLAG + 1).setValue('送信済');
Logger.log(`${invoiceNumber} の請求書を ${recipientEmail} へ送信しました。`);
} catch (e) {
Logger.log(`${invoiceNumber} のメール送信中にエラーが発生しました: ${e.toString()}`);
sheet.getRange(rowIndex + 1, COL_STATUS + 1).setValue('メール送信エラー');
}
// 作成したドキュメントを削除 (任意)
// DriveApp.getFileById(newDoc.getId()).setTrashed(true);
});
Logger.log('請求書自動生成・送信処理が完了しました。');
}
【重要】コード内の設定項目について
YOUR_TEMPLATE_DOCUMENT_ID: 作成した請求書テンプレートのドキュメントIDを貼り付けます。ドキュメントIDは、GoogleドキュメントのURLの/d/と/editの間の文字列です。
例: https://docs.google.com/document/d/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/edit の xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx の部分。
SHEET_NAME: 元データが入力されているスプレッドシートのシート名を正確に指定します。EMAIL_SUBJECT_PREFIX: メール件名の冒頭につく文字列です。必要に応じて変更してください。SENDER_NAME: Gmailの送信者名として表示される名前です。ATTACHMENT_FOLDER_NAME: 作成されたPDF請求書を保存するGoogleドライブ上のフォルダ名です。存在しない場合は自動で作成されます。PLACEHOLDER_MAP: ここが最も重要です。左側にGoogleドキュメントテンプレートで設定したプレースホルダー(例:{{会社名}})、右側にスプレッドシートのヘッダー行にある対応する列名を指定します。スプレッドシートの列名と完全に一致させてください。ITEM_TEMPLATE_ROW: 商品明細の1行分のHTMLテンプレートです。もしテンプレートのHTML構造が異なる場合は、ここを修正してください。COL_EMAIL_SENT_FLAG,COL_STATUS,COL_TOTAL_AMOUNT: それぞれメール送信フラグ、状況、合計金額の列のインデックス(0から始まる番号)を指定します。スプレッドシートの列構成に合わせて調整してください。- 商品明細の処理: 上記コードでは、商品明細の生成部分がコメントアウトされており、
TODOとなっています。これは、商品明細のデータ構造がユーザーによって大きく異なるためです。実際のデータ構造(別シートにあるか、1つのセルにカンマ区切りで入っているかなど)に合わせて、この部分を実装する必要があります。
ステップ4:トリガーの設定
GASコードを自動実行させるために、トリガーを設定します。スクリプトエディタの左側にある時計アイコン(トリガー)をクリックし、「トリガーを追加」ボタンを押します。
- 実行する関数を選択:
generateAndSendInvoices - 実行するデプロイを選択:
Head - イベントのソースを選択:
時間駆動 - 時間間隔の種類を選択:
毎日(または必要に応じた間隔) - 時間帯を選択: 実行したい時間帯
必要に応じて、他の条件(例: 特定のシートが更新されたら実行など)も設定できます。
ステップ5:権限の承認
初めてスクリプトを実行する際、Googleアカウントの権限を承認する必要があります。画面の指示に従って承認してください。
よくあるエラーと対処法
Document not foundまたはID is invalid:TEMPLATE_DOC_IDが正しく設定されていない可能性があります。GoogleドキュメントのURLからIDをコピーし、正確に貼り付けてください。Cannot retrieve the name of array(または似たエラー):PLACEHOLDER_MAPで指定しているスプレッドシートの列名が、実際のヘッダー行と一致していない可能性が高いです。大文字・小文字、全角・半角を確認してください。headerRow.indexOf(columnName)が-1を返している場合、その列名がヘッダーに存在しないことになります。- メールが送信されない、またはエラーメッセージが出る:
- Gmailの送信上限に達していないか確認してください。GASからのメール送信には1日の上限があります。
recipientEmailが正しく設定されているか、スプレッドシートの該当セルに有効なメールアドレスが入力されているか確認してください。- Gmailの「迷惑メール」フォルダに誤って振り分けられていないか確認してください。
- Gmailの「設定」>「すべての設定を表示」>「メールの作成と返信」>「署名」に内容が複数ある場合、GASからのメール送信で問題が発生することがあります。署名が1つであることを確認してください。
- PDFが作成されない、または文字化けする:
- テンプレートのフォントが、GASでサポートされていない特殊なフォントの可能性があります。標準的なフォント(Arial, Times New Romanなど)を使用してください。
- テンプレート内の画像や複雑なレイアウトが原因で、PDF変換に失敗することがあります。シンプルなレイアウトを心がけてください。
PERMISSION_DENIED:- スクリプトがGoogleドキュメントやGmailにアクセスするための権限が不足しています。スクリプト実行時に表示される権限承認画面で、必要な権限をすべて許可してください。
FAQ
Q1. 請求書だけでなく、納品書も同時に自動作成できますか?
A1. はい、可能です。納品書用のテンプレートも別途用意し、同様の手順でGASコードを記述すれば、請求書と納品書の両方を自動生成・送信できます。必要に応じて、generateAndSendInvoices 関数をコピー&ペーストして、テンプレートIDやメール件名などを変更した新しい関数を作成してください。
Q2. 複数商品(明細)がある場合の処理はどうすればいいですか?
A2. 上記コードのTODO部分で触れていますが、商品明細のデータ構造に合わせて実装する必要があります。一般的には、商品明細が別シートに顧客IDや請求番号と紐づけて管理されている場合、そのシートから該当する明細を取得し、HTMLテーブル形式で生成してテンプレートの特定の箇所(例: <tbody>...</tbody> の部分)に埋め込む形になります。より詳細な実装については、Google Apps Script のドキュメントやコミュニティでの質問をご参照ください。
Q3. PDFではなく、Excel(xlsx)形式で作成・送信することはできますか?
A3. GASで直接Excel形式のファイルを作成・送信するのは、PDFに比べて少し複雑になります。Googleスプレッドシートのデータを直接Excelファイルとしてダウンロードし、それを添付する方法や、外部ライブラリ(例: SheetsAPI を使ってスプレッドシートをExcel形式でエクスポート)を利用する方法が考えられます。PDF形式での自動化が最も一般的で手軽な方法です。
Q4. 毎月決まった日に自動送信したいのですが、どうすればいいですか?
A4. ステップ4のトリガー設定で、「時間駆動」を選び、「毎日」または「毎週」「毎月」といった頻度と、実行したい時間帯を設定することで実現できます。
まとめ
Google Apps Scriptを活用すれば、日々の請求書・納品書作成からメール送信までの一連の作業を劇的に効率化できます。
今回ご紹介したコードはあくまで一例ですが、これをベースに皆様の業務に合わせてカスタマイズすることで、さらなる生産性向上に繋がるはずです。
もし、これらの自動化ツール導入や、さらに複雑な業務プロセスのGASによる自動化にご興味があれば、ぜひ弊社のGASコンサルティングサービスにご相談ください。専門家が貴社の課題に合わせた最適なソリューションをご提案いたします。
まずは、この記事のコードをコピペして、ご自身の環境で試してみてください!
---
GAS自動化の導入相談
請求書PDF作成、Gmail自動送信、Slack通知、スプレッドシート連携などを業務に合わせて実装できます。