業務自動化

入札漏れ防止!GASで公共工事の締切を自動管理する方法

建設 の業務自動化レシピ
業種建設
ツールGoogle Apps Script (GAS)
難易度★★☆ 中
設定時間約 90 分

公共工事の入札締切を「誰かの記憶」に頼っていませんか?Google Apps Script(GAS)を使えば、入札日・書類締切をGoogleカレンダーに自動登録し、担当者へリマインドメールを自動送信できます。本記事では、工務店の入札管理を属人化から解放する具体的なレシピを、コード付きで解説します。

読了時間: 約 20 分(コードのコピーと貼り付けを含む)

この記事のポイント

編集長の見解
入札管理の失敗は、単なる「うっかり」では済みません。公共工事の入札機会を失うことは、編集部のシミュレーションでは数千万円規模の売上機会を失う可能性があります(実際の金額は案件規模により異なります)。本レシピは、こうしたリスクを「仕組み」で減らす方法です。担当者が休暇中でも、締切をチームで確認できる状態をつくります。


📋 入札管理の現状と課題

公共工事の入札は、中小工務店にとって重要な売上機会です。しかし、管理が担当者の記憶に依存している現場もあります。

手作業による入札管理の3つのリスク

入札スケジュールを手作業で管理している工務店には、次のリスクがあります。

  1. 締切漏れによる入札機会の喪失:書類提出の締切を過ぎると、その入札に参加できません。1件の漏れが売上計画に影響する場合があります。
  2. 書類不備による失格:必要書類が1点でも欠けると、参加できない場合があります。公告を読み、書類をそろえるには注意が必要です。
  3. 担当者依存による属人化:「あの案件は担当者が把握している」という状態では、休暇や異動時に管理が止まります。経営者が全体を把握しにくい点も課題です。

中小工務店が抱える入札管理の負担

大手ゼネコンには、入札管理を専門に行う部署やシステムがあります。一方、従業員10〜30名規模の工務店では、現場監督や事務員が本業の合間に対応するケースも少なくありません。

失敗事例:締切漏れで失った大型工事
編集部が複数の工務店への取材で確認した事例を一般化すると、常務が「〇〇市の学校改修工事の入札は、来週の水曜日」と記憶していたものの、実際の締切は火曜日だったケースがあります。担当者が確認した時には締切を過ぎており、数千万円規模の工事に参加できませんでした。実際の案件名や企業名は、守秘のため掲載していません。

本レシピで解決できること

本記事で紹介するレシピは、次の3業務を自動化します。

自動化する業務従来の方法本レシピ適用後
入札日・締切日の把握紙のカレンダーやExcelに手入力スプレッドシートに入力
カレンダーへの登録手作業でGoogleカレンダーに入力自動登録(重複防止付き)
担当者へのリマインド上司の声かけ、担当者の自己管理締切3日前・前日に自動メール

次のセクションでは、使うツールと全体の流れを整理します。


🎯 レシピの全体像

全体フローを3行で要約

本レシピは、次の3ステップで動きます。

  1. 入力:案件名、発注機関、入札日、書類締切日、担当者をスプレッドシートに入力します。
  2. 自動処理:GASが毎日スプレッドシートを確認し、カレンダー登録とメール送信を実行します。
  3. 確認:担当者はメールで締切を確認し、経営者は台帳で全体状況を把握します。

必要なもの

必要なもの用途コスト
GoogleアカウントGAS、Gmail、Googleカレンダーの利用無料プランあり
Googleスプレッドシート入札情報の管理台帳無料
Gmailリマインドメールの送信無料プランあり

所要時間と難易度

項目目安
所要時間約90分(コードのコピーと貼り付け、設定を含む)
難易度中級
必要なスキルスプレッドシートの基本操作、コピーと貼り付け

💡 プログラミング未経験でも進めやすい
コードはコピーと貼り付けで使えます。ただし、日付形式とトリガーの設定は正確に行ってください。操作前に、元のスプレッドシートを複製しておくと安心です。

次のセクションでは、入札管理用スプレッドシートを作成します。


🔧 事前準備:スプレッドシートとGASのセットアップ

入札管理用スプレッドシートの作成

次の手順で入札情報を管理するスプレッドシートを作成します。

  1. Googleドライブを開き、「新規」→「Googleスプレッドシート」を選択します。
  2. ファイル名を「入札管理台帳」に変更します。
  3. 1行目に見出し行を作成します。次の7列を設定してください。
列列名入力例説明
A案件名〇〇市立小学校 校舎改修工事入札案件の名称
B発注機関〇〇市役所 財政課発注者(自治体・官公庁)
C入札日2026/08/20入札(開札)の日付
D書類締切日2026/08/10提出書類の締切日
E担当者田中 健一担当者名
F担当者メールtanaka@example.comメール送信先
Gステータス書類準備中進捗状況
HイベントID自動入力重複登録防止用

⚠️ 日付は「日付形式」で入力してください
入札日と書類締切日は、スプレッドシートの表示形式を「日付」に設定します。文字列として入力すると、GASが日付として認識できない場合があります。

日付列の表示形式設定

  1. 列Cと列Dを選択します。
  2. メニューから「表示形式」→「数字」→「日付」を選択します。
  3. 「2026/08/20」のような日付として表示されることを確認します。

ステータス列の設定(任意)

ステータス列をプルダウン形式にすると、入力ミスを減らせます。

  1. 列Gのセルを選択します。
  2. メニューから「データ」→「データの入力規則」を選択します。
  3. 次の項目を選択肢として登録します。
公告確認済
書類準備中
書類提出済
入札完了
失格・辞退

Google Apps Scriptのエディタを開く

  1. スプレッドシートのメニューから「拡張機能」→「Apps Script」を選択します。
  2. 新しいタブでApps Scriptのエディタを開きます。
  3. プロジェクト名を「入札管理自動化」に変更します。

スプレッドシートとGASの連携設定

GASからスプレッドシートを操作するため、スプレッドシートIDを取得します。

  1. スプレッドシートのURLを確認します。URLは次の形式です。
https://docs.google.com/spreadsheets/d/【スプレッドシートID】/edit

💡 スプレッドシートIDの見つけ方
URLの「/spreadsheets/d/」と「/edit」の間にある英数字の文字列がIDです。後述のコードで使うため、控えておいてください。

次のセクションでは、カレンダーへの自動登録を実装します。


📅 カレンダー自動登録の実装

専用カレンダーの作成

入札管理用に専用のGoogleカレンダーを作成すると、予定を確認しやすくなります。

  1. Googleカレンダーを開きます。
  2. 「他のカレンダー」の横にある「+」をクリックします。
  3. 「新しいカレンダーを作成」を選択します。
  4. カレンダー名を「入札管理」に設定して作成します。

作成したカレンダーのIDは、次の手順で確認できます。

  1. カレンダーリストの「入札管理」にある3つの点をクリックします。
  2. 「設定と共有」を選択します。
  3. 「カレンダーの統合」まで移動し、「カレンダーID」をコピーします。

GASコード:カレンダー自動登録

次のコードをApps Scriptのエディタにコピーして使います。

// 入札管理用カレンダー自動登録スクリプト

const SPREADSHEET_ID = '【スプレッドシートIDをここに入力】';
const CALENDAR_ID = '【カレンダーIDをここに入力】';
const SHEET_NAME = 'シート1';

const DATA_START_ROW = 2;
const COLUMN = {
  CASE_NAME: 1,
  ORDERING_ORG: 2,
  BID_DATE: 3,
  DEADLINE_DATE: 4,
  PERSON: 5,
  EMAIL: 6,
  STATUS: 7,
  EVENT_ID: 8,
};

function syncBidEventsToCalendar() {
  const sheet = getSheet();
  const rows = getBidData(sheet);
  const calendar = CalendarApp.getCalendarById(CALENDAR_ID);

  if (!calendar) {
    throw new Error(`カレンダーが見つかりません: ${CALENDAR_ID}`);
  }

  rows.forEach(row => {
    if (row.eventId) {
      return;
    }

    const bidEventId = createBidEvent(calendar, row);
    const deadlineEventId = createDeadlineEvent(calendar, row);

    if (bidEventId && deadlineEventId) {
      sheet
        .getRange(row.rowNumber, COLUMN.EVENT_ID)
        .setValue(`${bidEventId},${deadlineEventId}`);
    }
  });
}

function getSheet() {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = ss.getSheetByName(SHEET_NAME);

  if (!sheet) {
    throw new Error(`シート「${SHEET_NAME}」が見つかりません`);
  }

  return sheet;
}

function getBidData(sheet) {
  const lastRow = sheet.getLastRow();

  if (lastRow < DATA_START_ROW) {
    return [];
  }

  const lastColumn = COLUMN.EVENT_ID;
  const values = sheet
    .getRange(DATA_START_ROW, 1, lastRow - DATA_START_ROW + 1, lastColumn)
    .getValues();

  return values.map((row, index) => ({
    rowNumber: DATA_START_ROW + index,
    caseName: row[COLUMN.CASE_NAME - 1],
    orderingOrg: row[COLUMN.ORDERING_ORG - 1],
    bidDate: row[COLUMN.BID_DATE - 1],
    deadlineDate: row[COLUMN.DEADLINE_DATE - 1],
    person: row[COLUMN.PERSON - 1],
    email: row[COLUMN.EMAIL - 1],
    status: row[COLUMN.STATUS - 1],
    eventId: row[COLUMN.EVENT_ID - 1],
  }));
}

function createBidEvent(calendar, row) {
  if (!(row.bidDate instanceof Date)) {
    console.warn(`入札日が日付形式ではありません: ${row.caseName}`);
    return null;
  }

  const title = `【入札】${row.caseName}`;
  const description =
    `発注機関: ${row.orderingOrg}\n` +
    `担当者: ${row.person}`;

  const event = calendar.createAllDayEvent(title, row.bidDate, {
    description,
  });

  return event.getId();
}

function createDeadlineEvent(calendar, row) {
  if (!(row.deadlineDate instanceof Date)) {
    console.warn(`書類締切日が日付形式ではありません: ${row.caseName}`);
    return null;
  }

  const bidDate = row.bidDate instanceof Date
    ? Utilities.formatDate(row.bidDate, 'Asia/Tokyo', 'yyyy/MM/dd')
    : '未設定';

  const title = `【締切】${row.caseName}`;
  const description =
    `発注機関: ${row.orderingOrg}\n` +
    `担当者: ${row.person}\n` +
    `入札日: ${bidDate}`;

  const event = calendar.createAllDayEvent(title, row.deadlineDate, {
    description,
  });

  return event.getId();
}

⚠️ コード内の設定値を変更してください
コード冒頭の「SPREADSHEET_ID」と「CALENDAR_ID」を、ご自身の環境に合わせて変更します。変更せずに実行すると、エラーになる場合があります。

イベントの重複登録を防ぐ仕組み

H列のイベントIDを利用し、同じ案件の重複登録を防ぎます。

  1. 初回実行時:H列が空のため、カレンダーにイベントを登録します。
  2. 登録後:H列に2つのイベントIDを書き込みます。
  3. 次回実行時:H列に値があれば、その案件をスキップします。

コードの動作確認

  1. スプレッドシートにテストデータを1件入力します。
  2. Apps Scriptエディタで「syncBidEventsToCalendar」を選択します。
  3. 「実行」をクリックします。
  4. 初回は権限承認が求められるため、「権限を確認」→「許可」を選択します。
  5. Googleカレンダーにイベントが登録されたことを確認します。

💡 テストデータを整理しましょう
動作確認後は、テストデータを削除するか、H列のイベントIDを消去してイベントも削除してください。

次のセクションでは、担当者へのリマインドメールを設定します。


📧 リマインドメール自動送信の実装

カレンダー登録の次は、担当者へのリマインドメールを設定します。

リマインドメールの設計

タイミング目的送信条件
締切3日前書類準備を始める書類締切日の3日前
締切前日最終確認を促す書類締切日の前日

編集長の見解
3日前の通知で準備を始め、前日の通知で最終確認を行います。複数回の確認を仕組みに組み込むと、担当者だけに記憶を頼る状態を減らせます。

GASコード:リマインドメール自動送信

先ほどのスクリプトに、次のコードを追記してください。

function sendReminderEmails() {
  const sheet = getSheet();
  const rows = getBidData(sheet);

  const today = new Date();
  today.setHours(0, 0, 0, 0);

  rows.forEach(row => {
    if (!(row.deadlineDate instanceof Date)) {
      return;
    }

    const deadline = new Date(row.deadlineDate);
    deadline.setHours(0, 0, 0, 0);

    const diffDays = Math.round(
      (deadline - today) / (1000 * 60 * 60 * 24)
    );

    const status = row.status || '';

    if (
      diffDays === 3 &&
      !status.includes('リマインド3日前送信済')
    ) {
      sendReminderEmail(row, 3);
      updateStatus(row.rowNumber, 'リマインド3日前送信済');
    }

    if (
      diffDays === 1 &&
      !status.includes('リマインド前日送信済')
    ) {
      sendReminderEmail(row, 1);
      updateStatus(row.rowNumber, 'リマインド前日送信済');
    }
  });
}

function sendReminderEmail(row, daysBefore) {
  const email = row.email;

  if (!email) {
    console.warn(`担当者メールが設定されていません: ${row.caseName}`);
    return;
  }

  const subject = daysBefore === 3
    ? `【リマインド】${row.caseName} 書類締切まであと3日`
    : `【リマインド】${row.caseName} 書類締切は明日です`;

  const body = createReminderBody(row, daysBefore);

  MailApp.sendEmail({
    to: email,
    subject,
    body,
  });

  console.log(
    `リマインドメールを送信しました: ${email} (${row.caseName})`
  );
}

function createReminderBody(row, daysBefore) {
  const formattedDeadline = Utilities.formatDate(
    row.deadlineDate,
    'Asia/Tokyo',
    'yyyy年M月d日'
  );
  const formattedBidDate = Utilities.formatDate(
    row.bidDate,
    'Asia/Tokyo',
    'yyyy年M月d日'
  );

  const header = daysBefore === 3
    ? 'お疲れ様です。\n以下の入札案件について、書類締切が近づいています。\n'
    : 'お疲れ様です。\n以下の入札案件について、書類締切は明日です。\n';

  return `${header}
【案件名】${row.caseName}
【発注機関】${row.orderingOrg}
【書類締切日】${formattedDeadline}
【入札日】${formattedBidDate}

【チェックリスト】
□ 入札参加申込書の提出
□ 見積書の作成
□ 工事経歴書の準備
□ 配置予定技術者名簿の作成
□ その他必要な書類の確認

締切に間に合うよう、余裕を持ってご対応ください。
`;
}

function updateStatus(rowNumber, newStatus) {
  const sheet = getSheet();
  const currentStatus = sheet
    .getRange(rowNumber, COLUMN.STATUS)
    .getValue();

  const updatedStatus = currentStatus
    ? `${currentStatus}, ${newStatus}`
    : newStatus;

  sheet
    .getRange(rowNumber, COLUMN.STATUS)
    .setValue(updatedStatus);
}

送信済みフラグの管理方法

G列のステータスを送信済みフラグとして利用します。

ステータス値意味
空欄未処理
リマインド3日前送信済3日前の通知を送信済み
リマインド前日送信済前日の通知を送信済み
書類提出済書類の提出が完了

💡 ステータス管理のコツ
ステータス列は、通知の送信状況と入札の進捗を管理できます。「書類提出済」「入札完了」などを記録すれば、台帳を進捗確認にも使えます。

次のセクションでは、これらの関数を毎日実行する設定を説明します。


⏰ 定期実行の設定(タイマートリガー)

トリガーの設定手順

  1. Apps Scriptエディタで、左サイドバーの「トリガー」をクリックします。
  2. 「+トリガーを追加」をクリックします。
  3. 次の設定を選びます。
項目設定値
実行する関数syncBidEventsToCalendar または sendReminderEmails
実行するデプロイヘッド
イベントのソース時間主導型
時間ベースのトリガーのタイプ日ベースのタイマー
時刻の間隔午前8時〜9時

⚠️ 関数ごとにトリガーを設定してください
2つの関数を実行する場合は、それぞれに1つずつトリガーを設定します。同じ関数に複数のトリガーを設定すると、重複処理の原因になります。

実行間隔とタイムゾーンの注意点

タイムゾーンの確認方法

  1. Apps Scriptエディタの「プロジェクトの設定」をクリックします。
  2. 「タイムゾーン」が「(GMT+9:00) 東京」になっているか確認します。
  3. 異なる場合は「(GMT+9:00) 東京」に変更します。

実行ログの確認方法

  1. Apps Scriptエディタで「実行数」をクリックします。
  2. 各実行の状態が「完了」になっているか確認します。
  3. エラーがあれば、メッセージを開いて詳細を確認します。
エラーメッセージ原因対処法
カレンダーが見つかりませんカレンダーIDの指定ミスカレンダーIDを再確認する
シートが見つかりませんシート名の指定ミスシート名を確認する
日付が日付形式ではありません日付列が文字列日付形式に変更する
メールアドレスが設定されていません担当者メール列が空F列にメールアドレスを入力する

💡 エラー時の確認手順
まず「実行数」からエラーの詳細を確認します。日付形式、メールアドレス、IDの設定を順番に確認すると、原因を絞り込みやすくなります。

次のセクションでは、運用時に起きやすい失敗パターンを紹介します。


⚠️ 編集部の警告:よくある失敗パターン

GASによる入札管理には、いくつか注意点があります。

失敗パターン1:カレンダーIDの指定ミス

症状:意図しないカレンダーにイベントが登録されます。

原因:CALENDAR_ID の値が間違っています。

回避策:

  1. 「カレンダーの統合」からカレンダーIDをコピーします。
  2. 前後にスペースや余分な文字がないか確認します。
  3. テスト実行後、登録先のカレンダーを確認します。

⚠️ カレンダーIDの形式
カレンダーIDは、メールアドレス形式や「@group.calendar.google.com」形式です。カレンダー名を入力しても動作しません。

失敗パターン2:メール送信が重複する

症状:同じ通知が複数回届きます。

原因:同じ関数に複数のトリガーが設定されています。

回避策:

  1. Apps Scriptエディタの「トリガー」画面を開きます。
  2. 同じ関数の重複トリガーを確認します。
  3. 不要なトリガーを削除します。

失敗パターン3:日付形式が文字列になっている

症状:イベントや通知が作成されません。

原因:日付列が文字列として入力されています。

回避策:

  1. 日付列を選択し、「表示形式」→「数字」→「日付」を選びます。
  2. 入力済みのデータも日付形式に変換します。
  3. 日付は「2026/8/20」のようにスラッシュ区切りで入力します。

⚠️ 日付形式の確認方法
日付セルを選択し、数式バーの表示を確認します。文字列として左寄せになっている場合は、日付形式に変換してください。

失敗パターン4:トリガーの実行時間を誤って設定する

症状:通知の時刻がずれます。

原因:トリガーの時刻やタイムゾーンが適切でない可能性があります。

回避策:

  1. トリガーを午前8時〜9時に設定します。
  2. プロジェクト設定のタイムゾーンを「(GMT+9:00) 東京」にします。

次のセクションでは、工務店での具体的な活用シナリオを紹介します。


💡 経営者・担当者への活用シナリオ

工務店での運用イメージ

従業員15名の工務店を例に、運用方法を紹介します。

週月曜日火曜日水曜日木曜日金曜日
第1週入札情報の確認台帳に入力書類準備開始書類作成社内確認
第2週書類修正書類提出提出確認入札準備入札日

この運用により、次の効果が期待できます。

自動通知の価値

本レシピの価値は、担当者が締切を記憶する負担を減らすことです。

編集長の見解
入札管理の目的は、締切を覚えることではなく、書類を整えて提出することです。「忘れない」を仕組みに任せれば、担当者は書類の確認に時間を使えます。

経営者が台帳を活用する方法

活用方法具体的な操作
入札機会の確認ステータス別に案件数を確認する
受注確度の管理入札予定額や過去の結果を追記する
売上予測の材料案件の予定価格を追記する

これらの情報を使うと、次の判断がしやすくなります。

次のセクションでは、よくある質問に回答します。


🔍 よくある質問(FAQ)

Q1. GASの無料枠について教えてください

GASには、Googleアカウントの種類などに応じたサービスの割り当てがあります。編集部確認時点の公式ドキュメントでは、一般的な無料アカウントの目安として次の数値が示されています。

項目無料アカウントの上限目安
トリガーの実行時間1日90分
Gmailからのメール送信1日100通
Googleカレンダーの作成1日5,000件
URL Fetchによる外部接続1日20,000回

本レシピでは、通常は1日1回のトリガー実行と数通のメール送信を行います。そのため、実際の利用量は上限を大きく下回る想定です。ただし、上限はアカウントの種類やGoogleの仕様変更により変わる場合があります。

最新の数値は、Google Apps Scriptの割り当て(公式ドキュメント)で確認してください。編集部確認日:2026年9月19日。

💡 無料枠を確認する方法
Apps Scriptエディタの実行履歴で、実行時間やエラーを確認できます。利用量が多い場合は、公式ドキュメントでアカウント別の上限を確認してください。

Q2. 複数担当者へのメール送信は可能ですか?

可能です。担当者メール列に、カンマ区切りで複数のメールアドレスを入力します。

tanaka@example.com, sato@example.com

MailApp.sendEmail の to に、カンマ区切りの文字列を渡すと複数の宛先に送信できます。

⚠️ メールアドレスの入力形式に注意
全角カンマやセミコロンは使用せず、半角カンマで区切ってください。

Q3. 入札情報の入力は誰が行うべきですか?

担当役割
事務員入札公告の確認、台帳への入力
現場監督書類の作成、提出
経営者台帳の確認、入札判断

入力担当者は1人に限定する必要はありません。Googleスプレッドシートは複数人で編集できるため、代替担当者を決めておくと安心です。

💡 入力ルールを決めておく
「入札公告を確認したら、その日のうちに入力する」などのルールを決めると、入力漏れを減らせます。

Q4. 入札日が過ぎた案件はどうすればよいですか?

入札日が過ぎた案件は、ステータスを「入札完了」または「失格・辞退」に更新します。

不要なイベントを削除する場合は、次のコードを追記できます。

function cleanupPastEvents() {
  const sheet = getSheet();
  const rows = getBidData(sheet);
  const calendar = CalendarApp.getCalendarById(CALENDAR_ID);

  if (!calendar) {
    throw new Error(`カレンダーが見つかりません: ${CALENDAR_ID}`);
  }

  rows.forEach(row => {
    const isClosed = row.status &&
      (row.status.includes('入札完了') ||
       row.status.includes('失格'));

    if (!isClosed || !row.eventId) {
      return;
    }

    row.eventId.split(',').forEach(id => {
      const event = calendar.getEventById(id.trim());

      if (event) {
        event.deleteEvent();
      }
    });

    sheet
      .getRange(row.rowNumber, COLUMN.EVENT_ID)
      .clearContent();
  });
}

Q5. 入札情報を自動取得する方法はありますか?

可能ですが、自治体ごとにシステムが異なります。一律の方法はありません。

💡 まずは手作業から始める
情報の自動取得は、自治体のサイト変更で動かなくなる場合があります。まずは手入力で運用し、必要性を確認してから自動化を検討してください。

次のセクションでは、公式資料と応用方法を紹介します。


📚 参考情報と次のステップ

Google Apps Script公式ドキュメント

入札情報を自動取得する方法

方法特徴導入難易度
RSSフィード無料で試せる。自治体が対応している場合のみ中
入札情報サービス複数自治体を一元管理できる。有料の場合あり低
スクレイピングサイト構造の変更に弱い高

⚠️ スクレイピングには注意
自治体サイトを自動取得する場合は、利用規約を確認し、サーバーに過度な負荷をかけない設定にしてください。

本レシピを応用した自動化アイデア

業務自動化の内容使用するGASの機能
見積書の作成台帳のデータから見積書を作成SpreadsheetApp、DocumentApp
工事報告書の管理現場写真を台帳に整理DriveApp、SpreadsheetApp
安全パトロールの管理結果をカレンダーに登録CalendarApp
経費申請の通知提出締切を自動通知MailApp

編集長の見解
GASは、プログラミング経験が少ない事業者でも業務改善を始めやすいツールです。まずは入札管理のように締切が明確な業務から始め、効果を確認しながら対象を広げていきましょう。

さらに学ぶためのリソース

本記事では扱わなかった自動化の考え方は、次の記事でも確認できます。


まとめ

本記事では、工務店の公共工事入札管理をGoogle Apps Scriptで自動化する方法を紹介しました。

編集長の最終見解
入札管理の自動化は、締切漏れによる機会損失を減らすための仕組みです。まずはテストデータで動作を確認し、自社の運用に合わせて導入してください。約90分の準備から、入札管理の改善を始められます。

Mira / AI経営ラボ編集長