はじめに
副業やフリーランスで複数のクライアントと取引し始めると、地味に効いてくるのがお金まわりの管理です。
- どの請求がまだ入金されていないのか、すぐに分からない
- プラットフォーム手数料や送金手数料を引いたら、実際の手取りがいくらなのか曖昧
- 月末になって「今月いくら稼いだっけ」と通帳とメールを行き来する
会計ソフトを入れるほどではないけれど、頭の中で管理するのは限界、という段階には**Googleスプレッドシート+Google Apps Script(GAS)**がちょうどいいです。無料で、スマホからも見られて、少しのコードで自動化できます。
この記事では、次の3つを作ります。
- 請求一覧シートと、手数料を引いた手取りの自動計算
- 支払期限を過ぎた請求を毎朝チェックして、自分宛てにメールで知らせるトリガー
- 月ごとの売上・手取り・未回収額をまとめるサマリーシートの自動生成
1. 請求一覧シートを作る
シート名を請求として、1行目に次の見出しを置きます。
| 列 | 見出し | 例 |
|---|---|---|
| A | 請求番号 | INV-2026-014 |
| B | クライアント | Acme Inc. |
| C | 経路 | Upwork / 直接 |
| D | 通貨 | USD |
| E | 請求額 | 600 |
| F | 手数料率 | 0.10 |
| G | 送金手数料 | 3 |
| H | 手取り(自動) | (数式) |
| I | 発行日 | 2026/10/01 |
| J | 支払期限 | 2026/10/15 |
| K | 入金日 | (空欄 or 日付) |
| L | ステータス(自動) | (数式) |
手数料率は自分で入力する列にしているのがポイントです。プラットフォームの手数料体系は変わることがあり、契約の種類やクライアントによっても異なる場合があります。スクリプトに数字を埋め込まず、その時点で確認した実際の率を行ごとに入れる方が安全です。たとえばUpworkの手数料の仕組みについては、Digital Dialogueの手数料解説記事で整理されているので、最新の公式情報とあわせて確認してから入力してください。
H列(手取り)には次の数式を入れます。
=IF(E2="", "", ROUND(E2 * (1 - F2) - G2, 2))
L列(ステータス)はこうです。
=IF(A2="", "", IF(K2<>"", "入金済み", IF(TODAY() > J2, "期限超過", "未入金")))
条件付き書式で「期限超過」を赤にしておくと、開いた瞬間に分かります。
円やルピーなど自国通貨での手取りも見たい場合は、GOOGLEFINANCE関数で為替レートを取得できます。
=H2 * GOOGLEFINANCE("CURRENCY:USDJPY")
ただしGOOGLEFINANCEのレートは参考値で、遅延があることもあります。実際に口座に入った金額とは一致しないので、確定値は入金後に実額で記録する運用がおすすめです。
2. 期限超過を毎朝メールで知らせる
スプレッドシートの「拡張機能」→「Apps Script」を開いて、次のコードを貼ります。
const SHEET_NAME = "請求";
function checkOverdueInvoices() {
const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
const rows = sheet.getDataRange().getValues();
const header = rows.shift();
const col = Object.fromEntries(header.map((h, i) => [h, i]));
const today = new Date();
today.setHours(0, 0, 0, 0);
const overdue = rows.filter((r) =>
r[col["請求番号"]] &&
!r[col["入金日"]] &&
r[col["支払期限"]] instanceof Date &&
r[col["支払期限"]] < today
);
if (overdue.length === 0) return;
const lines = overdue.map((r) => {
const days = Math.floor((today - r[col["支払期限"]]) / 86400000);
return `・${r[col["請求番号"]]} ${r[col["クライアント"]]} ` +
`${r[col["請求額"]]} ${r[col["通貨"]]}(${days}日超過)`;
});
MailApp.sendEmail({
to: Session.getActiveUser().getEmail(),
subject: `【請求】期限超過が${overdue.length}件あります`,
body: lines.join("\n") + "\n\n" + SpreadsheetApp.getActive().getUrl(),
});
}
見出しの文字列から列番号を引くようにしているので、あとで列を追加・並べ替えしてもコードを直す必要がありません。
メールの送信先は自分自身だけにしています。クライアントへの催促メールまで自動化することもできますが、催促は文面やタイミングで印象が大きく変わるので、通知は自動・連絡は手動、と分けておくのが無難です。
トリガーは、Apps Scriptエディタの「トリガー」画面から設定するか、次の関数を一度だけ実行します。
function installTriggers() {
ScriptApp.getProjectTriggers().forEach((t) => ScriptApp.deleteTrigger(t));
ScriptApp.newTrigger("checkOverdueInvoices").timeBased().everyDays(1).atHour(8).create();
ScriptApp.newTrigger("buildMonthlySummary").timeBased().onMonthDay(1).atHour(9).create();
}
初回実行時にはGoogleアカウントの権限承認が求められます。時刻はスクリプトのタイムゾーン設定(「プロジェクトの設定」)に従うので、日本時間やパキスタン時間など、自分の生活時間に合わせておきましょう。atHourで指定した時刻ちょうどではなく、その1時間の中のどこかで実行される点にも注意してください。
3. 月次サマリーを自動生成する
毎月1日に、月ごとの集計をサマリーシートに書き出します。
function buildMonthlySummary() {
const ss = SpreadsheetApp.getActive();
const rows = ss.getSheetByName(SHEET_NAME).getDataRange().getValues();
const header = rows.shift();
const col = Object.fromEntries(header.map((h, i) => [h, i]));
const tz = ss.getSpreadsheetTimeZone();
/** @type {Record<string, {billed:number, net:number, unpaid:number, count:number}>} */
const byMonth = {};
for (const r of rows) {
const issued = r[col["発行日"]];
if (!(issued instanceof Date)) continue;
const key = Utilities.formatDate(issued, tz, "yyyy-MM");
if (!byMonth[key]) byMonth[key] = { billed: 0, net: 0, unpaid: 0, count: 0 };
const m = byMonth[key];
m.billed += Number(r[col["請求額"]]) || 0;
m.net += Number(r[col["手取り(自動)"]]) || 0;
if (!r[col["入金日"]]) m.unpaid += Number(r[col["請求額"]]) || 0;
m.count++;
}
const out = [["月", "件数", "請求額合計", "手取り合計", "未回収額"]];
Object.keys(byMonth).sort().forEach((k) => {
const m = byMonth[k];
out.push([k, m.count, m.billed, Math.round(m.net * 100) / 100, m.unpaid]);
});
const sheet = ss.getSheetByName("サマリー") || ss.insertSheet("サマリー");
sheet.clearContents();
sheet.getRange(1, 1, out.length, out[0].length).setValues(out);
}
ここでは通貨をまたいだ合算をしていないことに注意してください。USDとJPYの請求が混在する場合は、通貨ごとにキーを分ける(${key} ${通貨}のようにする)か、入金後の実額で別列を作って合算するのが正確です。
なお、このコードはGASのV8ランタイム(現在のデフォルト)を前提にしています。古いRhinoランタイムのプロジェクトでは、アロー関数やconstが使えないので、「プロジェクトの設定」でV8を有効にしてください。
運用のコツ
- 請求を出したらその場で1行追加する。 後でまとめて入力しようとすると、必ず漏れます。
- 入金日は通知メールではなく実際の着金で入れる。 プラットフォームの「支払い完了」と口座への着金にはタイムラグがあることが多いです。
- 手数料率は契約ごとに確認する。 経路やクライアントによって違うことがあるので、思い込みで同じ数字をコピーしないようにします。
- シートは自分専用にしておく。 共有設定を間違えると、取引先や金額が見えてしまいます。
まとめ
- 請求一覧シートで、手数料率・送金手数料を行ごとに入れて手取りを自動計算する
- GASの時間主導トリガーで、期限超過の請求を毎朝自分にメール通知する
- 月初にサマリーシートを自動生成して、請求額・手取り・未回収額を把握する
- 手数料率や為替レートはハードコードせず、確認した実際の値を使う
数十行のスクリプトですが、「入金されたっけ?」と不安になる時間がほぼなくなります。お金の管理が楽になると、本業の開発に集中できますよ。
Abid Ali:パキスタンのWeb開発者・フリーランス。初心者向けのフリーランスガイドをDigital Dialogueで書いています。
