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.detail.validation;
017    
018    import java.math.BigDecimal;
019    import java.sql.Date;
020    import java.sql.Timestamp;
021    import java.util.ArrayList;
022    import java.util.List;
023    
024    import org.apache.commons.lang.StringUtils;
025    import org.joda.time.DateMidnight;
026    import org.joda.time.DateTime;
027    import org.joda.time.DateTimeConstants;
028    import org.joda.time.DateTimeZone;
029    import org.joda.time.Hours;
030    import org.joda.time.Interval;
031    import org.joda.time.LocalDateTime;
032    import org.kuali.hr.time.assignment.Assignment;
033    import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
034    import org.kuali.hr.time.calendar.CalendarEntries;
035    import org.kuali.hr.time.clocklog.ClockLog;
036    import org.kuali.hr.time.detail.web.TimeDetailActionFormBase;
037    import org.kuali.hr.time.earncode.EarnCode;
038    import org.kuali.hr.time.service.base.TkServiceLocator;
039    import org.kuali.hr.time.timeblock.TimeBlock;
040    import org.kuali.hr.time.timesheet.TimesheetDocument;
041    import org.kuali.hr.time.util.TKUser;
042    import org.kuali.hr.time.util.TKUtils;
043    import org.kuali.hr.time.util.TkConstants;
044    
045    public class TimeDetailValidationUtil {
046    
047        /**
048         * Convenience method for handling validation directly from the form object.
049         * @param tdaf The populated form.
050         *
051         * @return A list of error strings.
052         */
053        public static List<String> validateTimeEntryDetails(TimeDetailActionFormBase tdaf) {
054            return TimeDetailValidationUtil.validateTimeEntryDetails(
055                    tdaf.getHours(), tdaf.getAmount(), tdaf.getStartTime(), tdaf.getEndTime(),
056                    tdaf.getStartDate(), tdaf.getEndDate(), tdaf.getTimesheetDocument(),
057                    tdaf.getSelectedEarnCode(), tdaf.getSelectedAssignment(),
058                    tdaf.getAcrossDays().equalsIgnoreCase("y"), tdaf.getTkTimeBlockId(), tdaf.getOvertimePref(), tdaf.getSpanningWeeks().equalsIgnoreCase("y")
059            );
060        }
061    
062        public static List<String> validateTimeEntryDetails(BigDecimal hours, BigDecimal amount, String startTimeS, String endTimeS, String startDateS, String endDateS, TimesheetDocument timesheetDocument, String selectedEarnCode, String selectedAssignment, boolean acrossDays, String timeblockId, String overtimePref, boolean spanningWeeks) {
063            List<String> errors = new ArrayList<String>();
064    
065            if (timesheetDocument == null) {
066                errors.add("No timesheet document found.");
067            }
068            if (errors.size() > 0) return errors;
069    
070            CalendarEntries payCalEntry = timesheetDocument.getCalendarEntry();
071            java.sql.Date asOfDate = payCalEntry.getEndPeriodDate();
072    
073            errors.addAll(TimeDetailValidationUtil.validateDates(startDateS, endDateS));
074            errors.addAll(TimeDetailValidationUtil.validateTimes(startTimeS, endTimeS));
075            if (errors.size() > 0) return errors;
076    
077            Long startTime;
078            Long endTime;
079    
080            startTime = TKUtils.convertDateStringToTimestampWithoutZone(startDateS, startTimeS).getTime();
081            endTime = TKUtils.convertDateStringToTimestampWithoutZone(endDateS, endTimeS).getTime();
082    
083            errors.addAll(validateInterval(payCalEntry, startTime, endTime));
084            if (errors.size() > 0) return errors;
085    
086            EarnCode earnCode = new EarnCode();
087            if (StringUtils.isNotBlank(selectedEarnCode)) {
088                earnCode = TkServiceLocator.getEarnCodeService().getEarnCode(selectedEarnCode, asOfDate);
089    
090                if (earnCode != null && earnCode.getRecordMethod()!= null && earnCode.getRecordMethod().equalsIgnoreCase(TkConstants.EARN_CODE_TIME)) {
091                    if (startTimeS == null) errors.add("The start time is blank.");
092                    if (endTimeS == null) errors.add("The end time is blank.");
093                    if (startTime - endTime == 0) errors.add("Start time and end time cannot be equivalent");
094                }
095            }
096            if (errors.size() > 0) return errors;
097    
098            DateTime startTemp = new DateTime(startTime);
099            DateTime endTemp = new DateTime(endTime);
100    
101            if (errors.size() == 0 && !acrossDays && !StringUtils.equals(TkConstants.EARN_CODE_CPE, overtimePref)) {
102                Hours hrs = Hours.hoursBetween(startTemp, endTemp);
103                if (hrs.getHours() >= 24) errors.add("One timeblock cannot exceed 24 hours");
104            }
105            if (errors.size() > 0) return errors;
106    
107            //Check that assignment is valid for both days
108            AssignmentDescriptionKey assignKey = TkServiceLocator.getAssignmentService().getAssignmentDescriptionKey(selectedAssignment);
109            Assignment assign = TkServiceLocator.getAssignmentService().getAssignment(assignKey, new Date(startTime));
110            if (assign == null) errors.add("Assignment is not valid for start date " + TKUtils.formatDate(new Date(startTime)));
111            assign = TkServiceLocator.getAssignmentService().getAssignment(assignKey, new Date(endTime));
112            if (assign == null) errors.add("Assignment is not valid for end date " + TKUtils.formatDate(new Date(endTime)));
113            if (errors.size() > 0) return errors;
114    
115            //------------------------
116            // some of the simple validations are in the js side in order to reduce the server calls
117            // 1. check if the begin / end time is empty - tk.calenadr.js
118            // 2. check the time format - timeparse.js
119            // 3. only allows decimals to be entered in the hour field
120            //------------------------
121    
122            //------------------------
123            // check if the begin / end time are valid
124            //------------------------
125            if ((startTime.compareTo(endTime) > 0 || endTime.compareTo(startTime) < 0)) {
126                errors.add("The time or date is not valid.");
127            }
128            if (errors.size() > 0) return errors;
129            
130            // KPME-1446 
131            // -------------------------------
132            // check if there is a weekend day when the include weekends flag is checked
133            //--------------------------------
134            errors.addAll(validateSpanningWeeks(spanningWeeks, startTemp, endTemp));
135            if (errors.size() > 0) return errors;
136    
137            //------------------------
138            // check if the overnight shift is across days
139            //------------------------
140            if (acrossDays && hours == null && amount == null) {
141                if (startTemp.getHourOfDay() > endTemp.getHourOfDay()
142                        && !(endTemp.getDayOfYear() - startTemp.getDayOfYear() <= 1
143                            && endTemp.getHourOfDay() == 0)) {
144                    errors.add("The \"apply to each day\" box should not be checked.");
145                }
146            }
147            if (errors.size() > 0) return errors;
148    
149            //------------------------
150            // Amount cannot be zero
151            //------------------------
152            if (amount != null && earnCode != null && StringUtils.equals(earnCode.getEarnCodeType(), TkConstants.EARN_CODE_AMOUNT)) {
153                if (amount.equals(BigDecimal.ZERO)) {
154                    errors.add("Amount cannot be zero.");
155                }
156                if (amount.scale() > 2) {
157                    errors.add("Amount cannot have more than two digits after decimal point.");
158                }
159            }
160            if (errors.size() > 0) return errors;
161    
162            //------------------------
163            // check if the hours entered for hourly earn code is greater than 24 hours per day
164            // Hours cannot be zero
165            //------------------------
166            if (hours != null && earnCode != null && StringUtils.equals(earnCode.getEarnCodeType(), TkConstants.EARN_CODE_HOUR)) {
167                if (hours.equals(BigDecimal.ZERO)) {
168                    errors.add("Hours cannot be zero.");
169                }
170                if (hours.scale() > 2) {
171                    errors.add("Hours cannot have more than two digits after decimal point.");
172                }
173                int dayDiff = endTemp.getDayOfYear() - startTemp.getDayOfYear() + 1;
174                if (hours.compareTo(new BigDecimal(dayDiff * 24)) == 1) {
175                    errors.add("Cannot enter more than 24 hours per day.");
176                }
177            }
178            if (errors.size() > 0) return errors;
179    
180    
181    
182            //------------------------
183            // check if time blocks overlap with each other. Note that the tkTimeBlockId is used to
184            // determine is it's updating an existing time block or adding a new one
185            //------------------------
186    
187            boolean isRegularEarnCode = StringUtils.equals(assign.getJob().getPayTypeObj().getRegEarnCode(),selectedEarnCode);
188            errors.addAll(validateOverlap(startTime, endTime, acrossDays, startDateS, endTimeS,startTemp, endTemp, timesheetDocument, timeblockId, isRegularEarnCode));
189            if (errors.size() > 0) return errors;
190    
191            // Accrual Hour Limits Validation
192            //errors.addAll(TkServiceLocator.getTimeOffAccrualService().validateAccrualHoursLimitByEarnCode(timesheetDocument, selectedEarnCode));
193    
194            return errors;
195        }
196    
197        public static List<String> validateOverlap(Long startTime, Long endTime, boolean acrossDays, String startDateS, String endTimeS, DateTime startTemp, DateTime endTemp, TimesheetDocument timesheetDocument, String timeblockId, boolean isRegularEarnCode) {
198            List<String> errors = new ArrayList<String>();
199            Interval addedTimeblockInterval = new Interval(startTime, endTime);
200            List<Interval> dayInt = new ArrayList<Interval>();
201    
202            //if the user is clocked in, check if this time block overlaps with the clock action
203            ClockLog lastClockLog = TkServiceLocator.getClockLogService().getLastClockLog(TKUser.getCurrentTargetPersonId());
204            if(lastClockLog != null &&
205                            (lastClockLog.getClockAction().equals(TkConstants.CLOCK_IN) 
206                                            || lastClockLog.getClockAction().equals(TkConstants.LUNCH_IN))) {
207                     Timestamp lastClockTimestamp = lastClockLog.getClockTimestamp();
208                 String lastClockZone = lastClockLog.getClockTimestampTimezone();
209                 if (StringUtils.isEmpty(lastClockZone)) {
210                     lastClockZone = TKUtils.getSystemTimeZone();
211                 }
212                 DateTimeZone zone = DateTimeZone.forID(lastClockZone);
213                 DateTime clockWithZone = new DateTime(lastClockTimestamp, zone);
214                 DateTime currentTime = new DateTime(System.currentTimeMillis(), zone);
215                 Interval currentClockInInterval = new Interval(clockWithZone.getMillis(), currentTime.getMillis());
216           
217                if (isRegularEarnCode && addedTimeblockInterval.overlaps(currentClockInInterval)) {
218                     errors.add("The time block you are trying to add overlaps with the current clock action.");
219                     return errors;
220                 }
221            }
222           
223            if (acrossDays) {
224                DateTime start = new DateTime(startTime);
225                DateTime end = new DateTime(TKUtils.convertDateStringToTimestamp(startDateS, endTimeS).getTime());
226                if (endTemp.getDayOfYear() - startTemp.getDayOfYear() < 1) {
227                    end = new DateTime(endTime);
228                }
229                DateTime groupEnd = new DateTime(endTime);
230                Long startLong = start.getMillis();
231                Long endLong = end.getMillis();
232                //create interval span if start is before the end and the end is after the start except
233                //for when the end is midnight ..that converts to midnight of next day
234                DateMidnight midNight = new DateMidnight(endLong);
235                while (start.isBefore(groupEnd.getMillis()) && ((endLong >= startLong) || end.isEqual(midNight))) {
236                    Interval tempInt = null;
237                    if (end.isEqual(midNight)) {
238                        tempInt = addedTimeblockInterval;
239                    } else {
240                        tempInt = new Interval(startLong, endLong);
241                    }
242                    dayInt.add(tempInt);
243                    start = start.plusDays(1);
244                    end = end.plusDays(1);
245                    startLong = start.getMillis();
246                    endLong = end.getMillis();
247                }
248            } else {
249                dayInt.add(addedTimeblockInterval);
250            }
251    
252            for (TimeBlock timeBlock : timesheetDocument.getTimeBlocks()) {
253                if (errors.size() == 0 && StringUtils.equals(timeBlock.getEarnCodeType(), TkConstants.EARN_CODE_TIME)) {
254                    Interval timeBlockInterval = new Interval(timeBlock.getBeginTimestamp().getTime(), timeBlock.getEndTimestamp().getTime());
255                    for (Interval intv : dayInt) {
256                        if (isRegularEarnCode && timeBlockInterval.overlaps(intv) && (timeblockId == null || timeblockId.compareTo(timeBlock.getTkTimeBlockId()) != 0)) {
257                            errors.add("The time block you are trying to add overlaps with an existing time block.");
258                        }
259                    }
260                }
261            }
262    
263            return errors;
264        }
265    
266        public static List<String> validateDates(String startDateS, String endDateS) {
267            List<String> errors = new ArrayList<String>();
268            if (errors.size() == 0 && StringUtils.isEmpty(startDateS)) errors.add("The start date is blank.");
269            if (errors.size() == 0 && StringUtils.isEmpty(endDateS)) errors.add("The end date is blank.");
270            return errors;
271        }
272    
273        public static List<String> validateTimes(String startTimeS, String endTimeS) {
274            List<String> errors = new ArrayList<String>();
275            if (errors.size() == 0 && startTimeS == null) errors.add("The start time is blank.");
276            if (errors.size() == 0 && endTimeS == null) errors.add("The end time is blank.");
277            return errors;
278        }
279    
280        public static List<String> validateInterval(CalendarEntries payCalEntry, Long startTime, Long endTime) {
281            List<String> errors = new ArrayList<String>();
282            LocalDateTime pcb_ldt = payCalEntry.getBeginLocalDateTime();
283            LocalDateTime pce_ldt = payCalEntry.getEndLocalDateTime();
284            DateTimeZone utz = TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
285            DateTime p_cal_b_dt = pcb_ldt.toDateTime(utz);
286            DateTime p_cal_e_dt = pce_ldt.toDateTime(utz);
287    
288            Interval payInterval = new Interval(p_cal_b_dt, p_cal_e_dt);
289            if (errors.size() == 0 && !payInterval.contains(startTime)) {
290                errors.add("The start date/time is outside the pay period");
291            }
292            if (errors.size() == 0 && !payInterval.contains(endTime) && p_cal_e_dt.getMillis() != endTime) {
293                errors.add("The end date/time is outside the pay period");
294            }
295            return errors;
296        }
297        
298        // KPME-1446
299        public static List<String> validateSpanningWeeks(boolean spanningWeeks, DateTime startTemp, DateTime endTemp) {
300            List<String> errors = new ArrayList<String>();
301            
302            if (!spanningWeeks) {
303                    boolean isOnlyWeekendSpan = true;
304                    while ((startTemp.isBefore(endTemp) || startTemp.isEqual(endTemp)) && isOnlyWeekendSpan) {
305                            if (startTemp.getDayOfWeek() != DateTimeConstants.SATURDAY && startTemp.getDayOfWeek() != DateTimeConstants.SUNDAY) {
306                                    isOnlyWeekendSpan = false;
307                            }
308                            startTemp = startTemp.plusDays(1);
309                    }
310                    if (isOnlyWeekendSpan) {
311                            errors.add("Weekend day is selected, but include weekends checkbox is not checked");            //errorMessages
312                    }
313            }
314    
315            return errors;
316        }
317    }