001 /** 002 * Copyright 2004-2012 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.hr.time.timesummary.service; 017 018 import org.apache.commons.lang.StringUtils; 019 import org.joda.time.DateTimeFieldType; 020 import org.joda.time.LocalDateTime; 021 import org.kuali.hr.time.assignment.Assignment; 022 import org.kuali.hr.time.assignment.AssignmentDescriptionKey; 023 import org.kuali.hr.time.calendar.Calendar; 024 import org.kuali.hr.time.calendar.CalendarEntries; 025 import org.kuali.hr.time.earncode.EarnCode; 026 import org.kuali.hr.time.earngroup.EarnGroup; 027 import org.kuali.hr.time.flsa.FlsaDay; 028 import org.kuali.hr.time.flsa.FlsaWeek; 029 import org.kuali.hr.time.service.base.TkServiceLocator; 030 import org.kuali.hr.time.timeblock.TimeBlock; 031 import org.kuali.hr.time.timeblock.TimeHourDetail; 032 import org.kuali.hr.time.timesheet.TimesheetDocument; 033 import org.kuali.hr.time.timesummary.AssignmentRow; 034 import org.kuali.hr.time.timesummary.EarnCodeSection; 035 import org.kuali.hr.time.timesummary.EarnGroupSection; 036 import org.kuali.hr.time.timesummary.TimeSummary; 037 import org.kuali.hr.time.util.TKUtils; 038 import org.kuali.hr.time.util.TkConstants; 039 import org.kuali.hr.time.util.TkTimeBlockAggregate; 040 041 import java.math.BigDecimal; 042 import java.util.*; 043 044 public class TimeSummaryServiceImpl implements TimeSummaryService { 045 private static final String OTHER_EARN_GROUP = "Other"; 046 047 @Override 048 public TimeSummary getTimeSummary(TimesheetDocument timesheetDocument) { 049 TimeSummary timeSummary = new TimeSummary(); 050 051 if(timesheetDocument.getTimeBlocks() == null) { 052 return timeSummary; 053 } 054 055 List<Boolean> dayArrangements = new ArrayList<Boolean>(); 056 057 timeSummary.setSummaryHeader(getHeaderForSummary(timesheetDocument.getPayCalendarEntry(), dayArrangements)); 058 TkTimeBlockAggregate tkTimeBlockAggregate = new TkTimeBlockAggregate(timesheetDocument.getTimeBlocks(), timesheetDocument.getPayCalendarEntry(), TkServiceLocator.getCalendarService().getCalendar(timesheetDocument.getPayCalendarEntry().getHrCalendarId()), true); 059 timeSummary.setWorkedHours(getWorkedHours(tkTimeBlockAggregate)); 060 061 List<EarnGroupSection> earnGroupSections = getEarnGroupSections(tkTimeBlockAggregate, timeSummary.getSummaryHeader().size()+1, dayArrangements, timesheetDocument.getAsOfDate()); 062 timeSummary.setSections(earnGroupSections); 063 064 return timeSummary; 065 } 066 067 /** 068 * Aggregates timeblocks into the appropriate earngroup-> earncode -> assignment rows 069 * @param tkTimeBlockAggregate 070 * @param numEntries 071 * @param dayArrangements 072 * @param asOfDate 073 * @return 074 */ 075 public List<EarnGroupSection> getEarnGroupSections(TkTimeBlockAggregate tkTimeBlockAggregate, int numEntries, List<Boolean> dayArrangements, Date asOfDate ){ 076 List<EarnGroupSection> earnGroupSections = new ArrayList<EarnGroupSection>(); 077 List<FlsaWeek> flsaWeeks = tkTimeBlockAggregate.getFlsaWeeks(TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback()); 078 Map<String, EarnCodeSection> earnCodeToEarnCodeSection = new HashMap<String, EarnCodeSection>(); 079 Map<String, EarnGroupSection> earnGroupToEarnGroupSection = new HashMap<String, EarnGroupSection>(); 080 081 int dayCount = 0; 082 083 //TODO remove this and correct the aggregate .. not sure what the down stream changes are 084 //so leaving this for initial release 085 List<FlsaWeek> trimmedFlsaWeeks = new ArrayList<FlsaWeek>(); 086 for(FlsaWeek flsaWeek : flsaWeeks){ 087 if(flsaWeek.getFlsaDays().size() > 0){ 088 trimmedFlsaWeeks.add(flsaWeek); 089 } 090 } 091 092 //For every flsa week and day aggegate each time hour detail 093 // buckets it by earn code section first 094 for(FlsaWeek flsaWeek : trimmedFlsaWeeks){ 095 int weekSize = 0; 096 List<FlsaDay> flsaDays = flsaWeek.getFlsaDays(); 097 for(FlsaDay flsaDay : flsaDays){ 098 Map<String, List<TimeBlock>> earnCodeToTimeBlocks = flsaDay.getEarnCodeToTimeBlocks(); 099 100 for(String earnCode : earnCodeToTimeBlocks.keySet()){ 101 for(TimeBlock timeBlock : earnCodeToTimeBlocks.get(earnCode)){ 102 for(TimeHourDetail thd : timeBlock.getTimeHourDetails()){ 103 if(StringUtils.equals(TkConstants.LUNCH_EARN_CODE, thd.getEarnCode())){ 104 continue; 105 } 106 EarnCodeSection earnCodeSection = earnCodeToEarnCodeSection.get(thd.getEarnCode()); 107 if(earnCodeSection == null){ 108 earnCodeSection = new EarnCodeSection(); 109 earnCodeSection.setEarnCode(thd.getEarnCode()); 110 EarnCode earnCodeObj = TkServiceLocator.getEarnCodeService().getEarnCode(thd.getEarnCode(), TKUtils.getTimelessDate(asOfDate)); 111 earnCodeSection.setDescription(earnCodeObj.getDescription()); 112 earnCodeSection.setIsAmountEarnCode((earnCodeObj.getRecordMethod()!= null && earnCodeObj.getRecordMethod().equalsIgnoreCase(TkConstants.EARN_CODE_AMOUNT)) ? true : false); 113 for(int i = 0;i<(numEntries-1);i++){ 114 earnCodeSection.getTotals().add(BigDecimal.ZERO); 115 } 116 117 earnCodeToEarnCodeSection.put(thd.getEarnCode(), earnCodeSection); 118 } 119 String assignKey = timeBlock.getAssignmentKey(); 120 AssignmentRow assignRow = earnCodeSection.getAssignKeyToAssignmentRowMap().get(assignKey); 121 if(assignRow == null){ 122 assignRow = new AssignmentRow(); 123 assignRow.setAssignmentKey(assignKey); 124 AssignmentDescriptionKey assignmentKey = TkServiceLocator.getAssignmentService().getAssignmentDescriptionKey(assignKey); 125 Assignment assignment = TkServiceLocator.getAssignmentService().getAssignment(timeBlock.getPrincipalId(), assignmentKey, TKUtils.getTimelessDate(asOfDate)); 126 //TODO push this up to the assignment fetch/fully populated instead of like this 127 if(assignment != null){ 128 if(assignment.getJob() == null){ 129 assignment.setJob(TkServiceLocator.getJobService().getJob(assignment.getPrincipalId(),assignment.getJobNumber(),TKUtils.getTimelessDate(asOfDate))); 130 } 131 if(assignment.getWorkAreaObj() == null){ 132 assignment.setWorkAreaObj(TkServiceLocator.getWorkAreaService().getWorkArea(assignment.getWorkArea(), TKUtils.getTimelessDate(asOfDate))); 133 } 134 assignRow.setDescr(assignment.getAssignmentDescription()); 135 } 136 for(int i = 0;i<(numEntries-1);i++){ 137 assignRow.getTotal().add(BigDecimal.ZERO); 138 assignRow.getAmount().add(BigDecimal.ZERO); 139 } 140 assignRow.setEarnCodeSection(earnCodeSection); 141 earnCodeSection.addAssignmentRow(assignRow); 142 } 143 assignRow.addToTotal(dayCount, thd.getHours()); 144 assignRow.addToAmount(dayCount, thd.getAmount()); 145 } 146 } 147 } 148 dayCount++; 149 weekSize++; 150 } 151 //end of flsa week accumulate weekly totals 152 for(EarnCodeSection earnCodeSection : earnCodeToEarnCodeSection.values()){ 153 earnCodeSection.addWeeklyTotal(dayCount, weekSize); 154 } 155 weekSize = 0; 156 157 dayCount++; 158 } 159 160 dayCount = 0; 161 //now create all teh earn group sections and aggregate accordingly 162 for(EarnCodeSection earnCodeSection : earnCodeToEarnCodeSection.values()){ 163 String earnCode = earnCodeSection.getEarnCode(); 164 EarnGroup earnGroupObj = TkServiceLocator.getEarnGroupService().getEarnGroupSummaryForEarnCode(earnCode, TKUtils.getTimelessDate(asOfDate)); 165 String earnGroup = null; 166 if(earnGroupObj == null){ 167 earnGroup = OTHER_EARN_GROUP; 168 } else{ 169 earnGroup = earnGroupObj.getDescr(); 170 } 171 172 EarnGroupSection earnGroupSection = earnGroupToEarnGroupSection.get(earnGroup); 173 if(earnGroupSection == null){ 174 earnGroupSection = new EarnGroupSection(); 175 earnGroupSection.setEarnGroup(earnGroup); 176 for(int i =0;i<(numEntries-1);i++){ 177 earnGroupSection.getTotals().add(BigDecimal.ZERO); 178 } 179 earnGroupToEarnGroupSection.put(earnGroup, earnGroupSection); 180 } 181 earnGroupSection.addEarnCodeSection(earnCodeSection, dayArrangements); 182 183 } 184 for(EarnGroupSection earnGroupSection : earnGroupToEarnGroupSection.values()){ 185 earnGroupSections.add(earnGroupSection); 186 } 187 return earnGroupSections; 188 } 189 190 /** 191 * Generate a list of string describing this pay calendar entry for the summary 192 * @param payCalEntry 193 * @return 194 */ 195 protected List<String> getSummaryHeader(CalendarEntries payCalEntry){ 196 List<String> summaryHeader = new ArrayList<String>(); 197 int dayCount = 0; 198 Date beginDateTime = payCalEntry.getBeginPeriodDateTime(); 199 Date endDateTime = payCalEntry.getEndPeriodDateTime(); 200 boolean virtualDays = false; 201 LocalDateTime endDate = payCalEntry.getEndLocalDateTime(); 202 203 if (endDate.get(DateTimeFieldType.hourOfDay()) != 0 || endDate.get(DateTimeFieldType.minuteOfHour()) != 0 || 204 endDate.get(DateTimeFieldType.secondOfMinute()) != 0){ 205 virtualDays = true; 206 } 207 208 Date currDateTime = beginDateTime; 209 java.util.Calendar cal = GregorianCalendar.getInstance(); 210 211 while(currDateTime.before(endDateTime)){ 212 LocalDateTime currDate = new LocalDateTime(currDateTime); 213 summaryHeader.add(makeHeaderDiplayString(currDate, virtualDays)); 214 215 dayCount++; 216 if((dayCount % 7) == 0){ 217 summaryHeader.add("Week "+ ((dayCount / 7))); 218 } 219 cal.setTime(currDateTime); 220 cal.add(java.util.Calendar.HOUR, 24); 221 currDateTime = cal.getTime(); 222 } 223 224 summaryHeader.add("Period Total"); 225 return summaryHeader; 226 } 227 228 /** 229 * Provides the number of hours worked for the pay period indicated in the 230 * aggregate. 231 * 232 * @param aggregate The aggregate we are summing 233 * 234 * @return A list of BigDecimals containing the number of hours worked. 235 * This list will line up with the header. 236 */ 237 private List<BigDecimal> getWorkedHours(TkTimeBlockAggregate aggregate) { 238 List<BigDecimal> hours = new ArrayList<BigDecimal>(); 239 BigDecimal periodTotal = TkConstants.BIG_DECIMAL_SCALED_ZERO; 240 for (FlsaWeek week : aggregate.getFlsaWeeks(TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback())) { 241 BigDecimal weeklyTotal = TkConstants.BIG_DECIMAL_SCALED_ZERO; 242 for (FlsaDay day : week.getFlsaDays()) { 243 BigDecimal totalForDay = TkConstants.BIG_DECIMAL_SCALED_ZERO; 244 for (TimeBlock block : day.getAppliedTimeBlocks()) { 245 totalForDay = totalForDay.add(block.getHours(), TkConstants.MATH_CONTEXT); 246 weeklyTotal = weeklyTotal.add(block.getHours(), TkConstants.MATH_CONTEXT); 247 periodTotal = periodTotal.add(block.getHours(), TkConstants.MATH_CONTEXT); 248 } 249 hours.add(totalForDay); 250 } 251 hours.add(weeklyTotal); 252 } 253 hours.add(periodTotal); 254 255 return hours; 256 } 257 258 259 /** 260 * Handles the generation of the display header for the time summary. 261 * 262 * @param cal The PayCalendarEntries object we are using to derive information. 263 * @param dayArrangements Container passed in to store the position of week / period aggregate sums 264 * 265 * @return An in-order string of days for this period that properly accounts 266 * for FLSA week boundaries in the pay period. 267 */ 268 @Override 269 public List<String> getHeaderForSummary(CalendarEntries cal, List<Boolean> dayArrangements) { 270 List<String> header = new ArrayList<String>(); 271 272 // Maps directly to joda time day of week constants. 273 int flsaBeginDay = this.getPayCalendarForEntry(cal).getFlsaBeginDayConstant(); 274 boolean virtualDays = false; 275 LocalDateTime startDate = cal.getBeginLocalDateTime(); 276 LocalDateTime endDate = cal.getEndLocalDateTime(); 277 278 // Increment end date if we are on a virtual day calendar, so that the 279 // for loop can account for having the proper amount of days on the 280 // summary calendar. 281 if (endDate.get(DateTimeFieldType.hourOfDay()) != 0 || endDate.get(DateTimeFieldType.minuteOfHour()) != 0 || 282 endDate.get(DateTimeFieldType.secondOfMinute()) != 0) 283 { 284 endDate = endDate.plusDays(1); 285 virtualDays = true; 286 } 287 288 boolean afterFirstDay = false; 289 int week = 1; 290 for (LocalDateTime currentDate = startDate; currentDate.compareTo(endDate) < 0; currentDate = currentDate.plusDays(1)) { 291 292 if (currentDate.getDayOfWeek() == flsaBeginDay && afterFirstDay) { 293 header.add("Week " + week); 294 dayArrangements.add(false); 295 week++; 296 } 297 298 header.add(makeHeaderDiplayString(currentDate, virtualDays)); 299 dayArrangements.add(true); 300 301 302 afterFirstDay = true; 303 } 304 305 // We may have a very small final "week" on this pay period. For now 306 // we will mark it as a week, and if someone doesn't like it, it can 307 // be removed. 308 if (!header.get(header.size()-1).startsWith("Week")) { 309 dayArrangements.add(false); 310 header.add("Week " + week); 311 } 312 313 314 header.add("Period Total"); 315 dayArrangements.add(false); 316 return header; 317 } 318 319 /** 320 * Helper function to generate display text for the summary header. 321 * @param currentDate The date we are generating for. 322 * @param virtualDays Whether or not virtual days apply. 323 * @return A string appropriate for UI display. 324 */ 325 private String makeHeaderDiplayString(LocalDateTime currentDate, boolean virtualDays) { 326 StringBuilder display = new StringBuilder(currentDate.toString(TkConstants.DT_ABBREV_DATE_FORMAT)); 327 328 if (virtualDays) { 329 LocalDateTime nextDay = currentDate.plusDays(1); 330 display.append(" - "); 331 display.append(nextDay.toString(TkConstants.DT_ABBREV_DATE_FORMAT)); 332 } 333 334 return display.toString(); 335 } 336 337 /** 338 * @param calEntry Calendar entry we are using for lookup. 339 * @return The PayCalendar that owns the provided entry. 340 */ 341 private Calendar getPayCalendarForEntry(CalendarEntries calEntry) { 342 Calendar cal = null; 343 344 if (calEntry != null) { 345 cal = TkServiceLocator.getCalendarService().getCalendar(calEntry.getHrCalendarId()); 346 } 347 348 return cal; 349 } 350 351 }