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 }