View Javadoc

1   /**
2    * Copyright 2004-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.kpme.tklm.time.rules.overtime.weekly.service;
17  
18  import java.math.BigDecimal;
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.Collections;
22  import java.util.Comparator;
23  import java.util.List;
24  import java.util.ListIterator;
25  import java.util.Set;
26  
27  import org.apache.commons.collections.CollectionUtils;
28  import org.apache.commons.lang.ObjectUtils;
29  import org.apache.commons.lang.StringUtils;
30  import org.joda.time.DateTime;
31  import org.joda.time.DateTimeZone;
32  import org.joda.time.LocalDate;
33  import org.kuali.kpme.core.assignment.Assignment;
34  import org.kuali.kpme.core.calendar.entry.CalendarEntry;
35  import org.kuali.kpme.core.earncode.EarnCode;
36  import org.kuali.kpme.core.service.HrServiceLocator;
37  import org.kuali.kpme.core.util.HrConstants;
38  import org.kuali.kpme.core.workarea.WorkArea;
39  import org.kuali.kpme.tklm.leave.block.LeaveBlock;
40  import org.kuali.kpme.tklm.leave.service.LmServiceLocator;
41  import org.kuali.kpme.tklm.time.flsa.FlsaDay;
42  import org.kuali.kpme.tklm.time.flsa.FlsaWeek;
43  import org.kuali.kpme.tklm.time.rules.overtime.weekly.WeeklyOvertimeRule;
44  import org.kuali.kpme.tklm.time.rules.overtime.weekly.dao.WeeklyOvertimeRuleDao;
45  import org.kuali.kpme.tklm.time.service.TkServiceLocator;
46  import org.kuali.kpme.tklm.time.timeblock.TimeBlock;
47  import org.kuali.kpme.tklm.time.timehourdetail.TimeHourDetail;
48  import org.kuali.kpme.tklm.time.timesheet.TimesheetDocument;
49  
50  import org.kuali.kpme.tklm.time.util.TkTimeBlockAggregate;
51  import org.kuali.kpme.tklm.time.workflow.TimesheetDocumentHeader;
52  import org.kuali.rice.krad.service.KRADServiceLocator;
53  
54  public class WeeklyOvertimeRuleServiceImpl implements WeeklyOvertimeRuleService {
55  
56  	private WeeklyOvertimeRuleDao weeklyOvertimeRuleDao;
57  
58  	@Override
59  	public void processWeeklyOvertimeRule(TimesheetDocument timesheetDocument, TkTimeBlockAggregate aggregate) {
60  
61  		LocalDate asOfDate = timesheetDocument.getDocumentHeader().getEndDateTime().toLocalDate();
62  		String principalId = timesheetDocument.getDocumentHeader().getPrincipalId();
63  		DateTime beginDate = timesheetDocument.getDocumentHeader().getBeginDateTime();
64  		DateTime endDate = timesheetDocument.getDocumentHeader().getEndDateTime();
65  		List<WeeklyOvertimeRule> weeklyOvertimeRules = getWeeklyOvertimeRules(asOfDate);
66  
67  		List<List<FlsaWeek>> flsaWeeks = getFlsaWeeks(principalId, beginDate, endDate, aggregate);
68  
69  		for (WeeklyOvertimeRule weeklyOvertimeRule : weeklyOvertimeRules) {
70  			Set<String> maxHoursEarnCodes = HrServiceLocator.getEarnCodeGroupService().getEarnCodeListForEarnCodeGroup(weeklyOvertimeRule.getMaxHoursEarnGroup(), asOfDate);
71  			Set<String> convertFromEarnCodes = HrServiceLocator.getEarnCodeGroupService().getEarnCodeListForEarnCodeGroup(weeklyOvertimeRule.getConvertFromEarnGroup(), asOfDate);
72  
73  			for (List<FlsaWeek> flsaWeekParts : flsaWeeks) {
74  				BigDecimal existingMaxHours = getMaxHours(flsaWeekParts, maxHoursEarnCodes);
75  				BigDecimal newOvertimeHours = existingMaxHours.subtract(weeklyOvertimeRule.getMaxHours(), HrConstants.MATH_CONTEXT);
76  				
77  				if (newOvertimeHours.compareTo(BigDecimal.ZERO) != 0) {
78  					applyOvertimeToFlsaWeeks(flsaWeekParts, weeklyOvertimeRule, asOfDate, convertFromEarnCodes, newOvertimeHours);
79  				}
80  			}
81  		}
82  		
83  		savePreviousNextCalendarTimeBlocks(flsaWeeks);
84  	}
85  	
86  	/**
87  	 * Get the list of all FlsaWeek lists for this period.  An FlsaWeek list is a set of partial FlsaWeeks that overlap a CalendarEntry boundary.  The total of
88  	 * all days in an FlsaWeek list adds up to one complete week.  This is done so WeeklyOvertimeRule calculations are done for multiple Calendar periods.
89  	 * 
90  	 * @param principalId The principal id to apply the rules to
91  	 * @param beginDate The begin date of the CalendarEntry
92  	 * @param endDate The end date of the CalendarEntry
93  	 * @param aggregate The aggregate of the applicable TimeBlocks
94  	 * 
95  	 * @return the list of all FlsaWeek lists for this period
96  	 */
97  	protected List<List<FlsaWeek>> getFlsaWeeks(String principalId, DateTime beginDate, DateTime endDate, TkTimeBlockAggregate aggregate) {
98  		List<List<FlsaWeek>> flsaWeeks = new ArrayList<List<FlsaWeek>>();
99  		
100         DateTimeZone zone = HrServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
101 		List<FlsaWeek> currentWeeks = aggregate.getFlsaWeeks(zone);
102 		
103 		for (ListIterator<FlsaWeek> weekIterator = currentWeeks.listIterator(); weekIterator.hasNext(); ) {
104 			List<FlsaWeek> flsaWeek = new ArrayList<FlsaWeek>();
105 			
106 			int index = weekIterator.nextIndex();
107 			FlsaWeek currentWeek = weekIterator.next();
108 			
109 			if (index == 0 && !currentWeek.isFirstWeekFull()) {
110 				TimesheetDocumentHeader timesheetDocumentHeader = TkServiceLocator.getTimesheetDocumentHeaderService().getPreviousDocumentHeader(principalId, beginDate);
111 				if (timesheetDocumentHeader != null) {
112                     TimesheetDocument timesheetDocument = TkServiceLocator.getTimesheetService().getTimesheetDocument(timesheetDocumentHeader.getDocumentId());
113                     List<String> assignmentKeys = new ArrayList<String>();
114                     for(Assignment assignment : timesheetDocument.getAssignments()) {
115                         assignmentKeys.add(assignment.getAssignmentKey());
116                     }
117 
118                     List<TimeBlock> timeBlocks = TkServiceLocator.getTimeBlockService().getTimeBlocks(timesheetDocumentHeader.getDocumentId());
119                     List<LeaveBlock> leaveBlocks = LmServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(principalId, timesheetDocumentHeader.getBeginDateTime().toLocalDate(), timesheetDocumentHeader.getEndDateTime().toLocalDate(), assignmentKeys);
120                     if (CollectionUtils.isNotEmpty(timeBlocks)) {
121 						CalendarEntry calendarEntry = HrServiceLocator.getCalendarEntryService().getCalendarDatesByPayEndDate(principalId, timesheetDocumentHeader.getEndDateTime(), HrConstants.PAY_CALENDAR_TYPE);
122 						TkTimeBlockAggregate previousAggregate = new TkTimeBlockAggregate(timeBlocks, leaveBlocks, calendarEntry, calendarEntry.getCalendarObj(), true);
123 						List<FlsaWeek> previousWeek = previousAggregate.getFlsaWeeks(zone);
124 						if (CollectionUtils.isNotEmpty(previousWeek)) {
125 							flsaWeek.add(previousWeek.get(previousWeek.size() - 1));
126 						}
127 					}
128 				 }
129 			}
130 			
131 			flsaWeek.add(currentWeek);
132 			
133 			if (index == currentWeeks.size() - 1 && !currentWeek.isLastWeekFull()) {
134 				TimesheetDocumentHeader timesheetDocumentHeader = TkServiceLocator.getTimesheetDocumentHeaderService().getNextDocumentHeader(principalId, endDate);
135 				if (timesheetDocumentHeader != null) {
136                     TimesheetDocument timesheetDocument = TkServiceLocator.getTimesheetService().getTimesheetDocument(timesheetDocumentHeader.getDocumentId());
137                     List<String> assignmentKeys = new ArrayList<String>();
138                     for(Assignment assignment : timesheetDocument.getAssignments()) {
139                         assignmentKeys.add(assignment.getAssignmentKey());
140                     }
141 					List<TimeBlock> timeBlocks = TkServiceLocator.getTimeBlockService().getTimeBlocks(timesheetDocumentHeader.getDocumentId());
142                     List<LeaveBlock> leaveBlocks = LmServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(principalId, timesheetDocumentHeader.getBeginDateTime().toLocalDate(), timesheetDocumentHeader.getEndDateTime().toLocalDate(), assignmentKeys);
143 					if (CollectionUtils.isNotEmpty(timeBlocks)) {
144 						CalendarEntry calendarEntry = HrServiceLocator.getCalendarEntryService().getCalendarDatesByPayEndDate(principalId, timesheetDocumentHeader.getEndDateTime(), HrConstants.PAY_CALENDAR_TYPE);
145 						TkTimeBlockAggregate nextAggregate = new TkTimeBlockAggregate(timeBlocks, leaveBlocks, calendarEntry, calendarEntry.getCalendarObj(), true);
146 						List<FlsaWeek> nextWeek = nextAggregate.getFlsaWeeks(zone);
147 						if (CollectionUtils.isNotEmpty(nextWeek)) {
148 							flsaWeek.add(nextWeek.get(0));
149 						}
150 					}
151 				 }
152 			}
153 			
154 			flsaWeeks.add(flsaWeek);
155 		}
156 		
157 		return flsaWeeks;
158 	}
159 	
160 	/**
161 	 * Get the maximum worked hours for the given FlsaWeeks.
162 	 * 
163 	 * @param flsaWeeks The FlsaWeeks to get the hours from
164 	 * @param maxHoursEarnCodes The EarnCodes used to determine what is applicable as a max hour
165 	 * 
166 	 * @return the maximum worked hours for the given FlsaWeeks
167 	 */
168 	protected BigDecimal getMaxHours(List<FlsaWeek> flsaWeeks, Set<String> maxHoursEarnCodes) {
169 		BigDecimal maxHours = BigDecimal.ZERO;
170 		
171 		for (FlsaWeek flsaWeek : flsaWeeks) {
172 			for (FlsaDay flsaDay : flsaWeek.getFlsaDays()) {
173 				for (TimeBlock timeBlock : flsaDay.getAppliedTimeBlocks()) {
174 					for (TimeHourDetail timeHourDetail : timeBlock.getTimeHourDetails()) {
175 						if (maxHoursEarnCodes.contains(timeHourDetail.getEarnCode())) {
176 							maxHours = maxHours.add(timeHourDetail.getHours(), HrConstants.MATH_CONTEXT);
177 						}
178 					}
179 				}
180                 for (LeaveBlock leaveBlock : flsaDay.getAppliedLeaveBlocks()) {
181                     if (maxHoursEarnCodes.contains(leaveBlock.getEarnCode())) {
182                         maxHours = maxHours.add(leaveBlock.getLeaveAmount().negate());
183                     }
184                 }
185 			}
186 		}
187 		
188 		return maxHours;
189 	}
190 	
191 	/**
192 	 * Returns the overtime EarnCode.  Defaults to the EarnCode on the WeeklyOvertimeRule but overrides with the WorkArea default overtime EarnCode and 
193 	 * the TimeBlock overtime preference EarnCode in that order if they exist.
194 	 * 
195 	 * @param weeklyOvertimeRule The WeeklyOvertimeRule to use when calculating the overtime EarnCode
196 	 * @param timeBlock The TimeBlock to use when calculating the overtime EarnCode
197 	 * @param asOfDate The effectiveDate of the WorkArea
198 	 * 
199 	 * @return the overtime EarnCode
200 	 */
201 	protected String getOvertimeEarnCode(WeeklyOvertimeRule weeklyOvertimeRule, TimeBlock timeBlock, LocalDate asOfDate) {
202         String overtimeEarnCode = weeklyOvertimeRule.getConvertToEarnCode();
203 
204         // KPME-2554 use time block end date instead of passed in asOfDate
205         WorkArea workArea = HrServiceLocator.getWorkAreaService().getWorkArea(timeBlock.getWorkArea(), timeBlock.getEndDateTime().toLocalDate());
206 		if (StringUtils.isNotBlank(workArea.getDefaultOvertimeEarnCode())){
207 			overtimeEarnCode = workArea.getDefaultOvertimeEarnCode();
208 		}
209 		
210         if (StringUtils.isNotEmpty(timeBlock.getOvertimePref())) {
211         	overtimeEarnCode = timeBlock.getOvertimePref();
212         }
213         
214         return overtimeEarnCode;
215 	}
216 	
217 	/**
218 	 * Applies overtime to the given FlsaWeeks.
219 	 * 
220 	 * @param flsaWeeks The FlsaWeeks to apply the overtime to
221 	 * @param weeklyOvertimeRule The WeeklyOvertimeRule in effective
222 	 * @param asOfDate The effectiveDate of the WorkArea
223 	 * @param convertFromEarnCodes The EarnCodes to convert to overtime
224 	 * @param overtimeHours The number of overtime hours to apply
225 	 */
226 	protected void applyOvertimeToFlsaWeeks(List<FlsaWeek> flsaWeeks, WeeklyOvertimeRule weeklyOvertimeRule, LocalDate asOfDate, Set<String> convertFromEarnCodes, BigDecimal overtimeHours) {
227 		List<FlsaDay> flsaDays = getFlsaDays(flsaWeeks);
228 		
229 		if (overtimeHours.compareTo(BigDecimal.ZERO) > 0) {
230 			applyPositiveOvertimeToFlsaWeek(flsaDays, weeklyOvertimeRule, asOfDate, convertFromEarnCodes, overtimeHours);
231 		} else if (overtimeHours.compareTo(BigDecimal.ZERO) < 0) {
232 			applyNegativeOvertimeToFlsaWeek(flsaDays, weeklyOvertimeRule, asOfDate, convertFromEarnCodes, overtimeHours);
233 		}
234 		
235 		removeEmptyOvertime(flsaDays, weeklyOvertimeRule, asOfDate);
236 	}
237 	
238 	/**
239 	 * Gets the list of FlsaDays in the given FlsaWeek.
240 	 * 
241 	 * @param flsaWeeks The FlsaWeek to fetch the FlsaDays from
242 	 * 
243 	 * @return the list of FlsaDays in the given FlsaWeek
244 	 */
245 	protected List<FlsaDay> getFlsaDays(List<FlsaWeek> flsaWeeks) {
246 		List<FlsaDay> flsaDays = new ArrayList<FlsaDay>();
247 		
248 		for (FlsaWeek flsaWeek : flsaWeeks) {
249 			flsaDays.addAll(flsaWeek.getFlsaDays());
250 		}
251 		
252 		return flsaDays;
253 	}
254 	
255 	/**
256 	 * Applies positive overtime to the given FlsaDays.
257 	 * 
258 	 * @param flsaDays The FlsaDays to apply the overtime to
259 	 * @param weeklyOvertimeRule The WeeklyOvertimeRule in effective
260 	 * @param asOfDate The effectiveDate of the WorkArea
261 	 * @param convertFromEarnCodes The EarnCodes to convert to overtime
262 	 * @param overtimeHours The number of overtime hours to apply
263 	 */
264 	protected void applyPositiveOvertimeToFlsaWeek(List<FlsaDay> flsaDays, WeeklyOvertimeRule weeklyOvertimeRule, LocalDate asOfDate, Set<String> convertFromEarnCodes, BigDecimal overtimeHours) {
265 		for (ListIterator<FlsaDay> dayIterator = flsaDays.listIterator(flsaDays.size()); dayIterator.hasPrevious(); ) {
266 			FlsaDay flsaDay = dayIterator.previous();
267 			
268 			List<TimeBlock> timeBlocks = flsaDay.getAppliedTimeBlocks();
269 			Collections.sort(timeBlocks, new Comparator<TimeBlock>() {
270 				public int compare(TimeBlock timeBlock1, TimeBlock timeBlock2) {
271 					return ObjectUtils.compare(timeBlock1.getBeginTimestamp(), timeBlock2.getBeginTimestamp());
272 				}
273 			});
274 
275 			for (ListIterator<TimeBlock> timeBlockIterator = timeBlocks.listIterator(timeBlocks.size()); timeBlockIterator.hasPrevious(); ) {
276 				TimeBlock timeBlock = timeBlockIterator.previous();
277 				String overtimeEarnCode = getOvertimeEarnCode(weeklyOvertimeRule, timeBlock, asOfDate);
278 				overtimeHours = applyPositiveOvertimeOnTimeBlock(timeBlock, overtimeEarnCode, convertFromEarnCodes, overtimeHours);
279 			}
280 			
281 			flsaDay.remapTimeHourDetails();
282 		}
283 	}
284 	
285 	/**
286 	 * Applies negative overtime to the given FlsaDays.
287 	 * 
288 	 * @param flsaDays The FlsaDays to apply the overtime to
289 	 * @param weeklyOvertimeRule The WeeklyOvertimeRule in effective
290 	 * @param asOfDate The effectiveDate of the WorkArea
291 	 * @param convertFromEarnCodes The EarnCodes to convert to overtime
292 	 * @param overtimeHours The number of overtime hours to apply
293 	 */
294 	protected void applyNegativeOvertimeToFlsaWeek(List<FlsaDay> flsaDays, WeeklyOvertimeRule weeklyOvertimeRule, LocalDate asOfDate, Set<String> convertFromEarnCodes, BigDecimal overtimeHours) {
295 		for (ListIterator<FlsaDay> dayIterator = flsaDays.listIterator(); dayIterator.hasNext(); ) {
296 			FlsaDay flsaDay = dayIterator.next();
297 			
298 			List<TimeBlock> timeBlocks = flsaDay.getAppliedTimeBlocks();
299 			Collections.sort(timeBlocks, new Comparator<TimeBlock>() {
300 				public int compare(TimeBlock timeBlock1, TimeBlock timeBlock2) {
301 					return ObjectUtils.compare(timeBlock1.getBeginTimestamp(), timeBlock2.getBeginTimestamp());
302 				}
303 			});
304 
305 			for (ListIterator<TimeBlock> timeBlockIterator = timeBlocks.listIterator(); timeBlockIterator.hasNext(); ) {
306 				TimeBlock timeBlock = timeBlockIterator.next();
307 				String overtimeEarnCode = getOvertimeEarnCode(weeklyOvertimeRule, timeBlock, asOfDate);
308 				overtimeHours = applyNegativeOvertimeOnTimeBlock(timeBlock, overtimeEarnCode, convertFromEarnCodes, overtimeHours);
309 			}
310 			
311 			flsaDay.remapTimeHourDetails();
312 		}
313 	}
314 	
315 	protected void removeEmptyOvertime(List<FlsaDay> flsaDays, WeeklyOvertimeRule weeklyOvertimeRule, LocalDate asOfDate) {
316 		for (ListIterator<FlsaDay> dayIterator = flsaDays.listIterator(); dayIterator.hasNext(); ) {
317 			FlsaDay flsaDay = dayIterator.next();
318 			
319 			List<TimeBlock> timeBlocks = flsaDay.getAppliedTimeBlocks();
320 			for (ListIterator<TimeBlock> timeBlockIterator = timeBlocks.listIterator(); timeBlockIterator.hasNext(); ) {
321 				TimeBlock timeBlock = timeBlockIterator.next();
322 				String overtimeEarnCode = getOvertimeEarnCode(weeklyOvertimeRule, timeBlock, asOfDate);
323 
324 				List<TimeHourDetail> timeHourDetails = timeBlock.getTimeHourDetails();
325 				List<TimeHourDetail> oldTimeHourDetails = new ArrayList<TimeHourDetail>();
326 
327 				TimeHourDetail overtimeTimeHourDetail = getTimeHourDetailByEarnCode(timeHourDetails, Collections.singletonList(overtimeEarnCode));
328 				if (overtimeTimeHourDetail != null) {
329 					if (overtimeTimeHourDetail.getHours().compareTo(BigDecimal.ZERO) == 0) {
330 						oldTimeHourDetails.add(overtimeTimeHourDetail);
331 					}
332 				}
333 				
334 				for (TimeHourDetail oldTimeHourDetail : oldTimeHourDetails) {
335 					timeBlock.removeTimeHourDetail(oldTimeHourDetail);
336 				}
337 			}
338 		}
339 	}
340 
341 	/**
342 	 * Applies overtime additions to the indicated TimeBlock.
343 	 *
344 	 * @param timeBlock The time block to apply the overtime on.
345 	 * @param overtimeEarnCode The overtime earn code to apply overtime to.
346 	 * @param convertFromEarnCodes The other earn codes on the time block.
347 	 * @param overtimeHours The overtime hours to apply.
348 	 *
349 	 * @return the amount of overtime hours remaining to be applied.
350 	 */
351 	protected BigDecimal applyPositiveOvertimeOnTimeBlock(TimeBlock timeBlock, String overtimeEarnCode, Set<String> convertFromEarnCodes, BigDecimal overtimeHours) {
352 		BigDecimal applied = BigDecimal.ZERO;
353 		List<TimeHourDetail> timeHourDetails = timeBlock.getTimeHourDetails();
354 		List<TimeHourDetail> newTimeHourDetails = new ArrayList<TimeHourDetail>();
355 		
356 		for (TimeHourDetail timeHourDetail : timeHourDetails) {
357 			if (convertFromEarnCodes.contains(timeHourDetail.getEarnCode())) {
358 				if (timeHourDetail.getHours().compareTo(overtimeHours) >= 0) {
359 					applied = overtimeHours;
360 				} else {
361 					applied = timeHourDetail.getHours();
362 				}
363 				
364 				EarnCode earnCodeObj = HrServiceLocator.getEarnCodeService().getEarnCode(overtimeEarnCode, timeBlock.getEndDateTime().toLocalDate());
365 				BigDecimal hours = earnCodeObj.getInflateFactor().multiply(applied, HrConstants.MATH_CONTEXT).setScale(HrConstants.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_UP);
366 				
367 				TimeHourDetail overtimeTimeHourDetail = getTimeHourDetailByEarnCode(timeHourDetails, Collections.singletonList(overtimeEarnCode));
368 				if (overtimeTimeHourDetail != null) {
369 					overtimeTimeHourDetail.setHours(overtimeTimeHourDetail.getHours().add(hours, HrConstants.MATH_CONTEXT));
370 				} else {
371 					TimeHourDetail newTimeHourDetail = new TimeHourDetail();
372 					newTimeHourDetail.setTkTimeBlockId(timeBlock.getTkTimeBlockId());
373 					newTimeHourDetail.setEarnCode(overtimeEarnCode);
374 					newTimeHourDetail.setHours(hours);
375 		
376 					newTimeHourDetails.add(newTimeHourDetail);
377 				}
378 				
379 				timeHourDetail.setHours(timeHourDetail.getHours().subtract(applied, HrConstants.MATH_CONTEXT).setScale(HrConstants.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_UP));
380 			}
381 		}
382 		
383 		for (TimeHourDetail newTimeHourDetail : newTimeHourDetails) {
384 			timeBlock.addTimeHourDetail(newTimeHourDetail);
385 		}
386 		
387 		return overtimeHours.subtract(applied);
388 	}
389 	
390 	/**
391 	 * Applies overtime subtractions on the indicated TimeBlock.
392 	 *
393 	 * @param timeBlock The time block to reset the overtime on.
394 	 * @param overtimeEarnCode The overtime earn code to apply overtime to.
395 	 * @param convertFromEarnCodes The other earn codes on the time block.
396 	 * @param overtimeHours The overtime hours to apply.
397 	 *
398 	 * @return the amount of overtime hours remaining to be applied.
399 	 */
400 	protected BigDecimal applyNegativeOvertimeOnTimeBlock(TimeBlock timeBlock, String overtimeEarnCode, Set<String> convertFromEarnCodes, BigDecimal overtimeHours) {
401 		BigDecimal applied = BigDecimal.ZERO;
402 		List<TimeHourDetail> timeHourDetails = timeBlock.getTimeHourDetails();
403 		
404 		for (TimeHourDetail timeHourDetail : timeHourDetails) {
405 			if (convertFromEarnCodes.contains(timeHourDetail.getEarnCode())) {
406 				TimeHourDetail overtimeTimeHourDetail = getTimeHourDetailByEarnCode(timeHourDetails, Collections.singletonList(overtimeEarnCode));
407 			
408 				if (overtimeTimeHourDetail != null) {
409 					applied = overtimeTimeHourDetail.getHours().add(overtimeHours, HrConstants.MATH_CONTEXT);
410 					if (applied.compareTo(BigDecimal.ZERO) >= 0) {
411 						applied = overtimeHours;
412 					} else {
413 						applied = overtimeTimeHourDetail.getHours().negate();
414 					}
415 					
416 					EarnCode earnCodeObj = HrServiceLocator.getEarnCodeService().getEarnCode(overtimeEarnCode, timeBlock.getEndDateTime().toLocalDate());
417 					BigDecimal hours = earnCodeObj.getInflateFactor().multiply(applied, HrConstants.MATH_CONTEXT).setScale(HrConstants.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_DOWN);
418 					
419 					overtimeTimeHourDetail.setHours(overtimeTimeHourDetail.getHours().add(hours, HrConstants.MATH_CONTEXT));
420 					
421 					timeHourDetail.setHours(timeHourDetail.getHours().subtract(applied, HrConstants.MATH_CONTEXT).setScale(HrConstants.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_UP));
422 				}
423 			}
424 		}
425 		
426 		return overtimeHours.subtract(applied);
427 	}
428 	
429 	protected TimeHourDetail getTimeHourDetailByEarnCode(List<TimeHourDetail> timeHourDetails, Collection<String> earnCodes) {
430 		TimeHourDetail result = null;
431 		
432 		for (TimeHourDetail timeHourDetail : timeHourDetails) {
433 			if (earnCodes.contains(timeHourDetail.getEarnCode())) {
434 				result = timeHourDetail;
435 				break;
436 			}
437 		}
438 		
439 		return result;
440 	}
441 	
442 	/**
443 	 * Saves the TimeBlocks from both the previous and next CalendarEntry, since these are not saved automatically by calling methods.
444 	 * 
445 	 * @param flsaWeeks The list of FlsaWeek lists
446 	 */
447 	protected void savePreviousNextCalendarTimeBlocks(List<List<FlsaWeek>> flsaWeeks) {
448 		for (ListIterator<List<FlsaWeek>> weeksIterator = flsaWeeks.listIterator(); weeksIterator.hasNext(); ) {
449 			int index = weeksIterator.nextIndex();
450 			List<FlsaWeek> currentWeekParts = weeksIterator.next();
451 			
452 			if (index == 0 && currentWeekParts.size() > 1) {
453 				FlsaWeek previousFlsaWeek = currentWeekParts.get(0);
454 				for (FlsaDay flsaDay : previousFlsaWeek.getFlsaDays()) {
455 					KRADServiceLocator.getBusinessObjectService().save(flsaDay.getAppliedTimeBlocks());
456 				}
457 			}
458 				
459 			if (index == flsaWeeks.size() - 1 && currentWeekParts.size() > 1) {
460 				FlsaWeek nextFlsaWeek = currentWeekParts.get(currentWeekParts.size() - 1);
461 				for (FlsaDay flsaDay : nextFlsaWeek.getFlsaDays()) {
462 					KRADServiceLocator.getBusinessObjectService().save(flsaDay.getAppliedTimeBlocks());
463 				}
464 			}
465 		}
466 	}
467 
468 	@Override
469 	public List<WeeklyOvertimeRule> getWeeklyOvertimeRules(LocalDate asOfDate) {
470 		return weeklyOvertimeRuleDao.findWeeklyOvertimeRules(asOfDate);
471 	}
472 
473 	@Override
474 	public void saveOrUpdate(WeeklyOvertimeRule weeklyOvertimeRule) {
475 		weeklyOvertimeRuleDao.saveOrUpdate(weeklyOvertimeRule);
476 	}
477 
478 	@Override
479 	public void saveOrUpdate(List<WeeklyOvertimeRule> weeklyOvertimeRules) {
480 		weeklyOvertimeRuleDao.saveOrUpdate(weeklyOvertimeRules);
481 	}
482 
483 	public void setWeeklyOvertimeRuleDao(WeeklyOvertimeRuleDao weeklyOvertimeRuleDao) {
484 		this.weeklyOvertimeRuleDao = weeklyOvertimeRuleDao;
485 	}
486 
487 	@Override
488 	public WeeklyOvertimeRule getWeeklyOvertimeRule(String tkWeeklyOvertimeRuleId) {
489 		return weeklyOvertimeRuleDao.getWeeklyOvertimeRule(tkWeeklyOvertimeRuleId);
490 	}
491 
492 }