/*
* 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.web.planner;
import static org.libreplan.web.I18nHelper._;
import static org.libreplan.web.common.Util.addCurrencySymbol;
import static org.zkoss.ganttz.data.constraint.ConstraintOnComparableValues.biggerOrEqualThan;
import static org.zkoss.ganttz.data.constraint.ConstraintOnComparableValues.equalTo;
import static org.zkoss.ganttz.data.constraint.ConstraintOnComparableValues.lessOrEqualThan;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashSet;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.math.Fraction;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.Days;
import org.joda.time.Duration;
import org.joda.time.LocalDate;
import org.joda.time.Seconds;
import org.libreplan.business.calendars.entities.BaseCalendar;
import org.libreplan.business.common.IAdHocTransactionService;
import org.libreplan.business.common.IOnTransaction;
import org.libreplan.business.common.daos.IConfigurationDAO;
import org.libreplan.business.common.entities.ProgressType;
import org.libreplan.business.externalcompanies.daos.IExternalCompanyDAO;
import org.libreplan.business.labels.entities.Label;
import org.libreplan.business.orders.daos.IOrderElementDAO;
import org.libreplan.business.orders.entities.Order;
import org.libreplan.business.orders.entities.OrderElement;
import org.libreplan.business.orders.entities.OrderStatusEnum;
import org.libreplan.business.orders.entities.SumChargedEffort;
import org.libreplan.business.orders.entities.SumExpenses;
import org.libreplan.business.planner.daos.IResourceAllocationDAO;
import org.libreplan.business.planner.daos.ITaskElementDAO;
import org.libreplan.business.planner.entities.Dependency;
import org.libreplan.business.planner.entities.Dependency.Type;
import org.libreplan.business.planner.entities.GenericResourceAllocation;
import org.libreplan.business.planner.entities.IMoneyCostCalculator;
import org.libreplan.business.planner.entities.ITaskPositionConstrained;
import org.libreplan.business.planner.entities.MoneyCostCalculator;
import org.libreplan.business.planner.entities.PositionConstraintType;
import org.libreplan.business.planner.entities.ResourceAllocation;
import org.libreplan.business.planner.entities.ResourceAllocation.Direction;
import org.libreplan.business.planner.entities.SpecificResourceAllocation;
import org.libreplan.business.planner.entities.Task;
import org.libreplan.business.planner.entities.TaskElement;
import org.libreplan.business.planner.entities.TaskElement.IDatesHandler;
import org.libreplan.business.planner.entities.TaskGroup;
import org.libreplan.business.planner.entities.TaskPositionConstraint;
import org.libreplan.business.resources.daos.ICriterionDAO;
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.scenarios.entities.Scenario;
import org.libreplan.business.workingday.EffortDuration;
import org.libreplan.business.workingday.IntraDayDate;
import org.libreplan.business.workingday.IntraDayDate.PartialDay;
import org.libreplan.web.planner.order.PlanningStateCreator.PlanningState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.zkoss.ganttz.IDatesMapper;
import org.zkoss.ganttz.ProjectStatusEnum;
import org.zkoss.ganttz.adapters.DomainDependency;
import org.zkoss.ganttz.adapters.IAdapterToTaskFundamentalProperties;
import org.zkoss.ganttz.data.DependencyType;
import org.zkoss.ganttz.data.GanttDate;
import org.zkoss.ganttz.data.GanttDate.Cases;
import org.zkoss.ganttz.data.GanttDate.CustomDate;
import org.zkoss.ganttz.data.GanttDate.LocalDateBased;
import org.zkoss.ganttz.data.ITaskFundamentalProperties;
import org.zkoss.ganttz.data.constraint.Constraint;
import org.zkoss.ganttz.util.ReentranceGuard;
import org.zkoss.ganttz.util.ReentranceGuard.IReentranceCases;
/**
* @author Óscar González Fernández <ogonzalez@igalia.com>
* @author Manuel Rego Casasnovas <rego@igalia.com>
*/
@Component
@Scope(BeanDefinition.SCOPE_SINGLETON)
public class TaskElementAdapter {
private static final Log LOG = LogFactory.getLog(TaskElementAdapter.class);
@Autowired
private IAdHocTransactionService transactionService;
@Autowired
private IOrderElementDAO orderElementDAO;
@Autowired
private ITaskElementDAO taskDAO;
@Autowired
private ICriterionDAO criterionDAO;
@Autowired
private IResourceAllocationDAO resourceAllocationDAO;
@Autowired
private IExternalCompanyDAO externalCompanyDAO;
@Autowired
private IResourcesSearcher searcher;
@Autowired
private IConfigurationDAO configurationDAO;
@Autowired
private IMoneyCostCalculator moneyCostCalculator;
private final ReentranceGuard reentranceGuard = new ReentranceGuard();
public static List<Constraint<GanttDate>> getStartConstraintsFor(TaskElement taskElement, LocalDate orderInitDate) {
if ( taskElement instanceof ITaskPositionConstrained ) {
ITaskPositionConstrained task = (ITaskPositionConstrained) taskElement;
TaskPositionConstraint startConstraint = task.getPositionConstraint();
final PositionConstraintType constraintType = startConstraint.getConstraintType();
switch (constraintType) {
case AS_SOON_AS_POSSIBLE:
return orderInitDate != null
? Collections.singletonList(biggerOrEqualThan(toGantt(orderInitDate)))
: Collections.emptyList();
case START_IN_FIXED_DATE:
return Collections.singletonList(equalTo(toGantt(startConstraint.getConstraintDate())));
case START_NOT_EARLIER_THAN:
return Collections.singletonList(biggerOrEqualThan(toGantt(startConstraint.getConstraintDate())));
default:
break;
}
}
return Collections.emptyList();
}
public static List<Constraint<GanttDate>> getEndConstraintsFor(TaskElement taskElement, LocalDate deadline) {
if ( taskElement instanceof ITaskPositionConstrained ) {
ITaskPositionConstrained task = (ITaskPositionConstrained) taskElement;
TaskPositionConstraint endConstraint = task.getPositionConstraint();
PositionConstraintType type = endConstraint.getConstraintType();
switch (type) {
case AS_LATE_AS_POSSIBLE:
if ( deadline != null ) {
return Collections.singletonList(lessOrEqualThan(toGantt(deadline)));
}
case FINISH_NOT_LATER_THAN:
return Collections.singletonList(lessOrEqualThan(toGantt(endConstraint.getConstraintDate())));
default:
break;
}
}
return Collections.emptyList();
}
public static GanttDate toGantt(IntraDayDate date) {
return toGantt(date, null);
}
public static GanttDate toGantt(IntraDayDate date, EffortDuration dayCapacity) {
EffortDuration newDayCapacity = dayCapacity;
if ( date == null ) {
return null;
}
if ( newDayCapacity == null ) {
// A sensible default
newDayCapacity = EffortDuration.hours(8);
}
return new GanttDateAdapter(date, newDayCapacity);
}
public static GanttDate toGantt(LocalDate date) {
return date == null ? null : GanttDate.createFrom(date);
}
public static IntraDayDate toIntraDay(GanttDate date) {
if ( date == null ) {
return null;
}
return date.byCases(new Cases<GanttDateAdapter, IntraDayDate>(GanttDateAdapter.class) {
@Override
public IntraDayDate on(LocalDateBased localDate) {
return IntraDayDate.startOfDay(localDate.getLocalDate());
}
@Override
protected IntraDayDate onCustom(GanttDateAdapter customType) {
return customType.date;
}
});
}
public IAdapterToTaskFundamentalProperties<TaskElement> createForCompany(Scenario currentScenario) {
Adapter result = new Adapter();
result.useScenario(currentScenario);
result.setPreventCalculateResourcesText(true);
return result;
}
public IAdapterToTaskFundamentalProperties<TaskElement> createForOrder(
Scenario currentScenario, Order order, PlanningState planningState) {
Adapter result = new Adapter(planningState);
result.useScenario(currentScenario);
result.setInitDate(asLocalDate(order.getInitDate()));
result.setDeadline(asLocalDate(order.getDeadline()));
return result;
}
private LocalDate asLocalDate(Date date) {
return date != null ? LocalDate.fromDateFields(date) : null;
}
static class GanttDateAdapter extends CustomDate {
private static final int DAY_MILLISECONDS = (int) Days.days(1).toStandardDuration().getMillis();
private final IntraDayDate date;
private final Duration workingDayDuration;
GanttDateAdapter(IntraDayDate date, EffortDuration capacityForDay) {
this.date = date;
this.workingDayDuration = toMilliseconds(capacityForDay);
}
@Override
protected int compareToCustom(CustomDate customType) {
if ( customType instanceof GanttDateAdapter ) {
GanttDateAdapter other = (GanttDateAdapter) customType;
return this.date.compareTo(other.date);
}
throw new RuntimeException("incompatible type: " + customType);
}
@Override
protected int compareToLocalDate(LocalDate localDate) {
return this.date.compareTo(localDate);
}
public IntraDayDate getDate() {
return date;
}
@Override
public Date toDayRoundedDate() {
return date.toDateTimeAtStartOfDay().toDate();
}
@Override
public LocalDate toLocalDate() {
return date.getDate();
}
@Override
public LocalDate asExclusiveEnd() {
return date.asExclusiveEnd();
}
@Override
protected boolean isEqualsToCustom(CustomDate customType) {
if ( customType instanceof GanttDateAdapter ) {
GanttDateAdapter other = (GanttDateAdapter) customType;
return this.date.equals(other.date);
}
return false;
}
@Override
public int hashCode() {
return date.hashCode();
}
@Override
public int toPixels(IDatesMapper datesMapper) {
int pixesUntilDate = datesMapper.toPixels(this.date.getDate());
EffortDuration effortDuration = date.getEffortDuration();
Duration durationInDay = calculateDurationInDayFor(effortDuration);
int pixelsInsideDay = datesMapper.toPixels(durationInDay);
return pixesUntilDate + pixelsInsideDay;
}
private Duration calculateDurationInDayFor(EffortDuration effortDuration) {
if ( workingDayDuration.getStandardSeconds() == 0 ) {
return Duration.ZERO;
}
Fraction fraction = fractionOfWorkingDayFor(effortDuration);
try {
return new Duration(fraction.multiplyBy(Fraction.getFraction(DAY_MILLISECONDS, 1)).intValue());
} catch (ArithmeticException e) {
// If fraction overflows use floating point arithmetic
return new Duration((int) (fraction.doubleValue() * DAY_MILLISECONDS));
}
}
@SuppressWarnings("unchecked")
private Fraction fractionOfWorkingDayFor(EffortDuration effortDuration) {
Duration durationInDay = toMilliseconds(effortDuration);
// Cast to int is safe because there are not enough seconds in day to overflow
Fraction fraction = Fraction.getFraction(
(int) durationInDay.getStandardSeconds(),
(int) workingDayDuration.getStandardSeconds());
return Collections.min(Arrays.asList(fraction, Fraction.ONE));
}
private static Duration toMilliseconds(EffortDuration duration) {
return Seconds.seconds(duration.getSeconds()).toStandardDuration();
}
}
/**
* Responsible of adaptation a {@link TaskElement} into a {@link ITaskFundamentalProperties}.
* <br />
* @author Óscar González Fernández <ogonzalez@igalia.com>
*/
public class Adapter implements IAdapterToTaskFundamentalProperties<TaskElement> {
private Scenario scenario;
private LocalDate initDate;
private LocalDate deadline;
private boolean preventCalculateResourcesText = false;
private final PlanningState planningState;
public Adapter() {
this(null);
}
public Adapter(PlanningState planningState) {
this.planningState = planningState;
}
private void useScenario(Scenario scenario) {
this.scenario = scenario;
}
private void setInitDate(LocalDate initDate) {
this.initDate = initDate;
}
private void setDeadline(LocalDate deadline) {
this.deadline = deadline;
}
public boolean isPreventCalculateResourcesText() {
return preventCalculateResourcesText;
}
public void setPreventCalculateResourcesText(
boolean preventCalculateResourcesText) {
this.preventCalculateResourcesText = preventCalculateResourcesText;
}
private class TaskElementWrapper implements ITaskFundamentalProperties {
private final TaskElement taskElement;
private final Scenario currentScenario;
private final IUpdatablePosition position = new IUpdatablePosition() {
@Override
public void setEndDate(GanttDate endDate) {
stepsBeforePossibleReallocation();
getDatesHandler(taskElement).moveEndTo(toIntraDay(endDate));
}
@Override
public void setBeginDate(final GanttDate beginDate) {
stepsBeforePossibleReallocation();
getDatesHandler(taskElement).moveTo(toIntraDay(beginDate));
}
@Override
public void resizeTo(final GanttDate endDate) {
stepsBeforePossibleReallocation();
updateTaskPositionConstraint(endDate);
getDatesHandler(taskElement).resizeTo(toIntraDay(endDate));
}
private void stepsBeforePossibleReallocation() {
taskDAO.reattach(taskElement);
}
@Override
public void moveTo(GanttDate newStart) {
if ( taskElement instanceof ITaskPositionConstrained ) {
ITaskPositionConstrained task = (ITaskPositionConstrained) taskElement;
GanttDate newEnd = inferEndFrom(newStart);
if ( task.getPositionConstraint().isConstraintAppliedToStart() ) {
setBeginDate(newStart);
} else {
setEndDate(newEnd);
}
task.explicityMoved(toIntraDay(newStart), toIntraDay(newEnd));
}
}
private void updateTaskPositionConstraint(GanttDate endDate) {
if ( taskElement instanceof ITaskPositionConstrained ) {
ITaskPositionConstrained task = (ITaskPositionConstrained) taskElement;
PositionConstraintType constraintType = task.getPositionConstraint().getConstraintType();
if ( constraintType.compareTo(PositionConstraintType.FINISH_NOT_LATER_THAN) == 0 ||
constraintType.compareTo(PositionConstraintType.AS_LATE_AS_POSSIBLE) == 0) {
task.explicityMoved(taskElement.getIntraDayStartDate(), toIntraDay(endDate));
}
}
}
private GanttDate inferEndFrom(GanttDate newStart) {
return taskElement instanceof Task
? toGantt(((Task) taskElement).calculateEndKeepingLength(toIntraDay(newStart)))
: newStart;
}
};
protected TaskElementWrapper(Scenario currentScenario, TaskElement taskElement) {
Validate.notNull(currentScenario);
this.currentScenario = currentScenario;
this.taskElement = taskElement;
}
@Override
public void setName(String name) {
taskElement.setName(name);
}
@Override
public void setNotes(String notes) {
taskElement.setNotes(notes);
}
@Override
public String getName() {
return taskElement.getName();
}
@Override
public String getCode() {
return taskElement.getCode();
}
@Override
public String getProjectCode() {
return taskElement.getProjectCode();
}
@Override
public String getNotes() {
return taskElement.getNotes();
}
@Override
public GanttDate getBeginDate() {
return toGantt(taskElement.getIntraDayStartDate());
}
private GanttDate toGantt(IntraDayDate date) {
BaseCalendar calendar = taskElement.getCalendar();
return calendar == null
? TaskElementAdapter.toGantt(date)
: TaskElementAdapter.toGantt(date, calendar.getCapacityOn(PartialDay.wholeDay(date.getDate())));
}
@Override
public void doPositionModifications(final IModifications modifications) {
reentranceGuard.entranceRequested(new IReentranceCases() {
@Override
public void ifNewEntrance() {
transactionService.runOnReadOnlyTransaction(asTransaction(modifications));
}
IOnTransaction<Void> asTransaction(final IModifications modifications) {
return () -> {
if ( planningState != null ) {
planningState.reassociateResourcesWithSession();
}
modifications.doIt(position);
return null;
};
}
@Override
public void ifAlreadyInside() {
modifications.doIt(position);
}
});
}
@Override
public GanttDate getEndDate() {
return toGantt(taskElement.getIntraDayEndDate());
}
IDatesHandler getDatesHandler(TaskElement taskElement) {
return taskElement.getDatesHandler(currentScenario, searcher);
}
@Override
public GanttDate getHoursAdvanceBarEndDate() {
return calculateLimitDateProportionalToTaskElementSize(getHoursAdvanceBarPercentage());
}
@Override
public BigDecimal getHoursAdvanceBarPercentage() {
OrderElement orderElement = taskElement.getOrderElement();
if ( orderElement == null ) {
return BigDecimal.ZERO;
}
boolean cond = orderElement.getSumChargedEffort() != null;
EffortDuration totalChargedEffort = cond ? orderElement.getSumChargedEffort().getTotalChargedEffort()
: EffortDuration.zero();
EffortDuration estimatedEffort = taskElement.getSumOfAssignedEffort();
if( estimatedEffort.isZero() ) {
estimatedEffort = EffortDuration.hours(orderElement.getWorkHours());
if( estimatedEffort.isZero() ) {
return BigDecimal.ZERO;
}
}
return BigDecimal
.valueOf(totalChargedEffort.divivedBy(estimatedEffort).doubleValue())
.setScale(2, RoundingMode.HALF_UP);
}
@Override
public GanttDate getMoneyCostBarEndDate() {
return calculateLimitDateProportionalToTaskElementSize(getMoneyCostBarPercentage());
}
private GanttDate calculateLimitDateProportionalToTaskElementSize(BigDecimal proportion) {
if ( proportion.compareTo(BigDecimal.ZERO) == 0 ) {
return getBeginDate();
}
IntraDayDate start = taskElement.getIntraDayStartDate();
IntraDayDate end = taskElement.getIntraDayEndDate();
EffortDuration effortBetween = start.effortUntil(end);
int seconds = new BigDecimal(effortBetween.getSeconds()).multiply(proportion).toBigInteger().intValue();
return TaskElementAdapter.toGantt(
start.addEffort(EffortDuration.seconds(seconds)),
EffortDuration.hours(8));
}
@Override
public BigDecimal getMoneyCostBarPercentage() {
return MoneyCostCalculator.getMoneyCostProportion(getMoneyCost(), getBudget());
}
private BigDecimal getBudget() {
return (taskElement == null) || (taskElement.getOrderElement() == null)
? BigDecimal.ZERO
: taskElement.getOrderElement().getBudget();
}
private BigDecimal getTotalCalculatedBudget() {
return (taskElement == null) || (taskElement.getOrderElement() == null)
? BigDecimal.ZERO
: transactionService.runOnReadOnlyTransaction(() -> taskElement.getOrderElement().getTotalBudget());
}
private BigDecimal getMoneyCost() {
return (taskElement == null) || (taskElement.getOrderElement() == null)
? BigDecimal.ZERO
: transactionService.runOnReadOnlyTransaction(() -> moneyCostCalculator.getTotalMoneyCost(taskElement.getOrderElement()));
}
private BigDecimal getHoursMoneyCost() {
return (taskElement == null) || (taskElement.getOrderElement() == null)
? BigDecimal.ZERO
: transactionService.runOnReadOnlyTransaction(() -> moneyCostCalculator.getHoursMoneyCost(taskElement.getOrderElement()));
}
private BigDecimal getExpensesMoneyCost() {
return (taskElement == null) || (taskElement.getOrderElement() == null)
? BigDecimal.ZERO
: transactionService.runOnReadOnlyTransaction(() -> moneyCostCalculator.getExpensesMoneyCost(taskElement.getOrderElement()));
}
@Override
public GanttDate getAdvanceBarEndDate(String progressType) {
return getAdvanceBarEndDate(ProgressType.asEnum(progressType));
}
private GanttDate getAdvanceBarEndDate(ProgressType progressType) {
BigDecimal advancePercentage = BigDecimal.ZERO;
if ( taskElement.getOrderElement() != null ) {
advancePercentage = taskElement.getAdvancePercentage(progressType);
}
return getAdvanceBarEndDate(advancePercentage);
}
@Override
public GanttDate getAdvanceBarEndDate() {
return getAdvanceBarEndDate(getAdvancePercentage());
}
private boolean isTaskRoot(TaskElement taskElement) {
return taskElement instanceof TaskGroup && taskElement.getParent() == null;
}
private ProgressType getProgressTypeFromConfiguration() {
return transactionService.runOnReadOnlyTransaction(() -> configurationDAO.getConfiguration().getProgressType());
}
private GanttDate getAdvanceBarEndDate(BigDecimal advancePercentage) {
return calculateLimitDateProportionalToTaskElementSize(advancePercentage);
}
@Override
public String getTooltipText() {
if ( taskElement.isMilestone() || taskElement.getOrderElement() == null ) {
return "";
}
return transactionService.runOnReadOnlyTransaction(() -> {
orderElementDAO.reattach(taskElement.getOrderElement());
return buildTooltipText();
});
}
@Override
public String getLabelsText() {
if ( taskElement.isMilestone() || taskElement.getOrderElement() == null ) {
return "";
}
return transactionService.runOnReadOnlyTransaction(() -> {
orderElementDAO.reattach(taskElement.getOrderElement());
return buildLabelsText();
});
}
@Override
public String getResourcesText() {
if ( isPreventCalculateResourcesText() || taskElement.getOrderElement() == null ) {
return "";
}
try {
return transactionService.runOnAnotherReadOnlyTransaction(() -> {
orderElementDAO.reattach(taskElement.getOrderElement());
if ( taskElement.isSubcontracted() ) {
externalCompanyDAO.reattach(taskElement.getSubcontractedCompany());
}
return buildResourcesText();
});
} catch (Exception e) {
LOG.error("error calculating resources text", e);
return "";
}
}
private Set<Label> getLabelsFromElementAndPredecesors(OrderElement order) {
if ( order != null ) {
if ( order.getParent() == null ) {
return order.getLabels();
} else {
HashSet<Label> labels = new HashSet<>(order.getLabels());
labels.addAll(getLabelsFromElementAndPredecesors(order.getParent()));
return labels;
}
}
return new HashSet<>();
}
private String buildLabelsText() {
List<String> result = new ArrayList<>();
if ( taskElement.getOrderElement() != null ) {
Set<Label> labels = getLabelsFromElementAndPredecesors(taskElement.getOrderElement());
for (Label label : labels) {
String representation = label.getName();
if ( !result.contains(representation) ) {
result.add(representation);
}
}
}
Collections.sort(result);
return StringUtils.join(result, ", ");
}
private String buildResourcesText() {
List<String> result = new ArrayList<>();
for (ResourceAllocation<?> each : taskElement.getSatisfiedResourceAllocations()) {
if ( each instanceof SpecificResourceAllocation ) {
for (Resource r : each.getAssociatedResources()) {
String representation = r.getName();
if ( !result.contains(representation) ) {
result.add(representation);
}
}
} else {
String representation = extractRepresentationForGeneric((GenericResourceAllocation) each);
if ( !result.contains(representation) ) {
result.add(representation);
}
}
}
if ( taskElement.isSubcontracted() ) {
result.add(taskElement.getSubcontractionName());
}
Collections.sort(result);
return StringUtils.join(result, "; ");
}
private String extractRepresentationForGeneric(GenericResourceAllocation generic) {
if ( !generic.isNewObject() ) {
resourceAllocationDAO.reattach(generic);
}
Set<Criterion> criterions = generic.getCriterions();
List<String> forCriterionRepresentations = new ArrayList<>();
if ( !criterions.isEmpty() ) {
for (Criterion c : criterions) {
criterionDAO.reattachUnmodifiedEntity(c);
forCriterionRepresentations.add(c.getName());
}
} else {
forCriterionRepresentations.add(_("All workers"));
}
return "[" + StringUtils.join(forCriterionRepresentations, ", ") + "]";
}
@Override
public String updateTooltipText() {
return buildTooltipText();
}
@Override
public String updateTooltipText(String progressType) {
return buildTooltipText(ProgressType.asEnum(progressType));
}
@Override
public BigDecimal getAdvancePercentage() {
if ( taskElement != null ) {
BigDecimal advancePercentage;
if ( isTaskRoot(taskElement) ) {
ProgressType progressType = getProgressTypeFromConfiguration();
advancePercentage = taskElement.getAdvancePercentage(progressType);
} else {
advancePercentage = taskElement.getAdvancePercentage();
}
return advancePercentage;
}
return new BigDecimal(0);
}
private String buildTooltipText() {
return buildTooltipText(asPercentage(getAdvancePercentage()));
}
private BigDecimal asPercentage(BigDecimal value) {
return value.multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.DOWN);
}
private String buildTooltipText(BigDecimal progressPercentage) {
StringBuilder result = new StringBuilder();
result
.append("<strong>")
.append(getName())
.append("</strong><br/>");
result
.append(_("Progress"))
.append(": ")
.append(progressPercentage)
.append("% , ");
result
.append(_("Hours invested"))
.append(": ")
.append(getHoursAdvanceBarPercentage().multiply(new BigDecimal(100)))
.append("% <br/>");
if ( taskElement.getOrderElement() instanceof Order ) {
result
.append(_("State"))
.append(": ")
.append(getOrderState());
} else {
String budget = addCurrencySymbol(getTotalCalculatedBudget());
String moneyCost = addCurrencySymbol(getMoneyCost());
String costHours = addCurrencySymbol(getHoursMoneyCost());
String costExpenses = addCurrencySymbol(getExpensesMoneyCost());
result
.append(_("Budget: {0}, Consumed: {1} ({2}%)",
budget, moneyCost, getMoneyCostBarPercentage().multiply(new BigDecimal(100))))
.append("<br/>");
result.append(_("Hours cost: {0}, Expenses cost: {1}", costHours, costExpenses));
}
String labels = buildLabelsText();
if ( !"".equals(labels) ) {
result
.append("<div class='tooltip-labels'>")
.append(_("Labels"))
.append(": ")
.append(labels)
.append("</div>");
}
return result.toString();
}
private String buildTooltipText(ProgressType progressType) {
return buildTooltipText(asPercentage(taskElement.getAdvancePercentage(progressType)));
}
private String getOrderState() {
String cssClass;
OrderStatusEnum state = taskElement.getOrderElement().getOrder().getState();
if ( Arrays.asList(
OrderStatusEnum.ACCEPTED,
OrderStatusEnum.OFFERED,
OrderStatusEnum.STARTED,
OrderStatusEnum.OUTSOURCED).contains(state) ) {
if (Objects.equals(taskElement.getAssignedStatus(), "assigned")) {
cssClass = "order-open-assigned";
} else {
cssClass = "order-open-unassigned";
}
} else {
cssClass = "order-closed";
}
return "<font class='" + cssClass + "'>" + _(state.toString()) + "</font>";
}
@Override
public List<Constraint<GanttDate>> getStartConstraints() {
return getStartConstraintsFor(this.taskElement, initDate);
}
@Override
public List<Constraint<GanttDate>> getEndConstraints() {
return getEndConstraintsFor(this.taskElement, deadline);
}
@Override
public List<Constraint<GanttDate>> getCurrentLengthConstraint() {
if ( taskElement instanceof Task ) {
Task task = (Task) taskElement;
if ( task.getAllocationDirection() == Direction.FORWARD ) {
return Collections.singletonList(biggerOrEqualThan(getEndDate()));
}
}
return Collections.emptyList();
}
@Override
public Date getDeadline() {
LocalDate deadline = taskElement.getDeadline();
return deadline == null ? null : deadline.toDateTimeAtStartOfDay().toDate();
}
@Override
public void setDeadline(Date date) {
if ( date != null ) {
taskElement.setDeadline(LocalDate.fromDateFields(date));
} else {
taskElement.setDeadline(null);
}
}
@Override
public GanttDate getConsolidatedline() {
if ( !taskElement.isLeaf() || !taskElement.hasConsolidations() ) {
return null;
}
LocalDate consolidatedLine = ((Task) taskElement).getFirstDayNotConsolidated().getDate();
return TaskElementAdapter.toGantt(consolidatedLine);
}
@Override
public boolean isSubcontracted() {
return taskElement.isSubcontracted();
}
@Override
public boolean isLimiting() {
return taskElement.isLimiting();
}
@Override
public boolean isLimitingAndHasDayAssignments() {
return taskElement.isLimitingAndHasDayAssignments();
}
@Override
public boolean hasConsolidations() {
return taskElement.hasConsolidations();
}
@Override
public boolean canBeExplicitlyResized() {
return taskElement.canBeExplicitlyResized();
}
@Override
public String getAssignedStatus() {
return taskElement.getAssignedStatus();
}
@Override
public boolean isFixed() {
return taskElement.isLimitingAndHasDayAssignments() ||
taskElement.hasConsolidations() ||
taskElement.isUpdatedFromTimesheets();
}
@Override
public boolean isManualAnyAllocation() {
return taskElement.isTask() && ((Task) taskElement).isManualAnyAllocation();
}
@Override
public boolean belongsClosedProject() {
return taskElement.belongsClosedProject();
}
@Override
public boolean isRoot() {
return taskElement.isRoot();
}
@Override
public boolean isUpdatedFromTimesheets() {
return taskElement.isUpdatedFromTimesheets();
}
@Override
public Date getFirstTimesheetDate() {
OrderElement orderElement = taskElement.getOrderElement();
return orderElement != null ? orderElement.getFirstTimesheetDate() : null;
}
@Override
public Date getLastTimesheetDate() {
OrderElement orderElement = taskElement.getOrderElement();
return orderElement != null ? orderElement.getLastTimesheetDate() : null;
}
@Override
public ProjectStatusEnum getProjectHoursStatus() {
if ( taskElement.isTask() ) {
return getProjectHourStatus(taskElement.getOrderElement());
}
List<TaskElement> taskElements = taskElement.getAllChildren();
ProjectStatusEnum status = ProjectStatusEnum.AS_PLANNED;
ProjectStatusEnum highestStatus = null;
for (TaskElement taskElement : taskElements) {
if ( !taskElement.isTask() ) {
continue;
}
status = getProjectHourStatus(taskElement.getOrderElement());
if ( status == ProjectStatusEnum.MARGIN_EXCEEDED ) {
highestStatus = ProjectStatusEnum.MARGIN_EXCEEDED;
break;
}
if ( status == ProjectStatusEnum.WITHIN_MARGIN ) {
highestStatus = ProjectStatusEnum.WITHIN_MARGIN;
}
}
if ( highestStatus != null ) {
status = highestStatus;
}
return status;
}
/**
* Returns {@link ProjectStatusEnum} for the specified <code>orderElement</code>.
*
* @param orderElement
*/
private ProjectStatusEnum getProjectHourStatus(OrderElement orderElement) {
EffortDuration sumChargedEffort = getSumChargedEffort(orderElement);
EffortDuration estimatedEffort = getEstimatedEffort(orderElement);
if ( sumChargedEffort.isZero() || sumChargedEffort.compareTo(estimatedEffort) <= 0 ) {
return ProjectStatusEnum.AS_PLANNED;
}
EffortDuration withMarginEstimatedHours = orderElement.getWithMarginCalculatedHours();
return estimatedEffort.compareTo(sumChargedEffort) < 0 && sumChargedEffort.compareTo(withMarginEstimatedHours) <= 0
? ProjectStatusEnum.WITHIN_MARGIN
: ProjectStatusEnum.MARGIN_EXCEEDED;
}
/**
* Returns sum charged effort for the specified <code>orderElement</code>.
*
* @param orderElement
*/
private EffortDuration getSumChargedEffort(OrderElement orderElement) {
SumChargedEffort sumChargedEffort = orderElement.getSumChargedEffort();
return (sumChargedEffort != null) ? sumChargedEffort.getTotalChargedEffort() : EffortDuration.zero();
}
/**
* Returns the estimated effort for the specified <code>orderElement</code>.
*
* @param orderElement
*/
private EffortDuration getEstimatedEffort(OrderElement orderElement) {
return EffortDuration.fromHoursAsBigDecimal(new BigDecimal(orderElement.getWorkHours()).setScale(2));
}
@Override
public ProjectStatusEnum getProjectBudgetStatus() {
if ( taskElement.isTask() ) {
return getProjectBudgetStatus(taskElement.getOrderElement());
}
List<TaskElement> taskElements = taskElement.getAllChildren();
ProjectStatusEnum status = ProjectStatusEnum.AS_PLANNED;
ProjectStatusEnum highestStatus = null;
for (TaskElement taskElement : taskElements) {
if ( !taskElement.isTask() ) {
continue;
}
status = getProjectBudgetStatus(taskElement.getOrderElement());
if ( status == ProjectStatusEnum.MARGIN_EXCEEDED ) {
highestStatus = ProjectStatusEnum.MARGIN_EXCEEDED;
break;
}
if ( status == ProjectStatusEnum.WITHIN_MARGIN ) {
highestStatus = ProjectStatusEnum.WITHIN_MARGIN;
}
}
if ( highestStatus != null ) {
status = highestStatus;
}
return status;
}
/**
* Returns {@link ProjectStatusEnum} for the specified <code>orderElement</code>.
*
* @param orderElement
*/
private ProjectStatusEnum getProjectBudgetStatus(OrderElement orderElement) {
BigDecimal budget = orderElement.getBudget();
BigDecimal totalExpense = getTotalExpense(orderElement);
BigDecimal withMarginCalculatedBudget = orderElement.getWithMarginCalculatedBudget();
if ( totalExpense.compareTo(budget) <= 0 ) {
return ProjectStatusEnum.AS_PLANNED;
}
return budget.compareTo(totalExpense) < 0 && totalExpense.compareTo(withMarginCalculatedBudget) <= 0
? ProjectStatusEnum.WITHIN_MARGIN
: ProjectStatusEnum.MARGIN_EXCEEDED;
}
/**
* Returns total expense for the specified <code>orderElement</code>.
*
* @param orderElement
*/
public BigDecimal getTotalExpense(OrderElement orderElement) {
BigDecimal total = BigDecimal.ZERO;
SumExpenses sumExpenses = orderElement.getSumExpenses();
if ( sumExpenses != null ) {
BigDecimal directExpenes = sumExpenses.getTotalDirectExpenses();
BigDecimal indirectExpense = sumExpenses.getTotalIndirectExpenses();
if ( directExpenes != null ) {
total = total.add(directExpenes);
}
if ( indirectExpense != null ) {
total = total.add(indirectExpense);
}
}
return total;
}
@Override
public String getTooltipTextForProjectHoursStatus() {
return taskElement.isTask() ? buildHoursTooltipText(taskElement.getOrderElement()) : null;
}
@Override
public String getTooltipTextForProjectBudgetStatus() {
return taskElement.isTask() ? buildBudgetTooltipText(taskElement.getOrderElement()) : null;
}
/**
* Builds hours tooltipText for the specified <code>orderElement</code>.
*
* @param orderElement
*/
private String buildHoursTooltipText(OrderElement orderElement) {
StringBuilder result = new StringBuilder();
boolean condition = orderElement.getOrder().getHoursMargin() != null;
Integer margin = condition ? orderElement.getOrder().getHoursMargin() : 0;
result.append(_("Hours-status")).append("\n");
result.append(_("Project margin: {0}% ({1} hours)={2} hours",
margin, orderElement.getWorkHours(), orderElement.getWithMarginCalculatedHours()));
String totalEffortHours = orderElement.getEffortAsString();
result.append(_(". Already registered: {0} hours", totalEffortHours));
return result.toString();
}
private String buildBudgetTooltipText(OrderElement orderElement) {
StringBuilder result = new StringBuilder();
boolean condition = orderElement.getOrder().getBudgetMargin() != null;
Integer margin = condition ? orderElement.getOrder().getBudgetMargin() : 0;
result.append(_("Budget-status")).append("\n");
result.append(_("Project margin: {0}% ({1})={2}",
margin,
addCurrencySymbol(orderElement.getBudget()),
addCurrencySymbol(orderElement.getWithMarginCalculatedBudget())));
BigDecimal totalExpense = getTotalExpense(orderElement);
result.append(_(". Already spent: {0}", addCurrencySymbol(totalExpense)));
return result.toString();
}
}
@Override
public ITaskFundamentalProperties adapt(final TaskElement taskElement) {
return new TaskElementWrapper(scenario, taskElement);
}
@Override
public List<DomainDependency<TaskElement>> getIncomingDependencies(TaskElement taskElement) {
return toDomainDependencies(taskElement.getDependenciesWithThisDestination());
}
@Override
public List<DomainDependency<TaskElement>> getOutcomingDependencies(TaskElement taskElement) {
return toDomainDependencies(taskElement.getDependenciesWithThisOrigin());
}
private List<DomainDependency<TaskElement>> toDomainDependencies(Collection<? extends Dependency> dependencies) {
List<DomainDependency<TaskElement>> result = new ArrayList<>();
for (Dependency dependency : dependencies) {
result.add(DomainDependency.createDependency(
dependency.getOrigin(),
dependency.getDestination(),
toGanntType(dependency.getType())));
}
return result;
}
private DependencyType toGanntType(Type type) {
switch (type) {
case END_START:
return DependencyType.END_START;
case START_END:
return DependencyType.START_END;
case START_START:
return DependencyType.START_START;
case END_END:
return DependencyType.END_END;
default:
throw new RuntimeException(_("{0} not supported yet", type));
}
}
private Type toDomainType(DependencyType type) {
switch (type) {
case END_START:
return Type.END_START;
case START_END:
return Type.START_END;
case START_START:
return Type.START_START;
case END_END:
return Type.END_END;
default:
throw new RuntimeException(_("{0} not supported yet", type));
}
}
@Override
public void addDependency(DomainDependency<TaskElement> dependency) {
TaskElement source = dependency.getSource();
TaskElement destination = dependency.getDestination();
Type domainType = toDomainType(dependency.getType());
Dependency.create(source, destination, domainType);
}
@Override
public boolean canAddDependency(DomainDependency<TaskElement> dependency) {
return true;
}
@Override
public void removeDependency(DomainDependency<TaskElement> dependency) {
TaskElement source = dependency.getSource();
Type type = toDomainType(dependency.getType());
source.removeDependencyWithDestination(dependency.getDestination(), type);
}
@Override
public void doRemovalOf(TaskElement taskElement) {
taskElement.detach();
TaskGroup parent = taskElement.getParent();
if ( parent != null ) {
parent.remove(taskElement);
}
}
}
}