001 /**
002 * Copyright 2004-2013 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.hr.time.timesummary.service;
017
018 import org.apache.commons.lang.StringUtils;
019 import org.joda.time.DateTimeFieldType;
020 import org.joda.time.LocalDateTime;
021 import org.kuali.hr.job.Job;
022 import org.kuali.hr.time.assignment.Assignment;
023 import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
024 import org.kuali.hr.time.calendar.Calendar;
025 import org.kuali.hr.time.calendar.CalendarEntries;
026 import org.kuali.hr.time.earncode.EarnCode;
027 import org.kuali.hr.time.earngroup.EarnGroup;
028 import org.kuali.hr.time.flsa.FlsaDay;
029 import org.kuali.hr.time.flsa.FlsaWeek;
030 import org.kuali.hr.time.service.base.TkServiceLocator;
031 import org.kuali.hr.time.timeblock.TimeBlock;
032 import org.kuali.hr.time.timeblock.TimeHourDetail;
033 import org.kuali.hr.time.timesheet.TimesheetDocument;
034 import org.kuali.hr.time.timesummary.AssignmentRow;
035 import org.kuali.hr.time.timesummary.EarnCodeSection;
036 import org.kuali.hr.time.timesummary.EarnGroupSection;
037 import org.kuali.hr.time.timesummary.TimeSummary;
038 import org.kuali.hr.time.util.TKUtils;
039 import org.kuali.hr.time.util.TkConstants;
040 import org.kuali.hr.time.util.TkTimeBlockAggregate;
041 import org.kuali.hr.time.workarea.WorkArea;
042
043 import java.math.BigDecimal;
044 import java.util.*;
045
046 public class TimeSummaryServiceImpl implements TimeSummaryService {
047 private static final String OTHER_EARN_GROUP = "Other";
048
049 @Override
050 public TimeSummary getTimeSummary(TimesheetDocument timesheetDocument) {
051 TimeSummary timeSummary = new TimeSummary();
052
053 if(timesheetDocument.getTimeBlocks() == null) {
054 return timeSummary;
055 }
056
057 List<Boolean> dayArrangements = new ArrayList<Boolean>();
058
059 timeSummary.setSummaryHeader(getHeaderForSummary(timesheetDocument.getPayCalendarEntry(), dayArrangements));
060 TkTimeBlockAggregate tkTimeBlockAggregate = new TkTimeBlockAggregate(timesheetDocument.getTimeBlocks(), timesheetDocument.getPayCalendarEntry(), TkServiceLocator.getCalendarService().getCalendar(timesheetDocument.getPayCalendarEntry().getHrCalendarId()), true);
061 timeSummary.setWorkedHours(getWorkedHours(tkTimeBlockAggregate));
062
063 List<EarnGroupSection> earnGroupSections = getEarnGroupSections(tkTimeBlockAggregate, timeSummary.getSummaryHeader().size()+1,
064 dayArrangements, timesheetDocument.getAsOfDate(), timesheetDocument.getDocEndDate());
065 timeSummary.setSections(earnGroupSections);
066
067 return timeSummary;
068 }
069
070 /**
071 * Aggregates timeblocks into the appropriate earngroup-> earncode -> assignment rows
072 * @param tkTimeBlockAggregate
073 * @param numEntries
074 * @param dayArrangements
075 * @param asOfDate
076 * @return
077 */
078 public List<EarnGroupSection> getEarnGroupSections(TkTimeBlockAggregate tkTimeBlockAggregate, int numEntries, List<Boolean> dayArrangements, Date asOfDate , Date docEndDate){
079 List<EarnGroupSection> earnGroupSections = new ArrayList<EarnGroupSection>();
080 List<FlsaWeek> flsaWeeks = tkTimeBlockAggregate.getFlsaWeeks(TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback());
081 Map<String, EarnCodeSection> earnCodeToEarnCodeSection = new HashMap<String, EarnCodeSection>();
082 Map<String, EarnGroupSection> earnGroupToEarnGroupSection = new HashMap<String, EarnGroupSection>();
083
084 int dayCount = 0;
085
086 //TODO remove this and correct the aggregate .. not sure what the down stream changes are
087 //so leaving this for initial release
088 List<FlsaWeek> trimmedFlsaWeeks = new ArrayList<FlsaWeek>();
089 for(FlsaWeek flsaWeek : flsaWeeks){
090 if(flsaWeek.getFlsaDays().size() > 0){
091 trimmedFlsaWeeks.add(flsaWeek);
092 }
093 }
094
095 //For every flsa week and day aggegate each time hour detail
096 // buckets it by earn code section first
097 for(FlsaWeek flsaWeek : trimmedFlsaWeeks){
098 int weekSize = 0;
099 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 }