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; }