/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
// www.projectforge.org
//
// Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de)
//
// ProjectForge is dual-licensed.
//
// This community edition is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 3 of the License.
//
// This community edition 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 General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////
package org.projectforge.fibu;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.Validate;
import org.projectforge.calendar.DayHolder;
import org.projectforge.calendar.MonthHolder;
import org.projectforge.calendar.WeekHolder;
import org.projectforge.common.DateHolder;
import org.projectforge.common.NumberHelper;
import org.projectforge.common.StringHelper;
import org.projectforge.fibu.kost.Kost2DO;
import org.projectforge.task.TaskDO;
import org.projectforge.timesheet.TimesheetDO;
import org.projectforge.user.PFUserDO;
import org.projectforge.web.common.OutputType;
import org.projectforge.web.task.TaskFormatter;
/**
* Repräsentiert einen Monatsbericht eines Mitarbeiters.
* @author Kai Reinhard (k.reinhard@micromata.de)
*/
public class MonthlyEmployeeReport implements Serializable
{
private static final long serialVersionUID = -4636357379552246075L;
public class Kost2Row implements Serializable
{
private static final long serialVersionUID = -5379735557333691194L;
public Kost2Row(final Kost2DO kost2)
{
this.kost2 = kost2;
}
/**
* XML-escaped or null if not exists.
*/
public String getProjektname()
{
if (kost2 == null || kost2.getProjekt() == null) {
return null;
}
return StringEscapeUtils.escapeXml(kost2.getProjekt().getName());
}
/**
* XML-escaped or null if not exists.
*/
public String getKundename()
{
if (kost2 == null || kost2.getProjekt() == null || kost2.getProjekt().getKunde() == null) {
return null;
}
return StringEscapeUtils.escapeXml(kost2.getProjekt().getKunde().getName());
}
/**
* XML-escaped or null if not exists.
*/
public String getKost2ArtName()
{
if (kost2 == null || kost2.getKost2Art() == null) {
return null;
}
return StringEscapeUtils.escapeXml(kost2.getKost2Art().getName());
}
/**
* XML-escaped or null if not exists.
*/
public String getKost2Description()
{
if (kost2 == null) {
return null;
}
return StringEscapeUtils.escapeXml(kost2.getDescription());
}
public Kost2DO getKost2()
{
return kost2;
}
private final Kost2DO kost2;
}
private final int year;
private final int month;
private Date fromDate;
private Date toDate;
private BigDecimal numberOfWorkingDays;
/** Employee can be null, if not found. As fall back, store user. */
private PFUserDO user;
private EmployeeDO employee;
private long totalGrossDuration = 0, totalNetDuration = 0;
private Integer kost1Id;
private List<MonthlyEmployeeReportWeek> weeks;
/** Days with time sheets. */
private final Set<Integer> bookedDays = new HashSet<Integer>();
private final List<Integer> unbookedDays = new ArrayList<Integer>();
/**
* Key is kost2.id.
*/
private Map<Integer, MonthlyEmployeeReportEntry> kost2Durations;
/**
* Key is task.id.
*/
private Map<Integer, MonthlyEmployeeReportEntry> taskDurations;
/** String is formatted Kost2-String for sorting. */
private Map<String, Kost2Row> kost2Rows;
/** String is formatted Task path string for sorting. */
private Map<String, TaskDO> taskEntries;
public static final String getFormattedDuration(final long duration)
{
if (duration == 0) {
return "";
}
final BigDecimal hours = new BigDecimal(duration).divide(new BigDecimal(1000 * 60 * 60), 2, BigDecimal.ROUND_HALF_UP);
return NumberHelper.formatFraction2(hours);
}
/**
* Dont't forget to initialize: setFormatter and setUser or setEmployee.
* @param year
* @param month
*/
public MonthlyEmployeeReport(final int year, final int month)
{
this.year = year;
this.month = month;
}
/**
* Use only as fallback, if employee is not available.
* @param user
*/
public void setUser(final PFUserDO user)
{
this.user = user;
}
/**
* User will be set automatically from given employee.
* @param employee
*/
public void setEmployee(final EmployeeDO employee)
{
this.employee = employee;
if (employee != null) {
this.user = employee.getUser();
this.kost1Id = employee.getKost1Id();
}
}
public void init()
{
// Create the weeks:
this.weeks = new ArrayList<MonthlyEmployeeReportWeek>();
final DateHolder dh = new DateHolder();
dh.setDate(year, month, 1, 0, 0, 0);
fromDate = dh.getDate();
final DateHolder dh2 = new DateHolder(dh.getDate());
dh2.setEndOfMonth();
toDate = dh2.getDate();
int i = 0;
do {
final MonthlyEmployeeReportWeek week = new MonthlyEmployeeReportWeek(dh.getDate());
weeks.add(week);
dh.setEndOfWeek();
dh.add(Calendar.DAY_OF_WEEK, +1);
dh.setBeginOfWeek();
if (i++ > 10) {
throw new RuntimeException("Endless loop protection: Please contact developer!");
}
} while (dh.getDate().before(toDate));
}
public void addTimesheet(final TimesheetDO sheet)
{
final DayHolder day = new DayHolder(sheet.getStartTime());
bookedDays.add(day.getDayOfMonth());
for (final MonthlyEmployeeReportWeek week : weeks) {
if (week.matchWeek(sheet) == true) {
week.addEntry(sheet);
return;
}
}
throw new RuntimeException("Oups, given time sheet is not inside the month represented by this month object.");
}
public void calculate()
{
Validate.notEmpty(weeks);
kost2Rows = new TreeMap<String, Kost2Row>();
taskEntries = new TreeMap<String, TaskDO>();
kost2Durations = new HashMap<Integer, MonthlyEmployeeReportEntry>();
taskDurations = new HashMap<Integer, MonthlyEmployeeReportEntry>();
for (final MonthlyEmployeeReportWeek week : weeks) {
if (MapUtils.isNotEmpty(week.getKost2Entries()) == true) {
for (final MonthlyEmployeeReportEntry entry : week.getKost2Entries().values()) {
Validate.notNull(entry.getKost2());
kost2Rows.put(entry.getKost2().getShortDisplayName(), new Kost2Row(entry.getKost2()));
MonthlyEmployeeReportEntry kost2Total = kost2Durations.get(entry.getKost2().getId());
if (kost2Total == null) {
kost2Total = new MonthlyEmployeeReportEntry(entry.getKost2());
kost2Total.addMillis(entry.getWorkFractionMillis());
kost2Durations.put(entry.getKost2().getId(), kost2Total);
} else {
kost2Total.addMillis(entry.getWorkFractionMillis());
}
// Travelling times etc. (see cost 2 type factor):
totalGrossDuration += entry.getMillis();
totalNetDuration += entry.getWorkFractionMillis();
}
}
if (MapUtils.isNotEmpty(week.getTaskEntries()) == true) {
for (final MonthlyEmployeeReportEntry entry : week.getTaskEntries().values()) {
Validate.notNull(entry.getTask());
taskEntries.put(TaskFormatter.instance().getTaskPath(entry.getTask().getId(), true, OutputType.XML), entry.getTask());
MonthlyEmployeeReportEntry taskTotal = taskDurations.get(entry.getTask().getId());
if (taskTotal == null) {
taskTotal = new MonthlyEmployeeReportEntry(entry.getTask());
taskTotal.addMillis(entry.getMillis());
taskDurations.put(entry.getTask().getId(), taskTotal);
} else {
taskTotal.addMillis(entry.getMillis());
}
totalGrossDuration += entry.getMillis();
totalNetDuration += entry.getMillis();
}
}
}
final MonthHolder monthHolder = new MonthHolder(this.fromDate);
this.numberOfWorkingDays = monthHolder.getNumberOfWorkingDays();
for (final WeekHolder week : monthHolder.getWeeks()) {
for (final DayHolder day : week.getDays()) {
if (day.getMonth() == this.month && day.isWorkingDay() == true && bookedDays.contains(day.getDayOfMonth()) == false) {
unbookedDays.add(day.getDayOfMonth());
}
}
}
}
/**
* Gets the list of unbooked working days. These are working days without time sheets of the actual user.
*/
public List<Integer> getUnbookedDays()
{
return unbookedDays;
}
/**
* @return Days of month without time sheets: 03.11., 08.11., ... or null if no entries exists.
*/
public String getFormattedUnbookedDays()
{
final StringBuffer buf = new StringBuffer();
boolean first = true;
for (final Integer dayOfMonth : unbookedDays) {
if (first == true) {
first = false;
} else {
buf.append(", ");
}
buf.append(StringHelper.format2DigitNumber(dayOfMonth)).append(".").append(StringHelper.format2DigitNumber(month + 1)).append(".");
}
if (first == true) {
return null;
}
return buf.toString();
}
/**
* Key is the shortDisplayName of Kost2DO. The Map is a TreeMap sorted by the keys.
*/
public Map<String, Kost2Row> getKost2Rows()
{
return kost2Rows;
}
/**
* Key is the kost2 id.
*/
public Map<Integer, MonthlyEmployeeReportEntry> getKost2Durations()
{
return kost2Durations;
}
/**
* Key is the task path string of TaskDO. The Map is a TreeMap sorted by the keys.
*/
public Map<String, TaskDO> getTaskEntries()
{
return taskEntries;
}
/**
* Key is the task id.
*/
public Map<Integer, MonthlyEmployeeReportEntry> getTaskDurations()
{
return taskDurations;
}
public int getYear()
{
return year;
}
public int getMonth()
{
return month;
}
public List<MonthlyEmployeeReportWeek> getWeeks()
{
return weeks;
}
public String getFormmattedMonth()
{
return StringHelper.format2DigitNumber(month + 1);
}
public Date getFromDate()
{
return fromDate;
}
public Date getToDate()
{
return toDate;
}
/**
* Can be null, if not set (not available).
*/
public EmployeeDO getEmployee()
{
return employee;
}
public PFUserDO getUser()
{
return user;
}
/**
* @return Total duration in ms.
*/
public long getTotalGrossDuration()
{
return totalGrossDuration;
}
/**
* The net duration may differ from total duration (e. g. for travelling times if a fraction is defined for used cost 2 types).
* @return the netDuration in ms.
*/
public long getTotalNetDuration()
{
return totalNetDuration;
}
public String getFormattedTotalGrossDuration()
{
return MonthlyEmployeeReport.getFormattedDuration(totalGrossDuration);
}
public String getFormattedTotalNetDuration()
{
return MonthlyEmployeeReport.getFormattedDuration(totalNetDuration);
}
public Integer getKost1Id()
{
return kost1Id;
}
public BigDecimal getNumberOfWorkingDays()
{
return numberOfWorkingDays;
}
}