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.getPayCalendarEntry().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.getPayCalendarEntry().getBeginPeriodDateTime())); 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().getPayBeginDate()); 069 if (prevBlocks.size() > 0) { 070 TimesheetDocumentHeader prevTdh = TkServiceLocator.getTimesheetDocumentHeaderService().getPreviousDocumentHeader(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getPayBeginDate()); 071 if (prevTdh != null) { 072 CalendarEntries prevPayCalendarEntry = TkServiceLocator.getCalendarService().getCalendarDatesByPayEndDate(timesheetDocument.getPrincipalId(), prevTdh.getPayEndDate(), null); 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.getPayCalendarEntry().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 (Long jobNumber: jobNumberToShifts.keySet()) { 195 List<ShiftDifferentialRule> shiftDifferentialRules = jobNumberToShifts.get(jobNumber); 196 // Obtain and sort our previous and current time blocks. 197 List<TimeBlock> ruleTimeBlocksPrev = null; 198 List<TimeBlock> ruleTimeBlocksCurr = jobNumberToTimeBlocks.get(jobNumber); 199 if (ruleTimeBlocksCurr != null && ruleTimeBlocksCurr.size() > 0) { 200 if (jobNumberToTimeBlocksPreviousDay != null) 201 ruleTimeBlocksPrev = jobNumberToTimeBlocksPreviousDay.get(jobNumber); 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.getEarnGroupService().getEarnCodeListForEarnGroup(rule.getFromEarnGroup(), TKUtils.getTimelessDate(timesheetDocument.getPayCalendarEntry().getBeginPeriodDateTime())); 214 215 // Because of the way java.sql.Time are stored, we need to first 216 // construct a LocalTime in the System Time Zone, then convert that 217 // time to the users time zone. 218 LocalTime ruleStart = new LocalTime(rule.getBeginTime(), TKUtils.getSystemDateTimeZone()); 219 LocalTime ruleEnd = new LocalTime(rule.getEndTime(), TKUtils.getSystemDateTimeZone()); 220 ruleStart = new LocalTime(ruleStart, zone); 221 ruleEnd = new LocalTime(ruleEnd, zone); 222 223 224 DateTime shiftEnd = ruleEnd.toDateTime(currentDay); 225 DateTime shiftStart = ruleStart.toDateTime(currentDay); 226 227 if (shiftEnd.isBefore(shiftStart) || shiftEnd.isEqual(shiftStart)) 228 shiftEnd = shiftEnd.plusDays(1); 229 Interval shiftInterval = new Interval(shiftStart, shiftEnd); 230 231 // Set up buckets to handle previous days time accumulations 232 BigDecimal hoursBeforeVirtualDay = BigDecimal.ZERO; 233 234 // Check current day first block to see if start time gap from virtual day start is greater than max gap 235 // if so, we can skip the previous day checks. 236 TimeBlock firstBlockOfCurrentDay = null; 237 for (TimeBlock b : ruleTimeBlocksCurr) { 238 if (timeBlockHasEarnCode(fromEarnGroup, b)) { 239 firstBlockOfCurrentDay = b; 240 break; 241 } 242 } 243 244 // Previous Day :: We have prior block container of nonzero size, and the previous day is active. 245 Interval previousDayShiftInterval = new Interval(shiftStart.minusDays(1), shiftEnd.minusDays(1)); 246 247 // Blank initialization pointer for picking which interval to pass to applyPremium() 248 Interval evalInterval = null; 249 if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0 && dayIsRuleActive(currentDay.minusDays(1), rule)) { 250 // Simple heuristic to see if we even need to worry about 251 // the Shift rule for this set of data. 252 if (shiftEnd.isAfter(virtualDay.getEnd())) { 253 // Compare first block of previous day with first block of current day for max gaptitude. 254 TimeBlock firstBlockOfPreviousDay = null; 255 for (TimeBlock b : ruleTimeBlocksPrev) { 256 if (timeBlockHasEarnCode(fromEarnGroup, b)) { 257 firstBlockOfPreviousDay = b; 258 break; 259 } 260 } 261 // Only if we actually have at least one block. 262 // Adding Assumption: We must have both a valid current and previous block. Max Gap can not be more than a virtual day. 263 // If this assumption does not hold, additional logic will be needed to iteratively go back in time to figure out which 264 // blocks are valid. 265 if ( (firstBlockOfPreviousDay != null) && (firstBlockOfCurrentDay != null)) { 266 Interval previousBlockInterval = new Interval(new DateTime(firstBlockOfPreviousDay.getEndTimestamp(), zone), new DateTime(firstBlockOfCurrentDay.getBeginTimestamp(), zone)); 267 Duration blockGapDuration = previousBlockInterval.toDuration(); 268 BigDecimal bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis()); 269 // if maxGap is 0, ignore gaps and assign shift to time blocks within the hours 270 if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) { 271 // If we are here, we know we have at least one valid time block to pull some hours forward from. 272 273 274 // These are inversely sorted. 275 for (int i=0; i<ruleTimeBlocksPrev.size(); i++) { 276 TimeBlock b = ruleTimeBlocksPrev.get(i); 277 if (timeBlockHasEarnCode(fromEarnGroup, b)) { 278 Interval blockInterval = new Interval(new DateTime(b.getBeginTimestamp(), zone), new DateTime(b.getEndTimestamp(), zone)); 279 280 // Calculate Block Gap, the duration between clock outs and clock ins of adjacent time blocks. 281 if (previousBlockInterval != null) { 282 blockGapDuration = new Duration(new DateTime(b.getEndTimestamp(), zone), previousBlockInterval.getStart()); 283 bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis()); 284 } 285 286 // Check Gap, if good, sum hours, if maxGap is 0, ignore gaps 287 if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) { 288 // Calculate Overlap and add it to hours before virtual day bucket. 289 if (blockInterval.overlaps(previousDayShiftInterval)) { 290 BigDecimal hrs = TKUtils.convertMillisToHours(blockInterval.overlap(previousDayShiftInterval).toDurationMillis()); 291 hoursBeforeVirtualDay = hoursBeforeVirtualDay.add(hrs); 292 } 293 294 } else { 295 // Time blocks are reverse sorted, we can jump out as soon as the max gap is exceeded. 296 break; 297 } 298 299 previousBlockInterval = blockInterval; 300 301 } 302 } 303 } else { 304 // DO NOTHING! 305 } 306 } 307 } 308 } 309 310 BigDecimal hoursToApply = BigDecimal.ZERO; 311 BigDecimal hoursToApplyPrevious = BigDecimal.ZERO; 312 // If the hours before virtual day are less than or equal to 313 // min hours, we have already applied the time, so we don't 314 // set hoursToApplyPrevious 315 if (hoursBeforeVirtualDay.compareTo(rule.getMinHours()) <= 0) { 316 // we need to apply these hours. 317 hoursToApplyPrevious = hoursBeforeVirtualDay; 318 } 319 320 321 // Current Day 322 323 TimeBlock previous = null; // Previous Time Block 324 List<TimeBlock> accumulatedBlocks = new ArrayList<TimeBlock>(); // TimeBlocks we MAY or MAY NOT apply Shift Premium to. 325 List<Interval> accumulatedBlockIntervals = new ArrayList<Interval>(); // To save recompute time when checking timeblocks for application we store them as we create them. 326 // Iterate over sorted list, checking time boundaries vs Shift Intervals. 327 long accumulatedMillis = TKUtils.convertHoursToMillis(hoursBeforeVirtualDay); 328 329 boolean previousDayOnly = false; // IF the rule is not active today, but was on the previous day, we need to still look at time blocks. 330 if (!dayIsRuleActive(currentDay, rule)) { 331 if (dayIsRuleActive(currentDay.minusDays(1), rule)) { 332 previousDayOnly = true; 333 } else { 334 // Nothing to see here, move to next rule. 335 continue; 336 } 337 338 } 339 340 /* 341 * We will touch each time block and accumulate time blocks that are applicable to 342 * the current rule we are on. 343 */ 344 345 // These blocks are only used for detail application 346 // We don't want to pass along the previous pay period, 347 // because we don't want to modify the time blocks on that 348 // period. If null is passed, time will be placed on the 349 // first block of the first period if the previous period 350 // block had influence. 351 List<TimeBlock> previousBlocksFiltered = (previousPayPeriodPrevDay) ? null : filterBlocksByApplicableEarnGroup(fromEarnGroup, ruleTimeBlocksPrev); 352 353 for (TimeBlock current : ruleTimeBlocksCurr) { 354 if (!timeBlockHasEarnCode(fromEarnGroup, current)) { 355 // TODO: WorkSchedule considerations somewhere in here? 356 continue; 357 } 358 359 Interval blockInterval = new Interval(new DateTime(current.getBeginTimestamp(), zone), new DateTime(current.getEndTimestamp(), zone)); 360 361 // Check both Intervals, since the time blocks could still 362 // be applicable to the previous day. These two intervals should 363 // not have any overlap. 364 if (previousDayShiftInterval.overlaps(shiftInterval)) 365 throw new RuntimeException("Interval of greater than 24 hours created in the rules processing."); 366 367 // This block of code handles cases where you have time 368 // that spills to multiple days and a shift rule that 369 // has a valid window on multiple consecutive days. Time 370 // must be applied with the correct shift interval. 371 Interval overlap = previousDayShiftInterval.overlap(blockInterval); 372 evalInterval = previousDayShiftInterval; 373 if (overlap == null) { 374 if (hoursToApplyPrevious.compareTo(BigDecimal.ZERO) > 0) { 375 // we have hours from previous day, and the shift 376 // window is going to move to current day. 377 // Need to apply this now, and move window forward 378 // for current time block. 379 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); 380 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); 381 accumulatedMillis = 0L; // reset accumulated hours.. 382 hoursToApply = BigDecimal.ZERO; 383 hoursToApplyPrevious = BigDecimal.ZERO; 384 } 385 386 // Because of our position in the loop, when we are at this point, 387 // we know we've passed any previous day shift intervals, so we can 388 // determine if we should skip the current day based on the boolean 389 // we set earlier. 390 if (previousDayOnly) { 391 continue; 392 } 393 394 overlap = shiftInterval.overlap(blockInterval); 395 evalInterval = shiftInterval; 396 } 397 398 // Time bucketing and application as normal: 399 // 400 if (overlap != null) { 401 // There IS overlap. 402 if (previous != null) { 403 // only check max gap if max gap of rule is not 0 404 if (rule.getMaxGap().compareTo(BigDecimal.ZERO) != 0 && exceedsMaxGap(previous, current, rule.getMaxGap())) { 405 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); 406 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); 407 accumulatedMillis = 0L; // reset accumulated hours.. 408 hoursToApply = BigDecimal.ZERO; 409 hoursToApplyPrevious = BigDecimal.ZERO; 410 } else { 411 long millis = overlap.toDurationMillis(); 412 accumulatedMillis += millis; 413 hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis)); 414 } 415 } else { 416 // Overlap shift at first time block. 417 long millis = overlap.toDurationMillis(); 418 accumulatedMillis += millis; 419 hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis)); 420 } 421 accumulatedBlocks.add(current); 422 accumulatedBlockIntervals.add(blockInterval); 423 previous = current; // current can still apply to next. 424 } else { 425 // No Overlap / Outside of Rule 426 if (previous != null) { 427 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); 428 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); 429 accumulatedMillis = 0L; // reset accumulated hours.. 430 hoursToApply = BigDecimal.ZERO; 431 hoursToApplyPrevious = BigDecimal.ZERO; 432 } 433 } 434 435 } 436 437 // All time blocks are iterated over, check for remainders. 438 // Check containers for time, and apply if needed. 439 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); 440 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); 441 } 442 } 443 // Keep track of previous as we move day by day. 444 jobNumberToTimeBlocksPreviousDay = jobNumberToTimeBlocks; 445 previousPayPeriodPrevDay = false; 446 } 447 448 } 449 450 @Override 451 public List<ShiftDifferentialRule> getShiftDifferentialRules(String location, String hrSalGroup, String payGrade, Date fromEffdt, Date toEffdt, String active, String showHist) { 452 return shiftDifferentialRuleDao.getShiftDifferentialRules(location, hrSalGroup, payGrade, fromEffdt, toEffdt, active, showHist); 453 } 454 455 private List<TimeBlock> filterBlocksByApplicableEarnGroup(Set<String> fromEarnGroup, List<TimeBlock> blocks) { 456 List<TimeBlock> filtered; 457 458 if (blocks == null || blocks.size() == 0) 459 filtered = null; 460 else { 461 filtered = new ArrayList<TimeBlock>(); 462 for (TimeBlock b : blocks) { 463 if (timeBlockHasEarnCode(fromEarnGroup, b)) 464 filtered.add(b); 465 } 466 } 467 468 return filtered; 469 } 470 471 472 private void applyAccumulatedWrapper(BigDecimal accumHours, Interval evalInterval, List<Interval>accumulatedBlockIntervals, List<TimeBlock>accumulatedBlocks, List<TimeBlock> previousBlocks, BigDecimal hoursToApplyPrevious, BigDecimal hoursToApply, ShiftDifferentialRule rule) { 473 if (accumHours.compareTo(rule.getMinHours()) >= 0) { 474 this.applyPremium(evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocks, hoursToApplyPrevious, hoursToApply, rule.getEarnCode()); 475 } 476 accumulatedBlocks.clear(); 477 accumulatedBlockIntervals.clear(); 478 } 479 480 private void sortTimeBlocksInverse(List<TimeBlock> blocks) { 481 Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks 482 public int compare(TimeBlock tb1, TimeBlock tb2) { 483 if (tb1 != null && tb2 != null) 484 return -1 * tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp()); 485 return 0; 486 } 487 }); 488 } 489 490 private void sortTimeBlocksNatural(List<TimeBlock> blocks) { 491 Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks 492 public int compare(TimeBlock tb1, TimeBlock tb2) { 493 if (tb1 != null && tb2 != null) 494 return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp()); 495 return 0; 496 } 497 }); 498 } 499 500 /** 501 * 502 * @param shift The shift interval - need to examine the time block to determine how many hours are eligible per block. 503 * @param blockIntervals Intervals for each block present in the blocks list. Passed here to avoid re computation. 504 * @param blocks The blocks we are applying hours to. 505 * @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. 506 * @param initialHours hours accumulated from a previous boundary that need to be applied here (NOT SUBJECT TO INTERVAL) 507 * @param hours hours to apply 508 * @param earnCode what earn code to create time hour detail entry for. 509 */ 510 void applyPremium(Interval shift, List<Interval> blockIntervals, List<TimeBlock> blocks, List<TimeBlock> previousBlocks, BigDecimal initialHours, BigDecimal hours, String earnCode) { 511 for (int i=0; i<blocks.size(); i++) { 512 TimeBlock b = blocks.get(i); 513 514 // Only apply initial hours to the first timeblock. 515 if (i == 0 && (initialHours.compareTo(BigDecimal.ZERO) > 0)) { 516 // ONLY if they're on the same document ID, do we apply to previous, 517 // otherwise we dump all on the current document. 518 if (previousBlocks != null && previousBlocks.size() > 0 && previousBlocks.get(0).getDocumentId().equals(b.getDocumentId())) { 519 for (TimeBlock pb : previousBlocks) { 520 BigDecimal lunchSub = this.negativeTimeHourDetailSum(pb); // A negative number 521 initialHours = BigDecimal.ZERO.max(initialHours.add(lunchSub)); // We don't want negative premium hours! 522 if (initialHours.compareTo(BigDecimal.ZERO) <= 0) // check here now as well, we may not have anything at all to apply. 523 break; 524 525 // Adjust hours on the block by the lunch sub hours, so we're not over applying. 526 BigDecimal hoursToApply = initialHours.min(pb.getHours().add(lunchSub)); 527 addPremiumTimeHourDetail(pb, hoursToApply, earnCode); 528 initialHours = initialHours.subtract(hoursToApply, TkConstants.MATH_CONTEXT); 529 if (initialHours.compareTo(BigDecimal.ZERO) <= 0) 530 break; 531 } 532 } else { 533 addPremiumTimeHourDetail(b, initialHours, earnCode); 534 } 535 } 536 537 BigDecimal lunchSub = this.negativeTimeHourDetailSum(b); // A negative number 538 hours = BigDecimal.ZERO.max(hours.add(lunchSub)); // We don't want negative premium hours! 539 540 if (hours.compareTo(BigDecimal.ZERO) > 0) { 541 Interval blockInterval = blockIntervals.get(i); 542 Interval overlapInterval = shift.overlap(blockInterval); 543 if (overlapInterval == null) 544 continue; 545 546 long overlap = overlapInterval.toDurationMillis(); 547 BigDecimal hoursMax = TKUtils.convertMillisToHours(overlap); // Maximum number of possible hours applicable for this time block and shift rule 548 // Adjust this time block's hoursMax (below) by lunchSub to 549 // make sure the time applied is the correct amount per block. 550 BigDecimal hoursToApply = hours.min(hoursMax.add(lunchSub)); 551 552 addPremiumTimeHourDetail(b, hoursToApply, earnCode); 553 hours = hours.subtract(hoursToApply, TkConstants.MATH_CONTEXT); 554 } 555 } 556 } 557 558 void addPremiumTimeHourDetail(TimeBlock block, BigDecimal hours, String earnCode) { 559 List<TimeHourDetail> details = block.getTimeHourDetails(); 560 TimeHourDetail premium = new TimeHourDetail(); 561 premium.setHours(hours); 562 premium.setEarnCode(earnCode); 563 premium.setTkTimeBlockId(block.getTkTimeBlockId()); 564 details.add(premium); 565 } 566 567 /** 568 * Does the difference between the previous time blocks clock out time and the 569 * current time blocks clock in time exceed the max gap. max gap is in minutes 570 * 571 * @param previous 572 * @param current 573 * @param maxGap 574 * @return 575 */ 576 boolean exceedsMaxGap(TimeBlock previous, TimeBlock current, BigDecimal maxGap) { 577 long difference = current.getBeginTimestamp().getTime() - previous.getEndTimestamp().getTime(); 578 BigDecimal gapMinutes = TKUtils.convertMillisToMinutes(difference); 579 580 return (gapMinutes.compareTo(maxGap) > 0); 581 } 582 583 public void setShiftDifferentialRuleDao(ShiftDifferentialRuleDao shiftDifferentialRuleDao) { 584 this.shiftDifferentialRuleDao = shiftDifferentialRuleDao; 585 } 586 587 @Override 588 public ShiftDifferentialRule getShiftDifferentialRule(String tkShiftDifferentialRuleId) { 589 return this.shiftDifferentialRuleDao.findShiftDifferentialRule(tkShiftDifferentialRuleId); 590 } 591 592 @Override 593 public List<ShiftDifferentialRule> getShiftDifferentalRules(String location, String hrSalGroup, String payGrade, String pyCalendarGroup, Date asOfDate) { 594 List<ShiftDifferentialRule> sdrs = new ArrayList<ShiftDifferentialRule>(); 595 596 // location, sal group, pay grade 597 598 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, pyCalendarGroup, asOfDate)); 599 600 // location, sal group, * 601 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", pyCalendarGroup, asOfDate)); 602 603 // location, *, pay grade 604 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, pyCalendarGroup, asOfDate)); 605 606 // location, *, * 607 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", pyCalendarGroup, asOfDate)); 608 609 // *, sal group, pay grade 610 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, pyCalendarGroup, asOfDate)); 611 612 // *, sal group, * 613 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", pyCalendarGroup, asOfDate)); 614 615 // *, *, pay grade 616 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, pyCalendarGroup, asOfDate)); 617 618 // *, *, * 619 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", pyCalendarGroup, asOfDate)); 620 621 if (sdrs == null) 622 sdrs = Collections.emptyList(); 623 624 return sdrs; 625 } 626 627 private boolean dayIsRuleActive(DateTime currentDate, ShiftDifferentialRule sdr) { 628 boolean active = false; 629 630 switch (currentDate.getDayOfWeek()) { 631 case DateTimeConstants.MONDAY: 632 active = sdr.isMonday(); 633 break; 634 case DateTimeConstants.TUESDAY: 635 active = sdr.isTuesday(); 636 break; 637 case DateTimeConstants.WEDNESDAY: 638 active = sdr.isWednesday(); 639 break; 640 case DateTimeConstants.THURSDAY: 641 active = sdr.isThursday(); 642 break; 643 case DateTimeConstants.FRIDAY: 644 active = sdr.isFriday(); 645 break; 646 case DateTimeConstants.SATURDAY: 647 active = sdr.isSaturday(); 648 break; 649 case DateTimeConstants.SUNDAY: 650 active = sdr.isSunday(); 651 break; 652 } 653 654 return active; 655 } 656 657 @Override 658 public void saveOrUpdate(List<ShiftDifferentialRule> shiftDifferentialRules) { 659 shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRules); 660 } 661 662 @Override 663 public void saveOrUpdate(ShiftDifferentialRule shiftDifferentialRule) { 664 shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRule); 665 } 666 667 }