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    }