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    }