Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- https://github.com/ObeoNetwork/pepper/issues/90[#90] Display the duration of AbstractTask in days, rounding to the nearest half-day.
- https://github.com/ObeoNetwork/pepper/issues/89[#89] Make duration (AbstractTask and Workpackage) computed from non working days. For now, it corresponds to week-end and french public holidays 2026.
- https://github.com/ObeoNetwork/pepper/issues/94[#94] Rename the dependency link duration to dependency link delay and display the delay in days
- https://github.com/ObeoNetwork/pepper/issues/60[#60] Take Resource (including Unavailability period) into account for effort commutation

=== Bug fixes

Expand Down
30 changes: 23 additions & 7 deletions backend/pepper-domain-services/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,43 @@ A task is constrained by two `TimeConstraint` values among:

- `START`
- `END`
- `DURATION`
- `EFFORT`
If a constrained boundary (`START` or `END`) is also constrained by a dependency, the dependency constraint is considered stronger than the task constraint.

In that case, the duration is no longer considered constraining.
In that case, the effort is no longer considered constraining.

A boundary constrained by a dependency can not be changed directly.

For example: A START-DURATION task has its end date constrained by a dependency. If the start date is moved, the end date remains unchanged and the duration is updated accordingly.
For example: A START-EFFORT task has its end date constrained by a dependency. If the start date is moved, the end date remains unchanged and the effort is updated accordingly.

## Task bounds computation

An AbstractTask has its bounds defined as Instant.
When modifying the task, either from Gantt, details view or by the algorithm, the AbstractTask bounds are rounded to the closest half-day.
Non-working days (in week and configured fixed non-working days) do not consume any effort.

The workpackage has its bounds defined as LocalDate
Both workpackage startDate and endDate are included.

### Task with assigned persons

If a task has assigned persons, then the calculation of time constraints will consider the unavailability periods of Person.
An unavailability period does not consume any effort.
On the contrary if multiple persons are available on a task, the effort is more consumed.
If no person is assigned, one working day consume an effort of one day.

## Gantt interactions

### Moving a task

- If the task is constrained by dependencies nothing is done.
- Otherwise, the constraining boundary or boundaries are updated according to the task calculation option:
-- `START-END`: both boundaries are updated. The duration may change if the number of included non-working days changes.
-- `START-DURATION` and `END-DURATION`: the constraining boundary is updated. The constrained boundary may move by more than the drag delta if the number of included non-working days changes, because the duration is preserved.
-- `START-END`: both boundaries are updated. The effort may change if the number of included non-working days changes.
-- `START-EFFORT` and `END-EFFORT`: the constraining boundary is updated. The constrained boundary may move by more than the drag delta if the number of included non-working days changes, because the effort is preserved.

### Changing one task boundary

- If the boundary is constraining, this boundary is updated and the other constraint is preserved.
- If the opposite boundary is constrained by a dependency, the moved boundary is updated and the duration is updated as well.
- Otherwise, the change is interpreted as a user intent to update the duration by the move delta.
- If the opposite boundary is constrained by a dependency, the moved boundary is updated and the effort is updated as well.
- Otherwise, the change is interpreted as a user intent to update the effort by the move delta.
-- [FUTURE ENHANCEMENT] A global option could forbid changing a non-constraining boundary directly. In that mode, moving such a task boundary would not be allowed.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

/**
* Domain service related to AbstractTask entities.
*
* @author lfasani
*/
@Service
Expand All @@ -41,70 +42,75 @@ public class TaskComputationService {
private final ZoneId localZone = ZoneId.systemDefault();

/**
* Update the newStartTime and potentially duration or endTime according to the calculationOption.
* It also rounds newStartTime and shifts it sooner if included in a non-working day period.
* Update the newStartTime and potentially effort or endTime according to the calculationOption. It also rounds newStartTime and shifts it sooner if included in a non-working day period.
*/
public void updateStartTime(AbstractTask abstractTask, Instant newStartTime) {
TaskTimeBoundariesConstraint calculationOption = abstractTask.getCalculationOption();
if (!TaskTimeBoundariesConstraint.END_DURATION.equals(calculationOption) || this.hasDependency(abstractTask, StartOrEnd.START)) {
Instant roundedNewStartTime = this.roundToNearestHalfDay(newStartTime);
Instant previousStartTime = nonWorkingDaysService.getPreviousStartTime(roundedNewStartTime);
abstractTask.setStartTime(this.convertAccordingToTimeZone(previousStartTime));

Instant currentEndTime = this.roundToNearestHalfDay(abstractTask.getEndTime());
int currentDuration = abstractTask.getDuration();
if (calculationOption.equals(TaskTimeBoundariesConstraint.START_END) || this.hasDependency(abstractTask, StartOrEnd.END)) {
if (currentEndTime != null && previousStartTime != null) {
long hourDuration = nonWorkingDaysService.getDuration(previousStartTime, currentEndTime).toHours();
abstractTask.setDuration((int) hourDuration);
}
} else if (calculationOption.equals(TaskTimeBoundariesConstraint.START_DURATION) && previousStartTime != null) {
Instant newEndTime = nonWorkingDaysService.getEndTime(previousStartTime, currentDuration).minus(1, ChronoUnit.MINUTES);
abstractTask.setEndTime(this.convertAccordingToTimeZone(newEndTime));
Instant roundedNewStartTime = this.roundToNearestHalfDay(newStartTime);
Instant previousStartTime = nonWorkingDaysService.getPreviousStartTime(roundedNewStartTime, abstractTask.getAssignedPersons());
abstractTask.setStartTime(this.convertAccordingToTimeZone(previousStartTime));

Instant currentEndTime = this.roundToNearestHalfDay(abstractTask.getEndTime());
int currentEffort = abstractTask.getEffort();
if (calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT) && previousStartTime != null) {
Instant newEndTime = nonWorkingDaysService.getNextEndTime(previousStartTime, currentEffort, abstractTask.getAssignedPersons()).minus(1, ChronoUnit.MINUTES);
abstractTask.setEndTime(this.convertAccordingToTimeZone(newEndTime));
} else {
if (currentEndTime != null && previousStartTime != null) {
long hourEffort = nonWorkingDaysService.getEffort(previousStartTime, currentEndTime, abstractTask.getAssignedPersons()).toHours();
abstractTask.setEffort((int) hourEffort);
}
}

this.updateDuration(abstractTask);
}

/**
* Update the endTime and potentially duration or startTime according to the calculationOption.
* It also rounds newEndTime and shifts it later if included in a non-working day period.
* Update the endTime and potentially effort or startTime according to the calculationOption. It also rounds newEndTime and shifts it later if included in a non-working day period.
*/
public void updateEndTime(AbstractTask abstractTask, Instant newEndTime) {
TaskTimeBoundariesConstraint calculationOption = abstractTask.getCalculationOption();
if (!TaskTimeBoundariesConstraint.START_DURATION.equals(calculationOption) || this.hasDependency(abstractTask, StartOrEnd.END)) {
Instant roundedNewEndTime = this.roundToNearestHalfDay(newEndTime);
Instant nextEndTime = nonWorkingDaysService.getNextEndTime(roundedNewEndTime);
abstractTask.setEndTime(this.convertAccordingToTimeZone(nextEndTime).minus(1, ChronoUnit.MINUTES));

Instant currentStartTime = this.roundToNearestHalfDay(abstractTask.getStartTime());
int currentDuration = abstractTask.getDuration();
if (calculationOption.equals(TaskTimeBoundariesConstraint.START_END) || this.hasDependency(abstractTask, StartOrEnd.START)) {
if (nextEndTime != null && currentStartTime != null) {
long hourDuration = nonWorkingDaysService.getDuration(currentStartTime, nextEndTime).toHours();
abstractTask.setDuration((int) hourDuration);
}
} else if (calculationOption.equals(TaskTimeBoundariesConstraint.END_DURATION) && nextEndTime != null) {
Instant newStartTime = nonWorkingDaysService.getStartTime(nextEndTime, currentDuration); //.plus(1, ChronoUnit.MINUTES);
abstractTask.setStartTime(this.convertAccordingToTimeZone(newStartTime));
Instant roundedNewEndTime = this.roundToNearestHalfDay(newEndTime);
Instant nextEndTime = nonWorkingDaysService.getNextEndTime(roundedNewEndTime, abstractTask.getAssignedPersons());
abstractTask.setEndTime(this.convertAccordingToTimeZone(nextEndTime).minus(1, ChronoUnit.MINUTES));

Instant currentStartTime = this.roundToNearestHalfDay(abstractTask.getStartTime());
int currentEffort = abstractTask.getEffort();
if (calculationOption.equals(TaskTimeBoundariesConstraint.END_EFFORT) && nextEndTime != null) {
Instant newStartTime = nonWorkingDaysService.getPreviousStartTime(nextEndTime, currentEffort, abstractTask.getAssignedPersons()); //.plus(1, ChronoUnit.MINUTES);
abstractTask.setStartTime(this.convertAccordingToTimeZone(newStartTime));
} else {
if (nextEndTime != null && currentStartTime != null) {
long hourEffort = nonWorkingDaysService.getEffort(currentStartTime, nextEndTime, abstractTask.getAssignedPersons()).toHours();
abstractTask.setEffort((int) hourEffort);
}
}

this.updateDuration(abstractTask);
}

private void updateDuration(AbstractTask abstractTask) {
if (abstractTask.getStartTime() != null && abstractTask.getEndTime() != null) {
long hourDuration = nonWorkingDaysService.getDuration(this.roundToNearestHalfDay(abstractTask.getStartTime()), this.roundToNearestHalfDay(abstractTask.getEndTime())).toHours();
abstractTask.setDuration((int) hourDuration);
}
}

public void updateDuration(AbstractTask abstractTask, int newDuration) {
int newDurationRouned = this.roundToNearestHalfDay(newDuration);
public void updateEffort(AbstractTask abstractTask, int newEffort) {
int newEffortRouned = this.roundToNearestHalfDay(newEffort);
TaskTimeBoundariesConstraint calculationOption = abstractTask.getCalculationOption();
if (TaskTimeBoundariesConstraint.START_END.equals(calculationOption)) {
return;
}
abstractTask.setDuration(newDurationRouned);
abstractTask.setEffort(newEffortRouned);

Instant currentStartTime = this.roundToNearestHalfDay(abstractTask.getStartTime());
Instant currentEndTime = this.roundToNearestHalfDay(abstractTask.getEndTime());
if (calculationOption.equals(TaskTimeBoundariesConstraint.START_DURATION) && currentStartTime != null) {
Instant newEndTime = nonWorkingDaysService.getEndTime(currentStartTime, newDurationRouned).minus(1, ChronoUnit.MINUTES);
if (calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT) && currentStartTime != null) {
Instant newEndTime = nonWorkingDaysService.getNextEndTime(currentStartTime, newEffortRouned, abstractTask.getAssignedPersons()).minus(1, ChronoUnit.MINUTES);
abstractTask.setEndTime(this.convertAccordingToTimeZone(newEndTime));
} else if (calculationOption.equals(TaskTimeBoundariesConstraint.END_DURATION) && currentEndTime != null) {
Instant newStartTime = nonWorkingDaysService.getStartTime(currentEndTime, newDurationRouned); //.plus(1, ChronoUnit.MINUTES);
} else if (calculationOption.equals(TaskTimeBoundariesConstraint.END_EFFORT) && currentEndTime != null) {
Instant newStartTime = nonWorkingDaysService.getPreviousStartTime(currentEndTime, newEffortRouned, abstractTask.getAssignedPersons()); //.plus(1, ChronoUnit.MINUTES);
abstractTask.setStartTime(this.convertAccordingToTimeZone(newStartTime));
}
}
Expand Down Expand Up @@ -176,8 +182,7 @@ private int roundToNearestHalfDay(int nbHours) {

public Instant roundToNearestHalfDay(Instant instant) {
return Optional.ofNullable(instant)
.map(inst -> inst.plus(Duration.ofHours(6))
.truncatedTo(ChronoUnit.HALF_DAYS))
.map(inst -> inst.plus(Duration.ofHours(6)).truncatedTo(ChronoUnit.HALF_DAYS))
.orElse(null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,59 +34,66 @@ public class WorkpackageComputationService {
private final NonWorkingDaysService nonWorkingDaysService = new NonWorkingDaysService();

public void updateStartDate(Workpackage workpackage, LocalDate newStartDate) {
LocalDate previousNewStartDate = nonWorkingDaysService.getPreviousStartDate(newStartDate);
LocalDate previousNewStartDate = nonWorkingDaysService.getPreviousStartDate(newStartDate, workpackage.getAssignedPersons());
TaskTimeBoundariesConstraint calculationOption = workpackage.getCalculationOption();
if (!TaskTimeBoundariesConstraint.END_DURATION.equals(calculationOption) || this.hasDependency(workpackage, StartOrEnd.START)) {
workpackage.setStartDate(previousNewStartDate);

LocalDate currentEndDate = workpackage.getEndDate();
int currentDuration = workpackage.getDuration();
if (calculationOption.equals(TaskTimeBoundariesConstraint.START_END) || this.hasDependency(workpackage, StartOrEnd.END)) {
if (currentEndDate != null && previousNewStartDate != null) {
long newDuration = nonWorkingDaysService.getDuration(previousNewStartDate, currentEndDate).toDays();
workpackage.setDuration((int) newDuration);
}
} else if (calculationOption.equals(TaskTimeBoundariesConstraint.START_DURATION) && previousNewStartDate != null) {
LocalDate newEndDate = previousNewStartDate.plusDays(currentDuration - 1);
workpackage.setEndDate(newEndDate);
workpackage.setStartDate(previousNewStartDate);

LocalDate currentEndDate = workpackage.getEndDate();
int currentEffort = workpackage.getEffort();
if (calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT) && previousNewStartDate != null) {
LocalDate newEndDate = nonWorkingDaysService.getNextEndDate(previousNewStartDate, currentEffort, workpackage.getAssignedPersons());
workpackage.setEndDate(newEndDate);
} else {
if (currentEndDate != null && previousNewStartDate != null) {
long newEffort = nonWorkingDaysService.getEffort(previousNewStartDate, currentEndDate, workpackage.getAssignedPersons()).toDays();
workpackage.setEffort((int) newEffort);
}
}

this.updateDuration(workpackage);
}

private void updateDuration(Workpackage workpackage) {
if (workpackage.getStartDate() != null && workpackage.getEndDate() != null) {
long hourDuration = nonWorkingDaysService.getDuration(workpackage.getStartDate(), workpackage.getEndDate(), workpackage.getAssignedPersons()).toHours();
workpackage.setDuration((int) hourDuration);
}
}

public void updateEndDate(Workpackage workpackage, LocalDate newEndDate) {
LocalDate nextNewEndDate = nonWorkingDaysService.getNextEndDate(newEndDate);
LocalDate nextNewEndDate = nonWorkingDaysService.getNextEndDate(newEndDate, workpackage.getAssignedPersons());
TaskTimeBoundariesConstraint calculationOption = workpackage.getCalculationOption();
if (!TaskTimeBoundariesConstraint.START_DURATION.equals(calculationOption) || this.hasDependency(workpackage, StartOrEnd.END)) {
workpackage.setEndDate(nextNewEndDate);

LocalDate currentStartDate = workpackage.getStartDate();
int currentDuration = workpackage.getDuration();
if (calculationOption.equals(TaskTimeBoundariesConstraint.START_END) || this.hasDependency(workpackage, StartOrEnd.START)) {
if (nextNewEndDate != null && currentStartDate != null) {
long newDuration = nonWorkingDaysService.getDuration(currentStartDate, nextNewEndDate).toDays();
workpackage.setDuration((int) newDuration);
}
} else if (calculationOption.equals(TaskTimeBoundariesConstraint.END_DURATION) && nextNewEndDate != null) {
LocalDate newStartDate = nextNewEndDate.minusDays(currentDuration - 1);
workpackage.setStartDate(newStartDate);
workpackage.setEndDate(nextNewEndDate);

LocalDate currentStartDate = workpackage.getStartDate();
int currentEffort = workpackage.getEffort();
if (calculationOption.equals(TaskTimeBoundariesConstraint.END_EFFORT) && nextNewEndDate != null) {
LocalDate newStartDate = nonWorkingDaysService.getPreviousStartDate(nextNewEndDate, currentEffort, workpackage.getAssignedPersons());
workpackage.setStartDate(newStartDate);
} else {
if (nextNewEndDate != null && currentStartDate != null) {
long newEffort = nonWorkingDaysService.getEffort(currentStartDate, nextNewEndDate, workpackage.getAssignedPersons()).toDays();
workpackage.setEffort((int) newEffort);
}
}

this.updateDuration(workpackage);
}

public void updateDuration(Workpackage workpackage, int newDuration) {
public void updateEffort(Workpackage workpackage, int newEffort) {
TaskTimeBoundariesConstraint calculationOption = workpackage.getCalculationOption();
if (TaskTimeBoundariesConstraint.START_END.equals(calculationOption)) {
return;
}
workpackage.setDuration(newDuration);
workpackage.setEffort(newEffort);

LocalDate currentStartDate = workpackage.getStartDate();
LocalDate currentEndDate = workpackage.getEndDate();
if (calculationOption.equals(TaskTimeBoundariesConstraint.START_DURATION)) {
LocalDate newEndDate = currentStartDate.plusDays(newDuration - 1);
if (calculationOption.equals(TaskTimeBoundariesConstraint.START_EFFORT)) {
LocalDate newEndDate = nonWorkingDaysService.getNextEndDate(currentStartDate, newEffort, workpackage.getAssignedPersons());
workpackage.setEndDate(newEndDate);
} else if (calculationOption.equals(TaskTimeBoundariesConstraint.END_DURATION)) {
LocalDate newStartDate = currentEndDate.minusDays(newDuration - 1);
} else if (calculationOption.equals(TaskTimeBoundariesConstraint.END_EFFORT)) {
LocalDate newStartDate = nonWorkingDaysService.getPreviousStartDate(currentEndDate, newEffort, workpackage.getAssignedPersons());
workpackage.setStartDate(newStartDate);
}
}
Expand Down
Loading
Loading