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.lm.accrual.service; 017 018 import java.math.BigDecimal; 019 import java.sql.Date; 020 import java.sql.Timestamp; 021 import java.util.ArrayList; 022 import java.util.Calendar; 023 import java.util.GregorianCalendar; 024 import java.util.HashMap; 025 import java.util.HashSet; 026 import java.util.List; 027 import java.util.Map; 028 import java.util.Set; 029 030 import org.apache.commons.collections.CollectionUtils; 031 import org.apache.commons.lang.StringUtils; 032 import org.apache.log4j.Logger; 033 import org.joda.time.DateTime; 034 import org.joda.time.Interval; 035 import org.kuali.hr.job.Job; 036 import org.kuali.hr.lm.LMConstants; 037 import org.kuali.hr.lm.accrual.AccrualCategory; 038 import org.kuali.hr.lm.accrual.AccrualCategoryRule; 039 import org.kuali.hr.lm.accrual.PrincipalAccrualRan; 040 import org.kuali.hr.lm.accrual.RateRange; 041 import org.kuali.hr.lm.accrual.RateRangeAggregate; 042 import org.kuali.hr.lm.leaveblock.LeaveBlock; 043 import org.kuali.hr.lm.leaveplan.LeavePlan; 044 import org.kuali.hr.lm.timeoff.SystemScheduledTimeOff; 045 import org.kuali.hr.lm.workflow.LeaveCalendarDocumentHeader; 046 import org.kuali.hr.time.assignment.Assignment; 047 import org.kuali.hr.time.calendar.CalendarEntries; 048 import org.kuali.hr.time.earncode.EarnCode; 049 import org.kuali.hr.time.principal.PrincipalHRAttributes; 050 import org.kuali.hr.time.service.base.TkServiceLocator; 051 import org.kuali.hr.time.util.TKContext; 052 import org.kuali.hr.time.util.TKUtils; 053 import org.kuali.hr.time.util.TkConstants; 054 055 import edu.emory.mathcs.backport.java.util.Collections; 056 057 public class AccrualServiceImpl implements AccrualService { 058 private static final Logger LOG = Logger.getLogger(AccrualServiceImpl.class); 059 060 @Override 061 public void runAccrual(String principalId) { 062 Date startDate = getStartAccrualDate(principalId); 063 Date endDate = getEndAccrualDate(principalId); 064 065 LOG.info("AccrualServiceImpl.runAccrual() STARTED with Principal: "+principalId); 066 runAccrual(principalId,startDate,endDate, true); 067 068 } 069 070 @Override 071 public void runAccrual(String principalId, Date startDate, Date endDate, boolean recordRanData) { 072 runAccrual(principalId, startDate, endDate, recordRanData, TKContext.getPrincipalId()); 073 } 074 075 @SuppressWarnings("unchecked") 076 @Override 077 public void runAccrual(String principalId, Date startDate, Date endDate, boolean recordRanData, String runAsPrincipalId) { 078 List<LeaveBlock> accrualLeaveBlocks = new ArrayList<LeaveBlock>(); 079 Map<String, BigDecimal> accumulatedAccrualCatToAccrualAmounts = new HashMap<String,BigDecimal>(); 080 Map<String, BigDecimal> accumulatedAccrualCatToNegativeAccrualAmounts = new HashMap<String,BigDecimal>(); 081 082 if (startDate != null && endDate != null) { 083 LOG.info("AccrualServiceImpl.runAccrual() STARTED with Principal: "+principalId+" Start: "+startDate.toString()+" End: "+endDate.toString()); 084 } 085 if(startDate.after(endDate)) { 086 throw new RuntimeException("Start Date " + startDate.toString() + " should not be later than End Date " + endDate.toString()); 087 } 088 //Inactivate all previous accrual-generated entries for this span of time 089 deactivateOldAccruals(principalId, startDate, endDate, runAsPrincipalId); 090 091 //Build a rate range aggregate with appropriate information for this period of time detailing Rate Ranges for job 092 //entries for this range of time 093 RateRangeAggregate rrAggregate = this.buildRateRangeAggregate(principalId, startDate, endDate); 094 PrincipalHRAttributes phra = null; 095 PrincipalHRAttributes endPhra = null; 096 LeavePlan lp = null; 097 List<AccrualCategory> accrCatList = null; 098 099 //Iterate over every day in span 100 Calendar aCal = Calendar.getInstance(); 101 aCal.setTime(startDate); 102 while (!aCal.getTime().after(endDate)) { 103 java.util.Date currentDate = aCal.getTime(); 104 RateRange currentRange = rrAggregate.getRateOnDate(currentDate); 105 if(currentRange == null) { 106 aCal.add(Calendar.DATE, 1); 107 continue; 108 } 109 110 phra = currentRange.getPrincipalHRAttributes(); 111 if(phra == null || TKUtils.removeTime(currentDate).before(TKUtils.removeTime(phra.getServiceDate()))) { 112 aCal.add(Calendar.DATE, 1); 113 continue; 114 } 115 116 117 // use the effectiveDate of this principalHRAttribute to search for inactive entries for this principalId 118 // If there's an inactive entry, it means the job is going to end on the effectiveDate of the inactive entry 119 // used for minimumPercentage and proration 120 endPhra = currentRange.getEndPrincipalHRAttributes(); 121 if(endPhra != null && TKUtils.removeTime(currentDate).after(TKUtils.removeTime(endPhra.getEffectiveDate()))) { 122 aCal.add(Calendar.DATE, 1); 123 continue; 124 } 125 126 // if the date range is before the service date of this employee, do not calculate accrual 127 if(endDate.before(phra.getServiceDate())) { 128 return; 129 } 130 lp = currentRange.getLeavePlan(); 131 accrCatList = currentRange.getAcList(); 132 // if the employee status is changed, create an empty leave block on the currentDate 133 if(currentRange.isStatusChanged()) { 134 this.createEmptyLeaveBlockForStatusChange(principalId, accrualLeaveBlocks, currentDate); 135 } 136 // if no job found for the employee on the currentDate, do nothing 137 if(CollectionUtils.isEmpty(currentRange.getJobs())) { 138 aCal.add(Calendar.DATE, 1); 139 continue; 140 } 141 142 BigDecimal ftePercentage = currentRange.getAccrualRatePercentageModifier(); 143 BigDecimal totalOfStandardHours = currentRange.getStandardHours(); 144 boolean fullFteGranted = false; 145 for(AccrualCategory anAC : accrCatList) { 146 fullFteGranted = false; 147 if(!currentDate.before(phra.getEffectiveDate()) && !anAC.getAccrualEarnInterval().equals("N")) { // "N" means no accrual 148 boolean prorationFlag = this.getProrationFlag(anAC.getProration()); 149 // get the accrual rule 150 AccrualCategoryRule currentAcRule = this.getRuleForAccrualCategory(currentRange.getAcRuleList(), anAC); 151 152 // check if accrual category rule changed 153 if(currentAcRule != null) { 154 java.util.Date ruleStartDate = getRuleStartDate(currentAcRule.getServiceUnitOfTime(), phra.getServiceDate(), currentAcRule.getStart()); 155 Date ruleStartSqlDate = new java.sql.Date(ruleStartDate.getTime()); 156 java.util.Date previousIntervalDay = this.getPrevIntervalDate(ruleStartSqlDate, anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap()); 157 java.util.Date nextIntervalDay = this.getNextIntervalDate(ruleStartSqlDate, anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap()); 158 159 RateRange previousRange = rrAggregate.getRateOnDate(previousIntervalDay); 160 AccrualCategoryRule previousAcRule = null; 161 if(previousRange != null) { 162 previousAcRule = this.getRuleForAccrualCategory(previousRange.getAcRuleList(), anAC); 163 } 164 // rule changed 165 if(previousAcRule != null && !previousAcRule.getLmAccrualCategoryRuleId().equals(currentAcRule.getLmAccrualCategoryRuleId())) { 166 if(TKUtils.removeTime(currentDate).compareTo(TKUtils.removeTime(previousIntervalDay)) >= 0 167 && TKUtils.removeTime(currentDate).compareTo(TKUtils.removeTime(nextIntervalDay)) <= 0) { 168 int workDaysInBetween = TKUtils.getWorkDays(ruleStartSqlDate, nextIntervalDay); 169 boolean minReachedFlag = minimumPercentageReachedForPayPeriod(anAC.getMinPercentWorked(), 170 anAC.getAccrualEarnInterval(), workDaysInBetween, new java.sql.Date(nextIntervalDay.getTime()), 171 phra.getPayCalendar(), rrAggregate.getCalEntryMap()); 172 if(prorationFlag) { 173 if(minReachedFlag) { 174 // min reached, proration=true, rule changed, then use actual work days of currentRule for calculation 175 // so nothing special needs to be done here 176 } else { 177 //minimum percentage NOT reached, proration = true, rule changed, then use previousRule for the whole pay period 178 currentAcRule = previousAcRule; 179 } 180 } else { 181 if(minReachedFlag) { 182 // min reached, proration = false, rule changed, then accrual the whole fte of the new rule for this pay interval 183 accumulatedAccrualCatToAccrualAmounts.put(anAC.getLmAccrualCategoryId(), currentAcRule.getAccrualRate()); 184 fullFteGranted = true; 185 } else { 186 //min NOT reached, proration = false, rule changed, then accrual the whole fte of the previous rule for this pay interval 187 accumulatedAccrualCatToAccrualAmounts.put(anAC.getLmAccrualCategoryId(), previousAcRule.getAccrualRate()); 188 fullFteGranted = true; 189 } 190 } 191 } 192 } 193 } 194 195 // check for first pay period of principal attributes considering minimum percentage and proration 196 java.util.Date firstIntervalDate = this.getNextIntervalDate(phra.getEffectiveDate(), anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap()); 197 if(!TKUtils.removeTime(currentDate).before(TKUtils.removeTime(phra.getEffectiveDate())) 198 && !TKUtils.removeTime(currentDate).after(TKUtils.removeTime(firstIntervalDate))) { 199 int workDaysInBetween = TKUtils.getWorkDays(phra.getEffectiveDate(), firstIntervalDate); 200 boolean minReachedFlag = minimumPercentageReachedForPayPeriod(anAC.getMinPercentWorked(), anAC.getAccrualEarnInterval(), 201 workDaysInBetween, new java.sql.Date(firstIntervalDate.getTime()), 202 phra.getPayCalendar(), rrAggregate.getCalEntryMap()); 203 204 if(prorationFlag) { 205 if(minReachedFlag) { 206 // minimum reached, proration = true, first pay period, then use actual work days of currentRule for calculation 207 // so nothing special needs to be done here 208 } else { 209 // min NOT reached, proration = true, first pay period, then no accrual for this pay period 210 accumulatedAccrualCatToAccrualAmounts.remove(anAC.getLmAccrualCategoryId()); 211 accumulatedAccrualCatToNegativeAccrualAmounts.remove(anAC.getLmAccrualCategoryId()); 212 continue; 213 } 214 } else { 215 if(minReachedFlag) { 216 // minimum reached, proration = false, first pay period, then accrual the whole fte of current AC rule for this pay interval 217 accumulatedAccrualCatToAccrualAmounts.put(anAC.getLmAccrualCategoryId(), currentAcRule.getAccrualRate()); 218 fullFteGranted = true; 219 } else { 220 // min NOT reached, proration = false, first pay period, then no accrual for this pay period 221 accumulatedAccrualCatToAccrualAmounts.remove(anAC.getLmAccrualCategoryId()); 222 accumulatedAccrualCatToNegativeAccrualAmounts.remove(anAC.getLmAccrualCategoryId()); 223 continue; 224 } 225 } 226 } 227 // last accrual interval 228 if(endPhra != null) { // the employment is going to end on the effectiveDate of enPhra 229 java.util.Date previousIntervalDate = this.getPrevIntervalDate(endPhra.getEffectiveDate(), anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap()); 230 // currentDate is between the end date and the last interval date, so we are in the last interval 231 if(!TKUtils.removeTime(currentDate).after(TKUtils.removeTime(endPhra.getEffectiveDate())) 232 && TKUtils.removeTime(currentDate).after(TKUtils.removeTime(previousIntervalDate))) { 233 java.util.Date lastIntervalDate = this.getNextIntervalDate(endPhra.getEffectiveDate(), anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap()); 234 int workDaysInBetween = TKUtils.getWorkDays(previousIntervalDate, endPhra.getEffectiveDate()); 235 boolean minReachedFlag = minimumPercentageReachedForPayPeriod(anAC.getMinPercentWorked(), anAC.getAccrualEarnInterval(), 236 workDaysInBetween, new java.sql.Date(lastIntervalDate.getTime()), 237 phra.getPayCalendar(), rrAggregate.getCalEntryMap()); 238 if(prorationFlag) { 239 if(minReachedFlag) { 240 // minimum reached, proration = true, first pay period, then use actual work days of currentRule for calculation 241 // so nothing special needs to be done here 242 } else { 243 // min NOT reached, proration = true, first pay period, then no accrual for this pay period 244 accumulatedAccrualCatToAccrualAmounts.remove(anAC.getLmAccrualCategoryId()); 245 accumulatedAccrualCatToNegativeAccrualAmounts.remove(anAC.getLmAccrualCategoryId()); 246 continue; 247 } 248 } else { 249 if(minReachedFlag) { 250 // minimum reached, proration = false, first pay period, then accrual the whole fte of current AC rule for this pay interval 251 accumulatedAccrualCatToAccrualAmounts.put(anAC.getLmAccrualCategoryId(), currentAcRule.getAccrualRate()); 252 fullFteGranted = true; 253 } else { 254 // min NOT reached, proration = false, first pay period, then no accrual for this pay period 255 accumulatedAccrualCatToAccrualAmounts.remove(anAC.getLmAccrualCategoryId()); 256 accumulatedAccrualCatToNegativeAccrualAmounts.remove(anAC.getLmAccrualCategoryId()); 257 continue; 258 } 259 } 260 } 261 } 262 263 if(currentAcRule == null) { 264 accumulatedAccrualCatToAccrualAmounts.remove(anAC.getLmAccrualCategoryId()); 265 accumulatedAccrualCatToNegativeAccrualAmounts.remove(anAC.getLmAccrualCategoryId()); 266 continue; 267 } 268 269 // only accrual on work days 270 if(!TKUtils.isWeekend(currentDate) && !fullFteGranted) { 271 BigDecimal accrualRate = currentAcRule.getAccrualRate(); 272 int numberOfWorkDays = this.getWorkDaysInInterval(new java.sql.Date(currentDate.getTime()), anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap()); 273 BigDecimal dayRate = numberOfWorkDays > 0 ? accrualRate.divide(new BigDecimal(numberOfWorkDays), 6, BigDecimal.ROUND_HALF_UP) : new BigDecimal(0); 274 //Fetch the accural rate based on rate range for today(Rate range is the accumulated list of jobs and accrual rate for today) 275 //Add to total accumulatedAccrualCatToAccrualAmounts 276 //use rule and ftePercentage to calculate the hours 277 this.calculateHours(anAC.getLmAccrualCategoryId(), ftePercentage, dayRate, accumulatedAccrualCatToAccrualAmounts); 278 279 //get not eligible for accrual hours based on leave block on this day 280 BigDecimal noAccrualHours = getNotEligibleForAccrualHours(principalId, new java.sql.Date(currentDate.getTime())); 281 282 if(noAccrualHours.compareTo(BigDecimal.ZERO) != 0 && totalOfStandardHours.compareTo(BigDecimal.ZERO) != 0) { 283 BigDecimal dayHours = totalOfStandardHours.divide(new BigDecimal(5), 6, BigDecimal.ROUND_HALF_UP); 284 BigDecimal noAccrualRate = dayRate.multiply(noAccrualHours.divide(dayHours)); 285 this.calculateHours(anAC.getLmAccrualCategoryId(), ftePercentage, noAccrualRate, accumulatedAccrualCatToNegativeAccrualAmounts); 286 } 287 } 288 //Determine if we are at the accrual earn interval in the span, if so add leave block for accumulated accrual amount to list 289 //and reset accumulatedAccrualCatToAccrualAmounts and accumulatedAccrualCatToNegativeAccrualAmounts for this accrual category 290 if(this.isDateAnIntervalDate(currentDate, anAC.getAccrualEarnInterval(), phra.getPayCalendar(), rrAggregate.getCalEntryMap())) { 291 BigDecimal acHours = accumulatedAccrualCatToAccrualAmounts.get(anAC.getLmAccrualCategoryId()); 292 293 if(acHours != null) { 294 createLeaveBlock(principalId, accrualLeaveBlocks, currentDate, acHours, anAC, null, true, currentRange.getLeaveCalendarDocumentId()); 295 accumulatedAccrualCatToAccrualAmounts.remove(anAC.getLmAccrualCategoryId()); // reset accumulatedAccrualCatToAccrualAmounts 296 fullFteGranted = false; 297 } 298 299 BigDecimal adjustmentHours = accumulatedAccrualCatToNegativeAccrualAmounts.get(anAC.getLmAccrualCategoryId()); 300 if(adjustmentHours != null && adjustmentHours.compareTo(BigDecimal.ZERO) != 0) { 301 // do not create leave block if the ajustment amount is 0 302 createLeaveBlock(principalId, accrualLeaveBlocks, currentDate, adjustmentHours, anAC, null, false, currentRange.getLeaveCalendarDocumentId()); 303 accumulatedAccrualCatToNegativeAccrualAmounts.remove(anAC.getLmAccrualCategoryId()); // reset accumulatedAccrualCatToNegativeAccrualAmounts 304 } 305 } 306 } 307 } 308 //Determine if today is a system scheduled time off and accrue holiday if so. 309 SystemScheduledTimeOff ssto = currentRange.getSysScheTimeOff(); 310 if(ssto != null) { 311 AccrualCategory anAC = TkServiceLocator.getAccrualCategoryService().getAccrualCategory(ssto.getAccrualCategory(), ssto.getEffectiveDate()); 312 if(anAC == null) { 313 throw new RuntimeException("Cannot find Accrual Category for system scheduled time off " + ssto.getLmSystemScheduledTimeOffId()); 314 } 315 BigDecimal hrs = ssto.getAmountofTime().multiply(ftePercentage); 316 // system scheduled time off leave block 317 createLeaveBlock(principalId, accrualLeaveBlocks, currentDate, hrs, anAC, ssto.getLmSystemScheduledTimeOffId(), true, currentRange.getLeaveCalendarDocumentId()); 318 // usage leave block with negative amount 319 createLeaveBlock(principalId, accrualLeaveBlocks, currentDate, hrs.negate(), anAC, ssto.getLmSystemScheduledTimeOffId(), true, currentRange.getLeaveCalendarDocumentId()); 320 } 321 // if today is the last day of the employment, create leave blocks if there's any hours available 322 if(endPhra != null && TKUtils.removeTime(currentDate).equals(TKUtils.removeTime(endPhra.getEffectiveDate()))){ 323 // accumulated accrual amount 324 if(!accumulatedAccrualCatToAccrualAmounts.isEmpty()) { 325 for(Map.Entry<String, BigDecimal> entry : accumulatedAccrualCatToAccrualAmounts.entrySet()) { 326 if(entry.getValue() != null && entry.getValue().compareTo(BigDecimal.ZERO) != 0) { 327 AccrualCategory anAC = TkServiceLocator.getAccrualCategoryService().getAccrualCategory(entry.getKey()); 328 createLeaveBlock(principalId, accrualLeaveBlocks, currentDate, entry.getValue(), anAC, null, true, currentRange.getLeaveCalendarDocumentId()); 329 } 330 } 331 accumulatedAccrualCatToAccrualAmounts = new HashMap<String,BigDecimal>(); // reset accumulatedAccrualCatToAccrualAmounts 332 } 333 // negative/adjustment accrual amount 334 if(!accumulatedAccrualCatToNegativeAccrualAmounts.isEmpty()) { 335 for(Map.Entry<String, BigDecimal> entry : accumulatedAccrualCatToNegativeAccrualAmounts.entrySet()) { 336 if(entry.getValue() != null && entry.getValue().compareTo(BigDecimal.ZERO) != 0) { 337 AccrualCategory anAC = TkServiceLocator.getAccrualCategoryService().getAccrualCategory(entry.getKey()); 338 createLeaveBlock(principalId, accrualLeaveBlocks, currentDate, entry.getValue(), anAC, null, true, currentRange.getLeaveCalendarDocumentId()); 339 } 340 } 341 accumulatedAccrualCatToNegativeAccrualAmounts = new HashMap<String,BigDecimal>(); // reset accumulatedAccrualCatToNegativeAccrualAmounts 342 } 343 phra = null; // reset principal attribute so new value will be retrieved 344 endPhra = null; // reset end principal attribute so new value will be retrieved 345 } 346 347 aCal.add(Calendar.DATE, 1); 348 } 349 350 //Save accrual leave blocks at the very end 351 TkServiceLocator.getLeaveBlockService().saveLeaveBlocks(accrualLeaveBlocks); 352 353 // record timestamp of this accrual run in database 354 if(recordRanData) { 355 TkServiceLocator.getPrincipalAccrualRanService().updatePrincipalAccrualRanInfo(principalId); 356 } 357 358 } 359 360 private void deactivateOldAccruals(String principalId, Date startDate, Date endDate, String runAsPrincipalId) { 361 List<LeaveBlock> previousLB = TkServiceLocator.getLeaveBlockService().getAccrualGeneratedLeaveBlocks(principalId, startDate, endDate); 362 List<LeaveBlock> sstoAccrualList = new ArrayList<LeaveBlock>(); 363 List<LeaveBlock> sstoUsageList = new ArrayList<LeaveBlock>(); 364 365 for(LeaveBlock lb : previousLB) { 366 if(StringUtils.isNotEmpty(lb.getScheduleTimeOffId())) { 367 if(lb.getLeaveAmount().compareTo(BigDecimal.ZERO) > 0) { 368 sstoAccrualList.add(lb); 369 } else if(lb.getLeaveAmount().compareTo(BigDecimal.ZERO) < 0) { 370 sstoUsageList.add(lb); 371 } 372 } else { 373 if(!(StringUtils.equals(lb.getLeaveBlockType(),LMConstants.LEAVE_BLOCK_TYPE.BALANCE_TRANSFER) || 374 StringUtils.equals(lb.getLeaveBlockType(),LMConstants.LEAVE_BLOCK_TYPE.LEAVE_PAYOUT))) 375 TkServiceLocator.getLeaveBlockService().deleteLeaveBlock(lb.getLmLeaveBlockId(), runAsPrincipalId); 376 } 377 } 378 379 for(LeaveBlock accrualLb : sstoAccrualList) { 380 for(LeaveBlock usageLb : sstoUsageList) { 381 // both usage and accrual ssto leave blocks are there, so the ssto accural is not banked, removed both leave blocks 382 // if this is no ssto usage leave block, it means the user has banked this ssto hours. Don't delete this ssto accrual leave block 383 if(accrualLb.getScheduleTimeOffId().equals(usageLb.getScheduleTimeOffId())) { 384 TkServiceLocator.getLeaveBlockService().deleteLeaveBlock(accrualLb.getLmLeaveBlockId(), runAsPrincipalId); 385 TkServiceLocator.getLeaveBlockService().deleteLeaveBlock(usageLb.getLmLeaveBlockId(), runAsPrincipalId); 386 } 387 } 388 } 389 390 } 391 392 private BigDecimal getNotEligibleForAccrualHours(String principalId, Date currentDate) { 393 BigDecimal hours = BigDecimal.ZERO; 394 // check if there's any manual not-eligible-for-accrual leave blocks, use the hours of the leave block to adjust accrual calculation 395 List<LeaveBlock> lbs = TkServiceLocator.getLeaveBlockService().getNotAccrualGeneratedLeaveBlocksForDate(principalId, currentDate); 396 for(LeaveBlock lb : lbs) { 397 EarnCode ec = TkServiceLocator.getEarnCodeService().getEarnCode(lb.getEarnCode(), currentDate); 398 if(ec == null) { 399 throw new RuntimeException("Cannot find Earn Code for Leave block " + lb.getLmLeaveBlockId()); 400 } 401 if(ec.getEligibleForAccrual().equals("N") && lb.getLeaveAmount().compareTo(BigDecimal.ZERO) != 0) { 402 hours = hours.add(lb.getLeaveAmount()); 403 } 404 } 405 return hours; 406 } 407 408 private void createLeaveBlock(String principalId, List<LeaveBlock> accrualLeaveBlocks, 409 java.util.Date currentDate, BigDecimal hrs, AccrualCategory anAC, String sysSchTimeOffId, 410 boolean createZeroLeaveBlock, String leaveDocId) { 411 // Replacing Leave Code to earn code - KPME 1634 412 EarnCode ec = TkServiceLocator.getEarnCodeService().getEarnCode(anAC.getEarnCode(), anAC.getEffectiveDate()); 413 if(ec == null) { 414 throw new RuntimeException("Cannot find Earn Code for Accrual category " + anAC.getAccrualCategory()); 415 } 416 // use rounding option and fract time allowed of Leave Code to round the leave block hours 417 BigDecimal roundedHours = TkServiceLocator.getEarnCodeService().roundHrsWithEarnCode(hrs, ec); 418 if(!createZeroLeaveBlock && roundedHours.compareTo(BigDecimal.ZERO) == 0) { 419 return; // do not create leave block with zero amount 420 } 421 LeaveBlock aLeaveBlock = new LeaveBlock(); 422 aLeaveBlock.setAccrualCategory(anAC.getAccrualCategory()); 423 aLeaveBlock.setLeaveDate(new java.sql.Date(currentDate.getTime())); 424 aLeaveBlock.setPrincipalId(principalId); 425 //More than one earn code can be associated with an accrual category. Which one does this get? 426 aLeaveBlock.setEarnCode(anAC.getEarnCode()); 427 aLeaveBlock.setDateAndTime(new Timestamp(currentDate.getTime())); 428 aLeaveBlock.setAccrualGenerated(true); 429 aLeaveBlock.setBlockId(0L); 430 aLeaveBlock.setScheduleTimeOffId(sysSchTimeOffId); 431 aLeaveBlock.setLeaveAmount(roundedHours); 432 aLeaveBlock.setLeaveBlockType(LMConstants.LEAVE_BLOCK_TYPE.ACCRUAL_SERVICE); 433 aLeaveBlock.setRequestStatus(LMConstants.REQUEST_STATUS.APPROVED); 434 aLeaveBlock.setDocumentId(leaveDocId); 435 436 accrualLeaveBlocks.add(aLeaveBlock); 437 438 } 439 440 private void createEmptyLeaveBlockForStatusChange(String principalId, List<LeaveBlock> accrualLeaveBlocks, java.util.Date currentDate) { 441 LeaveBlock aLeaveBlock = new LeaveBlock(); 442 aLeaveBlock.setAccrualCategory(null); 443 aLeaveBlock.setLeaveDate(new java.sql.Date(currentDate.getTime())); 444 aLeaveBlock.setPrincipalId(principalId); 445 aLeaveBlock.setEarnCode(LMConstants.STATUS_CHANGE_EARN_CODE); // fake leave code 446 aLeaveBlock.setDateAndTime(new Timestamp(currentDate.getTime())); 447 aLeaveBlock.setAccrualGenerated(true); 448 aLeaveBlock.setBlockId(0L); 449 aLeaveBlock.setScheduleTimeOffId(null); 450 aLeaveBlock.setLeaveAmount(BigDecimal.ZERO); 451 aLeaveBlock.setLeaveBlockType(LMConstants.LEAVE_BLOCK_TYPE.ACCRUAL_SERVICE); 452 aLeaveBlock.setRequestStatus(LMConstants.REQUEST_STATUS.APPROVED); 453 454 accrualLeaveBlocks.add(aLeaveBlock); 455 456 } 457 458 private void calculateHours(String accrualCategoryId, BigDecimal fte, BigDecimal rate, Map<String, BigDecimal> accumulatedAmounts ) { 459 BigDecimal hours = rate.multiply(fte); 460 BigDecimal oldHours = accumulatedAmounts.get(accrualCategoryId); 461 BigDecimal newHours = oldHours == null ? hours : hours.add(oldHours); 462 accumulatedAmounts.put(accrualCategoryId, newHours); 463 } 464 465 public Date getStartAccrualDate(String principalId){ 466 return null; 467 } 468 469 public Date getEndAccrualDate(String principalId){ 470 //KPME-1246 Fetch planning months 471 472 return null; 473 } 474 475 @Override 476 public void runAccrual(List<String> principalIds) { 477 for(String principalId : principalIds){ 478 runAccrual(principalId); 479 } 480 } 481 482 private boolean isDateAnIntervalDate(java.util.Date aDate, String earnInterval, String payCalName, Map<String, List<CalendarEntries>> aMap) { 483 if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) { 484 return isDateAtPayCalInterval(aDate, earnInterval, payCalName, aMap); 485 } else { 486 return this.isDateAtEarnInterval(aDate, earnInterval); 487 } 488 } 489 490 private boolean isDateAtPayCalInterval(java.util.Date aDate, String earnInterval, String payCalName, Map<String, List<CalendarEntries>> aMap) { 491 if(StringUtils.isNotEmpty(payCalName) 492 && !aMap.isEmpty() 493 && earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) { // only used for ac earn interval == pay calendar 494 List<CalendarEntries> entryList = aMap.get(payCalName); 495 if(CollectionUtils.isNotEmpty(entryList)) { 496 for(CalendarEntries anEntry : entryList) { 497 // endPeriodDate of calendar entry is the beginning hour of the next day, so we need to substract one day from it to get the real end date 498 java.util.Date endDate = TKUtils.addDates(anEntry.getEndPeriodDate(), -1); 499 if(aDate.compareTo(endDate) == 0) { 500 return true; 501 } 502 } 503 } 504 } 505 return false; 506 } 507 508 @Override 509 public boolean isDateAtEarnInterval(java.util.Date aDate, String earnInterval) { 510 boolean atEarnInterval = false; 511 if(LMConstants.ACCRUAL_EARN_INTERVAL_MAP.containsKey(earnInterval)) { 512 Calendar aCal = Calendar.getInstance(); 513 aCal.setTime(aDate); 514 515 if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.DAILY)) { 516 atEarnInterval = true; 517 } else if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.WEEKLY)) { 518 // figure out if the day is a Saturday 519 if(aCal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) { 520 atEarnInterval = true; 521 } 522 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.SEMI_MONTHLY)) { 523 // either the 15th or the last day of the month 524 if(aCal.get(Calendar.DAY_OF_MONTH) == 15 || aCal.get(Calendar.DAY_OF_MONTH) == aCal.getActualMaximum(Calendar.DAY_OF_MONTH)) { 525 atEarnInterval = true; 526 } 527 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.MONTHLY)) { 528 // the last day of the month 529 if(aCal.get(Calendar.DAY_OF_MONTH) == aCal.getActualMaximum(Calendar.DAY_OF_MONTH)) { 530 atEarnInterval = true; 531 } 532 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.YEARLY)) { 533 // the last day of the year 534 if(aCal.get(Calendar.DAY_OF_YEAR) == aCal.getActualMaximum(Calendar.DAY_OF_YEAR)) { 535 atEarnInterval = true; 536 } 537 }else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.NO_ACCRUAL)) { 538 // no calculation 539 } 540 } 541 return atEarnInterval; 542 } 543 544 545 @Override 546 public RateRangeAggregate buildRateRangeAggregate(String principalId, Date startDate, Date endDate) { 547 RateRangeAggregate rrAggregate = new RateRangeAggregate(); 548 List<RateRange> rateRangeList = new ArrayList<RateRange>(); 549 Calendar gc = new GregorianCalendar(); 550 gc.setTime(startDate); 551 // get all active jobs that are effective before the endDate 552 List<Job> activeJobs = TkServiceLocator.getJobService().getAllActiveLeaveJobs(principalId, endDate); 553 List<Job> inactiveJobs = TkServiceLocator.getJobService().getAllInActiveLeaveJobsInRange(principalId, startDate, endDate); 554 555 List<PrincipalHRAttributes> phaList = TkServiceLocator.getPrincipalHRAttributeService().getAllActivePrincipalHrAttributesForPrincipalId(principalId, endDate); 556 List<PrincipalHRAttributes> inactivePhaList = TkServiceLocator.getPrincipalHRAttributeService().getAllInActivePrincipalHrAttributesForPrincipalId(principalId, endDate); 557 558 if(activeJobs.isEmpty() || phaList.isEmpty()) { 559 return rrAggregate; 560 } 561 562 Set<String> phaLpSet = new HashSet<String>(); 563 Set<String> calNameSet = new HashSet<String>(); 564 if(CollectionUtils.isNotEmpty(phaList)) { 565 for(PrincipalHRAttributes pha : phaList) { 566 phaLpSet.add(pha.getLeavePlan()); 567 calNameSet.add(pha.getPayCalendar()); 568 } 569 } 570 571 List<LeavePlan> activeLpList = new ArrayList<LeavePlan> (); 572 List<LeavePlan> inactiveLpList = new ArrayList<LeavePlan> (); 573 for(String lpString : phaLpSet) { 574 List<LeavePlan> aList = TkServiceLocator.getLeavePlanService().getAllActiveLeavePlan(lpString, endDate); 575 activeLpList.addAll(aList); 576 577 aList = TkServiceLocator.getLeavePlanService().getAllInActiveLeavePlan(lpString, endDate); 578 inactiveLpList.addAll(aList); 579 } 580 581 // get all pay calendar entries for this employee. used to determine interval dates 582 Map<String, List<CalendarEntries>> calEntryMap = new HashMap<String, List<CalendarEntries>>(); 583 for(String calName : calNameSet) { 584 org.kuali.hr.time.calendar.Calendar aCal = TkServiceLocator.getCalendarService().getCalendarByGroup(calName); 585 if(aCal != null) { 586 List<CalendarEntries> aList = TkServiceLocator.getCalendarEntriesService().getAllCalendarEntriesForCalendarId(aCal.getHrCalendarId()); 587 Collections.sort(aList); 588 calEntryMap.put(calName, aList); 589 } 590 } 591 rrAggregate.setCalEntryMap(calEntryMap); 592 593 Set<String> lpStringSet = new HashSet<String>(); 594 if(CollectionUtils.isNotEmpty(activeLpList)) { 595 for(LeavePlan lp : activeLpList) { 596 lpStringSet.add(lp.getLeavePlan()); 597 } 598 } 599 List<SystemScheduledTimeOff> sstoList = new ArrayList<SystemScheduledTimeOff>(); 600 for(String lpString : lpStringSet) { 601 List<SystemScheduledTimeOff> aList =TkServiceLocator.getSysSchTimeOffService().getSystemScheduledTimeOffsForLeavePlan(startDate, endDate, lpString); 602 if(CollectionUtils.isNotEmpty(aList)) { 603 sstoList.addAll(aList); 604 } 605 } 606 607 List<AccrualCategory> activeAccrCatList = new ArrayList<AccrualCategory>(); 608 List<AccrualCategory> inactiveAccrCatList = new ArrayList<AccrualCategory>(); 609 for(String lpString : lpStringSet) { 610 List<AccrualCategory> aList = TkServiceLocator.getAccrualCategoryService().getActiveLeaveAccrualCategoriesForLeavePlan(lpString, endDate); 611 if(CollectionUtils.isNotEmpty(aList)) { 612 activeAccrCatList.addAll(aList); 613 } 614 615 aList = TkServiceLocator.getAccrualCategoryService().getInActiveLeaveAccrualCategoriesForLeavePlan(lpString, endDate); 616 if(CollectionUtils.isNotEmpty(aList)) { 617 inactiveAccrCatList.addAll(aList); 618 } 619 } 620 621 List<AccrualCategoryRule> activeRuleList = new ArrayList<AccrualCategoryRule>(); 622 List<AccrualCategoryRule> inactiveRuleList = new ArrayList<AccrualCategoryRule>(); 623 for(AccrualCategory ac : activeAccrCatList) { 624 List<AccrualCategoryRule> aRuleList = TkServiceLocator.getAccrualCategoryRuleService().getActiveRulesForAccrualCategoryId(ac.getLmAccrualCategoryId(), endDate); 625 activeRuleList.addAll(aRuleList); 626 627 aRuleList = TkServiceLocator.getAccrualCategoryRuleService().getInActiveRulesForAccrualCategoryId(ac.getLmAccrualCategoryId(), endDate); 628 inactiveRuleList.addAll(aRuleList); 629 } 630 631 List<LeaveCalendarDocumentHeader> lcDocList = TkServiceLocator.getLeaveCalendarDocumentHeaderService().getAllDocumentHeadersInRangeForPricipalId(principalId, startDate, endDate); 632 633 BigDecimal previousFte = null; 634 List<Job> jobs = new ArrayList<Job>(); 635 636 while (!gc.getTime().after(endDate)) { 637 RateRange rateRange = new RateRange(); 638 java.util.Date currentDate = gc.getTime(); 639 640 jobs = this.getJobsForDate(activeJobs, inactiveJobs, currentDate); 641 if(jobs.isEmpty()) { // no jobs found for this day 642 gc.add(Calendar.DATE, 1); 643 continue; 644 } 645 rateRange.setJobs(jobs); 646 647 // detect if there's a status change 648 BigDecimal fteSum = TkServiceLocator.getJobService().getFteSumForJobs(jobs); 649 rateRange.setAccrualRatePercentageModifier(fteSum); 650 BigDecimal standardHours = TkServiceLocator.getJobService().getStandardHoursSumForJobs(jobs); 651 rateRange.setStandardHours(standardHours); 652 653 if(previousFte != null && !previousFte.equals(fteSum)) { 654 rateRange.setStatusChanged(true); 655 rrAggregate.setRateRangeChanged(true); 656 } 657 previousFte = fteSum; 658 659 // figure out the PrincipalHRAttributes for this day 660 PrincipalHRAttributes phra = this.getPrincipalHrAttributesForDate(phaList, currentDate); 661 rateRange.setPrincipalHRAttributes(phra); 662 663 if(rateRange.getPrincipalHRAttributes() != null) { 664 // figure out if there's an end principalHrAttributes for the initial principalHRAttributes 665 PrincipalHRAttributes endPhra = this.getInactivePrincipalHrAttributesForDate(inactivePhaList, rateRange.getPrincipalHRAttributes().getEffectiveDate(), currentDate); 666 rateRange.setEndPrincipalHRAttributes(endPhra); 667 } 668 669 // get leave plan for this day 670 if(rateRange.getPrincipalHRAttributes()!= null) { 671 rateRange.setLeavePlan(this.getLeavePlanForDate(activeLpList, inactiveLpList, rateRange.getPrincipalHRAttributes().getLeavePlan(), currentDate)); 672 } 673 674 if(rateRange.getLeavePlan() != null) { 675 // get accrual category list for this day 676 List<AccrualCategory> acsForDay = this.getAccrualCategoriesForDate(activeAccrCatList, inactiveAccrCatList, rateRange.getLeavePlan().getLeavePlan(), currentDate); 677 rateRange.setAcList(acsForDay); 678 679 // get System scheduled time off for this day 680 for(SystemScheduledTimeOff ssto : sstoList) { 681 if(TKUtils.removeTime(ssto.getAccruedDate()).equals(TKUtils.removeTime(currentDate) ) 682 && ssto.getLeavePlan().equals(rateRange.getLeavePlan().getLeavePlan())) { 683 // if there exists a ssto accrualed leave block with this ssto id, it means the ssto hours has been banked or transferred by the employee 684 // this logic depends on the deactivateOldAccruals() runs before buildRateRangeAggregate() 685 // because deactivateOldAccruals() removes accrued ssto leave blocks unless they are banked/transferred 686 List<LeaveBlock> sstoLbList = TkServiceLocator.getLeaveBlockService().getSSTOLeaveBlocks(principalId, ssto.getLmSystemScheduledTimeOffId(), ssto.getAccruedDate()); 687 if(CollectionUtils.isEmpty(sstoLbList)) { 688 rateRange.setSysScheTimeOff(ssto); 689 } 690 } 691 } 692 } 693 // set accrual category rules for the day 694 if(CollectionUtils.isNotEmpty(rateRange.getAcList())) { 695 List<AccrualCategoryRule> rulesForDay = new ArrayList<AccrualCategoryRule>(); 696 for(AccrualCategory ac : rateRange.getAcList()) { 697 rulesForDay.addAll(this.getAccrualCategoryRulesForDate 698 (activeRuleList, ac.getLmAccrualCategoryId(), currentDate, rateRange.getPrincipalHRAttributes().getServiceDate())); 699 } 700 rateRange.setAcRuleList(rulesForDay); 701 702 } 703 704 DateTime beginInterval = new DateTime(gc.getTime()); 705 gc.add(Calendar.DATE, 1); 706 DateTime endInterval = new DateTime(gc.getTime()); 707 Interval range = new Interval(beginInterval, endInterval); 708 rateRange.setRange(range); 709 // assign leave document id to range if there is an existing leave doc for currentDate. 710 // The doc Id will be assigned to leave blocks created at this rate range 711 rateRange.setLeaveCalendarDocumentId(this.getLeaveDocumentForDate(lcDocList, currentDate)); 712 rateRangeList.add(rateRange); 713 } 714 rrAggregate.setRateRanges(rateRangeList); 715 rrAggregate.setCurrentRate(null); 716 return rrAggregate; 717 } 718 719 private String getLeaveDocumentForDate(List<LeaveCalendarDocumentHeader> lcDocList, java.util.Date currentDate) { 720 for(LeaveCalendarDocumentHeader lcdh : lcDocList) { 721 if(!lcdh.getBeginDate().after(currentDate) && lcdh.getEndDate().after(currentDate)) { 722 return lcdh.getDocumentId(); 723 } 724 } 725 return ""; 726 } 727 728 public List<Job> getJobsForDate(List<Job> activeJobs, List<Job> inactiveJobs, java.util.Date currentDate) { 729 List<Job> jobs = new ArrayList<Job>(); 730 for(Job aJob : activeJobs) { 731 if(!aJob.getEffectiveDate().after(currentDate)) { 732 jobs.add(aJob); 733 } 734 } 735 if(CollectionUtils.isNotEmpty(jobs)) { 736 List<Job> tempList = new ArrayList<Job>(); 737 tempList.addAll(jobs); 738 for(Job aJob : tempList) { 739 for(Job inactiveJob : inactiveJobs) { 740 if(inactiveJob.getJobNumber().equals(aJob.getJobNumber()) 741 && inactiveJob.getEffectiveDate().after(aJob.getEffectiveDate()) 742 && !inactiveJob.getEffectiveDate().after(currentDate)) { 743 // remove inactive job from list 744 jobs.remove(aJob); 745 } 746 } 747 } 748 } 749 return jobs; 750 } 751 752 public PrincipalHRAttributes getPrincipalHrAttributesForDate(List<PrincipalHRAttributes> activeList, java.util.Date currentDate) { 753 List<PrincipalHRAttributes> phasForDay = new ArrayList<PrincipalHRAttributes>(); 754 for(PrincipalHRAttributes pha : activeList) { 755 if(pha != null && pha.getEffectiveDate() != null && pha.getServiceDate() != null 756 && !pha.getEffectiveDate().after(currentDate) && !pha.getServiceDate().after(currentDate)) { 757 phasForDay.add(pha); 758 } 759 } 760 if(CollectionUtils.isNotEmpty(phasForDay)) { 761 PrincipalHRAttributes pha = phasForDay.get(0); 762 int indexOfMaxEffDt = 0; 763 if(phasForDay.size() > 1) { 764 for(int i = 1; i < phasForDay.size(); i++) { 765 if( (phasForDay.get(i).getEffectiveDate().after(phasForDay.get(indexOfMaxEffDt).getEffectiveDate())) 766 ||(phasForDay.get(i).getEffectiveDate().equals(phasForDay.get(indexOfMaxEffDt).getEffectiveDate()) 767 && phasForDay.get(i).getTimestamp().after(phasForDay.get(indexOfMaxEffDt).getTimestamp()))) { 768 indexOfMaxEffDt = i; 769 } 770 } 771 pha = phasForDay.get(indexOfMaxEffDt); 772 } 773 return pha; 774 } 775 return null; 776 } 777 778 public PrincipalHRAttributes getInactivePrincipalHrAttributesForDate(List<PrincipalHRAttributes> inactiveList, java.util.Date activeDate, java.util.Date currentDate) { 779 List<PrincipalHRAttributes> inactivePhasForDay = new ArrayList<PrincipalHRAttributes>(); 780 for(PrincipalHRAttributes pha : inactiveList) { 781 if( pha.getEffectiveDate().after(activeDate) && !pha.getServiceDate().after(currentDate)) { 782 inactivePhasForDay.add(pha); 783 } 784 } 785 if(CollectionUtils.isNotEmpty(inactivePhasForDay)) { 786 PrincipalHRAttributes pha = inactivePhasForDay.get(0); 787 int indexOfMaxEffDt = 0; 788 if(inactivePhasForDay.size() > 1) { 789 for(int i = 1; i < inactivePhasForDay.size(); i++) { 790 if( (inactivePhasForDay.get(i).getEffectiveDate().after(inactivePhasForDay.get(indexOfMaxEffDt).getEffectiveDate())) 791 ||(inactivePhasForDay.get(i).getEffectiveDate().equals(inactivePhasForDay.get(indexOfMaxEffDt).getEffectiveDate()) 792 && inactivePhasForDay.get(i).getTimestamp().after(inactivePhasForDay.get(indexOfMaxEffDt).getTimestamp()))) { 793 indexOfMaxEffDt = i; 794 } 795 } 796 pha = inactivePhasForDay.get(indexOfMaxEffDt); 797 } 798 return pha; 799 } 800 return null; 801 } 802 803 public LeavePlan getLeavePlanForDate(List<LeavePlan> activeLpList, List<LeavePlan> inactiveLpList, String leavePlan, java.util.Date currentDate) { 804 List<LeavePlan> lpsForDay = new ArrayList<LeavePlan>(); 805 for(LeavePlan lp : activeLpList) { 806 if(lp.getLeavePlan().equals(leavePlan) && !lp.getEffectiveDate().after(currentDate)) { 807 lpsForDay.add(lp); 808 } 809 } 810 List<LeavePlan> aList = new ArrayList<LeavePlan>(); 811 aList.addAll(lpsForDay); 812 for(LeavePlan lp : aList) { 813 for(LeavePlan inactiveLp : inactiveLpList) { 814 if(inactiveLp.getLeavePlan().equals(lp.getLeavePlan()) 815 && inactiveLp.getEffectiveDate().after(lp.getEffectiveDate()) 816 && !inactiveLp.getEffectiveDate().after(currentDate)) { 817 // remove inactive leave plan from list 818 lpsForDay.remove(lp); 819 } 820 } 821 } 822 if(CollectionUtils.isNotEmpty(lpsForDay)) { 823 LeavePlan aLp = lpsForDay.get(0); 824 int indexOfMaxEffDt = 0; 825 if(lpsForDay.size() > 1) { 826 for(int i = 1; i < lpsForDay.size(); i++) { 827 if( (lpsForDay.get(i).getEffectiveDate().after(lpsForDay.get(indexOfMaxEffDt).getEffectiveDate())) 828 ||(lpsForDay.get(i).getEffectiveDate().equals(lpsForDay.get(indexOfMaxEffDt).getEffectiveDate()) 829 && lpsForDay.get(i).getTimestamp().after(lpsForDay.get(indexOfMaxEffDt).getTimestamp()))) { 830 indexOfMaxEffDt = i; 831 } 832 } 833 aLp = lpsForDay.get(indexOfMaxEffDt); 834 } 835 return aLp; 836 } 837 return null; 838 } 839 840 public List<AccrualCategory> getAccrualCategoriesForDate(List<AccrualCategory> activeAccrCatList, List<AccrualCategory> inactiveAccrCatList, String leavePlan, java.util.Date currentDate) { 841 Set<AccrualCategory> aSet = new HashSet<AccrualCategory>(); 842 for(AccrualCategory ac : activeAccrCatList) { 843 if(ac.getLeavePlan().equals(leavePlan) && !ac.getEffectiveDate().after(currentDate)) { 844 aSet.add(ac); 845 } 846 } 847 List<AccrualCategory> list1 = new ArrayList<AccrualCategory>(); 848 list1.addAll(aSet); 849 for(AccrualCategory ac : list1) { 850 for(AccrualCategory inactiveAc : inactiveAccrCatList) { 851 if(inactiveAc.getAccrualCategory().equals(ac.getAccrualCategory()) 852 && inactiveAc.getEffectiveDate().after(ac.getEffectiveDate()) 853 && !inactiveAc.getEffectiveDate().after(currentDate)) { 854 // remove inactive accrual category from list 855 aSet.remove(ac); 856 } 857 } 858 } 859 List<AccrualCategory> acsForDay = new ArrayList<AccrualCategory>(); 860 acsForDay.addAll(aSet); 861 return acsForDay; 862 } 863 864 @Override 865 public boolean isEmpoyeementFutureStatusChanged(String principalId, Date startDate, Date endDate) { 866 Date currentDate = TKUtils.getCurrentDate(); 867 if(endDate.after(currentDate)) { 868 RateRangeAggregate rrAggregate = this.buildRateRangeAggregate(principalId, startDate, endDate); 869 if(rrAggregate.isRateRangeChanged()) { 870 return true; 871 } 872 } 873 return false; 874 } 875 876 @Override 877 public void calculateFutureAccrualUsingPlanningMonth(String principalId, Date asOfDate) { 878 PrincipalHRAttributes phra = TkServiceLocator.getPrincipalHRAttributeService().getPrincipalCalendar(principalId, asOfDate); 879 if(phra != null) { 880 // use the date from pay period to get the leave plan 881 LeavePlan lp = TkServiceLocator.getLeavePlanService().getLeavePlan(phra.getLeavePlan(), asOfDate); 882 if(lp != null && StringUtils.isNotEmpty(lp.getPlanningMonths())) { 883 Calendar aCal = Calendar.getInstance(); 884 // go back a year 885 aCal.setTime(asOfDate); 886 aCal.add(Calendar.YEAR, -1); 887 if(aCal.getActualMaximum(Calendar.DAY_OF_MONTH) < aCal.get(Calendar.DATE)) { 888 aCal.set(Calendar.DATE, aCal.getActualMaximum(Calendar.DAY_OF_MONTH)); 889 } 890 Date startDate = new java.sql.Date(aCal.getTime().getTime()); 891 // go forward using planning months 892 aCal.setTime(asOfDate); 893 aCal.add(Calendar.MONTH, Integer.parseInt(lp.getPlanningMonths())); 894 // max days in months differ, if the date is bigger than the max day, set it to the max day of the month 895 if(aCal.getActualMaximum(Calendar.DAY_OF_MONTH) < aCal.get(Calendar.DATE)) { 896 aCal.set(Calendar.DATE, aCal.getActualMaximum(Calendar.DAY_OF_MONTH)); 897 } 898 Date endDate = new java.sql.Date(aCal.getTime().getTime()); 899 TkServiceLocator.getLeaveAccrualService().runAccrual(principalId, startDate, endDate, true); 900 } 901 } 902 } 903 904 private boolean minimumPercentageReachedForPayPeriod(BigDecimal min, String earnInterval, int workDays, Date intervalDate, String payCalName, Map<String, List<CalendarEntries>> aMap) { 905 if(min == null || min.compareTo(BigDecimal.ZERO) == 0) { 906 return true; 907 } 908 int daysInInterval = this.getWorkDaysInInterval(intervalDate, earnInterval, payCalName, aMap); 909 if(daysInInterval == 0) { 910 return true; 911 } 912 BigDecimal actualPercentage = new BigDecimal(workDays).divide(new BigDecimal(daysInInterval), 2, BigDecimal.ROUND_HALF_EVEN); 913 if(actualPercentage.compareTo(min) >= 0) { 914 return true; 915 } 916 917 return false; 918 } 919 920 private java.util.Date getPrevIntervalDate(Date aDate, String earnInterval, String payCalName, Map<String, List<CalendarEntries>> aMap) { 921 if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) { 922 return this.getPrevPayCalIntervalDate(aDate, earnInterval, payCalName, aMap); 923 } else { 924 return this.getPreviousAccrualIntervalDate(earnInterval, aDate); 925 } 926 } 927 928 @Override 929 public java.util.Date getPreviousAccrualIntervalDate(String earnInterval, Date aDate) { 930 Calendar aCal = Calendar.getInstance(); 931 aCal.setTime(aDate); 932 933 if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.DAILY)) { 934 aCal.add(Calendar.DAY_OF_YEAR, -1); 935 } else if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.WEEKLY)) { 936 aCal.add(Calendar.WEEK_OF_YEAR, -1); 937 aCal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY); // set to the Saturday of previous week 938 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.SEMI_MONTHLY)) { 939 aCal.add(Calendar.DAY_OF_YEAR, -15); 940 if(aCal.get(Calendar.DAY_OF_MONTH) <=15) { 941 aCal.set(Calendar.DAY_OF_MONTH, 15); 942 } else { 943 aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH)); 944 } 945 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.MONTHLY)) { 946 aCal.add(Calendar.MONTH, -1); 947 aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH)); 948 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.YEARLY)) { 949 aCal.add(Calendar.YEAR, -1); 950 aCal.set(Calendar.DAY_OF_YEAR, aCal.getActualMaximum(Calendar.DAY_OF_YEAR)); 951 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.NO_ACCRUAL)) { 952 // no change to calendar 953 } 954 return aCal.getTime(); 955 } 956 957 private java.util.Date getPrevPayCalIntervalDate(java.util.Date aDate, String earnInterval, String payCalName, Map<String, List<CalendarEntries>> aMap) { 958 if(StringUtils.isNotEmpty(payCalName) 959 && !aMap.isEmpty() 960 && earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) { // only used for ac earn interval == pay calendar 961 List<CalendarEntries> entryList = aMap.get(payCalName); 962 if(CollectionUtils.isNotEmpty(entryList)) { 963 for(CalendarEntries anEntry : entryList) { 964 // endPeriodDate of calendar entry is the beginning hour of the next day, so we need to substract one day from it to get the real end date 965 java.util.Date endDate = TKUtils.addDates(anEntry.getEndPeriodDate(), -1); 966 if(anEntry.getBeginPeriodDate().compareTo(aDate) <= 0 && endDate.compareTo(aDate) >= 0) { 967 // the day before the beginning date of the cal entry that contains the passed in date is the endDate of previous calendar entry 968 java.util.Date prevIntvDate = TKUtils.addDates(anEntry.getBeginPeriodDate(), -1); 969 return prevIntvDate; 970 } 971 } 972 } 973 } 974 return aDate; 975 } 976 977 private java.util.Date getNextIntervalDate(Date aDate, String earnInterval, String payCalName, Map<String, List<CalendarEntries>> aMap) { 978 if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) { 979 return this.getNextPayCalIntervalDate(aDate, earnInterval, payCalName, aMap); 980 } else { 981 return this.getNextAccrualIntervalDate(earnInterval, aDate); 982 } 983 } 984 985 private java.util.Date getNextPayCalIntervalDate(Date aDate, String earnInterval, String payCalName, Map<String, List<CalendarEntries>> aMap) { 986 if(StringUtils.isNotEmpty(payCalName) 987 && !aMap.isEmpty() 988 && earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) { // only used for ac earn interval == pay calendar 989 List<CalendarEntries> entryList = aMap.get(payCalName); 990 if(CollectionUtils.isNotEmpty(entryList)) { 991 for(CalendarEntries anEntry : entryList) { 992 // endPeriodDate of calendar entry is the beginning hour of the next day, so we need to substract one day from it to get the real end date 993 java.util.Date endDate = TKUtils.addDates(anEntry.getEndPeriodDate(), -1); 994 if(anEntry.getBeginPeriodDate().compareTo(aDate) <= 0 && endDate.compareTo(aDate) >= 0) { 995 // the endDate of the cal entry that contains the passed in date is the next pay calendar interval date 996 return endDate; 997 } 998 } 999 } 1000 } 1001 return aDate; 1002 } 1003 1004 @Override 1005 public java.util.Date getNextAccrualIntervalDate(String earnInterval, Date aDate) { 1006 Calendar aCal = Calendar.getInstance(); 1007 aCal.setTime(aDate); 1008 1009 if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.DAILY)) { 1010 // no change to calendar 1011 } else if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.WEEKLY)) { 1012 if(aCal.get(Calendar.DAY_OF_WEEK) != Calendar.SATURDAY) { 1013 aCal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY); // set to the Saturday of previous week 1014 } else { 1015 aCal.add(Calendar.WEEK_OF_YEAR, 1); 1016 } 1017 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.SEMI_MONTHLY)) { 1018 if(aCal.get(Calendar.DAY_OF_MONTH) <=15) { 1019 aCal.set(Calendar.DAY_OF_MONTH, 15); 1020 } else { 1021 aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH)); 1022 } 1023 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.MONTHLY)) { 1024 aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH)); 1025 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.YEARLY)) { 1026 aCal.set(Calendar.DAY_OF_YEAR, aCal.getActualMaximum(Calendar.DAY_OF_YEAR)); 1027 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.NO_ACCRUAL)) { 1028 // no change to calendar 1029 } 1030 return aCal.getTime(); 1031 } 1032 1033 private int getWorkDaysInInterval(Date aDate, String earnInterval, String payCalName, Map<String, List<CalendarEntries>> aMap) { 1034 if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) { 1035 return this.getWorkDaysInPayCalInterval(aDate, earnInterval, payCalName, aMap); 1036 } else { 1037 return this.getWorkDaysInAccrualInterval(earnInterval, aDate); 1038 } 1039 } 1040 1041 private int getWorkDaysInPayCalInterval(Date aDate, String earnInterval, String payCalName, Map<String, List<CalendarEntries>> aMap) { 1042 if(StringUtils.isNotEmpty(payCalName) 1043 && !aMap.isEmpty() 1044 && earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.PAY_CAL)) { // only used for ac earn interval == pay calendar 1045 List<CalendarEntries> entryList = aMap.get(payCalName); 1046 if(CollectionUtils.isNotEmpty(entryList)) { 1047 for(CalendarEntries anEntry : entryList) { 1048 // endPeriodDate of calendar entry is the beginning hour of the next day, so we need to substract one day from it to get the real end date 1049 java.util.Date endDate = TKUtils.addDates(anEntry.getEndPeriodDate(), -1); 1050 if(anEntry.getBeginPeriodDate().compareTo(aDate) <= 0 && endDate.compareTo(aDate) >= 0) { 1051 return TKUtils.getWorkDays(anEntry.getBeginPeriodDate(), endDate); 1052 } 1053 } 1054 } 1055 } 1056 return 0; 1057 } 1058 1059 @Override 1060 public int getWorkDaysInAccrualInterval(String earnInterval, Date aDate) { 1061 Calendar aCal = Calendar.getInstance(); 1062 aCal.setTime(aDate); 1063 1064 if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.DAILY)) { 1065 return 1; 1066 } else if(earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.WEEKLY)) { 1067 return 5; 1068 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.SEMI_MONTHLY)) { 1069 if(aCal.get(Calendar.DAY_OF_MONTH) <= 15) { 1070 aCal.set(Calendar.DAY_OF_MONTH, 1); 1071 java.util.Date start = aCal.getTime(); 1072 aCal.set(Calendar.DAY_OF_MONTH, 15); 1073 java.util.Date end = aCal.getTime(); 1074 return TKUtils.getWorkDays(start, end); 1075 } else { 1076 aCal.set(Calendar.DAY_OF_MONTH, 16); 1077 java.util.Date start = aCal.getTime(); 1078 aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH)); 1079 java.util.Date end = aCal.getTime(); 1080 return TKUtils.getWorkDays(start, end); 1081 } 1082 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.MONTHLY)) { 1083 aCal.set(Calendar.DAY_OF_MONTH, 1); 1084 java.util.Date start = aCal.getTime(); 1085 aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH)); 1086 java.util.Date end = aCal.getTime(); 1087 return TKUtils.getWorkDays(start, end); 1088 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.YEARLY)) { 1089 aCal.set(Calendar.DAY_OF_YEAR, 1); 1090 java.util.Date start = aCal.getTime(); 1091 aCal.set(Calendar.DAY_OF_YEAR, aCal.getActualMaximum(Calendar.DAY_OF_YEAR)); 1092 java.util.Date end = aCal.getTime(); 1093 return TKUtils.getWorkDays(start, end); 1094 } else if (earnInterval.equals(LMConstants.ACCRUAL_EARN_INTERVAL_CODE.NO_ACCRUAL)) { 1095 return 0; 1096 } 1097 return 0; 1098 } 1099 1100 public java.util.Date getRuleStartDate(String earnInterval, Date serviceDate, Long startAcc) { 1101 Calendar aCal = Calendar.getInstance(); 1102 aCal.setTime(serviceDate); 1103 String intervalValue = TkConstants.SERVICE_UNIT_OF_TIME.get(earnInterval); 1104 int startInt = startAcc.intValue(); 1105 1106 if (intervalValue.equals("Months")) { 1107 aCal.add(Calendar.MONTH, startInt); 1108 if(aCal.get(Calendar.DAY_OF_MONTH) > aCal.getActualMaximum(Calendar.DAY_OF_MONTH)) { 1109 aCal.set(Calendar.DAY_OF_MONTH, aCal.getActualMaximum(Calendar.DAY_OF_MONTH)); 1110 } 1111 } else if (intervalValue.equals("Years")) { 1112 aCal.set(Calendar.YEAR, startInt); 1113 }else { 1114 // no change to calendar 1115 } 1116 return aCal.getTime(); 1117 } 1118 1119 public boolean getProrationFlag(String proration) { 1120 if(proration == null) { 1121 return true; 1122 } 1123 return proration.equals("Y") ? true : false; 1124 } 1125 1126 @Override 1127 public boolean statusChangedSinceLastRun(String principalId) { 1128 PrincipalAccrualRan par = TkServiceLocator.getPrincipalAccrualRanService().getLastPrincipalAccrualRan(principalId); 1129 if(par == null) { 1130 return true; 1131 } 1132 Job aJob = TkServiceLocator.getJobService().getMaxTimestampJob(principalId); 1133 1134 if(aJob != null && aJob.getTimestamp().after(par.getLastRanTs())) { 1135 return true; 1136 } 1137 1138 Assignment anAssign = TkServiceLocator.getAssignmentService().getMaxTimestampAssignment(principalId); 1139 if(anAssign != null && anAssign.getTimestamp().after(par.getLastRanTs())) { 1140 return true; 1141 } 1142 1143 PrincipalHRAttributes pha = TkServiceLocator.getPrincipalHRAttributeService().getMaxTimeStampPrincipalHRAttributes(principalId); 1144 if(pha != null && pha.getTimestamp().after(par.getLastRanTs())) { 1145 return true; 1146 } 1147 // if there are leave blocks created for earn codes with eligible-for-accrual = no since the last accrual run, it should trigger recalculation 1148 List<LeaveBlock> lbList = TkServiceLocator.getLeaveBlockService().getABELeaveBlocksSinceTime(principalId, par.getLastRanTs()); 1149 if(CollectionUtils.isNotEmpty(lbList)) { 1150 return true; 1151 } 1152 return false; 1153 } 1154 1155 public List<AccrualCategoryRule> getAccrualCategoryRulesForDate(List<AccrualCategoryRule> acrList, String accrualCategoryId, java.util.Date currentDate, java.util.Date serviceDate) { 1156 Calendar startCal = new GregorianCalendar(); 1157 Calendar endCal = new GregorianCalendar(); 1158 List<AccrualCategoryRule> aList = new ArrayList<AccrualCategoryRule>(); 1159 if(CollectionUtils.isNotEmpty(acrList)) { 1160 for(AccrualCategoryRule acr : acrList) { 1161 if(acr.getLmAccrualCategoryId().equals(accrualCategoryId)) { 1162 String uot = acr.getServiceUnitOfTime(); 1163 int startTime = acr.getStart().intValue(); 1164 int endTime = acr.getEnd().intValue(); 1165 1166 startCal.setTime(serviceDate); 1167 endCal.setTime(serviceDate); 1168 if(uot.equals("M")) { // monthly 1169 startCal.add(Calendar.MONTH, startTime); 1170 endCal.add(Calendar.MONTH, endTime); 1171 endCal.add(Calendar.DATE, -1); 1172 } else if(uot.endsWith("Y")) { // yearly 1173 startCal.add(Calendar.YEAR, startTime); 1174 endCal.add(Calendar.YEAR, endTime); 1175 endCal.add(Calendar.DATE, -1); 1176 } 1177 1178 // max days in months differ, if the date is bigger than the max day, set it to the max day of the month 1179 if(startCal.getActualMaximum(Calendar.DAY_OF_MONTH) < startCal.get(Calendar.DATE)) { 1180 startCal.set(Calendar.DATE, startCal.getActualMaximum(Calendar.DAY_OF_MONTH)); 1181 } 1182 if(endCal.getActualMaximum(Calendar.DAY_OF_MONTH) < endCal.get(Calendar.DATE)) { 1183 endCal.set(Calendar.DATE, endCal.getActualMaximum(Calendar.DAY_OF_MONTH)); 1184 } 1185 1186 if(TKUtils.removeTime(currentDate).compareTo(TKUtils.removeTime(startCal.getTime())) >= 0 1187 && TKUtils.removeTime(currentDate).compareTo(TKUtils.removeTime(endCal.getTime())) <=0 ) { 1188 aList.add(acr); 1189 } 1190 } 1191 } 1192 } 1193 return aList; 1194 } 1195 1196 public AccrualCategoryRule getRuleForAccrualCategory(List<AccrualCategoryRule> acrList, AccrualCategory ac) { 1197 if(CollectionUtils.isNotEmpty(acrList)) { 1198 for(AccrualCategoryRule acr : acrList) { 1199 if(acr.getLmAccrualCategoryId().equals(ac.getLmAccrualCategoryId())) { 1200 return acr; 1201 } 1202 } 1203 } 1204 return null; 1205 } 1206 }