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 org.apache.commons.lang.StringUtils;
019    import org.joda.time.DateTime;
020    import org.joda.time.LocalTime;
021    import org.kuali.hr.time.assignment.Assignment;
022    import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
023    import org.kuali.hr.time.clocklog.ClockLog;
024    import org.kuali.hr.time.service.base.TkServiceLocator;
025    import org.kuali.hr.time.timesheet.TimesheetDocument;
026    import org.kuali.hr.time.util.TKUtils;
027    import org.kuali.hr.time.util.TkConstants;
028    import org.kuali.rice.kew.api.KewApiServiceLocator;
029    import org.kuali.rice.kew.api.document.DocumentStatus;
030    import org.kuali.rice.krad.document.Document;
031    import org.kuali.rice.krad.rules.TransactionalDocumentRuleBase;
032    import org.kuali.rice.krad.util.GlobalVariables;
033    
034    import java.text.ParseException;
035    import java.util.HashSet;
036    import java.util.Set;
037    
038    public class MissedPunchValidation extends TransactionalDocumentRuleBase {
039            
040            @Override
041            protected boolean processCustomRouteDocumentBusinessRules(Document document) {
042            boolean valid = true;
043            
044            MissedPunchDocument missedPunchDocument = (MissedPunchDocument) document;
045            DocumentStatus documentStatus = KewApiServiceLocator.getWorkflowDocumentService().getDocumentStatus(missedPunchDocument.getDocumentNumber());
046            
047            if (DocumentStatus.INITIATED.equals(DocumentStatus.fromCode(documentStatus.getCode()))
048                            || DocumentStatus.SAVED.equals(DocumentStatus.fromCode(documentStatus.getCode()))) {
049                    valid &= validateTimeSheet(missedPunchDocument);
050                valid &= validateAssignment(missedPunchDocument);
051    
052                    if (valid) {
053                            ClockLog lastClock = TkServiceLocator.getClockLogService().getLastClockLog(missedPunchDocument.getPrincipalId());
054                            try {
055                                    valid &= validateClockAction(missedPunchDocument, lastClock);
056                                    valid &= validateClockTime(missedPunchDocument, lastClock);
057                                    } catch (ParseException e) {
058                                            e.printStackTrace();
059                                    }
060                    }
061            }
062                
063            return valid;
064            }
065            
066        // do not allow a missed punch if the time sheet document is enroute or final
067        boolean validateTimeSheet(MissedPunchDocument mp) {
068            boolean valid = true;
069            TimesheetDocument tsd = TkServiceLocator.getTimesheetService().getTimesheetDocument(mp.getTimesheetDocumentId());
070            if(tsd != null 
071                            && (tsd.getDocumentHeader().getDocumentStatus().equals(TkConstants.ROUTE_STATUS.ENROUTE) 
072                                            || tsd.getDocumentHeader().getDocumentStatus().equals(TkConstants.ROUTE_STATUS.FINAL))) {
073                    GlobalVariables.getMessageMap().putError("document.timesheetDocumentId", "clock.mp.invalid.timesheet");
074                    valid = false;
075            }
076            
077            return valid;
078        }
079    
080        /**
081         * Checks the provided MissedPunch for a valid ClockAction.
082         *
083         * @param mp The MissedPunch we are validating.
084         * @param lastClock The ClockLog entry that happened before the one we want to create.
085         *
086         * @return true if valid, false otherwise.
087         */
088        boolean validateClockAction(MissedPunchDocument mp, ClockLog lastClock) {
089            boolean valid = true;
090            Set<String> validActions = (lastClock != null) ? TkConstants.CLOCK_ACTION_TRANSITION_MAP.get(lastClock.getClockAction()) : new HashSet<String>();
091    
092    
093            if (mp.getClockAction().equals(TkConstants.CLOCK_OUT) || mp.getClockAction().equals(TkConstants.LUNCH_OUT)) {
094                ClockLog lci = TkServiceLocator.getClockLogService().getLastClockLog(mp.getPrincipalId(),TkConstants.CLOCK_IN); //last clock in
095                ClockLog lli = TkServiceLocator.getClockLogService().getLastClockLog(mp.getPrincipalId(),TkConstants.LUNCH_IN); //last lunch in
096                if (lci != null) {
097                    MissedPunchDocument mpd = TkServiceLocator.getMissedPunchService().getMissedPunchByClockLogId(lci.getTkClockLogId());
098                    if(mpd != null) {
099                        GlobalVariables.getMessageMap().putError("document.clockAction", "clock.mp.onlyOne.action");
100                        return false;
101                    }
102                } else if(lli != null) {
103                    MissedPunchDocument mpd = TkServiceLocator.getMissedPunchService().getMissedPunchByClockLogId(lli.getTkClockLogId());
104                    if(mpd != null) {
105                        GlobalVariables.getMessageMap().putError("document.clockAction", "clock.mp.onlyOne.action");
106                        return false;
107                    }
108                }
109            }
110    
111            // if a clockIn/lunchIn has been put in by missed punch, do not allow missed punch for clockOut/LunchOut
112            // missed punch can only be used on a tiemblock once.
113    //        if(lastClock != null
114    //                      && (lastClock.getClockAction().equals(TkConstants.CLOCK_IN) || lastClock.getClockAction().equals(TkConstants.LUNCH_IN))) {
115    //              MissedPunchDocument mpd = TkServiceLocator.getMissedPunchService().getMissedPunchByClockLogId(lastClock.getTkClockLogId());
116    //              if(mpd != null) {
117    //                      GlobalVariables.getMessageMap().putError("document.clockAction", "clock.mp.onlyOne.action");
118    //                  return false;
119    //              }
120    //        }
121            if (!StringUtils.equals("A", mp.getDocumentStatus()) && !validActions.contains(mp.getClockAction())) {
122                GlobalVariables.getMessageMap().putError("document.clockAction", "clock.mp.invalid.action");
123                valid = false;
124            }
125            
126            return valid;
127        }
128    
129        /**
130         * Checks whether the chosen assignment is valid (both chosen and within proper timeframe).
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        /**
183         * Checks whether the chosen assignment is valid (both chosen and within proper timeframe).
184         *
185         * @param mp The MissedPunch we are validating.
186         *
187         * @return true if valid, false otherwise.
188         * @throws ParseException
189         */
190        boolean validateAssignment(MissedPunchDocument mp) {
191            boolean valid = true;
192    
193            String assignment = mp.getAssignment();
194    
195            if (mp.getAssignment() == null) {
196                GlobalVariables.getMessageMap().putError("document.assignment", "clock.mp.assignment.required");
197                valid = false;
198            }
199    
200            if (valid)
201            {
202                AssignmentDescriptionKey key = new AssignmentDescriptionKey(assignment);
203                if (key != null) {
204                    Assignment assignmentObj = TkServiceLocator.getAssignmentService().getAssignment(mp.getPrincipalId(), key, new java.sql.Date(mp.getActionDate().getTime()));
205                    if (assignmentObj == null) {
206                        GlobalVariables.getMessageMap().putError("document.assignment", "clock.mp.assignment.active");
207                        valid = false;
208                    }
209                }
210                else {
211                    valid = false;
212                }
213            }
214    
215            return valid;
216        }
217    }