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.missedpunch; 017 018 import java.sql.Timestamp; 019 import java.text.ParseException; 020 import java.text.SimpleDateFormat; 021 import java.util.Date; 022 import java.util.HashSet; 023 import java.util.List; 024 import java.util.Set; 025 026 import org.apache.commons.lang.StringUtils; 027 import org.joda.time.DateTime; 028 import org.joda.time.DateTimeZone; 029 import org.joda.time.LocalTime; 030 import org.kuali.hr.time.assignment.Assignment; 031 import org.kuali.hr.time.assignment.AssignmentDescriptionKey; 032 import org.kuali.hr.time.clocklog.ClockLog; 033 import org.kuali.hr.time.service.base.TkServiceLocator; 034 import org.kuali.hr.time.timesheet.TimesheetDocument; 035 import org.kuali.hr.time.util.TKContext; 036 import org.kuali.hr.time.util.TKUtils; 037 import org.kuali.hr.time.util.TkConstants; 038 import org.kuali.rice.kew.api.KewApiServiceLocator; 039 import org.kuali.rice.kew.api.document.DocumentStatus; 040 import org.kuali.rice.krad.document.Document; 041 import org.kuali.rice.krad.rules.TransactionalDocumentRuleBase; 042 import org.kuali.rice.krad.util.GlobalVariables; 043 044 public class MissedPunchValidation extends TransactionalDocumentRuleBase { 045 046 @Override 047 protected boolean processCustomRouteDocumentBusinessRules(Document document) { 048 boolean valid = true; 049 050 MissedPunchDocument missedPunchDocument = (MissedPunchDocument) document; 051 DocumentStatus documentStatus = KewApiServiceLocator.getWorkflowDocumentService().getDocumentStatus(missedPunchDocument.getDocumentNumber()); 052 053 if (DocumentStatus.INITIATED.equals(DocumentStatus.fromCode(documentStatus.getCode())) 054 || DocumentStatus.SAVED.equals(DocumentStatus.fromCode(documentStatus.getCode()))) { 055 valid &= validateTimeSheet(missedPunchDocument); 056 valid &= validateAssignment(missedPunchDocument); 057 058 if (valid) { 059 ClockLog lastClock = TkServiceLocator.getClockLogService().getLastClockLog(missedPunchDocument.getPrincipalId()); 060 try { 061 valid &= validateClockAction(missedPunchDocument, lastClock); 062 valid &= validateClockTime(missedPunchDocument, lastClock); 063 } catch (ParseException e) { 064 e.printStackTrace(); 065 } 066 } 067 } 068 069 return valid; 070 } 071 072 // do not allow a missed punch if the time sheet document is enroute or final 073 boolean validateTimeSheet(MissedPunchDocument mp) { 074 boolean valid = true; 075 TimesheetDocument tsd = TkServiceLocator.getTimesheetService().getTimesheetDocument(mp.getTimesheetDocumentId()); 076 if(tsd != null 077 && (tsd.getDocumentHeader().getDocumentStatus().equals(TkConstants.ROUTE_STATUS.ENROUTE) 078 || tsd.getDocumentHeader().getDocumentStatus().equals(TkConstants.ROUTE_STATUS.FINAL))) { 079 GlobalVariables.getMessageMap().putError("document.timesheetDocumentId", "clock.mp.invalid.timesheet"); 080 valid = false; 081 } 082 083 return valid; 084 } 085 086 /** 087 * Checks the provided MissedPunch for a valid ClockAction. 088 * 089 * @param mp The MissedPunch we are validating. 090 * @param lastClock The ClockLog entry that happened before the one we want to create. 091 * 092 * @return true if valid, false otherwise. 093 */ 094 boolean validateClockAction(MissedPunchDocument mp, ClockLog lastClock) { 095 boolean valid = true; 096 Set<String> validActions = (lastClock != null) ? TkConstants.CLOCK_ACTION_TRANSITION_MAP.get(lastClock.getClockAction()) : new HashSet<String>(); 097 098 099 if (mp.getClockAction().equals(TkConstants.CLOCK_OUT) || mp.getClockAction().equals(TkConstants.LUNCH_OUT)) { 100 ClockLog lci = TkServiceLocator.getClockLogService().getLastClockLog(mp.getPrincipalId(),TkConstants.CLOCK_IN); //last clock in 101 ClockLog lli = TkServiceLocator.getClockLogService().getLastClockLog(mp.getPrincipalId(),TkConstants.LUNCH_IN); //last lunch in 102 if (lci != null) { 103 MissedPunchDocument mpd = TkServiceLocator.getMissedPunchService().getMissedPunchByClockLogId(lci.getTkClockLogId()); 104 if(mpd != null) { 105 GlobalVariables.getMessageMap().putError("document.clockAction", "clock.mp.onlyOne.action"); 106 return false; 107 } 108 } else if(lli != null) { 109 MissedPunchDocument mpd = TkServiceLocator.getMissedPunchService().getMissedPunchByClockLogId(lli.getTkClockLogId()); 110 if(mpd != null) { 111 GlobalVariables.getMessageMap().putError("document.clockAction", "clock.mp.onlyOne.action"); 112 return false; 113 } 114 } 115 } 116 117 // if a clockIn/lunchIn has been put in by missed punch, do not allow missed punch for clockOut/LunchOut 118 // missed punch can only be used on a tiemblock once. 119 // if(lastClock != null 120 // && (lastClock.getClockAction().equals(TkConstants.CLOCK_IN) || lastClock.getClockAction().equals(TkConstants.LUNCH_IN))) { 121 // MissedPunchDocument mpd = TkServiceLocator.getMissedPunchService().getMissedPunchByClockLogId(lastClock.getTkClockLogId()); 122 // if(mpd != null) { 123 // GlobalVariables.getMessageMap().putError("document.clockAction", "clock.mp.onlyOne.action"); 124 // return false; 125 // } 126 // } 127 if (!StringUtils.equals("A", mp.getDocumentStatus()) && !validActions.contains(mp.getClockAction())) { 128 GlobalVariables.getMessageMap().putError("document.clockAction", "clock.mp.invalid.action"); 129 valid = false; 130 } 131 132 return valid; 133 } 134 135 /** 136 * Checks whether the chosen assignment is valid (both chosen and within proper timeframe). 137 * Checks whether the provided MissedPunch has a valid time based on the 138 * previous ClockLog. 139 * 140 * @param mp The MissedPunch we are validating. 141 * @param lastClock The ClockLog entry that happened before the one we want to create. 142 * 143 * @return true if valid, false otherwise. 144 * @throws ParseException 145 */ 146 boolean validateClockTime(MissedPunchDocument mp, ClockLog lastClock) throws ParseException { 147 boolean valid = true; 148 149 if (lastClock == null) { 150 return valid; 151 } 152 153 //Missed Action Date and Missed Action Time are required fields. KPME-1853 154 if(mp.getActionTime() == null || mp.getActionDate() == null) 155 return false; 156 157 DateTime clockLogDateTime = new DateTime(lastClock.getClockTimestamp().getTime()); 158 DateTime boundaryMax = clockLogDateTime.plusDays(1); 159 DateTime nowTime = new DateTime(TKUtils.getCurrentDate()); 160 long offset = TkServiceLocator.getTimezoneService().getTimezoneOffsetFromServerTime(TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback()); 161 long dateTimeLocal = new LocalTime(mp.getActionTime()).getMillisOfDay() + mp.getActionDate().getTime() - offset; 162 163 //this will be in system's timezone, but offset with user's timezone 164 DateTime actionDateTime = new DateTime(dateTimeLocal); 165 166 // if date is a future date 167 if(actionDateTime.getYear()> nowTime.getYear() 168 || (actionDateTime.getYear()==nowTime.getYear() && actionDateTime.getDayOfYear() > nowTime.getDayOfYear())) { 169 GlobalVariables.getMessageMap().putError("document.actionDate", "clock.mp.future.date"); 170 return false; 171 } 172 173 // if time is a future time 174 if(actionDateTime.getMillis() > nowTime.getMillis()) { 175 GlobalVariables.getMessageMap().putError("document.actionTime", "clock.mp.future.time"); 176 return false; 177 } 178 179 if ((!StringUtils.equals(lastClock.getClockAction(), TkConstants.CLOCK_OUT) && actionDateTime.isAfter(boundaryMax)) 180 || actionDateTime.isBefore(clockLogDateTime)) { 181 GlobalVariables.getMessageMap().putError("document.actionTime", "clock.mp.invalid.datetime"); 182 valid = false; 183 } 184 185 return valid; 186 } 187 188 /** 189 * Checks whether the chosen assignment is valid (both chosen and within proper timeframe). 190 * 191 * @param mp The MissedPunch we are validating. 192 * 193 * @return true if valid, false otherwise. 194 * @throws ParseException 195 */ 196 boolean validateAssignment(MissedPunchDocument mp) { 197 boolean valid = true; 198 199 String assignment = mp.getAssignment(); 200 201 if (mp.getAssignment() == null) { 202 GlobalVariables.getMessageMap().putError("document.assignment", "clock.mp.assignment.required"); 203 valid = false; 204 } 205 206 if (valid) 207 { 208 AssignmentDescriptionKey key = new AssignmentDescriptionKey(assignment); 209 if (key != null) { 210 Assignment assignmentObj = TkServiceLocator.getAssignmentService().getAssignment(mp.getPrincipalId(), key, new java.sql.Date(mp.getActionDate().getTime())); 211 if (assignmentObj == null) { 212 GlobalVariables.getMessageMap().putError("document.assignment", "clock.mp.assignment.active"); 213 valid = false; 214 } 215 } 216 else { 217 valid = false; 218 } 219 } 220 221 return valid; 222 } 223 }