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 }