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.collections.CollectionUtils;
019 import org.apache.commons.lang.StringUtils;
020 import org.joda.time.DateTime;
021 import org.joda.time.DateTimeFieldType;
022 import org.joda.time.LocalDateTime;
023 import org.kuali.hr.job.Job;
024 import org.kuali.hr.lm.LMConstants;
025 import org.kuali.hr.lm.leaveSummary.LeaveSummary;
026 import org.kuali.hr.lm.leaveSummary.LeaveSummaryRow;
027 import org.kuali.hr.lm.leaveblock.LeaveBlock;
028 import org.kuali.hr.lm.util.LeaveBlockAggregate;
029 import org.kuali.hr.time.assignment.Assignment;
030 import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
031 import org.kuali.hr.time.calendar.Calendar;
032 import org.kuali.hr.time.calendar.CalendarEntries;
033 import org.kuali.hr.time.earncode.EarnCode;
034 import org.kuali.hr.time.earncodegroup.EarnCodeGroup;
035 import org.kuali.hr.time.flsa.FlsaDay;
036 import org.kuali.hr.time.flsa.FlsaWeek;
037 import org.kuali.hr.time.service.base.TkServiceLocator;
038 import org.kuali.hr.time.timeblock.TimeBlock;
039 import org.kuali.hr.time.timeblock.TimeHourDetail;
040 import org.kuali.hr.time.timesheet.TimesheetDocument;
041 import org.kuali.hr.time.timesummary.AssignmentRow;
042 import org.kuali.hr.time.timesummary.EarnCodeSection;
043 import org.kuali.hr.time.timesummary.EarnGroupSection;
044 import org.kuali.hr.time.timesummary.TimeSummary;
045 import org.kuali.hr.time.util.TKUtils;
046 import org.kuali.hr.time.util.TkConstants;
047 import org.kuali.hr.time.util.TkTimeBlockAggregate;
048 import org.kuali.hr.time.workarea.WorkArea;
049
050 import java.math.BigDecimal;
051 import java.sql.Timestamp;
052 import java.util.*;
053
054 public class TimeSummaryServiceImpl implements TimeSummaryService {
055 private static final String OTHER_EARN_GROUP = "Other";
056
057 @Override
058 public TimeSummary getTimeSummary(TimesheetDocument timesheetDocument) {
059 TimeSummary timeSummary = new TimeSummary();
060
061 if(timesheetDocument.getTimeBlocks() == null) {
062 return timeSummary;
063 }
064
065 List<Boolean> dayArrangements = new ArrayList<Boolean>();
066
067 timeSummary.setSummaryHeader(getHeaderForSummary(timesheetDocument.getCalendarEntry(), dayArrangements));
068 TkTimeBlockAggregate tkTimeBlockAggregate = new TkTimeBlockAggregate(timesheetDocument.getTimeBlocks(), timesheetDocument.getCalendarEntry(), TkServiceLocator.getCalendarService().getCalendar(timesheetDocument.getCalendarEntry().getHrCalendarId()), true);
069
070 List<Assignment> timeAssignments = timesheetDocument.getAssignments();
071 List<String> tAssignmentKeys = new ArrayList<String>();
072 for(Assignment assign : timeAssignments) {
073 tAssignmentKeys.add(assign.getAssignmentKey());
074 }
075 List<LeaveBlock> leaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(timesheetDocument.getPrincipalId(),
076 timesheetDocument.getCalendarEntry().getBeginPeriodDate(), timesheetDocument.getCalendarEntry().getEndPeriodDate(), tAssignmentKeys);
077 LeaveBlockAggregate leaveBlockAggregate = new LeaveBlockAggregate(leaveBlocks, timesheetDocument.getCalendarEntry());
078 tkTimeBlockAggregate = combineTimeAndLeaveAggregates(tkTimeBlockAggregate, leaveBlockAggregate);
079
080 timeSummary.setWorkedHours(getWorkedHours(tkTimeBlockAggregate, leaveBlockAggregate));
081
082 List<EarnGroupSection> earnGroupSections = getEarnGroupSections(tkTimeBlockAggregate, timeSummary.getSummaryHeader().size()+1,
083 dayArrangements, timesheetDocument.getAsOfDate(), timesheetDocument.getDocEndDate());
084 timeSummary.setSections(earnGroupSections);
085
086 try {
087 List<LeaveSummaryRow> maxedLeaveRows = getMaxedLeaveRows(timesheetDocument.getCalendarEntry(),timesheetDocument.getPrincipalId());
088 timeSummary.setMaxedLeaveRows(maxedLeaveRows);
089 } catch (Exception e) {
090 // TODO Auto-generated catch block
091 e.printStackTrace();
092 }
093
094 return timeSummary;
095 }
096
097 private List<LeaveSummaryRow> getMaxedLeaveRows(
098 CalendarEntries calendarEntry, String principalId) throws Exception {
099 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 }