Skip to content

Commit

Permalink
MSPDI: experimental feature generate timephased data (#778)
Browse files Browse the repository at this point in the history
  • Loading branch information
joniles authored Nov 28, 2024
1 parent 4c7d380 commit a21ac4b
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<body>
<release date="unreleased" version="13.7.1">
<action dev="joniles" type="update">When writing PMXML files, improve handling of P6 schedules where activity code sequence numbers are missing.</action>
<action dev="joniles" type="update">Added an *experimental* feature to `MSPDIWriter` to allow the writer to generate timephased data when none is present. Disabled by default, call the `setGenerateMissingTimephasedData` and pass `true` to enable.</action>
</release>
<release date="2024-11-25" version="13.7.0">
<action dev="joniles" type="update">Update the MPXJ ruby gem to allow access to calendar data.</action>
Expand Down
105 changes: 104 additions & 1 deletion src/main/java/net/sf/mpxj/mspdi/MSPDIWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,30 @@ public boolean getWriteTimephasedData()
return m_writeTimephasedData;
}

/**
* Pass true to this method to enable an experimental feature where
* timephased data is generated for tasks with no timephased data present.
* NOTE: this feature is disabled by default.
*
* @param value true to enable timephased data generation
*/
public void setGenerateMissingTimephasedData(boolean value)
{
m_generateMissingTimephasedData = value;
}

/**
* Returns true if the experimental feature to generate timephased data
* for tasks with no timephased data present is enabled.
* NOTE: this feature is disabled by default.
*
* @return true if feature enabled
*/
public boolean getGenerateMissingTimephasedData()
{
return m_generateMissingTimephasedData;
}

/**
* Set the save version to use when generating an MSPDI file.
*
Expand Down Expand Up @@ -1961,6 +1985,14 @@ private void writeAssignments(Project project)
double actualWork = (durationValue * percentComplete) / 100;
double remainingWork = durationValue - actualWork;

if (m_generateMissingTimephasedData)
{
// I'm being conservative here... I'm sure there is no issue with including
// this in the MSPDI file, but asI've only added this for the "generate
// missing timephased data" feature, I'll keep it optional for now.
dummy.setActualStart(task.getActualStart());
}

dummy.setResourceUniqueID(MicrosoftProjectConstants.ASSIGNMENT_NULL_RESOURCE_ID);
dummy.setWork(duration);
dummy.setActualWork(Duration.getInstance(actualWork, durationUnits));
Expand Down Expand Up @@ -2193,7 +2225,12 @@ private void writeAssignmentExtendedAttribute(List<Project.Assignments.Assignmen
*/
private void writeAssignmentTimephasedData(ResourceAssignment mpx, Project.Assignments.Assignment xml)
{
if (!m_writeTimephasedData || !mpx.getHasTimephasedData() || m_sourceIsPrimavera)
if (!m_writeTimephasedData || m_sourceIsPrimavera)
{
return;
}

if (!mpx.getHasTimephasedData() && !m_generateMissingTimephasedData)
{
return;
}
Expand All @@ -2203,6 +2240,16 @@ private void writeAssignmentTimephasedData(ResourceAssignment mpx, Project.Assig
List<TimephasedWork> planned = mpx.getTimephasedWork();
List<TimephasedWork> completeOvertime = mpx.getTimephasedActualOvertimeWork();

if ((planned == null || planned.isEmpty()) && m_generateMissingTimephasedData)
{
planned = generateTimephasedPlannedWork(mpx);
}

if ((complete == null || complete.isEmpty()) && m_generateMissingTimephasedData)
{
complete = generateTimephasedCompleteWork(mpx);
}

complete = splitCompleteWork(calendar, planned, complete);
planned = splitPlannedWork(calendar, planned, complete);
completeOvertime = splitDays(calendar, completeOvertime, null, null);
Expand All @@ -2225,6 +2272,60 @@ private void writeAssignmentTimephasedData(ResourceAssignment mpx, Project.Assig
}
}

private List<TimephasedWork> generateTimephasedPlannedWork(ResourceAssignment assignment)
{
if (assignment.getActualFinish() != null)
{
return null;
}

LocalDateTime start;
ProjectCalendar calendar = assignment.getEffectiveCalendar();

if (assignment.getActualStart() == null)
{
start = assignment.getStart();
}
else
{
start = calendar.getNextWorkStart(calendar.getDate(assignment.getActualStart(), assignment.getActualWork()));
}

TimephasedWork work = new TimephasedWork();
work.setStart(start);
work.setFinish(assignment.getFinish());
work.setTotalAmount(assignment.getRemainingWork());
work.setAmountPerDay(Duration.getInstance(calendar.getMinutesPerDay(), TimeUnit.MINUTES));
return Collections.singletonList(work);
}

private List<TimephasedWork> generateTimephasedCompleteWork(ResourceAssignment assignment)
{
if (assignment.getActualStart() == null)
{
return null;
}

LocalDateTime finish;
ProjectCalendar calendar = assignment.getEffectiveCalendar();

if (assignment.getActualFinish() == null)
{
finish = calendar.getDate(assignment.getActualStart(), assignment.getActualWork());
}
else
{
finish = assignment.getActualFinish();
}

TimephasedWork work = new TimephasedWork();
work.setStart(assignment.getActualStart());
work.setFinish(finish);
work.setTotalAmount(assignment.getActualWork());
work.setAmountPerDay(Duration.getInstance(calendar.getMinutesPerDay(), TimeUnit.MINUTES));
return Collections.singletonList(work);
}

private List<TimephasedWork> splitCompleteWork(ProjectCalendar calendar, List<TimephasedWork> planned, List<TimephasedWork> complete)
{
if (!m_splitTimephasedAsDays || complete == null)
Expand Down Expand Up @@ -2716,6 +2817,8 @@ private String nullIfEmpty(String value)

private boolean m_writeTimephasedData;

private boolean m_generateMissingTimephasedData;

private boolean m_sourceIsPrimavera;

private SaveVersion m_saveVersion = SaveVersion.Project2016;
Expand Down

0 comments on commit a21ac4b

Please sign in to comment.