/*
* This file is part of LibrePlan
*
* Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e
* Desenvolvemento Tecnolóxico de Galicia
* Copyright (C) 2010-2012 Igalia, S.L.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.libreplan.business.planner.entities;
import static java.util.Collections.emptyList;
import static org.libreplan.business.workingday.EffortDuration.min;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.validation.Valid;
import javax.validation.constraints.AssertTrue;
import org.apache.commons.lang3.Validate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.libreplan.business.calendars.entities.AvailabilityTimeLine;
import org.libreplan.business.calendars.entities.ICalendar;
import org.libreplan.business.calendars.entities.SameWorkHoursEveryDay;
import org.libreplan.business.externalcompanies.entities.ExternalCompany;
import org.libreplan.business.orders.entities.AggregatedHoursGroup;
import org.libreplan.business.orders.entities.HoursGroup;
import org.libreplan.business.orders.entities.OrderElement;
import org.libreplan.business.orders.entities.SumChargedEffort;
import org.libreplan.business.orders.entities.TaskSource;
import org.libreplan.business.planner.entities.AssignedEffortForResource.IAssignedEffortForResource;
import org.libreplan.business.planner.entities.AssignedEffortForResource.WithTheLoadOf;
import org.libreplan.business.planner.entities.DayAssignment.FilterType;
import org.libreplan.business.planner.entities.Dependency.Type;
import org.libreplan.business.planner.entities.DerivedAllocationGenerator.IWorkerFinder;
import org.libreplan.business.planner.entities.ResourceAllocation.Direction;
import org.libreplan.business.planner.entities.allocationalgorithms.AllocationModification;
import org.libreplan.business.planner.entities.allocationalgorithms.EffortModification;
import org.libreplan.business.planner.entities.allocationalgorithms.ResourcesPerDayModification;
import org.libreplan.business.planner.entities.consolidations.Consolidation;
import org.libreplan.business.planner.limiting.entities.LimitingResourceQueueElement;
import org.libreplan.business.resources.daos.IResourcesSearcher;
import org.libreplan.business.resources.entities.Criterion;
import org.libreplan.business.resources.entities.Resource;
import org.libreplan.business.resources.entities.Worker;
import org.libreplan.business.scenarios.entities.Scenario;
import org.libreplan.business.util.TaskElementVisitor;
import org.libreplan.business.util.deepcopy.AfterCopy;
import org.libreplan.business.workingday.EffortDuration;
import org.libreplan.business.workingday.IntraDayDate;
import org.libreplan.business.workingday.IntraDayDate.PartialDay;
import org.libreplan.business.workingday.ResourcesPerDay;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Óscar González Fernández <ogonzalez@igalia.com>
* @author Manuel Rego Casasnovas <rego@igalia.com>
*/
public class Task extends TaskElement implements ITaskPositionConstrained {
private static final Log LOG = LogFactory.getLog(Task.class);
/**
* Maximum number of days in order to looking for calendar capacity (defined
* to 5 years)
*/
private final static int MAX_DAYS_LOOKING_CAPACITY = 360 * 5;
public static Task createTask(TaskSource taskSource) {
Task task = new Task();
OrderElement orderElement = taskSource.getOrderElement();
orderElement.applyInitialPositionConstraintTo(task);
Task result = create(task, taskSource);
result.initializeDates();
return result;
}
/**
* Needed for import external tasks.
*
* Creates a Task without initializing the dates.
*
* @param taskSource
* TaskSouce for create the task
*
* @return Task New Task
*/
public static Task createTaskWithoutDatesInitialized(TaskSource taskSource) {
Task task = new Task();
OrderElement orderElement = taskSource.getOrderElement();
orderElement.applyInitialPositionConstraintTo(task);
Task result = create(task, taskSource);
return result;
}
@Override
protected void initializeDates() {
EffortDuration workHours = EffortDuration.hours(getWorkHours());
DurationBetweenDates duration = fromFixedDuration(workHours);
IntraDayDate start = getIntraDayStartDate();
if ( start != null ) {
setIntraDayEndDate(duration.fromStartToEnd(start));
} else {
IntraDayDate end = getIntraDayEndDate();
setIntraDayStartDate(duration.fromEndToStart(end));
}
}
/**
* Calculates end date for a task, starting from start until fulfilling
* number of hours.
*
* For tasks with limiting resources it's needed to resize a task if the
* number of hours allocated to a resource changes. In non limiting
* resources, the task is resized because when the number of hours changes,
* new days assignments are generated, and then the task is resized
* accordingly.
*
* @param hours
*/
public void resizeToHours(int hours) {
Validate.isTrue(isLimiting());
EffortDuration workHours = EffortDuration.hours(hours);
DurationBetweenDates duration = new DurationBetweenDates(0, workHours);
setIntraDayEndDate(duration.fromStartToEnd(getIntraDayStartDate()));
}
private CalculatedValue calculatedValue = CalculatedValue.END_DATE;
private TaskStatusEnum currentStatus = null;
private Set<ResourceAllocation<?>> resourceAllocations = new HashSet<>();
@Valid
private Set<ResourceAllocation<?>> getResourceAllocations() {
return new HashSet<>(resourceAllocations);
}
@SuppressWarnings("unused")
@AfterCopy
private void ifLimitingAllocationRemove() {
if ( isLimiting() ) {
resourceAllocations.clear();
}
}
private TaskPositionConstraint positionConstraint = new TaskPositionConstraint();
private SubcontractedTaskData subcontractedTaskData;
private Integer priority;
private Consolidation consolidation;
private Integer workableDays;
private Direction lastAllocationDirection = Direction.FORWARD;
/**
* Constructor for hibernate. Do not use!
*/
public Task() {
}
@SuppressWarnings("unused")
@AssertTrue(message = "element associated to a task must be not empty")
private boolean isTheOrderElementMustBeNotNullConstraint() {
return getOrderElement() != null;
}
public HoursGroup getHoursGroup() {
return getTaskSource().getHoursGroups().iterator().next();
}
public Set<Criterion> getCriterions() {
return Collections.unmodifiableSet(getHoursGroup().getValidCriterions());
}
public Integer getHoursSpecifiedAtOrder() {
return getWorkHours();
}
public int getAssignedHours() {
return AggregateOfResourceAllocations.createFromSatisfied(resourceAllocations).getTotalHours();
}
public EffortDuration getAssignedEffort() {
return AggregateOfResourceAllocations.createFromSatisfied(resourceAllocations).getTotalEffort();
}
private EffortDuration getTotalNonConsolidatedEffort() {
return AggregateOfResourceAllocations.createFromAll(resourceAllocations).getNonConsolidatedEffort();
}
public int getTotalHours() {
return (getTaskSource() != null) ? getTaskSource().getTotalHours() : 0;
}
@Override
public boolean isLeaf() {
return true;
}
@Override
public List<TaskElement> getChildren() {
return Collections.emptyList();
}
public Set<ResourceAllocation<?>> getSatisfiedResourceAllocations() {
Set<ResourceAllocation<?>> result = new HashSet<>();
if ( isLimiting() ) {
result.addAll(getLimitingResourceAllocations());
} else {
result.addAll(ResourceAllocation.getSatisfied(resourceAllocations));
}
return Collections.unmodifiableSet(result);
}
@Override
@Transactional(readOnly = true)
public Set<ResourceAllocation<?>> getAllResourceAllocations() {
return Collections.unmodifiableSet(resourceAllocations);
}
public Set<ResourceAllocation<?>> getLimitingResourceAllocations() {
Set<ResourceAllocation<?>> result = new HashSet<>();
for (ResourceAllocation<?> each: resourceAllocations) {
if ( each.isLimiting() ) {
result.add(each);
}
}
return Collections.unmodifiableSet(result);
}
public Set<ResourceAllocation<?>> getNonLimitingResourceAllocations() {
Set<ResourceAllocation<?>> result = new HashSet<>();
for (ResourceAllocation<?> each: resourceAllocations) {
if ( !each.isLimiting() ) {
result.add(each);
}
}
return Collections.unmodifiableSet(result);
}
public boolean isLimiting() {
return !(getLimitingResourceAllocations().isEmpty());
}
private ResourceAllocation<?> getAssociatedLimitingResourceAllocation() {
Set<ResourceAllocation<?>> resourceAllocations = getLimitingResourceAllocations();
return (resourceAllocations.size() > 0) ? resourceAllocations.iterator().next() : null;
}
public LimitingResourceQueueElement getAssociatedLimitingResourceQueueElementIfAny() {
if ( !isLimiting() ) {
throw new IllegalStateException("this is not a limiting task");
}
return getAssociatedLimitingResourceAllocation().getLimitingResourceQueueElement();
}
public boolean isLimitingAndHasDayAssignments() {
ResourceAllocation<?> resourceAllocation = getAssociatedLimitingResourceAllocation();
return resourceAllocation != null && resourceAllocation.isLimitingAndHasDayAssignments();
}
public void addResourceAllocation(ResourceAllocation<?> resourceAllocation) {
addResourceAllocation(resourceAllocation, true);
}
public void addResourceAllocation(ResourceAllocation<?> resourceAllocation, boolean generateDayAssignments) {
if ( !resourceAllocation.getTask().equals(this) ) {
throw new IllegalArgumentException("the resourceAllocation's task must be this task");
}
resourceAllocations.add(resourceAllocation);
if ( generateDayAssignments ) {
resourceAllocation.associateAssignmentsToResource();
}
}
public ResourceAllocation<?> getResourceAllocation() {
Validate.isTrue(isLimiting());
return resourceAllocations.isEmpty() ? null : resourceAllocations.iterator().next();
}
public void setResourceAllocation(ResourceAllocation<?> resourceAllocation) {
Validate.isTrue(resourceAllocation.isLimiting());
removeAllResourceAllocations();
resourceAllocations.add(resourceAllocation);
}
public void removeResourceAllocation(ResourceAllocation<?> resourceAllocation) {
resourceAllocation.detach();
resourceAllocations.remove(resourceAllocation);
}
public CalculatedValue getCalculatedValue() {
if ( calculatedValue == null ) {
return CalculatedValue.END_DATE;
}
return calculatedValue;
}
public void setCalculatedValue(CalculatedValue calculatedValue) {
Validate.notNull(calculatedValue);
this.calculatedValue = calculatedValue;
}
/**
* Checks if there isn't any {@link Worker} repeated in the {@link Set} of
* {@link ResourceAllocation} of this {@link Task}.
* @return <code>true</code> if the {@link Task} is valid, that means there
* isn't any {@link Worker} repeated.
*/
public boolean isValidResourceAllocationWorkers() {
Set<Long> workers = new HashSet<>();
for (ResourceAllocation<?> resourceAllocation : resourceAllocations) {
if ( resourceAllocation instanceof SpecificResourceAllocation ) {
Resource resource = ((SpecificResourceAllocation) resourceAllocation).getResource();
if ( resource != null ) {
if ( workers.contains(resource.getId()) ) {
return false;
} else {
workers.add(resource.getId());
}
}
}
}
return true;
}
public Set<GenericResourceAllocation> getGenericResourceAllocations() {
return new HashSet<>(ResourceAllocation.getOfType(
GenericResourceAllocation.class,
getSatisfiedResourceAllocations()));
}
public Set<SpecificResourceAllocation> getSpecificResourceAllocations() {
return new HashSet<>(ResourceAllocation.getOfType(
SpecificResourceAllocation.class,
getSatisfiedResourceAllocations()));
}
public static class ModifiedAllocation {
public static List<ModifiedAllocation> copy(
Scenario onScenario,
Collection<ResourceAllocation<?>> resourceAllocations) {
List<ModifiedAllocation> result = new ArrayList<>();
for (ResourceAllocation<?> resourceAllocation : resourceAllocations) {
result.add(new ModifiedAllocation(resourceAllocation, resourceAllocation.copy(onScenario)));
}
return result;
}
public static List<ResourceAllocation<?>> modified(Collection<? extends ModifiedAllocation> collection) {
List<ResourceAllocation<?>> result = new ArrayList<>();
for (ModifiedAllocation modifiedAllocation : collection) {
result.add(modifiedAllocation.getModification());
}
return result;
}
public static List<ResourceAllocation<?>> originals
(Collection<? extends ModifiedAllocation> modifiedAllocations) {
List<ResourceAllocation<?>> result = new ArrayList<>();
for (ModifiedAllocation each : modifiedAllocations) {
result.add(each.getOriginal());
}
return result;
}
private final ResourceAllocation<?> original;
private final ResourceAllocation<?> modification;
public ModifiedAllocation(ResourceAllocation<?> original, ResourceAllocation<?> modification) {
Validate.notNull(original);
Validate.notNull(modification);
this.original = original;
this.modification = modification;
}
public ResourceAllocation<?> getOriginal() {
return original;
}
public ResourceAllocation<?> getModification() {
return modification;
}
}
public void mergeAllocation(
Scenario scenario,
final IntraDayDate start,
final IntraDayDate end, Integer newWorkableDays,
CalculatedValue calculatedValue,
List<ResourceAllocation<?>> newAllocations,
List<ModifiedAllocation> modifications,
Collection<? extends ResourceAllocation<?>> toRemove) {
this.calculatedValue = calculatedValue;
this.workableDays = calculatedValue == CalculatedValue.END_DATE ? null : newWorkableDays;
setIntraDayStartDate(start);
setIntraDayEndDate(end);
for (ModifiedAllocation pair : modifications) {
Validate.isTrue(resourceAllocations.contains(pair.getOriginal()));
pair.getOriginal().mergeAssignmentsAndResourcesPerDay(scenario, pair.getModification());
}
remove(toRemove);
addAllocations(scenario, newAllocations);
}
private void remove(Collection<? extends ResourceAllocation<?>> toRemove) {
for (ResourceAllocation<?> resourceAllocation : toRemove) {
removeResourceAllocation(resourceAllocation);
}
}
private void addAllocations(Scenario scenario, List<ResourceAllocation<?>> newAllocations) {
for (ResourceAllocation<?> resourceAllocation : newAllocations) {
resourceAllocation.switchToScenario(scenario);
addResourceAllocation(resourceAllocation);
}
}
public void explicityMoved(IntraDayDate startDate, IntraDayDate endDate) {
getPositionConstraint().explicityMovedTo(startDate, endDate, getOrderElement().getOrder().getSchedulingMode());
}
public TaskPositionConstraint getPositionConstraint() {
if ( positionConstraint == null ) {
positionConstraint = new TaskPositionConstraint();
}
return positionConstraint;
}
private static class ModificationsResult<T extends AllocationModification> {
static <T extends AllocationModification> ModificationsResult<T> create(
List<ResourceAllocation<?>> original, List<T> canBeModified) {
List<ResourceAllocation<?>> beingModified = AllocationModification.getBeingModified(canBeModified);
List<ResourceAllocation<?>> noLongerValid = new ArrayList<>();
for (ResourceAllocation<?> each : original) {
if ( !beingModified.contains(each) ) {
noLongerValid.add(each);
}
}
return new ModificationsResult<>(canBeModified, noLongerValid);
}
private final List<T> valid;
private final List<ResourceAllocation<?>> noLongerValid;
private ModificationsResult(List<T> valid, List<ResourceAllocation<?>> noLongerValid) {
this.valid = Collections.unmodifiableList(valid);
this.noLongerValid = Collections.unmodifiableList(noLongerValid);
}
public List<T> getBeingModified() {
return valid;
}
public List<ResourceAllocation<?>> getNoLongerValid() {
return noLongerValid;
}
}
private static class WithPotentiallyNewResources {
protected final IResourcesSearcher searcher;
public WithPotentiallyNewResources(IResourcesSearcher searcher) {
Validate.notNull(searcher);
this.searcher = searcher;
}
public ModificationsResult<EffortModification> getHoursModified(List<ResourceAllocation<?>> allocations) {
List<EffortModification> canBeModified = EffortModification.withNewResources(allocations, searcher);
return ModificationsResult.create(allocations, canBeModified);
}
public ModificationsResult<ResourcesPerDayModification> getResourcesPerDayModified(
List<ResourceAllocation<?>> allocations) {
List<ResourcesPerDayModification> canBeModified =
ResourcesPerDayModification.withNewResources(allocations, searcher);
return ModificationsResult.create(allocations, canBeModified);
}
}
public void copyAssignmentsFromOneScenarioToAnother(Scenario from, Scenario to) {
for (ResourceAllocation<?> each : getAllResourceAllocations()) {
each.copyAssignmentsFromOneScenarioToAnother(from, to);
}
}
@Override
protected IDatesHandler createDatesHandler(final Scenario scenario, final IResourcesSearcher searcher) {
return new IDatesHandler() {
@Override
public void moveTo(IntraDayDate newStartDate) {
IntraDayDate previousStart = getIntraDayStartDate();
if ( previousStart.equals(newStartDate) ) {
return;
}
setIntraDayEndDate(calculateEndKeepingLength(newStartDate));
setIntraDayStartDate(newStartDate);
doReassignment(Direction.FORWARD);
}
private void doReassignment(Direction direction) {
reassign(scenario, direction, new WithPotentiallyNewResources(searcher));
}
@Override
public void moveEndTo(IntraDayDate newEnd) {
if ( getIntraDayEndDate().equals(newEnd) ) {
return;
}
setIntraDayStartDate(calculateNewStartGivenEnd(newEnd));
setIntraDayEndDate(newEnd);
doReassignment(Direction.BACKWARD);
}
private IntraDayDate calculateNewStartGivenEnd(IntraDayDate newEnd) {
return calculateStartKeepingLength(newEnd);
}
@Override
public void resizeTo(IntraDayDate endDate) {
if ( !canBeResized() || getIntraDayEndDate().equals(endDate) ) {
return;
}
setIntraDayEndDate(endDate);
updateWorkableDays();
doReassignment(getAllocationDirection());
}
private void updateWorkableDays() {
assert calculatedValue != CalculatedValue.END_DATE;
workableDays = getWorkableDaysBetweenDates();
}
};
}
public IntraDayDate calculateEndKeepingLength(IntraDayDate newStartDate) {
DurationBetweenDates durationBetweenDates = getDurationBetweenDates();
return durationBetweenDates.fromStartToEnd(newStartDate);
}
public IntraDayDate calculateEndGivenWorkableDays(int days) {
Validate.isTrue(days >= 0);
DurationBetweenDates duration = fromFixedDuration(days);
return duration.fromStartToEnd(getIntraDayStartDate());
}
public IntraDayDate calculateStartGivenWorkableDays(int days) {
Validate.isTrue(days >= 0);
DurationBetweenDates duration = fromFixedDuration(days);
return duration.fromEndToStart(getIntraDayEndDate());
}
private IntraDayDate calculateStartKeepingLength(IntraDayDate newEnd) {
DurationBetweenDates durationBetweenDates = getDurationBetweenDates();
return durationBetweenDates.fromEndToStart(newEnd);
}
private DurationBetweenDates getDurationBetweenDates() {
if ( workableDays != null ) {
return fromFixedDuration(workableDays);
} else {
return fromCurrentDuration();
}
}
private DurationBetweenDates fromFixedDuration(int fixedNumberOfWorkableDays) {
return new DurationBetweenDates(fixedNumberOfWorkableDays, EffortDuration.zero());
}
private DurationBetweenDates fromCurrentDuration() {
IntraDayDate start = getIntraDayStartDate();
IntraDayDate end = getIntraDayEndDate();
int calculatedWorkableDays = getWorkableDaysFrom(start.roundUp(), end.roundDown());
EffortDuration extraDuration = getExtraDurationAtStart(start).plus(end.getEffortDuration());
return new DurationBetweenDates(calculatedWorkableDays, extraDuration);
}
private EffortDuration getExtraDurationAtStart(IntraDayDate start) {
if ( start.getEffortDuration().isZero() ) {
return EffortDuration.zero();
}
ICalendar calendar = getNullSafeCalendar();
EffortDuration capacity = calendar.getCapacityOn(PartialDay.wholeDay(start.getDate()));
return capacity.minus(min(start.getEffortDuration(), capacity));
}
private ICalendar getNullSafeCalendar() {
return getCalendar() != null ? getCalendar() : SameWorkHoursEveryDay.getDefaultWorkingDay();
}
private DurationBetweenDates fromFixedDuration(EffortDuration duration) {
return new DurationBetweenDates(0, duration);
}
private class DurationBetweenDates {
private final int numberOfWorkableDays;
private final EffortDuration remainderDuration;
private final ICalendar calendar;
private DurationBetweenDates(int numberOfWorkableDays, EffortDuration remainderDuration) {
this.numberOfWorkableDays = numberOfWorkableDays;
this.remainderDuration = remainderDuration;
this.calendar = getNullSafeCalendar();
}
public IntraDayDate fromStartToEnd(IntraDayDate newStartDate) {
LocalDate resultDay = afterSomeWorkableDays(newStartDate.getDate(), numberOfWorkableDays);
return plusDuration(
IntraDayDate.startOfDay(resultDay),
remainderDuration.plus(newStartDate.getEffortDuration()));
}
private LocalDate afterSomeWorkableDays(LocalDate start, int workableDays) {
LocalDate result = start;
for (int i = 0; i < workableDays; result = result.plusDays(1)) {
if ( isWorkable(result) ) {
i++;
}
}
return result;
}
private IntraDayDate plusDuration(IntraDayDate start, EffortDuration remaining) {
IntraDayDate result = IntraDayDate.startOfDay(start.getDate());
remaining = remaining.plus(start.getEffortDuration());
EffortDuration originalRemaining = remaining;
LocalDate startDate = start.getDate();
LocalDate current = startDate;
if ( !canBeFulfilled(start, originalRemaining) ) {
return roughApproximationDueToNotFullfilingCalendar(startDate, originalRemaining);
}
while (!remaining.isZero()) {
EffortDuration capacity = calendar.getCapacityOn(PartialDay.wholeDay(current));
result = IntraDayDate.create(current, remaining);
remaining = remaining.minus(min(capacity, remaining));
current = current.plusDays(1);
if ( Days.daysBetween(startDate, current).getDays() > MAX_DAYS_LOOKING_CAPACITY ) {
LOG.error("thereAreCapacityFor didn't detect that it didn't" +
" really have enough capacity to fulfill the required hours" +
" or this capacity is more than " +
MAX_DAYS_LOOKING_CAPACITY + " in the future");
return roughApproximationDueToNotFullfilingCalendar(startDate, originalRemaining);
}
}
return result;
}
private boolean canBeFulfilled(IntraDayDate start, EffortDuration originalRemaining) {
AvailabilityTimeLine availability = AvailabilityTimeLine.allValid();
availability.invalidUntil(start.getDate());
return calendar.thereAreCapacityFor(availability, ResourcesPerDay.amount(1), originalRemaining);
}
private IntraDayDate roughApproximationDueToNotFullfilingCalendar(
LocalDate startDate, EffortDuration originalRemaining) {
LOG.warn("Calendar " + calendar + " doesn't have enough capacity, " +
"using 8h per day to calculate end date for the task");
return IntraDayDate.create(
startDate.plusDays(originalRemaining.getHours() / 8),
EffortDuration.zero());
}
public IntraDayDate fromEndToStart(IntraDayDate newEnd) {
LocalDate resultDay = someWorkableDaysBefore(newEnd.getDate(), numberOfWorkableDays);
return minusDuration(
plusDuration(IntraDayDate.startOfDay(resultDay), newEnd.getEffortDuration()), remainderDuration);
}
private LocalDate someWorkableDaysBefore(LocalDate end, int workableDays) {
LocalDate result = end;
for (int i = 0; i < workableDays; result = result.minusDays(1)) {
if ( isWorkable(result.minusDays(1)) ) {
i++;
}
}
return result;
}
private IntraDayDate minusDuration(IntraDayDate date, EffortDuration decrement) {
IntraDayDate result = IntraDayDate.create(
date.getDate(),
date.getEffortDuration().minus(min(decrement, date.getEffortDuration())));
decrement = decrement.minus(min(date.getEffortDuration(), decrement));
LocalDate resultDay = date.getDate();
while (!decrement.isZero()) {
resultDay = resultDay.minusDays(1);
EffortDuration capacity = calendar.getCapacityOn(PartialDay.wholeDay(resultDay));
result = IntraDayDate.create(resultDay, capacity.minus(min(capacity, decrement)));
decrement = decrement.minus(min(capacity, decrement));
}
return result;
}
}
/**
* The allocation direction in which the allocation must be done
*/
public Direction getAllocationDirection() {
if ( lastAllocationDirection == null || hasConsolidations() ) {
return Direction.FORWARD;
}
return lastAllocationDirection;
}
public void reassignAllocationsWithNewResources(Scenario scenario, IResourcesSearcher searcher) {
reassign(scenario, getAllocationDirection(), new WithPotentiallyNewResources(searcher));
}
private void reassign(Scenario onScenario, Direction direction, WithPotentiallyNewResources strategy) {
try {
this.lastAllocationDirection = direction;
if ( isLimiting() ) {
return;
}
List<ModifiedAllocation> copied = ModifiedAllocation.copy(onScenario, getResourceAllocations());
List<ResourceAllocation<?>> toBeModified = ModifiedAllocation.modified(copied);
if ( toBeModified.isEmpty() ) {
return;
}
setCustomAssignedEffortForResource(copied);
doAllocation(strategy, direction, toBeModified);
updateDerived(copied);
List<ResourceAllocation<?>> newAllocations = emptyList(), removedAllocations = emptyList();
mergeAllocation(
onScenario,
getIntraDayStartDate(),
getIntraDayEndDate(),
workableDays,
calculatedValue,
newAllocations,
copied,
removedAllocations);
} catch (Exception e) {
LOG.error("reassignment for task: " + this + " couldn't be completed", e);
}
}
private void setCustomAssignedEffortForResource(List<ModifiedAllocation> modifiedAllocations) {
List<ResourceAllocation<?>> originals = ModifiedAllocation.originals(modifiedAllocations);
IAssignedEffortForResource discounting = AssignedEffortForResource.effortDiscounting(originals);
List<ResourceAllocation<?>> beingModified = ModifiedAllocation.modified(modifiedAllocations);
WithTheLoadOf allNewLoad = AssignedEffortForResource.withTheLoadOf(beingModified);
List<GenericResourceAllocation> generic =
ResourceAllocation.getOfType(GenericResourceAllocation.class, beingModified);
for (GenericResourceAllocation each : generic) {
each.setAssignedEffortForResource(
AssignedEffortForResource.sum(allNewLoad.withoutConsidering(each), discounting));
}
}
private void doAllocation(WithPotentiallyNewResources strategy, Direction direction,
List<ResourceAllocation<?>> toBeModified) {
ModificationsResult<ResourcesPerDayModification> modificationsResult =
strategy.getResourcesPerDayModified(toBeModified);
markAsUnsatisfied(modificationsResult.getNoLongerValid());
List<ResourcesPerDayModification> allocations = modificationsResult.getBeingModified();
if ( allocations.isEmpty() ) {
LOG.warn("all allocations for task " + this + " have no valid data that could be used");
return;
}
switch (calculatedValue) {
case NUMBER_OF_HOURS:
ResourceAllocation.allocating(allocations).allocateOnTaskLength();
break;
case END_DATE:
IntraDayDate date = ResourceAllocation.allocating(allocations)
.untilAllocating(direction, getTotalNonConsolidatedEffort());
if ( direction == Direction.FORWARD ) {
setIntraDayEndDate(date);
} else {
setIntraDayStartDate(date);
}
break;
case RESOURCES_PER_DAY:
ModificationsResult<EffortModification> hoursModificationResult = strategy.getHoursModified(toBeModified);
markAsUnsatisfied(hoursModificationResult.getNoLongerValid());
List<EffortModification> hoursModified = hoursModificationResult.getBeingModified();
if ( hoursModified.isEmpty() ) {
LOG.warn("all allocations for task " + this + " can't be used");
return;
}
ResourceAllocation.allocatingHours(hoursModified).allocateUntil(getIntraDayEndDate());
break;
default:
throw new RuntimeException("cant handle: " + calculatedValue);
}
AssignmentFunction.applyAssignmentFunctionsIfAny(toBeModified);
}
private void markAsUnsatisfied(Collection<? extends ResourceAllocation<?>> noLongerValid) {
for (ResourceAllocation<?> each : noLongerValid) {
each.markAsUnsatisfied();
}
}
private void updateDerived(List<ModifiedAllocation> allocations) {
for (ModifiedAllocation each : allocations) {
ResourceAllocation<?> original = each.getOriginal();
if ( !original.getDerivedAllocations().isEmpty() ) {
IWorkerFinder workersFinder = createFromExistentDerivedAllocationsFinder(original);
each.getModification().createDerived(workersFinder);
}
}
}
private IWorkerFinder createFromExistentDerivedAllocationsFinder(ResourceAllocation<?> original) {
Set<DerivedAllocation> derivedAllocations = original.getDerivedAllocations();
final Set<Worker> allWorkers = new HashSet<>();
for (DerivedAllocation each : derivedAllocations) {
allWorkers.addAll(Resource.workers(each.getResources()));
}
return new IWorkerFinder() {
@Override
public Collection<Worker> findWorkersMatching(Collection<? extends Criterion> requiredCriterions) {
if ( requiredCriterions.isEmpty() ) {
return new ArrayList<>();
}
Collection<Worker> result = new ArrayList<>();
for (Worker each : allWorkers) {
if ( each.satisfiesCriterions(requiredCriterions) ) {
result.add(each);
}
}
return result;
}
};
}
public List<AggregatedHoursGroup> getAggregatedByCriterions() {
return getTaskSource().getAggregatedByCriterions();
}
public void setSubcontractedTaskData(SubcontractedTaskData subcontractedTaskData) {
this.subcontractedTaskData = subcontractedTaskData;
}
@Valid
public SubcontractedTaskData getSubcontractedTaskData() {
return subcontractedTaskData;
}
public ExternalCompany getSubcontractedCompany() {
return subcontractedTaskData.getExternalCompany();
}
public void removeAllSatisfiedResourceAllocations() {
Set<ResourceAllocation<?>> resourceAllocations = getSatisfiedResourceAllocations();
for (ResourceAllocation<?> resourceAllocation : resourceAllocations) {
removeResourceAllocation(resourceAllocation);
}
}
public void removeAllResourceAllocations() {
for (Iterator<ResourceAllocation<?>> i = resourceAllocations.iterator(); i.hasNext();) {
ResourceAllocation<?> each = i.next();
removeResourceAllocation(each);
}
}
public boolean isSubcontracted() {
return (subcontractedTaskData != null);
}
public String getSubcontractionName() {
return subcontractedTaskData.getExternalCompany().getName();
}
public boolean isSubcontractedAndWasAlreadySent() {
return (subcontractedTaskData != null) &&
(!subcontractedTaskData.getState().equals(SubcontractState.PENDING_INITIAL_SEND));
}
public boolean hasSomeSatisfiedAllocation() {
return !getSatisfiedResourceAllocations().isEmpty();
}
@Override
protected boolean canBeResized() {
return calculatedValue != CalculatedValue.END_DATE || resourceAllocations.isEmpty();
}
@Override
public boolean canBeExplicitlyResized() {
return canBeResized() && !isSubcontracted() && !isManualAnyAllocation();
}
@Override
public boolean isMilestone() {
return false;
}
public void removeSubcontractCommunicationDate() {
if ( subcontractedTaskData != null ) {
subcontractedTaskData.setSubcontractCommunicationDate(null);
}
}
public boolean hasResourceAllocations() {
return !resourceAllocations.isEmpty();
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
public void setConsolidation(Consolidation consolidation) {
this.consolidation = consolidation;
}
@Valid
public Consolidation getConsolidation() {
return consolidation;
}
@Override
public boolean hasLimitedResourceAllocation() {
return !getLimitingResourceAllocations().isEmpty();
}
public boolean hasConsolidations() {
return ((consolidation != null) && (!consolidation.isEmpty()));
}
public IntraDayDate getFirstDayNotConsolidated() {
if ( consolidation != null ) {
LocalDate until = consolidation.getConsolidatedUntil();
if ( until != null ) {
return IntraDayDate.startOfDay(until.plusDays(1));
}
}
return getIntraDayStartDate();
}
public void updateAssignmentsConsolidatedValues() {
for (ResourceAllocation<?> each : getAllResourceAllocations()) {
each.updateAssignmentsConsolidatedValues();
}
}
public Integer getWorkableDays() {
return (workableDays == null) ? getWorkableDaysBetweenDates() : workableDays;
}
public Integer getDaysBetweenDates() {
Days daysBetween = Days.daysBetween(getStartAsLocalDate(), getIntraDayEndDate().asExclusiveEnd());
return daysBetween.getDays();
}
public Integer getSpecifiedWorkableDays() {
return workableDays;
}
private Integer getWorkableDaysBetweenDates() {
LocalDate end = getIntraDayEndDate().asExclusiveEnd();
return getWorkableDaysUntil(end);
}
public Integer getWorkableDaysUntil(LocalDate end) {
return getWorkableDaysFrom(getStartAsLocalDate(), end);
}
public Integer getWorkableDaysFrom(LocalDate startInclusive, LocalDate endExclusive) {
int result = 0;
for (LocalDate current = startInclusive; current.compareTo(endExclusive) < 0; current = current.plusDays(1)) {
if ( isWorkable(current) ) {
result++;
}
}
return result;
}
/* Older methods didn't consider until dates more recent than
* task end date
*/
public Integer getWorkableDaysFromLimitedByEndOfTheTask(LocalDate end) {
return getWorkableDaysFromLimitedByEndOfTheTask(getStartAsLocalDate(), end);
}
public Integer getWorkableDaysFromLimitedByEndOfTheTask(LocalDate startInclusive, LocalDate endExclusive) {
int result = 0;
if( endExclusive.compareTo(this.getEndAsLocalDate()) > 0 ) {
endExclusive = getIntraDayEndDate().asExclusiveEnd();
}
for (LocalDate current = startInclusive; current.compareTo(endExclusive) < 0; current = current.plusDays(1)) {
if ( isWorkable(current) ) {
result++;
}
}
return result;
}
private boolean isWorkable(LocalDate day) {
ICalendar calendar = getCalendar();
assert calendar != null;
return !calendar.getCapacityOn(PartialDay.wholeDay(day)).isZero();
}
public static void convertOnStartInFixedDate(Task task) {
TaskPositionConstraint taskConstraint = task.getPositionConstraint();
if ( taskConstraint.isValid(PositionConstraintType.START_IN_FIXED_DATE, task.getIntraDayStartDate()) ) {
taskConstraint.update(PositionConstraintType.START_IN_FIXED_DATE, task.getIntraDayStartDate());
}
}
public boolean isManualAnyAllocation() {
for (ResourceAllocation<?> each : resourceAllocations) {
if ( each.isManualAssignmentFunction() ) {
return true;
}
}
return false;
}
@Override
public boolean isTask() {
return true;
}
@Override
public EffortDuration getTheoreticalCompletedTimeUntilDate(Date date) {
return AggregateOfDayAssignments.createByDataRange(
this.getDayAssignments(FilterType.KEEP_ALL),
this.getStartDate(),
date)
.getTotalTime();
}
public TaskStatusEnum getTaskStatus() {
if ( this.isFinished() ) {
return TaskStatusEnum.FINISHED;
} else if ( this.isInProgress() ) {
return TaskStatusEnum.IN_PROGRESS;
} else if ( this.isReadyToStart() ) {
return TaskStatusEnum.READY_TO_START;
} else if ( this.isBlocked() ){
return TaskStatusEnum.BLOCKED;
} else {
throw new RuntimeException("Unknown task status. You've found a bug :)");
}
}
/* If the status of the task was needed in the past was because
* a TaskGroup needed to calculate children status, but only asked
* if this task was FINISHED or IN_PROGRESS. Thus, there is no need
* to cache other statutes because they only will be queried once.
*/
@Override
public boolean isFinished() {
if ( this.currentStatus != null ) {
return this.currentStatus == TaskStatusEnum.FINISHED;
} else {
boolean outcome = this.advancePercentageIsOne();
if ( outcome == true ) {
this.currentStatus = TaskStatusEnum.FINISHED;
}
return outcome;
}
}
@Override
public boolean isInProgress() {
if ( this.currentStatus != null ) {
return this.currentStatus == TaskStatusEnum.IN_PROGRESS;
} else {
boolean advanceBetweenZeroAndOne = this.advancePercentageIsGreaterThanZero() && !advancePercentageIsOne();
boolean outcome = advanceBetweenZeroAndOne || this.hasAttachedWorkReports();
if ( outcome == true ) {
this.currentStatus = TaskStatusEnum.IN_PROGRESS;
}
return outcome;
}
}
public boolean isReadyToStart() {
if ( !this.advancePercentageIsZero() || this.hasAttachedWorkReports() ) {
return false;
}
Set<Dependency> dependencies = getDependenciesWithThisDestinationAndAllParents();
for (Dependency dependency: dependencies) {
Type dependencyType = dependency.getType();
if ( dependencyType.equals(Type.END_START) ) {
if ( !dependency.getOrigin().isFinished() ) {
return false;
}
} else if ( dependencyType.equals(Type.START_START) ) {
if ( !dependency.getOrigin().isFinished() && !dependency.getOrigin().isInProgress() ) {
return false;
}
}
}
return true;
}
public boolean isBlocked() {
if ( !this.advancePercentageIsZero() || this.hasAttachedWorkReports() ) {
return false;
}
Set<Dependency> dependencies = getDependenciesWithThisDestinationAndAllParents();
for (Dependency dependency: dependencies) {
Type dependencyType = dependency.getType();
if ( dependencyType.equals(Type.END_START) ) {
if ( !dependency.getOrigin().isFinished() ) {
return true;
}
} else if ( dependencyType.equals(Type.START_START) ) {
if ( !dependency.getOrigin().isFinished() && !dependency.getOrigin().isInProgress() ) {
return true;
}
}
}
return false;
}
private boolean advancePercentageIsGreaterThanZero() {
return this.getAdvancePercentage().compareTo(BigDecimal.ZERO) > 0;
}
private boolean advancePercentageIsZero() {
return this.getAdvancePercentage().compareTo(BigDecimal.ZERO) == 0;
}
private boolean advancePercentageIsOne() {
return this.getAdvancePercentage().compareTo(BigDecimal.ONE) == 0;
}
private boolean hasAttachedWorkReports() {
SumChargedEffort sumChargedEffort = this.getOrderElement().getSumChargedEffort();
return sumChargedEffort != null && !sumChargedEffort.isZero();
}
@Override
public void acceptVisitor(TaskElementVisitor visitor) {
visitor.visit(this);
}
@Override
public void resetStatus() {
this.currentStatus = null;
}
@Override
public boolean isAnyTaskWithConstraint(PositionConstraintType type) {
return getPositionConstraint().getConstraintType().equals(type);
}
@Override
public void setParent(TaskGroup taskGroup) {
super.setParent(taskGroup);
}
@Override
public void setTaskSource(TaskSource taskSource) {
super.setTaskSource(taskSource);
}
}