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 }