今までの記事一覧
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)))
おさらい
前回、前々回と、フォームから受け付けたデータを変更する仕組みを作りました。
前々回は、集計シートを管理者が手動で修正し、その内容をフォームの説明欄や「回答の検証・選択肢」に反映する方法、
前回は、変更用フォームを作ってユーザー自らが変更を送信し、その内容をメインの注文フォームの説明欄や「回答の検証・選択肢」に反映する方法でした。
今回のこの記事、本来は「③編集URL活用編」になる予定でした。
送信内容の変更なら編集用URLを使えばいいのでは?と思う方、きっといらっしゃるでしょう。私もそう思いました。
ところが、実際に作ってみると大きな落とし穴があったのです。
やりたいこと
今まで作ってきたことに以下を追加。
- フォーム送信されたら編集用URLを取得する
- 送信者に編集用URLをメールする
編集用URLとは
フォームの管理画面で「設定」→「回答」→「回答の編集を許可する」をONにすると、送信済みのフォームを呼び出して、回答内容を変更して再送信することができます。
フォームを送信後に出るこの画面にある「回答を編集」をクリックすると開く画面です。

送信済みの内容のフォーム画面が開くので、変更したい部分を直して送信する仕組みです。このとき開かれるフォームのURLが 編集用URL(Edit Response URL) です。
今回のキモはコレ!
フォームの「一番最後の回答」から編集URLを取り出す
紐づいているフォームを取得した後、以下のコードで、最終回答を取り出し、その編集URLを取得します。
const formResponses = form.getResponses();
const latestResponse = formResponses[formResponses.length - 1];
const editUrl = latestResponse.getEditResponseUrl();
なお、今回の検証ではスプレッドシート側のフォーム送信トリガーを利用しているため、e.response.getEditResponseUrl() を使うことができません。そのため、フォームの最終回答から編集URLを取得していますが、この方法は「最後に送信された回答が今回の回答である」ことを前提としています。複数人がほぼ同時に送信した場合は別の利用者の編集URLを取得してしまう可能性があり、本番運用では注意が必要です。
編集用URLを使って突合
新たな回答が送信されたときに同じ編集用URLがあるかどうかで新規/変更を判定し、シートへの書き込み行の特定を行います。
// 今届いた編集URLが、集計シートのG列の中にすでにあるか確認
const urlIndex = beforeEditUrls.lastIndexOf(editUrl); // なければ -1、あればそのインデックスが返る
if (urlIndex == -1) {
// 【新規ルート】URLが見つからなければ新規注文
editRow = summaryLastRow + 1;
} else {
// 【変更ルート】同じURLが見つかったら、過去のその行を特定して上書き対象にする
editRow = urlIndex + startRow;
}
GASコード
というわけで、できたコードはこれ。
あらかじめ集計シートに「編集フォームURL」を入れる列を作っておきます。
以下のコードでは「編集フォームURL」のセルに名前付き範囲を設定しています。
function onFormSubmit(e) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = e.range.getSheet(); //フォーム回答が入力されたシート
const row = e.range.getRow(); //フォーム回答が入力された行
const column = e.values.length; // フォーム回答シートに書き込まれた列
let formData = sheet.getRange(row, 1, 1, column).getValues();
//e.valuesだと、変更のない項目はカラになるので、シートから取得する方法に変更
// const mail = e.values[1];
// const user = e.values[2];
// const num = Number(e.values[3]);
//フォームの回答シートから取得
const mail = sheet.getRange(row, 2).getValue();
const user = sheet.getRange(row, 3).getValue();
const num = Number(sheet.getRange(row, 4).getValue());
//紐づいているフォームを取得
const formUrl = ss.getFormUrl();
const form = FormApp.openByUrl(formUrl);
//★フォームの「一番最後の回答」から編集URLを取り出す
const formResponses = form.getResponses();
const latestResponse = formResponses[formResponses.length - 1];
const editUrl = latestResponse.getEditResponseUrl();
const sheet_summary = ss.getSheetByName("集計シート");
const summaryLastRow = sheet_summary
.getRange(sheet_summary.getMaxRows(), 1)
.getNextDataCell(SpreadsheetApp.Direction.UP)
.getRow(); //A列の最終行を取得
// G列(名前付き範囲「編集フォームURL」)の列番号を取得<あらかじめ「名前付き範囲」を設定
const editUrlCol = sheet_summary.getRange("編集フォームURL").getColumn();
// 判定用:集計シートのG列(5行目〜最終行)にある過去の編集URLを一斉取得
const beforeEditUrls = sheet_summary.getRange(5, editUrlCol, summaryLastRow, 1).getValues().flat();
const startRow = 5;
let editRow; // シートに書き込むべき行数
// 今届いた編集URLが、集計シートのG列の中にすでにあるか確認
const urlIndex = beforeEditUrls.lastIndexOf(editUrl); // なければ -1、あればそのインデックスが返る
if (urlIndex == -1) {
// 【新規ルート】URLが見つからなければ新規注文
editRow = summaryLastRow + 1;
} else {
// 【変更ルート】同じURLが見つかったら、過去のその行を特定して上書き対象にする
editRow = urlIndex + startRow;
}
// 集計シートに転記
sheet_summary.getRange(editRow, 1, 1, formData[0].length).setValues(formData);
// 新規でも変更でも、編集用URLを上書き入力
sheet_summary.getRange(editRow, editUrlCol).setValue(editUrl);
// 新規でも変更でも、管理者が手動で修正したE列(5列目)を自動でクリアする
sheet_summary.getRange(editRow, 5).clearContent();
SpreadsheetApp.flush(); // 今書き込んだ内容をシートに反映させる
const currentRemaining = Number(sheet_summary.getRange("B3").getValue());
if (currentRemaining <= 0) {
form.setCustomClosedFormMessage("本日の注文受付は【完売】のため終了いたしました。またのご利用をお待ちしております!");
form.setAcceptingResponses(false);
} else {
if (form.isAcceptingResponses() == false) {
form.setAcceptingResponses(true);
}
form.setDescription("【現在の残り在庫: " + currentRemaining + " 個】");
const items = form.getItems();
for (const item of items) {
if (item.getTitle() === "注文数を選択してください") {
const listItem = item.asListItem();
const choices = [];
for (let i = 1; i <= currentRemaining; i++) {
choices.push(String(i));
}
listItem.setChoiceValues(choices);
listItem.setHelpText("※現在の残り在庫は「あと " + currentRemaining + " 個」です。");
break;
}
}
}
const title = "お弁当の注文を受け付けました";
const body = `
${user} 様
ご注文ありがとうございます。
以下のとおり注文を受け付けました。
注文数:${num} 個
注文のキャンセル/修正は以下のリンクからフォームを修正して送信してください。
${editUrl}
`
GmailApp.sendEmail(mail, title, body);
}
動作確認
新規送信
まずは新規送信。編集用URLがきちんと入っています。メールも作成されました。
変更送信
では編集用URLをクリックしてフォームの編集画面を開き、注文数を「7」に変更してみましょう。
注文数が「7」に変更され、残数は「3」です。
ここまでは何の問題もないように思えるのですが・・・

フォームの選択肢が!
ここで白黒ハチ太郎さん、やっぱり注文数を「6」に減らしたくなりました。
ところが・・・

選択肢は1~3しかありません。本来この利用者はすでに7個注文しているため、残数3個を加えた「10個」までは変更できるはずです。ましてや「7」→「6」へ減少させるなら、注文を受けるのにまったく問題はないはずです。
選択肢や回答の検証はフォームを使う人すべて共通なので、白黒ハチ太郎さんだけの特別枠を作ることはできません。ここで選択肢を「6」までにしたとすると、これから入力しようとしている新規注文の人にも上限「6」が表示され、残数以上の注文を送信できるようになってしまうのです。
つまり、選択肢に注文可能数を入れる仕様にしているかぎり、編集用URLを使った変更処理は大きな不具合を生むということです。
ではどうする?
下図のように、記述式(短文)とし、「回答の検証」は「数値」「次の間にある」「0」「10」(販売上限。今回の場合は10)としておきます。
ただし、残数以上の入力や、小数など無効な入力をシステムで阻止することができないので、送信されたデータからGASを使って無効な入力をチェックし、メールなどで通知するシステムにするといいかと思います。
GASコード(自由記述版)
以下が修正したコードです。
function onFormSubmit(e) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = e.range.getSheet(); //フォーム回答が入力されたシート
const row = e.range.getRow(); //フォーム回答が入力された行
const column = e.values.length; // フォーム回答シートに書き込まれた列
let formData = sheet.getRange(row, 1, 1, column).getValues();
//フォームの回答シートから取得
const mail = sheet.getRange(row, 2).getValue();
const user = sheet.getRange(row, 3).getValue();
const num = Number(sheet.getRange(row, 4).getValue());
//紐づいているフォームを取得
const formUrl = ss.getFormUrl();
const form = FormApp.openByUrl(formUrl);
//★フォームの「一番最後の回答」から編集URLを取り出す
const formResponses = form.getResponses();
const latestResponse = formResponses[formResponses.length - 1];
const editUrl = latestResponse.getEditResponseUrl();
// 小数やマイナスをはじく
if (!Number.isInteger(num) || num < 0) {
invalidInputMail(mail, user, num, editUrl); // 不正入力用のお断りメールを作成
return;
}
const sheet_summary = ss.getSheetByName("集計シート");
const summaryLastRow = sheet_summary
.getRange(sheet_summary.getMaxRows(), 1)
.getNextDataCell(SpreadsheetApp.Direction.UP)
.getRow(); //A列の最終行を取得
// G列(名前付き範囲「編集フォームURL」)の列番号を取得<あらかじめ「名前付き範囲」を設定
const editUrlCol = sheet_summary.getRange("編集フォームURL").getColumn();
// 判定用:集計シートのG列(5行目〜最終行)にある過去の編集URLを一斉取得
const beforeEditUrls = sheet_summary.getRange(5, editUrlCol, summaryLastRow, 1).getValues().flat();
let currentRemaining = Number(sheet_summary.getRange("B3").getValue()); //現在の残数
const startRow = 5;
let editRow; // シートに書き込むべき行数
// 今届いた編集URLが、集計シートのG列の中にすでにあるか確認
const urlIndex = beforeEditUrls.lastIndexOf(editUrl); // なければ -1、あればそのインデックスが返る
if (urlIndex == -1) {
if (currentRemaining - num < 0) { // 在庫がマイナスになった場合はお断りメール
rejectionMail(mail, user, currentRemaining,editUrl); // function rejectionMail
return;
} else {
editRow = summaryLastRow + 1;
}
} else {
// 【変更ルート】同じURLが見つかったら、過去のその行を特定して上書き対象にする
editRow = urlIndex + startRow;
// 特定した行から、前回の注文数(preNum)を安全に取得(F列=6列目の場合)
const preNum = Number(sheet_summary.getRange(editRow, 6).getValue());
// preNumが確定したので、ここで在庫の計算を行う
if (currentRemaining - num + preNum < 0) { // 在庫がマイナスになった場合はお断りメール
rejectionMail(mail, user, currentRemaining + preNum,editUrl); // function rejectionMail
return;
}
}
// 集計シートに転記
sheet_summary.getRange(editRow, 1, 1, formData[0].length).setValues(formData);
// 新規でも変更でも、編集用URLを上書き入力
sheet_summary.getRange(editRow, editUrlCol).setValue(editUrl);
// 新規でも変更でも、管理者が手動で修正したE列(5列目)を自動でクリアする
sheet_summary.getRange(editRow, 5).clearContent();
SpreadsheetApp.flush(); // 今書き込んだ内容をシートに反映させる
currentRemaining = Number(sheet_summary.getRange("B3").getValue());
if (currentRemaining === 0) {
form.setCustomClosedFormMessage("本日の注文受付は【完売】のため終了いたしました。またのご利用をお待ちしております!");
form.setAcceptingResponses(false);
} else {
if (form.isAcceptingResponses() == false) {
form.setAcceptingResponses(true);
}
form.setDescription("【現在の残り在庫: " + currentRemaining + " 個】");
const items = form.getItems();
for (const item of items) {
if (item.getTitle() === "注文数を入力してください") {
// TextItem(記述式)に型変換して変数に格納
const textItem = item.asTextItem();
// ヘルプテキスト(説明欄)を更新
textItem.setHelpText("※現在の残り在庫は「あと " + currentRemaining + " 個」です。");
break;
}
}
}
//★★メール作成
const title = "お弁当の注文を受け付けました";
const body = `
${user} 様
ご注文ありがとうございます。
以下のとおり注文を受け付けました。
注文数:${num} 個
注文のキャンセル/修正は以下のリンクからフォームを修正して送信してください。
${editUrl}
`
GmailApp.sendEmail(mail, title, body);
}
///////////////////////////
//お断りメール(1)
//////////////////////////
function rejectionMail(mail, user, num, editUrl) {
const title = "【重要】お弁当のご注文を承れませんでした"
let body;
if (num <= 0) {
body = `
${user} 様
ご注文ありがとうございます。
大変申し訳ございませんが、先に他のお客様のご注文が確定し、
残数が0になったため、${user}様の注文を承ることができませんでした。
またのご利用をお待ちしております。
`;
} else {
const time = Utilities.formatDate(new Date(), "JST", "yyyy/MM/dd HH:mm");
body = `
${user} 様
ご注文ありがとうございます。
大変申し訳ございませんが、先に他のお客様のご注文が確定したため
${user}様の注文を承ることができませんでした。
恐れ入りますが再度以下のリンクから注文をお願いします。
${editUrl}
なお、${time} 現在の注文可能数は【 ${num} 個】となっております。
`;
}
GmailApp.sendEmail(mail, title, body);
}
////////////////////////////
//お断りメール(2)小数やマイナスなど不正な数を送信
////////////////////////////
function invalidInputMail(mail, user, num, editUrl) {
const title = "【重要】お弁当のご注文を承れませんでした"
let body =`
${user} 様
入力された注文数【${num}】は承れません。
0~10の整数を入力ください。
編集用URL
${editUrl}
`
GmailApp.sendEmail(mail, title, body);
}
まとめ
フォーム送信データを、編集用URLを使って変更する方法を考えてみました。
編集用URL方式の最大の問題は、「利用者ごとに異なる入力上限をフォーム側で設定できない」ことです。
今回のシステムは「残数に応じてフォームの選択肢を動的変更する」設計ですが、編集用URLで過去の回答を修正する利用者だけ特別扱いすることはできません。
そのため、編集用URLと動的在庫制御は相性が悪いことがわかりました。
では自由記述にすれば解決するかというと、今度は別の問題が発生します。
自由記述では、利用者が残数を超える数や完売後の注文数を入力できてしまいます。そのため、送信後にGASで在庫チェックを行い、在庫不足ならお断りメールを送る運用になります。
しかし、編集送信(編集URL)の仕様上、GAS側で処理を中断(return)しても、Googleフォーム側の回答データ自体は更新されてしまいます。
つまり、
- ユーザーには「送信完了」と表示される
- フォームの回答シートにも変更後の数が保存される
- 集計シートだけは更新されない
という状態が発生します。
結果として、「フォーム上では7個注文になっているのに、集計シートでは5個のまま」といったデータのねじれ(齟齬)が発生し、現場の混乱を招く可能性があります。
以上の理由に加え、せっかく実装した「回答の検証」や選択肢による入力制限を手放してしまうのは惜しいという思いもあり、今回のお弁当システムでの採用は見送りました。
ただ、getEditResponseUrl() 自体はとても便利な機能です。今回のような在庫管理との組み合わせには向きませんでしたが、用途によっては強力な武器になるので、また別の機会に活用してみたいと思います。



