今までの記事一覧
Googleフォーム×GASで実現する業務自動化シリーズ全記事まとめ目次
前提
この記事は、フォーム回答を保存しているスプレッドシート側のGASを前提にしています。
トリガーは以下を設定してください。
- 実行する関数:
onFormSubmit - イベントのソース:スプレッドシートから
- イベントの種類:フォーム送信時
スプレッドシート構成
スプレッドシート「集計シート」をこの図のように設定しています。
- A1:D6 セル:商品マスタ(商品コードや現在の残数が入っている)
- 11行目以下:フォームから送信された注文の受付内容が記録される
- 図の赤字の内容の数式が入っている
※以前作ったこの記事の数式をそのまま生かしています。ただし、変更フォームによって在庫不足による注文数の修正が発生したときのために、有効注文数の数式をほんの少しだけパワーアップさせ=IF(ISNUMBER(修正数のセル), 修正数のセル, 注文数のセル)としておきます。こうすることで、『修正数』の欄に修正後の確定数を書き込むだけで、有効注文数も、商品マスタの残数も、シート側で全自動で連動して計算されるようになります。
おさらい
これまで作り続けてきた「お弁当注文」システム、商品マスタを使って注文と突合させる・残数をフォームにも自動反映させる・変更フォームからの注文変更に対応させる・受付メール送信 と、ひととおりの機能を実装しました。
ここで今回はちょっと寄り道して、「売り切れになった商品はフォームから消してしまう」に挑戦してみます。
今日のミッション
- 今までは、売り切れになってもフォーム上で「残数0」「プルダウンの選択肢も0だけになる」だったが、質問そのものをフォーム上から見えなくしたい。
今回のキモはコレ!
質問そのものをフォームから消すのなら deleteItem() メソッドを使えば簡単。でも、消してしまったら、商品の在庫が復活したときに戻すのが大変、戻しても質問IDが変わってしまうためさらに面倒です。
そこで、フォームの末尾に新しいセクションを作ってここに質問アイテムを移動させるようにしてみます。
売り切れセクションを作る
フォームの最後の質問アイテムをクリックすると、図のように、右サイドにメニューが出ます。そこから「セクションを追加」ボタンをクリックします。

新しく作ったセクションは「売り切れ商品」とでも名付けておくといいでしょう。
そしてここが重要!
先ほどの質問アイテムの下に「セクション1以降 ▼」というのが出ていると思います。これをクリックして「フォームを送信」を選択します。(デフォルトでは「次のセクションに進む」になっていると思います)

1つ目のセクションで注文内容を入力した後は、次のセクションに行かずにフォーム送信するように設定します。つまり、ユーザーは「売り切れセクション」へ進むことができません。そのため、このセクションへ移動した商品は、ユーザーからは見えなくなります。
GASコード
まず、商品マスタ(itemMaster)に保存してある質問IDを取り出します。質問IDがわかれば、getItemById() を使ってフォームの質問アイテムを取得できます。
for (let col = itemMaster[0].length - 1; col >= 1; col--) {
const questionId = itemMaster[2][col]; //質問ID
const item = form.getItemById(questionId); //フォームから質問アイテム取得
for を逆順に回しているのは、質問アイテムを移動するときに順番が崩れないようにするためです。(後述)
質問アイテムの移動先はフォームの最後尾にします。
const hiddenIndex = form.getItems().length - 1; //フォームの最後尾
そして moveItem() メソッドを使って、在庫が0の場合は質問アイテムをフォームの最後尾に移動させます。Googleフォームでは、アイテムを最後尾へ移動すると、最後のセクション内に入ります。
在庫があるものはフォームの3番目の質問(「メールアドレス」「氏名」の後)に移動させます。
if (itemMaster[5][col] === 0) { //在庫が0だったら
form.moveItem(item, hiddenIndex); //そのアイテムをフォームの最後尾(つまり最後のセクション=売り切れセクション内)に移動させる
} else { //在庫があれば
form.moveItem(item, 2); //そのアイテムをフォームのアイテムの3番目に移動させる
}
この方法では、フォームの最後のセクションを「売り切れ商品」専用として利用します。
そのため、フォームの構成を変更する場合でも、
最後のセクションは必ず「売り切れセクション」 にしてください。
※for の逆順の理由
商品マスタ上の並び順「ハンバーグ」「唐揚げ」「焼き魚」どおりにフォームに並べるとすると、後ろの商品から順番に「3番目」に入るため、在庫があった場合にこのように意図どおりに並べることができるためです。
1巡目:「メールアドレス」「氏名」「焼き魚」
2巡目:「メールアドレス」「氏名」「唐揚げ」「焼き魚」
3巡目:「メールアドレス」「氏名」「ハンバーグ」「唐揚げ」「焼き魚」
GASコード全文
今回も、保守性を考えこのようにサブ関数にしました。これを、今まで作ったサブ関数 updateFormOptions から呼び出す形とします。
function moveSoldoutItem(itemMaster,form) {
const hiddenIndex = form.getItems().length - 1; //フォームの最後尾
//商品マスタ上の質問IDを逆順(右から左)に処理
for (let col = itemMaster[0].length - 1; col >= 1; col--) {
const questionId = itemMaster[2][col]; //質問ID
const item = form.getItemById(questionId); //フォームから質問アイテムを取り出す
if (itemMaster[5][col] === 0) { //在庫が0だったら
form.moveItem(item, hiddenIndex); //そのアイテムをフォームの最後尾(つまり最後のセクション=売り切れセクション内)に移動させる
} else { //在庫があれば
form.moveItem(item, 2); //そのアイテムをフォームのアイテムの3番目に移動させる
}
}
}
動作確認
以下のような注文を送信してみます。
ハンバーグ:残数5 → 注文5 → 残数0
唐揚げ :残数2 → 注文2 → 残数0
焼き魚 :残数8 → 注文4 → 残数4
注文によって「ハンバーグ弁当」と「唐揚げ弁当」の残数が0になりました。
フォームの注文画面には「焼き魚弁当」だけが表示されています。下部には「送信ボタン」のみが表示され、次のセクションに移動できないようになっています。
フォームの管理画面を見ると、「ハンバーグ弁当」と「唐揚げ弁当」が残数0になったうえで「売り切れセクション」に移動しているのが確認できます。
ここで先ほど注文した人が、すべての商品を0に変更して送信してみます。残数はこの状態に戻るので、フォームでも在庫が復活しているはずです。
ハンバーグ:残数5
唐揚げ :残数2
焼き魚 :残数8
注文フォームを開くと、「ハンバーグ弁当」と「唐揚げ弁当」が戻っていました。順番も元どおりです。
フォーム管理画面です。売り切れセクション内の質問アイテムはカラになりました。
まとめ
今回は、複数商品対応編の番外編として、売り切れ商品をフォームに表示させない方法を試してみました。
単にアイテムを「削除」するのではなく、「フォームの最後尾にある隠しセクションに移動させ、途中で送信終了させることで見えなくする」 というアプローチを取ったことで、以下のメリットが生まれました。
- 質問IDの維持:在庫が復活した時も、元の位置(3番目)に戻すだけで復活できる。
- UI/UX改善:ユーザーにとっては「今、本当に注文できる商品だけ」が並び、売る側にとっては完売商品を自動で退避できる。
これで、「完売時にフォーム自体を閉じる」機能に加え、「一部の商品が完売したときにフォームの見た目をスマートに変形させる」という、実用的な仕組みが完成しました。
今までは商品の残数 0 の表示とプルダウン選択肢が 0 だけになる設計でしたが、商品そのものが画面上に出なくなるため、ユーザーにとってよりわかりやすいUIになったと思います。
今までの「残数0・選択肢0だけを表示する方式」と、今回の「売り切れ商品を非表示にする方式」は、運用に合わせて使い分けられます。サブ関数化しているため、必要に応じて機能のON/OFFも簡単に行えます。
今回の成果コード
売り切れ商品をフォームの売り切れセクションに移動するサブ関数
function moveSoldoutItem(itemMaster,form) {
const hiddenIndex = form.getItems().length - 1; //フォームの最後尾
//商品マスタ上の質問IDを逆順(右から左)に処理
for (let col = itemMaster[0].length - 1; col >= 1; col--) {
const questionId = itemMaster[2][col]; //質問ID
const item = form.getItemById(questionId); //フォームから質問アイテムを取り出す
if (itemMaster[5][col] === 0) { //在庫が0だったら
form.moveItem(item, hiddenIndex); //そのアイテムをフォームの最後尾(つまり最後のセクション=売り切れセクション内)に移動させる
} else { //在庫があれば
form.moveItem(item, 2); //そのアイテムをフォームのアイテムの3番目に移動させる
}
}
}
そのサブ関数を呼び出す側(フォームの選択肢&説明欄を最新化)
末尾近くの else節の最後に moveSoldoutItem(itemMaster,form); を入れてください。
function updateFormOptions(itemMaster) {
//手動更新する場合はitemMasterがないのでここで作成
if (!itemMaster) {
const sheet_summary = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("集計シート");
//シートから手動で更新した場合(スマホからも含む)は、念のためスプレッドシートを強制再計算させる
SpreadsheetApp.flush();
// B1から右方向のデータ終端(列数取得用)
const summaryLastCol = sheet_summary.getRange("B1").getNextDataCell(SpreadsheetApp.Direction.NEXT).getColumn();
// 上部で宣言された引数の変数(itemMaster)にそのままデータを代入する
itemMaster = sheet_summary.getRange(1, 1, 6, summaryLastCol).getValues();
}
// 1. 注文フォームをID指定で開く
const form = FormApp.openById("1******"); // ここに注文フォームの管理用IDを入れてください
const corForm = FormApp.openById("1######"); // ここに変更フォームの管理用IDを入れてください
// マスタの列数(B列〜最終列)を取得
const summaryLastCol = itemMaster[0].length;
const formDescription = ["【現在の残数状況】"];
let sumStockNum = 0; //商品全部の残数を足したもの
// 2. 商品ごとにループ処理(B列=インデックス1 から開始)
for (let col = 1; col < summaryLastCol; col++) {
const productName = itemMaster[0][col]; // 1行目:商品名
const questionId = itemMaster[2][col]; // 3行目:フォームの質問ID
const stockNum = Number(itemMaster[5][col]); // 6行目:最新の残数
// 質問IDが空欄の場合はスキップ(エラー防止)
if (!questionId) continue;
try {
sumStockNum += stockNum; //商品全部の残数を足していく
// 3. 質問IDを元に、フォームから対象のプルダウン(ListItem)を取得
const listItem = form.getItemById(questionId).asListItem();
// 4. 残数に応じた選択肢(choices)を動的に組み立てる
const choices = []; //フォームの選択肢を入れる配列
if (stockNum <= 0) {
// 在庫がない場合は「0」にする(万が一マイナスになっても表示は0)
choices.push("0");
} else {
// 在庫がある場合は 1 〜 残数 までの数字を選択肢にする
// (0個も選択肢に入れたい場合は i = 0 からスタート)
for (let i = 1; i <= stockNum; i++) {
choices.push(String(i));
}
}
// 5. フォームのプルダウン選択肢を更新
listItem.setChoiceValues(choices);
// 質問のタイトルやヘルプテキスト(説明欄)も更新したい場合はここで変更可能
listItem.setHelpText(`残りあと ${stockNum} 個です`);
// 6. フォームタイトル下の説明欄を更新
formDescription.push(productName + ":" + stockNum);
} catch (e) {
Logger.log(`質問ID [${questionId}] の更新中にエラーが発生しました: ${e.message}`);
}
}
// フォームタイトル下の説明欄を書き換え
const formDescriptionText = formDescription.join("\n");
form.setDescription(formDescriptionText); //注文フォーム
corForm.setDescription(formDescriptionText + "\n ※元々のご自身の注文数にこの数を足したものが、注文可能数です。"); //変更フォーム
if (sumStockNum === 0) { //もし商品全部の残数(sumStockNum)が0だったらフォーム受付停止
if (form.isAcceptingResponses() === true) { // フォームがまだ受け付けていたら
form.setCustomClosedFormMessage("本日の注文受付は【完売】のため終了いたしました。またのご利用をお待ちしております!");
form.setAcceptingResponses(false); // 新規受付を停止
}
} else {
//フォームが受付停止になっていたら再開
if (form.isAcceptingResponses() === false) {
form.setAcceptingResponses(true);
}
//★★【サブ関数】売り切れ商品を売り切れセクションに移動
moveSoldoutItem(itemMaster,form);
}
return (formDescriptionText);
}






