View Javadoc

1   /**
2    * Copyright 2004-2012 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.lang.StringUtils;
19  import org.joda.time.DateTimeFieldType;
20  import org.joda.time.LocalDateTime;
21  import org.kuali.hr.time.assignment.Assignment;
22  import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
23  import org.kuali.hr.time.calendar.Calendar;
24  import org.kuali.hr.time.calendar.CalendarEntries;
25  import org.kuali.hr.time.earncode.EarnCode;
26  import org.kuali.hr.time.earngroup.EarnGroup;
27  import org.kuali.hr.time.flsa.FlsaDay;
28  import org.kuali.hr.time.flsa.FlsaWeek;
29  import org.kuali.hr.time.service.base.TkServiceLocator;
30  import org.kuali.hr.time.timeblock.TimeBlock;
31  import org.kuali.hr.time.timeblock.TimeHourDetail;
32  import org.kuali.hr.time.timesheet.TimesheetDocument;
33  import org.kuali.hr.time.timesummary.AssignmentRow;
34  import org.kuali.hr.time.timesummary.EarnCodeSection;
35  import org.kuali.hr.time.timesummary.EarnGroupSection;
36  import org.kuali.hr.time.timesummary.TimeSummary;
37  import org.kuali.hr.time.util.TKUtils;
38  import org.kuali.hr.time.util.TkConstants;
39  import org.kuali.hr.time.util.TkTimeBlockAggregate;
40  
41  import java.math.BigDecimal;
42  import java.util.*;
43  
44  public class TimeSummaryServiceImpl implements TimeSummaryService {
45  	private static final String OTHER_EARN_GROUP = "Other";
46  
47      @Override
48  	public TimeSummary getTimeSummary(TimesheetDocument timesheetDocument) {
49  		TimeSummary timeSummary = new TimeSummary();
50  
51  		if(timesheetDocument.getTimeBlocks() == null) {
52  			return timeSummary;
53  		}
54  
55          List<Boolean> dayArrangements = new ArrayList<Boolean>();
56  
57  		timeSummary.setSummaryHeader(getHeaderForSummary(timesheetDocument.getPayCalendarEntry(), dayArrangements));
58  		TkTimeBlockAggregate tkTimeBlockAggregate = new TkTimeBlockAggregate(timesheetDocument.getTimeBlocks(), timesheetDocument.getPayCalendarEntry(), TkServiceLocator.getCalendarService().getCalendar(timesheetDocument.getPayCalendarEntry().getHrCalendarId()), true);
59  		timeSummary.setWorkedHours(getWorkedHours(tkTimeBlockAggregate));
60  
61          List<EarnGroupSection> earnGroupSections = getEarnGroupSections(tkTimeBlockAggregate, timeSummary.getSummaryHeader().size()+1, dayArrangements, timesheetDocument.getAsOfDate());
62          timeSummary.setSections(earnGroupSections);
63  
64  		return timeSummary;
65  	}
66  	
67      /**
68       * Aggregates timeblocks into the appropriate earngroup-> earncode -> assignment rows
69       * @param tkTimeBlockAggregate
70       * @param numEntries
71       * @param dayArrangements
72       * @param asOfDate
73       * @return
74       */
75  	public List<EarnGroupSection> getEarnGroupSections(TkTimeBlockAggregate tkTimeBlockAggregate, int numEntries, List<Boolean> dayArrangements, Date asOfDate ){
76  		List<EarnGroupSection> earnGroupSections = new ArrayList<EarnGroupSection>();
77  		List<FlsaWeek> flsaWeeks = tkTimeBlockAggregate.getFlsaWeeks(TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback());
78  		Map<String, EarnCodeSection> earnCodeToEarnCodeSection = new HashMap<String, EarnCodeSection>();
79  		Map<String, EarnGroupSection> earnGroupToEarnGroupSection = new HashMap<String, EarnGroupSection>();
80  		
81  		int dayCount = 0;
82  		
83  		//TODO remove this and correct the aggregate .. not sure what the down stream changes are
84  		//so leaving this for initial release
85  		List<FlsaWeek> trimmedFlsaWeeks = new ArrayList<FlsaWeek>();
86  		for(FlsaWeek flsaWeek : flsaWeeks){
87  			if(flsaWeek.getFlsaDays().size() > 0){
88  				trimmedFlsaWeeks.add(flsaWeek);
89  			}
90  		}
91  		
92  		//For every flsa week and day aggegate each time hour detail 
93  		// buckets it by earn code section first
94  		for(FlsaWeek flsaWeek : trimmedFlsaWeeks){
95  			int weekSize = 0;
96  			List<FlsaDay> flsaDays = flsaWeek.getFlsaDays();
97  			for(FlsaDay flsaDay : flsaDays){
98  				Map<String, List<TimeBlock>> earnCodeToTimeBlocks = flsaDay.getEarnCodeToTimeBlocks();
99  				
100 				for(String earnCode : earnCodeToTimeBlocks.keySet()){
101 					for(TimeBlock timeBlock : earnCodeToTimeBlocks.get(earnCode)){
102 						for(TimeHourDetail thd : timeBlock.getTimeHourDetails()){
103 							if(StringUtils.equals(TkConstants.LUNCH_EARN_CODE, thd.getEarnCode())){
104 								continue;
105 							}
106 							EarnCodeSection earnCodeSection = earnCodeToEarnCodeSection.get(thd.getEarnCode());
107 							if(earnCodeSection == null){
108 								earnCodeSection = new EarnCodeSection();
109 								earnCodeSection.setEarnCode(thd.getEarnCode());
110 								EarnCode earnCodeObj = TkServiceLocator.getEarnCodeService().getEarnCode(thd.getEarnCode(), TKUtils.getTimelessDate(asOfDate));
111 								earnCodeSection.setDescription(earnCodeObj.getDescription());
112 								earnCodeSection.setIsAmountEarnCode((earnCodeObj.getRecordMethod()!= null && earnCodeObj.getRecordMethod().equalsIgnoreCase(TkConstants.EARN_CODE_AMOUNT)) ? true : false);
113 								for(int i = 0;i<(numEntries-1);i++){
114 									earnCodeSection.getTotals().add(BigDecimal.ZERO);
115 								}
116 								
117 								earnCodeToEarnCodeSection.put(thd.getEarnCode(), earnCodeSection);
118 							}
119 							String assignKey = timeBlock.getAssignmentKey();
120 							AssignmentRow assignRow = earnCodeSection.getAssignKeyToAssignmentRowMap().get(assignKey);
121 							if(assignRow == null){
122 								assignRow = new AssignmentRow();
123 								assignRow.setAssignmentKey(assignKey);
124 								AssignmentDescriptionKey assignmentKey = TkServiceLocator.getAssignmentService().getAssignmentDescriptionKey(assignKey);
125 								Assignment assignment = TkServiceLocator.getAssignmentService().getAssignment(timeBlock.getPrincipalId(), assignmentKey, TKUtils.getTimelessDate(asOfDate));
126 								//TODO push this up to the assignment fetch/fully populated instead of like this
127 								if(assignment != null){
128 									if(assignment.getJob() == null){
129 										assignment.setJob(TkServiceLocator.getJobService().getJob(assignment.getPrincipalId(),assignment.getJobNumber(),TKUtils.getTimelessDate(asOfDate)));
130 									}
131 									if(assignment.getWorkAreaObj() == null){
132 										assignment.setWorkAreaObj(TkServiceLocator.getWorkAreaService().getWorkArea(assignment.getWorkArea(), TKUtils.getTimelessDate(asOfDate)));
133 									}
134 									assignRow.setDescr(assignment.getAssignmentDescription());
135 								}
136 								for(int i = 0;i<(numEntries-1);i++){
137 									assignRow.getTotal().add(BigDecimal.ZERO);
138 									assignRow.getAmount().add(BigDecimal.ZERO);
139 								}
140 								assignRow.setEarnCodeSection(earnCodeSection);
141 								earnCodeSection.addAssignmentRow(assignRow);
142 							}
143 							assignRow.addToTotal(dayCount, thd.getHours());
144 							assignRow.addToAmount(dayCount, thd.getAmount());
145 						}
146 					}
147 				}
148 				dayCount++;
149 				weekSize++;
150 			}
151 			//end of flsa week accumulate weekly totals
152 			for(EarnCodeSection earnCodeSection : earnCodeToEarnCodeSection.values()){
153 				earnCodeSection.addWeeklyTotal(dayCount, weekSize);
154 			}			
155 			weekSize = 0;
156 
157 			dayCount++;
158 		}
159 		
160 		dayCount = 0;
161 		//now create all teh earn group sections and aggregate accordingly
162 		for(EarnCodeSection earnCodeSection : earnCodeToEarnCodeSection.values()){
163 			String earnCode = earnCodeSection.getEarnCode();
164             EarnGroup earnGroupObj = TkServiceLocator.getEarnGroupService().getEarnGroupSummaryForEarnCode(earnCode, TKUtils.getTimelessDate(asOfDate));
165 			String earnGroup = null;
166 			if(earnGroupObj == null){
167 				earnGroup = OTHER_EARN_GROUP;
168 			} else{
169 				earnGroup = earnGroupObj.getDescr();
170 			}
171 			
172 			EarnGroupSection earnGroupSection = earnGroupToEarnGroupSection.get(earnGroup);
173 			if(earnGroupSection == null){
174 				earnGroupSection = new EarnGroupSection();
175 				earnGroupSection.setEarnGroup(earnGroup);
176 				for(int i =0;i<(numEntries-1);i++){
177 					earnGroupSection.getTotals().add(BigDecimal.ZERO);
178 				}
179 				earnGroupToEarnGroupSection.put(earnGroup, earnGroupSection);
180 			}
181 			earnGroupSection.addEarnCodeSection(earnCodeSection, dayArrangements);
182 			
183 		}
184 		for(EarnGroupSection earnGroupSection : earnGroupToEarnGroupSection.values()){
185 			earnGroupSections.add(earnGroupSection);
186 		}
187 		return earnGroupSections;
188 	}
189 	
190 	/**
191 	 * Generate a list of string describing this pay calendar entry for the summary
192 	 * @param payCalEntry
193 	 * @return
194 	 */
195 	protected List<String> getSummaryHeader(CalendarEntries payCalEntry){
196 		List<String> summaryHeader = new ArrayList<String>();
197 		int dayCount = 0;
198 		Date beginDateTime = payCalEntry.getBeginPeriodDateTime();
199 		Date endDateTime = payCalEntry.getEndPeriodDateTime();
200 		boolean virtualDays = false;
201         LocalDateTime endDate = payCalEntry.getEndLocalDateTime();
202 
203         if (endDate.get(DateTimeFieldType.hourOfDay()) != 0 || endDate.get(DateTimeFieldType.minuteOfHour()) != 0 ||
204                 endDate.get(DateTimeFieldType.secondOfMinute()) != 0){
205             virtualDays = true;
206         }
207 		
208 		Date currDateTime = beginDateTime;
209 		java.util.Calendar cal = GregorianCalendar.getInstance();
210 		
211 		while(currDateTime.before(endDateTime)){
212 			LocalDateTime currDate = new LocalDateTime(currDateTime);
213 			summaryHeader.add(makeHeaderDiplayString(currDate, virtualDays));
214 			
215 			dayCount++;
216 			if((dayCount % 7) == 0){
217 				summaryHeader.add("Week "+ ((dayCount / 7)));
218 			}
219 			cal.setTime(currDateTime);
220 			cal.add(java.util.Calendar.HOUR, 24);
221 			currDateTime = cal.getTime();
222 		}
223 		
224 		summaryHeader.add("Period Total");
225 		return summaryHeader;
226 	}
227 
228     /**
229      * Provides the number of hours worked for the pay period indicated in the
230      * aggregate.
231      *
232      * @param aggregate The aggregate we are summing
233      *
234      * @return A list of BigDecimals containing the number of hours worked.
235      * This list will line up with the header.
236      */
237     private List<BigDecimal> getWorkedHours(TkTimeBlockAggregate aggregate) {
238         List<BigDecimal> hours = new ArrayList<BigDecimal>();
239         BigDecimal periodTotal = TkConstants.BIG_DECIMAL_SCALED_ZERO;
240         for (FlsaWeek week : aggregate.getFlsaWeeks(TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback())) {
241             BigDecimal weeklyTotal = TkConstants.BIG_DECIMAL_SCALED_ZERO;
242             for (FlsaDay day : week.getFlsaDays()) {
243                 BigDecimal totalForDay = TkConstants.BIG_DECIMAL_SCALED_ZERO;
244                 for (TimeBlock block : day.getAppliedTimeBlocks()) {
245                     totalForDay = totalForDay.add(block.getHours(), TkConstants.MATH_CONTEXT);
246                     weeklyTotal = weeklyTotal.add(block.getHours(), TkConstants.MATH_CONTEXT);
247                     periodTotal = periodTotal.add(block.getHours(), TkConstants.MATH_CONTEXT);
248                 }
249                 hours.add(totalForDay);
250             }
251             hours.add(weeklyTotal);
252         }
253         hours.add(periodTotal);
254 
255         return hours;
256     }
257 
258 
259     /**
260      * Handles the generation of the display header for the time summary.
261      *
262      * @param cal The PayCalendarEntries object we are using to derive information.
263      * @param dayArrangements Container passed in to store the position of week / period aggregate sums
264      *
265      * @return An in-order string of days for this period that properly accounts
266      * for FLSA week boundaries in the pay period.
267      */
268     @Override
269     public List<String> getHeaderForSummary(CalendarEntries cal, List<Boolean> dayArrangements) {
270         List<String> header = new ArrayList<String>();
271 
272         // Maps directly to joda time day of week constants.
273         int flsaBeginDay = this.getPayCalendarForEntry(cal).getFlsaBeginDayConstant();
274         boolean virtualDays = false;
275         LocalDateTime startDate = cal.getBeginLocalDateTime();
276         LocalDateTime endDate = cal.getEndLocalDateTime();
277 
278         // Increment end date if we are on a virtual day calendar, so that the
279         // for loop can account for having the proper amount of days on the
280         // summary calendar.
281         if (endDate.get(DateTimeFieldType.hourOfDay()) != 0 || endDate.get(DateTimeFieldType.minuteOfHour()) != 0 ||
282                 endDate.get(DateTimeFieldType.secondOfMinute()) != 0)
283         {
284             endDate = endDate.plusDays(1);
285             virtualDays = true;
286         }
287 
288         boolean afterFirstDay = false;
289         int week = 1;
290         for (LocalDateTime currentDate = startDate; currentDate.compareTo(endDate) < 0; currentDate = currentDate.plusDays(1)) {
291 
292             if (currentDate.getDayOfWeek() == flsaBeginDay && afterFirstDay) {
293                 header.add("Week " + week);
294                 dayArrangements.add(false);
295                 week++;
296             }
297 
298             header.add(makeHeaderDiplayString(currentDate, virtualDays));
299             dayArrangements.add(true);
300 
301 
302             afterFirstDay = true;
303         }
304 
305         // We may have a very small final "week" on this pay period. For now
306         // we will mark it as a week, and if someone doesn't like it, it can
307         // be removed.
308         if (!header.get(header.size()-1).startsWith("Week")) {
309             dayArrangements.add(false);
310             header.add("Week " + week);
311         }
312 
313 
314         header.add("Period Total");
315         dayArrangements.add(false);
316         return header;
317     }
318 
319     /**
320      * Helper function to generate display text for the summary header.
321      * @param currentDate The date we are generating for.
322      * @param virtualDays Whether or not virtual days apply.
323      * @return A string appropriate for UI display.
324      */
325     private String makeHeaderDiplayString(LocalDateTime currentDate, boolean virtualDays) {
326         StringBuilder display = new StringBuilder(currentDate.toString(TkConstants.DT_ABBREV_DATE_FORMAT));
327 
328         if (virtualDays) {
329             LocalDateTime nextDay = currentDate.plusDays(1);
330             display.append(" - ");
331             display.append(nextDay.toString(TkConstants.DT_ABBREV_DATE_FORMAT));
332         }
333 
334         return display.toString();
335     }
336 
337     /**
338      * @param calEntry Calendar entry we are using for lookup.
339      * @return The PayCalendar that owns the provided entry.
340      */
341     private Calendar getPayCalendarForEntry(CalendarEntries calEntry) {
342         Calendar cal = null;
343 
344         if (calEntry != null) {
345             cal = TkServiceLocator.getCalendarService().getCalendar(calEntry.getHrCalendarId());
346         }
347 
348         return cal;
349     }
350 
351 }