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 }