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.timesheet.service;
017    
018    import org.apache.commons.collections.CollectionUtils;
019    import org.apache.commons.lang.StringUtils;
020    import org.apache.log4j.Logger;
021    import org.joda.time.DateTime;
022    import org.kuali.hr.job.Job;
023    import org.kuali.hr.lm.LMConstants;
024    import org.kuali.hr.lm.leaveblock.LeaveBlock;
025    import org.kuali.hr.lm.timeoff.SystemScheduledTimeOff;
026    import org.kuali.hr.time.assignment.Assignment;
027    import org.kuali.hr.time.calendar.CalendarEntries;
028    import org.kuali.hr.time.earncode.EarnCode;
029    import org.kuali.hr.time.principal.PrincipalHRAttributes;
030    import org.kuali.hr.time.service.base.TkServiceLocator;
031    import org.kuali.hr.time.timeblock.TimeBlock;
032    import org.kuali.hr.time.timesheet.TimesheetDocument;
033    import org.kuali.hr.time.util.TKContext;
034    import org.kuali.hr.time.util.TKUser;
035    import org.kuali.hr.time.util.TKUtils;
036    import org.kuali.hr.time.util.TkConstants;
037    import org.kuali.hr.time.workflow.TimesheetDocumentHeader;
038    import org.kuali.rice.core.api.config.property.ConfigContext;
039    import org.kuali.rice.kew.api.KewApiServiceLocator;
040    import org.kuali.rice.kew.api.WorkflowDocument;
041    import org.kuali.rice.kew.api.WorkflowDocumentFactory;
042    import org.kuali.rice.kew.api.exception.WorkflowException;
043    import org.kuali.rice.kew.api.note.Note;
044    import org.kuali.rice.kim.api.identity.principal.EntityNamePrincipalName;
045    import org.kuali.rice.kim.api.identity.principal.Principal;
046    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
047    
048    import java.math.BigDecimal;
049    import java.sql.Timestamp;
050    import java.util.ArrayList;
051    import java.util.Date;
052    import java.util.LinkedList;
053    import java.util.List;
054    
055    public class TimesheetServiceImpl implements TimesheetService {
056    
057        @SuppressWarnings("unused")
058        private static final Logger LOG = Logger.getLogger(TimesheetServiceImpl.class);
059    
060        @Override
061        public void routeTimesheet(String principalId, TimesheetDocument timesheetDocument) {
062            routeTimesheet(principalId, timesheetDocument, TkConstants.DOCUMENT_ACTIONS.ROUTE);
063        }
064    
065        @Override
066        public void routeTimesheet(String principalId, TimesheetDocument timesheetDocument, String action) {
067            timesheetAction(action, principalId, timesheetDocument);
068        }
069    
070        @Override
071        public void approveTimesheet(String principalId, TimesheetDocument timesheetDocument) {
072            timesheetAction(TkConstants.DOCUMENT_ACTIONS.APPROVE, principalId, timesheetDocument);
073        }
074    
075        @Override
076        public void approveTimesheet(String principalId, TimesheetDocument timesheetDocument, String action) {
077            timesheetAction(action, principalId, timesheetDocument);
078        }
079    
080        @Override
081        public void disapproveTimesheet(String principalId, TimesheetDocument timesheetDocument) {
082            timesheetAction(TkConstants.DOCUMENT_ACTIONS.DISAPPROVE, principalId, timesheetDocument);
083        }
084    
085        protected void timesheetAction(String action, String principalId, TimesheetDocument timesheetDocument) {
086            WorkflowDocument wd = null;
087            if (timesheetDocument != null) {
088                String rhid = timesheetDocument.getDocumentId();
089                wd = WorkflowDocumentFactory.loadDocument(principalId, rhid);
090    
091                if (StringUtils.equals(action, TkConstants.DOCUMENT_ACTIONS.ROUTE)) {
092                    wd.route("Routing for Approval");
093                } else if (StringUtils.equals(action, TkConstants.BATCH_JOB_ACTIONS.BATCH_JOB_ROUTE)) {
094                    Note.Builder builder = Note.Builder.create(rhid, principalId);
095                    builder.setCreateDate(new DateTime());
096                    builder.setText("Routed via Employee Approval batch job");
097                    KewApiServiceLocator.getNoteService().createNote(builder.build());
098                    
099                    wd.route("Batch job routing timesheet");
100                } else if (StringUtils.equals(action, TkConstants.DOCUMENT_ACTIONS.APPROVE)) {
101                    if (TKUser.getCurrentTargetRoles().isSystemAdmin() &&
102                            !TKUser.getCurrentTargetRoles().isApproverForTimesheet(timesheetDocument)) {
103                        wd.superUserBlanketApprove("Superuser approving timesheet.");
104                    } else {
105                        wd.approve("Approving timesheet.");
106                    }
107                } else if (StringUtils.equals(action, TkConstants.BATCH_JOB_ACTIONS.BATCH_JOB_APPROVE)) {
108                    Note.Builder builder = Note.Builder.create(rhid, principalId);
109                            builder.setCreateDate(new DateTime());
110                            builder.setText("Approved via Supervisor Approval batch job");
111                            KewApiServiceLocator.getNoteService().createNote(builder.build());
112                    
113                    wd.superUserBlanketApprove("Batch job approving timesheet.");
114                } else if (StringUtils.equals(action, TkConstants.DOCUMENT_ACTIONS.DISAPPROVE)) {
115                    if (TKUser.getCurrentTargetRoles().isSystemAdmin()
116                            && !TKUser.getCurrentTargetRoles().isApproverForTimesheet(timesheetDocument)) {
117                        wd.superUserDisapprove("Superuser disapproving timesheet.");
118                    } else {
119                        wd.disapprove("Disapproving timesheet.");
120                    }
121                }
122            }
123        }
124    
125        @Override
126        public TimesheetDocument openTimesheetDocument(String principalId, CalendarEntries calendarDates) throws WorkflowException {
127            TimesheetDocument timesheetDocument = null;
128    
129            Date begin = calendarDates.getBeginPeriodDateTime();
130            Date end = calendarDates.getEndPeriodDateTime();
131    
132            TimesheetDocumentHeader header = TkServiceLocator.getTimesheetDocumentHeaderService().getDocumentHeader(principalId, begin, end);
133    
134            if (header == null) {
135                List<Assignment> activeAssignments = TkServiceLocator.getAssignmentService().getAssignmentsByCalEntryForTimeCalendar(principalId, calendarDates);
136                //TkServiceLocator.getAssignmentService().getAssignments(principalId, TKUtils.getTimelessDate(payCalendarDates.getEndPeriodDate()));
137                if (activeAssignments.size() == 0) {
138                    LOG.warn("No active assignments for " + principalId + " for " + calendarDates.getEndPeriodDate());
139                    return null;
140                    //throw new RuntimeException("No active assignments for " + principalId + " for " + calendarDates.getEndPeriodDate());
141                }
142                
143                EntityNamePrincipalName person = KimApiServiceLocator.getIdentityService().getDefaultNamesForPrincipalId(principalId);
144                String principalName = person != null && person.getDefaultName() != null ? person.getDefaultName().getCompositeName() : StringUtils.EMPTY;
145                String endDateString = TKUtils.formatDate(new java.sql.Date(end.getTime()));
146                String timesheetDocumentTitle = TimesheetDocument.TIMESHEET_DOCUMENT_TYPE + " - " + principalName + " (" + principalId + ") - " + endDateString;
147                
148                timesheetDocument = this.initiateWorkflowDocument(principalId, begin, end, calendarDates, TimesheetDocument.TIMESHEET_DOCUMENT_TYPE, timesheetDocumentTitle);
149                //timesheetDocument.setPayCalendarEntry(calendarDates);
150                //this.loadTimesheetDocumentData(timesheetDocument, principalId, calendarDates);
151                //TODO switch this to scheduled time offs
152                //this.loadHolidaysOnTimesheet(timesheetDocument, principalId, begin, end);
153            } else {
154                timesheetDocument = this.getTimesheetDocument(header.getDocumentId());
155                if (timesheetDocument != null) {
156                    timesheetDocument.setCalendarEntry(calendarDates);
157                }
158            }
159    
160            if (timesheetDocument != null) {
161                timesheetDocument.setTimeSummary(TkServiceLocator.getTimeSummaryService().getTimeSummary(timesheetDocument));
162            }
163            return timesheetDocument;
164        }
165    
166        public void loadHolidaysOnTimesheet(TimesheetDocument timesheetDocument, String principalId, Date beginDate, Date endDate) {
167            PrincipalHRAttributes principalCalendar = TkServiceLocator.getPrincipalHRAttributeService().getPrincipalCalendar(principalId, new java.sql.Date(beginDate.getTime()));
168            if (principalCalendar != null && StringUtils.isNotEmpty(principalCalendar.getLeavePlan())) {
169                    List<SystemScheduledTimeOff> sstoList = TkServiceLocator.getSysSchTimeOffService()
170                            .getSystemScheduledTimeOffForPayPeriod(principalCalendar.getLeavePlan(), beginDate, endDate);
171                    Assignment sstoAssign = TkServiceLocator.getAssignmentService().getAssignmentToApplyScheduledTimeOff(timesheetDocument, TKUtils.getTimelessDate(endDate));
172                    if (sstoAssign != null) {
173                            for(SystemScheduledTimeOff ssto : sstoList) {
174                      BigDecimal sstoCalcHours = TkServiceLocator.getSysSchTimeOffService().calculateSysSchTimeOffHours(sstoAssign.getJob(), ssto.getAmountofTime());
175                      TimeBlock timeBlock = TkServiceLocator.getTimeBlockService().createTimeBlock(timesheetDocument, new Timestamp(ssto.getScheduledTimeOffDate().getTime()),
176                              new Timestamp(ssto.getScheduledTimeOffDate().getTime()), sstoAssign, TkConstants.HOLIDAY_EARN_CODE, sstoCalcHours, BigDecimal.ZERO, false, false, TKContext.getPrincipalId());
177                      timesheetDocument.getTimeBlocks().add(timeBlock);
178                  }
179                        //If system scheduled time off are loaded will need to save them to the database
180                            if (CollectionUtils.isNotEmpty(sstoList)) {
181                               TkServiceLocator.getTimeBlockService().saveTimeBlocks(new LinkedList<TimeBlock>(), timesheetDocument.getTimeBlocks(), TKContext.getPrincipalId());
182                            }
183                    }
184            }
185        }
186    
187        protected TimesheetDocument initiateWorkflowDocument(String principalId, Date payBeginDate,  Date payEndDate, CalendarEntries calendarEntries, String documentType, String title) throws WorkflowException {
188            TimesheetDocument timesheetDocument = null;
189            WorkflowDocument workflowDocument = null;
190    
191            workflowDocument = WorkflowDocumentFactory.createDocument(principalId, documentType, title);
192    
193            String status = workflowDocument.getStatus().getCode();
194            TimesheetDocumentHeader documentHeader = new TimesheetDocumentHeader(workflowDocument.getDocumentId(), principalId, payBeginDate, payEndDate, status);
195    
196            documentHeader.setDocumentId(workflowDocument.getDocumentId().toString());
197            documentHeader.setDocumentStatus("I");
198    
199            TkServiceLocator.getTimesheetDocumentHeaderService().saveOrUpdate(documentHeader);
200            timesheetDocument = new TimesheetDocument(documentHeader);
201            timesheetDocument.setCalendarEntry(calendarEntries);
202            loadTimesheetDocumentData(timesheetDocument, principalId, calendarEntries);
203            TkServiceLocator.getTkSearchableAttributeService().updateSearchableAttribute(timesheetDocument, payEndDate);
204    
205            if (TkServiceLocator.getLeaveApprovalService().isActiveAssignmentFoundOnJobFlsaStatus(principalId, TkConstants.FLSA_STATUS_NON_EXEMPT, true)) {
206                    deleteNonApprovedLeaveBlocks(principalId, calendarEntries.getBeginPeriodDate(), calendarEntries.getEndPeriodDate());
207            }
208            
209            return timesheetDocument;
210        }
211        
212        private void deleteNonApprovedLeaveBlocks(String principalId, Date beginDate, Date endDate) {
213            String batchUserPrincipalId = getBatchUserPrincipalId();
214            
215            if (batchUserPrincipalId != null) {
216                    List<LeaveBlock> leaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocks(principalId, beginDate, endDate);
217            
218                    for (LeaveBlock leaveBlock : leaveBlocks) {
219                            if (!StringUtils.equals(leaveBlock.getRequestStatus(), LMConstants.REQUEST_STATUS.APPROVED)) {
220                                    TkServiceLocator.getLeaveBlockService().deleteLeaveBlock(leaveBlock.getLmLeaveBlockId(), batchUserPrincipalId);
221                            }
222                    }
223            } else {
224                    String principalName = ConfigContext.getCurrentContextConfig().getProperty(TkConstants.BATCH_USER_PRINCIPAL_NAME);
225                    LOG.error("Could not delete leave request blocks due to missing batch user " + principalName);
226            }
227        }
228        
229        private String getBatchUserPrincipalId() {
230            String principalName = ConfigContext.getCurrentContextConfig().getProperty(TkConstants.BATCH_USER_PRINCIPAL_NAME);
231            Principal principal = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(principalName);
232            return principal == null ? null : principal.getPrincipalId();
233        }
234    
235        public List<TimeBlock> getPrevDocumentTimeBlocks(String principalId, Date payBeginDate) {
236            TimesheetDocumentHeader prevTdh = TkServiceLocator.getTimesheetDocumentHeaderService().getPreviousDocumentHeader(principalId, payBeginDate);
237            if (prevTdh == null) {
238                return new ArrayList<TimeBlock>();
239            }
240            return TkServiceLocator.getTimeBlockService().getTimeBlocks(prevTdh.getDocumentId());
241        }
242    
243        @Override
244        public TimesheetDocument getTimesheetDocument(String documentId) {
245            TimesheetDocument timesheetDocument = null;
246            TimesheetDocumentHeader tdh = TkServiceLocator.getTimesheetDocumentHeaderService().getDocumentHeader(documentId);
247    
248            if (tdh != null) {
249                timesheetDocument = new TimesheetDocument(tdh);
250                CalendarEntries pce = TkServiceLocator.getCalendarService().getCalendarDatesByPayEndDate(tdh.getPrincipalId(), tdh.getEndDate(), TkConstants.PAY_CALENDAR_TYPE);
251                loadTimesheetDocumentData(timesheetDocument, tdh.getPrincipalId(), pce);
252    
253                timesheetDocument.setCalendarEntry(pce);
254            } else {
255                LOG.error("Could not find TimesheetDocumentHeader for DocumentID: " + documentId);
256                return null;
257            }
258            return timesheetDocument;
259        }
260    
261        protected void loadTimesheetDocumentData(TimesheetDocument tdoc, String principalId, CalendarEntries payCalEntry) {
262            tdoc.setAssignments(TkServiceLocator.getAssignmentService().getAssignmentsByCalEntryForTimeCalendar(principalId, payCalEntry));
263            if (payCalEntry != null) {
264                tdoc.setJobs(TkServiceLocator.getJobService().getJobs(principalId, TKUtils.getTimelessDate(payCalEntry.getEndPeriodDate())));
265            }
266            tdoc.setTimeBlocks(TkServiceLocator.getTimeBlockService().getTimeBlocks(tdoc.getDocumentHeader().getDocumentId()));
267        }
268    
269        public boolean isSynchronousUser() {
270            List<Assignment> assignments = TkServiceLocator.getAssignmentService().getAssignments(TKUser.getCurrentTargetPersonId(), TKUtils.getCurrentDate());
271            boolean isSynchronousUser = true;
272            for (Assignment assignment : assignments) {
273                isSynchronousUser &= assignment.isSynchronous();
274            }
275            return isSynchronousUser;
276        }
277    
278        //this is an admin function used for testing
279        public void deleteTimesheet(String documentId) {
280            TkServiceLocator.getTimeBlockService().deleteTimeBlocksAssociatedWithDocumentId(documentId);
281            TkServiceLocator.getTimesheetDocumentHeaderService().deleteTimesheetHeader(documentId);
282        }
283    
284        public TimeBlock resetWorkedHours(TimeBlock timeBlock, Date asOfDate) {
285            EarnCode earnCodeObj = TkServiceLocator.getEarnCodeService().getEarnCode(timeBlock.getEarnCode(), new java.sql.Date(asOfDate.getTime()));
286            
287            if (timeBlock.getBeginTime() != null && timeBlock.getEndTime() != null && StringUtils.equals(timeBlock.getEarnCodeType(), TkConstants.EARN_CODE_TIME)) {
288                BigDecimal hours = TKUtils.getHoursBetween(timeBlock.getBeginTime().getTime(), timeBlock.getEndTime().getTime());
289                
290                //If earn code has an inflate min hours check if it is greater than zero
291                //and compare if the hours specified is less than min hours awarded for this
292                //earn code
293                if (earnCodeObj.getInflateMinHours() != null) {
294                    if ((earnCodeObj.getInflateMinHours().compareTo(BigDecimal.ZERO) != 0) &&
295                            earnCodeObj.getInflateMinHours().compareTo(hours) > 0) {
296                        hours = earnCodeObj.getInflateMinHours();
297                    }
298                }
299                //If earn code has an inflate factor multiple hours specified by the factor
300                if (earnCodeObj.getInflateFactor() != null) {
301                    if ((earnCodeObj.getInflateFactor().compareTo(new BigDecimal(1.0)) != 0)
302                                    && (earnCodeObj.getInflateFactor().compareTo(BigDecimal.ZERO)!= 0) ) {
303                        hours = earnCodeObj.getInflateFactor().multiply(hours, TkConstants.MATH_CONTEXT).setScale(TkConstants.BIG_DECIMAL_SCALE);
304                    }
305                }
306                
307                timeBlock.setHours(hours);
308            }
309            return timeBlock;
310        }
311    
312        @Override
313        public void resetTimeBlock(List<TimeBlock> timeBlocks, Date asOfDate) {
314            for (TimeBlock tb : timeBlocks) {
315                resetWorkedHours(tb, asOfDate);
316            }
317            TkServiceLocator.getTimeBlockService().resetTimeHourDetail(timeBlocks);
318        }
319    
320            @Override
321            public boolean isReadyToApprove(TimesheetDocument document) {
322            if (document == null) {
323                return false;
324            }
325            List<LeaveBlock> leaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocksWithType(document.getPrincipalId(),
326                            document.getCalendarEntry().getBeginPeriodDate(), document.getCalendarEntry().getEndPeriodDate(), LMConstants.LEAVE_BLOCK_TYPE.BALANCE_TRANSFER);
327            leaveBlocks.addAll(TkServiceLocator.getLeaveBlockService().getLeaveBlocksWithType(document.getPrincipalId(),
328                            document.getCalendarEntry().getBeginPeriodDate(), document.getCalendarEntry().getEndPeriodDate(), LMConstants.LEAVE_BLOCK_TYPE.LEAVE_PAYOUT));
329            for(LeaveBlock lb : leaveBlocks) {
330                    if(!StringUtils.equals(lb.getRequestStatus(),LMConstants.REQUEST_STATUS.APPROVED) &&
331                                    !StringUtils.equals(lb.getRequestStatus(), LMConstants.REQUEST_STATUS.DISAPPROVED))
332                            return false;
333            }
334            return true;
335    /*        List<BalanceTransfer> balanceTransfers = TkServiceLocator.getBalanceTransferService().getBalanceTransfers(document.getPrincipalId(),
336                    document.getCalendarEntry().getBeginPeriodDate(),
337                    document.getCalendarEntry().getEndPeriodDate());
338            if (!CollectionUtils.isEmpty(balanceTransfers))   {
339                    for(BalanceTransfer balanceTransfer : balanceTransfers) {
340                            if(StringUtils.equals(TkConstants.DOCUMENT_STATUS.get(balanceTransfer.getStatus()), TkConstants.ROUTE_STATUS.ENROUTE))
341                                    return false;
342                        if (!StringUtils.equals(LMConstants.REQUEST_STATUS.APPROVED, balanceTransfer.getStatus())
343                                && !StringUtils.equals(LMConstants.REQUEST_STATUS.DISAPPROVED, balanceTransfer.getStatus())) {
344                            return false;
345                        }
346                    }
347            }
348            List<LeavePayout> leavePayouts = TkServiceLocator.getLeavePayoutService().getLeavePayouts(document.getPrincipalId(),
349                            document.getCalendarEntry().getBeginPeriodDate(),
350                            document.getCalendarEntry().getEndPeriodDate());
351            if (!CollectionUtils.isEmpty(leavePayouts)) {
352                    for(LeavePayout payout : leavePayouts) {
353                            if(StringUtils.equals(TkConstants.DOCUMENT_STATUS.get(payout.getStatus()), TkConstants.ROUTE_STATUS.ENROUTE))
354                                    return false;
355                        if (!StringUtils.equals(LMConstants.REQUEST_STATUS.APPROVED, payout.getStatus())
356                                && !StringUtils.equals(LMConstants.REQUEST_STATUS.DISAPPROVED, payout.getStatus())) {
357                            return false;
358                        }
359                    }
360            }
361            return true;*/
362            }
363    
364    }