今までの記事一覧
Googleフォーム×GASで実現する業務自動化シリーズ全記事まとめ目次
前提
この記事は、フォーム回答を保存しているスプレッドシート側のGASを前提にしています。
トリガーは以下を設定してください。
- 実行する関数:
onFormSubmit - イベントのソース:スプレッドシートから
- イベントの種類:フォーム送信時
スプレッドシート構成
スプレッドシート「集計シート」をこの図のように設定しています。
- B1セル:販売上限数
- B2セル:「有効注文数」の合計(
=SUM(F6:F)) - B3セル:残数(
=B1-B2) - 6行目以下のA~D列:フォームから送信された回答
- 6行目以下のE列:注文内容を変更・キャンセルした場合に、管理者が修正後の注文数を入力する列
- 6行目以下のF列:在庫計算に使用する「有効注文数」(以下の関数を設定)
- E列が空欄なら、フォームから送信された注文数(D列)を使用
- E列に修正数が入力されていれば、その値を使用
=ARRAYFORMULA(IF(D6:D="","",IF(E6:E="",D6:D,E6:E)))
- 6行目以下のG列:変更用の事前入力URLが入る
おさらい
前回は、送信者の名前やメールアドレスをフォームに事前入力したURLを、このようなGASコードを使って生成しました。
const items = conForm.getItems();
const mailItem = items[0].asTextItem(); // 1番目がメールアドレスの場合
const nameItem = items[1].asTextItem(); // 2番目が名前の場合
let response = conForm.createResponse();
response = response.withItemResponse(mailItem.createResponse(mail)); // メールをセット
response = response.withItemResponse(nameItem.createResponse(user)); // 名前をセット
// 最後にURLを取得
const prefilledUrl = response.toPrefilledUrl();
取得した事前入力URLを送信者にメールするところまでを実装しました。
じつは、事前入力URLの取得方法はもう1つあります。
GASの専用メソッドを使わず、もっと直感的にURLを文字列結合する方法です。
せっかくなので、今回はこの方法も試してみましょう。
文字列結合方式
前回の記事でも紹介しましたが、事前入力URLは手動でも取得できます。
フォームの管理画面で右上の三点リーダーから「フォームに事前入力する」を選択
出てきたフォーム画面に必要事項を入力
今回は、「メールアドレス」と「名前」を事前入力したものを作成したいので、この2つだけを入力します。入力内容は何でもいいですが、後で見て構造が分かる文字列にしましょう。今回はそれぞれ「test@mail.com」「name」としておきます。
注文数は空欄にしておいてくださいね。
【ここがコツ!】入力する内容は「半角英数字」にしておくのがおすすめです。
「名前」のような日本語(全角文字)入れると、書き出されたURLが %E5%90%8D%E5%89%8D のように暗号化された長文(エンコードされた状態)になってしまい、URLのどこを書き換えればいいのか見分けがつかなくなってしまいます。
入力したら左下の「リンクを取得」をクリック。
リンクをコピペ
左下に黒いメッセージが出るので「リンクをコピー」をクリック。URLがクリップボードにコピーされます。
コピーされた内容を、メモ帳などにペーストしてみましょう。このような長いURLが出てきたかと思います。
https://docs.google.com/forms/d/e/1FA*******/viewform?usp=pp_url&entry.384020918=test@mail.com&entry.1428111567=name
※フォームの入力で全角文字(「名前」など)を入れた場合、このURL内にはエンコードされた「%E5%90%8D%E5%89%8D」のようなものが入ります。
このURLをよく見ると、先ほど入力した「test@mail.com」「name」がそのまま出ているのが確認できるかと思います。
つまり、このURLの「test@mail.com」や「name」の部分を、GAS側で取得した実際の送信者のメールアドレスや名前にプログラムで入れ替えて(結合して)あげればOK、ということです。
(上図の緑字の部分はご自身のフォームによって異なります)
GASに入れる
今までのコードに入れるとしたらこうなります。変数userには「白黒ハチ太郎」などの日本語が入っているので、encodeURIComponent() を使って%E7%99%BD%E...の形に直して入れます。
const prefilledUrl = "https://docs.google.com/forms/d/e/1FA*******/viewform?usp=pp_url&entry.384020918="
+ mail + "&entry.1428111567="
+ encodeURIComponent(user)
entry.384020918やentry.1428111567はフォームの質問ID(エントリーID・入力項目ID)です。
今回のフォームでは、
メールアドレス項目:entry.384020918
名前項目:entry.1428111567
となっていますが、この数字はフォームごとにランダムで作成されます。必ずご自身のフォームで取得したURLを確認し、数字部分を書き換えて使ってくださいね。
上のコードを、前回作った変更フォームの事前入力URLを組み立てる部分(const items = ... から const prefilledUrl = ... までの部分)とごっそり入れ替えれば完成です。
シート関数
この文字列結合方式の最大の利点は、GASを使わずに スプレッドシートの関数(数式) だけでも実装できるということです。
このように、シートのB6から下にメールアドレス、C6から下に名前が入っている場合、
シートのG6に直接このように入力します。
="https://docs.google.com/forms/d/e/1FA******/viewform?usp=pp_url&entry.384020918=" & B6 & "&entry.1428111567=" & ENCODEURL(C6)
さらに、ARRAYFORMULAとIF関数を使って以下のように書けば、わざわざ下にオートフィル(コピー)しなくても、データがある行すべてに一括でURLを展開できます。
=ARRAYFORMULA(
IF(A6:A="","",
"https://docs.google.com/forms/d/e/1FA*****/viewform?usp=pp_url&entry.384020918=" & B6:B & "&entry.1428111567=" & ENCODEURL(C6:C)
)
)
このシート関数を使えば、ノンプログラマーの方でも簡単に事前入力URLを自動生成することができます。
文字列結合なら CONCATENATE() のほうがいいのでは?と思われた方もいるかもしれません。しかしARRAYFORMULA() と組み合わせて行ごとにURLを生成したい場合は注意が必要です。
CONCATENATE() は指定した範囲の内容をすべて連結して1つの文字列として返す関数 のため、
=ARRAYFORMULA(CONCATENATE(B6:B,C6:C))
のように書いても、各行ごとに結果が展開されるのではなく、すべてのデータが結合された1つの長い文字列になってしまいます。
そのため、ARRAYFORMULA() で行単位に処理したい場合は、今回のように & 演算子を使う方法がおすすめです。
作成したURLをGASで読み込む
ここでできた事前入力URLを、メール等で使いたい場合はこのように、シートから呼び出せばOK。
const sheet_summary = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("集計シート");
const prefilledUrl = sheet_summary.getRange(editRow, 7).getValue();
これをメール送信のサブ関数に入れます。
文字列結合でURLを作成する方法は Googleフォーム以外のURL生成にも応用できます。
「URLの一部だけを差し替える」仕組みなので、ユーザーIDや予約番号を埋め込んだリンクの生成、問い合わせページへのパラメータ受け渡しなどにも活用できます。
まとめ
今回は、URLを文字列結合することで事前入力URLを作成する方法を紹介しました。
この方法は toPrefilledUrl() を使う方法よりも仕組みが分かりやすく、URLの構造さえ理解できればGASだけでなくスプレッドシート関数だけでも実装できます。
私自身、Googleフォームの事前入力URLは長らくこの方法で作っていました。
次回はスピンオフとして、今回の文字列結合方式と前回紹介した toPrefilledUrl() を比較し、それぞれのメリット・デメリットを整理してみたいと思います。
次回の記事はこちら
Googleフォーム×在庫管理!注文数の変更・キャンセルに対応〈番外編〉toPrefilledUrl() vs 文字列結合方式を比較
今回の成果コード
パターン1:GASコード内でURLを組み立てる(GAS完結版)
メイン関数内で文字列結合を行い、サブ関数にURLを直接引き渡すシンプルな構成です。
function onFormSubmit(e) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = e.range.getSheet(); //フォーム回答が入力されたシート
const row = e.range.getRow(); //フォーム回答が入力された行
const column = e.values.length; // フォーム回答シートに書き込まれた列
const formData = sheet.getRange(row, 1, 1, column).getValues();
const mail = e.values[1]; //フォーム送信データからメールアドレスを取得(今回は2つめの回答=0から始まるので[1])
const user = e.values[2]; //フォーム送信データからユーザー名を取得
const num = Number(e.values[3]); //フォーム送信データから注文数を取得
// 紐づいているフォームを取得(管理用ID)
const form = FormApp.openById("1###############"); // 注文フォーム
const corForm = FormApp.openById("1************"); // 変更フォーム
/// ★★文字列結合で事前入力URL作成
const prefilledUrl = "https://docs.google.com/forms/d/e/1FA*******/viewform?usp=pp_url&entry.384020918="
+ mail + "&entry.1428111567="
+ encodeURIComponent(user)
//集計シートの最終行
const sheet_summary = ss.getSheetByName("集計シート");
const summaryLastRow = sheet_summary
.getRange(sheet_summary.getMaxRows(), 1)
.getNextDataCell(SpreadsheetApp.Direction.UP)
.getRow(); //A列の最終行を取得
const mailDataList = sheet_summary.getRange(5, 2, summaryLastRow - 4, 1).getValues().flat();
let currentRemaining = Number(sheet_summary.getRange("B3").getValue());
const sheetName = sheet.getName();
let editRow;
const lastHitIndex = mailDataList.lastIndexOf(mail);
if (sheetName === "フォームの回答1") { // 注文フォーム(新規)
if (currentRemaining - num < 0) { // 在庫がマイナスになった場合はお断りメール
rejectionMail(mail, user, currentRemaining, prefilledUrl); // function rejectionMail
return;
} else {
editRow = summaryLastRow + 1;
}
} else if (sheetName === "フォームの回答2") { // 変更フォーム(修正)
// 安全対策:もし過去の注文(メールアドレス)がシート内に見つからなければお断りメール(2)
if (lastHitIndex == -1) {
rejectionCorrectionMail(mail, user,form); //function rejectionCorrectionMail
return;
}
// 先にアドレスがある行を特定する
editRow = lastHitIndex + 5;
// 特定した行から、前回の注文数(preNum)を安全に取得(F列=6列目の場合)
const preNum = Number(sheet_summary.getRange(editRow, 6).getValue());
// preNumが確定したので、ここで在庫の計算を行う
if (currentRemaining - num + preNum < 0) { // 在庫がマイナスになった場合はお断りメール
rejectionMail(mail, user, currentRemaining + preNum, prefilledUrl); // function rejectionMail
return;
}
}
sheet_summary.getRange(editRow, 1, 1, column).setValues(formData);
sheet_summary.getRange(editRow, 7).setValue(prefilledUrl);
sheet_summary.getRange(editRow, 5).clearContent(); //管理者が入力したデータをクリアする
SpreadsheetApp.flush(); //今書き込んだ内容をシートに反映させる
// 最新の在庫数をシートから再取得
currentRemaining = Number(sheet_summary.getRange("B3").getValue());
// もし残数が0以下になったら、フォームを自動で締め切る
if (currentRemaining === 0) {
form.setCustomClosedFormMessage("本日の注文受付は【完売】のため終了いたしました。またのご利用をお待ちしております!");
form.setAcceptingResponses(false); // 新規受付を停止
//変更フォームにだけ、残数を表示
corForm.setDescription("【現在の残数: " + currentRemaining + " 個】"
+ "\n" + "元々のご自身の注文数にこの数を足したものが、注文可能数です。");
} else { //残数がまだある場合
//フォームが受付停止になっていたら再開
if (form.isAcceptingResponses() == false) {
form.setAcceptingResponses(true);
}
//フォーム全体の「説明欄」を書き換える場合
form.setDescription("【現在の残数: " + currentRemaining + " 個】");
corForm.setDescription("【現在の残数: " + currentRemaining + " 個】"
+ "\n" + "元々のご自身の注文数にこの数を足したものが、注文可能数です。");
//特定の質問(注文数)の「プルダウン選択肢」を書き換える
const items = form.getItems();
for (const item of items) {
if (item.getTitle() === "注文数を選択してください") {
// プルダウン(asListItem) に型変換
const listItem = item.asListItem();
const choices = [];
for (let i = 0; i <= currentRemaining; i++) {
choices.push(String(i)); // "1", "2", "3"... と文字列で入れていく
}
// プルダウンの選択肢を最新在庫に合わせて書き換え
listItem.setChoiceValues(choices);
// ヘルプテキスト(説明文)を更新
listItem.setHelpText("※現在の残数は「あと " + currentRemaining + " 個」です。");
break;
}
}
}
//サブ関数 acceptMail を呼び出し、メール送信
acceptMail(mail,user,num,prefilledUrl);
}
パターン2:シート関数で入れたURLをGASで読み込む(ハイブリッド版)
URLの自動生成はスプレッドシート側の =ARRAYFORMULA(...) に100%任せ、GAS側は SpreadsheetApp.flush() で計算結果を強制的に呼び出してメールに添付する連携手法です。
※新規注文を変更フォームから送信した場合は集計シートを更新しないため、シート関数で生成したURLを利用できません。このケースの処理については今回は変更していません。
function onFormSubmit(e) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = e.range.getSheet(); //フォーム回答が入力されたシート
const row = e.range.getRow(); //フォーム回答が入力された行
const column = e.values.length; // フォーム回答シートに書き込まれた列
const formData = sheet.getRange(row, 1, 1, column).getValues();
const mail = e.values[1]; //フォーム送信データからメールアドレスを取得(今回は2つめの回答=0から始まるので[1])
const user = e.values[2]; //フォーム送信データからユーザー名を取得
const num = Number(e.values[3]); //フォーム送信データから注文数を取得
// 紐づいているフォームを取得(管理用ID)
const form = FormApp.openById("1###############"); // 注文フォーム
const corForm = FormApp.openById("1************"); // 変更フォーム
//集計シートの最終行
const sheet_summary = ss.getSheetByName("集計シート");
const summaryLastRow = sheet_summary
.getRange(sheet_summary.getMaxRows(), 1)
.getNextDataCell(SpreadsheetApp.Direction.UP)
.getRow(); //A列の最終行を取得
const mailDataList = sheet_summary.getRange(5, 2, summaryLastRow - 4, 1).getValues().flat();
let currentRemaining = Number(sheet_summary.getRange("B3").getValue());
const sheetName = sheet.getName();
let editRow;
const lastHitIndex = mailDataList.lastIndexOf(mail);
if (sheetName === "フォームの回答1") { // 注文フォーム(新規)
editRow = summaryLastRow + 1;
if (currentRemaining - num < 0) { // 在庫がマイナスになった場合はお断りメール
rejectionMail(mail, user, currentRemaining, editRow); // function rejectionMail
return;
}
} else if (sheetName === "フォームの回答2") { // 変更フォーム(修正)
// 安全対策:もし過去の注文(メールアドレス)がシート内に見つからなければお断りメール(2)
if (lastHitIndex == -1) {
rejectionCorrectionMail(mail, user, form); //function rejectionCorrectionMail
return;
}
// 先にアドレスがある行を特定する
editRow = lastHitIndex + 5;
// 特定した行から、前回の注文数(preNum)を取得(F列=6列目の場合)
const preNum = Number(sheet_summary.getRange(editRow, 6).getValue());
// preNumが確定したので、ここで在庫の計算を行う
if (currentRemaining - num + preNum < 0) { // 在庫がマイナスになった場合はお断りメール
rejectionMail(mail, user, currentRemaining + preNum, editRow); // function rejectionMail
return;
}
}
sheet_summary.getRange(editRow, 1, 1, column).setValues(formData);
//sheet_summary.getRange(editRow, 7).setValue(prefilledUrl);
sheet_summary.getRange(editRow, 5).clearContent(); //管理者が入力したデータをクリアする
SpreadsheetApp.flush(); //今書き込んだ内容をシートに反映させる
// 最新の在庫数をシートから再取得
currentRemaining = Number(sheet_summary.getRange("B3").getValue());
// もし残数が0以下になったら、フォームを自動で締め切る
if (currentRemaining === 0) {
form.setCustomClosedFormMessage("本日の注文受付は【完売】のため終了いたしました。またのご利用をお待ちしております!");
form.setAcceptingResponses(false); // 新規受付を停止
//変更フォームにだけ、残数を表示
corForm.setDescription("【現在の残数: " + currentRemaining + " 個】"
+ "\n" + "元々のご自身の注文数にこの数を足したものが、注文可能数です。");
} else { //残数がまだある場合
//フォームが受付停止になっていたら再開
if (form.isAcceptingResponses() == false) {
form.setAcceptingResponses(true);
}
//フォーム全体の「説明欄」を書き換える場合
form.setDescription("【現在の残数: " + currentRemaining + " 個】");
corForm.setDescription("【現在の残数: " + currentRemaining + " 個】"
+ "\n" + "元々のご自身の注文数にこの数を足したものが、注文可能数です。");
//特定の質問(注文数)の「プルダウン選択肢」を書き換える
const items = form.getItems();
for (const item of items) {
if (item.getTitle() === "注文数を選択してください") {
// プルダウン(asListItem) に型変換
const listItem = item.asListItem();
const choices = [];
for (let i = 0; i <= currentRemaining; i++) {
choices.push(String(i)); // "1", "2", "3"... と文字列で入れていく
}
// プルダウンの選択肢を最新在庫に合わせて書き換え
listItem.setChoiceValues(choices);
// ヘルプテキスト(説明文)を更新
listItem.setHelpText("※現在の残数は「あと " + currentRemaining + " 個」です。");
break;
}
}
}
//サブ関数 acceptMail を呼び出し、メール送信
acceptMail(mail, user, num, editRow);
}
///////////////////////////
//お断りメール(1)
//////////////////////////
function rejectionMail(mail, user, num, editRow) {
const title = "【重要】お弁当のご注文を承れませんでした"
let body;
if (num <= 0) {
body = `
${user} 様
ご注文ありがとうございます。
大変申し訳ございませんが、先に他のお客様のご注文が確定し、
残数が0になったため、${user}様の注文を承ることができませんでした。
またのご利用をお待ちしております。
`;
} else {
SpreadsheetApp.flush();
const sheet_summary = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("集計シート");
const prefilledUrl = sheet_summary.getRange(editRow, 7).getValue();
const time = Utilities.formatDate(new Date(), "JST", "yyyy/MM/dd HH:mm");
body = `
${user} 様
ご注文ありがとうございます。
大変申し訳ございませんが、先に他のお客様のご注文が確定したため
${user}様の注文を承ることができませんでした。
恐れ入りますが再度以下のリンクから注文をお願いします。
${prefilledUrl}
なお、${time} 現在の注文可能数は【 ${num} 個】となっております。
`;
}
//GmailApp.sendEmail(mail, title, body);
GmailApp.createDraft(mail, title, body);
}
///////////////////////////
//受付メール
//////////////////////////
function acceptMail(mail, user, num, editRow) {
SpreadsheetApp.flush();
const sheet_summary = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("集計シート");
const prefilledUrl = sheet_summary.getRange(editRow, 7).getValue();
const title = "お弁当の注文を受け付けました"
let body = `
${user} 様
ご注文ありがとうございます。
以下のとおり、お弁当の注文を承りました。
注文数:${num} 個
注文内容の変更・キャンセルは、以下のURLから変更内容を送信してください。
${prefilledUrl}
`;
GmailApp.createDraft(mail, title, body);
}





