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 }