mps project vba how to summarize task predecessors work complete - vba

I have few tasks in ms project. They are grouped with few summary tasks, so the progress of work completed is tracked by those summary tasks. I need to add one separate single task which will summarize all progress from some of the summary tasks and some of single tasks. All tasks that need to be summarized will be linked as predecessors for this final summary task.
I've wrote code like this bellow but I'm struggling to make it work as intended.
Sub SumProgress()
Dim t As Task, subt As Task
Dim NumSub As Integer, TotalProgres As Integer, TaskProgres As Integer
Set area = ActiveProject.Tasks
For Each t In area
If t.Flag10 = True Then
NumSub = Int(t.PredecessorTasks.Count)
For Each subt In t.PredecessorTasks
TotalProgres = TotalProgres + Int(subt.PercentComplete)
Next subt
t.PercentComplete = TotalProgress / NumSub
End If
Next t
End Sub

There are two bugs in the code:
TotalProgres was not reset to 0 before each new progress summary task was calculated.
The variable was declared as "TotalProgres" but later it is used as "TotalProgress", different spelling = different variable. Always use the Option Explicit to easily avoid this kind of bug. See What do Option Strict and Option Explicit do?
Here is the code with those issues fixed:
Option Explicit
Sub SumProgress()
Dim area As Tasks
Dim t As Task, subt As Task
Dim NumSub As Integer, TotalProgress As Integer
Set area = ActiveProject.Tasks
For Each t In area
If Not t Is Nothing Then
If t.Flag10 = True Then
TotalProgress = 0
NumSub = Int(t.PredecessorTasks.Count)
For Each subt In t.PredecessorTasks
TotalProgress = TotalProgress + Int(subt.PercentComplete)
Next subt
t.PercentComplete = TotalProgress / NumSub
End If
End If
Next t
End Sub

Related

MS Project VBA - finding the last row of the last sub project inserted in a master project

I have a simple Master plan with 3 inserted small plans as a prototype for a much larger and more complex project.
I want to find out what the value in Text1 is for the last item in the master / sub project plan.
I have a macro which links up dependencies across the sub plans based on a unique reference - loop through the tasks, when you find a reference loop through all the tasks again to find a match and build the dependency link.
This works brilliantly unless there isn't a matching reference in the plan (for instance when there is an external dependency which doesn't appear in the sub plans). At this point it just links to the last item that it found which is not good.
To get around this I have established how many rows there are in the plan and will ignore anything which is returned at the end of the "sub search"
''''
For Each t In ActiveProject.Tasks
If t Is Nothing Then
'do nothing
Else
If LCase(t.Text1) = LCase("Dep_in") Then
ref = t.Text2
n = 0
For Each t_check In ActiveProject.Tasks
n = n + 1
If t_check Is Nothing Then
'do nothing
Else
If LCase(t_check.Text2) = LCase(ref) And LCase(t_check.Text1) = LCase("Dep_out") Then
ID = t_check.ID
Source = t_check.Project
If n < max_tasks Then t.ConstraintType = pjASAP
If n < max_tasks Then t.Predecessors = Dep_path & Source & ".mpp\" & ID
End If
End If
Next t_check
End If
End If
Next t
The issue with this method is that if there is a legitimate Deliverable on the last row of the last sub plan it will never be picked up.
Unless there is a neat way to handle the situation where there isn't a match in the sub loop how can I test the lastrow.text1 to see if it contains DEP and if so issue a message warning of this fact?
The only way I can think to do this would be the rather inelegant:
n = 0
For Each t In ActiveProject.Tasks
If t Is Nothing Then
'do nothing
Else
n = n + 1
End If
Next t
max_tasks = n
n = 0
For Each t In ActiveProject.Tasks
If t Is Nothing Then
'do nothing
Else
n = n + 1
If n = max_tasks Then Debug.Print t.Name
End If
Next t
Thanks
When working with master projects it is important to remember that the Tasks collection only contains the tasks in the master project. In the example posted in the question, ActiveProject.Tasks will contain 3 tasks--one for each of the subprojects.
To loop through all of the tasks, expand the schedule so that all are shown, select all, then loop through the selection.
FilterClear
SelectAll
OutlineShowAllTasks
SelectAll
Set allTasks = ActiveSelection.Tasks
To find the matching task to link, there are at least two options: 1) make a copy of the collection of tasks (allTasks2) and loop through that, or 2) use the Find method.
The Find method shines when looking for a single match in a single field. For example, configure Text3 with a formula that concatenates Text1 and Text2 and this is all that's needed:
If Find("Text3", "equals", t.Text1 & t.Text2) Then
Set tskOut = ActiveCell.Task
t.ConstraintType = pjASAP
t.Predecessors = Dep_path & Source & ".mpp\" & tskOut.ID
End If
However, the Find method can still be used efficiently by knowing that the method moves the active cell to the next match, if found. In this way, the Find method can be used in a loop to find the correct match, or indicate if no match was found.
The main body of the code can be reduced to this:
For Each t In allTasks
If Not t Is Nothing Then
If LCase(t.Text1) = LCase("Dep_in") Then
Dim tskOut As Task
Set tskOut = FindDepOutTask(t)
If tskOut.UniqueID <> t.UniqueID Then
t.ConstraintType = pjASAP
t.Predecessors = Dep_path & Source & ".mpp\" & tskOut.ID
End If
End If
End If
Next t
Using the helper function:
Function FindDepOutTask(depInTask As Task) As Task
' start at Dep In Task
Find "Unique ID", "equals", depInTask.UniqueID
Dim tskOut As Task
Set tskOut = depInTask
Do
Find "Text2", "equals", depInTask.Text2
Set tskOut = ActiveCell.Task
Loop Until tskOut.UniqueID = depInTask.UniqueID Or LCase(tskOut.Text1) = LCase("Dep_out")
Set FindDepOutTask = tskOut
End Function

Get TASK "OVERALLOCATED" (VBA) - Microsoft Project

I need to get when TASK is overallocated (because one or more resources are overallocated).
I am already able to get overallocated resources, but since for the app the resource (if overallocated) is ALWAYS overallocated, so I have to identify only when the resource for the specific TASK is overallocated.
I mean, The red-man in Indicators column is exactly what I want to get:
the tasks #2 and 6# are "overallocated" ( because resource "MCA" is engaged for same day ) --> yes trigger for my alert
the task #4 is not overallocated (no red man) --> no trigger (although MCA is globally overallocated)
So, how can I identify (using VBA) all the tasks with red man in indicators column?
Many thanks in advance
R
The correct property would be Task.Overallocated except that it doesn't seem to work--the value is always False (or "No" when shown in the Gantt Chart view).
The work-around is to loop through the resources using the Resource.Overallocated property (which does work) and then loop through the assignments for over-allocated resources to find the tasks on the over-allocated days.
Note: It is important to get the collection of TimeScaleValues at the resource level to get the total assigned to that resource for each day (e.g. use Set tsvs = res.TimeScaleData... instead of Set tsvs = asn.TimeScaleData...).
Sub FindOverAllocatedTasks()
Dim overAllocTasks As New Collection
Dim res As Resource
For Each res In ActiveProject.Resources
If res.overAllocated Then
Dim maxMinutes As Double
maxMinutes = res.MaxUnits * 60 * ActiveProject.HoursPerDay
Dim asn As Assignment
For Each asn In res.Assignments
Dim tsvs As TimeScaleValues
Set tsvs = res.TimeScaleData(asn.Start, asn.Finish, pjResourceTimescaledWork, pjTimescaleDays)
Dim tsv As TimeScaleValue
For Each tsv In tsvs
If VarType(tsv.Value) = vbDouble Then
If tsv.Value > maxMinutes Then
If Not Contains(overAllocTasks, CStr(asn.Task.UniqueID)) Then
overAllocTasks.Add asn.Task, CStr(asn.Task.UniqueID)
End If
End If
End If
Next tsv
Next asn
End If
Next res
MsgBox overAllocTasks.Count
End Sub
Public Function Contains(col As Collection, key As Variant) As Boolean
Dim obj As Variant
On Error GoTo err
Contains = True
obj = col(key)
Exit Function
err:
Contains = False
End Function

Take list box selection, add value to other list box without allowing duplicates

I have two list boxes on a form I am making. The first list box is linked to a table with various company names. The goal I am after is after double clicking a companies name, the value is inserted in the second list box.
It worked fine until I tried to add code to prevent duplicates from appearing in the second list box, so you couldn't accidentally insert the same company twice. I have tried several different iterations, but with no luck. Anyone able to help with this one? My end goal would be for a msgbox to pop up alerting the user that duplicates are not allowed.
Private Sub ContractorLstbx_DblClick(Cancel As Integer)
Dim found As Boolean
found = False
Dim ID As Long
Dim Contractor As String
For Each newItem In Me.ContractorLstbx.ItemsSelected
For j = 0 To Me.SelectedContractorLst.ListCount - 1
If (Me!ContractorLstbx.ItemData(newItem).Column(1) = Me.SelectedContractorLst.ItemData(j).Column(1)) Then
found = True
Exit For
End If
Next j
If found = False Then
ID = Me.ContractorLstbx.ItemData(newItem)
Me.SelectedContractorLst.AddItem ContractorLstbx!.ItemData(newItem).Column(0) & ";" & Me!ContractorLstbx.ItemData(newItem).Column(1)
End If
found = False
Next newItem
End Sub
This is the full code for your solution. I tried it on test sample and working fine. just copy and paste the code. If you need your comparison to be case sensitive (I mean A <> a) then use Option Compare Binary as in my code below. If it is required to be case insensitive (A = a) just leave the default Option Compare Database or better force it using Option Compare Text
Option Compare Binary
Private Sub ContractorLstbx_DblClick(Cancel As Integer)
Dim found As Boolean
found = False
Dim ID As Long
Dim Contractor As String
For i = 0 To Me.ContractorLstbx.ItemsSelected.Count - 1
For j = 0 To Me.SelectedContractorLst.ListCount - 1
If (Me.ContractorLstbx.Column(1, Me.ContractorLstbx.ItemsSelected(i)) = Me.SelectedContractorLst.Column(1, j)) Then
found = True
Exit For
End If
Next j
If found = False Then
ID = Me.ContractorLstbx.ItemData(Me.ContractorLstbx.ItemsSelected(i))
Me.SelectedContractorLst.AddItem (ContractorLstbx.Column(0, Me.ContractorLstbx.ItemsSelected(i)) & ";" & Me.ContractorLstbx.Column(1, Me.ContractorLstbx.ItemsSelected(i)))
End If
found = False
Next i
End Sub

Return the value in the task above the active cell in MS Project using VBA

I am familiar with VBA in Excel and for the life of me I can't work out how to return the value that is above the activecell in MS Project using VBA. In Excel I would just use something like activecell.offset(-1,0).value.
In the code below I have (incorrectly) used OFFSET so would need something that could replace that code in order to make my macro work.
The code is trying to add a new task summary when the value in the Text4 column changes for the next task (and indents otherwise).
Thank you in advance!
Sub Add_Task_Summaries()
'Add a Summary Task when the text in the Learning Path column changes and
indent otherwise
Dim T As Task
Dim LP As String
Dim RowNo As Long
Dim TU As Integer
For Each T In ActiveProject.Tasks
If T.Text4 <> "" And T.Summary = False Then
If T.Text4 = T.Text4.Offset(-1, 0) Then 'INCORRECT SYNTAX
T.OutlineIndent
ElseIf T.Text4 <> T.Text4.Offset(-1, 0) Then 'INCORRECT SYNTAX
LP = T.Text4
T.Add Name:=LP, before:=ActiveSelection.Tasks
End If
End If
Next T
End Sub
You can select cells in MS Project by using various Select methods of Application object (e.g. SelectCellDown, SelectCellRight, SelectBeginning). However you could also use a method like this:
Sub Add_Task_Summaries()
'Add a Summary Task when the text in the Learning Path column changes
'and indent otherwise
Dim T As Task
Dim S As Task
Dim LP As String
For Each T In ActiveProject.Tasks
If T.Text4 > "" And Not T.Summary Then
If LP <> T.Text4 Then
LP = T.Text4
Set S = ActiveProject.Tasks.Add(LP & " - Summary", T.ID)
S.OutlineLevel = T.OutlineLevel
End If
T.OutlineIndent
End If
Next T
End Sub

Elegant way to highlight chart data series in Excel

I want to outline the chart data range source(s) in a table, in much the same way that the GUI will outline a range in blue if the chart data series is clicked. The user can choose various chart views and the range highlight colours for each data series need to match those displayed in the chart.
For the record, here are the methods I considered:
Parse the chart series values string and extract the data range
Do a lookup on a table that stores information on the ranges and the colours to be used
In the end I went with option 2 as is seemed easier to implement and to properly manage the colours I would probably have to store them for method 1 anyway, negating its benefits.
The highlight procedure is called from the Worksheet_Change event, a lookup is done on the chart name, the ranges and colours pulled from the table and then the cell formatting is carried out. The limitation of this method is that the range/colour data for each new chart view must be pre-calculated. This isn't much of a problem for my current implementation, but my be a limiting factor in future use where the charts might be more dynamic.
So although I've got a version of this working fine, I'm sure there must be a more elegant way of achieving this.
Any suggestions?
Edit:
OK, this seems to handle more cases better. The triggering code is the same, but here is new code for the module:
Function SeriesRange(s As Series) As Range
Dim sf As String, fa() As String
sf = s.Formula
sf = Replace(sf, "=SERIES(", "")
If sf = "" Then
Set SeriesRange = Nothing
Exit Function
End If
fa = Split(sf, ",")
Set SeriesRange = Range(fa(2))
End Function
Sub x(c As Chart)
Dim sc As Series
Dim sr As Range
If SeriesRange(c.SeriesCollection(1)) Is Nothing Then
Exit Sub
End If
Set sr = SeriesRange(c.SeriesCollection(1))
sr.CurrentRegion.Interior.ColorIndex = xlNone
For Each sc In c.SeriesCollection
If sc.Interior.Color > 1 Then
SeriesRange(sc).Interior.Color = sc.Interior.Color
ElseIf sc.Border.ColorIndex > 1 Then
SeriesRange(sc).Interior.Color = sc.Border.Color
ElseIf sc.MarkerBackgroundColorIndex > 1 And sc.MarkerBackgroundColorIndex < 57 Then
SeriesRange(sc).Interior.ColorIndex = sc.MarkerBackgroundColorIndex
ElseIf sc.MarkerForegroundColorIndex > 1 And sc.MarkerForegroundColorIndex < 57 Then
SeriesRange(sc).Interior.ColorIndex = sc.MarkerForegroundColorIndex
Else
MsgBox "Unable to determine chart color for data series " & sc.Name & " ." & vbCrLf _
& "It may help to assign a color rather than allowing AutoColor to assign one."
End If
Next sc
End Sub
/Edit
This is probably more barbaric than elegant, but I think it does what you want. It involves your first bullet point to get the range from the Series object, along with a sub to run through all the Series objects in the SeriesCollection for the chart. This is activated on Chart_DeActivate. Most of this code is jacked - see comments for sources.
In a module:
Function SeriesRange(s As Series) As Range
Dim sf As String, fa() As String
Dim i As Integer
Dim result As Range
sf = s.Formula
sf = Replace(sf, "=SERIES(", "")
fa = Split(sf, ",")
Set SeriesRange = Range(fa(2))
End Function
Sub x(c As Chart)
Dim sc As Series
Dim sr As Range
Set sr = SeriesRange(c.SeriesCollection(1))
sr.CurrentRegion.Interior.ColorIndex = xlNone
For Each sc In c.SeriesCollection
SeriesRange(sc).Interior.Color = sc.Interior.Color
Next sc
End Sub
In the ThisWorkbook object module:
' Jacked from C Pearson http://www.cpearson.com/excel/Events.aspx '
Public WithEvents CHT As Chart
Private Sub CHT_Deactivate()
x CHT
End Sub
Private Sub Workbook_Open()
Set CHT = Worksheets(1).ChartObjects(1).Chart
End Sub
Have you tried using Conditional Formatting?