【実務・中級編】【中級者向け】リソースの単価計算をカスタムフィールドで自動化するロジック – Project VBA解析バイブル

スポンサーリンク
スポンサーリンク

ループを回すな、ロジックを埋め込め:MS Projectにおける「動的コスト計算エンジン」の設計思想

MS ProjectのVBA開発において、多くの開発者が陥る「初心者特有の罠」がある。それは、「全リソースをループで回し、If文で条件分岐させ、値を一つずつセルに書き込む」という手法だ。

確かに、そのコードは動く。しかし、それは「自動化」ではない。単なる「人間が手で行っていた作業の、低速なデジタルコピー」に過ぎない。リソースが1,000人を超えた時、あるいは単価体系が複雑化した時、そのコードはたちまち保守不能なスパゲッティと化し、プロジェクトの進行を阻害する負債となる。

真のアーキテクトが目指すべきは、「データ(属性)」と「ロジック(計算式)」を分離し、MS Project自身の計算エンジンを最大限に活用する設計である。

本記事では、リソースの属性に基づき、カスタムフィールドを用いて動的にコスト計算ロジックを構築する、プロフェッショナル向けの設計手法を伝授する。

1. 凡庸な設計と、堅牢な設計の決定的な違い

まず、君が書こうとしているコードがどちらの設計思想に近いか、自問自答してほしい。

【非効率な設計:命令型アプローチ】

' 典型的な「やってはいけない」コードのイメージ
For Each r In ActiveProject.Resources
    If r.Name = "Senior Engineer" Then
        r.Cost = r.Work * 10000
    ElseIf r.Name = "Junior Engineer" Then
        r.Cost = r.Work * 5000
    End If
Next r

この設計には、致命的な欠陥が3つある。
1. 計算の遅延: プロジェクトが大規模になると、ループの実行時間が指数関数的に増大する。
2. ロジックの硬直性: 単価が変わるたびにVBAコードを修正・再デプロイする必要がある。
3. 計算の不整合: VBAで書き込んだ「結果」は、その後のWork(工数)の変更を検知して自動更新されない。

【理想的な設計:宣言型アプローチ】

プロフェッショナルは、VBAを使って「値を書き込む」のではなく、「計算式(Formula)をセットアップする」。
リソースの属性(例えば `Number1` に格納されたレート)を定義し、カスタムフィールドに `[Number1] * [Work]` という式を流し込む。これにより、計算の実行主体はVBAからMS Projectの計算エンジンへと移譲される。

2. アーキテクチャの設計図:メタデータ駆動型計算

我々が構築するのは、以下の3層構造を持つシステムだ。

1. Data Layer (Metadata): リソースの属性(単価、スキルレベル、コスト係数など)を格納するカスタムフィールド。
2. Logic Layer (Formula): カスタムフィールドに設定される計算式。
3. Orchestration Layer (VBA): 外部データ(CSV/DB)から属性を流し込み、適切な計算式をフィールドに適用する制御プログラム。

この設計により、単価が変わっても「Data Layer」を更新するだけで済み、計算ロジックが変わっても「Logic Layer」を書き換えるだけで済む。VBAは、その橋渡しをするだけの存在になるのだ。

3. 実装:プロダクション・レディなコード例

以下に、リソースの属性(`Number1` を単価として使用)に基づき、カスタムフィールド(`Cost_Auto`)に動的な計算式を適用する、堅牢な実装例を示す。

Option Explicit

' ==============================================================================
' Module: ResourceCostEngine
' Description: リソース属性に基づき、カスタムフィールドに動的な計算式を設定する
' Author: Chief Architect
' ==============================================================================

' 定数定義:マジックナンバーを排除し、保守性を高める
Private Const FIELD_RATE_ATTR As String = "Number1"      ' 単価を格納する属性
Private Const FIELD_CALC_COST As String = "Cost_Auto"    ' 計算結果を表示するフィールド名
Private Const TARGET_FIELD_TYPE As Long = 2              ' 2 = Cost (コスト系フィールド)

Public Sub SetupResourceCostLogic()
    Dim r As Resource
    Dim targetField As CustomField
    Dim successCount As Long
    
    On Error GoTo ErrorHandler
    
    ' 1. 画面更新を停止し、パフォーマンスを最適化
    Application.ScreenUpdating = False
    
    Debug.Print "--- Cost Engine Initialization Started ---"

    ' 2. カスタムフィールドの存在確認と初期化
    ' ここでは、特定のカスタムフィールドが「計算式」を受け入れられる状態かを確認する
    ' 本来は、外部DB等から取得した単価をまずリソースの属性(Number1)にセットする処理が先行する
    
    ' 3. リソースループによる属性セット(本来は外部ファイル連携を推奨)
    ' ここではデモとして、リソース名に基づいて属性をセットする
    For Each r In ActiveProject.Resources
        If Not r Is Nothing Then
            Select Case r.Name
                Case "Senior Engineer"
                    r.Number1 = 12000 ' 単価
                Case "Junior Engineer"
                    r.Number1 = 5000  ' 単価
                Case Else
                    r.Number1 = 0
            End Select
        End If
    Next r

    ' 4. 【核心】計算式の動的注入
    ' リソースのWork(工数)とNumber1(単価)を掛け合わせる式を、
    ' MS Projectの計算エンジンに「宣言」する。
    ' 注意:フィールドの型が一致していないとエラーになるため、事前に検証が必要。
    
    ' CustomFieldオブジェクトへのアクセス(プロジェクト設定に依存)
    ' ※ここでは、あらかじめ「Cost_Auto」という名前のカスタムフィールドが
    '   プロジェクト内で定義されていることを前提とする。
    
    Set targetField = GetCustomFieldByName(FIELD_CALC_COST)
    
    If Not targetField Is Nothing Then
        ' 計算式を注入。これにより、リソースのWorkが変わるたびに自動再計算される。
        ' 数式内でリソースの属性を参照させるのがプロの技。
        targetField.Formula = "[Number1] * [Work]"
        Debug.Print "Logic Layer: Formula injected successfully."
    Else
        Err.Raise vbObjectError + 513, "SetupResourceCostLogic", _
            "Required Custom Field '" & FIELD_CALC_COST & "' not found."
    End If

    ' 5. 再計算の実行
    Application.CalculateFull

    Application.ScreenUpdating = True
    MsgBox "Cost Engine Setup Complete.", vbInformation
    Exit Sub

ErrorHandler:
    Application.ScreenUpdating = True
    MsgBox "Critical Error in Cost Engine:" & vbCrLf & Err.Description, vbCritical, "Engine Failure"
    ' 実務ではここでエラーログをファイルに書き出す処理を入れる
End Sub

' ------------------------------------------------------------------------------
' Function: GetCustomFieldByName
' Description: 名前からカスタムフィールドオブジェクトを取得する(堅牢な検索)
' ------------------------------------------------------------------------------
Private Function GetCustomFieldByName(ByVal fieldName As String) As CustomField
    Dim cf As CustomField
    For Each cf In ActiveProject.CustomFields
        ' 名前が一致するか、あるいは特定のタグが付いているかを確認
        If cf.Name = fieldName Then
            Set GetCustomFieldByName = cf
            Exit Function
        End If
    Next cf
    Set GetCustomFieldByName = Nothing
End Function

4. 伝説のアーキテクトが教える「落とし穴」への回避策

このコードを実戦投入する際、君が直面するであろう壁とその回避策を伝えておく。

① カスタムフィールドの「型」の不一致

MS Projectのカスタムフィールドには、`Text`, `Number`, `Date`, `Cost` といった厳格な型がある。`Cost` 型のフィールドに対して、文字列を返す数式を注入しようとすれば、即座にランタイムエラーで死ぬ。
対策: `GetCustomFieldByName` 関数内で、`cf.Type` をチェックし、期待する型と一致しているかバリデーションを行う工程を必ず組み込め。

② 計算の再帰ループ(Recalculation Overhead)

複雑な数式を多くのフィールドに設定すると、一つの値が変わるたびにプロジェクト全体の再計算が走り、動作が極端に重くなる。
対策:

  • 計算式は可能な限りシンプルに保て。
  • `Application.Calculation = xlCalculationManual`(※MS Projectにおける相当の制御)を検討せよ。
  • 頻繁に更新される値と、静的な値(マスターデータ)を明確に分離せよ。

③ 外部データ連携の整合性

実務では、単価はExcelやSQL Serverにあるはずだ。VBAでそれらを読み込む際、「リソースのユニークID(Resource ID)」をキーにせよ。名前(`r.Name`)をキーにすると、同姓同名のリソースが現れた瞬間に、コスト計算が崩壊する。

結言:保守性こそが最大のパフォーマンスである

優れたエンジニアは、コードの実行速度だけを追求しない。「変更に対する強さ」を追求する。

今回紹介した「メタデータ駆動型」の設計は、コード量を減らし、MS Projectのネイティブ機能を最大限に引き出す。これにより、君が作成したツールは、単なる「便利ツール」から、企業のインフラを支える「堅牢なシステム」へと昇華するのだ。

次にコードを書くときは、自分に問いかけてほしい。
「これは、人間がやっていた作業をなぞっているだけか? それとも、新しい計算の仕組みを構築しているのか?」と。

タイトルとURLをコピーしました