001 /**
002 * Copyright 2004-2012 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.time.assignment.Assignment;
022 import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
023 import org.kuali.hr.time.calendar.Calendar;
024 import org.kuali.hr.time.calendar.CalendarEntries;
025 import org.kuali.hr.time.earncode.EarnCode;
026 import org.kuali.hr.time.earngroup.EarnGroup;
027 import org.kuali.hr.time.flsa.FlsaDay;
028 import org.kuali.hr.time.flsa.FlsaWeek;
029 import org.kuali.hr.time.service.base.TkServiceLocator;
030 import org.kuali.hr.time.timeblock.TimeBlock;
031 import org.kuali.hr.time.timeblock.TimeHourDetail;
032 import org.kuali.hr.time.timesheet.TimesheetDocument;
033 import org.kuali.hr.time.timesummary.AssignmentRow;
034 import org.kuali.hr.time.timesummary.EarnCodeSection;
035 import org.kuali.hr.time.timesummary.EarnGroupSection;
036 import org.kuali.hr.time.timesummary.TimeSummary;
037 import org.kuali.hr.time.util.TKUtils;
038 import org.kuali.hr.time.util.TkConstants;
039 import org.kuali.hr.time.util.TkTimeBlockAggregate;
040
041 import java.math.BigDecimal;
042 import java.util.*;
043
044 public class TimeSummaryServiceImpl implements TimeSummaryService {
045 private static final String OTHER_EARN_GROUP = "Other";
046
047 @Override
048 public TimeSummary getTimeSummary(TimesheetDocument timesheetDocument) {
049 TimeSummary timeSummary = new TimeSummary();
050
051 if(timesheetDocument.getTimeBlocks() == null) {
052 return timeSummary;
053 }
054
055 List<Boolean> dayArrangements = new ArrayList<Boolean>();
056
057 timeSummary.setSummaryHeader(getHeaderForSummary(timesheetDocument.getPayCalendarEntry(), dayArrangements));
058 TkTimeBlockAggregate tkTimeBlockAggregate = new TkTimeBlockAggregate(timesheetDocument.getTimeBlocks(), timesheetDocument.getPayCalendarEntry(), TkServiceLocator.getCalendarService().getCalendar(timesheetDocument.getPayCalendarEntry().getHrCalendarId()), true);
059 timeSummary.setWorkedHours(getWorkedHours(tkTimeBlockAggregate));
060
061 List<EarnGroupSection> earnGroupSections = getEarnGroupSections(tkTimeBlockAggregate, timeSummary.getSummaryHeader().size()+1, dayArrangements, timesheetDocument.getAsOfDate());
062 timeSummary.setSections(earnGroupSections);
063
064 return timeSummary;
065 }
066
067 /**
068 * Aggregates timeblocks into the appropriate earngroup-> earncode -> assignment rows
069 * @param tkTimeBlockAggregate
070 * @param numEntries
071 * @param dayArrangements
072 * @param asOfDate
073 * @return
074 */
075 public List<EarnGroupSection> getEarnGroupSections(TkTimeBlockAggregate tkTimeBlockAggregate, int numEntries, List<Boolean> dayArrangements, Date asOfDate ){
076 List<EarnGroupSection> earnGroupSections = new ArrayList<EarnGroupSection>();
077 List<FlsaWeek> flsaWeeks = tkTimeBlockAggregate.getFlsaWeeks(TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback());
078 Map<String, EarnCodeSection> earnCodeToEarnCodeSection = new HashMap<String, EarnCodeSection>();
079 Map<String, EarnGroupSection> earnGroupToEarnGroupSection = new HashMap<String, EarnGroupSection>();
080
081 int dayCount = 0;
082
083 //TODO remove this and correct the aggregate .. not sure what the down stream changes are
084 //so leaving this for initial release
085 List<FlsaWeek> trimmedFlsaWeeks = new ArrayList<FlsaWeek>();
086 for(FlsaWeek flsaWeek : flsaWeeks){
087 if(flsaWeek.getFlsaDays().size() > 0){
088 trimmedFlsaWeeks.add(flsaWeek);
089 }
090 }
091
092 //For every flsa week and day aggegate each time hour detail
093 // buckets it by earn code section first
094 for(FlsaWeek flsaWeek : trimmedFlsaWeeks){
095 int weekSize = 0;
096 List<FlsaDay> flsaDays = flsaWeek.getFlsaDays();
097 for(FlsaDay flsaDay : flsaDays){
098 Map<String, List<TimeBlock>> earnCodeToTimeBlocks = flsaDay.getEarnCodeToTimeBlocks();
099
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 }