【Project VBA極限活用】過負荷リソースの悲鳴を聞け!タスク自動分割による平準化エンジン実装ガイド
プロジェクトマネジメントの現場において、特定の優秀なエンジニアやキーパーソンにタスクが集中し、「Resource Overallocated(リソースの過負荷)」の赤字警告が常態化していないだろうか。
プロジェクトの成否は、リソースの稼働率(Allocation)をいかに100%(または設定された上限)の適正値内にコントロールし続けるかにかかっている。手動でガントチャートを見つめ、長大なタスクを切り刻み、別のリソースへ再割り当てする作業は、百害あって一利なしの悪しきルーティンだ。
今回は、Microsoft ProjectのVBA(Project VBA)を駆使し、過負荷状態にあるリソースのタスクを検出し、指定された日数・期間で自動的に分割(Split)・平準化するプロダクションコードを授与する。
単なる「動くコード」ではない。Project特有のスケジューリングエンジン(Task Mode)の挙動、カレンダーの罠、オブジェクトのライフサイクルを完全に掌握したプロフェッショナルだけの知見をここに解き放つ。
—
1. なぜ「手動のタスク分割」と「標準レベリング」は現場を破綻させるのか?
プロジェクト管理ツールの標準機能である「レベリング(Leveling)」に全幅の信頼を置いていないだろうか? 標準レベリングは、タスクを遅延(Delay)させることで過負荷を解消しようとするが、これには致命的な欠陥がある。
- 致命的な納期遅延: タスクを後ろにずらすため、クリティカルパスが押し出され、最終納期がズルズルと後ろ倒しになる。
- リソースの遊休と断片化: 単純な遅延では、キャパシティを超えた瞬間の根本的な解決になっておらず、かえってプロジェクトの流動性を殺す。
我々が求めるべきは、「巨大すぎる作業の塊(モンスタータスク)を適切なサイズにぶ切りにし、ワークロードを分散させること」だ。しかし、これをGUIでやろうとすると、分割位置の計算、残存工数の再配分、リンク関係(FS, SS等)の再構築で膨大な工数が吹き飛ぶ。
だからこそ、VBAでロジックをコード化し、一瞬で「平準化の最適解」を弾き出す必要がある。
—
2. 堅牢な自動分割エンジンの設計思想と実装上の罠
Project VBAでタスクを操作する際、開発者が必ず踏む地雷がいくつかある。これらを回避しなければ、実務で使えるツールにはならない。
① タスクモード(Task Mode)の罠
Microsoft Project 2010以降、タスクには「手動調整(Manual)」と「自動スケジュール(Auto Scheduled)」が存在する。手動調整タスクに対してVBAで強引に期間や分割操作を行うと、スケジューリングエンジンが予期せぬ日付を算出し、データが破損する。
【対策】 自動分割の対象となるタスクは、強制的に `TaskMode = pjTaskModeAuto` へ昇格させる。
② 分割(Split)メソッドの厳密性
ProjectのTaskオブジェクトには `Split` メソッドが存在する。
`Task.Split StartDate, FinishDate`
しかし、このメソッドは「指定した期間だけタスクを中断する」ものであり、タスクそのものを2つの独立した別タスクに分裂させるわけではない。したがって、「元のタスクを短縮し、残りの工数を持つ後続タスクを新規作成してリンクする」というアプローチをとる方が、コスト管理や進捗管理(EVMS)において圧倒的に堅牢である。
—
3. 【プロダクションコード】過負荷タスク自動分割・平準化マクロ
以下のコードは、指定した閾値(例: 割り当てが100%を超える、または特定リソースの稼働時間が規定値オーバー)を超えるタスクを検出し、指定日数ごとに自動分割して負荷を分散させる実用スクリプトだ。
‘ ==============================================================================
‘ Module: ModResourceLevelingEngine
‘ Theme: 過負荷リソースのタスク自動分割・平準化スクリプト
‘ Description: 指定されたリソースの過負荷タスクを検出し、指定日数で分割・再配置する
‘ ==============================================================================
Option Explicit
Public Sub ExecuteTaskSplittingLeveling()
‘ — 定数定義 —
Const TARGET_RESOURCE_NAME As String = “シニアエンジニアA” ‘ 監視対象リソース
Const MAX_ALLOWED_UNITS As Double = 1# ‘ 許容最大アサイン率 (1.0 = 100%)
Const SPLIT_DURATION_DAYS As Double = 5# ‘ 1タスクあたりの最大継続日数(例: 5日単位で分割)
Dim t As Task
Dim assn As Assignment
Dim targetAssnCol As Collection
Set targetAssnCol = New Collection
‘ 1. アクティブプロジェクトの存在確認
If ActiveProject.Tasks.Count = 0 Then
MsgBox “有効なプロジェクトが開かれていません。”, vbCritical, “エラー”
Exit Sub
End If
‘ 2. ターゲットリソースの過負荷アサインメントを抽出
On Error GoTo ErrorHandler
Application.ScreenUpdating = False
Dim j As Long
For Each t In ActiveProject.Tasks
If Not t Is Nothing Then
If Not t.Summary Then ‘ サマリータスクは除外
For Each assn In t.Assignments
If assn.ResourceName = TARGET_RESOURCE_NAME Then
‘ 単位(Units)が許容値を超えている、または稼働に無理がある場合をキャッチ
If assn.Units > MAX_ALLOWED_UNITS Then
targetAssnCol.Add t
Exit For ‘ 同一タスクに複数アサインされていても1回として処理
End If
End If
Next assn
End If
End If
Next t
If targetAssnCol.Count = 0 Then
MsgBox “対象リソース [” & TARGET_RESOURCE_NAME & “] の過負荷タスクは検出されませんでした。”, vbInformation, “完了”
GoTo CleanUp
End If
‘ 3. 検出されたタスクの自動分割処理
Dim currentTask As Task
Dim newSubTask As Task
Dim originalDuration As Double
Dim remainingDuration As Double
Dim splitCount As Long
For j = 1 To targetAssnCol.Count
Set currentTask = targetAssnCol(j)
‘ 自動スケジュールモードへ強制変更(スケジューリングの整合性を担保)
currentTask.TaskMode = pjTaskModeAuto
originalDuration = currentTask.Duration / 480 ‘ 分単位(Minutes)から日単位(Days)へ換算 (1日=480分前提)
‘ 許容日数より大きいタスクのみ分割対象とする
If originalDuration > SPLIT_DURATION_DAYS Then
remainingDuration = originalDuration – SPLIT_DURATION_DAYS
‘ 元のタスクを指定日数に短縮
currentTask.Duration = SPLIT_DURATION_DAYS 480
‘ 分割された後続タスクを同一プロジェクト内(直下の行)に作成
‘ ※ProjectのID採番の挙動を考慮し、元のタスクの終了日の翌日から開始させる
Set newSubTask = ActiveProject.Tasks.Add(currentTask.Name & ” (分割)”, currentTask.ID + 1)
newSubTask.TaskMode = pjTaskModeAuto
newSubTask.Duration = remainingDuration 480
‘ predecessor(先行タスク)の設定:元タスク完了後に新タスクが始まるようリンク(FS)
newSubTask.Predecessors = CStr(currentTask.ID)
‘ 該当リソースを新しいサブタスクにも同じ比率で割り当て(必要に応じて調整)
Dim newAssn As Assignment
Set newAssn = newSubTask.Assignments.Add(ResourceID:=GetResourceIDByName(TARGET_RESOURCE_NAME))
newAssn.Units = MAX_ALLOWED_UNITS ‘ 平準化された上限値で再アサイン
End If
Next j
Application.ScreenUpdating = True
MsgBox “過負荷タスクの分割と平準化が完了しました。スケジュールを確認してください。”, vbInformation, “処理成功”
Exit Sub
ErrorHandler:
Application.ScreenUpdating = True
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “致命的エラー”
CleanUp:
Set targetAssnCol = Nothing
End Sub
‘ — ヘルパー関数: リソース名からResourceIDを取得 —
Private Function GetResourceIDByName(ByVal resName As String) As Long
Dim r As Resource
For Each r In ActiveProject.Resources
If Not r Is Nothing Then
If r.Name = resName Then
GetResourceIDByName = r.ID
Exit Function
End If
End If
Next r
GetResourceIDByName = 0
End Function
—
4. プロフェッショナルが教える運用上の注意点
このスクリプトを実際のプロジェクトリポジトリやエンタープライズ環境(Project Server / Project Online)で運用する際には、以下の点に厳意な注意を払う必要がある。
1. カレンダーと単位(Minutes)の魔力
Microsoft Projectの内部データは、期間(Duration)や工数(Work)をすべて「分(Minutes)」で保持している。コード内で `480`(8時間×60分)で割っているのはそのためだ。組織の稼働日設定(カレンダー)が変則的な場合、この係数は慎重に調整しなければならない。
2. データベース連携(EPM / Project Online)時の排他制御
ローカルMMPファイルであれば上記のコードで完結するが、Project Server等のPWA環境下でこれを実行する場合、サーバー側のチェックアウト(Check-Out)状態やエンタープライズリソースプールの権限に阻まれることがある。サーバーサイドでバッチ処理を行う場合は、COMアドインやPWA REST API経由でのアプローチを検討すべきだ。
3. リンク関係の連鎖
タスクを分割した際、もともと設定されていた後続タスク(Successors)の依存関係が「元タスク」にぶら下がったままになる。厳密なクリティカルパスを維持するためには、分割された新タスク側へ後続タスクのリンク付け替えロジックを拡張する必要がある。
—
5. おわりに:現場を救うのは「場当たり的な気合」ではなく「構造化された自動化」だ
「人が足りない」「スケジュールが無理だ」と嘆く前に、システムが持つデータを正しく解釈し、アルゴリズムによって構造的な負荷分散を行うこと。それが、真にモダンなプロジェクトマネジメントを支えるエンジニアの役割である。
今回提供したコードをベースに、自社のプロジェクト体制やリソースプールに合わせたカスタムロジックを組み込み、泥臭いリソース調整から解放される環境を構築してほしい。
