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 }