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.shiftdiff.rule.service; 017 018 import org.apache.log4j.Logger; 019 import org.joda.time.*; 020 import org.kuali.hr.job.Job; 021 import org.kuali.hr.time.calendar.CalendarEntries; 022 import org.kuali.hr.time.principal.PrincipalHRAttributes; 023 import org.kuali.hr.time.service.base.TkServiceLocator; 024 import org.kuali.hr.time.shiftdiff.rule.ShiftDifferentialRule; 025 import org.kuali.hr.time.shiftdiff.rule.dao.ShiftDifferentialRuleDao; 026 import org.kuali.hr.time.timeblock.TimeBlock; 027 import org.kuali.hr.time.timeblock.TimeHourDetail; 028 import org.kuali.hr.time.timesheet.TimesheetDocument; 029 import org.kuali.hr.time.util.TKUtils; 030 import org.kuali.hr.time.util.TkConstants; 031 import org.kuali.hr.time.util.TkTimeBlockAggregate; 032 import org.kuali.hr.time.workflow.TimesheetDocumentHeader; 033 034 import java.math.BigDecimal; 035 import java.sql.Date; 036 import java.util.*; 037 038 039 public class ShiftDifferentialRuleServiceImpl implements ShiftDifferentialRuleService { 040 041 @SuppressWarnings("unused") 042 private static final Logger LOG = Logger.getLogger(ShiftDifferentialRuleServiceImpl.class); 043 /** 044 * The maximum allowable time between timeblocks before we consider them to 045 * be day-boundary single time blocks. 046 */ 047 private ShiftDifferentialRuleDao shiftDifferentialRuleDao = null; 048 049 private Map<Long,List<ShiftDifferentialRule>> getJobNumberToShiftRuleMap(TimesheetDocument timesheetDocument) { 050 Map<Long,List<ShiftDifferentialRule>> jobNumberToShifts = new HashMap<Long,List<ShiftDifferentialRule>>(); 051 PrincipalHRAttributes principalCal = TkServiceLocator.getPrincipalHRAttributeService().getPrincipalCalendar(timesheetDocument.getPrincipalId(),timesheetDocument.getCalendarEntry().getEndPeriodDate()); 052 053 for (Job job : timesheetDocument.getJobs()) { 054 List<ShiftDifferentialRule> shiftDifferentialRules = getShiftDifferentalRules(job.getLocation(),job.getHrSalGroup(),job.getPayGrade(),principalCal.getPayCalendar(), 055 TKUtils.getTimelessDate(timesheetDocument.getCalendarEntry().getEndPeriodDateTime())); 056 if (shiftDifferentialRules.size() > 0) 057 jobNumberToShifts.put(job.getJobNumber(), shiftDifferentialRules); 058 } 059 060 return jobNumberToShifts; 061 } 062 063 private Map<Long,List<TimeBlock>> getPreviousPayPeriodLastDayJobToTimeBlockMap(TimesheetDocument timesheetDocument, Map<Long,List<ShiftDifferentialRule>> jobNumberToShifts) { 064 Map<Long, List<TimeBlock>> jobNumberToTimeBlocksPreviousDay = null; 065 066 // Get the last day of the last week of the previous pay period. 067 // This is the only day that can have impact on the current day. 068 List<TimeBlock> prevBlocks = TkServiceLocator.getTimesheetService().getPrevDocumentTimeBlocks(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getBeginDate()); 069 if (prevBlocks.size() > 0) { 070 TimesheetDocumentHeader prevTdh = TkServiceLocator.getTimesheetDocumentHeaderService().getPreviousDocumentHeader(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getBeginDate()); 071 if (prevTdh != null) { 072 CalendarEntries prevPayCalendarEntry = TkServiceLocator.getCalendarService().getCalendarDatesByPayEndDate(timesheetDocument.getPrincipalId(), prevTdh.getEndDate(), TkConstants.PAY_CALENDAR_TYPE); 073 TkTimeBlockAggregate prevTimeAggregate = new TkTimeBlockAggregate(prevBlocks, prevPayCalendarEntry, prevPayCalendarEntry.getCalendarObj(), true); 074 List<List<TimeBlock>> dayBlocks = prevTimeAggregate.getDayTimeBlockList(); 075 List<TimeBlock> previousPeriodLastDayBlocks = dayBlocks.get(dayBlocks.size() - 1); 076 // Set back to null if there is nothing in the list. 077 if (previousPeriodLastDayBlocks.size() > 0) { 078 jobNumberToTimeBlocksPreviousDay = new HashMap<Long, List<TimeBlock>>(); 079 080 for (TimeBlock block : previousPeriodLastDayBlocks) { 081 // Job Number to TimeBlock for Last Day of Previous Time 082 // Period 083 Long jobNumber = block.getJobNumber(); 084 if (jobNumberToShifts.containsKey(jobNumber)) { 085 // we have a useful timeblock. 086 List<TimeBlock> jblist = jobNumberToTimeBlocksPreviousDay.get(jobNumber); 087 if (jblist == null) { 088 jblist = new ArrayList<TimeBlock>(); 089 jobNumberToTimeBlocksPreviousDay.put(jobNumber, jblist); 090 } 091 jblist.add(block); 092 } 093 } 094 } 095 } 096 } 097 098 return jobNumberToTimeBlocksPreviousDay; 099 } 100 101 private boolean timeBlockHasEarnCode(Set<String> earnCodes, TimeBlock block) { 102 boolean present = false; 103 104 if (block != null && earnCodes != null) 105 present = earnCodes.contains(block.getEarnCode()); 106 107 return present; 108 } 109 110 /** 111 * Returns a BigDecimal representing the sum of all of the negative time 112 * hour detail types. In this case, only LUN is considered. This can be 113 * modified to add other considerations. 114 * 115 * @param block The Timeblock to inspect. 116 * 117 * @return A big decimal. 118 */ 119 private BigDecimal negativeTimeHourDetailSum(TimeBlock block) { 120 BigDecimal sum = BigDecimal.ZERO; 121 122 if (block != null) { 123 List<TimeHourDetail> details = block.getTimeHourDetails(); 124 for (TimeHourDetail detail : details) { 125 if (detail.getEarnCode().equals(TkConstants.LUNCH_EARN_CODE)) { 126 sum = sum.add(detail.getHours()); 127 } 128 } 129 } 130 131 return sum; 132 } 133 134 @Override 135 public void processShiftDifferentialRules(TimesheetDocument timesheetDocument, TkTimeBlockAggregate aggregate) { 136 DateTimeZone zone = TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback(); 137 List<List<TimeBlock>> blockDays = aggregate.getDayTimeBlockList(); 138 DateTime periodStartDateTime = timesheetDocument.getCalendarEntry().getBeginLocalDateTime().toDateTime(zone); 139 Map<Long,List<ShiftDifferentialRule>> jobNumberToShifts = getJobNumberToShiftRuleMap(timesheetDocument); 140 141 142 // If there are no shift differential rules, we have an early exit. 143 if (jobNumberToShifts.isEmpty()) { 144 return; 145 } 146 147 // Get the last day of the previous pay period. We need this to determine 148 // if there are hours from the previous pay period that will effect the 149 // shift rule on the first day of the currently-being-processed pay period. 150 // 151 // Will be set to null if not applicable. 152 boolean previousPayPeriodPrevDay = true; 153 Map<Long, List<TimeBlock>> jobNumberToTimeBlocksPreviousDay = 154 getPreviousPayPeriodLastDayJobToTimeBlockMap(timesheetDocument, jobNumberToShifts); 155 156 // We are going to look at the time blocks grouped by Days. 157 // 158 // This is a very large outer loop. 159 for (int pos = 0; pos < blockDays.size(); pos++) { 160 List<TimeBlock> blocks = blockDays.get(pos); // Timeblocks for this day. 161 if (blocks.isEmpty()) 162 continue; // No Time blocks, no worries. 163 164 DateTime currentDay = periodStartDateTime.plusDays(pos); 165 Interval virtualDay = new Interval(currentDay, currentDay.plusHours(24)); 166 167 // Builds our JobNumber to TimeBlock for Current Day List. 168 // 169 // Shift Differential Rules are also grouped by Job number, this 170 // provides a quick way to do the lookup / reference. 171 // We don't need every time block, only the ones that will be 172 // applicable to the shift rules. 173 Map<Long, List<TimeBlock>> jobNumberToTimeBlocks = new HashMap<Long,List<TimeBlock>>(); 174 for (TimeBlock block : blocks) { 175 Long jobNumber = block.getJobNumber(); 176 if (jobNumberToShifts.containsKey(jobNumber)) { 177 List<TimeBlock> jblist = jobNumberToTimeBlocks.get(jobNumber); 178 if (jblist == null) { 179 jblist = new ArrayList<TimeBlock>(); 180 jobNumberToTimeBlocks.put(jobNumber, jblist); 181 } 182 jblist.add(block); 183 } 184 } 185 186 187 // Large Outer Loop to look at applying the Shift Rules based on 188 // the current JobNumber. 189 // 190 // This loop will handle previous day boundary time as well as the 191 // current day. 192 // 193 // There is room for refactoring here! 194 for (Map.Entry<Long, List<ShiftDifferentialRule>> entry : jobNumberToShifts.entrySet()) { 195 List<ShiftDifferentialRule> shiftDifferentialRules = entry.getValue(); 196 // Obtain and sort our previous and current time blocks. 197 List<TimeBlock> ruleTimeBlocksPrev = null; 198 List<TimeBlock> ruleTimeBlocksCurr = jobNumberToTimeBlocks.get(entry.getKey()); 199 if (ruleTimeBlocksCurr != null && ruleTimeBlocksCurr.size() > 0) { 200 if (jobNumberToTimeBlocksPreviousDay != null) 201 ruleTimeBlocksPrev = jobNumberToTimeBlocksPreviousDay.get(entry.getKey()); 202 if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0) 203 this.sortTimeBlocksInverse(ruleTimeBlocksPrev); 204 this.sortTimeBlocksNatural(ruleTimeBlocksCurr); 205 } else { 206 // Skip to next job, there is nothing for this job 207 // on this day, and because of this we don't care 208 // about the previous day either. 209 continue; 210 } 211 212 for (ShiftDifferentialRule rule : shiftDifferentialRules) { 213 Set<String> fromEarnGroup = TkServiceLocator.getEarnCodeGroupService().getEarnCodeListForEarnCodeGroup(rule.getFromEarnGroup(), TKUtils.getTimelessDate(timesheetDocument.getCalendarEntry().getBeginPeriodDateTime())); 214 215 LocalTime ruleStart = new LocalTime(rule.getBeginTime(), zone); 216 LocalTime ruleEnd = new LocalTime(rule.getEndTime(), zone); 217 218 219 DateTime shiftEnd = ruleEnd.toDateTime(currentDay); 220 DateTime shiftStart = ruleStart.toDateTime(currentDay); 221 222 if (shiftEnd.isBefore(shiftStart) || shiftEnd.isEqual(shiftStart)) { 223 shiftEnd = shiftEnd.plusDays(1); 224 } 225 Interval shiftInterval = new Interval(shiftStart, shiftEnd); 226 227 // Set up buckets to handle previous days time accumulations 228 BigDecimal hoursBeforeVirtualDay = BigDecimal.ZERO; 229 230 // Check current day first block to see if start time gap from virtual day start is greater than max gap 231 // if so, we can skip the previous day checks. 232 TimeBlock firstBlockOfCurrentDay = null; 233 for (TimeBlock b : ruleTimeBlocksCurr) { 234 if (timeBlockHasEarnCode(fromEarnGroup, b)) { 235 firstBlockOfCurrentDay = b; 236 break; 237 } 238 } 239 240 // Previous Day :: We have prior block container of nonzero size, and the previous day is active. 241 Interval previousDayShiftInterval = new Interval(shiftStart.minusDays(1), shiftEnd.minusDays(1)); 242 243 // Blank initialization pointer for picking which interval to pass to applyPremium() 244 Interval evalInterval = null; 245 if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0 && dayIsRuleActive(currentDay.minusDays(1), rule)) { 246 // Simple heuristic to see if we even need to worry about 247 // the Shift rule for this set of data. 248 if (shiftEnd.isAfter(virtualDay.getEnd())) { 249 // Compare first block of previous day with first block of current day for max gaptitude. 250 TimeBlock firstBlockOfPreviousDay = null; 251 for (TimeBlock b : ruleTimeBlocksPrev) { 252 if (timeBlockHasEarnCode(fromEarnGroup, b)) { 253 firstBlockOfPreviousDay = b; 254 break; 255 } 256 } 257 // Only if we actually have at least one block. 258 // Adding Assumption: We must have both a valid current and previous block. Max Gap can not be more than a virtual day. 259 // If this assumption does not hold, additional logic will be needed to iteratively go back in time to figure out which 260 // blocks are valid. 261 if ( (firstBlockOfPreviousDay != null) && (firstBlockOfCurrentDay != null)) { 262 Interval previousBlockInterval = new Interval(new DateTime(firstBlockOfPreviousDay.getEndTimestamp(), zone), new DateTime(firstBlockOfCurrentDay.getBeginTimestamp(), zone)); 263 Duration blockGapDuration = previousBlockInterval.toDuration(); 264 BigDecimal bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis()); 265 // if maxGap is 0, ignore gaps and assign shift to time blocks within the hours 266 if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) { 267 // If we are here, we know we have at least one valid time block to pull some hours forward from. 268 269 270 // These are inversely sorted. 271 for (int i=0; i<ruleTimeBlocksPrev.size(); i++) { 272 TimeBlock b = ruleTimeBlocksPrev.get(i); 273 if (timeBlockHasEarnCode(fromEarnGroup, b)) { 274 Interval blockInterval = new Interval(new DateTime(b.getBeginTimestamp(), zone), new DateTime(b.getEndTimestamp(), zone)); 275 276 // Calculate Block Gap, the duration between clock outs and clock ins of adjacent time blocks. 277 if (previousBlockInterval != null) { 278 blockGapDuration = new Duration(new DateTime(b.getEndTimestamp(), zone), previousBlockInterval.getStart()); 279 bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis()); 280 } 281 282 // Check Gap, if good, sum hours, if maxGap is 0, ignore gaps 283 if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) { 284 // Calculate Overlap and add it to hours before virtual day bucket. 285 if (blockInterval.overlaps(previousDayShiftInterval)) { 286 BigDecimal hrs = TKUtils.convertMillisToHours(blockInterval.overlap(previousDayShiftInterval).toDurationMillis()); 287 hoursBeforeVirtualDay = hoursBeforeVirtualDay.add(hrs); 288 } 289 290 } else { 291 // Time blocks are reverse sorted, we can jump out as soon as the max gap is exceeded. 292 break; 293 } 294 295 previousBlockInterval = blockInterval; 296 297 } 298 } 299 } else { 300 // DO NOTHING! 301 } 302 } 303 } 304 } 305 306 BigDecimal hoursToApply = BigDecimal.ZERO; 307 BigDecimal hoursToApplyPrevious = BigDecimal.ZERO; 308 // If the hours before virtual day are less than or equal to 309 // min hours, we have already applied the time, so we don't 310 // set hoursToApplyPrevious 311 if (hoursBeforeVirtualDay.compareTo(rule.getMinHours()) <= 0) { 312 // we need to apply these hours. 313 hoursToApplyPrevious = hoursBeforeVirtualDay; 314 } 315 316 317 // Current Day 318 319 TimeBlock previous = null; // Previous Time Block 320 List<TimeBlock> accumulatedBlocks = new ArrayList<TimeBlock>(); // TimeBlocks we MAY or MAY NOT apply Shift Premium to. 321 List<Interval> accumulatedBlockIntervals = new ArrayList<Interval>(); // To save recompute time when checking timeblocks for application we store them as we create them. 322 // Iterate over sorted list, checking time boundaries vs Shift Intervals. 323 long accumulatedMillis = TKUtils.convertHoursToMillis(hoursBeforeVirtualDay); 324 325 boolean previousDayOnly = false; // IF the rule is not active today, but was on the previous day, we need to still look at time blocks. 326 if (!dayIsRuleActive(currentDay, rule)) { 327 if (dayIsRuleActive(currentDay.minusDays(1), rule)) { 328 previousDayOnly = true; 329 } else { 330 // Nothing to see here, move to next rule. 331 continue; 332 } 333 334 } 335 336 /* 337 * We will touch each time block and accumulate time blocks that are applicable to 338 * the current rule we are on. 339 */ 340 341 // These blocks are only used for detail application 342 // We don't want to pass along the previous pay period, 343 // because we don't want to modify the time blocks on that 344 // period. If null is passed, time will be placed on the 345 // first block of the first period if the previous period 346 // block had influence. 347 List<TimeBlock> previousBlocksFiltered = (previousPayPeriodPrevDay) ? null : filterBlocksByApplicableEarnGroup(fromEarnGroup, ruleTimeBlocksPrev); 348 349 for (TimeBlock current : ruleTimeBlocksCurr) { 350 if (!timeBlockHasEarnCode(fromEarnGroup, current)) { 351 // TODO: WorkSchedule considerations somewhere in here? 352 continue; 353 } 354 355 Interval blockInterval = new Interval(new DateTime(current.getBeginTimestamp(), zone), new DateTime(current.getEndTimestamp(), zone)); 356 357 // Check both Intervals, since the time blocks could still 358 // be applicable to the previous day. These two intervals should 359 // not have any overlap. 360 if (previousDayShiftInterval.overlaps(shiftInterval)) 361 throw new RuntimeException("Interval of greater than 24 hours created in the rules processing."); 362 363 // This block of code handles cases where you have time 364 // that spills to multiple days and a shift rule that 365 // has a valid window on multiple consecutive days. Time 366 // must be applied with the correct shift interval. 367 Interval overlap = previousDayShiftInterval.overlap(blockInterval); 368 evalInterval = previousDayShiftInterval; 369 if (overlap == null) { 370 if (hoursToApplyPrevious.compareTo(BigDecimal.ZERO) > 0) { 371 // we have hours from previous day, and the shift 372 // window is going to move to current day. 373 // Need to apply this now, and move window forward 374 // for current time block. 375 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); 376 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); 377 accumulatedMillis = 0L; // reset accumulated hours.. 378 hoursToApply = BigDecimal.ZERO; 379 hoursToApplyPrevious = BigDecimal.ZERO; 380 } 381 382 // Because of our position in the loop, when we are at this point, 383 // we know we've passed any previous day shift intervals, so we can 384 // determine if we should skip the current day based on the boolean 385 // we set earlier. 386 if (previousDayOnly) { 387 continue; 388 } 389 390 overlap = shiftInterval.overlap(blockInterval); 391 evalInterval = shiftInterval; 392 } 393 394 // Time bucketing and application as normal: 395 // 396 if (overlap != null) { 397 // There IS overlap. 398 if (previous != null) { 399 // only check max gap if max gap of rule is not 0 400 if (rule.getMaxGap().compareTo(BigDecimal.ZERO) != 0 && exceedsMaxGap(previous, current, rule.getMaxGap())) { 401 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); 402 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); 403 accumulatedMillis = 0L; // reset accumulated hours.. 404 hoursToApply = BigDecimal.ZERO; 405 hoursToApplyPrevious = BigDecimal.ZERO; 406 } else { 407 long millis = overlap.toDurationMillis(); 408 accumulatedMillis += millis; 409 hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis)); 410 } 411 } else { 412 // Overlap shift at first time block. 413 long millis = overlap.toDurationMillis(); 414 accumulatedMillis += millis; 415 hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis)); 416 } 417 accumulatedBlocks.add(current); 418 accumulatedBlockIntervals.add(blockInterval); 419 previous = current; // current can still apply to next. 420 } else { 421 // No Overlap / Outside of Rule 422 if (previous != null) { 423 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); 424 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); 425 accumulatedMillis = 0L; // reset accumulated hours.. 426 hoursToApply = BigDecimal.ZERO; 427 hoursToApplyPrevious = BigDecimal.ZERO; 428 } 429 } 430 431 } 432 433 // All time blocks are iterated over, check for remainders. 434 // Check containers for time, and apply if needed. 435 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); 436 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); 437 } 438 } 439 // Keep track of previous as we move day by day. 440 jobNumberToTimeBlocksPreviousDay = jobNumberToTimeBlocks; 441 previousPayPeriodPrevDay = false; 442 } 443 444 } 445 446 @Override 447 public List<ShiftDifferentialRule> getShiftDifferentialRules(String location, String hrSalGroup, String payGrade, Date fromEffdt, Date toEffdt, String active, String showHist) { 448 return shiftDifferentialRuleDao.getShiftDifferentialRules(location, hrSalGroup, payGrade, fromEffdt, toEffdt, active, showHist); 449 } 450 451 private List<TimeBlock> filterBlocksByApplicableEarnGroup(Set<String> fromEarnGroup, List<TimeBlock> blocks) { 452 List<TimeBlock> filtered; 453 454 if (blocks == null || blocks.size() == 0) 455 filtered = null; 456 else { 457 filtered = new ArrayList<TimeBlock>(); 458 for (TimeBlock b : blocks) { 459 if (timeBlockHasEarnCode(fromEarnGroup, b)) 460 filtered.add(b); 461 } 462 } 463 464 return filtered; 465 } 466 467 private void applyAccumulatedWrapper(BigDecimal accumHours, 468 Interval evalInterval, 469 List<Interval>accumulatedBlockIntervals, 470 List<TimeBlock>accumulatedBlocks, 471 List<TimeBlock> previousBlocks, 472 BigDecimal hoursToApplyPrevious, 473 BigDecimal hoursToApply, 474 ShiftDifferentialRule rule) { 475 if (accumHours.compareTo(rule.getMinHours()) >= 0) { 476 this.applyPremium(evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocks, hoursToApplyPrevious, hoursToApply, rule.getEarnCode()); 477 } 478 accumulatedBlocks.clear(); 479 accumulatedBlockIntervals.clear(); 480 } 481 482 private void sortTimeBlocksInverse(List<TimeBlock> blocks) { 483 Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks 484 public int compare(TimeBlock tb1, TimeBlock tb2) { 485 if (tb1 != null && tb2 != null) 486 return -1 * tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp()); 487 return 0; 488 } 489 }); 490 } 491 492 private void sortTimeBlocksNatural(List<TimeBlock> blocks) { 493 Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks 494 public int compare(TimeBlock tb1, TimeBlock tb2) { 495 if (tb1 != null && tb2 != null) 496 return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp()); 497 return 0; 498 } 499 }); 500 } 501 502 /** 503 * 504 * @param shift The shift interval - need to examine the time block to determine how many hours are eligible per block. 505 * @param blockIntervals Intervals for each block present in the blocks list. Passed here to avoid re computation. 506 * @param blocks The blocks we are applying hours to. 507 * @param previousBlocks If present, this is the list of time blocks from a previous "day", on which the initial hours (from previous day) should be placed. 508 * @param initialHours hours accumulated from a previous boundary that need to be applied here (NOT SUBJECT TO INTERVAL) 509 * @param hours hours to apply 510 * @param earnCode what earn code to create time hour detail entry for. 511 */ 512 void applyPremium(Interval shift, List<Interval> blockIntervals, List<TimeBlock> blocks, List<TimeBlock> previousBlocks, BigDecimal initialHours, BigDecimal hours, String earnCode) { 513 for (int i=0; i<blocks.size(); i++) { 514 TimeBlock b = blocks.get(i); 515 516 // Only apply initial hours to the first timeblock. 517 if (i == 0 && (initialHours.compareTo(BigDecimal.ZERO) > 0)) { 518 // ONLY if they're on the same document ID, do we apply to previous, 519 // otherwise we dump all on the current document. 520 if (previousBlocks != null && previousBlocks.size() > 0 && previousBlocks.get(0).getDocumentId().equals(b.getDocumentId())) { 521 for (TimeBlock pb : previousBlocks) { 522 BigDecimal lunchSub = this.negativeTimeHourDetailSum(pb); // A negative number 523 initialHours = BigDecimal.ZERO.max(initialHours.add(lunchSub)); // We don't want negative premium hours! 524 if (initialHours.compareTo(BigDecimal.ZERO) <= 0) // check here now as well, we may not have anything at all to apply. 525 break; 526 527 // Adjust hours on the block by the lunch sub hours, so we're not over applying. 528 BigDecimal hoursToApply = initialHours.min(pb.getHours().add(lunchSub)); 529 addPremiumTimeHourDetail(pb, hoursToApply, earnCode); 530 initialHours = initialHours.subtract(hoursToApply, TkConstants.MATH_CONTEXT); 531 if (initialHours.compareTo(BigDecimal.ZERO) <= 0) 532 break; 533 } 534 } else { 535 addPremiumTimeHourDetail(b, initialHours, earnCode); 536 } 537 } 538 539 BigDecimal lunchSub = this.negativeTimeHourDetailSum(b); // A negative number 540 hours = BigDecimal.ZERO.max(hours.add(lunchSub)); // We don't want negative premium hours! 541 542 if (hours.compareTo(BigDecimal.ZERO) > 0) { 543 Interval blockInterval = blockIntervals.get(i); 544 Interval overlapInterval = shift.overlap(blockInterval); 545 if (overlapInterval == null) 546 continue; 547 548 long overlap = overlapInterval.toDurationMillis(); 549 BigDecimal hoursMax = TKUtils.convertMillisToHours(overlap); // Maximum number of possible hours applicable for this time block and shift rule 550 // Adjust this time block's hoursMax (below) by lunchSub to 551 // make sure the time applied is the correct amount per block. 552 BigDecimal hoursToApply = hours.min(hoursMax.add(lunchSub)); 553 554 addPremiumTimeHourDetail(b, hoursToApply, earnCode); 555 hours = hours.subtract(hoursToApply, TkConstants.MATH_CONTEXT); 556 } 557 } 558 } 559 560 void addPremiumTimeHourDetail(TimeBlock block, BigDecimal hours, String earnCode) { 561 List<TimeHourDetail> details = block.getTimeHourDetails(); 562 TimeHourDetail premium = new TimeHourDetail(); 563 premium.setHours(hours); 564 premium.setEarnCode(earnCode); 565 premium.setTkTimeBlockId(block.getTkTimeBlockId()); 566 details.add(premium); 567 } 568 569 /** 570 * Does the difference between the previous time blocks clock out time and the 571 * current time blocks clock in time exceed the max gap. max gap is in minutes 572 * 573 * @param previous 574 * @param current 575 * @param maxGap 576 * @return 577 */ 578 boolean exceedsMaxGap(TimeBlock previous, TimeBlock current, BigDecimal maxGap) { 579 long difference = current.getBeginTimestamp().getTime() - previous.getEndTimestamp().getTime(); 580 BigDecimal gapMinutes = TKUtils.convertMillisToMinutes(difference); 581 582 return (gapMinutes.compareTo(maxGap) > 0); 583 } 584 585 public void setShiftDifferentialRuleDao(ShiftDifferentialRuleDao shiftDifferentialRuleDao) { 586 this.shiftDifferentialRuleDao = shiftDifferentialRuleDao; 587 } 588 589 @Override 590 public ShiftDifferentialRule getShiftDifferentialRule(String tkShiftDifferentialRuleId) { 591 return this.shiftDifferentialRuleDao.findShiftDifferentialRule(tkShiftDifferentialRuleId); 592 } 593 594 @Override 595 public List<ShiftDifferentialRule> getShiftDifferentalRules(String location, String hrSalGroup, String payGrade, String pyCalendarGroup, Date asOfDate) { 596 List<ShiftDifferentialRule> sdrs = new ArrayList<ShiftDifferentialRule>(); 597 598 // location, sal group, pay grade, calendar 599 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, pyCalendarGroup, asOfDate)); 600 601 // location, sal group, *, calendar 602 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", pyCalendarGroup, asOfDate)); 603 604 // location, *, pay grade, calendar 605 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, pyCalendarGroup, asOfDate)); 606 607 // location, *, *, calendar 608 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", pyCalendarGroup, asOfDate)); 609 610 // *, sal group, pay grade, calendar 611 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, pyCalendarGroup, asOfDate)); 612 613 // *, sal group, *, calendar 614 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", pyCalendarGroup, asOfDate)); 615 616 // *, *, pay grade, calendar 617 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, pyCalendarGroup, asOfDate)); 618 619 // *, *, *, calendar 620 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", pyCalendarGroup, asOfDate)); 621 622 // location, sal group, pay grade, * 623 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, "%", asOfDate)); 624 625 // location, sal group, *, * 626 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", "%", asOfDate)); 627 628 // location, *, pay grade, * 629 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, "%", asOfDate)); 630 631 // location, *, *, * 632 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", "%", asOfDate)); 633 634 // *, sal group, pay grade, * 635 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, "%", asOfDate)); 636 637 // *, sal group, *, * 638 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", "%", asOfDate)); 639 640 // *, *, pay grade, * 641 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, "%", asOfDate)); 642 643 // *, *, *, * 644 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", "%", asOfDate)); 645 646 return sdrs; 647 } 648 649 private boolean dayIsRuleActive(DateTime currentDate, ShiftDifferentialRule sdr) { 650 boolean active = false; 651 652 switch (currentDate.getDayOfWeek()) { 653 case DateTimeConstants.MONDAY: 654 active = sdr.isMonday(); 655 break; 656 case DateTimeConstants.TUESDAY: 657 active = sdr.isTuesday(); 658 break; 659 case DateTimeConstants.WEDNESDAY: 660 active = sdr.isWednesday(); 661 break; 662 case DateTimeConstants.THURSDAY: 663 active = sdr.isThursday(); 664 break; 665 case DateTimeConstants.FRIDAY: 666 active = sdr.isFriday(); 667 break; 668 case DateTimeConstants.SATURDAY: 669 active = sdr.isSaturday(); 670 break; 671 case DateTimeConstants.SUNDAY: 672 active = sdr.isSunday(); 673 break; 674 } 675 676 return active; 677 } 678 679 @Override 680 public void saveOrUpdate(List<ShiftDifferentialRule> shiftDifferentialRules) { 681 shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRules); 682 } 683 684 @Override 685 public void saveOrUpdate(ShiftDifferentialRule shiftDifferentialRule) { 686 shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRule); 687 } 688 689 }