From 94aa02fed1c826eefd8ad28e4f12fa318808ccb5 Mon Sep 17 00:00:00 2001 From: Diego Pino Date: Thu, 3 May 2012 13:21:05 +0200 Subject: [PATCH] Implement Dashboard 'Cost Status' table * Table contains several 'Earned Value' measures related with cost FEA: ItEr76S15OrganizingPerProjectDashboard --- .../web/dashboard/CostStatusController.java | 123 +++++++++++ .../web/dashboard/CostStatusModel.java | 202 ++++++++++++++++++ .../web/dashboard/DashboardController.java | 13 ++ .../web/dashboard/ICostStatusModel.java | 58 +++++ .../src/main/webapp/dashboard/_costStatus.zul | 53 +++++ .../webapp/dashboard/_dashboardfororder.zul | 11 + 6 files changed, 460 insertions(+) create mode 100644 libreplan-webapp/src/main/java/org/libreplan/web/dashboard/CostStatusController.java create mode 100644 libreplan-webapp/src/main/java/org/libreplan/web/dashboard/CostStatusModel.java create mode 100644 libreplan-webapp/src/main/java/org/libreplan/web/dashboard/ICostStatusModel.java create mode 100644 libreplan-webapp/src/main/webapp/dashboard/_costStatus.zul diff --git a/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/CostStatusController.java b/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/CostStatusController.java new file mode 100644 index 000000000..47097524a --- /dev/null +++ b/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/CostStatusController.java @@ -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 . + */ + +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 + * + * 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())); + } + +} \ No newline at end of file diff --git a/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/CostStatusModel.java b/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/CostStatusModel.java new file mode 100644 index 000000000..1198c5443 --- /dev/null +++ b/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/CostStatusModel.java @@ -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 . + */ + +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 + * + * 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 actualCost = calculateActualCostWorkPerformed(); + BigDecimal result = actualCost.get(date); + return (result != null) ? result : BigDecimal.ZERO; + } + + private SortedMap calculateActualCostWorkPerformed() { + SortedMap result = new TreeMap(); + for (TaskElement taskElement : getAllTaskElements(order)) { + if (taskElement instanceof Task) { + addCost(result, getWorkReportCost((Task) taskElement)); + } + } + return accumulateResult(result); + } + + private SortedMap accumulateResult( + SortedMap map) { + SortedMap result = new TreeMap(); + 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 currentCost, + SortedMap 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 getAllTaskElements(Order order) { + List result = order.getAllChildrenAssociatedTaskElements(); + result.add(order.getAssociatedTaskElement()); + return result; + } + + private SortedMap getWorkReportCost(Task taskElement) { + return hoursCostCalculator.getWorkReportCost(taskElement); + } + + @Override + @Transactional(readOnly = true) + public BigDecimal getBudgetAtCompletion() { + SortedMap budgedtedCost = calculateBudgetedCostWorkScheduled(); + LocalDate lastKey = budgedtedCost.lastKey(); + return (lastKey) != null ? budgedtedCost.get(lastKey) : BigDecimal.ZERO; + } + + private SortedMap calculateBudgetedCostWorkScheduled() { + SortedMap result = new TreeMap(); + for (TaskElement taskElement : getAllTaskElements(order)) { + if (taskElement instanceof Task) { + addCost(result, getEstimatedCost((Task) taskElement)); + } + } + return accumulateResult(result); + } + + private SortedMap getEstimatedCost(Task task) { + return hoursCostCalculator.getEstimatedCost(task); + } + + @Override + @Transactional(readOnly = true) + public BigDecimal getBudgetedCostWorkPerformedAt(LocalDate date) { + SortedMap budgetedCost = calculateBudgetedCostWorkPerformed(); + BigDecimal result = budgetedCost.get(date); + return (result != null) ? result : BigDecimal.ZERO; + } + + private SortedMap calculateBudgetedCostWorkPerformed() { + SortedMap estimatedCost = new TreeMap(); + for (TaskElement taskElement : getAllTaskElements(order)) { + if (taskElement instanceof Task) { + addCost(estimatedCost, getAdvanceCost((Task) taskElement)); + } + } + return accumulateResult(estimatedCost); + } + + private SortedMap 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; + } + +} \ No newline at end of file diff --git a/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/DashboardController.java b/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/DashboardController.java index 5d2c5a014..0702dbacc 100644 --- a/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/DashboardController.java +++ b/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/DashboardController.java @@ -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 diff --git a/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/ICostStatusModel.java b/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/ICostStatusModel.java new file mode 100644 index 000000000..ae75c0891 --- /dev/null +++ b/libreplan-webapp/src/main/java/org/libreplan/web/dashboard/ICostStatusModel.java @@ -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 . + */ + +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 + * + */ +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); + +} \ No newline at end of file diff --git a/libreplan-webapp/src/main/webapp/dashboard/_costStatus.zul b/libreplan-webapp/src/main/webapp/dashboard/_costStatus.zul new file mode 100644 index 000000000..780e00fe7 --- /dev/null +++ b/libreplan-webapp/src/main/webapp/dashboard/_costStatus.zul @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libreplan-webapp/src/main/webapp/dashboard/_dashboardfororder.zul b/libreplan-webapp/src/main/webapp/dashboard/_dashboardfororder.zul index 54a57f812..61cefce43 100644 --- a/libreplan-webapp/src/main/webapp/dashboard/_dashboardfororder.zul +++ b/libreplan-webapp/src/main/webapp/dashboard/_dashboardfororder.zul @@ -17,6 +17,8 @@ along with this program. If not, see . --> + + + + + + + + + + +