Apps Scriptで納期前日に未完了案件を通知する方法

PCを使う人と色分けされた表、カレンダー・封筒・ベルの記号 デジタルツール
翌日納期を知らせるイメージ。受け取った後の連絡先と、結果を残す欄まで決めておく。

納期の前日にメールを送れても、完了した案件や宛先の違う案件まで含まれていれば、かえって連絡が増えてしまいます。Apps Scriptで通知を作る際は、先に案件ID、納期、進捗、通知先、通知済み日をそろえましょう。この記事では、暦日の翌日が納期で、指定した未完了状態の案件を宛先別にまとめるコードと、導入前に試す手順を紹介します。

対象は、スプレッドシートの納期一覧を朝の連絡に使いたい生産管理担当者です。自動通知を付ける前に、どの日付を使い、誰が進捗を更新するかを決めておくと、メールを受けた後の行動までつなげやすくなります。冒頭の表で今の状態に近いものを選び、入力のばらつきが残る場合は、列と選択肢の見直しから進めてください。

状況判断最初の対応次に調べること
納期や進捗の入力が統一されていないコード作成より先に入力ルールをそろえる必須列と進捗の選択肢を決める納期が日付として入力されているか
翌日納期を毎朝把握したい宛先別の集約メールで始める翌日納期・指定の未完了状態・有効な通知先を対象にする受信後に誰が工程へ照会するか
同じ案件が複数回通知される当日の履歴を除外し、重複実行をロックで抑える送信処理後に履歴を記録し、失敗時は再実行前に調べる手動実行とトリガーが重なっていないか
メールが届かない対象条件と実行結果を分けて調べる納期、進捗、通知先、通知済み日を見る実行履歴、権限、送信制限の状況

通知対象は翌日納期・指定の未完了状態・有効な通知先

判定の中心は「翌日が納期」「指定した未完了の進捗」「有効な通知先」の3条件です。ただし、それだけでは案件の取り違えや同日再送を防げないため、コードでは案件IDと通知履歴も調べます。また、納期列に客先への出荷日と社内完成予定日を混ぜると、同じ通知でも受け手の意味が変わります。どちらを知らせる一覧なのか、運用を始める前に決めましょう。

サンプルの進捗は「未着手・進行中・確認待ち・完了・中止」です。空欄や別の表記を未完了と推測せず、通知から外してログへ残します。工程担当者は進捗と備考を更新し、生産管理担当者が朝に対象行を読む役割にすると、入力と対応を分担できます。なお、休日を除く処理は含みません。前営業日の連絡が前提の現場では、休日対応を追加するか手作業の連絡を組み合わせてから使ってください。

通知条件が決まったら、次の4点を運用ルールへ書きます。目的や時刻が曖昧なままだと、受け手が「読むだけの連絡」と受け取り、工程への照会が始まらないことがあるためです。メールの送信と、その後の対応を一続きで決めておきましょう。

  • 通知の基準日:案件一覧の「納期」列だけを使う
  • メールの目的:担当者へ照会するか、管理者が工程を把握するかを決める
  • 通知時刻:始業直前を避け、内容を読んで連絡できる時間帯にする
  • 納期変更時:変更後の納期前日に再通知するかを決める

ルールは、別の「運用ルール」シートなどへ置きます。このコードは1行目を列見出しとして使うため、案件一覧の先頭に説明行を挿入しないでください。通知を受ける人、工程担当者へ連絡する期限、返答を進捗と備考のどちらへ残すかも同じ場所へ書くと、引継ぎ時に使えます。

納期管理シートは見出し名と更新担当を固定する

案件一覧は、1行目に見出し、2行目以降にデータを置きます。下の表で併記した「品名・案件名」と「工程」は、それぞれ別の列です。コードは9つの見出し名から列を探すため、順番を変えられますが、名前の重複や不足がある場合は停止します。「通知済み日」は送信処理の後に記録する欄であり、相手の受信や対応完了を示す欄ではありません。

見出し主な更新担当通知での使い方
案件ID案件登録者案件を重複なく識別する
品名・案件名/工程(別々の列)案件登録者・工程担当者受信者が対象案件を特定する
納期生産管理担当者翌日かどうかを判定する
進捗工程担当者指定の未完了状態だけを対象にする
担当者生産管理担当者連絡先を判断する材料にする
通知先生産管理担当者メールを受ける宛先を指定する
通知済み日スクリプト当日の履歴がある行を送信対象から外す
備考更新担当者材料待ちなどの照会事項を載せる

案件IDは、同じ品名を繰り返し製作する場合も、どの依頼かを区別するために使います。サンプルでは、データがある行のIDが空欄、または重複していれば、メールを送る前に全体を停止します。登録時にもスプレッドシートで重複データを安全に見つける方法を使い、重複の理由を調べてから修正しましょう。別案件を削除して番号をそろえる方法にはしません。

納期は、見た目だけでなくシート上の日付として入力します。たとえば「2025/4/1」と表示されていても、文字列ならこのコードの対象から外れます。一方、進捗は入力規則で選択肢をそろえると、想定外の表記を減らせます。誰がどの欄を更新するかは、Googleスプレッドシートで作業者別の入力欄を分ける方法も参考にしてください。

翌日納期の未完了案件を宛先別に1通へ集約するコード

最初は、本番とは別の非公開の検証用スプレッドシートを用意し、そのファイルに紐づくApps Scriptへ貼り付けます。既存の関数や定数と名前が重複しないかも調べてください。設定のsheetNameを対象シート名に合わせ、進捗の選択肢を変える場合はシート側とコード側の両方へ反映します。スプレッドシートとスクリプトのタイムゾーンも、同じ地域の設定にそろえます。

const DUE_ALERT_CONFIG = {
  sheetName: '案件管理',
  subjectPrefix: '【要確認】翌日納期の未完了案件',
  completedStatuses: ['完了', '中止'],
  activeStatuses: ['未着手', '進行中', '確認待ち'],
  requiredHeaders: [
    '案件ID', '品名・案件名', '工程', '納期', '進捗',
    '担当者', '通知先', '通知済み日', '備考'
  ]
};

function sendDueTomorrowAlert() {
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(10000)) throw new Error('別の実行が進行中です。');
  try {
    const config = DUE_ALERT_CONFIG;
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    if (!spreadsheet) throw new Error('対象ブックに紐づけて実行してください。');
    const sheet = spreadsheet.getSheetByName(config.sheetName);
    if (!sheet) throw new Error('シートが見つかりません: ' + config.sheetName);
    const timezone = spreadsheet.getSpreadsheetTimeZone();
    if (Session.getScriptTimeZone() !== timezone) {
      throw new Error('ブックとスクリプトのタイムゾーンを同じ設定にしてください。');
    }

    const values = sheet.getDataRange().getValues();
    const index = Object.create(null);
    (values[0] || []).forEach((header, column) => {
      const name = String(header).trim();
      if (!name) return;
      if (index[name] !== undefined) throw new Error('見出しが重複しています: ' + name);
      index[name] = column;
    });
    const missing = config.requiredHeaders.filter(header => index[header] === undefined);
    if (missing.length) throw new Error('見出しが不足しています: ' + missing.join('、'));
    if (values.length < 2) return;

    const now = new Date();
    const todayText = Utilities.formatDate(now, timezone, 'yyyy/MM/dd');
    const parts = todayText.split('/').map(Number);
    const tomorrow = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2] + 1));
    const tomorrowText = Utilities.formatDate(tomorrow, 'UTC', 'yyyy/MM/dd');
    const grouped = Object.create(null);
    const ids = new Set();
    const isDate = value => value instanceof Date && !isNaN(value.getTime());
    const toText = value => String(value == null ? '' : value).trim();

    values.slice(1).forEach((row, offset) => {
      const rowNumber = offset + 2;
      if (row.every(value => toText(value) === '')) return;
      const id = toText(row[index['案件ID']]);
      if (!id) throw new Error('案件IDが空です。行: ' + rowNumber);
      if (ids.has(id)) throw new Error('案件IDが重複しています。行: ' + rowNumber);
      ids.add(id);
      const dueDate = row[index['納期']];
      const status = toText(row[index['進捗']]);
      const address = toText(row[index['通知先']]);
      const notified = row[index['通知済み日']];
      if (!isDate(dueDate)) {
        console.warn('納期が有効な日付ではありません。行: ' + rowNumber);
        return;
      }
      if (config.completedStatuses.includes(status)) return;
      if (!config.activeStatuses.includes(status)) {
        console.warn('進捗が空欄または定義外です。行: ' + rowNumber);
        return;
      }
      if (!/^[A-Za-z0-9.!#$%&'*+\/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?\.[A-Za-z]{2,}$/.test(address)) {
        console.warn('通知先は単独アドレスで入力してください。行: ' + rowNumber);
        return;
      }
      if (toText(notified) !== '' && !isDate(notified)) {
        console.warn('通知済み日に日付以外の値があります。行: ' + rowNumber);
        return;
      }
      const dueText = Utilities.formatDate(dueDate, timezone, 'yyyy/MM/dd');
      const notifiedText = isDate(notified)
        ? Utilities.formatDate(notified, timezone, 'yyyy/MM/dd') : '';
      if (dueText !== tomorrowText || notifiedText === todayText) return;

      if (!grouped[address]) grouped[address] = { items: [], rows: [] };
      grouped[address].items.push([
        id, row[index['品名・案件名']], row[index['工程']], dueText,
        status, row[index['担当者']], row[index['備考']]
      ]);
      grouped[address].rows.push(rowNumber);
    });

    const addresses = Object.keys(grouped);
    if (MailApp.getRemainingDailyQuota() < addresses.length) {
      throw new Error('今回の宛先数に対し、メール送信の残り割り当てが不足しています。');
    }
    addresses.forEach(address => {
      const group = grouped[address];
      const body = group.items.map(item =>
        '案件ID: ' + item[0] + '\n' +
        '品名・案件名: ' + item[1] + '\n' +
        '工程: ' + item[2] + '\n' +
        '納期: ' + item[3] + '\n' +
        '進捗: ' + item[4] + '\n' +
        '担当者: ' + item[5] + '\n' +
        '備考: ' + item[6]
      ).join('\n\n---\n\n');
      MailApp.sendEmail({
        to: address,
        subject: config.subjectPrefix + ' ' + group.items.length + '件',
        body: '翌日納期の未完了案件です。元の行と現場の状況を照合し、対応を決めてください。\n\n' + body
      });
      group.rows.forEach(rowNumber => {
        sheet.getRange(rowNumber, index['通知済み日'] + 1).setValue(now);
      });
      SpreadsheetApp.flush();
    });
  } finally {
    lock.releaseLock();
  }
}

同じ通知先の案件は1通にまとめ、宛先が違えば別メールにします。通知先は1行につき単独のアドレスを扱い、複数宛先や表示名付きの形式は除外します。ただし、形式が正しくても送る相手が正しいとは限りません。検証用ファイルでは全行の宛先を自分の受信できるアドレスへ変え、IDの不備による全体停止と、日付・進捗・宛先の不備による行の除外をそれぞれ試しましょう。

コードはメール送信処理が戻った後に、対象行へ通知済み日を書きます。そのため、送信前に止まった行へ履歴を付けることは避けられますが、送信と記録を一括で確定する仕組みではありません。送信後に書き込みが失敗すれば、次の実行で同じ案件を再送する可能性があります。また、途中の宛先まで送れた場合もあるため、エラー後は案件ID、実行ログ、受信状況を照合してから再実行を判断してください。

メールには朝の工程連絡に使う項目を載せる

件名は「【要確認】翌日納期の未完了案件 3件」のように、対象と件数を示します。本文には案件ID、品名・案件名、工程、納期、進捗、担当者、備考を載せます。対象が0件ならメールを送らないため、届かないことだけで問題がないとは言えません。朝の担当者は元シートと実行履歴も開き、処理が動いたか、除外された行がないかを読みましょう。

備考には「外注戻り照会中」「材料入荷予定を照会」のように、次に行う連絡を短く書きます。通知を受けた担当者は案件IDから元の行へ戻り、工程担当者の返答を進捗や備考へ残してください。返答をもらったことと、品物が完成したことは別です。完了へ変える条件と更新者を決め、回答待ちのまま完了扱いにしない運用にしましょう。

手動テスト後に時間主導型トリガーを設定する

検証は、本番一覧の一部だけを使うより、通知先をすべて自分にした別ファイルで行うと試験の範囲を限定できます。このコードに試験専用の送信停止モードはありません。自動実行はまだ登録せず、初回に表示されるシート操作やメール送信の権限を読み、継続運用を担うアカウントで手動実行してください。共有相手のデータや実際の通知先を試験用に残さないことも先に点検します。

手動実行から自動実行へは、次の順序で進めます。正常な行だけで終わらせず、完了、日付の不備、IDの重複、同日の再実行も試してください。さらに、送信後の記録に失敗した場合に誰が調べるかを決めておくと、本番で単純に再実行してしまうことを防げます。

  • 翌日納期・進行中・有効な通知先のテスト行を1件作る
  • sendDueTomorrowAlertを手動実行する
  • メールの案件ID、納期、進捗、備考を元の行と照合する
  • 通知済み日に当日の日時が記録されたかを調べる
  • 同日にもう一度実行し、同じ案件が再送されないかを調べる
  • 除外・停止の条件も試した後、対象関数を毎日実行する時間主導型トリガーを設定する

時間主導型トリガーは、指定した時刻ちょうどに動くとは限りません。始業直前を避け、内容を読んで連絡できる時間帯に設定しましょう。実行にはトリガー作成者の権限が使われるため、担当交代時には旧担当者のトリガー停止と後任者の設定を引き継ぎます。他のアカウントが作ったトリガーは後任者から見えない場合があるので、一覧にないことだけで停止済みと扱わないでください。

通知済み日で同日再送を抑え、納期変更の扱いを決める

通知済み日が当日の行は除外するため、通常の再実行では同日分を送らなくなります。同じプロジェクトの重複実行はロックでも抑えますが、シートを人が編集・並べ替えする操作や、別プロジェクトの処理は止められません。読み取りから履歴の書き込みまで、対象一覧を変更しない時間帯で運用してください。また、当日に納期や宛先を修正しても、この履歴がある行は再送されません。

再通知のルールは、何をもって「同じ通知」とするかから決めます。この例では、過去日の履歴がある案件でも、新しい納期の前日に再び対象になります。一度知らせた案件は今後送らないなら案件単位の履歴、案件と納期の組合せごとに送るなら両者を記録する履歴が求められます。「通知した納期」の列を足すだけで、あらゆる再通知を防げるわけではありません。変更理由と日時も残し、コードを変える前に期待する動きを書き出しましょう。

メール未着と日付ずれは対象行と実行履歴を分けて調べる

メールが来ない場合は、まず案件IDと元の行を調べます。文字列の納期、空欄や想定外の進捗、宛先の不備、当日の通知履歴は、それぞれ通知されない理由になります。履歴欄に日付以外の値が入った場合も、送ったかどうかを推測せず除外します。担当者はログと入力値を照合し、調べた時刻、案件ID、修正内容を運用記録へ残してください。

行の条件が合っているなら、実行履歴で関数が動いたか、どの段階で止まったかを調べます。サンプルは、シートとスクリプトのタイムゾーンが一致しなければ送信前に停止します。また、残りのメール割り当てを宛先数と比べますが、その値は常に固定ではありません。宛先をまとめても利用上限や権限による失敗は起こり得るため、エラーの内容に沿って対処しましょう。

導入時のテストケースと毎朝の点検表

下の表は、導入時に試す条件と記録欄の例です。「可」という記載は、このファイルで試験が済んだ意味ではありません。生産管理担当者が検証用のデータで実行し、実行日、担当者、可否、修正内容を残します。ID・見出しの重複やタイムゾーンの不一致で送信前に止まること、想定外の進捗がログへ出ることも加えて試してください。

テスト条件期待する結果結果の記入例
翌日納期・進行中・通知先あり該当宛先のメールに掲載され、通知済み日が記録される可/メールとシートを照合
翌日納期・完了メールに掲載されない可/除外結果を照合
当日納期または翌々日納期メールに掲載されない可/日付判定を照合
納期空欄・文字列・通知先空欄送信せず、ログに除外理由が出る要修正/行番号を記録
同日に2回実行2回目は同じ案件を送らない可/通知済み日を照合
納期変更後に新しい前日を迎える履歴が過去日なら再通知、当日なら除外される可/備考とメールを照合

運用後は毎朝、メール件数だけでなく実行の成功・失敗と除外ログを読み、元の一覧へ戻ります。月に一度は、担当者や宛先、トリガー作成者が変わっていないかも見直しましょう。効果を見る際は、再発防止策の効果を見直す時期と6項目|有効性の判定法の考え方も使えます。送信数の増加ではなく、連絡漏れや工程対応の遅れがどう変わったかを記録するためです。

よくある質問

導入後は、休日や納期変更のように、コードだけでは決められない場面が出てきます。次の回答は、サンプルが行う処理と、自社で追加する運用を分けたものです。扱いを変える際は、まず検証用ファイルで期待する結果になるか試してください。

納期が土日や自社休業日の翌日に当たる場合は、いつ通知しますか?

暦日の翌日だけを見るため、休日も同じように判定します。たとえば休業日の間に送られても担当者が読めないなら、このままでは連絡が間に合いません。休業日表を使って前営業日に知らせる処理を設けるか、休業前に対象期間の案件を人が洗い出す運用を併用しましょう。金曜日に休日中・休日明けの納期を読む場合も、誰がどこまで連絡するかを決めます。

納期を変更した案件だけ、もう一度通知するにはどうしますか?

変更後の納期の前日になり、通知済み日が過去日なら、サンプルでは再び通知されます。ただし、同日に通知した後で内容を変更しても、その日の再実行では送られません。変更直後の連絡を行いたいのか、次の前日通知だけでよいのかを先に決めましょう。再送を一切行わない場合も、案件単位の履歴へ変更するなど、別の判定が求められます。

担当者ごとに別メールを送りたい場合、案件一覧はどう分けますか?

担当者ごとに一覧を分割することなく、通知先が同じ行を1通へまとめられます。ただし、受信者が読んでよい案件だけをその宛先へ割り当ててください。初めは管理者への集約で試し、担当者宛てを追加する際に、住所録と一覧のメールアドレスを照合するとよいでしょう。サンプルの形式検査は、誤った相手への送信を防ぐ本人照合の代わりにはなりません。

メールを送ったのに通知済み日が書き込まれなかった場合はどうしますか?

送信後に履歴の書き込みが失敗している可能性があります。再実行すれば必ず正しく復旧するとは限らず、既に送った案件が再送されることもあります。実行ログ、受信メール、案件IDと履歴欄を照らし合わせ、どこまで送れたかを担当者が調べてください。そのうえで、再送する範囲と履歴の扱いを決め、対応内容を記録します。

参考にした公式情報

下記のGoogle公式資料では、時間主導型トリガー、MailApp、割り当てとエラーの扱いを調べられます。同じプロジェクト内の実行を重ねない処理は、LockServiceの公式資料(https://developers.google.com/apps-script/reference/lock/lock-service)も参照しました。ここで扱うのはサンプルの仕様と試験手順です。実際のメール配送、権限、時刻、シート書き込みは、導入先の検証用ファイルで試してください。

コードを変更するときも、宛先を置き換えた別ファイルで抽出と記録を試してから反映します。公式仕様を読んでも、自社の列名やデータ、共有権限まで正しいとは限らないためです。変更後は通常の処理だけでなく、除外と停止の条件も再試験し、トリガーを重複登録していないかを点検しましょう。

まとめ

最初に、通知で使う納期の意味、進捗の選択肢、受信者、休日と変更時の扱いを決めます。その後に案件IDと履歴を含む一覧を作り、別ファイルで抽出、送信、記録、同日の再実行を順に試してください。正常なメールが届くことに加え、不備がある行を送らず、全体停止の理由を読めることまでが導入前の試験になります。

運用を始めたら、朝の担当者が実行履歴と元の案件を読み、入力漏れや工程への連絡を進めます。月次では担当者と宛先、休日・納期変更のルールを見直しましょう。通知は、工程の完了や納期の妥当性を判断する機能ではありません。届いた情報を誰が行動へ移すかまで決めることで、一覧の更新と日々の対応をつなげられます。

タイトルとURLをコピーしました