Skip to content

Commit

Permalink
P6 Timephased Data (#724)
Browse files Browse the repository at this point in the history
  • Loading branch information
joniles authored Jul 25, 2024
1 parent 0e34cb4 commit 30a3c4d
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 17 deletions.
2 changes: 2 additions & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<body>
<release date="unreleased" version="13.0.3">
<action dev="joniles" type="update">Updated to POI 5.3.0</action>
<action dev="joniles" type="update">Add support for reading and writing timephased data for activities in P6 schedules which have a "manual" curve. (Note: MPXJ does not currently support translating timephased data between different applications, so timephased data read from an MPP file won't be written to a P6 schedule and vice versa).</action>
<action dev="joniles" type="update">Add an attribute to the `ResourceAssignment` class to represent timephased planned work. This is read from/written to P6 as Budgeted Work.</action>
<action dev="joniles" type="update">Update Phoenix schemas to ensure that cost types are represented as doubles.</action>
<action dev="joniles" type="update">Updated to avoid reading apparently invalid resources from Project Commander files.</action>
<action dev="joniles" type="update">Correct the `Finish` attribute for resource assignments when reading PMXML files.</action>
Expand Down
29 changes: 27 additions & 2 deletions src/main/java/net/sf/mpxj/ResourceAssignment.java
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,28 @@ public void setRateSource(RateSource source)
set(AssignmentField.RATE_SOURCE, source);
}

/**
* Retrieves the timephased breakdown of the planned work for this
* resource assignment.
*
* @return timephased planned work
*/
public List<TimephasedWork> getTimephasedPlannedWork()
{
return m_timephasedPlannedWork == null ? null : m_timephasedPlannedWork.getData();
}

/**
* Sets the timephased breakdown of the planned work for this
* resource assignment.
*
* @param data timephased data
*/
public void setTimephasedPlannedWork(TimephasedWorkContainer data)
{
m_timephasedPlannedWork = data;
}

/**
* Retrieves the timephased breakdown of the completed work for this
* resource assignment.
Expand Down Expand Up @@ -3210,12 +3232,15 @@ private Boolean defaultCalculateCostsFromUnits()
return Boolean.TRUE;
}

private TimephasedWorkContainer m_timephasedWork;
private TimephasedCostContainer m_timephasedCost;
private TimephasedWorkContainer m_timephasedPlannedWork;
private TimephasedCostContainer m_timephasedPlannedCost;

private TimephasedWorkContainer m_timephasedActualWork;
private TimephasedCostContainer m_timephasedActualCost;

private TimephasedWorkContainer m_timephasedWork;
private TimephasedCostContainer m_timephasedCost;

private TimephasedWorkContainer m_timephasedOvertimeWork;
private TimephasedWorkContainer m_timephasedActualOvertimeWork;

Expand Down
5 changes: 4 additions & 1 deletion src/main/java/net/sf/mpxj/mspdi/MSPDIWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ public SaveVersion getSaveVersion()
m_extendedAttributes = getExtendedAttributesList();

m_sourceIsMicrosoftProject = MICROSOFT_PROJECT_FILES.contains(m_projectFile.getProjectProperties().getFileType());
m_sourceIsPrimavera = "Primavera".equals(m_projectFile.getProjectProperties().getFileApplication());
m_userDefinedFieldMap = new UserDefinedFieldMap(projectFile, MAPPING_TARGET_CUSTOM_FIELDS);

m_taskMapper = new MicrosoftProjectUniqueIDMapper(m_projectFile.getTasks());
Expand Down Expand Up @@ -2191,7 +2192,7 @@ private void writeAssignmentExtendedAttribute(List<Project.Assignments.Assignmen
*/
private void writeAssignmentTimephasedData(ResourceAssignment mpx, Project.Assignments.Assignment xml)
{
if (!m_writeTimephasedData || !mpx.getHasTimephasedData())
if (!m_writeTimephasedData || !mpx.getHasTimephasedData() || m_sourceIsPrimavera)
{
return;
}
Expand Down Expand Up @@ -2710,6 +2711,8 @@ private String nullIfEmpty(String value)

private boolean m_writeTimephasedData;

private boolean m_sourceIsPrimavera;

private SaveVersion m_saveVersion = SaveVersion.Project2016;

private MicrosoftProjectUniqueIDMapper m_taskMapper;
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/net/sf/mpxj/primavera/AbstractUnitsHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
import net.sf.mpxj.Duration;
import net.sf.mpxj.ProjectFile;
import net.sf.mpxj.ResourceAssignment;
import net.sf.mpxj.ResourceType;
import net.sf.mpxj.Task;
import net.sf.mpxj.TimeUnit;
import net.sf.mpxj.common.NumberHelper;

Expand Down
19 changes: 18 additions & 1 deletion src/main/java/net/sf/mpxj/primavera/CurveHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

package net.sf.mpxj.primavera;

import net.sf.mpxj.ProjectFile;
import net.sf.mpxj.WorkContour;

/**
Expand All @@ -32,11 +33,27 @@ final class CurveHelper
{
public static Integer getCurveID(WorkContour contour)
{
if (contour == null || contour.isContourFlat() || contour.isContourManual())
if (contour == null || contour.isContourFlat())
{
return null;
}

return contour.getUniqueID();
}

public static WorkContour getWorkContour(ProjectFile file, Integer id)
{
if (id == null)
{
return null;
}

// Special case: the "manual" curve type won't be present in an exported file, but the ID is a fixed value
if (id.equals(WorkContour.CONTOURED.getUniqueID()))
{
return WorkContour.CONTOURED;
}

return file.getWorkContours().getByUniqueID(id);
}
}
15 changes: 13 additions & 2 deletions src/main/java/net/sf/mpxj/primavera/PrimaveraPMFileReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@

import net.sf.mpxj.BaselineStrategy;
import net.sf.mpxj.DataType;
import net.sf.mpxj.TimephasedWorkContainer;
import net.sf.mpxj.UnitOfMeasure;
import net.sf.mpxj.UnitOfMeasureContainer;
import net.sf.mpxj.common.DayOfWeekHelper;
Expand Down Expand Up @@ -1866,6 +1867,7 @@ private void processAssignments(List<ResourceAssignmentType> assignments)
for (ResourceAssignmentType row : assignments)
{
Task task = m_projectFile.getTaskByUniqueID(m_activityClashMap.getID(row.getActivityObjectId()));
ProjectCalendar effectiveCalendar = task.getEffectiveCalendar();

Integer roleID = m_roleClashMap.getID(row.getRoleObjectId());
Integer resourceID = row.getResourceObjectId();
Expand Down Expand Up @@ -1897,7 +1899,7 @@ private void processAssignments(List<ResourceAssignmentType> assignments)
assignment.setGUID(DatatypeConverter.parseUUID(row.getGUID()));
assignment.setActualOvertimeCost(row.getActualOvertimeCost());
assignment.setActualOvertimeWork(getDuration(row.getActualOvertimeUnits()));
assignment.setWorkContour(m_projectFile.getWorkContours().getByUniqueID(row.getResourceCurveObjectId()));
assignment.setWorkContour(CurveHelper.getWorkContour(m_projectFile, row.getResourceCurveObjectId()));
assignment.setRateIndex(RateTypeHelper.getInstanceFromXml(row.getRateType()));
assignment.setRole(m_projectFile.getResourceByUniqueID(roleID));
assignment.setOverrideRate(readRate(row.getCostPerQuantity()));
Expand All @@ -1913,7 +1915,7 @@ private void processAssignments(List<ResourceAssignmentType> assignments)
// calculate work
Duration remainingWork = assignment.getRemainingWork();
Duration actualWork = assignment.getActualWork();
Duration totalWork = Duration.add(actualWork, remainingWork, assignment.getEffectiveCalendar());
Duration totalWork = Duration.add(actualWork, remainingWork, effectiveCalendar);
assignment.setWork(totalWork);

// calculate cost
Expand All @@ -1931,8 +1933,17 @@ private void processAssignments(List<ResourceAssignmentType> assignments)
assignment.setUnits(Double.valueOf(NumberHelper.getDouble(row.getPlannedUnitsPerTime()) * 100));
assignment.setRemainingUnits(Double.valueOf(NumberHelper.getDouble(row.getRemainingUnitsPerTime()) * 100));

// Add User Defined Fields
populateUserDefinedFieldValues(assignment, row.getUDF());

// Read timephased data
TimephasedWorkContainer timephasedPlannedWork = TimephasedHelper.read(effectiveCalendar, assignment.getPlannedStart(), row.getPlannedCurve());
TimephasedWorkContainer timephasedActualWork = TimephasedHelper.read(effectiveCalendar, assignment.getActualStart(), row.getActualCurve());
TimephasedWorkContainer timephasedRemainingWork = TimephasedHelper.read(effectiveCalendar, assignment.getRemainingEarlyStart(), row.getRemainingCurve());
assignment.setTimephasedPlannedWork(timephasedPlannedWork);
assignment.setTimephasedActualWork(timephasedActualWork);
assignment.setTimephasedWork(timephasedRemainingWork);

m_eventManager.fireAssignmentReadEvent(assignment);
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/net/sf/mpxj/primavera/PrimaveraPMProjectWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ private void write(boolean baseline)
m_activityTypePopulated = m_projectFile.getTasks().getPopulatedFields().contains(TaskField.ACTIVITY_TYPE);
m_wbsSequence = new ObjectSequence(0);
m_userDefinedFields = UdfHelper.getUserDefinedFieldsSet(m_projectFile);
m_projectFromPrimavera = "Primavera".equals(m_projectFile.getProjectProperties().getFileApplication());

if (baseline)
{
Expand Down Expand Up @@ -1186,6 +1187,14 @@ private void writeAssignment(ResourceAssignment mpxj)
xml.setRemainingUnits(unitsHelper.getRemainingUnits());
xml.setRemainingUnitsPerTime(unitsHelper.getRemainingUnitsPerTime());
xml.setRemainingDuration(getResourceAssignmentRemainingDuration(task, mpxj));

if (m_projectFromPrimavera)
{
ProjectCalendar calendar = task.getEffectiveCalendar();
xml.setPlannedCurve(TimephasedHelper.write(calendar, mpxj.getTimephasedPlannedWork()));
xml.setActualCurve(TimephasedHelper.write(calendar, mpxj.getTimephasedActualWork()));
xml.setRemainingCurve(TimephasedHelper.write(calendar, mpxj.getTimephasedWork()));
}
}

private Double getResourceAssignmentRemainingDuration(Task task, ResourceAssignment mpxj)
Expand Down Expand Up @@ -2090,4 +2099,5 @@ ProjectFile getProjectFile()
private ObjectSequence m_wbsSequence;
private Set<FieldType> m_userDefinedFields;
private boolean m_activityTypePopulated;
private boolean m_projectFromPrimavera;
}
16 changes: 13 additions & 3 deletions src/main/java/net/sf/mpxj/primavera/PrimaveraReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
import net.sf.mpxj.common.NumberHelper;
import net.sf.mpxj.common.ObjectSequence;
import net.sf.mpxj.common.SlackHelper;
import net.sf.mpxj.TimephasedWorkContainer;

/**
* This class provides a generic front end to read project data from
Expand Down Expand Up @@ -1734,6 +1735,7 @@ public void processAssignments(List<Row> rows)
for (Row row : rows)
{
Task task = m_project.getTaskByUniqueID(m_activityClashMap.getID(row.getInteger("task_id")));
ProjectCalendar effectiveCalendar = task.getEffectiveCalendar();

Integer roleID = m_roleClashMap.getID(row.getInteger("role_id"));
Integer resourceID = row.getInteger("rsrc_id");
Expand All @@ -1751,7 +1753,7 @@ public void processAssignments(List<Row> rows)
ResourceAssignment assignment = task.addResourceAssignment(resource);
processFields(m_assignmentFields, row, assignment);

assignment.setWorkContour(m_project.getWorkContours().getByUniqueID(row.getInteger("curv_id")));
assignment.setWorkContour(CurveHelper.getWorkContour(m_project, row.getInteger("curv_id")));
assignment.setRateIndex(RateTypeHelper.getInstanceFromXer(row.getString("rate_type")));
assignment.setRole(m_project.getResourceByUniqueID(roleID));
assignment.setOverrideRate(readRate(row.getDouble("cost_per_qty")));
Expand All @@ -1764,9 +1766,9 @@ public void processAssignments(List<Row> rows)
Duration remainingWork = assignment.getRemainingWork();
Duration actualRegularWork = row.getDuration("act_reg_qty");
Duration actualOvertimeWork = assignment.getActualOvertimeWork();
Duration actualWork = Duration.add(actualRegularWork, actualOvertimeWork, assignment.getEffectiveCalendar());
Duration actualWork = Duration.add(actualRegularWork, actualOvertimeWork, effectiveCalendar);
assignment.setActualWork(actualWork);
Duration totalWork = Duration.add(actualWork, remainingWork, assignment.getEffectiveCalendar());
Duration totalWork = Duration.add(actualWork, remainingWork, effectiveCalendar);
assignment.setWork(totalWork);

// calculate cost
Expand All @@ -1790,6 +1792,14 @@ public void processAssignments(List<Row> rows)
// Add User Defined Fields
populateUserDefinedFieldValues("TASKRSRC", FieldTypeClass.ASSIGNMENT, assignment, assignment.getUniqueID());

// Read timephased data
TimephasedWorkContainer timephasedPlannedWork = TimephasedHelper.read(effectiveCalendar, assignment.getPlannedStart(), row.getString("target_crv"));
TimephasedWorkContainer timephasedActualWork = TimephasedHelper.read(effectiveCalendar, assignment.getActualStart(), row.getString("actual_crv"));
TimephasedWorkContainer timephasedRemainingWork = TimephasedHelper.read(effectiveCalendar, assignment.getRemainingEarlyStart(), row.getString("remain_crv"));
assignment.setTimephasedPlannedWork(timephasedPlannedWork);
assignment.setTimephasedActualWork(timephasedActualWork);
assignment.setTimephasedWork(timephasedRemainingWork);

m_eventManager.fireAssignmentReadEvent(assignment);
}
}
Expand Down
28 changes: 22 additions & 6 deletions src/main/java/net/sf/mpxj/primavera/PrimaveraXERFileWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ public Charset getCharset()
m_rateObjectID = new ObjectSequence(1);
m_noteObjectID = new ObjectSequence(1);
m_userDefinedFields = UdfHelper.getUserDefinedFieldsSet(projectFile);
m_projectFromPrimavera = "Primavera".equals(m_file.getProjectProperties().getFileApplication());

// We need to do this first to ensure the default topic is created if required
populateWbsNotes();
Expand Down Expand Up @@ -330,8 +331,22 @@ private void writePredecessors()
*/
private void writeResourceAssignments()
{
Map<String, ExportFunction<ResourceAssignment>> columns;
if (m_projectFromPrimavera)
{
columns = RESOURCE_ASSIGNMENT_COLUMNS;
}
else
{
// Don't write timephased data if the schedule isn't from P6
columns = new LinkedHashMap<>(RESOURCE_ASSIGNMENT_COLUMNS);
columns.put("target_crv", r -> null);
columns.put("remain_crv", r -> null);
columns.put("actual_crv", r -> null);
}

m_writer.writeTable("TASKRSRC", RESOURCE_ASSIGNMENT_COLUMNS);
m_file.getResourceAssignments().stream().filter(t -> isValidAssignment(t)).sorted(Comparator.comparing(ResourceAssignment::getUniqueID)).forEach(t -> m_writer.writeRecord(RESOURCE_ASSIGNMENT_COLUMNS, t));
m_file.getResourceAssignments().stream().filter(t -> isValidAssignment(t)).sorted(Comparator.comparing(ResourceAssignment::getUniqueID)).forEach(t -> m_writer.writeRecord(columns, t));
}

/**
Expand Down Expand Up @@ -977,6 +992,7 @@ private static PercentCompleteType getPercentCompleteType(Task task)
private Set<FieldType> m_userDefinedFields;
private Task m_temporaryRootWbs;
private Integer m_originalOutlineLevel;
private boolean m_projectFromPrimavera;

private static final Integer DEFAULT_PROJECT_ID = Integer.valueOf(1);
private static final String RESOURCE_ID_PREFIX = "RESOURCE-";
Expand Down Expand Up @@ -1306,12 +1322,12 @@ interface ExportFunction<T>
RESOURCE_ASSIGNMENT_COLUMNS.put("target_lag_drtn_hr_cnt", r -> r.getDelay());
RESOURCE_ASSIGNMENT_COLUMNS.put("target_qty_per_hr", r -> new XerUnitsHelper(r).getPlannedUnitsPerTime());
RESOURCE_ASSIGNMENT_COLUMNS.put("act_ot_qty", r -> r.getActualOvertimeWork());
RESOURCE_ASSIGNMENT_COLUMNS.put("act_reg_qty", r -> PrimaveraXERFileWriter.getActualRegularWork(r));
RESOURCE_ASSIGNMENT_COLUMNS.put("act_reg_qty", r -> getActualRegularWork(r));
RESOURCE_ASSIGNMENT_COLUMNS.put("relag_drtn_hr_cnt", r -> null);
RESOURCE_ASSIGNMENT_COLUMNS.put("ot_factor", r -> null);
RESOURCE_ASSIGNMENT_COLUMNS.put("cost_per_qty", r -> r.getOverrideRate());
RESOURCE_ASSIGNMENT_COLUMNS.put("target_cost", r -> Currency.getInstance(r.getPlannedCost()));
RESOURCE_ASSIGNMENT_COLUMNS.put("act_reg_cost", r -> Currency.getInstance(PrimaveraXERFileWriter.getActualRegularCost(r)));
RESOURCE_ASSIGNMENT_COLUMNS.put("act_reg_cost", r -> Currency.getInstance(getActualRegularCost(r)));
RESOURCE_ASSIGNMENT_COLUMNS.put("act_ot_cost", r -> Currency.getInstance(r.getActualOvertimeCost()));
RESOURCE_ASSIGNMENT_COLUMNS.put("remain_cost", r -> Currency.getInstance(r.getRemainingCost()));
RESOURCE_ASSIGNMENT_COLUMNS.put("act_start_date", r -> r.getActualStart());
Expand All @@ -1323,9 +1339,9 @@ interface ExportFunction<T>
RESOURCE_ASSIGNMENT_COLUMNS.put("rem_late_start_date", r -> r.getRemainingLateStart());
RESOURCE_ASSIGNMENT_COLUMNS.put("rem_late_end_date", r -> r.getRemainingLateFinish());
RESOURCE_ASSIGNMENT_COLUMNS.put("rollup_dates_flag", r -> Boolean.TRUE);
RESOURCE_ASSIGNMENT_COLUMNS.put("target_crv", r -> null);
RESOURCE_ASSIGNMENT_COLUMNS.put("remain_crv", r -> null);
RESOURCE_ASSIGNMENT_COLUMNS.put("actual_crv", r -> null);
RESOURCE_ASSIGNMENT_COLUMNS.put("target_crv", r -> TimephasedHelper.write(r.getTask().getEffectiveCalendar(), r.getTimephasedPlannedWork()));
RESOURCE_ASSIGNMENT_COLUMNS.put("remain_crv", r -> TimephasedHelper.write(r.getTask().getEffectiveCalendar(), r.getTimephasedWork()));
RESOURCE_ASSIGNMENT_COLUMNS.put("actual_crv", r -> TimephasedHelper.write(r.getTask().getEffectiveCalendar(), r.getTimephasedActualWork()));
RESOURCE_ASSIGNMENT_COLUMNS.put("ts_pend_act_end_flag", r -> Boolean.FALSE);
RESOURCE_ASSIGNMENT_COLUMNS.put("guid", r -> r.getGUID());
RESOURCE_ASSIGNMENT_COLUMNS.put("rate_type", r -> RateTypeHelper.getXerFromInstance(r.getRateIndex()));
Expand Down
Loading

0 comments on commit 30a3c4d

Please sign in to comment.