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    }