/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.module.ec.service.impl; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.coa.businessobject.Account; import org.kuali.kfs.integration.cg.ContractsAndGrantsModuleService; import org.kuali.kfs.integration.ld.LaborLedgerExpenseTransferAccountingLine; import org.kuali.kfs.integration.ld.LaborLedgerExpenseTransferSourceAccountingLine; import org.kuali.kfs.integration.ld.LaborLedgerExpenseTransferTargetAccountingLine; import org.kuali.kfs.integration.ld.LaborModuleService; import org.kuali.kfs.module.ec.EffortConstants; import org.kuali.kfs.module.ec.EffortKeyConstants; import org.kuali.kfs.module.ec.businessobject.EffortCertificationDetail; import org.kuali.kfs.module.ec.businessobject.EffortCertificationDetailBuild; import org.kuali.kfs.module.ec.businessobject.EffortCertificationDocumentBuild; import org.kuali.kfs.module.ec.businessobject.EffortCertificationReportDefinition; import org.kuali.kfs.module.ec.document.EffortCertificationDocument; import org.kuali.kfs.module.ec.document.validation.impl.EffortCertificationDocumentRuleUtil; import org.kuali.kfs.module.ec.service.EffortCertificationDocumentService; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.KFSPropertyConstants; import org.kuali.kfs.sys.MessageBuilder; import org.kuali.kfs.sys.businessobject.AccountingLineOverride; import org.kuali.kfs.sys.businessobject.AccountingLineOverride.COMPONENT; import org.kuali.kfs.sys.businessobject.FinancialSystemDocumentHeader; import org.kuali.rice.core.api.util.type.KualiDecimal; import org.kuali.rice.kew.api.WorkflowDocument; import org.kuali.rice.kew.api.action.ActionRequestType; import org.kuali.rice.kew.api.action.ActionTaken; import org.kuali.rice.kew.api.action.ActionType; import org.kuali.rice.kew.api.exception.WorkflowException; import org.kuali.rice.kim.api.identity.Person; import org.kuali.rice.kim.api.identity.PersonService; import org.kuali.rice.kim.api.services.KimApiServiceLocator; import org.kuali.rice.krad.UserSession; import org.kuali.rice.krad.bo.AdHocRoutePerson; import org.kuali.rice.krad.service.BusinessObjectService; import org.kuali.rice.krad.service.DocumentService; import org.kuali.rice.krad.service.KualiModuleService; import org.kuali.rice.krad.util.GlobalVariables; import org.springframework.transaction.annotation.Transactional; /** * To implement the services related to the effort certification document */ @Transactional public class EffortCertificationDocumentServiceImpl implements EffortCertificationDocumentService { public static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(EffortCertificationDocumentServiceImpl.class); private LaborModuleService laborModuleService; private KualiModuleService kualiModuleService; private ContractsAndGrantsModuleService contractsAndGrantsModuleService; private DocumentService documentService; private BusinessObjectService businessObjectService; /** * @see org.kuali.kfs.module.ec.service.EffortCertificationDocumentService#processApprovedEffortCertificationDocument(org.kuali.kfs.module.ec.document.EffortCertificationDocument) */ @Override public void processApprovedEffortCertificationDocument(EffortCertificationDocument effortCertificationDocument) { WorkflowDocument workflowDocument = effortCertificationDocument.getDocumentHeader().getWorkflowDocument(); if (workflowDocument.isProcessed()) { GlobalVariables.setUserSession(new UserSession(KFSConstants.SYSTEM_USER)); generateSalaryExpenseTransferDocument(effortCertificationDocument); } } /** * @see org.kuali.kfs.module.ec.service.EffortCertificationDocumentService#createAndRouteEffortCertificationDocument(org.kuali.kfs.module.ec.businessobject.EffortCertificationDocumentBuild) */ @Override public boolean createAndRouteEffortCertificationDocument(EffortCertificationDocumentBuild effortCertificationDocumentBuild) { try { EffortCertificationDocument effortCertificationDocument = (EffortCertificationDocument) documentService.getNewDocument(EffortConstants.EffortDocumentTypes.EFFORT_CERTIFICATION_DOCUMENT); populateEffortCertificationDocument(effortCertificationDocument, effortCertificationDocumentBuild); documentService.routeDocument(effortCertificationDocument, KFSConstants.EMPTY_STRING, null); } catch (WorkflowException we) { LOG.error( "Unable to route ECD document: " + effortCertificationDocumentBuild, we); throw new RuntimeException("Unable to route ECD document: " + effortCertificationDocumentBuild, we); } return true; } /** * @see org.kuali.kfs.module.ec.service.EffortCertificationDocumentService#populateEffortCertificationDocument(org.kuali.kfs.module.ec.document.EffortCertificationDocument, * org.kuali.kfs.module.ec.businessobject.EffortCertificationDocumentBuild) */ @Override public boolean populateEffortCertificationDocument(EffortCertificationDocument effortCertificationDocument, EffortCertificationDocumentBuild effortCertificationDocumentBuild) { // populate the fields of the document effortCertificationDocument.setUniversityFiscalYear(effortCertificationDocumentBuild.getUniversityFiscalYear()); effortCertificationDocument.setEmplid(effortCertificationDocumentBuild.getEmplid()); effortCertificationDocument.setEffortCertificationReportNumber(effortCertificationDocumentBuild.getEffortCertificationReportNumber()); effortCertificationDocument.setEffortCertificationDocumentCode(effortCertificationDocumentBuild.getEffortCertificationDocumentCode()); // populate the detail line of the document List<EffortCertificationDetail> detailLines = effortCertificationDocument.getEffortCertificationDetailLines(); detailLines.clear(); List<EffortCertificationDetailBuild> detailLinesBuild = effortCertificationDocumentBuild.getEffortCertificationDetailLinesBuild(); for (EffortCertificationDetailBuild detailLineBuild : detailLinesBuild) { detailLines.add(new EffortCertificationDetail(detailLineBuild)); } // populate the document header of the document FinancialSystemDocumentHeader documentHeader = effortCertificationDocument.getFinancialSystemDocumentHeader(); documentHeader.setDocumentDescription(effortCertificationDocumentBuild.getEmplid()); documentHeader.setFinancialDocumentTotalAmount(EffortCertificationDocument.getDocumentTotalAmount(effortCertificationDocument)); return true; } /** * @see org.kuali.kfs.module.ec.service.EffortCertificationDocumentService#resetEffortCertificationDetailLines(org.kuali.kfs.module.ec.document.EffortCertificationDocument) */ @Override public void removeEffortCertificationDetailLines(EffortCertificationDocument effortCertificationDocument) { Map<String, String> fieldValues = new HashMap<String, String>(); fieldValues.put(KFSPropertyConstants.DOCUMENT_NUMBER, effortCertificationDocument.getDocumentNumber()); businessObjectService.deleteMatching(EffortCertificationDetail.class, fieldValues); } /** * @see org.kuali.kfs.module.ec.service.EffortCertificationDocumentService#generateSalaryExpenseTransferDocument(org.kuali.kfs.module.ec.document.EffortCertificationDocument) */ @Override public boolean generateSalaryExpenseTransferDocument(EffortCertificationDocument effortCertificationDocument) { List<LaborLedgerExpenseTransferAccountingLine> sourceAccoutingLines = this.buildSourceAccountingLines(effortCertificationDocument); List<LaborLedgerExpenseTransferAccountingLine> targetAccoutingLines = this.buildTargetAccountingLines(effortCertificationDocument); if (sourceAccoutingLines.isEmpty() || targetAccoutingLines.isEmpty()) { return true; } String description = effortCertificationDocument.getEmplid(); String explanation = MessageBuilder.buildMessageWithPlaceHolder(EffortKeyConstants.MESSAGE_CREATE_SET_DOCUMENT_DESCRIPTION, effortCertificationDocument.getDocumentNumber()).toString(); String annotation = KFSConstants.EMPTY_STRING; List<String> adHocRecipients = new ArrayList<String>(); adHocRecipients.addAll(this.getFiscalOfficersIfAmountChanged(effortCertificationDocument)); try { laborModuleService.createAndBlankApproveSalaryExpenseTransferDocument(description, explanation, annotation, adHocRecipients, sourceAccoutingLines, targetAccoutingLines); } catch (WorkflowException we) { LOG.error( "Error while routing SET document created from ECD: " + effortCertificationDocument, we); throw new RuntimeException("Error while routing SET document created from ECD: " + effortCertificationDocument, we); } return true; } /** * @see org.kuali.kfs.module.ec.service.EffortCertificationDocumentService#addRouteLooping(org.kuali.kfs.module.ec.document.EffortCertificationDocument) */ @Override public void addRouteLooping(EffortCertificationDocument effortCertificationDocument) { WorkflowDocument workflowDocument = effortCertificationDocument.getDocumentHeader().getWorkflowDocument(); Set<Person> priorApprovers = getPriorApprovers(workflowDocument); for (EffortCertificationDetail detailLine : effortCertificationDocument.getEffortCertificationDetailLines()) { boolean hasBeenChanged = EffortCertificationDocumentRuleUtil.isPayrollAmountChangedFromPersisted(detailLine); if (!hasBeenChanged) { continue; } boolean isNewLine = detailLine.isNewLineIndicator(); if ( LOG.isInfoEnabled() ) { LOG.info( "EC Detail Line has been changed: " + detailLine ); } Account account = detailLine.getAccount(); Person fiscalOfficer = account.getAccountFiscalOfficerUser(); if ( fiscalOfficer != null && StringUtils.isNotBlank(fiscalOfficer.getPrincipalName())) { // KULEFR-206 // String actionRequestOfOfficer = this.getActionRequest(routeLevelName, KFSConstants.RouteLevelNames.ACCOUNT); AdHocRoutePerson adHocRoutePerson = buildAdHocRouteRecipient(fiscalOfficer.getPrincipalName(), ActionRequestType.APPROVE); addAdHocRoutePerson(effortCertificationDocument.getAdHocRoutePersons(), priorApprovers, adHocRoutePerson, isNewLine); } else { LOG.warn( "Unable to obtain a fiscal officer for the detail line's account: " + account.getChartOfAccountsCode() + "-" + account.getAccountNumber() ); } Person projectDirector = contractsAndGrantsModuleService.getProjectDirectorForAccount(account); if (projectDirector != null) { String accountProjectDirectorPersonUserId = projectDirector.getPrincipalName(); // KULEFR-206 // String actionRequestOfDirector = this.getActionRequest(routeLevelName, // KFSConstants.RouteLevelNames.PROJECT_MANAGEMENT); AdHocRoutePerson adHocRoutePerson = buildAdHocRouteRecipient(accountProjectDirectorPersonUserId, ActionRequestType.APPROVE); addAdHocRoutePerson(effortCertificationDocument.getAdHocRoutePersons(), priorApprovers, adHocRoutePerson, isNewLine); } } } /** * add the given ad hoc route person in the list if the person is one of prior approvers and is not in the list * * @param adHocRoutePersonList Collection of adhoc route persons * @param priorApprovers Set of prior approvers * @param adHocRoutePerson person to adhoc route to */ protected void addAdHocRoutePerson(Collection<AdHocRoutePerson> adHocRoutePersonList, Set<Person> priorApprovers, AdHocRoutePerson adHocRoutePerson) { addAdHocRoutePerson(adHocRoutePersonList, priorApprovers, adHocRoutePerson, false); } /** * add the given ad hoc route person in the list if the person is one of prior approvers, or the change was a new line, and the person is not in the list * * @param adHocRoutePersonList Collection of adhoc route persons * @param priorApprovers Set of prior approvers * @param adHocRoutePerson person to adhoc route to * @param isNewLine whether the change was a new line */ protected void addAdHocRoutePerson(Collection<AdHocRoutePerson> adHocRoutePersonList, Set<Person> priorApprovers, AdHocRoutePerson adHocRoutePerson, boolean isNewLine) { boolean canBeAdded = false; // if it's a new line, person can be added even if they weren't a prior approver if (priorApprovers == null || isNewLine) { canBeAdded = true; } else { // we only want to ad-hoc if the user previously approved this document for (Person approver : priorApprovers) { if (StringUtils.equals(approver.getPrincipalName(), adHocRoutePerson.getId())) { canBeAdded = true; break; } } } if (canBeAdded) { // check that we have not already added them for the same action for (AdHocRoutePerson person : adHocRoutePersonList) { if (isSameAdHocRoutePerson(person, adHocRoutePerson)) { canBeAdded = false; break; } } } if (canBeAdded) { adHocRoutePersonList.add(adHocRoutePerson); } } protected boolean isSameAdHocRoutePerson(AdHocRoutePerson person1, AdHocRoutePerson person2) { if (person1 == null || person2 == null) { return false; } boolean isSameAdHocRoutePerson = StringUtils.equals(person1.getId(), person2.getId()); isSameAdHocRoutePerson &= person1.getType().equals(person2.getType()); isSameAdHocRoutePerson &= StringUtils.equals(person1.getActionRequested(), person2.getActionRequested()); return isSameAdHocRoutePerson; } protected Set<Person> getPriorApprovers(WorkflowDocument workflowDocument) { PersonService personService = KimApiServiceLocator.getPersonService(); List<ActionTaken> actionsTaken = workflowDocument.getActionsTaken(); Set<String> principalIds = new HashSet<String>(); Set<Person> persons = new HashSet<Person>(); for (ActionTaken actionTaken : actionsTaken) { if (ActionType.APPROVE.equals(actionTaken.getActionTaken())) { String principalId = actionTaken.getPrincipalId(); if (!principalIds.contains(principalId)) { principalIds.add(principalId); persons.add(personService.getPerson(principalId)); } } } return persons; } /** * build an adhoc route recipient from the given person user id and action request * * @param personUserId the given person user id * @param actionRequest the given action request * @return an adhoc route recipient built from the given information */ protected AdHocRoutePerson buildAdHocRouteRecipient(String personUserId, ActionRequestType actionRequest) { AdHocRoutePerson adHocRoutePerson = new AdHocRoutePerson(); adHocRoutePerson.setActionRequested(actionRequest.getCode()); adHocRoutePerson.setId(personUserId); return adHocRoutePerson; } /** * build the source accounting lines for a salary expense transfer document from the given effort certification document. In the * holder, the first item is source accounting line list and the second the target accounting line list. * * @param effortCertificationDocument the given effort certification document * @return the source accounting lines for a salary expense transfer document built from the given effort certification document */ protected List<LaborLedgerExpenseTransferAccountingLine> buildSourceAccountingLines(EffortCertificationDocument effortCertificationDocument) { List<LaborLedgerExpenseTransferAccountingLine> sourceAccountingLines = new ArrayList<LaborLedgerExpenseTransferAccountingLine>(); List<EffortCertificationDetail> effortCertificationDetailLines = effortCertificationDocument.getEffortCertificationDetailLines(); for (EffortCertificationDetail detailLine : effortCertificationDetailLines) { if (this.getDifference(detailLine).isPositive()) { LaborLedgerExpenseTransferSourceAccountingLine sourceLine = kualiModuleService.getResponsibleModuleService(LaborLedgerExpenseTransferSourceAccountingLine.class).createNewObjectFromExternalizableClass(LaborLedgerExpenseTransferSourceAccountingLine.class); this.addAccountingLineIntoList(sourceAccountingLines, sourceLine, effortCertificationDocument, detailLine); } } return sourceAccountingLines; } /** * build the target accounting lines for a salary expense transfer document from the given effort certification document. In the * holder, the first item is source accounting line list and the second the target accounting line list. * * @param effortCertificationDocument the given effort certification document * @return the target accounting lines for a salary expense transfer document built from the given effort certification document */ protected List<LaborLedgerExpenseTransferAccountingLine> buildTargetAccountingLines(EffortCertificationDocument effortCertificationDocument) { List<LaborLedgerExpenseTransferAccountingLine> targetAccountingLines = new ArrayList<LaborLedgerExpenseTransferAccountingLine>(); List<EffortCertificationDetail> effortCertificationDetailLines = effortCertificationDocument.getEffortCertificationDetailLines(); for (EffortCertificationDetail detailLine : effortCertificationDetailLines) { if (this.getDifference(detailLine).isNegative()) { LaborLedgerExpenseTransferTargetAccountingLine targetLine = kualiModuleService.getResponsibleModuleService(LaborLedgerExpenseTransferTargetAccountingLine.class).createNewObjectFromExternalizableClass(LaborLedgerExpenseTransferTargetAccountingLine.class); this.addAccountingLineIntoList(targetAccountingLines, targetLine, effortCertificationDocument, detailLine); } } return targetAccountingLines; } /** * get all fiscal officers of the detail line accounts where the salary amounts are changed * * @param effortCertificationDocument the given document that contains the detail lines * @return all fiscal officers of the detail line accounts where the salary amounts are changed */ protected Set<String> getFiscalOfficersIfAmountChanged(EffortCertificationDocument effortCertificationDocument) { Set<String> fiscalOfficers = new HashSet<String>(); List<EffortCertificationDetail> effortCertificationDetailLines = effortCertificationDocument.getEffortCertificationDetailLines(); for (EffortCertificationDetail detailLine : effortCertificationDetailLines) { if (this.getDifference(detailLine).isNonZero()) { Account account = detailLine.getAccount(); String accountFiscalOfficerPersonUserId = account.getAccountFiscalOfficerUser().getPrincipalName(); if (StringUtils.isEmpty(accountFiscalOfficerPersonUserId)) { fiscalOfficers.add(accountFiscalOfficerPersonUserId); } } } return fiscalOfficers; } /** * add a new accounting line into the given accounting line list. The accounting line is generated from the given detail line * * @param accountingLines a list of accounting lines * @param clazz the specified class of the accounting line * @param effortCertificationDocument the given effort certification document that contains the given detail line * @param detailLine the given detail line that is used to generate an accounting line */ protected void addAccountingLineIntoList(List<LaborLedgerExpenseTransferAccountingLine> accountingLineList, LaborLedgerExpenseTransferAccountingLine accountingLine, EffortCertificationDocument effortCertificationDocument, EffortCertificationDetail detailLine) { accountingLine.setSequenceNumber(accountingLineList.size() + 1); this.populateAccountingLine(effortCertificationDocument, detailLine, accountingLine); accountingLineList.add(accountingLine); } /** * populate an accounting line from the given detail line * * @param effortCertificationDocument the given effort certification document that contains the given detail line * @param detailLine the given detail line * @param accountingLine the accounting line needed to be populated */ protected void populateAccountingLine(EffortCertificationDocument effortCertificationDocument, EffortCertificationDetail detailLine, LaborLedgerExpenseTransferAccountingLine accountingLine) { accountingLine.setChartOfAccountsCode(detailLine.getChartOfAccountsCode()); accountingLine.setAccountNumber(detailLine.getAccountNumber()); accountingLine.setSubAccountNumber(detailLine.getSubAccountNumber()); accountingLine.setPostingYear(detailLine.getUniversityFiscalYear()); accountingLine.setFinancialObjectCode(detailLine.getFinancialObjectCode()); accountingLine.setBalanceTypeCode(KFSConstants.BALANCE_TYPE_ACTUAL); accountingLine.setAmount(this.getDifference(detailLine).abs()); accountingLine.setFinancialSubObjectCode(null); accountingLine.setProjectCode(null); accountingLine.setOrganizationReferenceId(null); accountingLine.setEmplid(effortCertificationDocument.getEmplid()); accountingLine.setPositionNumber(detailLine.getPositionNumber()); accountingLine.setPayrollTotalHours(BigDecimal.ZERO); EffortCertificationReportDefinition reportDefinition = effortCertificationDocument.getEffortCertificationReportDefinition(); accountingLine.setPayrollEndDateFiscalYear(reportDefinition.getExpenseTransferFiscalYear()); accountingLine.setPayrollEndDateFiscalPeriodCode(reportDefinition.getExpenseTransferFiscalPeriodCode()); accountingLine.refreshNonUpdateableReferences(); AccountingLineOverride override = laborModuleService.determineNeededOverrides(null, accountingLine); // if an expired account override is needed, set it, otherwise validations on the downstream ST doc could fail accountingLine.setAccountExpiredOverrideNeeded(override.hasComponent(COMPONENT.EXPIRED_ACCOUNT)); accountingLine.setAccountExpiredOverride(accountingLine.getAccountExpiredOverrideNeeded()); // if a non-budgeted object code override is needed, set it, otherwise validations on the downstream ST doc could fail accountingLine.setObjectBudgetOverrideNeeded(override.hasComponent(COMPONENT.NON_BUDGETED_OBJECT)); accountingLine.setObjectBudgetOverride(accountingLine.isObjectBudgetOverrideNeeded()); if (accountingLine.getAccountExpiredOverrideNeeded() || accountingLine.isObjectBudgetOverrideNeeded()) { accountingLine.setOverrideCode(override.getCode()); } } /** * get the difference between the original amount and updated amount of the given detail line * * @param detailLine the given detail line * @return the difference between the original amount and updated amount of the given detail line */ protected KualiDecimal getDifference(EffortCertificationDetail detailLine) { return detailLine.getEffortCertificationOriginalPayrollAmount().subtract(detailLine.getEffortCertificationPayrollAmount()); } /** * Sets the laborModuleService attribute value. * * @param laborModuleService The laborModuleService to set. */ public void setLaborModuleService(LaborModuleService laborModuleService) { this.laborModuleService = laborModuleService; } /** * Sets the documentService attribute value. * * @param documentService The documentService to set. */ public void setDocumentService(DocumentService documentService) { this.documentService = documentService; } /** * Sets the businessObjectService attribute value. * * @param businessObjectService The businessObjectService to set. */ public void setBusinessObjectService(BusinessObjectService businessObjectService) { this.businessObjectService = businessObjectService; } /** * Sets the contractsAndGrantsModuleService attribute value. * @param contractsAndGrantsModuleService The contractsAndGrantsModuleService to set. */ public void setContractsAndGrantsModuleService(ContractsAndGrantsModuleService contractsAndGrantsModuleService) { this.contractsAndGrantsModuleService = contractsAndGrantsModuleService; } /** * Sets the kualiModuleService attribute value. * * @param kualiModuleService The kualiModuleService to set. */ public void setKualiModuleService(KualiModuleService kualiModuleService) { this.kualiModuleService = kualiModuleService; } }