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.daily.rule.service;
17  
18  import org.apache.log4j.Logger;
19  import org.kuali.hr.job.Job;
20  import org.kuali.hr.time.assignment.Assignment;
21  import org.kuali.hr.time.overtime.daily.rule.DailyOvertimeRule;
22  import org.kuali.hr.time.overtime.daily.rule.dao.DailyOvertimeRuleDao;
23  import org.kuali.hr.time.service.base.TkServiceLocator;
24  import org.kuali.hr.time.timeblock.TimeBlock;
25  import org.kuali.hr.time.timeblock.TimeHourDetail;
26  import org.kuali.hr.time.timesheet.TimesheetDocument;
27  import org.kuali.hr.time.util.TKUtils;
28  import org.kuali.hr.time.util.TkConstants;
29  import org.kuali.hr.time.util.TkTimeBlockAggregate;
30  
31  import java.math.BigDecimal;
32  import java.sql.Date;
33  import java.util.*;
34  
35  public class DailyOvertimeRuleServiceImpl implements DailyOvertimeRuleService {
36  
37      private static final Logger LOG = Logger.getLogger(DailyOvertimeRuleServiceImpl.class);
38  
39  	private DailyOvertimeRuleDao dailyOvertimeRuleDao = null;
40  
41  	public void saveOrUpdate(DailyOvertimeRule dailyOvertimeRule) {
42  		dailyOvertimeRuleDao.saveOrUpdate(dailyOvertimeRule);
43  	}
44  
45  	public void saveOrUpdate(List<DailyOvertimeRule> dailyOvertimeRules) {
46  		dailyOvertimeRuleDao.saveOrUpdate(dailyOvertimeRules);
47  	}
48  
49  	@Override
50  	/**
51  	 * Search for the valid Daily Overtime Rule, wild cards are allowed on
52  	 * location
53  	 * paytype
54  	 * department
55  	 * workArea
56  	 *
57  	 * asOfDate is required.
58  	 */
59  	public DailyOvertimeRule getDailyOvertimeRule(String location, String paytype, String dept, Long workArea, Date asOfDate) {
60  		DailyOvertimeRule dailyOvertimeRule = null;
61  
62  		//		l, p, d, w
63  		if (dailyOvertimeRule == null)
64  			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule(location, paytype, dept, workArea, asOfDate);
65  
66  		//		l, p, d, -1
67  		if (dailyOvertimeRule == null)
68  			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule(location, paytype, dept, -1L, asOfDate);
69  
70  		//		l, p, *, w
71  		if (dailyOvertimeRule == null)
72  			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule(location, paytype, "%", workArea, asOfDate);
73  
74  		//		l, p, *, -1
75  		if (dailyOvertimeRule == null)
76  			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule(location, paytype, "%", -1L, asOfDate);
77  
78  		//		l, *, d, w
79  		if (dailyOvertimeRule == null)
80  			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule(location, "%", dept, workArea, asOfDate);
81  
82  		//		l, *, d, -1
83  		if (dailyOvertimeRule == null)
84  			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule(location, "%", dept, -1L, asOfDate);
85  
86  		//		l, *, *, w
87  		if (dailyOvertimeRule == null)
88  			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule(location, "%", "%", workArea, asOfDate);
89  
90  		//		l, *, *, -1
91  		if (dailyOvertimeRule == null)
92  			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule(location, "%", "%", -1L, asOfDate);
93  
94  		//		*, p, d, w
95  		if (dailyOvertimeRule == null)
96  			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule("%", paytype, dept, workArea, asOfDate);
97  
98  		//		*, p, d, -1
99  		if (dailyOvertimeRule == null)
100 			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule("%", paytype, dept, -1L, asOfDate);
101 
102 		//		*, p, *, w
103 		if (dailyOvertimeRule == null)
104 			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule("%", paytype, "%", workArea, asOfDate);
105 
106 		//		*, p, *, -1
107 		if (dailyOvertimeRule == null)
108 			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule("%", paytype, "%", -1L, asOfDate);
109 
110 		//		*, *, d, w
111 		if (dailyOvertimeRule == null)
112 			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule("%", "%", dept, workArea, asOfDate);
113 
114 		//		*, *, d, -1
115 		if (dailyOvertimeRule == null)
116 			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule("%", "%", dept, -1L, asOfDate);
117 
118 		//		*, *, *, w
119 		if (dailyOvertimeRule == null)
120 			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule("%", "%", "%", workArea, asOfDate);
121 
122 		//		*, *, *, -1
123 		if (dailyOvertimeRule == null)
124 			dailyOvertimeRule = dailyOvertimeRuleDao.findDailyOvertimeRule("%", "%", "%", -1L, asOfDate);
125 
126 		return dailyOvertimeRule;
127 	}
128 
129 
130 
131 	public void setDailyOvertimeRuleDao(DailyOvertimeRuleDao dailyOvertimeRuleDao) {
132 		this.dailyOvertimeRuleDao = dailyOvertimeRuleDao;
133 	}
134 
135 	/**
136 	 * Provides a key used to store and look up Daily Overtime Rules.
137 	 * @param assignment
138 	 * @return
139 	 */
140 	private String getIdentifyingKey(Assignment assignment) {
141 		StringBuffer keybuf = new StringBuffer();
142 		Job job = assignment.getJob();
143 
144 		keybuf.append(job.getLocation()).append('_');
145 		keybuf.append(job.getHrPayType()).append('_');
146 		keybuf.append(job.getDept()).append('_');
147 		keybuf.append(assignment.getWorkArea());
148 
149 		return keybuf.toString();
150 	}
151 
152 	private Assignment getIdentifyingKey(TimeBlock block, Date asOfDate, String principalId) {
153 		List<Assignment> lstAssign = TkServiceLocator.getAssignmentService().getAssignments(principalId, asOfDate);
154 
155 		for(Assignment assign : lstAssign){
156 			if((assign.getJobNumber().compareTo(block.getJobNumber()) == 0) && (assign.getWorkArea().compareTo(block.getWorkArea()) == 0)){
157 				return assign;
158 			}
159 		}
160 		return null;
161 	}
162 
163 
164 	public void processDailyOvertimeRules(TimesheetDocument timesheetDocument, TkTimeBlockAggregate timeBlockAggregate){
165 		Map<DailyOvertimeRule, List<Assignment>> mapDailyOvtRulesToAssignment = new HashMap<DailyOvertimeRule, List<Assignment>>();
166 
167 		for(Assignment assignment : timesheetDocument.getAssignments()) {
168 			Job job = assignment.getJob();
169 			DailyOvertimeRule dailyOvertimeRule = getDailyOvertimeRule(job.getLocation(), job.getHrPayType(), job.getDept(), assignment.getWorkArea(), timesheetDocument.getAsOfDate());
170 
171 			if(dailyOvertimeRule !=null) {
172 				if(mapDailyOvtRulesToAssignment.containsKey(dailyOvertimeRule)){
173 					List<Assignment> lstAssign = mapDailyOvtRulesToAssignment.get(dailyOvertimeRule);
174 					lstAssign.add(assignment);
175 					mapDailyOvtRulesToAssignment.put(dailyOvertimeRule, lstAssign);
176 				}  else {
177 					List<Assignment> lstAssign = new ArrayList<Assignment>();
178 					lstAssign.add(assignment);
179 					mapDailyOvtRulesToAssignment.put(dailyOvertimeRule, lstAssign);
180 				}
181 			}
182 		}
183 
184 		//Quick bail
185 		if(mapDailyOvtRulesToAssignment.isEmpty()){
186 			return;
187 		}
188 
189 		// TODO: We iterate Day by Day
190 		for(List<TimeBlock> dayTimeBlocks : timeBlockAggregate.getDayTimeBlockList()){
191 
192 			if (dayTimeBlocks.size() == 0)
193 				continue;
194 
195 			// 1: ... bucketing by (DailyOvertimeRule -> List<TimeBlock>)
196 			Map<DailyOvertimeRule,List<TimeBlock>> dailyOvtRuleToDayTotals = new HashMap<DailyOvertimeRule,List<TimeBlock>>();
197 			for(TimeBlock timeBlock : dayTimeBlocks) {
198 				Assignment assign = this.getIdentifyingKey(timeBlock, timesheetDocument.getAsOfDate(), timesheetDocument.getPrincipalId());
199 				for(DailyOvertimeRule dr : mapDailyOvtRulesToAssignment.keySet()){
200 					List<Assignment> lstAssign = mapDailyOvtRulesToAssignment.get(dr);
201 
202                     // for this kind of operation to work, equals() and hashCode() need to
203                     // be over ridden for the object of comparison.
204 					if(lstAssign.contains(assign)){
205                         // comparison here will always work, because we're comparing
206                         // against our existing instantiation of the object.
207 						if(dailyOvtRuleToDayTotals.get(dr) != null){
208 							List<TimeBlock> lstTimeBlock = dailyOvtRuleToDayTotals.get(dr);
209 							lstTimeBlock.add(timeBlock);
210 							dailyOvtRuleToDayTotals.put(dr, lstTimeBlock);
211 						} else {
212 							List<TimeBlock> lstTimeBlock = new ArrayList<TimeBlock>();
213 							lstTimeBlock.add(timeBlock);
214 							dailyOvtRuleToDayTotals.put(dr, lstTimeBlock);
215 						}
216 					}
217 				}
218 			}
219 
220 			for(DailyOvertimeRule dr : mapDailyOvtRulesToAssignment.keySet() ){
221 				Set<String> fromEarnGroup = TkServiceLocator.getEarnCodeGroupService().getEarnCodeListForEarnCodeGroup(dr.getFromEarnGroup(), TKUtils.getTimelessDate(timesheetDocument.getCalendarEntry().getEndPeriodDateTime()));
222 				List<TimeBlock> blocksForRule = dailyOvtRuleToDayTotals.get(dr);
223 				if (blocksForRule == null || blocksForRule.size() == 0)
224 					continue; // skip to next rule and check for valid blocks.
225 				sortTimeBlocksNatural(blocksForRule);
226 
227 				// 3: Iterate over the timeblocks, apply the rule when necessary.
228 				BigDecimal hours = BigDecimal.ZERO;
229 				List<TimeBlock> applicationList = new LinkedList<TimeBlock>();
230 				TimeBlock previous = null;
231 				for (TimeBlock block : blocksForRule) {
232 					if (exceedsMaxGap(previous, block, dr.getMaxGap())) {
233 						apply(hours, applicationList, dr, fromEarnGroup);
234 						applicationList.clear();
235 						hours = BigDecimal.ZERO;
236 						previous = null; // reset our chain
237 					} else {
238 						previous = block; // build up our chain
239 					}
240                     applicationList.add(block);
241 					for (TimeHourDetail thd : block.getTimeHourDetails())
242 						if (fromEarnGroup.contains(thd.getEarnCode()))
243 							hours = hours.add(thd.getHours(), TkConstants.MATH_CONTEXT);
244 				}
245 				// when we run out of blocks, we may have more to apply.
246 				apply(hours, applicationList, dr, fromEarnGroup);
247 			}
248 		}
249 	}
250 
251 	/**
252 	 * Reverse sorts blocks and applies hours to matching earn codes in the
253 	 * time hour detail entries.
254 	 *
255 	 * @param hours Total number of Daily Overtime Hours to apply.
256 	 * @param blocks Time blocks found to need rule application.
257 	 * @param rule The rule we are applying.
258 	 * @param earnGroup Earn group we've already loaded for this rule.
259 	 */
260 	private void apply(BigDecimal hours, List<TimeBlock> blocks, DailyOvertimeRule rule, Set<String> earnGroup) {
261 		sortTimeBlocksInverse(blocks);
262 		if (blocks != null && blocks.size() > 0)
263 			if (hours.compareTo(rule.getMinHours()) >= 0) {
264                 BigDecimal remaining = hours.subtract(rule.getMinHours(), TkConstants.MATH_CONTEXT);
265 				for (TimeBlock block : blocks) {
266 					remaining = applyOvertimeToTimeBlock(block, rule.getEarnCode(), earnGroup, remaining);
267                 }
268                 if (remaining.compareTo(BigDecimal.ZERO) > 0) {
269                     LOG.warn("Hours remaining that were unapplied in DailyOvertimeRule.");
270                 }
271             }
272 	}
273 
274 
275 	/**
276 	 * Method to apply (if applicable) overtime additions to the indiciated TimeBlock.  TimeBlock
277 	 * earn code is checked against the convertFromEarnCodes Set.
278 	 *
279 	 * @param block
280 	 * @param otEarnCode
281 	 * @param convertFromEarnCodes
282 	 * @param otHours
283 	 *
284 	 * @return The amount of overtime hours remaining to be applied.  (BigDecimal is immutable)
285 	 */
286 	private BigDecimal applyOvertimeToTimeBlock(TimeBlock block, String otEarnCode, Set<String> convertFromEarnCodes, BigDecimal otHours) {
287 		BigDecimal applied = BigDecimal.ZERO;
288 
289 		if (otHours.compareTo(BigDecimal.ZERO) <= 0)
290 			return BigDecimal.ZERO;
291 
292 		List<TimeHourDetail> details = block.getTimeHourDetails();
293 		List<TimeHourDetail> addDetails = new LinkedList<TimeHourDetail>();
294 		for (TimeHourDetail detail : details) {
295 			if (convertFromEarnCodes.contains(detail.getEarnCode())) {
296 				// n = detailHours - otHours
297 				BigDecimal n = detail.getHours().subtract(otHours, TkConstants.MATH_CONTEXT);
298 				// n >= 0 (meaning there are greater than or equal amount of Detail hours vs. OT hours, so apply all OT hours here)
299 				// n < = (meaning there were more OT hours than Detail hours, so apply only the # of hours in detail and update applied.
300 				if (n.compareTo(BigDecimal.ZERO) >= 0) {
301 					// if
302 					applied = otHours;
303 				} else {
304 					applied = detail.getHours();
305 				}
306 
307 				// Make a new TimeHourDetail with the otEarnCode with "applied" hours
308 				TimeHourDetail timeHourDetail = new TimeHourDetail();
309 				timeHourDetail.setHours(applied);
310 				timeHourDetail.setEarnCode(otEarnCode);
311 				timeHourDetail.setTkTimeBlockId(block.getTkTimeBlockId());
312 
313 				// Decrement existing matched FROM earn code.
314 				detail.setHours(detail.getHours().subtract(applied, TkConstants.MATH_CONTEXT));
315 				addDetails.add(timeHourDetail);
316 			}
317 		}
318 
319 		if (addDetails.size() > 0) {
320 			details.addAll(addDetails);
321 			block.setTimeHourDetails(details);
322 		}
323 
324 		return otHours.subtract(applied);
325 	}
326 
327 
328 	// TODO : Refactor this Copy-Pasta mess to util/comparator classes.
329 
330 	private void sortTimeBlocksInverse(List<TimeBlock> blocks) {
331 		Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
332 			public int compare(TimeBlock tb1, TimeBlock tb2) {
333 				if (tb1 != null && tb2 != null)
334 					return -1 * tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
335 				return 0;
336 			}
337 		});
338 	}
339 
340 
341 	private void sortTimeBlocksNatural(List<TimeBlock> blocks) {
342 		Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
343 			public int compare(TimeBlock tb1, TimeBlock tb2) {
344 				if (tb1 != null && tb2 != null)
345 					return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
346 				return 0;
347 			}
348 		});
349 	}
350 
351 	/**
352 	 * Does the difference between the previous time blocks clock out time and the
353 	 * current time blocks clock in time exceed the max gap?
354 	 *
355 	 * @param previous If null, false is returned.
356 	 * @param current
357 	 * @param maxGap
358 	 * @return
359 	 */
360 	boolean exceedsMaxGap(TimeBlock previous, TimeBlock current, BigDecimal maxGap) {
361 		if (previous == null)
362 			return false;
363 
364 		long difference = current.getBeginTimestamp().getTime() - previous.getEndTimestamp().getTime();
365 		BigDecimal gapHours = TKUtils.convertMillisToHours(difference);
366 		BigDecimal cmpGapHrs = TKUtils.convertMinutesToHours(maxGap);
367 		return (gapHours.compareTo(cmpGapHrs) > 0);
368 	}
369 
370 	@Override
371 	public DailyOvertimeRule getDailyOvertimeRule(String tkDailyOvertimeRuleId) {
372 		return dailyOvertimeRuleDao.getDailyOvertimeRule(tkDailyOvertimeRuleId);
373 	}
374 }