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