0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Chatworkシリーズ #4】「Chatworkに確定連絡が来たら請求書を送る」をGASで自動化する

0
Last updated at Posted at 2026-03-12

毎月同じ手順を踏む仕事が、どこにでもある。

相手から「今月の金額が確定しました」という連絡が来る。スプレッドシートに書く。請求書を作る。PDFをダウンロードして送る。完了を報告する。

フリーランスでも、複数の取引先を持つ経営者でも、社内の経理担当者でも、この流れは同じだ。毎月同じことをやる。ミスると面倒なので集中する。集中が必要な作業を毎月やり続けるのはコスパが悪い。

請求書は自動で作って、PDFにして、送れる。 そのための実装をまとめた。

この記事ではChatworkをトリガーに使っているが、トリガーをGmailに変えるだけでSlackでもLINE WORKSでも同じ構造で動く。「Chatworkじゃないから関係ない」ではなく、月次確定処理のパターンとして読んでほしい。

パターンを図にすると

月次確定処理の典型的な流れはこうだ。

[確定連絡]
  Chatwork or Gmail に金額・明細が届く
      ↓
[記録]
  スプレッドシートの該当月列を更新
      ↓
[発行]
  請求書API(Misocaなど)で請求書を作成
  PDFをDriveに保存
      ↓
[通知]
  関係者にChatworkで送る(複数ルームのことも多い)
  Gmailで相手方に請求書を送付
      ↓
[完了]

この流れ自体は、フリーランスでも中小企業でも、レベニューシェア契約でも受託でも、だいたい同じだと思う。違うのは「どこから来るか」「どこに送るか」「請求書の品目が何か」だけ。

変わらない骨格があるなら、コードにできる。

最小構成で動かす

まず1案件、最小構成で作る。

① Chatworkからトリガーを拾う

担当者が「2月分が確定しました。売上: ○○円、経費: ○○円」のようなメッセージを送ってくる前提。それを拾う。

const CW_API_TOKEN = PropertiesService.getScriptProperties().getProperty('CW_TOKEN');
const ROOM_ID      = 'xxxxx';  // 担当者とのルームID
const TARGET_AID   = 'xxxxx';  // 担当者のアカウントID

function fetchLatestReport() {
  const res = UrlFetchApp.fetch(
    `https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages?force=1`,
    { method: 'get', headers: { 'X-ChatWorkToken': CW_API_TOKEN } }
  );

  const messages = JSON.parse(res.getContentText());

  // 新しい順に「確定」を含む相手のメッセージを探す
  return [...messages].reverse().find(m =>
    String(m.account.account_id) === TARGET_AID && m.body.includes('が確定')
  );
}

force=1 をつけないと既読メッセージが返ってこない。#2で触れたやつ。

② テキストから金額を抜く

メッセージフォーマットは毎月少し揺れる。コロンが全角だったり、スペースが入ったり。正規表現で複数パターンを試す。

function extractAmount(text, keyword) {
  const patterns = [
    new RegExp(keyword + '\\s*[::]\\s*([\\d,,]+)'),
    new RegExp(keyword + '[^\\d\\n]*([\\d,,]+)'),
  ];
  for (const p of patterns) {
    const m = text.match(p);
    if (m) return parseInt(m[1].replace(/[,,]/g, ''));
  }
  return null;
}

// 使い方
const netAmount = extractAmount(messageBody, '差し引き') || extractAmount(messageBody, '差引');

③ Misocaで請求書を作る

税込金額が来る場合、Misocaに渡すのは税抜金額。ここで注意点がある。

taxExcl = Math.floor(incomeAmount / 1.1) だと1〜2円ずれることがある。Misocaは消費税を切り捨てで計算するため、taxExcl + floor(taxExcl × 0.1) = incomeAmount になる値を逆算する必要がある。

function toTaxExcl(taxIncl) {
  let s = Math.floor(taxIncl / 1.1);
  // 切り捨て誤差を補正(最大2円のズレを確認)
  for (; s <= Math.floor(taxIncl / 1.1) + 2; s++) {
    if (s + Math.floor(s * 0.1) === taxIncl) return s;
  }
  return Math.floor(taxIncl / 1.1);  // フォールバック
}

請求書作成は通常のPOSTリクエスト。

function createInvoice(targetYear, month, taxInclAmount) {
  const service = getMisocaService();  // OAuth2認証済みのサービス
  const token   = service.getAccessToken();
  const taxExcl = toTaxExcl(taxInclAmount);

  const res = UrlFetchApp.fetch('https://app.misoca.jp/api/v3/invoice', {
    method: 'post',
    headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
    payload: JSON.stringify({
      issue_date:  Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd'),
      contact_id:  CONTACT_ID,  // 請求先の取引先ID
      subject:     `${targetYear}${month}月分 レベニューシェア`,
      body: { tax_option: 'EXCLUDE' },
      items: [{
        name:         '分配金',
        quantity:     1,
        unit_price:   taxExcl,
        unit_name:    '',
        tax_type:     'STANDARD_TAX_10',
      }],
    }),
  });

  return JSON.parse(res.getContentText()).id;  // 請求書ID
}

④ GASからChatworkにPDFを添付送信する(ここが一番詰まった)

GASからChatworkにPDFを添付送信するのが地味にハマりポイントだった。UrlFetchApp はmultipart/form-dataを直接サポートしていない。バイト配列を手動で組み立てる。

function sendPdfToChatwork(roomId, pdfBlob, message) {
  const token    = PropertiesService.getScriptProperties().getProperty('CW_TOKEN');
  const boundary = 'BOUNDARY' + Date.now();
  const CRLF     = '\r\n';

  // multipart/form-data を手組み
  const header = `--${boundary}${CRLF}` +
    `Content-Disposition: form-data; name="file"; filename="${pdfBlob.getName()}"${CRLF}` +
    `Content-Type: application/pdf${CRLF}${CRLF}`;

  const footer = `${CRLF}--${boundary}${CRLF}` +
    `Content-Disposition: form-data; name="message"${CRLF}${CRLF}` +
    `${message}${CRLF}--${boundary}--`;

  const payload = Utilities.newBlob(header).getBytes()
    .concat(pdfBlob.getBytes())
    .concat(Utilities.newBlob(footer).getBytes());

  UrlFetchApp.fetch(`https://api.chatwork.com/v2/rooms/${roomId}/files`, {
    method:      'post',
    headers:     { 'X-ChatWorkToken': token, 'Content-Type': `multipart/form-data; boundary=${boundary}` },
    payload:     payload,
  });
}

文字列部分を getBytes() でバイト変換して、Blobの生バイトと連結している。

「GAS Chatwork PDF 添付」で検索してもサンプルがほとんど出てこない。Chatwork APIの /rooms/{room_id}/files エンドポイントにmultipartでPOSTする実装例が少ないのはこの手組みが面倒だからだと思う。ハマった人の参考になれば。

⑤ 再実行を安全にする

途中でエラーが出て再実行するとき、請求書が二重に発行されると困る。請求書IDをスクリプトプロパティに保存して、既存のものがあればそのIDを使いまわす。

const invKey = `INV_${targetYear}_${month}`;
const existing = PropertiesService.getScriptProperties().getProperty(invKey);

let invoiceId;
if (existing) {
  invoiceId = existing;  // 既存IDを再利用
} else {
  invoiceId = createInvoice(targetYear, month, amount);
  PropertiesService.getScriptProperties().setProperty(invKey, String(invoiceId));
}

これでどこで止まっても再実行できる。Misocaの請求書は増えない。

freeeで同じことをやろうとして沼った

案件Aはfreeeにも請求書を作る必要があった。MisocaのAPIはシンプルで、POSTに必要なパラメータが揃っていれば動いた。同じノリでfreeeに行ったら全然違った。

認証フローが複雑で、エンドポイントのバージョンが複数あって、どれが請求書発行に使えるか判断が難しい。ドキュメントを読むコストが高い。

最終的にやったこと:GASでfreee APIを叩いて請求書のドラフトを作成するところまで自動化して、PDFダウンロードと添付送信だけ手動にした。完全自動化はあきらめて、「ダイアログに手順と直リンクを出して、クリックするだけ」 に着地させた。

// freee請求書作成後、ダイアログで手動手順を案内する
if (freeeInvoiceNumber) {
  // 完了ダイアログ内に手順を表示
  html += '<div>① freee請求書(' + freeeInvoiceNumber + ')→ PDFダウンロード</div>'
        + '<div>② DriveフォルダにPDF保存</div>'
        + '<div>③ 担当者にChatworkでPDF添付送信</div>';
}

ボタンを押せばfreeeとDriveのフォルダが自動で開く。あとはダイアログの指示通りに動くだけ。3ステップが手動に残ったけど、迷いがなくなった分ミスが減った。

freee MCP、早く請求書発行に対応してくれ。対応したらここは全自動になる。

実際に組んだもの

うちの場合は3案件あった。

案件 トリガー 請求先 送り先
A Chatworkのメッセージ MisocaとfreeeW Chatwork複数ルーム
B Gmailの添付メール Misoca Chatwork + Gmail返信
C 別スプレッドシートから自動取得 Misoca Chatwork

骨格は同じ。トリガーとAPIの向き先が違うだけ。

1つのGASプロジェクト(Code.gs + InvoiceB.gs + InvoiceC.gs)にまとめて、共通処理(Chatwork送信、OAuth2、税計算)は Code.gs に集約した。スプレッドシートのメニューから各案件を選んで実行する。

請求書処理
├── 案件A 実行 / 案件A テスト実行
├── 案件B 実行 / 案件B テスト実行
└── 案件C 実行 / 案件C テスト実行

テスト実行は本番に触れない。Chatworkはマイチャットに飛んで、Misocaは作らない。実装中に本番に飛ばして冷や汗をかかないためのやつ。

プレビューダイアログ

「実行」の前に確認画面を出している。

function buildPreviewHtml(parsedData, isTest) {
  const html = `
    <p>対象月: ${parsedData.targetMonth}</p>
    <p>差し引き: ${parsedData.netAmount.toLocaleString()}円</p>
    <button onclick="google.script.run.executeProcess()">実行</button>
  `;
  return HtmlService.createHtmlOutput(html).setWidth(500).setHeight(300);
}

実際はもう少し凝っているが、骨格はこれ。数字がパースできていれば「実行」ボタンが有効になる。パース失敗なら赤字で警告が出て、実行できない。

組んで変わったこと

21ステップが、確認ダイアログを見てボタンを押すだけになった。

「ミスが怖いから集中する」というモードが不要になった。確認画面の数字を見て問題なければ実行する。それだけ。月次処理で消耗していた集中力が、他のことに使えるようになった。


Chatworkって「メッセージが来たら人間が読んで動く」という使い方しかしていなかった。

でも構造としては、「特定のメッセージが来たらGASが動く」というトリガーとして使える。毎月くる定型連絡を「人間への通知」ではなく「処理のスタートライン」として扱うだけ。

月次処理だけじゃなく、「承認が来たら次のステップに進む」「集計完了のメッセージが来たらレポートを生成する」みたいな使い方もできる。Chatworkに情報が集まっている会社なら、自動化のトリガーとしてそのまま使えばいい。

792万IDが使っているのに、APIで遊んでいる人がほとんどいない。だからまだ発見がある。


こういう人に刺さると思う

  • 毎月Chatworkで「確定しました」系の連絡を受け取っている
  • Misocaかfreeeで請求書を作っている
  • 請求書をPDFにしてChatworkやメールで送っている
  • それを複数案件やっている(1案件でも十分元は取れる)

GASとChatwork APIのトークンがあれば始められる。Misocaは無料プランでもAPIが使える。


Chatworkシリーズ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?