【GAS業務効率化】業務委託の方からの請求書発行をGoogleフォームで受け付けたい

## ゴール：Googleフォームを使って業務委託さんからの請求書提出を自動化

とある会社様で、普段メールでやり取りしている請求書提出をベタに自動化できないかとご相談いただきました。そちらでご紹介させていただいた弊社社内で用いている、Googleフォームとスプレッドシート、GASを使って、自動でPDF化・保存まで完結するフローをご紹介します。

## 内容

- フォームに入力された請求内容が、自動でスプレッドシートに反映
- PDFが自動でGoogleドライブに保存されるので原本どこ行った問題が解消
- 通知もDiscordに飛ばせるようにして、確認漏れを防ぐ

## 開発環境（私の作業環境）

- OS: Windows（あまり関係ないですが）
- エディタ: GASエディタ
- 使用サービス: Google フォーム, Google スプレッドシート, Google Apps Script, Discord（Webhook）

## 事前準備

- Googleアカウント（当然ですね）
- 請求書テンプレートのスプレッドシート（後述）
- Discord Webhook URL（SlackでもOK。後述します）

## やったこと（全体の流れ）

### 1. Googleフォームとスプレッドシートの準備

まずは請求内容を入力してもらうためのフォームを準備します。以下のテンプレートをコピーして使ってください。

- [Googleフォームのテンプレートはこちら](https://docs.google.com/forms/d/1bzcAtvk4B3mjojzJhBkIIhsudUHUlzO3CUlVboTSSRQ/template/preview)

フォームの「回答」タブからスプレッドシートをリンクしておきましょう。

---

### 2. 請求書テンプレートの準備

こちらのスプレッドシートテンプレートから、「請求書コントローラー」と「請求書」シートをコピーします。
- [テンプレートスプレッドシートはこちら](https://docs.google.com/spreadsheets/d/1ND1_JzNIcWcfmVGyZclj0q8WwHq_vQ4rhmklbg99Of8/template/preview)

> コピー後、エラーが出てもセルをクリックしてEnterすれば直ることが多いです。

---

### 3. GASスクリプトの追加

スプレッドシートで「拡張機能 > Apps Script」から新しいスクリプトを作成して、下記コードを貼り付けます。

```javascript
/**
 * @fileoverview フォーム送信時に請求書をPDFとして出力し、
 *     Google ドライブに保存するスクリプト。
 * @package
 */

/**
 * フォーム送信時に実行される関数。
 *
 * 1. フォームの最終行のデータを取得し、請求書コントローラーに反映。
 * 2. 請求書シートをPDFとして出力。
 * 3. Google ドライブにPDFを保存。
 *
 * @param {!GoogleAppsScript.Events.SheetsOnFormSubmit} e
 *     フォーム送信イベントオブジェクト。
 */
function onFormSubmit(e) {
  /**
   * 現在アクティブなスプレッドシート。
   * @type {!GoogleAppsScript.Spreadsheet.Spreadsheet}
   */
  const ss = SpreadsheetApp.getActiveSpreadsheet();

  /**
   * PDF出力用のURL文字列。
   * @type {string}
   */
  let url = 'https://docs.google.com/spreadsheets/d/' + ss.getId() + '/export?' +
      'exportFormat=pdf&format=pdf' +
      '&size=A4' +
      '&portrait=true' +
      '&fitw=true' +
      '&sheetnames=false&printtitle=false' +
      '&pagenumbers=false' +
      '&gridlines=false' +
      '&fzr=false';

  /**
   * フォームの回答シート。
   * @type {?GoogleAppsScript.Spreadsheet.Sheet}
   */
  let sheet = ss.getSheetByName("フォームの回答 1");

  if (!sheet) return;

  /**
   * 回答シートの最終行番号。
   * @type {number}
   */
  const lastRow = sheet.getLastRow();

  // 請求書コントローラーシートに最終行番号を反映
  sheet = ss.getSheetByName("請求書コントローラー");
  if (!sheet) return;

  sheet.getRange("B3").setValue(lastRow);
  SpreadsheetApp.flush();

  // 請求書シートのPDF出力用にGIDをURLへ追加
  sheet = ss.getSheetByName("請求書");
  if (!sheet) return;

  url += '&gid=' + sheet.getSheetId();

  /**
   * 認証用トークン。
   * @type {string}
   */
  const token = ScriptApp.getOAuthToken();

  /**
   * PDFファイルのHTTPレスポンス。
   * @type {!GoogleAppsScript.URL_Fetch.HTTPResponse}
   */
  const response = UrlFetchApp.fetch(url, {
    headers: {
      'Authorization': 'Bearer ' + token
    }
  });

  /**
   * 請求書の宛名（氏名）。
   * @type {string}
   */
  const personName = /** @type {string} */ (sheet.getRange("F5").getValue());

  /**
   * 請求日。
   * @type {string}
   */
  const invoiceDate = /** @type {string} */ (sheet.getRange("G2").getValue());

  /**
   * PDFデータのBlob。
   * @type {!GoogleAppsScript.Base.Blob}
   */
  const pdfBlob = response.getBlob().setName(invoiceDate + "_" + personName + ".pdf");

  /**
   * 保存先のGoogleドライブフォルダ。
   * @type {!GoogleAppsScript.Drive.Folder}
   */
  const folder = DriveApp.getFolderById(
    /** @type {string} */
    (PropertiesService.getScriptProperties().getProperty("FOLDER_ID"))
  );

  folder.createFile(pdfBlob);
}
```

フォーム送信時のトリガーも忘れずに設定してください。

---

### 4. GASの設定：スクリプトプロパティ

PDFを保存するために、保存先のGoogleドライブのフォルダIDを環境変数として登録しておきます。
- `FOLDER_ID`：PDF格納用のフォルダID

環境変数の設定場所（2025年4月現在）
![スクリーンショット 2025-04-01 061218.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3905122/2c58cc7a-a1f8-427d-bb08-810ad375569d.png)

---

### 5. Tips：Discordへの通知機能を追加

GASからWebhookで通知を飛ばすようにすれば、チームでの承認もスムーズになります。Discordを使っている方はそのまま、Slackを使っている方は、WebhookのコードをChatGPTに投げれば簡単に変換してもらえます。

```javascript
  /**
   * Discordに送信するペイロード（PDFファイル＋メッセージ）。
   * @type {!ArrayBuffer}
   */
  const payload = Utilities.newBlob([]).getBytes()
    .concat(Utilities.newBlob(`--${boundary}${eol}`).getBytes())
    .concat(Utilities.newBlob(`Content-Disposition: form-data; name="content"${eol}`).getBytes())
    .concat(Utilities.newBlob(`${eol}`).getBytes())
    .concat(Utilities.newBlob(
      `# 請求書のお知らせ (${invoiceDate})` +
      `**${personName}**さんが請求書を作成しました。` +
      `担当者は内容を確認し、間違いがなければ請求者にPDFの控えを電子送付し内容の承諾をもらってください。` +
      `- 金額(税込み)：\`${sheet.getRange("F21").getValue()}円\`` +
      `- 振込期限：\`${sheet.getRange("B10").getValue().toString().substring(7)}\`` +
      `${eol}`
    ).getBytes())
    .concat(Utilities.newBlob(`--${boundary}${eol}`).getBytes())
    .concat(Utilities.newBlob(
      `Content-Disposition: form-data; name="files[0]"; filename="${invoiceDate + "_" + personName + ".pdf"}"${eol}`
    ).getBytes())
    .concat(Utilities.newBlob(`Content-Type: application/pdf${eol}`).getBytes())
    .concat(Utilities.newBlob(`${eol}`).getBytes())
    .concat(pdfBlob.getBytes())
    .concat(Utilities.newBlob(eol).getBytes())
    .concat(Utilities.newBlob(`--${boundary}--`).getBytes());

  /**
   * DiscordのWebhook URL。
   * @type {string}
   */
  const webhook = PropertiesService.getScriptProperties().getProperty("DISCORD_WEBHOOK");

  /**
   * DiscordへのPOSTリクエストオプション。
   * @type {!GoogleAppsScript.URL_Fetch.URLFetchRequestOptions}
   */
  const content = {
    method: "POST",
    contentType: `multipart/form-data; boundary=${boundary}`,
    payload: payload,
    muteHttpExceptions: true
  };

  response = UrlFetchApp.fetch(webhook, content);
  console.log(response.getContentText());
```

> `DISCORD_WEBHOOK` という環境変数にURLを入れておけばOKです！

---

## 補足と参考リンク

- [Google Apps Script公式ドキュメント](https://developers.google.com/apps-script)
- [Discord Webhookの使い方（公式）](https://support.discord.com/hc/ja/articles/228383668)
- [【appscript】スプレットシートの内容をPDF化し、指定のフォルダに保存](https://note.com/kris_saito/n/ne2fecea2e370)
- フォームやテンプレートは、自社用にカスタマイズして使うともっと便利になります！

## まとめ

Slackで通知したいときは、Webhookの形式をChatGPTに頼めば変換してもらえるので、社内システムごときにコード書くなんてという過激派の方でも結構なんとかなります。

GASをガチガチに組み込むより、スプレッドシートだけで完結する方が気楽ですし引継ぎもしやすいので、スプレッドシート単体である程度動くものを作るのがいいと思います（これもGASなしで請求書の形式変換まではできます）

というわけで、少しでも同じような悩みを抱えていた方の助けになれば幸いです:smirk: