今までの記事一覧
Googleフォーム×GASで実現する業務自動化シリーズ全記事まとめ目次
前提
この記事は、フォーム回答を保存しているスプレッドシート側のGASを前提にしています。
トリガーは以下を設定してください。
- フォーム送信時に実行する関数:
onFormSubmit - イベントのソース:スプレッドシートから
- イベントの種類:フォーム送信時
スプレッドシート構成
スプレッドシート「集計シート」をこの図のように設定しています。
- A1:D6 セル:商品マスタ(商品コードや現在の残数が入っている)
- 11行目以下:フォームから送信された注文の受付内容が記録される
- 図の赤字の内容の数式が入っている
※以前作ったこの記事の数式をそのまま生かしています。ただし、変更フォームによって在庫不足による注文数の修正が発生したときのために、有効注文数の数式を少しだけ変更し=IF(ISNUMBER(修正数のセル), 修正数のセル, 注文数のセル)としておきます。こうすることで、『修正数』の欄に修正後の確定数を書き込むだけで、有効注文数も、商品マスタの残数も、シート側で全自動で連動して計算されるようになります。
おさらい
前回は、フォーム注文の集計の日次管理として、1日の終わりに日次集計用の「集計シート」をリセットし、「注文履歴」シートにデータを蓄積すると同時に、フォームの残数も自動でリセットする処理を作りました。
あとは毎日決まった時間にこれらの処理を行う設定をするだけです。
今回のミッション
- 毎朝決まった時間にフォームの受付を開始する
- その後、決まった時間にフォームの受付を停止する
- 合わせて、決まった時間に集計シートをリセットする
「決まった時間」ならトリガーを使えばいいですね。
「時間主導型」の「特定の日時」を選べば、分単位で時間指定をすることができます。
ただこの「特定日時」トリガーの弱点は、 ワンタイム(1回限り) であることです。つまり、指定した日時に発動したらそこで終わり。次の日は別のトリガーを作らないと動いてくれません。
そこで、毎日GASを使ってこの日時指定のトリガーを動的に作成するようにしましょう。トリガーを毎日自動で作り続ければ、毎日決まった時間に動かすことができますね。
今回のキモはコレ!
トリガーに設定する時刻を作る
まずはこれで現在日時を取得します。
let time = new Date(); //今の日時
この日時に、設定したい「時間」「分」を上書きします。
time.setHours(8); //8時に書き換え
time.setMinutes(30); //30分に書き換え
これで変数 time の中には、今の年月日・8時・30分・今の秒・今のミリ秒 が入ることになります。
トリガーを作る構文
今作った time をこの構文の at() の引数に入れます。
これで、今日の8時30分に "動かしたい関数" が発動するトリガーが作成されます。
ScriptApp.newTrigger("動かしたい関数").timeBased().at(time).create();
秒も入れたい場合は time.setSeconds(0); を追加することもできますが、以下の実験記事のとおり、、Apps Script の時間トリガーは秒単位での実行を保証していません。
ScriptApp.at() の秒指定は意味がある?実際に検証してみた
トリガーを削除する
この、日時指定のトリガーのもう1つの注意点は、1回限りの使い捨てなのに発動後もこのように「無効」としてトリガー管理画面に残り続けてしまうことです。
このままにしておくと画面が見づらいだけでなく、次々にトリガーが作られた結果トリガー作成上限(1プロジェクトあたり20個)に達してしまい、新しいトリガーを作ることができなくなってしまいます。
そのため、無効になったトリガーはこまめに消すことが重要です。新しいトリガーを作る前に、このコードを使って削除をするといいでしょう。
//プロジェクトのトリガーを全取得し、紐づいている関数名が「動かしたい関数」だったら削除
const triggers = ScriptApp.getProjectTriggers();
for (const trigger of triggers) {
if (trigger.getHandlerFunction() === "動かしたい関数") {
ScriptApp.deleteTrigger(trigger);
}
GASコード
朝8:30に、注文フォームと変更フォームを受付開始する場合
//グローバルスコープ
const form = FormApp.openById("1*******"); // 注文フォームのID
const corForm = FormApp.openById("1#######"); // 変更フォームのID
////////////////////////////////
//トリガー作成(8:30にフォームをオープンするトリガー)
////////////////////////////////
function makeTrigger_open() {
//それぞれのトリガーを取り出して、紐づいている関数名が「formOpen」だったら削除
const triggers = ScriptApp.getProjectTriggers();
for (const trigger of triggers) {
if (trigger.getHandlerFunction() === "formOpen") {
ScriptApp.deleteTrigger(trigger);
}
}
//新しく「今日の8:30」に発動するトリガー作成
let time = new Date(); //今の日時
time.setHours(8); //8時に書き換え
time.setMinutes(30); //30分に書き換え
ScriptApp.newTrigger("formOpen").timeBased().at(time).create();
}
////////////////////////////////
//注文フォームと変更フォームをオープン
////////////////////////////////
function formOpen() {
//注文フォームの受付開始する
form.setAcceptingResponses(true);
//変更フォームの受付開始する
corForm.setAcceptingResponses(true);
}
11:20に注文フォームと変更フォームを受付停止する場合
////////////////////////////////
//トリガー作成(11:20にフォームをクローズするトリガー)
////////////////////////////////
function makeTrigger_close() {
const triggers = ScriptApp.getProjectTriggers();
//それぞれのトリガーを取り出して、紐づいている関数名が「formClose」だったら削除
for (const trigger of triggers) {
if (trigger.getHandlerFunction() === "formClose") {
ScriptApp.deleteTrigger(trigger);
}
}
//新しく「今日の11:20」に発動するトリガー作成
let time = new Date();
time.setHours(11);
time.setMinutes(20);
ScriptApp.newTrigger("formClose").timeBased().at(time).create();
}
////////////////////////////////
//注文フォームと変更フォームをクローズ
////////////////////////////////
function formClose() {
//注文フォームが受付中なら
if (form.isAcceptingResponses() === true) {
//メッセージを入れる
form.setCustomClosedFormMessage("本日の注文受付は終了いたしました。");
// 受付を停止する
form.setAcceptingResponses(false); // 受付を停止
}
//変更フォームが受付中なら
if (corForm.isAcceptingResponses() === true) {
corForm.setCustomClosedFormMessage("本日の注文受付は終了いたしました。");
corForm.setAcceptingResponses(false); // 受付を停止
}
}
それぞれ、当日のトリガー設定時刻 time(8:30や11:20)までに makeTrigger_open makeTrigger_close を動かす前提です。
設定時刻を過ぎてからこの関数を動かすと、現在時刻の直後など、意図しない時刻にトリガーが設定されてしまいます。
動作確認
それでは実際に動かしてみましょう。
ここに、実行が終わって「無効」になったトリガーがあります。バインドされている関数は formOpen です。ここで手動で makeTrigger_open を動かしてみます。
「無効」の文字が消え、無事に新しいトリガーが設定されたことが分かります。関数 formOpen も紐づけられています。
※トリガー画面の何番目の行に入るかは実行タイミングによって異なります。
トリガーの内容(特定の日時・8:30)もきちんと設定されています。
運用
さて、出来上がった makeTrigger_open と makeTrigger_close ですが、作っただけでは自動では動きません。この関数自体もトリガー設定して初めて自動運転できます。
今回は、前回のこの記事(Googleフォーム×在庫管理!日次運用② 注文履歴へデータを退避して残数をリセットする)で作った日次集計もトリガー運用したいので、ここで作った関数 summaryCopy の中でまとめて呼び出す形にしましょう。
summaryCopy の最後にサブ関数として makeTrigger_open() と makeTrigger_close() を入れます。
function summaryCopy() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 1. 集計シートの注文データを注文履歴シートにコピー
//集計シートの最終行と最終列取得し、注文内容を変数dataに入れる
const sheet_summary = ss.getSheetByName("集計シート");
const summaryLastRow = sheet_summary
.getRange(sheet_summary.getMaxRows(), 1)
.getNextDataCell(SpreadsheetApp.Direction.UP)
.getRow();
//11行目以降に注文データがある場合(注文がある場合)
if (summaryLastRow >= 11) {
const summaryLastCol = sheet_summary.getLastColumn();
const dataRange = sheet_summary.getRange(11, 1, summaryLastRow - 10, summaryLastCol);
const data = dataRange.getValues();
//注文履歴シートの最終行以降に変数dataの内容を入れる
const sheet_history = ss.getSheetByName("注文履歴");
const historyLastRow = sheet_history.getLastRow();
sheet_history.getRange(historyLastRow + 1, 1, data.length, data[0].length).setValues(data);
// 2. 集計シートの注文データを消去する
dataRange.clearContent();
}
// 3. フォームの残数表示・受付状態をリセット
//以前作った【サブ関数】(引数を省略すると、自動で itemMaster を取得するように作ってある)
updateFormOptions();
// 4. ★★【サブ関数】フォーム開始トリガーをセット
makeTrigger_open();
// 5. ★★【サブ関数】フォーム停止トリガーをセット
makeTrigger_close();
}
summaryCopy 自体のトリガーを手動で次のように設定します。
「時間主導型」>「日付ベースのタイマー」>8時より前の時間(例:「午前0時~1時」)
これによって図のように、きれいなリレー形式で処理が自動接続されます。
- 日次処理
summaryCopy - フォーム開始/停止トリガー作成
makeTrigger_openmakeTrigger_close - フォーム開始/停止処理
formOpenformClose
まとめ
今回は、日次運用の仕上げとして、特定日時に発動するトリガーをGASで毎日動的に作成する仕組みを作りました。
作り続けるだけでは上限に達してパンクしてしまうため、使用済みの「無効」トリガーを自動で削除するクリーンアップ機能も組み込んでいます。
「注文履歴」シートへのデータ退避処理と組み合わせることで、毎日の注文管理を自動化する仕組みがこれで完成しました。ここまでお読みいただき本当にありがとうございました。
次回からはリファクタリング編として、これまで作ったコードの見直しを行いながら、サブ関数に細分化し、必要なオプションだけを柔軟に選択できるようなシステムへとブラッシュアップしていきたいと思います。
成果コード
メイン関数
※メイン関数(データの移動処理)のトリガーは「当日の午前0時~8時」の実行を想定しています。
もし前日夜などに動かしたい場合は、トリガー作成サブ関数の日付を翌日に設定(time.setDate(time.getDate() + 1))する必要があります。
//グローバルスコープ
const form = FormApp.openById("1*******"); // 注文フォームのID
const corForm = FormApp.openById("1#######"); // 変更フォームのID
////////////////////////////////
//その日のデータを履歴シートにコピーして、毎日の集計シートはリセット
//午前0時以降、フォーム開始時刻までの時間にトリガー設定すること
////////////////////////////////
function summaryCopy() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 1. 集計シートの注文データを注文履歴シートにコピー
//集計シートの最終行と最終列取得し、注文内容を変数dataに入れる
const sheet_summary = ss.getSheetByName("集計シート");
const summaryLastRow = sheet_summary
.getRange(sheet_summary.getMaxRows(), 1)
.getNextDataCell(SpreadsheetApp.Direction.UP)
.getRow();
//11行目以降に注文データがある場合(注文がある場合)
if (summaryLastRow >= 11) {
const summaryLastCol = sheet_summary.getLastColumn();
const dataRange = sheet_summary.getRange(11, 1, summaryLastRow - 10, summaryLastCol);
const data = dataRange.getValues();
//注文履歴シートの最終行以降に変数dataの内容を入れる
const sheet_history = ss.getSheetByName("注文履歴");
const historyLastRow = sheet_history.getLastRow();
sheet_history.getRange(historyLastRow + 1, 1, data.length, data[0].length).setValues(data);
// 2. 集計シートの注文データを消去する
dataRange.clearContent();
}
// 3. フォームの残数表示・受付状態をリセット
//【サブ関数】(引数を省略すると、自動で itemMaster を取得するように作ってある)
updateFormOptions();
// 4. ★★【サブ関数】フォーム開始トリガーをセット
makeTrigger_open();
// 5. ★★【サブ関数】フォーム停止トリガーをセット
makeTrigger_close();
}
※サブ関数 updateFormOptions は、今までと変更ないため、省略します。
サブ関数:フォーム開始トリガー作成 & 開始処理
不要な旧トリガー(formOpen / formClose)の削除処理は、ここで一括して行うことで処理時間を短縮しています。
※メイン関数を当日中に動かす場合は、トリガーは「明日」の8:30に設定する必要があるため、変数timeの設定で time.setDate(time.getDate() + 1); の1行を足してください。
////////////////////////////////
//トリガー作成(翌日の8:30にフォームをオープンするトリガー)
////////////////////////////////
function makeTrigger_open() {
//それぞれのトリガーを取り出して、紐づいている関数名が「formOpen」「formClose」だったら削除
const triggers = ScriptApp.getProjectTriggers();
for (const trigger of triggers) {
if (trigger.getHandlerFunction() === "formOpen" || trigger.getHandlerFunction() === "formClose") {
ScriptApp.deleteTrigger(trigger);
}
}
//「今日の8:30」に発動して【サブ関数】formOpenを動かすトリガー作成
let time = new Date(); //今の日時
//time.setDate(time.getDate() + 1); //翌日に動かしたいとき
time.setHours(8); //8時に書き換え
time.setMinutes(30); //30分に書き換え
ScriptApp.newTrigger("formOpen").timeBased().at(time).create();
}
////////////////////////////////
//注文フォームと変更フォームをオープン
////////////////////////////////
function formOpen() {
//注文フォームの受付開始する
form.setAcceptingResponses(true);
//変更フォームの受付開始する
corForm.setAcceptingResponses(true);
}
サブ関数:フォーム停止トリガー作成 & 停止処理
不要な旧トリガー(formOpen / formClose)の削除処理は、ここではなく makeTrigger_open で一括して行うことで処理時間を短縮しています。
※メイン関数を当日中に動かす場合は、トリガーは「明日」の8:30に設定する必要があるため、変数timeの設定で time.setDate(time.getDate() + 1); の1行を足してください。
////////////////////////////////
//トリガー作成(11:20にフォームをクローズするトリガー)
////////////////////////////////
function makeTrigger_close() {
const triggers = ScriptApp.getProjectTriggers();
//「今日の11:20」に発動して【サブ関数】formCloseを動かすトリガー作成
let time = new Date();
//time.setDate(time.getDate() + 1); //翌日に動かしたいとき
time.setHours(11);
time.setMinutes(20);
ScriptApp.newTrigger("formClose").timeBased().at(time).create();
}
////////////////////////////////
//注文フォームと変更フォームをクローズ
////////////////////////////////
function formClose() {
//注文フォームが受付中なら
if (form.isAcceptingResponses() === true) {
//メッセージを入れる
form.setCustomClosedFormMessage("本日の注文受付は終了いたしました。");
// 受付を停止する
form.setAcceptingResponses(false); // 受付を停止
}
//変更フォームが受付中なら
if (corForm.isAcceptingResponses() === true) {
corForm.setCustomClosedFormMessage("本日の注文受付は終了いたしました。");
corForm.setAcceptingResponses(false); // 受付を停止
}
}
