ねらい:明細から「請求書の自動組版→金額計算→PDF出力」まで一括で回す
請求書は「ヘッダ(請求先・請求番号・期間)」「明細(品目・数量・単価・小計)」「金額計算(小計→消費税→税込)」「体裁(罫線・書式)」「PDF出力」が一本で動くと事故が激減します。VBAなら“配列I/O+テンプレ貼り込み+安全な税計算+一括書き出し”で、枚数が多くても数秒で終わります。初心者でも貼って動くテンプレを、例題付きでかみ砕いて解説します。
入力仕様とシート構成(最低限で壊れない)
シート構成と列の意味
- Data(請求明細): A=請求先ID, B=請求先名, C=請求月, D=品目, E=数量, F=単価, G=税区分(課税/非課税/軽減)
例:C=2025-12(yyyy-mm)、税区分は“課税”“非課税”“軽減”のいずれか。 - Cust(請求先マスタ): A=請求先ID, B=請求先名, C=住所, D=担当者名, E=メール
- Template(請求書テンプレ): レイアウト固定。セルにプレースホルダ(例:{{INVOICE_NO}}, {{BILL_TO}}, {{PERIOD}})を置く。
重要ポイントの深掘り
請求書は「明細→集計→組版」の順で作ります。明細の税区分を列で持つと、課税・非課税の混在でも安全に合算できます。テンプレートは“枠と見出しがあるシート”を用意し、プレースホルダ置換で埋める方式にすると整形処理がシンプルになります。
共通基盤:配列I/O・安全な税計算・プレースホルダ置換
ユーティリティと税計算
' ModInv_Base.bas
Option Explicit
Public Function ReadRegion(ws As Worksheet, Optional startAddr As String = "A1") As Variant
ReadRegion = ws.Range(startAddr).CurrentRegion.Value
End Function
Public Sub WriteBlock(ws As Worksheet, a As Variant, startAddr As String)
ws.Range(startAddr).Resize(UBound(a, 1), UBound(a, 2)).Value = a
End Sub
Public Function Norm(ByVal s As Variant) As String
Norm = LCase$(Trim$(CStr(s)))
End Function
Public Function TaxRate(ByVal taxType As String) As Double
Select Case Norm(taxType)
Case "課税": TaxRate = 0.1 ' 10%
Case "軽減": TaxRate = 0.08 ' 8%
Case Else: TaxRate = 0#
End Select
End Function
Public Function RoundMoney(ByVal amount As Double, Optional ByVal mode As String = "round") As Double
Select Case LCase$(mode)
Case "ceil": RoundMoney = WorksheetFunction.RoundUp(amount, 0)
Case "floor": RoundMoney = WorksheetFunction.RoundDown(amount, 0)
Case Else: RoundMoney = WorksheetFunction.Round(amount, 0)
End Select
End Function
Public Sub ReplacePlaceholder(ws As Worksheet, ByVal key As String, ByVal val As String)
Dim c As Range
For Each c In ws.UsedRange.Cells
If InStr(1, CStr(c.Value), "{{" & key & "}}", vbTextCompare) > 0 Then
c.Value = Replace(CStr(c.Value), "{{" & key & "}}", val, , , vbTextCompare)
End If
Next
End Sub
Public Function EnsureSheet(ByVal name As String) As Worksheet
Dim ws As Worksheet
On Error Resume Next: Set ws = Worksheets(name): On Error GoTo 0
If ws Is Nothing Then Set ws = Worksheets.Add: ws.Name = name
ws.Cells.Clear
Set EnsureSheet = ws
End Function
VB重要部分の深掘り
消費税は「税区分→税率→小計ごとに税計算→端数処理」を順番に。丸めはルール(四捨五入/切り捨て/切り上げ)を冒頭で決め、関数にまとめます。プレースホルダ置換を使うとテンプレートの見出し文言を壊さずに差し込みできます。
請求書テンプレートのレイアウト(項目と差し込み位置)
テンプレの最低限項目
- ヘッダ: 請求書、請求番号、請求日、請求期間、請求先名、住所、担当者
- 明細表: 品目、数量、単価、金額(小計)。明細は「B15:D?」のように固定開始セルで表現。
- サマリ: 課税対象小計、非課税小計、消費税額、合計(税込)
重要部分の深掘り
明細の開始セルと列順を固定すると、コード側は「ヘッダ差し込み→明細一括貼り→サマリ計算→書式適用」で完結します。テンプレの枠線・社印・注意書きはシート側に置き、VBAは“データだけ差し込む”。これが壊れない鉄則です。
請求書自動生成:請求先×請求月でグループ化し、PDFまで出力
生成と出力テンプレ(貼って動く)
' ModInv_Generate.bas
Option Explicit
Public Sub GenerateInvoices(ByVal billMonth As String, Optional ByVal roundMode As String = "round")
Dim wsData As Worksheet: Set wsData = Worksheets("Data")
Dim wsCust As Worksheet: Set wsCust = Worksheets("Cust")
Dim wsTpl As Worksheet: Set wsTpl = Worksheets("Template")
Dim data As Variant: data = ReadRegion(wsData)
Dim cust As Variant: cust = ReadRegion(wsCust)
' 請求先ID→顧客情報
Dim mapCust As Object: Set mapCust = CreateObject("Scripting.Dictionary")
Dim r As Long
For r = 2 To UBound(cust, 1)
mapCust(Norm(cust(r, 1))) = Array(cust(r, 2), cust(r, 3), cust(r, 4), cust(r, 5)) ' 名,住所,担当,メール
Next
' グループ化:請求先ID×請求月 → 明細行の行番号
Dim groups As Object: Set groups = CreateObject("Scripting.Dictionary")
For r = 2 To UBound(data, 1)
If Norm(data(r, 3)) = Norm(billMonth) Then
Dim k As String: k = Norm(data(r, 1)) & "|" & Norm(data(r, 3))
If Not groups.Exists(k) Then
Dim col As New Collection: col.Add r: Set groups(k) = col
Else
groups(k).Add r
End If
End If
Next
If groups.Count = 0 Then
MsgBox "対象月の明細がありません: " & billMonth, vbExclamation
Exit Sub
End If
' 出力シート準備
Dim wsOut As Worksheet: Set wsOut = EnsureSheet("Invoices_" & billMonth)
' 請求番号の連番(例:INV-YYYYMM-001〜)
Dim seq As Long: seq = 1
Dim k As Variant, outRow As Long: outRow = 1
For Each k In groups.Keys
Dim parts() As String: parts = Split(CStr(k), "|")
Dim custId As String: custId = parts(0)
' 顧客情報
Dim ci As Variant
If mapCust.Exists(custId) Then
ci = mapCust(custId)
Else
ci = Array("不明", "", "", "")
End If
' テンプレを複製して埋める(1枚ずつ)
wsTpl.Copy After:=wsOut.Parent.Sheets(wsOut.Parent.Sheets.Count)
Dim wsInv As Worksheet: Set wsInv = wsOut.Parent.Sheets(wsOut.Parent.Sheets.Count)
wsInv.Name = "INV_" & billMonth & "_" & Format$(seq, "000")
' ヘッダ差し込み
ReplacePlaceholder wsInv, "INVOICE_NO", "INV-" & Replace(billMonth, "-", "") & "-" & Format$(seq, "000")
ReplacePlaceholder wsInv, "BILL_TO", CStr(ci(0))
ReplacePlaceholder wsInv, "ADDRESS", CStr(ci(1))
ReplacePlaceholder wsInv, "CONTACT", CStr(ci(2))
ReplacePlaceholder wsInv, "PERIOD", billMonth
ReplacePlaceholder wsInv, "DATE", Format$(Date, "yyyy-mm-dd")
' 明細抽出と配列化
Dim col As Collection: Set col = groups(k)
Dim n As Long: n = col.Count
Dim lines() As Variant: ReDim lines(1 To n, 1 To 4) ' 品目,数量,単価,金額
Dim i As Long
Dim taxedSum As Double: taxedSum = 0#
Dim nonTaxedSum As Double: nonTaxedSum = 0#
Dim taxAmt As Double: taxAmt = 0#
For i = 1 To n
Dim rr As Long: rr = col(i)
Dim qty As Double: qty = CDbl(data(rr, 5))
Dim price As Double: price = CDbl(data(rr, 6))
Dim subTot As Double: subTot = qty * price
lines(i, 1) = data(rr, 4)
lines(i, 2) = qty
lines(i, 3) = price
lines(i, 4) = subTot
Dim tRate As Double: tRate = TaxRate(CStr(data(rr, 7)))
If tRate > 0 Then
taxedSum = taxedSum + subTot
taxAmt = taxAmt + subTot * tRate
Else
nonTaxedSum = nonTaxedSum + subTot
End If
Next
taxAmt = RoundMoney(taxAmt, roundMode)
Dim grand As Double: grand = taxedSum + nonTaxedSum + taxAmt
' 明細貼り込み(テンプレの明細開始セルをB15に仮定)
wsInv.Range("B15").Resize(n, 4).Value = lines
' サマリ差し込み(セルを固定想定:税対象小計=F15、非課税=F16、税額=F17、合計=F18)
wsInv.Range("F15").Value = taxedSum
wsInv.Range("F16").Value = nonTaxedSum
wsInv.Range("F17").Value = taxAmt
wsInv.Range("F18").Value = grand
' 書式
ApplyInvoiceFormatting wsInv, n
' PDF出力(任意)
Dim pdfPath As String: pdfPath = ThisWorkbook.Path & "\" & wsInv.Name & ".pdf"
ExportInvoicePdf wsInv, pdfPath
seq = seq + 1
Next
MsgBox "請求書の自動生成(" & billMonth & ")が完了しました。", vbInformation
End Sub
Private Sub ApplyInvoiceFormatting(ByVal ws As Worksheet, ByVal lineCount As Long)
With ws
.Range("B15:D" & 14 + lineCount).NumberFormatLocal = "#,##0"
.Range("E15:E" & 14 + lineCount).NumberFormatLocal = "#,##0"
.Range("F15:F18").NumberFormatLocal = "#,##0"
.Range("B15:E" & 14 + lineCount).Borders.LineStyle = xlContinuous
End With
End Sub
Private Sub ExportInvoicePdf(ByVal ws As Worksheet, ByVal pdfPath As String)
With ws.PageSetup
.Orientation = xlPortrait
.Zoom = False
.FitToPagesWide = 1
.FitToPagesTall = 1
.LeftMargin = Application.CentimetersToPoints(1)
.RightMargin = Application.CentimetersToPoints(1)
.TopMargin = Application.CentimetersToPoints(1)
.BottomMargin = Application.CentimetersToPoints(1)
End With
ws.ExportAsFixedFormat Type:=xlTypePDF, Filename:=pdfPath, Quality:=xlQualityStandard, OpenAfterPublish:=False
End Sub
VB重要部分の深掘り
- 明細の税区分が混在しても「小計→課税・非課税に分離→税額合算→丸め→税込合計」の順なら崩れません。
- テンプレを複製して差し込む方式は“枠・ロゴ・朱書き”を壊さない。差し込みセル番地はテンプレ設計で固定しておきます。
- PDF出力は印刷設定をコードで制御。毎回同じ見た目が担保され、配布導線が安定します。
例題の通し方:請求月指定→請求先ごとに1枚ずつ生成→PDF保存
実行例
Sub Demo_RunInvoices()
' 請求月を“yyyy-mm”で指定
GenerateInvoices "2025-12", "round" ' 端数は四捨五入(ceil/floorも選択可能)
End Sub
VB期待動作:Dataの2025-12分が請求先ごとに集計され、Templateから複製されたシートにヘッダ・明細・サマリが差し込まれ、Invoices_2025-12ブック内に各請求書シートが並び、同名のPDFが出力されます。
落とし穴と対策(深掘り)
税の丸め位置・順序のブレで合計が合わない
小計×税率→税額合算→丸め→税込合計、の順序に固定します。行単位で丸めると端数差が増えるため、まずは合算後に丸める方針が安全です。
テンプレのセル番地が変わって壊れる
明細開始セル(例:B15)、サマリセル(F15〜F18)をテンプレ設計書に明示し、変更時はコード側の定数を一箇所で差し替えできるようにします。
書式の揺れ(千区切り・桁ズレ)
NumberFormatLocalを必ず適用。金額列は“#,##0”で統一し、貼り込み後に自動幅調整と罫線を入れて視認性を確保します。
請求先マスタ欠損・住所未登録
欠損は「不明/空」で埋めて請求書を作り、監査用に欠損リストも出力する仕組みを追加すると修正が早まります(必要なら後付け可能)。
セル逐次書きで遅い
明細は配列でまとめて貼る→サマリは値だけ差し込み。これで大型データでもUIが固まりません。
まとめ:テンプレ複製+差し込み+税計算+PDFの型で、毎月の請求を“同じ品質”にする
請求先×月でグループ化し、テンプレへヘッダ・明細・サマリを差し込み、税計算は順序と丸めを固定、最後にPDFへ。見た目をテンプレ側に寄せ、VBAは“正確に差し込む”だけにすると壊れません。
