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