001/* 002 * Copyright 2007 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 */ 016package org.kuali.ole.sys.businessobject; 017 018import java.sql.Date; 019import java.util.Arrays; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026 027import org.kuali.ole.coa.businessobject.Account; 028import org.kuali.ole.coa.businessobject.ObjectCode; 029import org.kuali.ole.sys.context.SpringContext; 030import org.kuali.ole.sys.document.AccountingDocument; 031import org.kuali.ole.sys.document.service.AccountPresenceService; 032import org.kuali.rice.core.api.datetime.DateTimeService; 033import org.kuali.rice.kew.api.exception.WorkflowException; 034import org.kuali.rice.krad.document.Document; 035import org.kuali.rice.krad.service.DocumentService; 036import org.kuali.rice.krad.util.ObjectUtils; 037 038/** 039 * This class helps implement AccountingLine overrides. It is not persisted itself, but it simplifies working with the persisted 040 * codes. Instances break the code into components. Static methods help with the AccountingLine. 041 */ 042public class AccountingLineOverride { 043 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AccountingLineOverride.class); 044 045 /** 046 * These codes are the way the override is persisted in the AccountingLine. 047 */ 048 public static final class CODE { // todo: use JDK 1.5 enum 049 public static final String NONE = "NONE"; 050 public static final String EXPIRED_ACCOUNT = "EXPIRED_ACCOUNT"; 051 public static final String NON_BUDGETED_OBJECT = "NON_BUDGETED_OBJECT"; 052 public static final String TRANSACTION_EXCEEDS_REMAINING_BUDGET = "TRANSACTION_EXCEEDS_REMAINING_BUDGET"; 053 public static final String EXPIRED_ACCOUNT_AND_NON_BUDGETED_OBJECT = "EXPIRED_ACCOUNT_AND_NON_BUDGETED_OBJECT"; 054 public static final String NON_BUDGETED_OBJECT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET = "NON_BUDGETED_OBJECT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET"; 055 public static final String EXPIRED_ACCOUNT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET = "EXPIRED_ACCOUNT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET"; 056 public static final String EXPIRED_ACCOUNT_AND_NON_BUDGETED_OBJECT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET = "EXPIRED_ACCOUNT_AND_NON_BUDGETED_OBJECT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET"; 057 public static final String NON_FRINGE_ACCOUNT_USED = "NON_FRINGE_ACCOUNT_USED"; 058 public static final String EXPIRED_ACCOUNT_AND_NON_FRINGE_ACCOUNT_USED = "EXPIRED_ACCOUNT_AND_NON_FRINGE_ACCOUNT_USED"; 059 } 060 061 /** 062 * These are the somewhat independent components of an override. 063 */ 064 public static final class COMPONENT { // todo: use JDK 1.5 enum 065 public static final Integer EXPIRED_ACCOUNT = new Integer(1); 066 public static final Integer NON_BUDGETED_OBJECT = new Integer(2); 067 public static final Integer TRANSACTION_EXCEEDS_REMAINING_BUDGET = new Integer(3); 068 public static final Integer NON_FRINGE_ACCOUNT_USED = new Integer(8); 069 } 070 071 /** 072 * The names of the AccountingLine properties that the processForOutput() and determineNeededOverrides() methods use. Callers of 073 * those methods may need to refresh these fields from OJB. 074 */ 075 public static final List<String> REFRESH_FIELDS = Collections.unmodifiableList(Arrays.asList(new String[] { "account", "objectCode" })); 076 077 /** 078 * This holds an instance of every valid override, mapped by code. 079 */ 080 private static final Map<String, AccountingLineOverride> codeToOverrideMap = new HashMap<String, AccountingLineOverride>(); 081 082 /** 083 * This holds an instance of every valid override, mapped by components. 084 */ 085 private static final Map componentsToOverrideMap = new HashMap(); 086 087 static { 088 // populate the code map 089 new AccountingLineOverride(CODE.NONE, new Integer[] {}); 090 new AccountingLineOverride(CODE.EXPIRED_ACCOUNT, 091 // todo: use JDK 1.5 ... args 092 new Integer[] { COMPONENT.EXPIRED_ACCOUNT }); 093 new AccountingLineOverride(CODE.NON_BUDGETED_OBJECT, new Integer[] { COMPONENT.NON_BUDGETED_OBJECT }); 094 new AccountingLineOverride(CODE.TRANSACTION_EXCEEDS_REMAINING_BUDGET, new Integer[] { COMPONENT.TRANSACTION_EXCEEDS_REMAINING_BUDGET }); 095 new AccountingLineOverride(CODE.EXPIRED_ACCOUNT_AND_NON_BUDGETED_OBJECT, new Integer[] { COMPONENT.EXPIRED_ACCOUNT, COMPONENT.NON_BUDGETED_OBJECT }); 096 new AccountingLineOverride(CODE.NON_BUDGETED_OBJECT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET, new Integer[] { COMPONENT.NON_BUDGETED_OBJECT, COMPONENT.TRANSACTION_EXCEEDS_REMAINING_BUDGET }); 097 new AccountingLineOverride(CODE.EXPIRED_ACCOUNT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET, new Integer[] { COMPONENT.EXPIRED_ACCOUNT, COMPONENT.TRANSACTION_EXCEEDS_REMAINING_BUDGET }); 098 new AccountingLineOverride(CODE.EXPIRED_ACCOUNT_AND_NON_BUDGETED_OBJECT_AND_TRANSACTION_EXCEEDS_REMAINING_BUDGET, new Integer[] { COMPONENT.EXPIRED_ACCOUNT, COMPONENT.NON_BUDGETED_OBJECT, COMPONENT.TRANSACTION_EXCEEDS_REMAINING_BUDGET }); 099 new AccountingLineOverride(CODE.NON_FRINGE_ACCOUNT_USED, new Integer[] { COMPONENT.NON_FRINGE_ACCOUNT_USED }); 100 new AccountingLineOverride(CODE.EXPIRED_ACCOUNT_AND_NON_FRINGE_ACCOUNT_USED, new Integer[] { COMPONENT.EXPIRED_ACCOUNT, COMPONENT.NON_FRINGE_ACCOUNT_USED }); 101 } 102 103 private final String code; 104 private final Set components; 105 106 /** 107 * This private constructor is for the static initializer. 108 * 109 * @param myCode 110 * @param myComponents 111 */ 112 private AccountingLineOverride(String myCode, Integer[] myComponents) { 113 code = myCode; 114 components = componentsAsSet(myComponents); 115 codeToOverrideMap.put(code, this); 116 componentsToOverrideMap.put(components, this); 117 } 118 119 /** 120 * Checks whether this override contains the given component. 121 * 122 * @param component 123 * @return whether this override contains the given component. 124 */ 125 public boolean hasComponent(Integer component) { 126 return components.contains(component); 127 } 128 129 /** 130 * Gets the code of this override. 131 * 132 * @return the code of this override. 133 */ 134 public String getCode() { 135 return code; 136 } 137 138 /** 139 * Gets the components of this override. 140 * 141 * @return the components of this override. 142 */ 143 private Set getComponents() { 144 return components; 145 } 146 147 /** 148 * @see java.lang.Object#toString() 149 */ 150 @Override 151 public String toString() { 152 return "AccountingLineOverride (code " + code + ", components " + components + ")"; 153 } 154 155 /** 156 * Returns the AccountingLineOverride that has the components of this AccountingLineOverride minus any components not in the 157 * given mask. This is like <code>&</code>(a bit-wise and), if the components were bits. 158 * 159 * @param mask 160 * @return the AccountingLineOverride that has the components of this AccountingLineOverride minus any components not in the 161 * given mask. 162 * @throws IllegalArgumentException if there is no such valid combination of components 163 */ 164 public AccountingLineOverride mask(AccountingLineOverride mask) { 165 Set key = maskComponents(mask); 166 if (!isValidComponentSet(key)) { 167 throw new IllegalArgumentException("invalid component set " + key); 168 } 169 return valueOf(key); 170 } 171 172 /** 173 * Returns the Set of components that this override and the given override have in common. 174 * 175 * @param mask 176 * @return the Set of components that this override and the given override have in common. 177 */ 178 private Set maskComponents(AccountingLineOverride mask) { 179 Set retval = new HashSet(components); 180 retval.retainAll(mask.getComponents()); 181 return retval; 182 } 183 184 /** 185 * Returns whether this override, when masked by the given override, is valid. Some combinations of components have no override 186 * code defined. 187 * 188 * @param mask 189 * @return whether this override, when masked by the given override, is valid. 190 */ 191 public boolean isValidMask(AccountingLineOverride mask) { 192 return isValidComponentSet(maskComponents(mask)); 193 } 194 195 /** 196 * Returns whether the given String is a valid override code. 197 * 198 * @param code 199 * @return whether the given String is a valid override code. 200 */ 201 public static boolean isValidCode(String code) { 202 return codeToOverrideMap.containsKey(code); 203 } 204 205 /** 206 * Returns whether the given Integers are a valid set of components. Some combinations of components are invalid and have no 207 * code defined. 208 * 209 * @param components 210 * @return whether the given Integers are a valid set of components. 211 */ 212 public static boolean isValidComponentSet(Integer[] components) { 213 return isValidComponentSet(componentsAsSet(components)); 214 } 215 216 private static boolean isValidComponentSet(Set components) { // todo: JDK 1.5 generic Set 217 return componentsToOverrideMap.containsKey(components); 218 } 219 220 /** 221 * Factory method from code. 222 * 223 * @param code the override code 224 * @return the AccountingLineOverride instance corresponding to the given code. 225 * @throws IllegalArgumentException if the given code is not valid 226 */ 227 public static AccountingLineOverride valueOf(String code) { 228 if (!isValidCode(code)) { 229 throw new IllegalArgumentException("invalid code " + code); 230 } 231 return (AccountingLineOverride) codeToOverrideMap.get(code); // todo: JDK 1.5 generic Map instead of cast 232 } 233 234 /** 235 * Factory method from components. 236 * 237 * @param components the override components, treated as a set 238 * @return the AccountingLineOverride instance corresponding to the given component set. 239 * @throws IllegalArgumentException if the given set of components is not valid 240 */ 241 public static AccountingLineOverride valueOf(Integer[] components) { 242 Set key = componentsAsSet(components); 243 if (!isValidComponentSet(key)) { 244 throw new IllegalArgumentException("invalid component set " + key); 245 } 246 return valueOf(key); 247 } 248 249 public static AccountingLineOverride valueOf(Set components) { 250 return (AccountingLineOverride) componentsToOverrideMap.get(components); // todo: JDK 1.5 generic Map instead of cast 251 } 252 253 private static Set componentsAsSet(Integer[] components) { 254 return Collections.unmodifiableSet(new HashSet(Arrays.asList(components))); 255 } 256 257 /** 258 * On the given AccountingLine, converts override input checkboxes from a Struts Form into a persistable override code. 259 * 260 * @param line 261 */ 262 public static void populateFromInput(AccountingLine line) { 263 // todo: this logic won't work if a single account checkbox might also stands for NON_FRINGE_ACCOUNT_USED; needs thought 264 265 Set overrideInputComponents = new HashSet(); 266 if (line.getAccountExpiredOverride()) { 267 overrideInputComponents.add(COMPONENT.EXPIRED_ACCOUNT); 268 } 269 if (line.isObjectBudgetOverride()) { 270 overrideInputComponents.add(COMPONENT.NON_BUDGETED_OBJECT); 271 } 272 if (!isValidComponentSet(overrideInputComponents)) { 273 // todo: error for invalid override checkbox combinations, for which there is no override code 274 } 275 line.setOverrideCode(valueOf(overrideInputComponents).getCode()); 276 } 277 278 /** 279 * Prepares the given AccountingLine in a Struts Action for display by a JSP. This means converting the override code to 280 * checkboxes for display and input, as well as analysing the accounting line and determining which override checkboxes are 281 * needed. 282 * 283 * @param line 284 */ 285 public static void processForOutput(AccountingDocument document ,AccountingLine line) { 286 AccountingLineOverride fromCurrentCode = valueOf(line.getOverrideCode()); 287 AccountingLineOverride needed = determineNeededOverrides(document,line); 288 line.setAccountExpiredOverride(fromCurrentCode.hasComponent(COMPONENT.EXPIRED_ACCOUNT)); 289 line.setAccountExpiredOverrideNeeded(needed.hasComponent(COMPONENT.EXPIRED_ACCOUNT)); 290 line.setObjectBudgetOverride(fromCurrentCode.hasComponent(COMPONENT.NON_BUDGETED_OBJECT)); 291 line.setObjectBudgetOverrideNeeded(needed.hasComponent(COMPONENT.NON_BUDGETED_OBJECT)); 292 } 293 294 /** 295 * Determines what overrides the given line needs. 296 * 297 * @param line 298 * @return what overrides the given line needs. 299 */ 300 public static AccountingLineOverride determineNeededOverrides(AccountingDocument document ,AccountingLine line) { 301 boolean isDocumentFinalOrProcessed = false; 302 if(ObjectUtils.isNotNull(document)) { 303 AccountingDocument accountingDocument = (AccountingDocument) document; 304 isDocumentFinalOrProcessed = accountingDocument.isDocumentFinalOrProcessed(); 305 } 306 307 Set neededOverrideComponents = new HashSet(); 308 if (needsExpiredAccountOverride(line, isDocumentFinalOrProcessed)) { 309 neededOverrideComponents.add(COMPONENT.EXPIRED_ACCOUNT); 310 } 311 if (needsObjectBudgetOverride(line.getAccount(), line.getObjectCode())) { 312 neededOverrideComponents.add(COMPONENT.NON_BUDGETED_OBJECT); 313 } 314 315 if (!isValidComponentSet(neededOverrideComponents)) { 316 // todo: error for invalid override checkbox combinations, for which there is no override code 317 } 318 return valueOf(neededOverrideComponents); 319 } 320 321 /** 322 * Returns whether the given account needs an expired account override. 323 * 324 * @param account 325 * @return whether the given account needs an expired account override. 326 */ 327 public static boolean needsExpiredAccountOverride(Account account) { 328 return !ObjectUtils.isNull(account) && account.isActive() && account.isExpired(); 329 } 330 331 /** 332 * Returns whether the given account needs an expired account override. 333 * 334 * @param account 335 * @return whether the given account needs an expired account override. 336 */ 337 public static boolean needsExpiredAccountOverride(AccountingLine line, boolean isDocumentFinalOrProcessed ) { 338 if(isDocumentFinalOrProcessed){ 339 if(CODE.EXPIRED_ACCOUNT.equals(line.getOverrideCode())) { 340 return true; 341 } 342 else { 343 return false; 344 } 345 } 346 else { 347 return !ObjectUtils.isNull(line.getAccount()) && line.getAccount().isActive() && line.getAccount().isExpired(); 348 } 349 } 350 351 /** 352 * Returns whether the given account needs an expired account override. 353 * 354 * @param account 355 * @return whether the given account needs an expired account override. 356 */ 357 public static boolean needsNonFringAccountOverride(Account account) { 358 return !ObjectUtils.isNull(account) && account.isActive() && !account.isAccountsFringesBnftIndicator(); 359 } 360 361 /** 362 * Returns whether the given object code needs an object budget override 363 * 364 * @param account 365 * @return whether the given object code needs an object budget override 366 */ 367 public static boolean needsObjectBudgetOverride(Account account, ObjectCode objectCode) { 368 return !ObjectUtils.isNull(account) && !ObjectUtils.isNull(objectCode) && account.isActive() && !SpringContext.getBean(AccountPresenceService.class).isObjectCodeBudgetedForAccountPresence(account, objectCode); 369 } 370 371 public static Document getDocument(AccountingLine line) { 372 Document document = null; 373 try { 374 document = SpringContext.getBean(DocumentService.class).getByDocumentHeaderId(line.getDocumentNumber()); 375 376 }catch(WorkflowException exception) { 377 LOG.error("Unable to locate document for documentId :: " + line.getDocumentNumber() ); 378 } 379 380 return document; 381 } 382}