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 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 (isRegularEarnCode && 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 }