株式会社UKMETAのエンジビズ開発チームです。フリーランスエンジニア向けの案件サイトを運営しています。
何を作ったか
SES(準委任)の請求書には「精算幅」という業界特有の計算があります。契約が 140〜180h なら、180h を超えた分は加算、140h に満たない分は減額。その時間単価が 中間割(月額 ÷ 中央値)か 上下割(超過は ÷ 上限、控除は ÷ 下限)かで変わり、弊社の掲載案件 987 件ではちょうど半々でした。
freee にも Misoca にもこの計算はないので、Claude Code(Opus 5)に Excel の請求書テンプレートを渡して作ってもらいました。
Gatsby + TypeScript の既存サイトに 1 ページ追加。計算ロジック、フォーム、A4 プレビュー、インボイス対応、localStorage 保存まで、最初の PR は実働 1 日でした。Excel の =rounddown(I17*G17,0)+... という数式まで読んで同じ計算にしてくれたのは正直驚きました。
ここまでは AI がほぼ全部やっています。この記事は、その後 AI が気づけず、人間が PDF を開いて初めて分かった 3 つ の話です。
1. 印刷したら 5 ページになった
最初のコードは、ブラウザ印刷の定番パターンでした。
@media print {
body * { visibility: hidden; }
#invoice, #invoice * { visibility: visible; }
#invoice { position: absolute; left: 0; top: 0; }
}
PDF に保存すると 5 ページ。visibility: hidden は描画を消すだけで高さは残るので、フォームや FAQ の分だけ白紙が続きます。AI はこの CSS を「正しい」と判断して出しています。実際、Web 上のサンプルはほぼこの書き方です。
直し方は、印刷用の請求書を body 直下に Portal で描画し、他を display: none にすること。
{printRoot && createPortal(<InvoiceDocument {...props} />, printRoot)}
#invoice-print-root { display: none; }
@media print {
body > *:not(#invoice-print-root) { display: none !important; }
#invoice-print-root { display: block; }
}
これで 1 ページ。さらに PDF を見ると、表の見出しの背景色が消えている(Chrome は背景を印刷しない → print-color-adjust: exact)、右端の罫線だけ細い(<colgroup> の幅合計が 102% で右端が欠けていた)も見つかりました。どれも 印刷して目で見ないと分からない 種類のバグです。
2. right: 8 が 512px になった
数値入力の右端に「円」「h」を重ねる、ありふれた UI です。
<span sx={{ position: 'absolute', right: 8 }}>円</span>
これが画面外に飛びました。theme-ui では top / right / bottom / left の数値は space スケールのインデックス で、right: 8 は theme.space[8] = このプロジェクトでは 512px。right: '8px' で解決です。
AI は theme-ui の sx を大量に書いているのに、ここは素の CSS の感覚で書いていました。margin/padding のスケール変換は知っていても、position 系にも効くことは「知識としては持っていても、書くときに適用されない」典型でした。
3. 税込契約の請求書を税込で書いた
契約が「月額 88 万円(税込)」の案件があります。最初の実装は、入力された税込金額をそのまま単価欄に書き、「うち消費税 80,000」と添える形でした。計算としては合っています。
でも SES の請求書は 税抜単価で書く のが慣例で、超過・控除の時間単価も税抜に換算します。税込のまま出すと経理に差し戻されます。これは調べないと分からない業務ルールで、AI は聞かれなければ「正しい計算」を返すだけです。
換算すると端数の問題が出ます。税込 850,000 ÷ 1.1 = 772,727.27…。税抜小計 × 10% を足し直しても 850,000 に戻らず、合計が契約額とズレます。
// 税込 → 税抜は切り捨て、消費税は「税込合計 − 税抜小計」の差額で出す
const toNet = (gross: number, rate: number) =>
Math.floor((gross * 100) / (100 + rate) + 1e-9)
const taxOf = (rate: 10 | 8) =>
inclusive
? grossByRate[rate] - subtotalByRate[rate]
: roundBy(subtotalByRate[rate] * (rate / 100), taxRounding)
税抜 772,727、消費税 77,273、合計 850,000。インボイスの「税率ごとに端数処理は 1 回」も満たします。方針を伝えたあとの実装と、この境界値のテストは AI が一発で書きました。
まとめ
- Opus 5 は、仕様(Excel)があれば業務ツールを 1 日で形にする。計算の再現精度は高い
- 気づけなかったのは 出力を物理的に見ないと分からないもの(印刷)と、聞かれないと出てこない業務慣習(税務)
- つまり人間の仕事は「作る」より「刷って見る」「経理に聞く」側に寄った
月末に請求書を書く方は、よければ使ってみてください。入力内容はブラウザの外に出ません。