🧾事務自動化ラボ

請求書PDFを1クリックで作る無料テンプレート(GAS・全文コピペ配布)

スプレッドシートに明細を入力してメニューを押すと、インボイス対応の請求書PDFがDriveに保存されます。コードは全文このページに置いています。実機で動作を確認した版です。

請求書を1枚作るのに、テンプレートを複製して、宛先を書き換えて、金額を打ち直して、消費税を電卓で出して、PDFに書き出して、ファイル名を付けて保存する。この一連の作業を、明細を入れてメニューを1回押すだけにするGoogle Apps Script(GAS)のコードを、このページに全文置いています。

無料です。ダウンロード先もメールアドレスの入力も要りません。下のコードをコピーして貼るだけです。

このテンプレートですること・しないこと

売り込みのために曖昧にしても意味がないので、最初に線を引いておきます。

できること

  • 「請求書」シートに明細を入力 → メニュー「請求書 > この請求書をPDFにする」でPDFがDriveに保存される
  • 標準税率10%と軽減税率8%が混ざった請求書に対応(端数処理は税率ごとに1回だけ切り捨て)
  • 適格請求書(インボイス)の必須項目を印字する構成。登録番号・税率別の対価の額・税率別の消費税額を出す
  • 発行日が空のままPDFにしようとすると止まる
  • ファイル名は 20260817_取引先名_35160.pdf の形式で自動生成

できないこと

  • 複数の取引先へまとめて発行すること(1枚ずつです)
  • 請求書番号の自動採番(手入力欄になっています)
  • 発行履歴の記録と、同じ月・同じ相手への二重発行の警告
  • 源泉徴収10.21%の自動計算

後半の4つは、取引先が増えたときに効いてくる部分です。そこまで必要になった場合の選択肢はページの最後に書きます。まずは1枚分で足りるなら、この無料版で完結します。

導入手順

Googleアカウントがあれば5分ほどで動きます。

  1. Googleスプレッドシートを新規作成する
  2. メニュー「拡張機能 > Apps Script」を開く
  3. エディタに最初から入っている function myFunction() {} をすべて消す(Cmd+A / Ctrl+A → Delete)
  4. このページ下部のコードを全文コピーして貼り付ける
  5. Cmd+S / Ctrl+S で保存する
  6. スプレッドシートのタブに戻ってページを再読み込みする(メニューはここで初めて出ます)
  7. メニューバーに現れた「請求書」→「初期シートを作成」を押す
  8. 初回だけ承認画面が出ます。「詳細」→「(プロジェクト名)に移動」→「許可」の順に進む
  9. 「設定」シートに自分の事業者名・住所・登録番号・振込先を入力する
  10. 「請求書」シートに宛先と明細を入れて、「請求書 > この請求書をPDFにする」

承認画面で「このアプリは確認されていません」と出る場合

自分で貼ったコードなので、Googleの審査を受けていない状態です。表示されるのは正常です。「詳細」を開いて「安全ではないページに移動」に進むと承認できます。**心配なら、コードを読んでから実行してください。**外部へ通信しているのはPDF書き出しのためにGoogle自身のエクスポートURLを叩く1箇所だけで、他のサーバーへは何も送っていません。

承認するアカウントを間違えないこと

複数のGoogleアカウントにログインしている場合、スプレッドシートを所有しているアカウントと、承認画面で選ぶアカウントが一致していないと error=access_denied で弾かれます。所有アカウントが分からないときは、いったん myaccount.google.com を開いて確認してください。

入力の仕方

「請求書」シートは、色のついていない欄がすべて手入力です。

場所 入れるもの
A3 宛先(例: 株式会社サンプル商事 御中)
E3 発行日。初期シート作成時に当日の日付が入ります
E4 請求書番号(任意。無料版は手入力)
A13〜 品目・数量・単価・税区分

税区分はプルダウンで「標準10%」「軽減8%」「対象外」から選びます。金額の列と合計欄は触らなくて構いません。PDF化を押した時点でスクリプトが計算して書き込みます。

「対象外」を選んだ行は、小計には入りますが消費税の計算対象からは外れます。

インボイスの必須項目との対応

適格請求書として必要な項目は6つあります。このテンプレートがどこで満たしているかを並べます。

必須項目 このテンプレートでの出力先
発行者の氏名または名称 「設定」シートの事業者名 → 右上に印字
登録番号 「設定」シートの登録番号 → 右上に印字
取引年月日 発行日(空欄のままだとPDF化が止まります)
取引内容(軽減税率対象である旨) 明細の品目 + 税区分「軽減8%」の表示
税率ごとに区分した対価の額と適用税率 「消費税10%(対象¥30,000)」の形で印字
税率ごとに区分した消費税額 上の各行の右側の金額
交付を受ける事業者の氏名または名称 宛先

端数処理は、税率ごとに1回だけ切り捨てています。明細1行ごとに消費税を出して足し上げる方式にはしていません。これは制度上、1つの適格請求書につき税率ごとに1回とされているためです。

登録番号を持っていない場合は、「設定」シートの登録番号欄を空にしてください。その行自体が印字されなくなります。

中身を自分で組みたい場合

このページのコードは、貼れば動くことだけを目的にしています。なぜその書き方なのか——結合セルへの差し込みでハマる箇所、PDF化の前に flush() が要る理由、請求書番号を実行をまたいで一意に保つ方法——は、スプレッドシートで請求書を自動発行する仕組みの作り方 に分解して書いています。自分の業務に合わせて作り変えるなら、そちらを先に読んでください。

実機で確認したこと

コードの動作は、実際にスプレッドシートを新規作成して確かめています(2026年8月17日)。

  • 標準10%と軽減8%が混ざった請求書で、小計¥32,000 / 消費税10%(対象¥30,000)¥3,000 / 消費税8%(対象¥2,000)¥160 / 合計¥35,160 が出ること
  • 発行日を空にしてPDF化を実行すると警告が出て、「いいえ」を選ぶとPDFが作られないこと
  • 印字範囲の右端で登録番号や住所が切れないこと

列幅を変えるときは注意してください。ブラウザ上では全部見えていても、PDFでは印字範囲の外にはみ出した分が消えます。登録番号は13桁あるので、桁が欠けると請求書として成立しません。列幅を変えたら、PDFを開いて目で確かめてください。

コード

以下を全文コピーして、Apps Scriptエディタに貼り付けてください。

/**
 * ============================================================
 * 請求書PDF作成スプレッドシート(GAS)— 無料版
 * フリーランス・個人事業主向け
 *
 * この無料版でできること:
 *  - 「請求書」シートに直接入力 → メニュー1クリックでPDF化してDriveへ保存
 *  - インボイス制度対応(登録番号・品目・適用税率・税率別内訳を印字)
 *  - 軽減税率8% と 標準税率10% の混在に対応(端数は税率ごとに1回切り捨て)
 *  - 合計金額の自動計算
 *
 * 無料版に入っていない機能(有料版で提供):
 *  - 取引先マスタと請求データからの自動集計(複数社を一括発行)
 *  - 請求書番号の月別自動採番
 *  - 発行履歴への自動記録
 *  - 同月・同宛先への二重発行の警告
 *  - 源泉徴収 10.21% の自動計算
 *  - 合計金額の二重計算による検証
 *  → https://jimu-jidoka.booth.pm/items/8644136
 *
 * 動作要件: Google Apps Script V8 ランタイム
 * ============================================================
 */

// ------------------------------------------------------------
// 定数(シート名・セル番地はすべてここに集約)
// ------------------------------------------------------------

const SHEET_CONFIG = '設定';
const SHEET_TEMPLATE = '請求書';

const CONFIG_KEYS = {
  OWN_NAME: '事業者名',
  OWN_ADDRESS: '住所',
  OWN_TEL: '電話番号',
  REG_NUMBER: '適格請求書発行事業者登録番号',
  BANK_INFO: '振込先',
  TAX_RATE_STD: '消費税率(標準)',
  TAX_RATE_RED: '消費税率(軽減)',
  PDF_FOLDER_ID: 'PDF保存フォルダID',
  REMARKS: '備考(請求書に印字)',
};

// 「請求書」シートのセル番地(結合範囲は必ず左上アンカーを指す)
const T = {
  CLIENT_NAME: 'A3',      // A3:C3 結合
  CLIENT_ADDRESS: 'A4',   // A4:C5 結合
  ISSUE_DATE: 'E3',       // E3:F3 結合
  INVOICE_NO: 'E4',       // E4:F4 結合
  OWN_NAME: 'E6',
  OWN_ADDRESS: 'E7',
  OWN_TEL: 'E8',
  REG_NUMBER: 'E9',
  TOTAL_HEADLINE: 'B9',   // B9:C9 結合
  BILLING_MONTH: 'A11',
  DETAIL_START_ROW: 13,
  DETAIL_MAX_ROWS: 20,
  SUBTOTAL: 'E34',
  TAX_STD: 'E35',
  TAX_RED: 'E36',
  TOTAL: 'E38',
  TAX_STD_LABEL: 'D35',
  TAX_RED_LABEL: 'D36',
  BANK_INFO: 'A35',       // A35:C37 結合
  REMARKS: 'A40',         // A40:F41 結合
  PRINT_RANGE: 'A1:F42',
};

// ------------------------------------------------------------
// メニュー
// ------------------------------------------------------------

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('請求書')
    .addItem('この請求書をPDFにする', 'exportCurrentInvoice')
    .addSeparator()
    .addItem('初期シートを作成', 'setupSheets')
    .addSeparator()
    .addItem('有料版でできること', 'showUpgradeInfo')
    .addToUi();
}

function showUpgradeInfo() {
  const ui = SpreadsheetApp.getUi();
  ui.alert(
    '有料版でできること',
    [
      'この無料版は「1枚の請求書をPDFにする」ところまでです。',
      '',
      '有料版では次の作業がなくなります。',
      '',
      '・取引先マスタと明細を入力しておけば、月を指定するだけで',
      ' 該当する取引先すべての請求書を一括で発行します',
      '・請求書番号を月別に自動採番します(重複しません)',
      '・発行のたびに履歴シートへ自動で記録します',
      '・同じ月・同じ宛先へ2回発行しようとすると警告します',
      '・源泉徴収10.21%を設定1箇所でON/OFFできます',
      '・合計金額をシートの実データから別ルートで再計算して照合し、',
      ' 一致しなければ発行を止めます',
      '',
      'https://jimu-jidoka.booth.pm/items/8644136',
    ].join('\n'),
    ui.ButtonSet.OK
  );
}

// ------------------------------------------------------------
// PDF出力(無料版の本体)
// ------------------------------------------------------------

function exportCurrentInvoice() {
  const ui = SpreadsheetApp.getUi();
  const ss = SpreadsheetApp.getActiveSpreadsheet();

  try {
    const config = loadConfig_(ss);
    const sheet = mustGetSheet_(ss, SHEET_TEMPLATE);

    // 自社情報を差し込む
    sheet.getRange(T.OWN_NAME).setValue(config[CONFIG_KEYS.OWN_NAME] || '');
    sheet.getRange(T.OWN_ADDRESS).setValue(config[CONFIG_KEYS.OWN_ADDRESS] || '');
    sheet.getRange(T.OWN_TEL).setValue(config[CONFIG_KEYS.OWN_TEL] ? 'TEL: ' + config[CONFIG_KEYS.OWN_TEL] : '');
    sheet.getRange(T.REG_NUMBER).setValue(config[CONFIG_KEYS.REG_NUMBER] ? '登録番号: ' + config[CONFIG_KEYS.REG_NUMBER] : '');
    sheet.getRange(T.BANK_INFO).setValue(config[CONFIG_KEYS.BANK_INFO] || '');
    sheet.getRange(T.REMARKS).setValue(config[CONFIG_KEYS.REMARKS] || '');

    // 明細から合計を計算して書き込む
    const totals = calcTotalsFromSheet_(sheet, config);
    sheet.getRange(T.SUBTOTAL).setValue(totals.subtotal);
    // 税率別の行は、その税率の対象がある時だけ印字する(適用税率と対価の額はセットで表示)
    writeTaxRow_(sheet, T.TAX_STD_LABEL, T.TAX_STD, totals.pctStd, totals.baseStd, totals.taxStd);
    writeTaxRow_(sheet, T.TAX_RED_LABEL, T.TAX_RED, totals.pctRed, totals.baseRed, totals.taxRed);
    sheet.getRange(T.TOTAL).setValue(totals.total);
    sheet.getRange(T.TOTAL_HEADLINE).setValue('¥' + formatNumber_(totals.total));

    SpreadsheetApp.flush();

    const clientName = String(sheet.getRange(T.CLIENT_NAME).getValue() || '').replace(/\s*様$|\s*御中$/, '').trim();
    if (!clientName) {
      ui.alert('宛先が空です', '「請求書」シートの ' + T.CLIENT_NAME + ' に宛先を入力してください。', ui.ButtonSet.OK);
      return;
    }

    // 発行日(取引年月日)は適格請求書の必須項目。空のままPDFにしない
    if (!hasIssueDate_(sheet)) {
      const answer = ui.alert(
        '発行日が入っていません',
        [
          '発行日(取引年月日)は、適格請求書に必ず必要な項目です。',
          'このままPDFにすると、要件を満たさない請求書になります。',
          '',
          '本日の日付(' + todayText_() + ')を入れて続けますか?',
          '',
          '「いいえ」を選ぶと中止します。別の日付にしたい場合は',
          '「請求書」シートの ' + T.ISSUE_DATE + ' を直接編集してください。',
        ].join('\n'),
        ui.ButtonSet.YES_NO
      );
      if (answer !== ui.Button.YES) return;
      sheet.getRange(T.ISSUE_DATE).setValue('発行日: ' + todayText_());
      SpreadsheetApp.flush();
    }

    const folder = getOrCreatePdfFolder_(ss, config);
    const fileName = buildPdfFileName_(clientName, totals.total);
    const file = exportPdf_(ss, sheet, fileName, folder);

    ui.alert(
      'PDFを作成しました',
      [
        fileName,
        '',
        '保存先フォルダ: ' + folder.getName(),
        '',
        '合計: ¥' + formatNumber_(totals.total) + '(税込)',
        '',
        '※ 複数社の一括発行・請求書番号の自動採番・発行履歴・',
        '  二重発行の警告・源泉徴収は有料版の機能です。',
        '  メニュー「請求書 > 有料版でできること」をご覧ください。',
      ].join('\n'),
      ui.ButtonSet.OK
    );
    Logger.log('作成: ' + file.getUrl());
  } catch (e) {
    ui.alert('エラー', String(e && e.message ? e.message : e), ui.ButtonSet.OK);
    throw e;
  }
}

// ------------------------------------------------------------
// 計算
// ------------------------------------------------------------

/**
 * 「請求書」シートの明細エリアから合計を計算する。
 * 端数処理は税率ごとに1回だけ切り捨てる(インボイス制度の要件)。
 */
function calcTotalsFromSheet_(sheet, config) {
  // 表示する税率は必ず計算に使った税率と同じものを返す(設定を変えたら印字も変わる)
  const pctStd = Number(config[CONFIG_KEYS.TAX_RATE_STD] || 10);
  const pctRed = Number(config[CONFIG_KEYS.TAX_RATE_RED] || 8);
  const rateStd = pctStd / 100;
  const rateRed = pctRed / 100;

  const rows = sheet
    .getRange(T.DETAIL_START_ROW, 1, T.DETAIL_MAX_ROWS, 5)
    .getValues();

  let baseStd = 0;
  let baseRed = 0;
  let baseExempt = 0;

  rows.forEach((row, i) => {
    const item = String(row[0] || '').trim();
    if (!item) return;

    const qtyRaw = row[1];
    const priceRaw = row[2];
    if (qtyRaw === '' || priceRaw === '') {
      throw new Error('明細 ' + (T.DETAIL_START_ROW + i) + ' 行目の数量または単価が空欄です。');
    }
    const amount = Number(qtyRaw) * Number(priceRaw);
    sheet.getRange(T.DETAIL_START_ROW + i, 5).setValue(amount);

    const taxType = String(row[3] || '').trim();
    if (taxType.indexOf('8') >= 0 || taxType.indexOf('軽減') >= 0) {
      baseRed += amount;
    } else if (taxType.indexOf('対象外') >= 0 || taxType.indexOf('非課税') >= 0) {
      baseExempt += amount;
    } else {
      baseStd += amount;
    }
  });

  const taxStd = Math.floor(baseStd * rateStd);
  const taxRed = Math.floor(baseRed * rateRed);
  const subtotal = baseStd + baseRed + baseExempt;

  return {
    baseStd: baseStd,
    baseRed: baseRed,
    pctStd: pctStd,
    pctRed: pctRed,
    subtotal: subtotal,
    taxStd: taxStd,
    taxRed: taxRed,
    total: subtotal + taxStd + taxRed,
  };
}

// ------------------------------------------------------------
// PDF
// ------------------------------------------------------------

function getOrCreatePdfFolder_(ss, config) {
  const id = String(config[CONFIG_KEYS.PDF_FOLDER_ID] || '').trim();
  if (id) return DriveApp.getFolderById(id);

  const parents = DriveApp.getFileById(ss.getId()).getParents();
  const parent = parents.hasNext() ? parents.next() : DriveApp.getRootFolder();
  const name = '請求書PDF';
  const existing = parent.getFoldersByName(name);
  return existing.hasNext() ? existing.next() : parent.createFolder(name);
}

// 税率別の1行を書く。対象が0円の税率は行ごと空にする(¥0の行を印字しない)
function writeTaxRow_(sheet, labelCell, valueCell, pct, base, tax) {
  if (base <= 0) {
    sheet.getRange(labelCell).clearContent();
    sheet.getRange(valueCell).clearContent();
    return;
  }
  sheet.getRange(labelCell).setValue('消費税' + pct + '%(対象¥' + formatNumber_(base) + ')');
  sheet.getRange(valueCell).setValue(tax);
}

function todayText_() {
  return Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy/MM/dd');
}

// 「発行日: 」というラベルだけの状態を「未入力」とみなす
function hasIssueDate_(sheet) {
  const raw = sheet.getRange(T.ISSUE_DATE).getValue();
  if (raw instanceof Date) return true;
  const text = String(raw || '').replace(/^\s*発行日\s*[::]?\s*/, '').trim();
  return text.length > 0;
}

function buildPdfFileName_(clientName, total) {
  const tz = Session.getScriptTimeZone();
  const ymd = Utilities.formatDate(new Date(), tz, 'yyyyMMdd');
  return ymd + '_' + sanitizeFileName_(clientName) + '_' + total + '.pdf';
}

function sanitizeFileName_(name) {
  return String(name).replace(/[\\\/:*?"<>|]/g, '_').trim();
}

function exportPdf_(ss, sheet, fileName, folder) {
  SpreadsheetApp.flush();
  const url = 'https://docs.google.com/spreadsheets/d/' + ss.getId() + '/export'
    + '?format=pdf&gid=' + sheet.getSheetId()
    + '&size=A4&portrait=true&fitw=true'
    + '&sheetnames=false&printtitle=false&pagenumbers=false&gridlines=false&fzr=false'
    + '&range=' + encodeURIComponent(T.PRINT_RANGE)
    + '&top_margin=0.50&bottom_margin=0.50&left_margin=0.50&right_margin=0.50';

  const response = UrlFetchApp.fetch(url, {
    headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
    muteHttpExceptions: true,
  });
  if (response.getResponseCode() !== 200) {
    throw new Error('PDFの生成に失敗しました(HTTP ' + response.getResponseCode() + ')。時間を置いて再実行してください。');
  }
  return folder.createFile(response.getBlob().setName(fileName));
}

// ------------------------------------------------------------
// 共通
// ------------------------------------------------------------

function loadConfig_(ss) {
  const sheet = mustGetSheet_(ss, SHEET_CONFIG);
  const values = sheet.getRange('A2:B' + sheet.getLastRow()).getValues();
  const config = {};
  values.forEach(function (row) {
    const key = String(row[0] || '').trim();
    if (key) config[key] = row[1];
  });
  return config;
}

function mustGetSheet_(ss, name) {
  const sheet = ss.getSheetByName(name);
  if (!sheet) {
    throw new Error('シート「' + name + '」が見つかりません。メニュー「請求書 > 初期シートを作成」を実行してください。');
  }
  return sheet;
}

function formatNumber_(n) {
  return Number(n).toLocaleString('ja-JP');
}

// ------------------------------------------------------------
// 初期シート作成
// ------------------------------------------------------------

function setupSheets() {
  const ui = SpreadsheetApp.getUi();
  const ss = SpreadsheetApp.getActiveSpreadsheet();

  const config = ss.getSheetByName(SHEET_CONFIG) || ss.insertSheet(SHEET_CONFIG);
  const template = ss.getSheetByName(SHEET_TEMPLATE) || ss.insertSheet(SHEET_TEMPLATE);

  buildConfigSheet_(config);
  buildTemplateSheet_(template);

  ui.alert(
    '初期シートを作成しました',
    [
      '「設定」と「請求書」の2枚を作成しました。',
      '',
      '1. 「設定」シートに自社情報を入力してください',
      '2. 「請求書」シートに宛先と明細を入力してください',
      '3. メニュー「請求書 > この請求書をPDFにする」を押してください',
    ].join('\n'),
    ui.ButtonSet.OK
  );
}

function buildConfigSheet_(sheet) {
  sheet.clear();
  const rows = [
    ['項目', '値', '説明'],
    [CONFIG_KEYS.OWN_NAME, 'サンプル制作所', '請求書の発行者名(屋号または氏名)'],
    [CONFIG_KEYS.OWN_ADDRESS, '〒000-0000 サンプル県サンプル市見本町0-0-0', '請求書に印字する住所'],
    [CONFIG_KEYS.OWN_TEL, '000-0000-0000', '空欄可'],
    [CONFIG_KEYS.REG_NUMBER, 'T0000000000000', 'インボイス登録番号。未登録なら空欄'],
    [CONFIG_KEYS.BANK_INFO, 'サンプル銀行 本店 普通 0000000 サンプル セイサクジョ', '改行を入れる場合は Ctrl+Enter'],
    [CONFIG_KEYS.TAX_RATE_STD, 10, '標準税率(%)'],
    [CONFIG_KEYS.TAX_RATE_RED, 8, '軽減税率(%)'],
    [CONFIG_KEYS.PDF_FOLDER_ID, '', '空欄ならシートと同じ場所に「請求書PDF」フォルダを自動作成'],
    [CONFIG_KEYS.REMARKS, '恐れ入りますが、振込手数料は貴社にてご負担をお願いいたします。', '請求書の備考欄に印字'],
  ];
  sheet.getRange(1, 1, rows.length, 3).setValues(rows);
  sheet.getRange('A1:C1').setFontWeight('bold').setBackground('#efefef');
  sheet.setColumnWidth(1, 220);
  sheet.setColumnWidth(2, 320);
  sheet.setColumnWidth(3, 380);
  sheet.setFrozenRows(1);
}

function buildTemplateSheet_(sheet) {
  sheet.clear();
  sheet.getRange('A1:F42').clearFormat();

  sheet.setColumnWidth(1, 200);
  sheet.setColumnWidth(2, 60);
  sheet.setColumnWidth(3, 80);
  // D列は「消費税10%(対象¥1,234,567)」を折り返さずに収める必要がある
  sheet.setColumnWidth(4, 160);
  sheet.setColumnWidth(5, 100);
  // 自社情報(住所・登録番号)はE列からF列へはみ出して表示される。
  // F列が狭いと印字範囲の右端で文字が切れ、登録番号の桁が欠ける
  sheet.setColumnWidth(6, 150);

  // タイトル
  sheet.getRange('A1:F1').merge().setValue('請 求 書')
    .setFontSize(18).setFontWeight('bold').setHorizontalAlignment('center');

  // 宛先
  sheet.getRange('A3:C3').merge().setValue('(宛先を入力) 様')
    .setFontSize(13).setFontWeight('bold').setBorder(false, false, true, false, false, false);
  sheet.getRange('A4:C5').merge().setValue('(宛先の住所)').setVerticalAlignment('top');

  // 発行日・請求書番号(無料版は手入力)
  // 発行日は適格請求書の必須項目なので、空欄で放置されないよう本日の日付を入れておく
  sheet.getRange('E3:F3').merge().setValue('発行日: ' + todayText_()).setHorizontalAlignment('right');
  sheet.getRange('E4:F4').merge().setValue('請求書番号: ').setHorizontalAlignment('right');

  // 自社情報(住所が長い事業者でも右端で切れないよう1段小さくする)
  sheet.getRange('E6:E9').setFontSize(9);
  sheet.getRange('E6').setValue('').setFontWeight('bold');
  sheet.getRange('E7').setValue('');
  sheet.getRange('E8').setValue('');
  sheet.getRange('E9').setValue('');

  sheet.getRange('A7').setValue('下記の通りご請求申し上げます。');

  // 合計見出し
  sheet.getRange('A9').setValue('ご請求金額').setFontWeight('bold');
  sheet.getRange('B9:C9').merge().setFontSize(16).setFontWeight('bold').setHorizontalAlignment('center');
  sheet.getRange('D9').setValue('(税込)');
  sheet.getRange('A9:C9').setBorder(false, false, true, false, false, false);

  sheet.getRange('A10').setValue('お支払期限: ');
  sheet.getRange('A11').setValue('ご請求対象: ');

  // 明細ヘッダ
  const header = [['品目', '数量', '単価', '税区分', '金額']];
  sheet.getRange('A12:E12').setValues(header)
    .setFontWeight('bold').setBackground('#efefef').setHorizontalAlignment('center');

  // 明細エリア
  sheet.getRange('A12:E32').setBorder(true, true, true, true, true, true);
  sheet.getRange('B13:B32').setHorizontalAlignment('center');
  sheet.getRange('C13:C32').setNumberFormat('¥#,##0');
  sheet.getRange('D13:D32').setHorizontalAlignment('center');
  sheet.getRange('E13:E32').setNumberFormat('¥#,##0');

  // 税区分のプルダウン
  const rule = SpreadsheetApp.newDataValidation()
    .requireValueInList(['標準10%', '軽減8%', '対象外'], true)
    .setAllowInvalid(false)
    .build();
  sheet.getRange('D13:D32').setDataValidation(rule);

  // サンプル明細
  sheet.getRange('A13:D13').setValues([['Webサイト更新作業(7月分)', 1, 30000, '標準10%']]);

  // 金額まとめ
  sheet.getRange('D34').setValue('小計(税抜)');
  sheet.getRange('D34:E38').setBorder(true, true, true, true, true, true);
  sheet.getRange('D34:D36').setFontSize(9);  // 税率別ラベルが長いので1段小さくする
  sheet.getRange('E34:E38').setNumberFormat('¥#,##0');
  sheet.getRange('D38').setValue('合計金額(税込)');
  sheet.getRange('D38:E38').setFontWeight('bold').setBackground('#efefef');

  // 振込先
  sheet.getRange('A34').setValue('お振込先').setFontWeight('bold');
  sheet.getRange('A35:C37').merge().setWrap(true).setVerticalAlignment('top');
  sheet.getRange('A34:C37').setBorder(true, true, true, true, false, false);

  // 備考
  sheet.getRange('A39').setValue('備考').setFontWeight('bold');
  sheet.getRange('A40:F41').merge().setWrap(true).setVerticalAlignment('top');
  sheet.getRange('A39:F41').setBorder(true, true, true, true, false, false);

  sheet.setHiddenGridlines(true);
}

取引先が増えてきたら

このテンプレートは1枚ずつ発行する前提です。取引先が5社を超えたあたりから、別の問題が出てきます。

  • 毎月同じ内容を5回入力し直すことになる
  • 請求書番号を手で管理していると、抜けや重複が起きる
  • 「先月あの会社に出したかどうか」が分からなくなる

このうち番号の重複は、自分で仕組みを組むといちばん起きやすいところです。実行のたびにカウンタをメモリ上で持っていると、実行が終わった時点で消えるため、次に実行したときに1に戻ることがあります。防ぐには Document Properties への永続化とドキュメントロックが要ります。

下は、有料版で3回に分けて発行したときの履歴です。INV-202607-01 から -06 まで、実行をまたいでも番号が戻らず重複もしていません。

発行履歴シート。3回の独立した実行で INV-202607-01 から -06 まで採番され、重複していない

源泉徴収を設定でONにすると、税込金額から10.21%を差し引いた行が請求書に入ります。下は税抜20,000円の例で、消費税2,000円を足して源泉所得税2,042円を引いた19,958円が請求額になっています。

源泉徴収を適用した請求書PDF。小計20,000円、消費税10%が2,000円、源泉所得税10.21%が-2,042円、合計19,958円

複数社の一括発行・月別の自動採番・発行履歴・二重発行の警告・源泉徴収10.21%まで組んだ版は、有料で配布しています。同梱物・できないこと・実機での検証記録は 請求書自動作成スプレッドシート(GAS)の詳細ページ に書いています。無料版で足りるならそのまま使ってください。

請求の手前、つまり問い合わせや申込の受付そのものを自動化したい場合は、Googleフォームの受付を自動化する に別のコードを置いています。受付番号付きの自動返信と、対応ステータス付きの台帳まで作るものです。

← 記事一覧へ