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>&amp;</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}