View Javadoc

1   /**
2    * Copyright 2004-2012 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 org.apache.commons.lang.StringUtils;
19  import org.joda.time.DateTimeZone;
20  import org.kuali.hr.time.calendar.CalendarEntries;
21  import org.kuali.hr.time.earncode.EarnCode;
22  import org.kuali.hr.time.flsa.FlsaDay;
23  import org.kuali.hr.time.flsa.FlsaWeek;
24  import org.kuali.hr.time.overtime.weekly.rule.WeeklyOvertimeRule;
25  import org.kuali.hr.time.overtime.weekly.rule.dao.WeeklyOvertimeRuleDao;
26  import org.kuali.hr.time.service.base.TkServiceLocator;
27  import org.kuali.hr.time.timeblock.TimeBlock;
28  import org.kuali.hr.time.timeblock.TimeHourDetail;
29  import org.kuali.hr.time.timesheet.TimesheetDocument;
30  import org.kuali.hr.time.util.TKUtils;
31  import org.kuali.hr.time.util.TkConstants;
32  import org.kuali.hr.time.util.TkTimeBlockAggregate;
33  import org.kuali.hr.time.workarea.WorkArea;
34  import org.kuali.hr.time.workflow.TimesheetDocumentHeader;
35  
36  import java.math.BigDecimal;
37  import java.sql.Date;
38  import java.util.*;
39  
40  public class WeeklyOvertimeRuleServiceImpl implements WeeklyOvertimeRuleService {
41  
42  	private WeeklyOvertimeRuleDao weeklyOvertimeRuleDao;
43  
44  	@Override
45  	public void processWeeklyOvertimeRule(TimesheetDocument timesheetDocument, TkTimeBlockAggregate aggregate) {
46          DateTimeZone zone = TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
47  		java.sql.Date asOfDate = TKUtils.getTimelessDate(timesheetDocument.getDocumentHeader().getPayEndDate());
48  		String principalId = timesheetDocument.getDocumentHeader().getPrincipalId();
49  		List<WeeklyOvertimeRule> weeklyOvertimeRules = this.getWeeklyOvertimeRules(asOfDate);
50  
51  		// For the given payperiod, this is our time broken into FLSA weeks.
52  		List<FlsaWeek> flsaWeeks = aggregate.getFlsaWeeks(zone);
53  		List<FlsaWeek> previousWeeks = null;
54  
55  		if (flsaWeeks.size() == 0) {
56  			return;
57  		}
58  
59  		FlsaWeek firstWeek = flsaWeeks.get(0);
60  
61  		// Grab the previous list of FLSA Weeks.
62  		if (!firstWeek.isFirstWeekFull()) {
63  			 List<TimeBlock> prevBlocks = TkServiceLocator.getTimesheetService().getPrevDocumentTimeBlocks(principalId, timesheetDocument.getDocumentHeader().getPayBeginDate());
64  			 if (prevBlocks.size() > 0) {
65  				TimesheetDocumentHeader prevTdh = TkServiceLocator.getTimesheetDocumentHeaderService().getPreviousDocumentHeader(principalId, timesheetDocument.getDocumentHeader().getPayBeginDate());
66  				if (prevTdh != null) {
67  					CalendarEntries prevPayCalendarEntry = TkServiceLocator.getCalendarService().getCalendarDatesByPayEndDate(principalId, prevTdh.getPayEndDate(), null);
68  					TkTimeBlockAggregate prevTimeAggregate = new TkTimeBlockAggregate(prevBlocks, prevPayCalendarEntry, prevPayCalendarEntry.getCalendarObj(), true);
69  					previousWeeks = prevTimeAggregate.getFlsaWeeks(zone);
70  					if (previousWeeks.size() == 0) {
71  						previousWeeks = null;
72  					}
73  				}
74  			 }
75  		}
76  
77  		// Iterate over each Weekly Overtime Rule (We have to grab them all to see if they apply)
78  		for (WeeklyOvertimeRule wor : weeklyOvertimeRules) {
79  			// Grab all the earn codes for the convert from max hours group
80  			Set<String> maxHoursEarnCodes = TkServiceLocator.getEarnGroupService().getEarnCodeListForEarnGroup(wor.getMaxHoursEarnGroup(), asOfDate);
81  			Set<String> convertFromEarnCodes = TkServiceLocator.getEarnGroupService().getEarnCodeListForEarnGroup(wor.getConvertFromEarnGroup(), asOfDate);
82  
83  			// Iterate over the weeks for this Pay Period (FLSA)
84  			//
85  			// Moving Week By week..
86  			for (int i = 0; i < flsaWeeks.size(); i++) {
87  				BigDecimal maxHoursSum = BigDecimal.ZERO; // To consider whether we've met overtime
88  				BigDecimal overtimeHours = BigDecimal.ZERO; // The number of hours to apply
89  
90  				// TODO : Step 1 - get maxHours from previous pay period week if available.
91  				// We have to consider our previous pay period, last week,
92  				// starting from the flsa begin day to the end.
93  				if (i == 0 && previousWeeks != null) {
94  					FlsaWeek previousLastWeek = previousWeeks.get(previousWeeks.size() - 1);
95  					// We know that this week is all admissible, we have already
96  					// filtered by time and days.
97  					for (FlsaDay day : previousLastWeek.getFlsaDays()) {
98  						// figure out our reg hours to count towards this week.
99  						for (String ec : day.getEarnCodeToHours().keySet()) {
100 							if (maxHoursEarnCodes.contains(ec)) {
101 								maxHoursSum = maxHoursSum.add(day.getEarnCodeToHours().get(ec), TkConstants.MATH_CONTEXT);
102 							}
103 						}
104 					}
105 				}
106 
107 				// TODO : Step 2 - Continue Computing Max Hours
108 				FlsaWeek currentWeek = flsaWeeks.get(i);
109 				for (FlsaDay day : currentWeek.getFlsaDays()) {
110 					for (String ec : day.getEarnCodeToHours().keySet()) {
111 						if (maxHoursEarnCodes.contains(ec)) {
112 							maxHoursSum = maxHoursSum.add(day.getEarnCodeToHours().get(ec), TkConstants.MATH_CONTEXT);
113 						}
114 					}
115 				}
116 
117 				// TODO : Compute how many hours to apply
118 				overtimeHours = maxHoursSum.subtract(wor.getMaxHours(), TkConstants.MATH_CONTEXT);
119 				if (overtimeHours.compareTo(BigDecimal.ZERO) <= 0) {
120 					// nothing for this week, move to next week.
121 					continue;
122 				}
123 
124 				// TODO : Step 3 - Reverse Sort current Time Blocks for this week.
125 				List<FlsaDay> daysOfCurrentWeek = currentWeek.getFlsaDays();
126 				if (daysOfCurrentWeek.size() > 0) {
127 					for (int j=daysOfCurrentWeek.size()-1; j >= 0; j--) {
128 						FlsaDay day = daysOfCurrentWeek.get(j);
129 						boolean otApplied = false;
130 
131 						List<TimeBlock> dayBlocks = day.getAppliedTimeBlocks();
132 						Collections.sort(dayBlocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
133 							public int compare(TimeBlock tb1, TimeBlock tb2) {
134 								if (tb1 != null && tb2 != null)
135 									return -1*tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
136 								return 0;
137 							}
138 						});
139 
140 						// Apply OT
141 						for (TimeBlock block : dayBlocks) {
142 							if (overtimeHours.compareTo(BigDecimal.ZERO) > 0) {
143 								String overtimeEarnCode = getOvertimeEarnCode(principalId, block, wor, asOfDate);
144 								overtimeHours = applyOvertimeToTimeBlock(block, overtimeEarnCode, convertFromEarnCodes, overtimeHours);
145 								otApplied = true;
146 							}
147 						}
148 
149 						// ReCalculate FlsaDay Information if necessary.
150 						if (otApplied)
151 							day.remapTimeHourDetails();
152 					}
153 				}
154 			}
155 		}
156 	}
157 
158 	/**
159 	 * If a WorkArea for this timeblock has an overtime preference use that
160 	 * otherwise use the convert to on the rule.
161 	 * @param principalId
162 	 * @param block
163 	 * @param wor
164 	 * @param asOfDate
165 	 * @return
166 	 */
167 	protected String getOvertimeEarnCode(String principalId, TimeBlock block, WeeklyOvertimeRule wor, Date asOfDate) {
168         // if there is an overtime preference, use that ovt earncode
169         if(StringUtils.isNotEmpty(block.getOvertimePref())) {
170             return block.getOvertimePref();
171         }
172         WorkArea workArea = TkServiceLocator.getWorkAreaService().getWorkArea(block.getWorkArea(), asOfDate);
173 		if(StringUtils.isNotBlank(workArea.getDefaultOvertimeEarnCode())){
174 			return workArea.getDefaultOvertimeEarnCode();
175 		}
176 		return wor.getConvertToEarnCode();
177 	}
178 
179 	/**
180 	 * Method to apply (if applicable) overtime additions to the indiciated TimeBlock.  TimeBlock
181 	 * earn code is checked against the convertFromEarnCodes Set.
182 	 *
183 	 * @param block
184 	 * @param otEarnCode
185 	 * @param convertFromEarnCodes
186 	 * @param otHours
187 	 *
188 	 * @return The amount of overtime hours remaining to be applied.  (BigDecimal is immutable)
189 	 */
190 	protected BigDecimal applyOvertimeToTimeBlock(TimeBlock block, String otEarnCode, Set<String> convertFromEarnCodes, BigDecimal otHours) {
191 		BigDecimal applied = BigDecimal.ZERO;
192 		List<TimeHourDetail> details = block.getTimeHourDetails();
193 		List<TimeHourDetail> addDetails = new LinkedList<TimeHourDetail>();
194 		for (TimeHourDetail detail : details) {
195 			// Apply
196 			String thdEarnCode = detail.getEarnCode();
197 			if (convertFromEarnCodes.contains(thdEarnCode)) {
198 				// n = detailHours - otHours
199 				BigDecimal n = detail.getHours().subtract(otHours, TkConstants.MATH_CONTEXT);
200 				// n >= 0 (meaning there are greater than or equal amount of Detail hours vs. OT hours, so apply all OT hours here)
201 				// n < = (meaning there were more OT hours than Detail hours, so apply only the # of hours in detail and update applied.
202 				if (n.compareTo(BigDecimal.ZERO) >= 0) {
203 					// if
204 					applied = otHours;
205 				} else {
206 					applied = detail.getHours();
207 				}
208 
209 				// Make a new TimeHourDetail with the otEarnCode with "applied" hours
210 				TimeHourDetail timeHourDetail = new TimeHourDetail();
211 
212 
213 				EarnCode earnCodeObj = TkServiceLocator.getEarnCodeService().getEarnCode(otEarnCode, block.getEndDate());
214 				BigDecimal hrs = earnCodeObj.getInflateFactor().multiply(applied, TkConstants.MATH_CONTEXT).setScale(TkConstants.BIG_DECIMAL_SCALE,BigDecimal.ROUND_HALF_UP);
215 				timeHourDetail.setEarnCode(otEarnCode);
216 				timeHourDetail.setHours(hrs);
217 				timeHourDetail.setTkTimeBlockId(block.getTkTimeBlockId());
218 
219 				// Decrement existing matched FROM earn code.
220 				detail.setHours(detail.getHours().subtract(applied, TkConstants.MATH_CONTEXT).setScale(TkConstants.BIG_DECIMAL_SCALE,BigDecimal.ROUND_HALF_UP));
221 				addDetails.add(timeHourDetail);
222 			}
223 		}
224 
225 		// If we have new time Hour details to add to the time block, add them.
226 		// We are modifying the data in the list in place, the caller into this service
227 		// will handle the diff / persist.
228 		if (addDetails.size() > 0) {
229 			details.addAll(addDetails);
230 			block.setTimeHourDetails(details);
231 		}
232 
233 		return otHours.subtract(applied);
234 	}
235 
236 	@Override
237 	public List<WeeklyOvertimeRule> getWeeklyOvertimeRules(Date asOfDate) {
238 		return weeklyOvertimeRuleDao.findWeeklyOvertimeRules(asOfDate);
239 	}
240 
241 	@Override
242 	public void saveOrUpdate(WeeklyOvertimeRule weeklyOvertimeRule) {
243 		weeklyOvertimeRuleDao.saveOrUpdate(weeklyOvertimeRule);
244 	}
245 
246 	@Override
247 	public void saveOrUpdate(List<WeeklyOvertimeRule> weeklyOvertimeRules) {
248 		weeklyOvertimeRuleDao.saveOrUpdate(weeklyOvertimeRules);
249 	}
250 
251 	public void setWeeklyOvertimeRuleDao(WeeklyOvertimeRuleDao weeklyOvertimeRuleDao) {
252 		this.weeklyOvertimeRuleDao = weeklyOvertimeRuleDao;
253 	}
254 
255 
256 	@Override
257 	public WeeklyOvertimeRule getWeeklyOvertimeRule(String tkWeeklyOvertimeRuleId) {
258 		return weeklyOvertimeRuleDao.getWeeklyOvertimeRule(tkWeeklyOvertimeRuleId);
259 	}
260 
261 }