/*
* 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.cg.document;
import static org.kuali.kfs.sys.KFSPropertyConstants.AWARD_ACCOUNTS;
import static org.kuali.kfs.sys.KFSPropertyConstants.AWARD_FUND_MANAGERS;
import static org.kuali.kfs.sys.KFSPropertyConstants.AWARD_PROJECT_DIRECTORS;
import static org.kuali.kfs.sys.KFSPropertyConstants.AWARD_SUBCONTRACTORS;
import static org.kuali.kfs.sys.KFSPropertyConstants.DOCUMENT;
import static org.kuali.kfs.sys.KFSPropertyConstants.NEW_MAINTAINABLE_OBJECT;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.integration.ar.AccountsReceivableModuleBillingService;
import org.kuali.kfs.module.cg.CGConstants;
import org.kuali.kfs.module.cg.CGPropertyConstants;
import org.kuali.kfs.module.cg.businessobject.Award;
import org.kuali.kfs.module.cg.businessobject.AwardAccount;
import org.kuali.kfs.module.cg.businessobject.AwardFundManager;
import org.kuali.kfs.module.cg.businessobject.AwardOrganization;
import org.kuali.kfs.module.cg.businessobject.AwardProjectDirector;
import org.kuali.kfs.module.cg.businessobject.AwardSubcontractor;
import org.kuali.kfs.module.cg.businessobject.CGProjectDirector;
import org.kuali.kfs.module.cg.businessobject.Proposal;
import org.kuali.kfs.module.cg.document.validation.impl.AwardRuleUtil;
import org.kuali.kfs.module.cg.service.ContractsAndGrantsBillingService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.coreservice.framework.parameter.ParameterService;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kim.api.identity.Person;
import org.kuali.rice.kim.api.identity.PersonService;
import org.kuali.rice.kim.api.identity.principal.Principal;
import org.kuali.rice.kim.api.services.KimApiServiceLocator;
import org.kuali.rice.kns.document.MaintenanceDocument;
import org.kuali.rice.kns.maintenance.Maintainable;
import org.kuali.rice.krad.bo.DocumentHeader;
import org.kuali.rice.krad.bo.PersistableBusinessObject;
import org.kuali.rice.krad.maintenance.MaintenanceLock;
import org.kuali.rice.krad.service.BusinessObjectService;
import org.kuali.rice.krad.util.GlobalVariables;
import org.kuali.rice.krad.util.ObjectUtils;
/**
* Methods for the Award maintenance document UI.
*/
public class AwardMaintainableImpl extends ContractsGrantsBillingMaintainable {
private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AwardMaintainableImpl.class);
private static volatile AccountsReceivableModuleBillingService accountsReceivableModuleBillingService;
/**
* This method is called for refreshing the Agency before display to show the full name in case the agency number was changed by
* hand before any submit that causes a redisplay.
*/
@Override
public void processAfterRetrieve() {
refreshAward(false);
super.processAfterRetrieve();
}
/**
* Not to copy over the Accounts tab, Predetermined tab, Milestone schedule tab, award account tab and award budgets tab when
* copying
*/
@Override
public void processAfterCopy(MaintenanceDocument document, Map<String, String[]> parameters) {
super.processAfterCopy(document, parameters);
Award oldAward = (Award) document.getOldMaintainableObject().getBusinessObject();
Award newAward = (Award) document.getNewMaintainableObject().getBusinessObject();
if (ObjectUtils.isNotNull(oldAward) && ObjectUtils.isNotNull(newAward)){
// Clear Accounts
oldAward.getAwardAccounts().clear();
newAward.getAwardAccounts().clear();
}
getAward().setMilestoneSchedule(SpringContext.getBean(AccountsReceivableModuleBillingService.class).getMilestoneSchedule());
getAward().setPredeterminedBillingSchedule(SpringContext.getBean(AccountsReceivableModuleBillingService.class).getPredeterminedBillingSchedule());
}
/**
* Setting the initial field value to that specified by the default invoice parameter and default billing frequency parameter,
* otherwise defaulting to invoice by account and monthly, respectively.
*/
@Override
public void processAfterNew(MaintenanceDocument document, Map<String, String[]> parameters) {
super.processAfterNew(document, parameters);
// Retrieving Default Invoicing Option
String defaultInvoiceParm = getAccountsReceivableModuleBillingService().getDefaultInvoicingOption();
// Retrieving Default Billing Schedule
String defaultBillingScheduleParm = getAccountsReceivableModuleBillingService().getDefaultBillingFrequency();
// Set Invoicing Option
getAward().setInvoicingOptionCode(defaultInvoiceParm);
// Set Billing Schedule
if (StringUtils.isNotBlank(defaultBillingScheduleParm)) {
getAward().setBillingFrequencyCode(defaultBillingScheduleParm);
}
else {
getAward().setBillingFrequencyCode(CGConstants.MONTHLY_BILLING_SCHEDULE_CODE);
}
}
/**
* This method is called for refreshing the Agency before a save to display the full name in case the agency number was changed
* by hand just before the save.
*/
@Override
public void prepareForSave() {
refreshAward(false);
List<AwardProjectDirector> directors = getAward().getAwardProjectDirectors();
if (directors.size() == 1) {
directors.get(0).setAwardPrimaryProjectDirectorIndicator(true);
}
List<AwardFundManager> managers = getAward().getAwardFundManagers();
if (managers != null && managers.size() == 1) {
managers.get(0).setPrimaryFundManagerIndicator(true);
}
List<AwardOrganization> organizations = getAward().getAwardOrganizations();
if (organizations.size() == 1) {
organizations.get(0).setAwardPrimaryOrganizationIndicator(true);
}
// need to populate the synthetic keys for these records
// since we can not depend on the keys which exist, we need to determine all those which could match
// so we can avoid them
List<AwardSubcontractor> awardSubcontractors = getAward().getAwardSubcontractors();
if (awardSubcontractors != null && !awardSubcontractors.isEmpty()) {
// convert the list into a map of lists containing the used award subcontractor number/amendment number
Map<String, List<AwardSubcontractor>> subcontractorAwardMap = new HashMap<String, List<AwardSubcontractor>>();
List<AwardSubcontractor> newSubcontractorRecords = new ArrayList<AwardSubcontractor>();
for (AwardSubcontractor awardSubcontractor : awardSubcontractors) {
if (!StringUtils.isBlank(awardSubcontractor.getAwardSubcontractorNumber())) {
// already has key - add to map
if (!subcontractorAwardMap.containsKey(awardSubcontractor.getSubcontractorNumber())) {
subcontractorAwardMap.put(awardSubcontractor.getSubcontractorNumber(), new ArrayList<AwardSubcontractor>());
}
subcontractorAwardMap.get(awardSubcontractor.getSubcontractorNumber()).add(awardSubcontractor);
}
else {
// new record, add to new map
newSubcontractorRecords.add(awardSubcontractor);
}
}
// now, loop over the new records
for (AwardSubcontractor awardSubcontractor : newSubcontractorRecords) {
String awardSubcontractorNumber = "1";
String awardSubcontractorAmendmentNumber = "1";
// get the other ones for the same subcontractor
List<AwardSubcontractor> oldSubcontractors = subcontractorAwardMap.get(awardSubcontractor.getSubcontractorNumber());
if (oldSubcontractors != null) {
// we have a hit - find the first non-used number
// build an array from the unsorted list
boolean[][] nums = new boolean[100][100];
for (AwardSubcontractor oldSub : oldSubcontractors) {
try {
nums[Integer.valueOf(oldSub.getAwardSubcontractorNumber())][Integer.valueOf(oldSub.getAwardSubcontractorAmendmentNumber())] = true;
}
catch (NumberFormatException ex) {
// do nothing
LOG.warn("Unexpected non-integer award subcontractor / amendment number: " + oldSub.getAwardSubcontractorNumber() + " / " + oldSub.getAwardSubcontractorAmendmentNumber());
}
}
// iterate over the array to get the first empty value
// loop over the awardSubcontractorNumbers first
boolean isFoundNumbers = false;
for (int i = 1; i <= 99; i++) {
for (int j = 1; j <= 99; j++) {
if (!nums[j][i]) {
// save the values
awardSubcontractorNumber = Integer.toString(j);
awardSubcontractorAmendmentNumber = Integer.toString(i);
// mark the cell as used before the next pass
nums[j][i] = true;
// just a flag to allow us to break out of both loops
isFoundNumbers = true;
break;
}
}
if (isFoundNumbers) {
break;
}
// JHK - yes, this will break down if there are more than 9801 subcontracts
// however, the UI will probably break down far before then...
}
}
awardSubcontractor.setAwardSubcontractorNumber(awardSubcontractorNumber);
awardSubcontractor.setAwardSubcontractorAmendmentNumber(awardSubcontractorAmendmentNumber);
}
}
super.prepareForSave();
}
@Override
@SuppressWarnings("unchecked")
public void processAfterPost(MaintenanceDocument document, Map<String, String[]> parameters) {
super.processAfterPost(document, parameters);
Award newAward = (Award) document.getNewMaintainableObject().getBusinessObject();
// KFSTP-16 Check for null proposal number before saving
if (ObjectUtils.isNotNull(newAward)){
Proposal proposal = newAward.getProposal();
if (ObjectUtils.isNotNull(proposal) && proposal.getProposalNumber() != null){
SpringContext.getBean(BusinessObjectService.class).save(newAward.getProposal());
}
}
}
/**
* This method is called for refreshing the Agency after a lookup to display its full name without AJAX.
*
* @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#refresh(java.lang.String, java.util.Map,
* org.kuali.rice.kns.document.MaintenanceDocument)
*/
@SuppressWarnings("unchecked")
@Override
public void refresh(String refreshCaller, Map fieldValues, MaintenanceDocument document) {
if (StringUtils.equals(CGPropertyConstants.PROPOSAL_LOOKUPABLE, (String) fieldValues.get(KFSConstants.REFRESH_CALLER))) {
boolean isAwarded = AwardRuleUtil.isProposalAwarded(getAward());
if (isAwarded) {
String pathToMaintainable = DOCUMENT + "." + NEW_MAINTAINABLE_OBJECT;
GlobalVariables.getMessageMap().addToErrorPath(pathToMaintainable);
GlobalVariables.getMessageMap().putError(KFSPropertyConstants.PROPOSAL_NUMBER, KFSKeyConstants.ERROR_AWARD_PROPOSAL_AWARDED, new String[] { getAward().getProposalNumber().toString() });
GlobalVariables.getMessageMap().removeFromErrorPath(pathToMaintainable);
}
// SEE KULCG-315 for details on why this code is commented out.
// if (AwardRuleUtil.isProposalInactive(getAward())) {
// GlobalVariables.getMessageMap().putError(KFSPropertyConstants.PROPOSAL_NUMBER,
// KFSKeyConstants.ERROR_AWARD_PROPOSAL_INACTIVE, new String[] { getAward().getProposalNumber().toString() });
// }
// copy over proposal values after refresh
if (!isAwarded) {
refreshAward(true);
fieldValues.put(KFSConstants.REFERENCES_TO_REFRESH, "proposal");
super.refresh(refreshCaller, fieldValues, document);
getAward().populateFromProposal(getAward().getProposal());
refreshAward(true);
}
}
else {
refreshAward(KFSConstants.KUALI_LOOKUPABLE_IMPL.equals(fieldValues.get(KFSConstants.REFRESH_CALLER)));
super.refresh(refreshCaller, fieldValues, document);
}
}
/**
* Load related objects from the database as needed.
*
* @param refreshFromLookup
*/
private void refreshAward(boolean refreshFromLookup) {
Award award = getAward();
award.refreshNonUpdateableReferences();
getNewCollectionLine(AWARD_SUBCONTRACTORS).refreshNonUpdateableReferences();
getNewCollectionLine(AWARD_PROJECT_DIRECTORS).refreshNonUpdateableReferences();
getNewCollectionLine(AWARD_FUND_MANAGERS).refreshNonUpdateableReferences();
getNewCollectionLine(AWARD_ACCOUNTS).refreshNonUpdateableReferences();
// the org list doesn't need any refresh
refreshNonUpdateableReferences(award.getAwardOrganizations());
refreshNonUpdateableReferences(award.getAwardAccounts());
refreshNonUpdateableReferences(award.getAwardSubcontractors());
refreshAwardProjectDirectors(refreshFromLookup);
refreshAwardFundManagers(refreshFromLookup);
}
/**
* Refresh the collection of associated AwardProjectDirectors.
*
* @param refreshFromLookup a lookup returns only the primary key, so ignore the secondary key when true
*/
private void refreshAwardProjectDirectors(boolean refreshFromLookup) {
if (refreshFromLookup) {
getNewCollectionLine(AWARD_PROJECT_DIRECTORS).refreshNonUpdateableReferences();
refreshNonUpdateableReferences(getAward().getAwardProjectDirectors());
getNewCollectionLine(AWARD_ACCOUNTS).refreshNonUpdateableReferences();
refreshNonUpdateableReferences(getAward().getAwardAccounts());
}
else {
refreshWithSecondaryKey((AwardProjectDirector) getNewCollectionLine(AWARD_PROJECT_DIRECTORS));
for (AwardProjectDirector projectDirector : getAward().getAwardProjectDirectors()) {
refreshWithSecondaryKey(projectDirector);
}
refreshWithSecondaryKey((AwardAccount) getNewCollectionLine(AWARD_ACCOUNTS));
for (AwardAccount account : getAward().getAwardAccounts()) {
refreshWithSecondaryKey(account);
}
}
}
/**
* Refresh the collection of associated AwardFundManagers.
*
* @param refreshFromLookup a lookup returns only the primary key, so ignore the secondary key when true
*/
private void refreshAwardFundManagers(boolean refreshFromLookup) {
if (refreshFromLookup) {
getNewCollectionLine(AWARD_FUND_MANAGERS).refreshNonUpdateableReferences();
refreshNonUpdateableReferences(getAward().getAwardFundManagers());
getNewCollectionLine(AWARD_ACCOUNTS).refreshNonUpdateableReferences();
refreshNonUpdateableReferences(getAward().getAwardAccounts());
}
else {
refreshWithSecondaryKey((AwardFundManager) getNewCollectionLine(AWARD_FUND_MANAGERS));
for (AwardFundManager fundManager : getAward().getAwardFundManagers()) {
refreshWithSecondaryKey(fundManager);
}
}
}
/**
* @param collection
*/
private static void refreshNonUpdateableReferences(Collection<? extends PersistableBusinessObject> collection) {
for (PersistableBusinessObject item : collection) {
item.refreshNonUpdateableReferences();
}
}
/**
* Refreshes the reference to ProjectDirector, giving priority to its secondary key. Any secondary key that it has may be user
* input, so that overrides the primary key, setting the primary key. If its primary key is blank or nonexistent, then leave the
* current reference as it is, because it may be a nonexistent instance which is holding the secondary key (the username, i.e.,
* principalName) so we can redisplay it to the user for correction. If it only has a primary key then use that, because it may
* be coming from the database, without any user input.
*
* @param director the ProjectDirector to refresh
*/
private static void refreshWithSecondaryKey(CGProjectDirector director) {
Person cgdir = director.getProjectDirector();
if (ObjectUtils.isNotNull(cgdir)) {
String secondaryKey = cgdir.getPrincipalName();
if (StringUtils.isNotBlank(secondaryKey)) {
Principal dir = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(secondaryKey);
director.setPrincipalId(dir == null ? null : dir.getPrincipalId());
}
if (StringUtils.isNotBlank(director.getPrincipalId())) {
Principal principal = KimApiServiceLocator.getIdentityService().getPrincipal(director.getPrincipalId());
if (principal != null) {
((PersistableBusinessObject) director).refreshNonUpdateableReferences();
}
}
}
}
/**
* Refreshes the reference to FundManager, giving priority to its secondary key. Any secondary key that it has may be user
* input, so that overrides the primary key, setting the primary key. If its primary key is blank or nonexistent, then leave the
* current reference as it is, because it may be a nonexistent instance which is holding the secondary key (the username, i.e.,
* principalName) so we can redisplay it to the user for correction. If it only has a primary key then use that, because it may
* be coming from the database, without any user input.
*
* @param director the FundManager to refresh
*/
private static void refreshWithSecondaryKey(AwardFundManager fundManager) {
Person cdFundMgr = fundManager.getFundManager();
if (ObjectUtils.isNotNull(cdFundMgr)) {
String secondaryKey = cdFundMgr.getPrincipalName();
if (StringUtils.isNotBlank(secondaryKey)) {
Person fundMgr = SpringContext.getBean(PersonService.class).getPersonByPrincipalName(secondaryKey);
fundManager.setPrincipalId(fundMgr == null ? null : fundMgr.getPrincipalId());
}
if (StringUtils.isNotBlank(fundManager.getPrincipalId())) {
Person person = SpringContext.getBean(PersonService.class).getPerson(fundManager.getPrincipalId());
if (person != null) {
((PersistableBusinessObject) fundManager).refreshNonUpdateableReferences();
}
}
}
}
/**
* Gets the underlying Award.
*
* @return
*/
public Award getAward() {
return (Award) getBusinessObject();
}
/**
* Called for refreshing the {@link Subcontractor} on {@link ProposalSubcontractor} before adding to the proposalSubcontractors
* collection on the proposal. this is to ensure that the summary fields are show correctly. i.e. {@link Subcontractor} name
*
* @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#addNewLineToCollection(java.lang.String)
*/
@Override
public void addNewLineToCollection(String collectionName) {
refreshAward(false);
super.addNewLineToCollection(collectionName);
}
/**
* Called to manipulate which sections are shown on the Award Maintenance document
*
* @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#getSections(org.kuali.rice.kns.document.MaintenanceDocument,
* org.kuali.rice.kns.maintenance.Maintainable)
*/
@Override
public List getSections(MaintenanceDocument document, Maintainable oldMaintainable) {
Award award = getAward();
// To set billing frequency to "LOC Billing" if LOC Fund is chosen - Cannot be done via scripts, as they get
// refreshed back to old values.
// To be optimized if there is a better approach.
if (StringUtils.isNotEmpty(award.getLetterOfCreditFundCode()) && StringUtils.isNotBlank(award.getLetterOfCreditFundCode())) {
award.setBillingFrequencyCode(CGConstants.LOC_BILLING_SCHEDULE_CODE);
}
return super.getSections(document, oldMaintainable);
}
/**
* If the Contracts & Grants Billing (CGB) enhancement is disabled, we don't want to
* process sections only related to CGB.
*
* @return list of section ids to ignore
*/
@Override
protected Collection<?> getSectionIdsToIgnore() {
if (!SpringContext.getBean(AccountsReceivableModuleBillingService.class).isContractsGrantsBillingEnhancementActive()) {
return SpringContext.getBean(ContractsAndGrantsBillingService.class).getAwardContractsGrantsBillingSectionIds();
} else {
return CollectionUtils.EMPTY_COLLECTION;
}
}
/**
* This method overrides the parent method to check the status of the award document and change the linked
* {@link ProposalStatus} to A (Approved) if the {@link Award} is now in approved status.
*
* @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#doRouteStatusChange(org.kuali.rice.krad.bo.DocumentHeader)
*/
@Override
public void doRouteStatusChange(DocumentHeader header) {
super.doRouteStatusChange(header);
Award award = getAward();
WorkflowDocument workflowDoc = header.getWorkflowDocument();
// Use the isProcessed() method so this code is only executed when the final approval occurs
if (workflowDoc.isProcessed()) {
Proposal proposal = award.getProposal();
proposal.setProposalStatusCode(Proposal.AWARD_CODE);
SpringContext.getBean(BusinessObjectService.class).save(proposal);
}
}
@Override
public List<MaintenanceLock> generateMaintenanceLocks() {
List<MaintenanceLock> locks = super.generateMaintenanceLocks();
return locks;
}
public static AccountsReceivableModuleBillingService getAccountsReceivableModuleBillingService() {
if (accountsReceivableModuleBillingService == null) {
accountsReceivableModuleBillingService = SpringContext.getBean(AccountsReceivableModuleBillingService.class);
}
return accountsReceivableModuleBillingService;
}
}