脱Excel!GASで「スマホ→見積PDF」を即実装

仕事

「PCが古くても、孤軍奮闘でも、今日から回せるDXがいい」
そんなあなたへ。Googleフォーム(スマホOK)→ スプシで一覧管理 → 見積書PDF → URLを書き出して保存最小限のGASで仕上げます。

Excelからの“ひとりDX”脱出計画

  • Excelは得意。でもVBAは苦手でDXは進まない。プログラミングは別世界。かつて独学を試みるも挫折。そんな経験ありませんか。
  • Google Apps Script(GAS)なら、「入力はスマホ」「出力はPDF」の動線をノンストップで作れます。
  • コードは最小、説明は丁寧。今日からあなたの現場で回ることをゴールにします。

フォーム①〜⑨→スプシ→PDF→URLまで一気通貫

1) 事前準備

  • Googleフォーム(質問は次の①〜⑨を用意)
    1. 宛名(短文)
    2. 件名(短文)
    3. 見積日(日付型推奨)
    4. 納期(日付型)
    5. 品名(短文)
    6. 数量(数値)
    7. 単価(数値)
    8. 備考(段落)
    9. 担当者(短文)
Googleフォームで宛名・件名・見積日など9項目を設定する画面
  • 回答先スプレッドシートをリンク(シート名は既定「フォームの回答 1」でOK)
  • 見積テンプレスプレッドシートを用意(すでにひな型があるならそちらを使いましょう。無ければ、無料サイトからダウンロードも可能。Excel仕様ならスプシに変換しましょう。)
    • シート名:見積テンプレ(A列からレイアウト開始/A4縦1枚前提)
    • 例レイアウト(自由に):
      • B2:「見積書」(B2〜I2を結合&中央揃え)
      • B5:宛名、C9:件名、H2:見積日、C10:納期、H10:担当者
      • B17〜H17:ヘッダ(品名・数量・単価・金額)
      • B18〜H25:明細8行
      • 小計/消費税/税込合計はH26〜H28などに配置
    • 印刷プレビューでA4縦/ページに合わせるを確認(最終的にはGASのURLパラメータで強制します)
    • 下記の名前付き範囲をシート内の該当セルに設定してください(データ > 名前付き範囲)。
    • 入力値(フォーム①〜⑨と対応)
    • EST_宛名
    • EST_件名
    • EST_見積日
    • EST_納期
    • EST_品名1
    • EST_数量1
    • EST_単価1
    • EST_備考
    • EST_担当者
見積テンプレートにEST_宛名やEST_担当者などの名前付き範囲を設定
  • 保存フォルダは作成された見積書を保存するものです(PDFを入れる先)。Googleドライブ画面から「マイドライブ」→「新しいフォルダ」を作成して、そこに保存します。

2) GAS(コピペOK・最小実装)

Apps Script を回答スプレッドシートに紐付けて作成。

Apps Script を回答スプレッドシートに紐付けて作成


下記の TEMPLATE_FILE_ID / OUTPUT_FOLDER_ID をあなたのIDに差し替えるだけで走ります。

/** ===== 設定(置き換え必須) ===== */
const TEMPLATE_FILE_ID = 'YOUR_TEMPLATE_SPREADSHEET_ID';    // 見積テンプレ
const OUTPUT_FOLDER_ID = 'YOUR_OUTPUT_FOLDER_ID';       // 新しく作成した保存フォルダ
const FORM_SHEET_NAME = 'フォームの回答 1';                 // 回答シート名を合わせる
const COL_ESTIMATE_URL = '見積ファイルURL';
const COL_PDF_URL = 'PDFダウンロードURL';

/** フォーム質問タイトル → キー(タイトルはフォームと一致させる) */
const FORM_KEYS = {
  '宛名': 'atena',
  '件名': 'kenmei',
  '見積日(yyyy/MM/dd)': 'date',
  '納期(yyyy/MM/dd)': 'exp',
  '品名': 'item1',
  '数量': 'qty1',
  '単価': 'price1',
  '備考': 'notes',
  '担当者': 'tanto',
};

/** 名前付き範囲(テンプレ側) */
const NR = {
  atena: 'EST_宛名',
  kenmei: 'EST_件名',
  date: 'EST_見積日',
  exp: 'EST_納期',
  item1: 'EST_品名1',
  qty1: 'EST_数量1',
  price1: 'EST_単価1',
  notes: 'EST_備考',
  tanto: 'EST_担当者',
};

/** ===== 1) フォーム送信トリガー =====
 * スクリプトエディタ → トリガー → 「onFormSubmit」を"フォーム送信時"で登録
 */
function onFormSubmit(e) {
  const ans = parseAnswers(e);
  const { ssUrl, pdfUrl } = createEstimateFromTemplate(ans);
  writeBackUrls_(e, ssUrl, pdfUrl);
}

/** ===== 2) 回答パース ===== */
function parseAnswers(e) {
  const nv = e.namedValues || {};
  const g = (title) => (nv[title] ? String(nv[title][0]).trim() : '');
  const toNum = (v) => Number(String(v).replace(/[, ]/g, '')) || 0;

  return {
    atena: g('宛名'),
    kenmei: g('件名'),
    date: Utilities.formatDate(new Date(g('見積日')), 'Asia/Tokyo', 'yyyy年M月d日'),
    exp: Utilities.formatDate(new Date(g('納期')),'Asia/Tokyo','yyyy年M月d日'),
    item1: g('品名'),
    qty1: toNum(g('数量')),
    price1: toNum(g('単価')),
    notes: g('備考'),
    tanto: g('担当者'),
  };
}

/** ===== 3) 見積生成 ===== */
function createEstimateFromTemplate(ans) {
  // テンプレをコピー(ファイル名に日付+件名)
  const ymd = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyyMMdd_HHmm');
  const name = `見積_${ans.kenmei || '無題'}_${ymd}`;
  const copyFile = DriveApp.getFileById(TEMPLATE_FILE_ID).makeCopy(name, DriveApp.getFolderById(OUTPUT_FOLDER_ID));
  const ss = SpreadsheetApp.openById(copyFile.getId());

  // 名前付き範囲へ値を注入
  setNR_(ss, NR.atena, ans.atena);
  setNR_(ss, NR.kenmei, ans.kenmei);
  setNR_(ss, NR.date, ans.date);
  setNR_(ss, NR.exp, ans.exp);
  setNR_(ss, NR.item1, ans.item1);
  setNR_(ss, NR.qty1, ans.qty1);
  setNR_(ss, NR.price1, ans.price1);
  setNR_(ss, NR.notes, ans.notes);
  setNR_(ss, NR.tanto, ans.tanto);
  SpreadsheetApp.flush();

  // === ここで書式をお好み調整:URLエクスポート方式 ===
  const sheet = ss.getSheets()[0]; // 1枚目のシート(見積書)を対象に
  const sheetId = sheet.getSheetId();
  const url = ss.getUrl().replace(/edit$/, '') +
    'export?format=pdf' +
    '&gid=' + sheetId +
    '&range=A1:J32' +        // 範囲指定
    '&size=A4' +             // A4
    '&portrait=true' +       // タテ向き
    '&fitw=true' +           // ページ幅に収める
    '&scale=4' +             // 1ページにフィット
    '&gridlines=false' +     // 枠線なし
    '&sheetnames=false' +    // シート名非表示
    '&printtitle=false' +    // タイトル行/列なし
    '&pagenum=UNDEFINED';    // ページ番号非表示
  const token = ScriptApp.getOAuthToken();
  const response = UrlFetchApp.fetch(url, {
    headers: { Authorization: 'Bearer ' + token }
  });
  const pdfFile = DriveApp.getFolderById(OUTPUT_FOLDER_ID)
    .createFile(response.getBlob().setName(name + '.pdf'));

  return { ssUrl: ss.getUrl(), pdfUrl: pdfFile.getUrl() };
}

/** 名前付き範囲にセット(存在しなければ無視:安全です) */
function setNR_(ss, name, value) {
  const r = ss.getRangeByName(name);
  if (r) r.setValue(value);
}

/** ===== 4) 回答行へURLを書き戻し ===== */
function writeBackUrls_(e, ssUrl, pdfUrl) {
  const sheet = e.range.getSheet();
  const row = e.range.getRow();
  ensureHeader_(sheet, [COL_ESTIMATE_URL, COL_PDF_URL]);

  const map = headerMap_(sheet);
  sheet.getRange(row, map[COL_ESTIMATE_URL]).setFormula(`=HYPERLINK("${ssUrl}","見積ファイル")`);
  sheet.getRange(row, map[COL_PDF_URL]).setFormula(`=HYPERLINK("${pdfUrl}","PDF")`);
}

/** ヘッダ確保&列位置マップ */
function headerMap_(sheet) {
  const lastCol = sheet.getLastColumn();
  const headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0];
  const map = {};
  headers.forEach((h, i) => (map[h] = i + 1));
  return map;
}

function ensureHeader_(sheet, need) {
  const map = headerMap_(sheet);
  const adds = need.filter((h) => !map[h]);
  if (adds.length === 0) return;
  const lastCol = sheet.getLastColumn();
  sheet.getRange(1, lastCol + 1, 1, adds.length).setValues([adds]);
}

セットアップの山場はここだけ
YOUR_TEMPLATE_SPREADSHEET_IDとは 見積テンプレのスプレッドシートID:https://docs.google.com/spreadsheets/d/○○○/edit?gid=……の○○○部分がIDです(あなたのテンプレからコピペしてください)。
YOUR_OUTPUT_FOLDER_IDとは 保存先フォルダのID
https://drive.google.com/drive/folders/◯◯◯ の ◯◯◯ 部分がIDです(こちらも同様にコピペしてください)。
トリガー:onFormSubmit を 「フォーム送信時」 に設定
(この仕組みがキモ)

Apps ScriptでonFormSubmitをフォーム送信時トリガーに設定する

3) 動作確認チェックリスト(3分テスト)

  1. テンプレに名前付き範囲が全部あるか(スペル完全一致)
  2. フォームを1件送信(数量・単価は数値)
  3. 回答行に「見積ファイルURL」「PDFダウンロードURL」が入る
  4. PDFを開いて宛名・件名・金額が想定どおりか
  5. 小計、消費税率、合計、見積金額は見積テンプレにあらかじめ計算式を入力しておくとラク◎

4) よくあるつまずき(秒速で解決)

  • URLが返らない:トリガー未設定 or 権限未付与。エディタの実行ボタンで一度手動実行→許可。
  • 金額がゼロEST_数量1EST_単価1 のセルが文字列扱い。数値に。
  • 日付がおかしい:入力のフォーマットを 2025/10/04 形式にそろえる(フォーム側の質問タイプを日付に)。
  • 名前付き範囲が見つからない:綴り相違。テンプレのデータ > 名前付き範囲で再確認。

5) すぐできる拡張(必要なときだけ)

  • 品目を3行にEST_品名2/数量2/単価2 などをテンプレとGASに追加し、小計を合算。
  • 番号採番&件名に付与Utilities.getUuid().slice(0,8) を使って 見積No を自動付与。
  • 顧客マスタ参照:宛名から住所・社名・担当者TELをVLOOKUPで引く。
  • 送信メール通知:GmailAppでPDFを添付して営業担当へ自動送信。

まずは“1品目・9項目”で回す。そこから必要な分だけ足す。

完璧主義は敵。最小の仕組みを1本稼働。あとは“処理量”を増やして生産性アップです。
GASの良さは、小さく始めて徐々に伸ばせること。今回のテンプレ+GASは「最小で実運用に耐える」ラインに合わせています。

  • この記事のコピペでまず1本稼働
  • “足りない”と思ったところだけ名前付き範囲とGASを増やす

ここまで作れたら、あなたはもう“ただのExcel使い”じゃありません。
ちょっとだけドヤ顔、許されます。💪

コメント

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