はじめに
私は請求書作成の際、PCを開いてExcelを編集してPDF化してメール送信して……という流れで作業していました。
ただ、
- 外出先
- 客先
- 移動中
など、PCを開きたくない場面も多く、
「スマホだけで請求書を作成し、そのまま送付できたら便利なのでは?」
と思ったことが今回の開発のきっかけです。
完成すると、Slackで /invoice と入力するだけで請求書作成から送付まで完結します。
完成イメージ
↓
↓
↓
システム構成
今回の構成は次のようになっています。
ポイントは
- Slack Integration
- Invoice
を分離したことです。
Slack
│
▼
Slack Integration(GAS)
│
▼
Invoice Library(GAS)
│
├── 請求書作成
├── PDF作成
├── Gmail下書き作成
├── 承認
├── メール送信
└── Queue
Slack IntegrationはSlackとのやり取りだけを担当します。
請求書のロジックはすべてInvoiceライブラリへ委譲しています。
function handleInteraction(e) {
const payload = JSON.parse(e.parameter.payload);
const action = payload.actions[0];
if (action.action_id.includes("invoice")) {
Invoice.handleInteractionInvoice(e);
}
}
最初はSlack Integration側にも請求書の処理を書いていました。
しかし、Slack Integrationは修正すると再デプロイが必要になります。
そのため、
業務ロジックはすべてInvoice側へ寄せる
構成に変更しました。
これにより、請求書機能を改修してもSlack Integrationは基本的に変更不要になっています。
処理の流れ
全体のシーケンスは次のようになります。
Slack
↓
/invoice
↓
Modal表示
↓
入力
↓
Queue登録
↓
請求書作成
↓
PDF生成
↓
Gmail下書き
↓
Slack承認依頼
↓
承認
↓
メール送信
Google FormではなくSlack Modalにした理由
最初はGoogle Formを使っていました。
しかし、
- Slack→ブラウザへ遷移
- Form入力
- Slackへ戻る
という操作がかなり煩雑でした。
そこでSlack Modalへ変更しました。
結果として
- Slackアプリだけで完結
- 入力が速い
- UXが大きく改善
しました。
会社一覧はSpreadsheetから動的取得
取引先は増減するため、
static_select
ではなく
external_select
を利用しています。
会社一覧はSpreadsheetから取得しています。
function getInvoiceCompanyOptions(keyword) {
return getDataTable()
.filter(row => row.company.includes(keyword))
.map(row => ({
text: {
type: "plain_text",
text: row.company
},
value: row.company
}));
}
これにより、
Spreadsheetへ会社を追加するだけでSlack側にも反映されます。
Slackの3秒制限
SlackではSlash CommandやModal Submitは
約3秒以内に応答
しなければなりません。
しかし請求書作成では
- Spreadsheet生成
- PDF生成
- Gmail下書き
など時間のかかる処理があります。
普通に実行すると
operation_timeout
になってしまいます。
解決方法
そこで直接請求書を作成するのではなく、
まずQueueへ登録する方式にしました。
function enqueueInvoice(job) {
queueSheet.appendRow([
Utilities.getUuid(),
"WAITING",
JSON.stringify(job)
]);
}
処理の流れは
Modal Submit
↓
Queue登録
↓
Slackへ即応答
↓
バックグラウンド処理
となります。
これでSlack側は数百ms程度で応答できるようになりました。
Queue処理
Queueは1分ごとの時間主導トリガーで処理しています。
function processInvoiceQueue() {
const job = dequeueInvoice();
if (!job) {
return;
}
createInvoice(job);
}
ジョブが存在しない場合はすぐ終了するため、
毎分実行でも負荷はほとんどありません。
ワンショットトリガーも検討した
当初は
Queue追加
↓
ワンショットトリガー生成
↓
実行後削除
という方式も検討しました。
メリットは
- 必要な時だけ実行
- 空実行ゼロ
です。
一方で、
ジョブ実行中に例外が発生した場合、
再実行の仕組みを自前で作る必要があります。
最終的には
シンプルさ
と
障害復旧のしやすさ
を優先して毎分実行方式を採用しました。
二重実行防止
時間主導トリガーでは重複実行も考慮する必要があります。
そこでLockServiceを利用しています。
const lock = LockService.getScriptLock();
if (!lock.tryLock(300)) {
return;
}
try {
processInvoiceQueue();
} finally {
lock.releaseLock();
}
これで同じ請求書が二重に作成されることを防いでいます。
最終的なプロジェクト構成
最終的には
Slack Integration
Invoice
の2プロジェクト構成になりました。
Invoiceには
- Queue
- Approval
- Gmail
- Drive
- Spreadsheet
など請求書業務だけを実装しています。
これにより、
今後
- 見積書
- 納品書
- 経費精算
など別業務を追加しても、
Slack Integration側は
if(actionId.includes("invoice")){
Invoice.handleInteractionInvoice(e);
}
else if(actionId.includes("expenses")){
Expenses.handleInteractionExpenses(e);
}
程度の修正だけで済みます。
業務ロジックは各ライブラリ側に閉じ込められるため、
保守性も高くなりました。
今後やりたいこと
現在は請求書のみですが、
同じ仕組みで
- 見積書
- 納品書
- レポート
- 経費精算
なども実装していきたいと考えています。
おわりに
Google Apps Scriptというと
「ちょっとした自動化」
というイメージを持たれがちですが、
設計を工夫することで
- Slack
- Gmail
- Drive
- Spreadsheet
を組み合わせた実用的な業務システムを構築できます。
今回開発した仕組みによって、PCを開かなくてもスマホだけで請求書を作成・送付できるようになりました。
同じように「日々の定型業務をもっと手軽にしたい」と考えている方の参考になれば幸いです。





