View Javadoc

1   /**
2    * Copyright 2004-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.hr.time.detail.validation;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.joda.time.*;
20  import org.kuali.hr.time.assignment.Assignment;
21  import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
22  import org.kuali.hr.time.calendar.CalendarEntries;
23  import org.kuali.hr.time.clocklog.ClockLog;
24  import org.kuali.hr.time.detail.web.TimeDetailActionFormBase;
25  import org.kuali.hr.time.earncode.EarnCode;
26  import org.kuali.hr.time.service.base.TkServiceLocator;
27  import org.kuali.hr.time.timeblock.TimeBlock;
28  import org.kuali.hr.time.timesheet.TimesheetDocument;
29  import org.kuali.hr.time.util.TKUser;
30  import org.kuali.hr.time.util.TKUtils;
31  import org.kuali.hr.time.util.TkConstants;
32  
33  import java.math.BigDecimal;
34  import java.sql.Date;
35  import java.sql.Timestamp;
36  import java.util.ArrayList;
37  import java.util.List;
38  
39  public class TimeDetailValidationUtil {
40  
41      /**
42       * Convenience method for handling validation directly from the form object.
43       * @param tdaf The populated form.
44       *
45       * @return A list of error strings.
46       */
47      public static List<String> validateTimeEntryDetails(TimeDetailActionFormBase tdaf) {
48          return TimeDetailValidationUtil.validateTimeEntryDetails(
49                  tdaf.getHours(), tdaf.getAmount(), tdaf.getStartTime(), tdaf.getEndTime(),
50                  tdaf.getStartDate(), tdaf.getEndDate(), tdaf.getTimesheetDocument(),
51                  tdaf.getSelectedEarnCode(), tdaf.getSelectedAssignment(),
52                  tdaf.getAcrossDays().equalsIgnoreCase("y"), tdaf.getTkTimeBlockId(), tdaf.getOvertimePref(), tdaf.getSpanningWeeks().equalsIgnoreCase("y")
53          );
54      }
55  
56      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) {
57          List<String> errors = new ArrayList<String>();
58  
59          if (timesheetDocument == null) {
60              errors.add("No timesheet document found.");
61          }
62          if (errors.size() > 0) return errors;
63  
64          CalendarEntries payCalEntry = timesheetDocument.getCalendarEntry();
65          java.sql.Date asOfDate = payCalEntry.getEndPeriodDate();
66  
67          errors.addAll(TimeDetailValidationUtil.validateDates(startDateS, endDateS));
68          errors.addAll(TimeDetailValidationUtil.validateTimes(startTimeS, endTimeS));
69          if (errors.size() > 0) return errors;
70  
71          Long startTime;
72          Long endTime;
73          //if startTime and end time are equal, lets ignore timezone
74          if (startTimeS.equals(endTimeS)
75                  && startDateS.equals(endDateS)) {
76              startTime = TKUtils.convertDateStringToTimestampWithoutZone(startDateS, startTimeS).getTime();
77              endTime = TKUtils.convertDateStringToTimestampWithoutZone(endDateS, endTimeS).getTime();
78          } else {
79          // These methods use the UserTimeZone.
80              startTime = TKUtils.convertDateStringToTimestamp(startDateS, startTimeS).getTime();
81              endTime = TKUtils.convertDateStringToTimestamp(endDateS, endTimeS).getTime();
82          }
83  
84          errors.addAll(validateInterval(payCalEntry, startTime, endTime));
85          if (errors.size() > 0) return errors;
86  
87          EarnCode earnCode = new EarnCode();
88          if (StringUtils.isNotBlank(selectedEarnCode)) {
89              earnCode = TkServiceLocator.getEarnCodeService().getEarnCode(selectedEarnCode, asOfDate);
90  
91              if (earnCode != null && earnCode.getRecordMethod()!= null && earnCode.getRecordMethod().equalsIgnoreCase(TkConstants.EARN_CODE_TIME)) {
92                  if (startTimeS == null) errors.add("The start time is blank.");
93                  if (endTimeS == null) errors.add("The end time is blank.");
94                  if (startTime - endTime == 0) errors.add("Start time and end time cannot be equivalent");
95              }
96          }
97          if (errors.size() > 0) return errors;
98  
99          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.getCurrentTargetPersonId());
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 }