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