From 9b1fb6c87cff2fc05c591b519a4a628e580a02e5 Mon Sep 17 00:00:00 2001 From: Diego Pino Garcia Date: Fri, 22 Apr 2011 13:07:47 +0200 Subject: [PATCH] [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 --- .../planner/entities/ResourceAllocation.java | 16 +++- .../business/planner/entities/Stretch.java | 70 +++++++++++++- .../planner/entities/StretchesFunction.java | 94 +++++++++++++++---- .../entities/StretchesFunctionTypeEnum.java | 18 ++-- .../StrechesFunctionConfiguration.java | 2 +- .../streches/StretchesFunctionModel.java | 51 +++++++--- 6 files changed, 205 insertions(+), 46 deletions(-) diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/ResourceAllocation.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/ResourceAllocation.java index aba60dfdb..1ec7e6cad 100644 --- a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/ResourceAllocation.java +++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/ResourceAllocation.java @@ -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 * @author Manuel Rego Casasnovas + * + * Resources are allocated to planner tasks. */ public abstract class ResourceAllocation extends BaseEntity { @@ -2010,4 +2012,14 @@ public abstract class ResourceAllocation 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 nonConsolidated = getNonConsolidatedAssignments(); + return (!nonConsolidated.isEmpty()) ? nonConsolidated.get(0).getDay() + : null; + } + +} \ No newline at end of file diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/Stretch.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/Stretch.java index 00af7ef68..7fc5bc513 100644 --- a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/Stretch.java +++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/Stretch.java @@ -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 + * + * Builds Stretch from consolidated progress in resource allocation. + * + */ +class ConsolidatedStretch { + + protected static Stretch fromConsolidatedProgress( + ResourceAllocation resourceAllocation) { + + List 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 days) { + return days.get(days.size() - 1).getDay(); + } + +} \ No newline at end of file diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/StretchesFunction.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/StretchesFunction.java index f3f4fc669..bb35390f3 100644 --- a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/StretchesFunction.java +++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/StretchesFunction.java @@ -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 * @author Manuel Rego Casasnovas + * + * 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 stretches = new ArrayList(); @@ -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 intervalsFor( Collection streches) { ArrayList result = new ArrayList(); - 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 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 stretchesPlusConsolidated = getStrechesPlusConsolidated(); + if (stretchesPlusConsolidated.isEmpty()) { return false; } - sortStretches(); - - Iterator iterator = stretches.iterator(); + Iterator 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 getStrechesPlusConsolidated() { + List result = new ArrayList(); + 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 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; + } + } diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/StretchesFunctionTypeEnum.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/StretchesFunctionTypeEnum.java index 8684fdf0a..e8d50ad55 100644 --- a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/StretchesFunctionTypeEnum.java +++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/StretchesFunctionTypeEnum.java @@ -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 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 intervals = new ArrayList(); + 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, diff --git a/navalplanner-webapp/src/main/java/org/navalplanner/web/planner/allocation/streches/StrechesFunctionConfiguration.java b/navalplanner-webapp/src/main/java/org/navalplanner/web/planner/allocation/streches/StrechesFunctionConfiguration.java index 704a6f791..5a22cdc21 100644 --- a/navalplanner-webapp/src/main/java/org/navalplanner/web/planner/allocation/streches/StrechesFunctionConfiguration.java +++ b/navalplanner-webapp/src/main/java/org/navalplanner/web/planner/allocation/streches/StrechesFunctionConfiguration.java @@ -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); } diff --git a/navalplanner-webapp/src/main/java/org/navalplanner/web/planner/allocation/streches/StretchesFunctionModel.java b/navalplanner-webapp/src/main/java/org/navalplanner/web/planner/allocation/streches/StretchesFunctionModel.java index b5e47df24..5afff11d0 100644 --- a/navalplanner-webapp/src/main/java/org/navalplanner/web/planner/allocation/streches/StretchesFunctionModel.java +++ b/navalplanner-webapp/src/main/java/org/navalplanner/web/planner/allocation/streches/StretchesFunctionModel.java @@ -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 + * @author Diego Pino García + * + * 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 allStretches() { List result = new ArrayList(); 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; }