[Bug #975] Respect consolidated day assignments of a Resource Allocation when applying a Stretch function

When an advance function is applied to a resource allocation, in this
case a Stretch function, the function should respect the period of
consolidated day assignments if any.

This means that an advance function should only calculate hours for day
assigments considering the period of time in the resource allocation that
is free (has no day assignments).

The class ConsolidatedStretch represents an interval that encompasses the
consolidated day assignments of a resource allocation, if any. This interval
is infered from the resource allocation and is never persisted.

This interval is shown in the window of intervals as disabled (the user
cannot edit it). It can happen that the StretchFunction may have several
intervals defined, then the user consolidates some days, resulting
that some of the intervals previously defined are now outdated (by the
consolidated interval). In this case, an overlap error will happen if
the user tries to apply the function, and the user should remove by hand
those outdated intervals.

FEA: ItEr74S04BugFixing
This commit is contained in:
Diego Pino Garcia 2011-04-22 13:07:47 +02:00
parent cd9cb77e4a
commit 9b1fb6c87c
6 changed files with 205 additions and 46 deletions

View file

@ -76,8 +76,10 @@ import org.navalplanner.business.workingday.IntraDayDate.PartialDay;
import org.navalplanner.business.workingday.ResourcesPerDay;
/**
* Resources are allocated to planner tasks.
* @author Diego Pino García <dpino@igalia.com>
* @author Manuel Rego Casasnovas <mrego@igalia.com>
*
* Resources are allocated to planner tasks.
*/
public abstract class ResourceAllocation<T extends DayAssignment> extends
BaseEntity {
@ -2010,4 +2012,14 @@ public abstract class ResourceAllocation<T extends DayAssignment> extends
protected abstract void removeContainersFor(Scenario scenario);
}
/*
* Returns first non consolidated day, if there are not consolidations
* returns first day of task
*/
public LocalDate getFirstNonConsolidatedDate() {
List<T> nonConsolidated = getNonConsolidatedAssignments();
return (!nonConsolidated.isEmpty()) ? nonConsolidated.get(0).getDay()
: null;
}
}

View file

@ -27,6 +27,7 @@ import java.util.Comparator;
import java.util.List;
import org.hibernate.validator.NotNull;
import org.joda.time.Days;
import org.joda.time.LocalDate;
/**
@ -38,8 +39,8 @@ import org.joda.time.LocalDate;
*/
public class Stretch {
public static Stretch create(LocalDate date, BigDecimal lengthPercent, BigDecimal progressPercent) {
return new Stretch(date, lengthPercent, progressPercent);
public static Stretch create(LocalDate date, BigDecimal datePercent, BigDecimal workPercent) {
return new Stretch(date, datePercent, workPercent);
}
public static Stretch copy(Stretch stretch) {
@ -70,7 +71,8 @@ public class Stretch {
@NotNull
private BigDecimal amountWorkPercentage = BigDecimal.ZERO;
// Transient value
// Trasient value, a stretch is readOnly if it's a consolidated stretch
// or if it is a stretch user cannot edit
private boolean readOnly = false;
private Stretch(LocalDate date, BigDecimal lengthPercent, BigDecimal progressPercent) {
@ -144,3 +146,65 @@ public class Stretch {
}
}
/**
*
* @author Diego Pino García <dpino@igalia.com>
*
* Builds Stretch from consolidated progress in resource allocation.
*
*/
class ConsolidatedStretch {
protected static Stretch fromConsolidatedProgress(
ResourceAllocation<?> resourceAllocation) {
List<? extends DayAssignment> consolidated = resourceAllocation.getConsolidatedAssignments();
if (consolidated.isEmpty()) {
return null;
}
final Task task = resourceAllocation.getTask();
final LocalDate start = task.getStartAsLocalDate();
final LocalDate taskEnd = task.getEndAsLocalDate();
final LocalDate consolidatedEnd = lastDay(consolidated);
Days daysDuration = Days.daysBetween(start, taskEnd);
Days daysWorked = Days.daysBetween(start, consolidatedEnd);
BigDecimal daysPercent = daysPercent(daysWorked, daysDuration);
return create(consolidatedEnd.plusDays(1), daysPercent, task.getAdvancePercentage());
}
private static Stretch create(LocalDate consolidatedEnd,
BigDecimal advancePercentage, BigDecimal percentWorked) {
Stretch result = Stretch.create(consolidatedEnd, advancePercentage, percentWorked);
result.readOnly(true);
return result;
}
private ConsolidatedStretch() {
}
private static BigDecimal daysPercent(Days daysPartial, Days daysTotal) {
return percentWorked(daysPartial.getDays(), daysTotal.getDays());
}
private static BigDecimal percentWorked(int daysPartial, int daysTotal) {
return divide(BigDecimal.valueOf(daysPartial), Integer.valueOf(daysTotal));
}
private static BigDecimal divide(BigDecimal numerator, Integer denominator) {
if (Integer.valueOf(0).equals(denominator)) {
return BigDecimal.ZERO;
}
return numerator.divide(BigDecimal.valueOf(denominator), 8,
BigDecimal.ROUND_HALF_EVEN);
}
private static LocalDate lastDay(List<? extends DayAssignment> days) {
return days.get(days.size() - 1).getDay();
}
}

View file

@ -21,6 +21,9 @@
package org.navalplanner.business.planner.entities;
import static org.navalplanner.business.i18n.I18nHelper._;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
@ -37,8 +40,11 @@ import org.joda.time.Days;
import org.joda.time.LocalDate;
/**
* Assignment function by stretches.
* @author Diego Pino García <dpino@igalia.com>
* @author Manuel Rego Casasnovas <mrego@igalia.com>
*
* Assignment function by stretches.
*
*/
public class StretchesFunction extends AssignmentFunction {
@ -50,6 +56,20 @@ public class StretchesFunction extends AssignmentFunction {
private final BigDecimal loadProportion;
private boolean readOnly = false;
public static Interval create(BigDecimal loadProportion, LocalDate start,
LocalDate end, boolean readOnly) {
Interval result = create(loadProportion, start, end);
result.readOnly(readOnly);
return result;
}
public static Interval create(BigDecimal loadProportion, LocalDate start,
LocalDate end) {
return new Interval(loadProportion, start, end);
}
public Interval(BigDecimal loadProportion, LocalDate start,
LocalDate end) {
Validate.notNull(loadProportion);
@ -113,6 +133,8 @@ public class StretchesFunction extends AssignmentFunction {
private void apply(ResourceAllocation<?> resourceAllocation,
LocalDate startInclusive, LocalDate taskEnd,
int intervalHours) {
Validate.isTrue(!isReadOnly());
// End has to be exclusive on last Stretch
LocalDate endDate = getEnd();
if (endDate.equals(taskEnd)) {
@ -130,6 +152,8 @@ public class StretchesFunction extends AssignmentFunction {
if (intervalsDefinedByStreches.isEmpty()) {
return;
}
Validate.isTrue(totalHours == allocation.getNonConsolidatedHours());
int[] hoursPerInterval = getHoursPerInterval(
intervalsDefinedByStreches, totalHours);
int remainder = totalHours - sum(hoursPerInterval);
@ -139,7 +163,6 @@ public class StretchesFunction extends AssignmentFunction {
interval.apply(allocation, allocationStart, allocationEnd,
hoursPerInterval[i++]);
}
Validate.isTrue(totalHours == allocation.getAssignedHours());
}
private static int[] getHoursPerInterval(
@ -160,6 +183,18 @@ public class StretchesFunction extends AssignmentFunction {
return result;
}
public String toString() {
return String.format("[%s, %s]: %s ", start, end, loadProportion);
}
public boolean isReadOnly() {
return readOnly;
}
public void readOnly(boolean value) {
readOnly = value;
}
}
private List<Stretch> stretches = new ArrayList<Stretch>();
@ -169,6 +204,8 @@ public class StretchesFunction extends AssignmentFunction {
// Transient field. Not stored
private StretchesFunctionTypeEnum desiredType;
// Transient. Calculated from resourceAllocation
private Stretch consolidatedStretch;
public static StretchesFunction create() {
return (StretchesFunction) create(new StretchesFunction());
@ -184,15 +221,17 @@ public class StretchesFunction extends AssignmentFunction {
public static List<Interval> intervalsFor(
Collection<? extends Stretch> streches) {
ArrayList<Interval> result = new ArrayList<Interval>();
LocalDate previous = null;
BigDecimal sumOfProportions = new BigDecimal(0);
LocalDate previous = null, stretchDate = null;
BigDecimal sumOfProportions = BigDecimal.ZERO, loadedProportion = BigDecimal.ZERO;
for (Stretch each : streches) {
LocalDate strechDate = each.getDate();
result.add(new Interval(each.getAmountWorkPercentage().subtract(
sumOfProportions), previous,
strechDate));
stretchDate = each.getDate();
loadedProportion = each.getAmountWorkPercentage().subtract(
sumOfProportions);
result.add(Interval.create(loadedProportion, previous,
stretchDate, each.isReadOnly()));
sumOfProportions = each.getAmountWorkPercentage();
previous = strechDate;
previous = stretchDate;
}
return result;
}
@ -206,6 +245,7 @@ public class StretchesFunction extends AssignmentFunction {
result.resetToStrechesFrom(this);
result.type = type;
result.desiredType = desiredType;
result.consolidatedStretch = consolidatedStretch;
return result;
}
@ -214,6 +254,7 @@ public class StretchesFunction extends AssignmentFunction {
for (Stretch each : from.getStretches()) {
this.addStretch(Stretch.copy(each));
}
this.consolidatedStretch = from.consolidatedStretch;
}
public void setStretches(List<Stretch> stretches) {
@ -261,19 +302,18 @@ public class StretchesFunction extends AssignmentFunction {
@AssertTrue(message = "At least one stretch is needed")
public boolean checkNoEmpty() {
return !stretches.isEmpty();
return !getStrechesPlusConsolidated().isEmpty();
}
@AssertTrue(message = "Some stretch has higher or equal values than the "
+ "previous stretch")
public boolean checkStretchesOrder() {
if (stretches.isEmpty()) {
List<Stretch> stretchesPlusConsolidated = getStrechesPlusConsolidated();
if (stretchesPlusConsolidated.isEmpty()) {
return false;
}
sortStretches();
Iterator<Stretch> iterator = stretches.iterator();
Iterator<Stretch> iterator = stretchesPlusConsolidated.iterator();
Stretch previous = iterator.next();
while (iterator.hasNext()) {
Stretch current = iterator.next();
@ -293,6 +333,15 @@ public class StretchesFunction extends AssignmentFunction {
return true;
}
public List<Stretch> getStrechesPlusConsolidated() {
List<Stretch> result = new ArrayList<Stretch>();
result.addAll(stretches);
if (consolidatedStretch != null) {
result.add(consolidatedStretch);
}
return Collections.unmodifiableList(Stretch.sortByDate(result));
}
@AssertTrue(message = "Last stretch should have one hundred percent for "
+ "length and amount of work percentage")
public boolean checkOneHundredPercent() {
@ -334,14 +383,17 @@ public class StretchesFunction extends AssignmentFunction {
if (stretches.isEmpty()) {
return Collections.emptyList();
}
List<Interval> result = intervalsFor(stretches);
checkStretchesSumOneHundredPercent();
return intervalsFor(stretches);
}
private void checkStretchesSumOneHundredPercent() {
BigDecimal sumOfProportions = stretches.isEmpty() ? BigDecimal.ZERO
: last(stretches).getAmountWorkPercentage();
BigDecimal left = calculateLeftFor(sumOfProportions);
if (!left.equals(BigDecimal.ZERO)) {
throw new IllegalStateException("the streches must sum the 100%");
throw new IllegalStateException(_("Stretches must sum 100%"));
}
return result;
}
private BigDecimal calculateLeftFor(BigDecimal sumOfProportions) {
@ -354,4 +406,12 @@ public class StretchesFunction extends AssignmentFunction {
return getDesiredType() != StretchesFunctionTypeEnum.INTERPOLATED || stretches.size() >= 2;
}
public void setConsolidatedStretch(Stretch stretch) {
consolidatedStretch = stretch;
}
public Stretch getConsolidatedStretch() {
return consolidatedStretch;
}
}

View file

@ -21,6 +21,7 @@
package org.navalplanner.business.planner.entities;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.math.FunctionEvaluationException;
@ -171,17 +172,16 @@ public enum StretchesFunctionTypeEnum {
}
}
public void applyTo(ResourceAllocation<?> resourceAllocation,
StretchesFunction stretchesFunction) {
List<Interval> intervalsDefinedByStreches = stretchesFunction
.getIntervalsDefinedByStreches();
int totalHours = resourceAllocation.getAssignedHours();
Task task = resourceAllocation.getTask();
LocalDate start = LocalDate.fromDateFields(task.getStartDate());
LocalDate end = LocalDate.fromDateFields(task.getEndDate());
apply(resourceAllocation, intervalsDefinedByStreches, start, end,
totalHours);
List<Interval> intervals = new ArrayList<Interval>();
intervals.addAll(stretchesFunction.getIntervalsDefinedByStreches());
LocalDate start = resourceAllocation.getFirstNonConsolidatedDate();
LocalDate end = resourceAllocation.getTask().getEndAsLocalDate();
int totalHours = resourceAllocation.getNonConsolidatedHours();
apply(resourceAllocation, intervals, start, end, totalHours);
}
protected abstract void apply(ResourceAllocation<?> allocation,

View file

@ -96,7 +96,7 @@ public abstract class StrechesFunctionConfiguration implements
public void applyOn(ResourceAllocation<?> resourceAllocation) {
StretchesFunction stretchesFunction = StretchesFunctionModel
.createDefaultStretchesFunction(resourceAllocation.getTask()
.getEndDate());
.getEndAsLocalDate());
resourceAllocation.setAssignmentFunction(stretchesFunction);
}

View file

@ -52,21 +52,21 @@ import org.springframework.transaction.annotation.Transactional;
import org.zkoss.util.Locales;
/**
* Model for UI operations related to {@link StretchesFunction} configuration.
*
* @author Manuel Rego Casasnovas <mrego@igalia.com>
* @author Diego Pino García <dpino@igalia.com>
*
* Model for UI operations related to {@link StretchesFunction}
* configuration.
*
*/
@Service
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class StretchesFunctionModel implements IStretchesFunctionModel {
public static StretchesFunction createDefaultStretchesFunction(Date endDate) {
public static StretchesFunction createDefaultStretchesFunction(LocalDate endDate) {
StretchesFunction stretchesFunction = StretchesFunction.create();
Stretch stretch = new Stretch();
stretch.setDate(new LocalDate(endDate));
stretch.setLengthPercentage(BigDecimal.ONE);
stretch.setAmountWorkPercentage(BigDecimal.ONE);
stretchesFunction.addStretch(stretch);
stretchesFunction.addStretch(Stretch.create(endDate, BigDecimal.ONE,
BigDecimal.ONE));
return stretchesFunction;
}
@ -100,16 +100,32 @@ public class StretchesFunctionModel implements IStretchesFunctionModel {
StretchesFunctionTypeEnum type) {
if (stretchesFunction != null) {
assignmentFunctionDAO.reattach(stretchesFunction);
this.originalStretchesFunction = stretchesFunction;
this.stretchesFunction = stretchesFunction.copy();
this.stretchesFunction.changeTypeTo(type);
// Initialize resourceAllocation and task
this.resourceAllocation = resourceAllocation;
this.task = resourceAllocation.getTask();
forceLoadData();
this.taskEndDate = task.getEndDate();
// Initialize stretchesFunction
this.originalStretchesFunction = stretchesFunction;
this.stretchesFunction = stretchesFunction.copy();
this.stretchesFunction.changeTypeTo(type);
addConsolidatedStretchIfAny();
}
}
private void addConsolidatedStretchIfAny() {
Stretch consolidated = consolidatedStretchFor(resourceAllocation);
if (consolidated != null) {
stretchesFunction.setConsolidatedStretch(consolidated);
}
}
private Stretch consolidatedStretchFor(ResourceAllocation<?> resourceAllocation) {
return Stretch.buildFromConsolidatedProgress(resourceAllocation);
}
private void forceLoadData() {
taskSourceDAO.reattach(task.getTaskSource());
taskElementDAO.reattach(task);
@ -126,19 +142,26 @@ public class StretchesFunctionModel implements IStretchesFunctionModel {
}
/**
* Returns an empty stretch plus the stretches from stretchesFunction
* Returns an empty stretch plus the stretches from stretchesFunction and
* the consolidated stretch if any
*
* @return
*/
private List<Stretch> allStretches() {
List<Stretch> result = new ArrayList<Stretch>();
result.add(firstStretch());
result.addAll(stretchesFunction.getStretches());
result.addAll(stretchesFunction.getStrechesPlusConsolidated());
return result;
}
/**
* Defines an initial read-only stretch with 0% hours worked and 0% progress
*
* @return
*/
private Stretch firstStretch() {
Stretch result = Stretch.create(task.getStartAsLocalDate(), BigDecimal.ZERO, BigDecimal.ZERO);
Stretch result = Stretch.create(task.getStartAsLocalDate(),
BigDecimal.ZERO, BigDecimal.ZERO);
result.readOnly(true);
return result;
}