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 	private Assignment getIdentifyingKey(TimeBlock block, Date asOfDate, String principalId) {
136 		List<Assignment> lstAssign = TkServiceLocator.getAssignmentService().getAssignments(principalId, asOfDate);
137 
138 		for(Assignment assign : lstAssign){
139 			if((assign.getJobNumber().compareTo(block.getJobNumber()) == 0) && (assign.getWorkArea().compareTo(block.getWorkArea()) == 0)){
140 				return assign;
141 			}
142 		}
143 		return null;
144 	}
145 
146 
147 	public void processDailyOvertimeRules(TimesheetDocument timesheetDocument, TkTimeBlockAggregate timeBlockAggregate){
148 		Map<DailyOvertimeRule, List<Assignment>> mapDailyOvtRulesToAssignment = new HashMap<DailyOvertimeRule, List<Assignment>>();
149 
150 		for(Assignment assignment : timesheetDocument.getAssignments()) {
151 			Job job = assignment.getJob();
152 			DailyOvertimeRule dailyOvertimeRule = getDailyOvertimeRule(job.getLocation(), job.getHrPayType(), job.getDept(), assignment.getWorkArea(), timesheetDocument.getDocEndDate());
153 
154 			if(dailyOvertimeRule !=null) {
155 				if(mapDailyOvtRulesToAssignment.containsKey(dailyOvertimeRule)){
156 					List<Assignment> lstAssign = mapDailyOvtRulesToAssignment.get(dailyOvertimeRule);
157 					lstAssign.add(assignment);
158 					mapDailyOvtRulesToAssignment.put(dailyOvertimeRule, lstAssign);
159 				}  else {
160 					List<Assignment> lstAssign = new ArrayList<Assignment>();
161 					lstAssign.add(assignment);
162 					mapDailyOvtRulesToAssignment.put(dailyOvertimeRule, lstAssign);
163 				}
164 			}
165 		}
166 
167 		//Quick bail
168 		if(mapDailyOvtRulesToAssignment.isEmpty()){
169 			return;
170 		}
171 
172 		// TODO: We iterate Day by Day
173 		for(List<TimeBlock> dayTimeBlocks : timeBlockAggregate.getDayTimeBlockList()){
174 
175 			if (dayTimeBlocks.size() == 0)
176 				continue;
177 
178 			// 1: ... bucketing by (DailyOvertimeRule -> List<TimeBlock>)
179 			Map<DailyOvertimeRule,List<TimeBlock>> dailyOvtRuleToDayTotals = new HashMap<DailyOvertimeRule,List<TimeBlock>>();
180 			for(TimeBlock timeBlock : dayTimeBlocks) {
181 				Assignment assign = this.getIdentifyingKey(timeBlock, timesheetDocument.getAsOfDate(), timesheetDocument.getPrincipalId());
182 				for(Map.Entry<DailyOvertimeRule, List<Assignment>> entry : mapDailyOvtRulesToAssignment.entrySet()){
183 					List<Assignment> lstAssign = entry.getValue();
184 
185                     // for this kind of operation to work, equals() and hashCode() need to
186                     // be over ridden for the object of comparison.
187 					if(lstAssign.contains(assign)){
188                         // comparison here will always work, because we're comparing
189                         // against our existing instantiation of the object.
190 						if(dailyOvtRuleToDayTotals.get(entry.getKey()) != null){
191 							List<TimeBlock> lstTimeBlock = dailyOvtRuleToDayTotals.get(entry.getKey());
192 							lstTimeBlock.add(timeBlock);
193 							dailyOvtRuleToDayTotals.put(entry.getKey(), lstTimeBlock);
194 						} else {
195 							List<TimeBlock> lstTimeBlock = new ArrayList<TimeBlock>();
196 							lstTimeBlock.add(timeBlock);
197 							dailyOvtRuleToDayTotals.put(entry.getKey(), lstTimeBlock);
198 						}
199 					}
200 				}
201 			}
202 
203 			for(DailyOvertimeRule dr : mapDailyOvtRulesToAssignment.keySet() ){
204 				Set<String> fromEarnGroup = TkServiceLocator.getEarnCodeGroupService().getEarnCodeListForEarnCodeGroup(dr.getFromEarnGroup(), TKUtils.getTimelessDate(timesheetDocument.getCalendarEntry().getEndPeriodDateTime()));
205 				List<TimeBlock> blocksForRule = dailyOvtRuleToDayTotals.get(dr);
206 				if (blocksForRule == null || blocksForRule.size() == 0)
207 					continue; // skip to next rule and check for valid blocks.
208 				sortTimeBlocksNatural(blocksForRule);
209 
210 				// 3: Iterate over the timeblocks, apply the rule when necessary.
211 				BigDecimal hours = BigDecimal.ZERO;
212 				List<TimeBlock> applicationList = new LinkedList<TimeBlock>();
213 				TimeBlock previous = null;
214 				for (TimeBlock block : blocksForRule) {
215 					if (exceedsMaxGap(previous, block, dr.getMaxGap())) {
216 						apply(hours, applicationList, dr, fromEarnGroup);
217 						applicationList.clear();
218 						hours = BigDecimal.ZERO;
219 						previous = null; // reset our chain
220 					} else {
221 						previous = block; // build up our chain
222 					}
223                     applicationList.add(block);
224 					for (TimeHourDetail thd : block.getTimeHourDetails())
225 						if (fromEarnGroup.contains(thd.getEarnCode()))
226 							hours = hours.add(thd.getHours(), TkConstants.MATH_CONTEXT);
227 				}
228 				// when we run out of blocks, we may have more to apply.
229 				apply(hours, applicationList, dr, fromEarnGroup);
230 			}
231 		}
232 	}
233 
234 	/**
235 	 * Reverse sorts blocks and applies hours to matching earn codes in the
236 	 * time hour detail entries.
237 	 *
238 	 * @param hours Total number of Daily Overtime Hours to apply.
239 	 * @param blocks Time blocks found to need rule application.
240 	 * @param rule The rule we are applying.
241 	 * @param earnGroup Earn group we've already loaded for this rule.
242 	 */
243 	private void apply(BigDecimal hours, List<TimeBlock> blocks, DailyOvertimeRule rule, Set<String> earnGroup) {
244 		sortTimeBlocksInverse(blocks);
245 		if (blocks != null && blocks.size() > 0)
246 			if (hours.compareTo(rule.getMinHours()) >= 0) {
247                 BigDecimal remaining = hours.subtract(rule.getMinHours(), TkConstants.MATH_CONTEXT);
248 				for (TimeBlock block : blocks) {
249 					remaining = applyOvertimeToTimeBlock(block, rule.getEarnCode(), earnGroup, remaining);
250                 }
251                 if (remaining.compareTo(BigDecimal.ZERO) > 0) {
252                     LOG.warn("Hours remaining that were unapplied in DailyOvertimeRule.");
253                 }
254             }
255 	}
256 
257 
258 	/**
259 	 * Method to apply (if applicable) overtime additions to the indiciated TimeBlock.  TimeBlock
260 	 * earn code is checked against the convertFromEarnCodes Set.
261 	 *
262 	 * @param block
263 	 * @param otEarnCode
264 	 * @param convertFromEarnCodes
265 	 * @param otHours
266 	 *
267 	 * @return The amount of overtime hours remaining to be applied.  (BigDecimal is immutable)
268 	 */
269 	private BigDecimal applyOvertimeToTimeBlock(TimeBlock block, String otEarnCode, Set<String> convertFromEarnCodes, BigDecimal otHours) {
270 		BigDecimal applied = BigDecimal.ZERO;
271 
272 		if (otHours.compareTo(BigDecimal.ZERO) <= 0)
273 			return BigDecimal.ZERO;
274 
275 		List<TimeHourDetail> details = block.getTimeHourDetails();
276 		List<TimeHourDetail> addDetails = new LinkedList<TimeHourDetail>();
277 		for (TimeHourDetail detail : details) {
278 			if (convertFromEarnCodes.contains(detail.getEarnCode())) {
279 				// n = detailHours - otHours
280 				BigDecimal n = detail.getHours().subtract(otHours, TkConstants.MATH_CONTEXT);
281 				// n >= 0 (meaning there are greater than or equal amount of Detail hours vs. OT hours, so apply all OT hours here)
282 				// n < = (meaning there were more OT hours than Detail hours, so apply only the # of hours in detail and update applied.
283 				if (n.compareTo(BigDecimal.ZERO) >= 0) {
284 					// if
285 					applied = otHours;
286 				} else {
287 					applied = detail.getHours();
288 				}
289 
290 				// Make a new TimeHourDetail with the otEarnCode with "applied" hours
291 				TimeHourDetail timeHourDetail = new TimeHourDetail();
292 				timeHourDetail.setHours(applied);
293 				timeHourDetail.setEarnCode(otEarnCode);
294 				timeHourDetail.setTkTimeBlockId(block.getTkTimeBlockId());
295 
296 				// Decrement existing matched FROM earn code.
297 				detail.setHours(detail.getHours().subtract(applied, TkConstants.MATH_CONTEXT));
298 				addDetails.add(timeHourDetail);
299 			}
300 		}
301 
302 		if (addDetails.size() > 0) {
303 			details.addAll(addDetails);
304 			block.setTimeHourDetails(details);
305 		}
306 
307 		return otHours.subtract(applied);
308 	}
309 
310 
311 	// TODO : Refactor this Copy-Pasta mess to util/comparator classes.
312 
313 	private void sortTimeBlocksInverse(List<TimeBlock> blocks) {
314 		Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
315 			public int compare(TimeBlock tb1, TimeBlock tb2) {
316 				if (tb1 != null && tb2 != null)
317 					return -1 * tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
318 				return 0;
319 			}
320 		});
321 	}
322 
323 
324 	private void sortTimeBlocksNatural(List<TimeBlock> blocks) {
325 		Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
326 			public int compare(TimeBlock tb1, TimeBlock tb2) {
327 				if (tb1 != null && tb2 != null)
328 					return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
329 				return 0;
330 			}
331 		});
332 	}
333 
334 	/**
335 	 * Does the difference between the previous time blocks clock out time and the
336 	 * current time blocks clock in time exceed the max gap?
337 	 *
338 	 * @param previous If null, false is returned.
339 	 * @param current
340 	 * @param maxGap
341 	 * @return
342 	 */
343 	boolean exceedsMaxGap(TimeBlock previous, TimeBlock current, BigDecimal maxGap) {
344 		if (previous == null)
345 			return false;
346 
347 		long difference = current.getBeginTimestamp().getTime() - previous.getEndTimestamp().getTime();
348 		BigDecimal gapHours = TKUtils.convertMillisToHours(difference);
349 		BigDecimal cmpGapHrs = TKUtils.convertMinutesToHours(maxGap);
350 		return (gapHours.compareTo(cmpGapHrs) > 0);
351 	}
352 
353 	@Override
354 	public DailyOvertimeRule getDailyOvertimeRule(String tkDailyOvertimeRuleId) {
355 		return dailyOvertimeRuleDao.getDailyOvertimeRule(tkDailyOvertimeRuleId);
356 	}
357 }