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 java.math.BigDecimal;
19  import java.sql.Date;
20  import java.sql.Timestamp;
21  import java.util.ArrayList;
22  import java.util.List;
23  
24  import org.apache.commons.lang.StringUtils;
25  import org.joda.time.DateMidnight;
26  import org.joda.time.DateTime;
27  import org.joda.time.DateTimeConstants;
28  import org.joda.time.DateTimeZone;
29  import org.joda.time.Hours;
30  import org.joda.time.Interval;
31  import org.joda.time.LocalDateTime;
32  import org.kuali.hr.time.assignment.Assignment;
33  import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
34  import org.kuali.hr.time.calendar.CalendarEntries;
35  import org.kuali.hr.time.clocklog.ClockLog;
36  import org.kuali.hr.time.detail.web.TimeDetailActionFormBase;
37  import org.kuali.hr.time.earncode.EarnCode;
38  import org.kuali.hr.time.service.base.TkServiceLocator;
39  import org.kuali.hr.time.timeblock.TimeBlock;
40  import org.kuali.hr.time.timesheet.TimesheetDocument;
41  import org.kuali.hr.time.util.TKUser;
42  import org.kuali.hr.time.util.TKUtils;
43  import org.kuali.hr.time.util.TkConstants;
44  
45  public class TimeDetailValidationUtil {
46  
47      /**
48       * Convenience method for handling validation directly from the form object.
49       * @param tdaf The populated form.
50       *
51       * @return A list of error strings.
52       */
53      public static List<String> validateTimeEntryDetails(TimeDetailActionFormBase tdaf) {
54          return TimeDetailValidationUtil.validateTimeEntryDetails(
55                  tdaf.getHours(), tdaf.getAmount(), tdaf.getStartTime(), tdaf.getEndTime(),
56                  tdaf.getStartDate(), tdaf.getEndDate(), tdaf.getTimesheetDocument(),
57                  tdaf.getSelectedEarnCode(), tdaf.getSelectedAssignment(),
58                  tdaf.getAcrossDays().equalsIgnoreCase("y"), tdaf.getTkTimeBlockId(), tdaf.getOvertimePref(), tdaf.getSpanningWeeks().equalsIgnoreCase("y")
59          );
60      }
61  
62      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) {
63          List<String> errors = new ArrayList<String>();
64  
65          if (timesheetDocument == null) {
66              errors.add("No timesheet document found.");
67          }
68          if (errors.size() > 0) return errors;
69  
70          CalendarEntries payCalEntry = timesheetDocument.getCalendarEntry();
71          java.sql.Date asOfDate = payCalEntry.getEndPeriodDate();
72  
73          errors.addAll(TimeDetailValidationUtil.validateDates(startDateS, endDateS));
74          errors.addAll(TimeDetailValidationUtil.validateTimes(startTimeS, endTimeS));
75          if (errors.size() > 0) return errors;
76  
77          Long startTime;
78          Long endTime;
79  
80          startTime = TKUtils.convertDateStringToTimestampWithoutZone(startDateS, startTimeS).getTime();
81          endTime = TKUtils.convertDateStringToTimestampWithoutZone(endDateS, endTimeS).getTime();
82  
83          errors.addAll(validateInterval(payCalEntry, startTime, endTime));
84          if (errors.size() > 0) return errors;
85  
86          EarnCode earnCode = new EarnCode();
87          if (StringUtils.isNotBlank(selectedEarnCode)) {
88              earnCode = TkServiceLocator.getEarnCodeService().getEarnCode(selectedEarnCode, asOfDate);
89  
90              if (earnCode != null && earnCode.getRecordMethod()!= null && earnCode.getRecordMethod().equalsIgnoreCase(TkConstants.EARN_CODE_TIME)) {
91                  if (startTimeS == null) errors.add("The start time is blank.");
92                  if (endTimeS == null) errors.add("The end time is blank.");
93                  if (startTime - endTime == 0) errors.add("Start time and end time cannot be equivalent");
94              }
95          }
96          if (errors.size() > 0) return errors;
97  
98          DateTime startTemp = new DateTime(startTime);
99          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 }