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