001 /* 002 * Copyright 2011 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.kfs.module.cg.document; 017 018 import static org.kuali.kfs.sys.KFSPropertyConstants.AWARD_ACCOUNTS; 019 import static org.kuali.kfs.sys.KFSPropertyConstants.AWARD_PROJECT_DIRECTORS; 020 import static org.kuali.kfs.sys.KFSPropertyConstants.AWARD_SUBCONTRACTORS; 021 import static org.kuali.kfs.sys.KFSPropertyConstants.DOCUMENT; 022 import static org.kuali.kfs.sys.KFSPropertyConstants.NEW_MAINTAINABLE_OBJECT; 023 024 import java.util.ArrayList; 025 import java.util.Collection; 026 import java.util.HashMap; 027 import java.util.List; 028 import java.util.Map; 029 030 import org.apache.commons.lang.StringUtils; 031 import org.kuali.kfs.module.cg.businessobject.Award; 032 import org.kuali.kfs.module.cg.businessobject.AwardAccount; 033 import org.kuali.kfs.module.cg.businessobject.AwardOrganization; 034 import org.kuali.kfs.module.cg.businessobject.AwardProjectDirector; 035 import org.kuali.kfs.module.cg.businessobject.AwardSubcontractor; 036 import org.kuali.kfs.module.cg.businessobject.CGProjectDirector; 037 import org.kuali.kfs.module.cg.businessobject.Proposal; 038 import org.kuali.kfs.module.cg.document.validation.impl.AwardRuleUtil; 039 import org.kuali.kfs.sys.KFSConstants; 040 import org.kuali.kfs.sys.KFSKeyConstants; 041 import org.kuali.kfs.sys.KFSPropertyConstants; 042 import org.kuali.kfs.sys.context.SpringContext; 043 import org.kuali.kfs.sys.document.FinancialSystemMaintainable; 044 import org.kuali.rice.kim.bo.Person; 045 import org.kuali.rice.kim.service.PersonService; 046 import org.kuali.rice.kns.bo.DocumentHeader; 047 import org.kuali.rice.kns.bo.PersistableBusinessObject; 048 import org.kuali.rice.kns.document.MaintenanceDocument; 049 import org.kuali.rice.kns.document.MaintenanceLock; 050 import org.kuali.rice.kns.service.BusinessObjectService; 051 import org.kuali.rice.kns.util.GlobalVariables; 052 import org.kuali.rice.kns.util.ObjectUtils; 053 import org.kuali.rice.kns.workflow.service.KualiWorkflowDocument; 054 055 /** 056 * Methods for the Award maintenance document UI. 057 */ 058 public class AwardMaintainableImpl extends FinancialSystemMaintainable { 059 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AwardMaintainableImpl.class); 060 /** 061 * Constructs an AwardMaintainableImpl. 062 */ 063 public AwardMaintainableImpl() { 064 super(); 065 } 066 067 /** 068 * Constructs a AwardMaintainableImpl. 069 * 070 * @param award 071 */ 072 public AwardMaintainableImpl(Award award) { 073 super(award); 074 this.setBoClass(award.getClass()); 075 } 076 077 /** 078 * This method is called for refreshing the Agency before display to show the full name in case the agency number was changed by 079 * hand before any submit that causes a redisplay. 080 */ 081 @Override 082 public void processAfterRetrieve() { 083 refreshAward(false); 084 super.processAfterRetrieve(); 085 } 086 087 /** 088 * This method is called for refreshing the Agency before a save to display the full name in case the agency number was changed 089 * by hand just before the save. 090 */ 091 @Override 092 public void prepareForSave() { 093 refreshAward(false); 094 List<AwardProjectDirector> directors = getAward().getAwardProjectDirectors(); 095 if (directors.size() == 1) { 096 directors.get(0).setAwardPrimaryProjectDirectorIndicator(true); 097 } 098 List<AwardOrganization> organizations = getAward().getAwardOrganizations(); 099 if (organizations.size() == 1) { 100 organizations.get(0).setAwardPrimaryOrganizationIndicator(true); 101 } 102 // need to populate the synthetic keys for these records 103 // since we can not depend on the keys which exist, we need to determine all those which could match 104 // so we can avoid them 105 List<AwardSubcontractor> awardSubcontractors = getAward().getAwardSubcontractors(); 106 if (awardSubcontractors != null && !awardSubcontractors.isEmpty()) { 107 // convert the list into a map of lists containing the used award subcontractor number/amendment number 108 Map<String,List<AwardSubcontractor>> subcontractorAwardMap = new HashMap<String, List<AwardSubcontractor>>(); 109 List<AwardSubcontractor> newSubcontractorRecords = new ArrayList<AwardSubcontractor>(); 110 for (AwardSubcontractor awardSubcontractor : awardSubcontractors) { 111 if ( !StringUtils.isBlank(awardSubcontractor.getAwardSubcontractorNumber()) ) { 112 // already has key - add to map 113 if ( !subcontractorAwardMap.containsKey(awardSubcontractor.getSubcontractorNumber()) ) { 114 subcontractorAwardMap.put(awardSubcontractor.getSubcontractorNumber(), new ArrayList<AwardSubcontractor>() ); 115 } 116 subcontractorAwardMap.get(awardSubcontractor.getSubcontractorNumber()).add(awardSubcontractor); 117 } else { 118 // new record, add to new map 119 newSubcontractorRecords.add(awardSubcontractor); 120 } 121 } 122 123 // now, loop over the new records 124 for (AwardSubcontractor awardSubcontractor : newSubcontractorRecords) { 125 String awardSubcontractorNumber = "1"; 126 String awardSubcontractorAmendmentNumber = "1"; 127 // get the other ones for the same subcontractor 128 List<AwardSubcontractor> oldSubcontractors = subcontractorAwardMap.get(awardSubcontractor.getSubcontractorNumber()); 129 if ( oldSubcontractors != null ) { 130 // we have a hit - find the first non-used number 131 // build an array from the unsorted list 132 boolean[][] nums = new boolean[100][100]; 133 for ( AwardSubcontractor oldSub : oldSubcontractors ) { 134 try { 135 nums[Integer.valueOf( oldSub.getAwardSubcontractorNumber() )][Integer.valueOf( oldSub.getAwardSubcontractorAmendmentNumber() )] = true; 136 } catch ( NumberFormatException ex ) { 137 // do nothing 138 LOG.warn( "Unexpected non-integer award subcontractor / amendment number: " + oldSub.getAwardSubcontractorNumber() + " / " + oldSub.getAwardSubcontractorAmendmentNumber() ); 139 } 140 } 141 // iterate over the array to get the first empty value 142 // loop over the awardSubcontractorNumbers first 143 boolean foundNumbers = false; 144 for ( int i = 1; i <= 99; i++ ) { 145 for ( int j = 1; j <= 99; j++ ) { 146 if ( !nums[j][i] ) { 147 // save the values 148 awardSubcontractorNumber = Integer.toString(j); 149 awardSubcontractorAmendmentNumber = Integer.toString(i); 150 // mark the cell as used before the next pass 151 nums[j][i] = true; 152 // just a flag to allow us to break out of both loops 153 foundNumbers = true; 154 break; 155 } 156 } 157 if ( foundNumbers ) { 158 break; 159 } 160 // JHK - yes, this will break down if there are more than 9801 subcontracts 161 // however, the UI will probably break down far before then... 162 } 163 } 164 awardSubcontractor.setAwardSubcontractorNumber(awardSubcontractorNumber); 165 awardSubcontractor.setAwardSubcontractorAmendmentNumber(awardSubcontractorAmendmentNumber); 166 } 167 168 } 169 170 171 // The implementation below is **** - allows for easy key collisions 172 // List<AwardSubcontractor> awardSubcontractors = getAward().getAwardSubcontractors(); 173 // int i = 0; 174 // if (awardSubcontractors != null && !awardSubcontractors.isEmpty()) { 175 // for (AwardSubcontractor awardSubcontractor : awardSubcontractors) { 176 // i++; 177 // if (StringUtils.isBlank(awardSubcontractor.getAwardSubcontractorAmendmentNumber())) { 178 // awardSubcontractor.setAwardSubcontractorAmendmentNumber("" + i); 179 // } 180 // if (StringUtils.isBlank(awardSubcontractor.getAwardSubcontractorNumber())) { 181 // awardSubcontractor.setAwardSubcontractorNumber("" + i); 182 // } 183 // } 184 // } 185 186 super.prepareForSave(); 187 } 188 189 /** 190 * This method is called for refreshing the Agency after a lookup to display its full name without AJAX. 191 * 192 * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#refresh(java.lang.String, java.util.Map, 193 * org.kuali.rice.kns.document.MaintenanceDocument) 194 */ 195 @SuppressWarnings("unchecked") 196 @Override 197 public void refresh(String refreshCaller, Map fieldValues, MaintenanceDocument document) { 198 if (StringUtils.equals("proposalLookupable", (String) fieldValues.get(KFSConstants.REFRESH_CALLER))) { 199 200 boolean awarded = AwardRuleUtil.isProposalAwarded(getAward()); 201 if (awarded) { 202 String pathToMaintainable = DOCUMENT + "." + NEW_MAINTAINABLE_OBJECT; 203 GlobalVariables.getMessageMap().addToErrorPath(pathToMaintainable); 204 GlobalVariables.getMessageMap().putError(KFSPropertyConstants.PROPOSAL_NUMBER, KFSKeyConstants.ERROR_AWARD_PROPOSAL_AWARDED, new String[] { getAward().getProposalNumber().toString() }); 205 GlobalVariables.getMessageMap().removeFromErrorPath(pathToMaintainable); 206 } 207 208 // SEE KULCG-315 for details on why this code is commented out. 209 // if (AwardRuleUtil.isProposalInactive(getAward())) { 210 // GlobalVariables.getMessageMap().putError(KFSPropertyConstants.PROPOSAL_NUMBER, 211 // KFSKeyConstants.ERROR_AWARD_PROPOSAL_INACTIVE, new String[] { getAward().getProposalNumber().toString() }); 212 // } 213 214 // copy over proposal values after refresh 215 if (!awarded) { 216 refreshAward(true); 217 fieldValues.put(KFSConstants.REFERENCES_TO_REFRESH, "proposal"); 218 super.refresh(refreshCaller, fieldValues, document); 219 getAward().populateFromProposal(getAward().getProposal()); 220 refreshAward(true); 221 } 222 } else { 223 refreshAward(KFSConstants.KUALI_LOOKUPABLE_IMPL.equals(fieldValues.get(KFSConstants.REFRESH_CALLER))); 224 super.refresh(refreshCaller, fieldValues, document); 225 } 226 227 } 228 229 /** 230 * Load related objects from the database as needed. 231 * 232 * @param refreshFromLookup 233 */ 234 private void refreshAward(boolean refreshFromLookup) { 235 Award award = getAward(); 236 award.refreshNonUpdateableReferences(); 237 238 getNewCollectionLine(AWARD_SUBCONTRACTORS).refreshNonUpdateableReferences(); 239 getNewCollectionLine(AWARD_PROJECT_DIRECTORS).refreshNonUpdateableReferences(); 240 getNewCollectionLine(AWARD_ACCOUNTS).refreshNonUpdateableReferences(); 241 242 // the org list doesn't need any refresh 243 refreshNonUpdateableReferences(award.getAwardOrganizations()); 244 refreshNonUpdateableReferences(award.getAwardAccounts()); 245 refreshNonUpdateableReferences(award.getAwardSubcontractors()); 246 refreshAwardProjectDirectors(refreshFromLookup); 247 } 248 249 /** 250 * Refresh the collection of associated AwardProjectDirectors. 251 * 252 * @param refreshFromLookup a lookup returns only the primary key, so ignore the secondary key when true 253 */ 254 private void refreshAwardProjectDirectors(boolean refreshFromLookup) { 255 if (refreshFromLookup) { 256 getNewCollectionLine(AWARD_PROJECT_DIRECTORS).refreshNonUpdateableReferences(); 257 refreshNonUpdateableReferences(getAward().getAwardProjectDirectors()); 258 259 getNewCollectionLine(AWARD_ACCOUNTS).refreshNonUpdateableReferences(); 260 refreshNonUpdateableReferences(getAward().getAwardAccounts()); 261 } 262 else { 263 refreshWithSecondaryKey((AwardProjectDirector) getNewCollectionLine(AWARD_PROJECT_DIRECTORS)); 264 for (AwardProjectDirector projectDirector : getAward().getAwardProjectDirectors()) { 265 refreshWithSecondaryKey(projectDirector); 266 } 267 268 refreshWithSecondaryKey((AwardAccount) getNewCollectionLine(AWARD_ACCOUNTS)); 269 for (AwardAccount account : getAward().getAwardAccounts()) { 270 refreshWithSecondaryKey(account); 271 } 272 } 273 } 274 275 /** 276 * @param collection 277 */ 278 private static void refreshNonUpdateableReferences(Collection<? extends PersistableBusinessObject> collection) { 279 for (PersistableBusinessObject item : collection) { 280 item.refreshNonUpdateableReferences(); 281 } 282 } 283 284 /** 285 * Refreshes the reference to ProjectDirector, giving priority to its secondary key. Any secondary key that it has may be user 286 * input, so that overrides the primary key, setting the primary key. If its primary key is blank or nonexistent, then leave the 287 * current reference as it is, because it may be a nonexistent instance which is holding the secondary key (the username, i.e., 288 * principalName) so we can redisplay it to the user for correction. If it only has a primary key then use that, because it may 289 * be coming from the database, without any user input. 290 * 291 * @param director the ProjectDirector to refresh 292 */ 293 private static void refreshWithSecondaryKey(CGProjectDirector director) { 294 Person cgdir = director.getProjectDirector(); 295 if (ObjectUtils.isNotNull(cgdir)) { 296 String secondaryKey = cgdir.getPrincipalName(); 297 if (StringUtils.isNotBlank(secondaryKey)) { 298 Person dir = SpringContext.getBean(PersonService.class).getPersonByPrincipalName(secondaryKey); 299 director.setPrincipalId(dir == null ? null : dir.getPrincipalId()); 300 } 301 if (StringUtils.isNotBlank(director.getPrincipalId())) { 302 Person person = SpringContext.getBean(PersonService.class).getPerson(director.getPrincipalId()); 303 if (person != null) { 304 ((PersistableBusinessObject) director).refreshNonUpdateableReferences(); 305 } 306 } 307 } 308 } 309 310 /** 311 * Gets the underlying Award. 312 * 313 * @return 314 */ 315 public Award getAward() { 316 return (Award) getBusinessObject(); 317 } 318 319 /** 320 * Called for refreshing the {@link Subcontractor} on {@link ProposalSubcontractor} before adding to the proposalSubcontractors 321 * collection on the proposal. this is to ensure that the summary fields are show correctly. i.e. {@link Subcontractor} name 322 * 323 * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#addNewLineToCollection(java.lang.String) 324 */ 325 @Override 326 public void addNewLineToCollection(String collectionName) { 327 refreshAward(false); 328 super.addNewLineToCollection(collectionName); 329 } 330 331 /** 332 * This method overrides the parent method to check the status of the award document and change the linked 333 * {@link ProposalStatus} to A (Approved) if the {@link Award} is now in approved status. 334 * 335 * @see org.kuali.rice.kns.maintenance.KualiMaintainableImpl#doRouteStatusChange(org.kuali.rice.kns.bo.DocumentHeader) 336 */ 337 @Override 338 public void doRouteStatusChange(DocumentHeader header) { 339 super.doRouteStatusChange(header); 340 341 Award award = getAward(); 342 KualiWorkflowDocument workflowDoc = header.getWorkflowDocument(); 343 344 // Use the stateIsProcessed() method so this code is only executed when the final approval occurs 345 if (workflowDoc.stateIsProcessed()) { 346 Proposal proposal = award.getProposal(); 347 proposal.setProposalStatusCode(Proposal.AWARD_CODE); 348 SpringContext.getBean(BusinessObjectService.class).save(proposal); 349 } 350 351 } 352 353 public List<MaintenanceLock> generateMaintenanceLocks() { 354 List<MaintenanceLock> locks = super.generateMaintenanceLocks(); 355 return locks; 356 } 357 }