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.timesummary.service;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.joda.time.DateTime;
21  import org.joda.time.DateTimeFieldType;
22  import org.joda.time.LocalDateTime;
23  import org.kuali.hr.job.Job;
24  import org.kuali.hr.lm.LMConstants;
25  import org.kuali.hr.lm.leaveSummary.LeaveSummary;
26  import org.kuali.hr.lm.leaveSummary.LeaveSummaryRow;
27  import org.kuali.hr.lm.leaveblock.LeaveBlock;
28  import org.kuali.hr.lm.util.LeaveBlockAggregate;
29  import org.kuali.hr.time.assignment.Assignment;
30  import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
31  import org.kuali.hr.time.calendar.Calendar;
32  import org.kuali.hr.time.calendar.CalendarEntries;
33  import org.kuali.hr.time.earncode.EarnCode;
34  import org.kuali.hr.time.earncodegroup.EarnCodeGroup;
35  import org.kuali.hr.time.flsa.FlsaDay;
36  import org.kuali.hr.time.flsa.FlsaWeek;
37  import org.kuali.hr.time.service.base.TkServiceLocator;
38  import org.kuali.hr.time.timeblock.TimeBlock;
39  import org.kuali.hr.time.timeblock.TimeHourDetail;
40  import org.kuali.hr.time.timesheet.TimesheetDocument;
41  import org.kuali.hr.time.timesummary.AssignmentRow;
42  import org.kuali.hr.time.timesummary.EarnCodeSection;
43  import org.kuali.hr.time.timesummary.EarnGroupSection;
44  import org.kuali.hr.time.timesummary.TimeSummary;
45  import org.kuali.hr.time.util.TKUtils;
46  import org.kuali.hr.time.util.TkConstants;
47  import org.kuali.hr.time.util.TkTimeBlockAggregate;
48  import org.kuali.hr.time.workarea.WorkArea;
49  
50  import java.math.BigDecimal;
51  import java.sql.Timestamp;
52  import java.util.*;
53  
54  public class TimeSummaryServiceImpl implements TimeSummaryService {
55  	private static final String OTHER_EARN_GROUP = "Other";
56  
57      @Override
58  	public TimeSummary getTimeSummary(TimesheetDocument timesheetDocument) {
59  		TimeSummary timeSummary = new TimeSummary();
60  
61  		if(timesheetDocument.getTimeBlocks() == null) {
62  			return timeSummary;
63  		}
64  
65          List<Boolean> dayArrangements = new ArrayList<Boolean>();
66  
67  		timeSummary.setSummaryHeader(getHeaderForSummary(timesheetDocument.getCalendarEntry(), dayArrangements));
68  		TkTimeBlockAggregate tkTimeBlockAggregate = new TkTimeBlockAggregate(timesheetDocument.getTimeBlocks(), timesheetDocument.getCalendarEntry(), TkServiceLocator.getCalendarService().getCalendar(timesheetDocument.getCalendarEntry().getHrCalendarId()), true);
69  
70          List<Assignment> timeAssignments = timesheetDocument.getAssignments();
71          List<String> tAssignmentKeys = new ArrayList<String>();
72          for(Assignment assign : timeAssignments) {
73              tAssignmentKeys.add(assign.getAssignmentKey());
74          }
75          List<LeaveBlock> leaveBlocks =  TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(timesheetDocument.getPrincipalId(),
76                  timesheetDocument.getCalendarEntry().getBeginPeriodDate(), timesheetDocument.getCalendarEntry().getEndPeriodDate(), tAssignmentKeys);
77          LeaveBlockAggregate leaveBlockAggregate = new LeaveBlockAggregate(leaveBlocks, timesheetDocument.getCalendarEntry());
78          tkTimeBlockAggregate = combineTimeAndLeaveAggregates(tkTimeBlockAggregate, leaveBlockAggregate);
79  
80  		timeSummary.setWorkedHours(getWorkedHours(tkTimeBlockAggregate, leaveBlockAggregate));
81  
82          List<EarnGroupSection> earnGroupSections = getEarnGroupSections(tkTimeBlockAggregate, timeSummary.getSummaryHeader().size()+1, 
83          			dayArrangements, timesheetDocument.getAsOfDate(), timesheetDocument.getDocEndDate());
84          timeSummary.setSections(earnGroupSections);
85          
86          try {
87  			List<LeaveSummaryRow> maxedLeaveRows = getMaxedLeaveRows(timesheetDocument.getCalendarEntry(),timesheetDocument.getPrincipalId());
88  			timeSummary.setMaxedLeaveRows(maxedLeaveRows);
89  		} catch (Exception e) {
90  			// TODO Auto-generated catch block
91  			e.printStackTrace();
92  		}
93  
94  		return timeSummary;
95  	}
96  	
97      private List<LeaveSummaryRow> getMaxedLeaveRows(
98  			CalendarEntries calendarEntry, String principalId) throws Exception {
99      	List<LeaveSummaryRow> maxedLeaveRows = new ArrayList<LeaveSummaryRow>();
100     	if (TkServiceLocator.getLeaveApprovalService().isActiveAssignmentFoundOnJobFlsaStatus(principalId, TkConstants.FLSA_STATUS_NON_EXEMPT, true)) {
101         	Map<String,ArrayList<String>> eligibilities = TkServiceLocator.getBalanceTransferService().getEligibleTransfers(calendarEntry,principalId);
102         	Map<String,ArrayList<String>> payouts = TkServiceLocator.getLeavePayoutService().getEligiblePayouts(calendarEntry, principalId);
103         	List<String> onDemandTransfers = eligibilities.get(LMConstants.MAX_BAL_ACTION_FREQ.ON_DEMAND);
104         	onDemandTransfers.addAll(payouts.get(LMConstants.MAX_BAL_ACTION_FREQ.ON_DEMAND));
105         	if(!onDemandTransfers.isEmpty()) {
106             	LeaveSummary summary = TkServiceLocator.getLeaveSummaryService().getLeaveSummary(principalId, calendarEntry);
107             	for(LeaveSummaryRow row : summary.getLeaveSummaryRows()) {
108             		if(onDemandTransfers.contains(row.getAccrualCategoryRuleId()))
109             			maxedLeaveRows.add(row);
110             			
111             	}
112         	}
113     	}
114 		return maxedLeaveRows;
115 	}
116 
117 	/**
118      * Aggregates timeblocks into the appropriate earngroup-> earncode -> assignment rows
119      * @param tkTimeBlockAggregate
120      * @param numEntries
121      * @param dayArrangements
122      * @param asOfDate
123      * @return
124      */
125 	public List<EarnGroupSection> getEarnGroupSections(TkTimeBlockAggregate tkTimeBlockAggregate, int numEntries, List<Boolean> dayArrangements, Date asOfDate , Date docEndDate){
126 		List<EarnGroupSection> earnGroupSections = new ArrayList<EarnGroupSection>();
127 		List<FlsaWeek> flsaWeeks = tkTimeBlockAggregate.getFlsaWeeks(TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback());
128 		Map<String, EarnCodeSection> earnCodeToEarnCodeSection = new HashMap<String, EarnCodeSection>();
129 		Map<String, EarnGroupSection> earnGroupToEarnGroupSection = new HashMap<String, EarnGroupSection>();
130 		
131 		int dayCount = 0;
132 		
133 		//TODO remove this and correct the aggregate .. not sure what the down stream changes are
134 		//so leaving this for initial release
135 		List<FlsaWeek> trimmedFlsaWeeks = new ArrayList<FlsaWeek>();
136 		for(FlsaWeek flsaWeek : flsaWeeks){
137 			if(flsaWeek.getFlsaDays().size() > 0){
138 				trimmedFlsaWeeks.add(flsaWeek);
139 			}
140 		}
141 		
142 		//For every flsa week and day aggegate each time hour detail 
143 		// buckets it by earn code section first
144 		for(FlsaWeek flsaWeek : trimmedFlsaWeeks){
145 			int weekSize = 0;
146 			List<FlsaDay> flsaDays = flsaWeek.getFlsaDays();
147 			for(FlsaDay flsaDay : flsaDays){
148 				Map<String, List<TimeBlock>> earnCodeToTimeBlocks = flsaDay.getEarnCodeToTimeBlocks();
149 				
150 				for(String earnCode : earnCodeToTimeBlocks.keySet()){
151 					for(TimeBlock timeBlock : earnCodeToTimeBlocks.get(earnCode)){
152 						for(TimeHourDetail thd : timeBlock.getTimeHourDetails()){
153 							if(StringUtils.equals(TkConstants.LUNCH_EARN_CODE, thd.getEarnCode())){
154 								continue;
155 							}
156 							EarnCodeSection earnCodeSection = earnCodeToEarnCodeSection.get(thd.getEarnCode());
157 							if(earnCodeSection == null){
158 								earnCodeSection = new EarnCodeSection();
159 								earnCodeSection.setEarnCode(thd.getEarnCode());
160 								EarnCode earnCodeObj = TkServiceLocator.getEarnCodeService().getEarnCode(thd.getEarnCode(), TKUtils.getTimelessDate(asOfDate));
161 								earnCodeSection.setDescription(earnCodeObj.getDescription());
162 								earnCodeSection.setIsAmountEarnCode((earnCodeObj.getRecordMethod()!= null && earnCodeObj.getRecordMethod().equalsIgnoreCase(TkConstants.EARN_CODE_AMOUNT)) ? true : false);
163 								for(int i = 0;i<(numEntries-1);i++){
164 									earnCodeSection.getTotals().add(BigDecimal.ZERO);
165 								}
166 								
167 								earnCodeToEarnCodeSection.put(thd.getEarnCode(), earnCodeSection);
168 							}
169 							String assignKey = timeBlock.getAssignmentKey();
170 							AssignmentRow assignRow = earnCodeSection.getAssignKeyToAssignmentRowMap().get(assignKey);
171 							if(assignRow == null){
172 								assignRow = new AssignmentRow();
173 								assignRow.setAssignmentKey(assignKey);
174 								AssignmentDescriptionKey assignmentKey = TkServiceLocator.getAssignmentService().getAssignmentDescriptionKey(assignKey);
175 								Assignment assignment = TkServiceLocator.getAssignmentService().getAssignment(timeBlock.getPrincipalId(), assignmentKey, TKUtils.getTimelessDate(asOfDate));
176 								// some assignment may not be effective at the beginning of the pay period, use the end date of the period to find it
177 								if(assignment == null) {
178 									assignment = TkServiceLocator.getAssignmentService().getAssignment(timeBlock.getPrincipalId(), assignmentKey, TKUtils.getTimelessDate(docEndDate));
179 								}
180 								//TODO push this up to the assignment fetch/fully populated instead of like this
181 								if(assignment != null){
182 									if(assignment.getJob() == null){
183 										Job aJob = TkServiceLocator.getJobService().getJob(assignment.getPrincipalId(),assignment.getJobNumber(),TKUtils.getTimelessDate(assignment.getEffectiveDate()));
184 										assignment.setJob(aJob);
185 									}
186 									if(assignment.getWorkAreaObj() == null){
187 										WorkArea aWorkArea = TkServiceLocator.getWorkAreaService().getWorkArea(assignment.getWorkArea(), TKUtils.getTimelessDate(assignment.getEffectiveDate()));
188 										assignment.setWorkAreaObj(aWorkArea);
189 									}
190 									assignRow.setDescr(assignment.getAssignmentDescription());
191 								}
192 								for(int i = 0;i<(numEntries-1);i++){
193 									assignRow.getTotal().add(BigDecimal.ZERO);
194 									assignRow.getAmount().add(BigDecimal.ZERO);
195 								}
196 								assignRow.setEarnCodeSection(earnCodeSection);
197 								earnCodeSection.addAssignmentRow(assignRow);
198 							}
199 							assignRow.addToTotal(dayCount, thd.getHours());
200 							assignRow.addToAmount(dayCount, thd.getAmount());
201 						}
202 					}
203 				}
204 				dayCount++;
205 				weekSize++;
206 			}
207 			//end of flsa week accumulate weekly totals
208 			for(EarnCodeSection earnCodeSection : earnCodeToEarnCodeSection.values()){
209 				earnCodeSection.addWeeklyTotal(dayCount, weekSize);
210 			}			
211 			weekSize = 0;
212 
213 			dayCount++;
214 		}
215 		
216 		dayCount = 0;
217 		//now create all teh earn group sections and aggregate accordingly
218 		for(EarnCodeSection earnCodeSection : earnCodeToEarnCodeSection.values()){
219 			String earnCode = earnCodeSection.getEarnCode();
220 			EarnCodeGroup earnGroupObj = TkServiceLocator.getEarnCodeGroupService().getEarnCodeGroupSummaryForEarnCode(earnCode, TKUtils.getTimelessDate(asOfDate));
221 			String earnGroup = null;
222 			if(earnGroupObj == null){
223 				earnGroup = OTHER_EARN_GROUP;
224 			} else{
225 				earnGroup = earnGroupObj.getDescr();
226 			}
227 			
228 			EarnGroupSection earnGroupSection = earnGroupToEarnGroupSection.get(earnGroup);
229 			if(earnGroupSection == null){
230 				earnGroupSection = new EarnGroupSection();
231 				earnGroupSection.setEarnGroup(earnGroup);
232 				for(int i =0;i<(numEntries-1);i++){
233 					earnGroupSection.getTotals().add(BigDecimal.ZERO);
234 				}
235 				earnGroupToEarnGroupSection.put(earnGroup, earnGroupSection);
236 			}
237 			earnGroupSection.addEarnCodeSection(earnCodeSection, dayArrangements);
238 			
239 		}
240 		for(EarnGroupSection earnGroupSection : earnGroupToEarnGroupSection.values()){
241 			earnGroupSections.add(earnGroupSection);
242 		}
243 		return earnGroupSections;
244 	}
245 	
246 	/**
247 	 * Generate a list of string describing this pay calendar entry for the summary
248 	 * @param payCalEntry
249 	 * @return
250 	 */
251 	protected List<String> getSummaryHeader(CalendarEntries payCalEntry){
252 		List<String> summaryHeader = new ArrayList<String>();
253 		int dayCount = 0;
254 		Date beginDateTime = payCalEntry.getBeginPeriodDateTime();
255 		Date endDateTime = payCalEntry.getEndPeriodDateTime();
256 		boolean virtualDays = false;
257         LocalDateTime endDate = payCalEntry.getEndLocalDateTime();
258 
259         if (endDate.get(DateTimeFieldType.hourOfDay()) != 0 || endDate.get(DateTimeFieldType.minuteOfHour()) != 0 ||
260                 endDate.get(DateTimeFieldType.secondOfMinute()) != 0){
261             virtualDays = true;
262         }
263 		
264 		Date currDateTime = beginDateTime;
265 		java.util.Calendar cal = GregorianCalendar.getInstance();
266 		
267 		while(currDateTime.before(endDateTime)){
268 			LocalDateTime currDate = new LocalDateTime(currDateTime);
269 			summaryHeader.add(makeHeaderDiplayString(currDate, virtualDays));
270 			
271 			dayCount++;
272 			if((dayCount % 7) == 0){
273 				summaryHeader.add("Week "+ ((dayCount / 7)));
274 			}
275 			cal.setTime(currDateTime);
276 			cal.add(java.util.Calendar.HOUR, 24);
277 			currDateTime = cal.getTime();
278 		}
279 		
280 		summaryHeader.add("Period Total");
281 		return summaryHeader;
282 	}
283 
284     //kind of a hack
285     private TkTimeBlockAggregate combineTimeAndLeaveAggregates(TkTimeBlockAggregate tbAggregate, LeaveBlockAggregate lbAggregate) {
286         if (tbAggregate != null
287                 && lbAggregate != null
288                 && tbAggregate.getDayTimeBlockList().size() == lbAggregate.getDayLeaveBlockList().size()) {
289             for (int i = 0; i < tbAggregate.getDayTimeBlockList().size(); i++) {
290                 List<LeaveBlock> leaveBlocks = lbAggregate.getDayLeaveBlockList().get(i);
291                 if (CollectionUtils.isNotEmpty(leaveBlocks)) {
292                     for (LeaveBlock lb : leaveBlocks) {
293                         //convert leave block to generic time block and add to list
294                         //conveniently, we only really need the hours amount
295                         TimeBlock timeBlock = new TimeBlock();
296                         timeBlock.setHours(lb.getLeaveAmount().negate());
297                         timeBlock.setBeginTimestamp(new Timestamp(lb.getLeaveDate().getTime()));
298                         timeBlock.setEndTimestamp(new Timestamp(new DateTime(lb.getLeaveDate()).plusMinutes(timeBlock.getHours().intValue()).getMillis()));
299                         timeBlock.setAssignmentKey(lb.getAssignmentKey());
300                         timeBlock.setEarnCode(lb.getEarnCode());
301                         timeBlock.setPrincipalId(lb.getPrincipalId());
302                         timeBlock.setWorkArea(lb.getWorkArea());
303                         TimeHourDetail timeHourDetail = new TimeHourDetail();
304                         timeHourDetail.setEarnCode(timeBlock.getEarnCode());
305                         timeHourDetail.setHours(timeBlock.getHours());
306                         timeHourDetail.setAmount(BigDecimal.ZERO);
307                         timeBlock.addTimeHourDetail(timeHourDetail);
308                         tbAggregate.getDayTimeBlockList().get(i).add(timeBlock);
309                     }
310                 }
311 
312             }
313         }
314         return tbAggregate;
315     }
316 
317 
318 
319     /**
320      * Provides the number of hours worked for the pay period indicated in the
321      * aggregate.
322      *
323      * @param aggregate The aggregate we are summing
324      *
325      * @return A list of BigDecimals containing the number of hours worked.
326      * This list will line up with the header.
327      */
328     private List<BigDecimal> getWorkedHours(TkTimeBlockAggregate aggregate, LeaveBlockAggregate lbAggregate) {
329         List<BigDecimal> hours = new ArrayList<BigDecimal>();
330         BigDecimal periodTotal = TkConstants.BIG_DECIMAL_SCALED_ZERO;
331         for (FlsaWeek week : aggregate.getFlsaWeeks(TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback())) {
332             BigDecimal weeklyTotal = TkConstants.BIG_DECIMAL_SCALED_ZERO;
333             for (FlsaDay day : week.getFlsaDays()) {
334                 BigDecimal totalForDay = TkConstants.BIG_DECIMAL_SCALED_ZERO;
335                 for (TimeBlock block : day.getAppliedTimeBlocks()) {
336                     totalForDay = totalForDay.add(block.getHours(), TkConstants.MATH_CONTEXT);
337                     weeklyTotal = weeklyTotal.add(block.getHours(), TkConstants.MATH_CONTEXT);
338                     periodTotal = periodTotal.add(block.getHours(), TkConstants.MATH_CONTEXT);
339                 }
340                 hours.add(totalForDay);
341             }
342             hours.add(weeklyTotal);
343         }
344         hours.add(periodTotal);
345 
346         return hours;
347     }
348 
349 
350     /**
351      * Handles the generation of the display header for the time summary.
352      *
353      * @param cal The PayCalendarEntries object we are using to derive information.
354      * @param dayArrangements Container passed in to store the position of week / period aggregate sums
355      *
356      * @return An in-order string of days for this period that properly accounts
357      * for FLSA week boundaries in the pay period.
358      */
359     @Override
360     public List<String> getHeaderForSummary(CalendarEntries cal, List<Boolean> dayArrangements) {
361         List<String> header = new ArrayList<String>();
362 
363         // Maps directly to joda time day of week constants.
364         int flsaBeginDay = this.getPayCalendarForEntry(cal).getFlsaBeginDayConstant();
365         boolean virtualDays = false;
366         LocalDateTime startDate = cal.getBeginLocalDateTime();
367         LocalDateTime endDate = cal.getEndLocalDateTime();
368 
369         // Increment end date if we are on a virtual day calendar, so that the
370         // for loop can account for having the proper amount of days on the
371         // summary calendar.
372         if (endDate.get(DateTimeFieldType.hourOfDay()) != 0 || endDate.get(DateTimeFieldType.minuteOfHour()) != 0 ||
373                 endDate.get(DateTimeFieldType.secondOfMinute()) != 0)
374         {
375             endDate = endDate.plusDays(1);
376             virtualDays = true;
377         }
378 
379         boolean afterFirstDay = false;
380         int week = 1;
381         for (LocalDateTime currentDate = startDate; currentDate.compareTo(endDate) < 0; currentDate = currentDate.plusDays(1)) {
382 
383             if (currentDate.getDayOfWeek() == flsaBeginDay && afterFirstDay) {
384                 header.add("Week " + week);
385                 dayArrangements.add(false);
386                 week++;
387             }
388 
389             header.add(makeHeaderDiplayString(currentDate, virtualDays));
390             dayArrangements.add(true);
391 
392 
393             afterFirstDay = true;
394         }
395 
396         // We may have a very small final "week" on this pay period. For now
397         // we will mark it as a week, and if someone doesn't like it, it can
398         // be removed.
399         if (!header.get(header.size()-1).startsWith("Week")) {
400             dayArrangements.add(false);
401             header.add("Week " + week);
402         }
403 
404 
405         header.add("Period Total");
406         dayArrangements.add(false);
407         return header;
408     }
409 
410     /**
411      * Helper function to generate display text for the summary header.
412      * @param currentDate The date we are generating for.
413      * @param virtualDays Whether or not virtual days apply.
414      * @return A string appropriate for UI display.
415      */
416     private String makeHeaderDiplayString(LocalDateTime currentDate, boolean virtualDays) {
417         StringBuilder display = new StringBuilder();
418         
419         display.append(currentDate.toString("E"));
420         if (virtualDays) {
421         	LocalDateTime nextDate = currentDate.plusDays(1);
422         	display.append(" - ");
423             display.append(nextDate.toString("E"));
424         }
425         
426         display.append("<br />");
427         
428         display.append(currentDate.toString(TkConstants.DT_ABBREV_DATE_FORMAT));
429         if (virtualDays) {
430             LocalDateTime nextDate = currentDate.plusDays(1);
431             display.append(" - ");
432             display.append(nextDate.toString(TkConstants.DT_ABBREV_DATE_FORMAT));
433         }
434 
435         return display.toString();
436     }
437 
438     /**
439      * @param calEntry Calendar entry we are using for lookup.
440      * @return The PayCalendar that owns the provided entry.
441      */
442     private Calendar getPayCalendarForEntry(CalendarEntries calEntry) {
443         Calendar cal = null;
444 
445         if (calEntry != null) {
446             cal = TkServiceLocator.getCalendarService().getCalendar(calEntry.getHrCalendarId());
447         }
448 
449         return cal;
450     }
451 
452 }