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