この事例は架空の見本です。登場する社名「コトリ商事」・支店・商品・売上数値はすべてこのデモのために作成した架空の見本値で、実在の企業・実務データとは一切関係ありません。処理時間のみ実測値です。

Excel VBA — Case Study / 新規開発

月次売上レポートの自動集計:
毎月 約2〜3時間の手作業を、ボタン1つ・約1.1秒に。

本業では、社内のExcelマクロ群の開発・保守・改修(新規作成を含む)を担当しています。 そのコードは公開できないため、「新しくツールを作るとき、どんな進め方で・どんなコードを書くのか」を そのまま見ていただけるように、架空の題材でツールを1本、新規に開発しました。

題材は、架空の雑貨卸「コトリ商事」の月次売上レポート作成業務。 フォルダに溜まった日次売上CSVをボタン1つで一括取込し、月次集計からA4印刷用レポートの生成までを自動化します。

※「約2〜3時間」は手作業で行った場合の作業内訳からの想定値(内訳は下の比較表に記載)、 「約1.1秒」はこのデモでの実測値(複数回実行で1.07〜1.46秒)です。

INPUT

CSV 61ファイル
4,312行

当月30+前月比較用の前月31ファイル(架空データ)

処理時間(実測)

約1.1秒

取込〜レポート完成まで。VBA内Timer計測 1.07〜1.46秒

OUTPUT

A4縦1枚の
月次レポート

前月比・支店別・カテゴリ別・Top10・グラフ+印刷設定済み

01 — Problem

背景と問題:毎月のCSVコピペ集計、「ミスが怖い」まま続いていた

想定したのは、多くの会社で実際に起きている状況です。各支店の日次売上はCSVで共有フォルダに溜まっていく。 月初になると担当者がそれを1つずつ開き、集計ブックへコピペし、ピボットで支店別・カテゴリ別に集計し、 前月比を電卓や手入力の式で計算して、A4のレポートに整形する——。

ファイル数は前月分も合わせると60を超えます。時間がかかるのはもちろんですが、本当の問題は 「貼り付け漏れ・二重貼り付け・行ズレにその場で気づけない」こと。 数字が合わない月は原因探しでさらに時間が溶け、レポートの数字そのものへの信頼が揺らぎます。

02 — Solution

打ち手:フォルダ一括取込 → 集計 → A4レポート自動生成

「操作はボタン1つだけ。判断も入力もさせない」を方針に、処理を4段階に分けて自動化しました。 モジュールも処理の段階ごとに分割しています(全7モジュール)。

INPUT:フォルダに溜まった日次売上CSV(sales_*.csv・61ファイル)

  1. CSV一括取込 modImport.bas

    対象CSVを全件・ファイル名順に読み込み。列数が合わない行は取り込まずにスキップし、ファイルごとの取込行数・スキップ行数を「取込ログ」シートに記録します(=貼り付け漏れが起きない・起きても見える)。

  2. 月次集計 modAggregate.bas

    Dictionaryを使ったピボット相当の集計。4,312行を1回走査するだけで、月別合計・支店×月・カテゴリ×月・商品×月をまとめて集計します。

  3. レポート組立 modReport.bas

    KPI帯(当月・前月・前月比・数量)、支店別/カテゴリ別の内訳表、売上上位商品Top10、支店別の当月・前月比較グラフを生成。A4縦1ページに収まる印刷設定まで自動で行います。

  4. 結果の記録 modMain.bas

    処理時間と取込件数を操作パネルに書き残します。「前回いつ・何件処理したか」が後から確認できます。

OUTPUT:A4縦1枚の月次レポート(そのまま印刷・PDF化できる状態)

手作業とツールの比較
作業 手作業の場合(想定) このツール
① 61ファイルのCSVを開いて集計ブックへ転記 1ファイル1分強 × 61 ≒ 70分 自動(取込ログつき)
② 支店×カテゴリ×商品のピボット集計・前月比計算 30〜60分 自動(Dictionary集計)
③ A4レポートの表・グラフ整形 30〜60分 自動(印刷設定込み)
合計 約2〜3時間/月 実測 約1.1秒

※手作業の時間は上記内訳による想定値です。ツールの処理時間は、このデモの見本データ(61ファイル・4,312行)での実測値で、 VBA内のTimer計測で1.07〜1.46秒(複数回実行)、外部からのストップウォッチ計測でもほぼ同値(約1.1〜1.5秒)でした。

03 — Code

コードで見る実装品質:「読ませるコード」の実物抜粋

納品したコードは、作った本人がいなくても読める必要があります。 このデモのソースから、普段どおりの書き方をそのまま3か所抜粋しました (全文はページ末尾のソース一式で読めます。中略した箇所はコメントで明示しています)。

抜粋1:「変わりうる値」はコードに埋め込まない — 定数の一元管理

シート名の変更やフォルダの移動は、運用の中で必ず起きる仕様変更です。 シート名・パス・色などを modConfig に集約してあるため、その手の変更はこのモジュール1か所の修正で済みます。 各定数に「なぜこの値か」のコメントを添えるのも普段どおりです。

src/modConfig.bas(抜粋)
'=====================================================================
' modConfig : 定数集約モジュール
'---------------------------------------------------------------------
' シート名・フォルダ名・色などの「変わりうる値」をここに集約する。
' 仕様変更(シート名変更・フォルダ移動など)はこのモジュールの修正
' だけで済むようにしておく。
'=====================================================================

'--- シート名 ---------------------------------------------------------
Public Const SHEET_MENU As String = "操作パネル"
Public Const SHEET_RAW As String = "生データ"
Public Const SHEET_LOG As String = "取込ログ"
Public Const SHEET_REPORT As String = "月次レポート"

'--- CSV取込設定 ------------------------------------------------------
' ブックと同じ階層から見た相対パス(見つからない場合はフォルダ選択にフォールバック)
Public Const CSV_FOLDER_RELATIVE As String = "sample-data\csv"
Public Const CSV_FILE_PATTERN As String = "sales_*.csv"
Public Const CSV_COL_COUNT As Long = 8        ' 日付,支店,カテゴリ,商品コード,商品名,数量,単価,金額

'--- 色定数(RGB値は Const にできないため Long 直値で保持) -----------
Public Const COLOR_POSITIVE As Long = 25600       ' RGB(0, 100, 0)     緑(前月比プラス)
Public Const COLOR_NEGATIVE As Long = 192         ' RGB(192, 0, 0)     赤(前月比マイナス)

抜粋2:エラーで落ちても、Excelを「壊れた状態」にしない

高速化のために止めた画面更新・自動計算は、途中でエラーが起きても必ず元に戻します(SpeedUp / RestoreAppState の対)。 また、MsgBox をこの関数から出さないことで、ボタン実行と外部からの自動実行(COM)の両方に同じ本体を使えます。 「マクロを実行したらExcelの画面が固まったまま・自動計算が切れたまま」という定番トラブルを設計で防ぐ書き方です。

src/modMain.bas(抜粋)
'---------------------------------------------------------------------
' レポート生成の本体
'   戻り値: 処理時間(秒)。エラー時は -1
'   ※MsgBoxを出さないため、COM自動実行からはこちらを直接呼べる
'---------------------------------------------------------------------
Public Function RunReport() As Double
    Dim startTime As Single
    startTime = Timer
    mLastError = ""

    On Error GoTo ErrHandler
    SpeedUp

    ' 1) CSV一括取込
    Dim fileCount As Long, rowCount As Long
    Dim rawData As Variant
    rawData = ImportAllCsv(fileCount, rowCount)

    ' 2) 月次集計
    Aggregate rawData

    ' 3) レポート生成
    BuildReport fileCount, rowCount

    RestoreAppState

    ' 実行結果を操作パネルに記録
    Dim elapsed As Double
    elapsed = Timer - startTime
    If elapsed < 0 Then elapsed = elapsed + 86400   ' 日付またぎ対策
    WriteResultToMenu fileCount, rowCount, elapsed

    RunReport = elapsed
    Exit Function

ErrHandler:
    ' MsgBoxはここでは出さない(COM自動実行時にダイアログで止まるのを防ぐ)
    mLastError = Err.Description
    RestoreAppState
    RunReport = -1
End Function

抜粋3:Dictionaryによるピボット相当の集計 — 4,312行を1回の走査で

セルにいちいち書き込みながら集計するのではなく、配列を1回なめて Dictionary に集計してから、最後に一括で書き出します。 「月×支店」「月×カテゴリ」「月×商品」をキー文字列で持つことで、前月比の計算も同じ仕組みでそのまま引けます。 Dictionary は実行時バインディングにして、納品先のPCで参照設定が不要になるようにしています。

src/modAggregate.bas(抜粋)
'---------------------------------------------------------------------
' 生データ配列 (1 To N, 1 To 8) を集計する
'   列: 1=日付 2=支店 3=カテゴリ 4=商品コード 5=商品名 6=数量 7=単価 8=金額
'---------------------------------------------------------------------
Public Sub Aggregate(ByRef rawData As Variant)
    Set mDictMonth = CreateObject("Scripting.Dictionary")
    ' …(残り6つの Dictionary も同様に初期化。中略)…

    Dim r As Long
    For r = LBound(rawData, 1) To UBound(rawData, 1)
        Dim ym As String
        ym = Format$(rawData(r, 1), "yyyymm")
        Dim amount As Double, qty As Long
        amount = rawData(r, 8)
        qty = rawData(r, 6)

        AddTo mDictMonth, ym, amount
        AddTo mDictQtyMonth, ym, CDbl(qty)
        AddTo mDictBranch, ym & KEY_SEP & rawData(r, 2), amount
        AddTo mDictCategory, ym & KEY_SEP & rawData(r, 3), amount
        AddTo mDictProductAmt, ym & KEY_SEP & rawData(r, 4), amount
        AddTo mDictProductQty, ym & KEY_SEP & rawData(r, 4), CDbl(qty)
    Next r
    ' …(対象月=データ中の最新月の決定処理。中略)…
End Sub

'---------------------------------------------------------------------
' Dictionary への加算ヘルパ
'---------------------------------------------------------------------
Private Sub AddTo(ByVal dict As Object, ByVal key As String, ByVal value As Double)
    If dict.Exists(key) Then
        dict(key) = dict(key) + value
    Else
        dict.Add key, value
    End If
End Sub

このほかの読みどころ(ソース一式で確認できます)

  • セルへの逐次書き込みをしない配列一括転記modImport.bas / WriteRawSheet
  • 列数が合わない行はスキップして取込ログに件数を記録(黙って握りつぶさない) — modImport.bas / ReadOneCsv
  • 前月比の符号つき表示形式 +0.0%;-0.0% とプラス緑/マイナス赤の色分け — modReport.bas
  • 前月データが無い月は前月比を「―」表示(0除算を起こさない) — modReport.bas / RatioText
  • 二重呼び出しでも計算モードを壊さない SpeedUp / RestoreAppState の対称設計 — modUtil.bas
  • プリンタ未接続環境でも印刷設定で処理が止まらないガード — modReport.bas / SetupPrintLayout

04 — Before / After

視覚的証拠:入力のCSVから、完成レポートまで

画像はすべてこのデモの実物です(数値は架空の見本値)。クリック(タップ)で原寸のPNGが開きます。

BEFORE

入力:フォルダに溜まった日次売上CSV

sales_20260501.csv 〜 sales_20260630.csv の61ファイル・計4,312行。文字コードは実務で最も一般的な cp932(Shift_JIS)です。

sample-data\csv\sales_20260601.csv(61ファイルのうちの1つ)
日付,支店,カテゴリ,商品コード,商品名,数量,単価,金額
2026/06/01,東京本店,キッチン雑貨,KT-001,木製カッティングボード,30,1480,44400
2026/06/01,東京本店,キッチン雑貨,KT-002,ホーロー保存容器 3点セット,28,1980,55440
2026/06/01,東京本店,キッチン雑貨,KT-003,シリコン調理スプーン,56,520,29120
(…以下続く。これが61ファイル)
取込直後の生データシート。日付・支店・カテゴリ・商品コード・商品名・数量・単価・金額の8列にオートフィルタが設定されている
取込直後の「生データ」シート。61ファイル分を1シートに集約し、オートフィルタつきでそのまま確認できます。
AFTER

出力:ボタン1つで生成される月次レポート

KPI帯・支店別/カテゴリ別内訳(前月比は+緑/−赤)・Top10・比較グラフ。A4縦1枚の印刷設定済みで、そのまま提出できる状態です。

生成された月次売上レポート全体。上からタイトル帯、当月売上・前月売上・前月比・販売数量のKPI帯、支店別売上表、カテゴリ別売上表、売上上位商品Top10、支店別売上の当月・前月比較グラフ
生成された「月次レポート」シート全体(見本値)。前月比 +7.6% などの数値もマクロが計算・色分けしています。
CSV取込ログシート。ファイル名ごとに取込行数とスキップ行数が記録されている
「取込ログ」シート。どのファイルを何行取り込み、何行スキップしたかが毎回残ります。数字が合わないときの原因調査がここから始められます。
支店別売上の当月・前月比較の集合縦棒グラフ。6支店それぞれに当月と前月の2本の棒が並ぶ
支店別売上の当月・前月比較グラフ(Chart.Export で書き出した実物)。データの並び順は当月売上の降順で、マクロがソートしています。

05 — Source & Next

ソース一式と、実際のご依頼について

実際のご依頼でも、納品物はこのデモと同じ方針です—— VBAプロジェクトのロック(保護)なし・コメント付きソースコード一式・非エンジニアの方向けの操作手順書つきでお渡しします。 納品後にお客様側で中身を確認・修正できない「ブラックボックス納品」はいたしません。