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