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