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