001 /** 002 * Copyright 2004-2012 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 TimeDetailValidationService { 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 TimeDetailValidationService.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.getPayCalendarEntry(); 065 java.sql.Date asOfDate = payCalEntry.getEndPeriodDate(); 066 067 errors.addAll(TimeDetailValidationService.validateDates(startDateS, endDateS)); 068 errors.addAll(TimeDetailValidationService.validateTimes(startTimeS, endTimeS)); 069 if (errors.size() > 0) return errors; 070 // These methods use the UserTimeZone. 071 Long startTime = TKUtils.convertDateStringToTimestamp(startDateS, startTimeS).getTime(); 072 Long endTime = TKUtils.convertDateStringToTimestamp(endDateS, endTimeS).getTime(); 073 074 errors.addAll(validateInterval(payCalEntry, startTime, endTime)); 075 if (errors.size() > 0) return errors; 076 077 EarnCode earnCode = new EarnCode(); 078 if (StringUtils.isNotBlank(selectedEarnCode)) { 079 earnCode = TkServiceLocator.getEarnCodeService().getEarnCode(selectedEarnCode, asOfDate); 080 if (earnCode != null && earnCode.getRecordMethod()!= null && earnCode.getRecordMethod().equalsIgnoreCase(TkConstants.EARN_CODE_TIME)) { 081 if (startTimeS == null) errors.add("The start time is blank."); 082 if (endTimeS == null) errors.add("The end time is blank."); 083 if (startTime - endTime == 0) errors.add("Start time and end time cannot be equivalent"); 084 } 085 } 086 if (errors.size() > 0) return errors; 087 088 DateTime startTemp = new DateTime(startTime); 089 DateTime endTemp = new DateTime(endTime); 090 091 if (errors.size() == 0 && !acrossDays && !StringUtils.equals(TkConstants.EARN_CODE_CPE, overtimePref)) { 092 Hours hrs = Hours.hoursBetween(startTemp, endTemp); 093 if (hrs.getHours() >= 24) errors.add("One timeblock cannot exceed 24 hours"); 094 } 095 if (errors.size() > 0) return errors; 096 097 //Check that assignment is valid for both days 098 AssignmentDescriptionKey assignKey = TkServiceLocator.getAssignmentService().getAssignmentDescriptionKey(selectedAssignment); 099 Assignment assign = TkServiceLocator.getAssignmentService().getAssignment(assignKey, new Date(startTime)); 100 if (assign == null) errors.add("Assignment is not valid for start date " + TKUtils.formatDate(new Date(startTime))); 101 assign = TkServiceLocator.getAssignmentService().getAssignment(assignKey, new Date(endTime)); 102 if (assign == null) errors.add("Assignment is not valid for end date " + TKUtils.formatDate(new Date(endTime))); 103 if (errors.size() > 0) return errors; 104 105 //------------------------ 106 // some of the simple validations are in the js side in order to reduce the server calls 107 // 1. check if the begin / end time is empty - tk.calenadr.js 108 // 2. check the time format - timeparse.js 109 // 3. only allows decimals to be entered in the hour field 110 //------------------------ 111 112 //------------------------ 113 // check if the begin / end time are valid 114 //------------------------ 115 if ((startTime.compareTo(endTime) > 0 || endTime.compareTo(startTime) < 0)) { 116 errors.add("The time or date is not valid."); 117 } 118 if (errors.size() > 0) return errors; 119 120 //------------------------ 121 // check if the overnight shift is across days 122 //------------------------ 123 if (acrossDays && hours == null && amount == null) { 124 if (startTemp.getHourOfDay() >= endTemp.getHourOfDay() 125 && !(endTemp.getDayOfYear() - startTemp.getDayOfYear() <= 1 126 && endTemp.getHourOfDay() == 0)) { 127 errors.add("The \"apply to each day\" box should not be checked."); 128 } 129 } 130 if (errors.size() > 0) return errors; 131 132 //------------------------ 133 // Amount cannot be zero 134 //------------------------ 135 if (amount != null && earnCode != null && StringUtils.equals(earnCode.getEarnCodeType(), TkConstants.EARN_CODE_AMOUNT)) { 136 if (amount.equals(BigDecimal.ZERO)) { 137 errors.add("Amount cannot be zero."); 138 } 139 if (amount.scale() > 2) { 140 errors.add("Amount cannot have more than two digits after decimal point."); 141 } 142 } 143 if (errors.size() > 0) return errors; 144 145 //------------------------ 146 // check if the hours entered for hourly earn code is greater than 24 hours per day 147 // Hours cannot be zero 148 //------------------------ 149 if (hours != null && earnCode != null && StringUtils.equals(earnCode.getEarnCodeType(), TkConstants.EARN_CODE_HOUR)) { 150 if (hours.equals(BigDecimal.ZERO)) { 151 errors.add("Hours cannot be zero."); 152 } 153 if (hours.scale() > 2) { 154 errors.add("Hours cannot have more than two digits after decimal point."); 155 } 156 int dayDiff = endTemp.getDayOfYear() - startTemp.getDayOfYear() + 1; 157 if (hours.compareTo(new BigDecimal(dayDiff * 24)) == 1) { 158 errors.add("Cannot enter more than 24 hours per day."); 159 } 160 } 161 if (errors.size() > 0) return errors; 162 163 164 165 //------------------------ 166 // check if time blocks overlap with each other. Note that the tkTimeBlockId is used to 167 // determine is it's updating an existing time block or adding a new one 168 //------------------------ 169 170 boolean isRegularEarnCode = StringUtils.equals(assign.getJob().getPayTypeObj().getRegEarnCode(),selectedEarnCode); 171 errors.addAll(validateOverlap(startTime, endTime, acrossDays, startDateS, endTimeS,startTemp, endTemp, timesheetDocument, timeblockId, isRegularEarnCode)); 172 if (errors.size() > 0) return errors; 173 174 // Accrual Hour Limits Validation 175 //errors.addAll(TkServiceLocator.getTimeOffAccrualService().validateAccrualHoursLimitByEarnCode(timesheetDocument, selectedEarnCode)); 176 177 return errors; 178 } 179 180 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) { 181 List<String> errors = new ArrayList<String>(); 182 Interval addedTimeblockInterval = new Interval(startTime, endTime); 183 List<Interval> dayInt = new ArrayList<Interval>(); 184 185 //if the user is clocked in, check if this time block overlaps with the clock action 186 ClockLog lastClockLog = TkServiceLocator.getClockLogService().getLastClockLog(TKUser.getCurrentTargetPerson().getPrincipalId()); 187 if(lastClockLog != null && 188 (lastClockLog.getClockAction().equals(TkConstants.CLOCK_IN) 189 || lastClockLog.getClockAction().equals(TkConstants.LUNCH_IN))) { 190 Timestamp lastClockTimestamp = lastClockLog.getClockTimestamp(); 191 String lastClockZone = lastClockLog.getClockTimestampTimezone(); 192 if (StringUtils.isEmpty(lastClockZone)) { 193 lastClockZone = TKUtils.getSystemTimeZone(); 194 } 195 DateTimeZone zone = DateTimeZone.forID(lastClockZone); 196 DateTime clockWithZone = new DateTime(lastClockTimestamp, zone); 197 DateTime currentTime = new DateTime(System.currentTimeMillis(), zone); 198 Interval currentClockInInterval = new Interval(clockWithZone.getMillis(), currentTime.getMillis()); 199 200 if (isRegularEarnCode && addedTimeblockInterval.overlaps(currentClockInInterval)) { 201 errors.add("The time block you are trying to add overlaps with the current clock action."); 202 return errors; 203 } 204 } 205 206 if (acrossDays) { 207 DateTime start = new DateTime(startTime); 208 DateTime end = new DateTime(TKUtils.convertDateStringToTimestamp(startDateS, endTimeS).getTime()); 209 if (endTemp.getDayOfYear() - startTemp.getDayOfYear() < 1) { 210 end = new DateTime(endTime); 211 } 212 DateTime groupEnd = new DateTime(endTime); 213 Long startLong = start.getMillis(); 214 Long endLong = end.getMillis(); 215 //create interval span if start is before the end and the end is after the start except 216 //for when the end is midnight ..that converts to midnight of next day 217 DateMidnight midNight = new DateMidnight(endLong); 218 while (start.isBefore(groupEnd.getMillis()) && ((endLong >= startLong) || end.isEqual(midNight))) { 219 Interval tempInt = null; 220 if (end.isEqual(midNight)) { 221 tempInt = addedTimeblockInterval; 222 } else { 223 tempInt = new Interval(startLong, endLong); 224 } 225 dayInt.add(tempInt); 226 start = start.plusDays(1); 227 end = end.plusDays(1); 228 startLong = start.getMillis(); 229 endLong = end.getMillis(); 230 } 231 } else { 232 dayInt.add(addedTimeblockInterval); 233 } 234 235 for (TimeBlock timeBlock : timesheetDocument.getTimeBlocks()) { 236 if (errors.size() == 0 && StringUtils.equals(timeBlock.getEarnCodeType(), TkConstants.EARN_CODE_TIME)) { 237 Interval timeBlockInterval = new Interval(timeBlock.getBeginTimestamp().getTime(), timeBlock.getEndTimestamp().getTime()); 238 for (Interval intv : dayInt) { 239 if (timeBlockInterval.overlaps(intv) && (timeblockId == null || timeblockId.compareTo(timeBlock.getTkTimeBlockId()) != 0)) { 240 errors.add("The time block you are trying to add overlaps with an existing time block."); 241 } 242 } 243 } 244 } 245 246 return errors; 247 } 248 249 public static List<String> validateDates(String startDateS, String endDateS) { 250 List<String> errors = new ArrayList<String>(); 251 if (errors.size() == 0 && StringUtils.isEmpty(startDateS)) errors.add("The start date is blank."); 252 if (errors.size() == 0 && StringUtils.isEmpty(endDateS)) errors.add("The end date is blank."); 253 return errors; 254 } 255 256 public static List<String> validateTimes(String startTimeS, String endTimeS) { 257 List<String> errors = new ArrayList<String>(); 258 if (errors.size() == 0 && startTimeS == null) errors.add("The start time is blank."); 259 if (errors.size() == 0 && endTimeS == null) errors.add("The end time is blank."); 260 return errors; 261 } 262 263 public static List<String> validateInterval(CalendarEntries payCalEntry, Long startTime, Long endTime) { 264 List<String> errors = new ArrayList<String>(); 265 LocalDateTime pcb_ldt = payCalEntry.getBeginLocalDateTime(); 266 LocalDateTime pce_ldt = payCalEntry.getEndLocalDateTime(); 267 DateTimeZone utz = TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback(); 268 DateTime p_cal_b_dt = pcb_ldt.toDateTime(utz); 269 DateTime p_cal_e_dt = pce_ldt.toDateTime(utz); 270 271 Interval payInterval = new Interval(p_cal_b_dt, p_cal_e_dt); 272 if (errors.size() == 0 && !payInterval.contains(startTime)) { 273 errors.add("The start date/time is outside the pay period"); 274 } 275 if (errors.size() == 0 && !payInterval.contains(endTime) && p_cal_e_dt.getMillis() != endTime) { 276 errors.add("The end date/time is outside the pay period"); 277 } 278 return errors; 279 } 280 281 }