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    }