Implement Dashboard 'Cost Status' table

* Table contains several 'Earned Value' measures related with cost

FEA: ItEr76S15OrganizingPerProjectDashboard
This commit is contained in:
Diego Pino 2012-05-03 13:21:05 +02:00
parent 398355295f
commit 94aa02fed1
6 changed files with 460 additions and 0 deletions

View file

@ -0,0 +1,123 @@
/*
* This file is part of LibrePlan
*
* 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.dashboard;
import static org.libreplan.web.I18nHelper._;
import java.math.BigDecimal;
import java.util.Date;
import org.joda.time.LocalDate;
import org.libreplan.business.orders.entities.Order;
import org.libreplan.web.common.Util;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.zkoss.zk.ui.util.GenericForwardComposer;
import org.zkoss.zul.Label;
/**
* @author Diego Pino García <dpino@igalia.com>
*
* Contains operations for calculations in the CostStatus table in the
* Dashboard view
*/
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class CostStatusController extends GenericForwardComposer {
private ICostStatusModel costStatusModel;
// Cost Variance
public Label lblCV;
// Cost Performance Index
public Label lblCPI;
// Budget at Completion
public Label lblBAC;
// Estimate at Completion
public Label lblEAC;
// Variance at Completion
public Label lblVAC;
@Override
public void doAfterCompose(org.zkoss.zk.ui.Component comp) throws Exception {
super.doAfterCompose(comp);
self.setAttribute("controller", this);
Util.createBindingsFor(self);
}
public void setOrder(Order order) {
costStatusModel.setCurrentOrder(order);
}
public void render() {
LocalDate today = LocalDate.fromDateFields(new Date());
BigDecimal budgetedCost = costStatusModel
.getBudgetedCostWorkPerformedAt(today);
BigDecimal actualCost = costStatusModel
.getActualCostWorkPerformedAt(today);
BigDecimal costVariance = costStatusModel.getCostVariance(budgetedCost,
actualCost);
setCostVariance(costVariance);
BigDecimal costPerformanceIndex = costStatusModel
.getCostPerformanceIndex(budgetedCost, actualCost);
setCostPerformanceIndex(costPerformanceIndex);
BigDecimal budgetAtCompletion = costStatusModel.getBudgetAtCompletion();
setBudgetAtCompletion(budgetAtCompletion);
BigDecimal estimateAtCompletion = costStatusModel
.getEstimateAtCompletion(budgetAtCompletion,
costPerformanceIndex);
setEstimateAtCompletion(estimateAtCompletion);
BigDecimal varianceAtCompletion = costStatusModel
.getVarianceAtCompletion(budgetAtCompletion,
estimateAtCompletion);
setVarianceAtCompletion(varianceAtCompletion);
}
private void setEstimateAtCompletion(BigDecimal value) {
lblEAC.setValue(String.format("%.2f %%", value.doubleValue()));
}
private void setCostPerformanceIndex(BigDecimal value) {
lblCPI.setValue(String.format("%.2f %%", value.doubleValue()));
}
private void setBudgetAtCompletion(BigDecimal value) {
lblBAC.setValue(String.format(_("%s h"), value.toString()));
}
private void setCostVariance(BigDecimal value) {
lblCV.setValue(String.format(_("%s h"), value.toString()));
}
private void setVarianceAtCompletion(BigDecimal value) {
lblVAC.setValue(String.format(_("%s h"), value.toString()));
}
}

View file

@ -0,0 +1,202 @@
/*
* This file is part of LibrePlan
*
* 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.dashboard;
import java.math.BigDecimal;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import org.joda.time.LocalDate;
import org.libreplan.business.orders.entities.Order;
import org.libreplan.business.planner.entities.ICostCalculator;
import org.libreplan.business.planner.entities.Task;
import org.libreplan.business.planner.entities.TaskElement;
import org.libreplan.web.planner.order.OrderPlanningModel;
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.springframework.transaction.annotation.Transactional;
/**
* @author Diego Pino García <ltilve@igalia.com>
*
* Model for UI operations related to CostStatus in Dashboard view
*
* FIXME: This Model contains several operations for calculating 'Earned
* Value' measures related with cost. The code for calculating the basic
* measures: BCWP, ACWP and BCWS is copied from
* {@link OrderPlanningModel}. At this moment this code cannot be reused
* as it's coupled with the logic for displaying the 'Earned Value'
* chart. We may consider to refactor this code in the future.
*/
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class CostStatusModel implements ICostStatusModel {
@Autowired
private ICostCalculator hoursCostCalculator;
private Order order;
public CostStatusModel() {
}
@Override
@Transactional(readOnly = true)
public BigDecimal getActualCostWorkPerformedAt(LocalDate date) {
SortedMap<LocalDate, BigDecimal> actualCost = calculateActualCostWorkPerformed();
BigDecimal result = actualCost.get(date);
return (result != null) ? result : BigDecimal.ZERO;
}
private SortedMap<LocalDate, BigDecimal> calculateActualCostWorkPerformed() {
SortedMap<LocalDate, BigDecimal> result = new TreeMap<LocalDate, BigDecimal>();
for (TaskElement taskElement : getAllTaskElements(order)) {
if (taskElement instanceof Task) {
addCost(result, getWorkReportCost((Task) taskElement));
}
}
return accumulateResult(result);
}
private SortedMap<LocalDate, BigDecimal> accumulateResult(
SortedMap<LocalDate, BigDecimal> map) {
SortedMap<LocalDate, BigDecimal> result = new TreeMap<LocalDate, BigDecimal>();
if (map.isEmpty()) {
return result;
}
BigDecimal accumulatedResult = BigDecimal.ZERO;
for (LocalDate day : map.keySet()) {
BigDecimal value = map.get(day);
accumulatedResult = accumulatedResult.add(value);
result.put(day, accumulatedResult);
}
return result;
}
private void addCost(SortedMap<LocalDate, BigDecimal> currentCost,
SortedMap<LocalDate, BigDecimal> additionalCost) {
for (LocalDate day : additionalCost.keySet()) {
if (!currentCost.containsKey(day)) {
currentCost.put(day, BigDecimal.ZERO);
}
currentCost.put(day,
currentCost.get(day).add(additionalCost.get(day)));
}
}
private List<TaskElement> getAllTaskElements(Order order) {
List<TaskElement> result = order.getAllChildrenAssociatedTaskElements();
result.add(order.getAssociatedTaskElement());
return result;
}
private SortedMap<LocalDate, BigDecimal> getWorkReportCost(Task taskElement) {
return hoursCostCalculator.getWorkReportCost(taskElement);
}
@Override
@Transactional(readOnly = true)
public BigDecimal getBudgetAtCompletion() {
SortedMap<LocalDate, BigDecimal> budgedtedCost = calculateBudgetedCostWorkScheduled();
LocalDate lastKey = budgedtedCost.lastKey();
return (lastKey) != null ? budgedtedCost.get(lastKey) : BigDecimal.ZERO;
}
private SortedMap<LocalDate, BigDecimal> calculateBudgetedCostWorkScheduled() {
SortedMap<LocalDate, BigDecimal> result = new TreeMap<LocalDate, BigDecimal>();
for (TaskElement taskElement : getAllTaskElements(order)) {
if (taskElement instanceof Task) {
addCost(result, getEstimatedCost((Task) taskElement));
}
}
return accumulateResult(result);
}
private SortedMap<LocalDate, BigDecimal> getEstimatedCost(Task task) {
return hoursCostCalculator.getEstimatedCost(task);
}
@Override
@Transactional(readOnly = true)
public BigDecimal getBudgetedCostWorkPerformedAt(LocalDate date) {
SortedMap<LocalDate, BigDecimal> budgetedCost = calculateBudgetedCostWorkPerformed();
BigDecimal result = budgetedCost.get(date);
return (result != null) ? result : BigDecimal.ZERO;
}
private SortedMap<LocalDate, BigDecimal> calculateBudgetedCostWorkPerformed() {
SortedMap<LocalDate, BigDecimal> estimatedCost = new TreeMap<LocalDate, BigDecimal>();
for (TaskElement taskElement : getAllTaskElements(order)) {
if (taskElement instanceof Task) {
addCost(estimatedCost, getAdvanceCost((Task) taskElement));
}
}
return accumulateResult(estimatedCost);
}
private SortedMap<LocalDate, BigDecimal> getAdvanceCost(Task task) {
return hoursCostCalculator.getAdvanceCost(task);
}
@Override
public BigDecimal getCostPerformanceIndex(BigDecimal budgetedCost,
BigDecimal actualCost) {
if (BigDecimal.ZERO.compareTo(actualCost) == 0) {
return BigDecimal.ZERO;
}
return asPercentage(budgetedCost.divide(actualCost));
}
private BigDecimal asPercentage(BigDecimal value) {
return value.multiply(BigDecimal.valueOf(100)).setScale(2);
}
@Override
public BigDecimal getCostVariance(BigDecimal budgetedCost,
BigDecimal actualCost) {
return budgetedCost.subtract(actualCost);
}
@Override
public BigDecimal getEstimateAtCompletion(BigDecimal budgetAtCompletion,
BigDecimal costPerformanceIndex) {
if (BigDecimal.ZERO.compareTo(costPerformanceIndex) == 0) {
return BigDecimal.ZERO;
}
return asPercentage(budgetAtCompletion.divide(costPerformanceIndex));
}
@Override
public BigDecimal getVarianceAtCompletion(BigDecimal budgetAtCompletion,
BigDecimal estimateAtCompletion) {
return budgetAtCompletion.subtract(estimateAtCompletion);
}
@Override
public void setCurrentOrder(Order order) {
this.order = order;
}
}

View file

@ -67,6 +67,8 @@ public class DashboardController extends GenericForwardComposer {
private Grid gridTasksSummary;
private Grid gridMarginWithDeadline;
private org.zkoss.zk.ui.Component costStatus;
private Div projectDashboardChartsDiv;
private Div projectDashboardNoTasksWarningDiv;
@ -96,9 +98,20 @@ public class DashboardController extends GenericForwardComposer {
renderDeadlineViolation();
renderMarginWithDeadline();
renderEstimationAccuracy();
renderCostStatus(order);
}
}
private void renderCostStatus(Order order) {
CostStatusController costStatusController = getCostStatusController();
costStatusController.setOrder(order);
costStatusController.render();
}
private CostStatusController getCostStatusController() {
return (CostStatusController) costStatus.getAttribute("controller");
}
private void renderMarginWithDeadline() {
marginWithDeadline(dashboardModel.getMarginWithDeadLine());
absoluteMarginWithDeadline(dashboardModel

View file

@ -0,0 +1,58 @@
/*
* This file is part of LibrePlan
*
* 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.dashboard;
import java.math.BigDecimal;
import org.joda.time.LocalDate;
import org.libreplan.business.orders.entities.Order;
/**
*
* @author Diego Pino García <dpino@igalia.com>
*
*/
interface ICostStatusModel {
BigDecimal getActualCostWorkPerformedAt(LocalDate day);
// Budget at Completion (BAC)
BigDecimal getBudgetAtCompletion();
BigDecimal getBudgetedCostWorkPerformedAt(LocalDate day);
// Cost Performance Index (CPI)
BigDecimal getCostPerformanceIndex(BigDecimal budgetedCost,
BigDecimal actualCost);
// Cost Variance (CV)
BigDecimal getCostVariance(BigDecimal budgetedCost, BigDecimal actualCost);
// Estimate at Completion (EAC)
BigDecimal getEstimateAtCompletion(BigDecimal budgetAtCompletion,
BigDecimal costPerformanceIndex);
// Variance at Completion (VAC)
BigDecimal getVarianceAtCompletion(BigDecimal budgetAtCompletion,
BigDecimal estimateAtCompletion);
void setCurrentOrder(Order order);
}

View file

@ -0,0 +1,53 @@
<!--
This file is part of LibrePlan
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/>.
-->
<!-- Cost status -->
<grid id="${arg.id}"
apply="org.libreplan.web.dashboard.CostStatusController"
width="400px">
<auxhead>
<auxheader label="${i18n:_('Cost status')}" colspan="2"/>
</auxhead>
<columns sizable="false">
<column width="200px"/>
<column />
</columns>
<rows>
<row>
<label value="${i18n:_('CV (Cost Variance)')}:"/>
<label id="lblCV" />
</row>
<row>
<label value="${i18n:_('EAC (Estimate At Completion)')}:"/>
<label id="lblEAC" />
</row>
<row>
<label value="${i18n:_('BAC (Budget At Completion)')}:"/>
<label id="lblBAC" />
</row>
<row>
<label value="${i18n:_('VAC (Variance At Completion)')}:"/>
<label id="lblVAC" />
</row>
<row>
<label value="${i18n:_('CPI (Cost Performance Index)')}:"/>
<label id="lblCPI" />
</row>
</rows>
</grid>

View file

@ -17,6 +17,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<?component name="costStatus" inline="true" macroURI="_costStatus.zul"?>
<zk xmlns:n="http://www.zkoss.org/2005/zk/native">
<zscript>
<![CDATA[
@ -114,6 +116,15 @@
</hbox>
</groupbox>
<!-- Cost -->
<groupbox closable="false">
<caption label="${i18n:_('Cost')}" />
<hbox>
<!-- Cost status -->
<costStatus id="costStatus" />
</hbox>
</groupbox>
</div>
<div id="projectDashboardNoTasksWarningDiv" visible="false">