GASとGoogleフォームで経費申請の仕組みを作る方法

GASとGoogleフォームで経費申請の仕組みを作る方法

経費申請を紙の申請書や口頭確認で受け付けていると、誰がいつ何にいくら使ったのかを月末にまとめて突き合わせる作業が発生します。

領収書の紛失や科目の記載ミスも、申請の時点でチェックできないため後工程で発覚しがちです。

この記事では、GoogleフォームとGoogle Apps Scriptで経費申請を受け付け、上長への承認依頼から月次の科目別集計までを一続きの仕組みにする方法をまとめました。

経費申請の自動化を無料で相談する

完成イメージと仕組み

完成すると、申請者がフォームから日付・科目・金額・領収書を送信した瞬間にスプレッドシートへ記録され、同時に上長へ承認依頼メールが届きます。

上長がすることは、メールに並ぶ承認と却下のリンクのどちらかをクリックするだけです。

クリックされた行の承認状況と承認日時がスプレッドシートに書き込まれるため、経費が承認された記録として残ります。

さらに月に1回、承認済みの申請だけを科目別に自動集計する処理を組み合わせておけば、月末に手作業で電卓を叩く工程がなくなります。

承認の仕組み自体は、経費申請に限らず休暇申請などでも使える汎用的な作り方です。

承認リンクの仕組みをフォーム申請全般に広げて詳しく知りたい場合は、別記事にまとめています。

この仕組みが向いているのは、承認者が1人で科目もシンプルな小規模チームです。

部署ごとに承認者を分けたい、稟議のように複数人の承認を順番に通したいといった要件が出てくると、条件分岐が一気に増えます。

経費申請の運用規模

向いている作り方

承認者1人・科目が10種類前後

この記事のGoogle Apps Script構成

部署別承認者・複数人の順次承認・仕訳連携が必要

専用の経費精算システム(後述)

手順

フォームの設計から月次集計の自動化まで、大きく4つのステップです。

Step1. フォームとスプレッドシートを設計する

Googleフォームで経費申請フォームを作り、氏名・日付・科目(プルダウン)・金額・領収書の5項目を質問として並べます。

領収書はファイルアップロード形式の質問にしておくと、回答者がアップロードした画像がフォーム専用のGoogleドライブフォルダに保存され、スプレッドシート上には該当ファイルへのリンクが自動で入ります。

回答をスプレッドシートに送るよう設定すれば、A列送信日時・B列氏名・C列日付・D列科目・E列金額・F列領収書リンクの順に自動で並ぶシートのできあがりです。

続けて、このシートのG列を「申請ID」、H列を「承認状況」、I列を「承認日時」の見出しにしておきましょう。

Step2. コードを貼り付ける(コード全文)

スプレッドシートのメニューから拡張機能を開き、Apps Scriptを選んでスクリプトエディタを開きます。

表示されているコードを全て消し、次のコードをまるごと貼り付けてください。

function onFormSubmitSendApprovalRequest(e) {
  const sheetName = '経費申請一覧'; // ★ここを変える: フォーム回答先シート名
  const managerEmail = 'manager@example.com'; // ★ここを変える: 承認者のメールアドレス
  const webAppUrl = 'https://script.google.com/macros/s/xxxxx/exec'; // ★ここを変える: Step3でデプロイしたWebアプリのURL

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  const lastRow = sheet.getLastRow();
  const applicantName = sheet.getRange(lastRow, 2).getValue();
  const category = sheet.getRange(lastRow, 4).getValue();
  const amount = sheet.getRange(lastRow, 5).getValue();
  const applicationId = lastRow;

  sheet.getRange(lastRow, 7).setValue(applicationId);
  sheet.getRange(lastRow, 8).setValue('未承認');

  const approveUrl = `${webAppUrl}?id=${applicationId}&action=approve`;
  const rejectUrl = `${webAppUrl}?id=${applicationId}&action=reject`;

  const body = [
    `${applicantName}さんから経費申請が届きました。`,
    '',
    `科目: ${category}`,
    `金額: ${amount}円`,
    '',
    `承認する場合はこちら: ${approveUrl}`,
    `却下する場合はこちら: ${rejectUrl}`,
  ].join('\n');

  MailApp.sendEmail(managerEmail, '【承認依頼】経費申請が届きました', body);
}

function doGet(e) {
  const sheetName = '経費申請一覧';
  const targetRow = Number(e.parameter.id);
  const action = e.parameter.action;

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  const statusLabel = action === 'approve' ? '承認済み' : '却下';

  sheet.getRange(targetRow, 8).setValue(statusLabel);
  sheet.getRange(targetRow, 9).setValue(new Date());

  return ContentService.createTextOutput(`申請ID ${targetRow} を${statusLabel}にしました。`);
}

function aggregateApprovedExpensesMonthly() {
  const sheetName = '経費申請一覧'; // ★ここを変える: フォーム回答先シート名
  const summarySheetName = '月次集計'; // ★ここを変える: 集計結果を書き込むシート名

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  const summarySheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(summarySheetName);
  const data = sheet.getDataRange().getValues();

  const today = new Date();
  const targetMonth = Utilities.formatDate(
    new Date(today.getFullYear(), today.getMonth() - 1, 1),
    'Asia/Tokyo',
    'yyyy-MM',
  );

  const categoryTotals = new Map();
  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    const expenseDate = row[2];
    const category = row[3];
    const amount = row[4];
    const approvalStatus = row[7];

    if (approvalStatus !== '承認済み') {
      continue;
    }
    const rowMonth = Utilities.formatDate(new Date(expenseDate), 'Asia/Tokyo', 'yyyy-MM');
    if (rowMonth !== targetMonth) {
      continue;
    }

    const currentTotal = categoryTotals.get(category) || 0;
    categoryTotals.set(category, currentTotal + Number(amount));
  }

  categoryTotals.forEach((total, category) => {
    summarySheet.appendRow([targetMonth, category, total]);
  });
}

onFormSubmitSendApprovalRequest が送信直後の行から氏名・科目・金額を読み取り、行番号をそのまま申請IDとして使って承認依頼メールを送ります。

doGet はWebアプリとして公開したときに承認リンクがクリックされるたびに呼ばれ、対象行の承認状況と承認日時を書き込みます。

aggregateApprovedExpensesMonthly は先月分の承認済み申請だけを抜き出し、科目ごとの合計金額を「月次集計」シートに書き足す関数です。

Step3. トリガーを設定してWebアプリを公開する

スクリプトエディタ左側の時計マーク「トリガー」を開き、右下の「トリガーを追加」を押します。

1つ目は実行する関数にonFormSubmitSendApprovalRequest、イベントのソースにスプレッドシート、イベントの種類にフォーム送信時を選んで保存してください。

続けてスクリプトエディタ右上の「デプロイ」から「新しいデプロイ」を選び、種類でWebアプリを選択します。

このとき「次のユーザーとして実行」は「自分」のままにしておきます。

承認リンクをクリックした上長ではなくアカウント所有者の権限でスクリプトが動くため、上長にシートの編集権限がなくても承認状況を書き込めるからです。

「アクセスできるユーザー」は社内の運用ポリシーに沿って選び、発行されたURLをStep2のコード内webAppUrlに貼り付けて再度保存すれば、承認リンクが正しく動きます。

ただしこの承認リンクには、パスワードのような認証がありません。

URLのidの数字を書き換えれば別の申請も承認できてしまうため、公開範囲を「全員」にするとURLを知っている人なら社外からでも承認や却下を実行できます。

承認者が同じ組織のGoogleアカウントを持っているなら公開範囲を組織内に絞り、社外のアカウントで運用する場合は承認依頼メールの転送範囲に気をつけてください。

Step4. 月次集計を自動で走らせる

再びトリガー画面を開き、2つ目のトリガーを追加してください。

実行する関数にaggregateApprovedExpensesMonthly、イベントのソースに時間主導型を選び、時間ベースのトリガーのタイプは月タイマーにします。

毎月1日の午前中あたりに実行されるよう設定しておけば、前月分の承認済み経費が科目別の合計として月次集計シートに自動で積み上がっていく仕組みです。

経費申請から承認・月次集計までを自動化する構築手順の4ステップ。フォームとシートを設計→承認コードを貼り付ける→Webアプリを公開する→月次集計を自動化する

集計の粒度をさらに部署別・プロジェクト別に分けたい場合の考え方は、月次集計を扱う別記事が参考になるはずです。

経費申請以外にどの業務から自動化を始めるべきか迷う場合は、着手順の考え方を別記事で整理しています。

専用ワークフローシステムとの使い分け

ここまでの仕組みは、承認者1人・科目もシンプルな会社が自前で経費申請をデジタル化するための最小構成です。

一方で、電子帳簿保存法に対応したタイムスタンプ付与や訂正削除履歴の管理、交通系ICカードの利用明細取り込み、会計ソフトへの仕訳連携といった機能は、この構成には含まれていません。

こうした要件が出てきた時点で、経費精算に特化した専用システムへの切り替えを検討する価値があります。

専用システムは月額利用料がかかりますが、法対応やシステム連携をベンダー側が保守してくれる分、自社でコードを保守し続ける手間からは解放されます。

規模が小さいうちはこの記事の構成で経費申請の記録を残しておき、承認者が複数階層になる、部署をまたぐ、会計ソフトとの連携が必須になるといった変化が起きたタイミングで移行を判断する。

そういう順番が現実的です。


Google Apps Scriptでの経費申請の仕組みづくりについて相談してみませんか?

「承認者が複数階層になり自社の構成では対応しきれない」「領収書の電子帳簿保存法対応まで含めて設計したい」「社内にGoogle Apps Scriptを設計できる人材がいない」

少しでもお心当たりがあれば、お気軽にご相談ください。

現在、Google Apps Scriptを使った業務自動化のお悩みをお伺いする 無料の個別相談 を実施しています。


よくある質問

Q. 科目が増えたときプルダウンの選択肢はどう変えればいいですか?

Googleフォームの科目質問を編集画面で開き、選択肢を追加または削除するだけで反映されます。

すでに送信済みの回答データには影響しないため、運用中でも科目はいつでも見直せる状態です。

Q. 領収書の画像が不鮮明で金額を確認しづらい場合はどうすればいいですか?

現在の仕組みは領収書ファイルへのリンクをそのまま記録するだけなので、画像の鮮明さまではチェックしていません。

承認前に上長がリンクを開いて目視確認する運用を前提にしており、自動で金額を読み取りたい場合はAI Builderなどの文書解析機能を使う別の仕組みが必要になります。

Q. 申請者にも承認結果を通知したいです。

doGetの中で承認状況を書き込んだ直後に、その行の申請者のメールアドレスを取得してMailApp.sendEmail()を呼び出せば通知できます。

申請フォームにメールアドレスの質問項目を追加し、B列と同じように取得する処理を足す形で対応してください。

Q. 月次集計シートに同じ月のデータが重複して追加されませんか?

現在のコードはappendRowで行を追加し続ける仕様のため、aggregateApprovedExpensesMonthlyを同じ月に2回実行すると同じ科目が2行できてしまいます。

実行前に月次集計シートの対象月の行を削除するか、実行は月1回だけに絞る運用で対応してください。