10分で構築!Googleフォームで申請書電子化

国内のバックオフィスでは、紙やメール添付の申請書がいまだ根強く残っています。複数の公開調査や業務ログの分析から、紙の申請フローは1件あたり数分の手作業が継続発生し、記入不備や二重申請による再工数が全体の数割を占めるケースも珍しくありません。こうした紙業務の非効率は、ペーパーレスやDXの必要性が広く認識されつつも現場実装で苦戦する理由としても指摘されています。⁹ 一方でGoogle Workspaceはグローバルで多数の有料顧客を抱え¹、フォーム(Googleフォーム)とスプレッドシートの連携、Apps Script(Google提供の自動化スクリプト環境)、Forms API(フォームをプログラムから操作するAPI)などが公式に提供されています。²³¹¹ つまり、既存の認証と監査に乗り、個別開発を最小に抑えて高速に申請書の電子化へ移行できる素地は整っています。本稿ではCTO・エンジニアリーダーの視点から、10分で到達できる最小構成を定義し、実運用に耐えるための拡張ポイント、パフォーマンス指標、具体的コードまで一気通貫で示します。
10分で到達する最小構成の定義
最小構成のゴールは明確です。申請者がGoogleフォームに入力し、回答はスプレッドシートへ自動集計され²、担当者へ通知が飛び、申請番号が自動採番される。この一連をノーコードでつなぐと、初期立ち上げは10分前後で十分です。具体的にはフォームのテンプレートを用意し、組織内のメールドメイン限定でアクセスを制限(メール収集/1人1回答の設定を含む)³、回答先のスプレッドシートを紐付け²、通知をオンにするだけで基礎は完了します。申請番号は、スプレッドシート側でARRAYFORMULAやUNIQUE関数、タイムスタンプの組み合わせで「REQ-日付-行番号」のように採番しておくとノーコードで実現できます(高負荷や厳密な一意性は後述のサーバサイド実装に委ねます)。ここでの重要点は、最小構成の段階でも必須項目の定義、入力形式の制約、社内ドメイン制限、申請番号の一意性という四点を外さないことです。これらを押さえると、初日から二重申請の抑止と追跡可能性が担保され、現場が「使える」ラインに乗ります。⁹
セキュリティについては、共有ドライブ上に回答スプレッドシートを配置し、アクセス権はグループベースで最小権限に絞るのがよい選択です。¹⁰ フォーム自体は組織内限定を基本とし、外部公開が必要な場合は別環境で分離すると運用事故を起こしにくくなります。監査の見通しを確保するため、フォームの編集権限とシートの編集権限を分離し、オーナーは業務アカウントに一元化します。ダウンロードや印刷の制限も適宜有効化すると、情報の持ち出し抑止に寄与します。⁸
実装の核となるコードと設計の勘所
10分版の構築だけならGUIで完結しますが、運用で効かせるには自動化とガバナンスの仕込みが物を言います。ここでは作成自動化、回答処理、PDF生成、検証と分岐、データウェアハウス連携という五つの観点で、動かせるコードと共に要点を整理します。以降で触れる技術要素は、Forms API(フォーム定義の自動生成/更新)、Apps Script(イベントトリガでの自動処理)、BigQuery(分析基盤)といったGoogle公式のコンポーネントです。
フォームをAPIで自動生成する(Python)
手作業の作成ではフォームの差分管理が難しくなります。Google Forms APIを使えば、申請種別ごとのテンプレートをコード化し、レビューと再現性を確保できます。¹¹ OAuth認可(アクセス権限を与える標準的な仕組み)を用いた最小実装の例を示します。ドメイン全体でのサービスアカウント運用を行う場合は、管理コンソール側で委任設定が必要です。
from __future__ import annotations
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
SCOPES = ["https://www.googleapis.com/auth/forms.body", "https://www.googleapis.com/auth/forms.responses.readonly"]
def get_service():
flow = InstalledAppFlow.from_client_secrets_file("client_secret.json", SCOPES)
creds = flow.run_local_server(port=0)
return build("forms", "v1", credentials=creds)
def create_form():
service = get_service()
try:
form = service.forms().create(body={"info": {"title": "稟議申請", "documentTitle": "稟議申請フォーム"}}).execute()
form_id = form["formId"]
requests = [
{"createItem": {"item": {"title": "申請者メール", "questionItem": {"question": {"required": True, "textQuestion": {}}}}, "location": {"index": 0}}},
{"createItem": {"item": {"title": "申請金額", "questionItem": {"question": {"required": True, "textQuestion": {"paragraph": False}}}}, "location": {"index": 1}}},
{"createItem": {"item": {"title": "用途", "questionItem": {"question": {"required": True, "textQuestion": {"paragraph": True}}}}, "location": {"index": 2}}}
]
service.forms().batchUpdate(formId=form_id, body={"requests": requests}).execute()
print("Form created:", form_id)
except HttpError as e:
if e.resp.status in (429, 503):
print("Rate limited or service unavailable. Retry with backoff.")
else:
raise
if __name__ == "__main__":
create_form()
このアプローチでテンプレートのコード化が進むと、申請種別の追加や項目変更がプルリクレビューに乗るため、現場変更の監査性が飛躍的に高まります。
回答の集計と重複排除を堅くする(Node.js)
回答の初期格納先をスプレッドシートに置く限り、重複排除と採番はサーバサイドで制御するのが堅実です。以下は回答シートを監視し、メールアドレスとタイムスタンプの組で冪等な処理を行う例です。スプレッドシートAPIはバッチ更新を使うことでリクエスト数を削減できます。⁴
import { google } from "googleapis";
import { JWT } from "google-auth-library";
const SCOPES = ["https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/drive"];
const SHEET_ID = process.env.SHEET_ID;
async function getSheets() {
const auth = new JWT({
email: process.env.GSA_CLIENT_EMAIL,
key: (process.env.GSA_PRIVATE_KEY || "").replace(/\\n/g, "\n"),
scopes: SCOPES,
subject: process.env.DELEGATED_USER,
});
return google.sheets({ version: "v4", auth });
}
async function assignTicket() {
const sheets = await getSheets();
const range = "Responses!A:E";
const res = await sheets.spreadsheets.values.get({ spreadsheetId: SHEET_ID, range });
const values = res.data.values || [];
const header = values[0];
const emailIdx = header.indexOf("メールアドレス");
const tsIdx = header.indexOf("タイムスタンプ");
const ticketIdx = header.indexOf("申請番号");
const updates = [];
const seen = new Set();
for (let i = 1; i < values.length; i++) {
const key = `${values[i][emailIdx]}_${values[i][tsIdx]}`;
if (seen.has(key)) continue;
seen.add(key);
if (!values[i][ticketIdx]) {
const ticket = `REQ-${Date.now()}-${i}`;
values[i][ticketIdx] = ticket;
updates.push({ range: `Responses!A${i + 1}:E${i + 1}`, values: [values[i]] });
}
}
if (updates.length) {
await sheets.spreadsheets.values.batchUpdate({
spreadsheetId: SHEET_ID,
requestBody: { valueInputOption: "RAW", data: updates },
});
}
}
assignTicket().catch(console.error);
PDFの自動生成と通知(Apps Script)
承認フローや監査用途ではPDF保管が求められます。Apps Script(Googleのクラウド上で動くスクリプト環境)で回答イベントをトリガし、ドキュメントテンプレートへ差し込み、DriveにPDF化保存し、メール通知まで自動化します。Apps Scriptには実行時間や同時実行に関するクォータ/制約があるため、バースト負荷にはキューイングや再試行の仕組みを組み合わせると安定します。⁵ また、Gmailの送信クォータには上限があるため、通知の集約やキュー処理を前提に設計します。⁶
/***** Apps Script *****/
function onFormSubmit(e) {
try {
const values = e.values; // [timestamp, email, amount, purpose, ...]
const email = values[1];
const amount = values[2];
const purpose = values[3];
const ticket = values[4];
const templateId = "DOC_TEMPLATE_ID";
const folderId = "PDF_ARCHIVE_FOLDER_ID";
const doc = DocumentApp.openById(templateId).makeCopy(`申請_${ticket}`, DriveApp.getFolderById(folderId));
const body = DocumentApp.openById(doc.getId()).getBody();
body.replaceText("{{TICKET}}", ticket);
body.replaceText("{{EMAIL}}", email);
body.replaceText("{{AMOUNT}}", amount);
body.replaceText("{{PURPOSE}}", purpose);
DocumentApp.openById(doc.getId()).saveAndClose();
const pdfBlob = DriveApp.getFileById(doc.getId()).getAs("application/pdf");
const pdfFile = DriveApp.getFolderById(folderId).createFile(pdfBlob).setName(`申請_${ticket}.pdf`);
GmailApp.sendEmail(email, `申請を受け付けました: ${ticket}`, "受付完了しました。PDFを添付します。", { attachments: [pdfFile.getBlob()] });
} catch (err) {
console.error(err);
// Cloud Logging への出力やSlack通知等へ拡張
}
}
入力検証と分岐ロジック(Apps Script)
フォーム送信前の強力なバリデーションはフォームアイテムの検証機能で実現します。従業員番号の形式や上限金額の説明テキストなど、ユーザーの入力時点でエラーを明示できます。以下はフォームをコードで生成し、正規表現検証を付与する例です。³ 金額閾値で承認経路を分岐したい場合、初手は担当者通知の宛先や本文をスクリプトで切り替える実装が効果的です。高トラフィックや厳格なSLAが前提であれば、Pub/Sub(イベントを非同期で配信するメッセージング基盤)などのイベント基盤へ逃がし、外部の承認基盤と疎結合にするのが拡張の近道です。¹²
/***** Apps Script: フォーム生成 *****/
function buildFormWithValidation() {
const form = FormApp.create("購買申請");
form.setIsQuiz(false).setCollectEmail(true).setLimitOneResponsePerUser(true);
const emp = form.addTextItem();
emp.setTitle("従業員番号");
emp.setRequired(true);
const rule = FormApp.createTextValidation().requireTextMatchesPattern("^[A-Z]{2}\\d{6}$").setHelpText("例: AB123456").build();
emp.setValidation(rule);
const amount = form.addTextItem();
amount.setTitle("申請金額(円)").setRequired(true);
const purpose = form.addParagraphTextItem();
purpose.setTitle("用途").setRequired(true);
Logger.log(form.getPublishedUrl());
}
BigQueryへ流し込み、全社横断の可視化を得る(Python)
属人化を避けるため、最終的な意思決定は集計ダッシュボードで可視化します。スプレッドシートの行を定期バッチでBigQuery(Googleのデータ分析基盤)に積み上げる例を示します。BigQueryのストリーミング挿入は高スループットに対応し、近いリアルタイムでの反映が可能です。⁷
from google.oauth2 import service_account
from google.cloud import bigquery
from googleapiclient.discovery import build
SHEET_ID = "YOUR_SHEET_ID"
RANGE = "Responses!A:E"
creds = service_account.Credentials.from_service_account_file("svc.json", scopes=[
"https://www.googleapis.com/auth/spreadsheets.readonly",
"https://www.googleapis.com/auth/bigquery"
])
def read_sheet():
sheets = build("sheets", "v4", credentials=creds)
resp = sheets.spreadsheets().values().get(spreadsheetId=SHEET_ID, range=RANGE).execute()
rows = resp.get("values", [])
header, data = rows[0], rows[1:]
for r in data:
yield dict(zip(header, r))
client = bigquery.Client(credentials=creds, project=creds.project_id)
table = client.dataset("ops").table("requests")
errors = client.insert_rows_json(table, list(read_sheet()))
if errors:
print("BQ errors:", errors)
BIへの反映レイテンシは分単位で十分という組織が多く、Looker Studio等で指標をつくり、承認リードタイム、差し戻し率、金額帯別件数などを常時モニタすれば、ボトルネックが明確になります。⁷
運用設計とSLA、セキュリティの現実解
最小構成が稼働し始めると、即座に問われるのは運用の強度です。まずアクセス制御はグループドリブンで設計し、申請フォームは社内限定、審査側は編集権限、監査側は閲覧権限という役割分担を定義します。共有ドライブに保管されたPDFとシートは、データ損失防止の観点からダウンロード制限を組み合わせるとよいでしょう。¹⁰⁸ フォームの編集履歴やApps Scriptのデプロイ履歴は監査ログとして残るため、変更はチケット駆動で記録しておくと後日の説明責任が果たしやすくなります。
SLA設定では、受付から通知までの平均処理時間、ピーク時の遅延、失敗時の再試行ポリシーが要素になります。Apps Script単体で詰まる場合は、通知だけをWebhook化して外部に逃がすと詰まりにくくなります。クォータや実行時間の制約を考慮し、遅延のp95が数秒程度に収まるよう監視・調整すると、承認者の初動は十分に速くなります。⁵⁶ Gmailの送信クォータに留意しつつ、宛先をまとめるか、承認者のサマリーメールに切り替えるとさらに安定します。⁶
障害対応は単純さが武器になります。スプレッドシートの更新はbatchUpdateで冪等化し⁴、API側は指数バックオフで429と5xxをリトライ、PDF生成は一時ファイルの再利用を許容し、メール送信はキューに積んで順次吐き出す。これらを組み合わせるだけで、バーストにも壊れない土台が作れます。監視はエラー件数と遅延分布を最低限にし、閾値超過でSlackへ通知、週次でダッシュボードをレビューするという運用リズムを定めると、現場の安心感が確保できます。⁵
ビジネス効果とROI、拡張ロードマップ
工数の削減効果は極めてストレートに現れます。紙運用に伴う転記や差し戻しが減ることで、直接工数の削減に加え、リードタイム短縮やミス削減による二次効果も期待できます。⁹ さらに、紙の持ち出しリスクが消え、アクセスログとデータ保全が既存のGoogleインフラに乗ることで、監査対応の心理的負担も軽くなります。共有ドライブやダウンロード制限等の組み合わせは、現実的な情報保護策として有効です。¹⁰⁸
現場運用が回り始めたら、承認経路のデータ駆動化と外部SaaS連携が次の一手です。金額閾値や部門属性での自動ルーティング、プロキュアメントの発注番号採番、会計システムへの仕訳起票にまで踏み込めば、申請から支出までの一気通貫が見えてきます。高頻度の承認では、メールからのワンクリック承認とチャットのアクションを併用し、承認者の摩擦を最小化すると効果が大きいでしょう。イベント連携にはPub/Sub等の疎結合なメッセージングを使うと、スケーラビリティと保守性を両立できます。¹²
補足:フォーム生成をNode.jsで行う場合
チームの標準がNode.jsであれば、Forms APIの呼び出しも統一できます。ここでは最小限のフォーム生成例を提示します。¹¹
import { google } from "googleapis";
import open from "open";
const scopes = ["https://www.googleapis.com/auth/forms.body"];
async function main() {
const auth = new google.auth.OAuth2(process.env.CLIENT_ID, process.env.CLIENT_SECRET, "http://localhost:3000/callback");
const url = auth.generateAuthUrl({ scope: scopes, access_type: "offline" });
await open(url);
// 実装簡略化: コールバックでトークン取得するコードを省略
const forms = google.forms({ version: "v1", auth });
const res = await forms.forms.create({ requestBody: { info: { title: "備品申請" } } });
console.log(res.data.formId);
}
main().catch(console.error);
補足:承認APIをイベント駆動で外出しする(Python/Flask)
Apps Scriptの同期トリガからの疎結合化により、ピーク時の安定性が増します。以下は承認処理を受け付け、Pub/Subへエンキューする最小のエンドポイント例です。¹²
from flask import Flask, request, jsonify
from google.cloud import pubsub_v1
import os
app = Flask(__name__)
publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path(os.getenv("GCP_PROJECT"), "approvals")
@app.post("/approve")
def approve():
payload = request.get_json(force=True)
publisher.publish(topic_path, data=b"approve", ticket=payload["ticket"].encode())
return jsonify({"status": "queued"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
パフォーマンスの目安と具体的数値
フォーム送信からシート反映までは通常、数秒程度で伝搬します。² Apps ScriptのonFormSubmitからPDF生成と通知完了までも、設計が適切であれば数秒〜十数秒程度で安定させられます。⁵ 通知のメール送信は、Gmailの送信クォータ内で運用する必要があります。⁶ 大量の採番・集計が発生する場合も、スプレッドシートAPIのバッチ更新やBigQueryのストリーミング挿入を活用すれば、短時間で処理・可視化まで到達できます。⁴⁷ これらの具体的数値や制約を基準に、SLOを設定し、遅延のp95を監視対象に据えると、実務の品質が保たれます。⁵⁶
まとめ:今日から回せる、明日に拡張できる
Googleフォームによる申請書の電子化は、大掛かりな基盤投資を先送りしながらも、現場の痛みを即座に減らす現実解です。フォーム、シート、Apps Script、APIという既存資産をつなぐだけで短時間の立ち上げに到達し、PDF化や承認分岐、BI連携まで段階的に拡張できます。まずは最小構成で申請番号の一意性と通知の確実さを押さえ、次に差し戻し率とリードタイムを計測して、ボトルネックに手を入れてください。あなたの組織では、どの申請から電子化すると投資回収が早いでしょうか。今週ひとつのフローを置き換え、来週メトリクスを見直す。その短いサイクルが、全社の業務品質を着実に底上げしていきます。⁹
参考文献
- Business Insider. Google says 9 million organizations pay for Workspace (2023). https://www.businessinsider.com/google-workspace-9-million-paying-organizations-2023-3
- Google Docs Editors Help. View & manage form responses in Google Sheets. https://support.google.com/docs/answer/2917686?hl=ja
- Google Apps Script Reference. Form class (collect email, limit one response). https://developers.google.com/apps-script/reference/forms/form
- Google Sheets API. spreadsheets.values.batchUpdate. https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate
- Google Apps Script. Quotas for Google Services. https://developers.google.com/apps-script/guides/services/quotas
- Google Workspace Admin Help. Gmail sending limits in Google Workspace. https://support.google.com/a/answer/166852?hl=ja
- Google BigQuery Quotas and limits: Streaming inserts. https://cloud.google.com/bigquery/quotas?hl=ja#streaming_inserts
- Google Drive Help. Prevent viewers from downloading, printing, or copying. https://support.google.com/drive/answer/2494892?hl=ja
- freee. ペーパーレス化とは?メリット・デメリットや進め方を解説. https://www.freee.co.jp/kb/kb-trend/paperless/
- Google Workspace Admin Help. About shared drives. https://support.google.com/a/answer/7212025?hl=ja
- Google Forms API. Overview. https://developers.google.com/forms/api
- Google Cloud Pub/Sub. Overview. https://cloud.google.com/pubsub/docs/overview?hl=ja