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.coa.document.validation.impl;
017    
018    import java.sql.Timestamp;
019    import java.util.Calendar;
020    import java.util.Collection;
021    import java.util.HashMap;
022    import java.util.Map;
023    
024    import org.apache.commons.lang.StringUtils;
025    import org.apache.commons.lang.time.DateUtils;
026    import org.kuali.kfs.coa.businessobject.Account;
027    import org.kuali.kfs.coa.businessobject.AccountDelegate;
028    import org.kuali.kfs.sys.KFSConstants;
029    import org.kuali.kfs.sys.KFSKeyConstants;
030    import org.kuali.kfs.sys.KFSPropertyConstants;
031    import org.kuali.kfs.sys.context.SpringContext;
032    import org.kuali.kfs.sys.document.service.FinancialSystemDocumentTypeService;
033    import org.kuali.rice.kim.bo.Person;
034    import org.kuali.rice.kns.document.MaintenanceDocument;
035    import org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase;
036    import org.kuali.rice.kns.util.KualiDecimal;
037    import org.kuali.rice.kns.util.ObjectUtils;
038    
039    /**
040     * Validates content of a <code>{@link AccountDelegate}</code> maintenance document upon triggering of a approve, save, or route
041     * event.
042     */
043    public class DelegateRule extends MaintenanceDocumentRuleBase {
044    
045        protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DelegateRule.class);
046    
047        protected AccountDelegate oldDelegate;
048        protected AccountDelegate newDelegate;
049    
050        /**
051         * Constructs a DelegateRule.java.
052         */
053        public DelegateRule() {
054            super();
055        }
056    
057        /**
058         * This runs specific rules that are called when a document is saved:
059         * <ul>
060         * <li>{@link DelegateRule#checkSimpleRules()}</li>
061         * <li>{@link DelegateRule#checkOnlyOnePrimaryRoute(MaintenanceDocument)}</li>
062         * <li>{@link DelegateRule#checkDelegateUserRules(MaintenanceDocument)}</li>
063         * </ul>
064         * 
065         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
066         * @return doesn't fail on save, even if sub-rules fail
067         */
068        @Override
069        protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) {
070    
071            LOG.info("Entering processCustomSaveDocumentBusinessRules()");
072            setupConvenienceObjects(document);
073    
074            // check simple rules
075            checkSimpleRules();
076    
077            // disallow more than one PrimaryRoute for a given Chart/Account/Doctype
078            checkOnlyOnePrimaryRoute(document);
079    
080            // delegate user must be Active and Professional
081            checkDelegateUserRules(document);
082    
083            return true;
084        }
085    
086        /**
087         * This runs specific rules that are called when a document is routed:
088         * <ul>
089         * <li>{@link DelegateRule#checkSimpleRules()}</li>
090         * <li>{@link DelegateRule#checkOnlyOnePrimaryRoute(MaintenanceDocument)}</li>
091         * <li>{@link DelegateRule#checkDelegateUserRules(MaintenanceDocument)}</li>
092         * </ul>
093         * 
094         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomRouteDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
095         * @return fails if sub-rules fail
096         */
097        protected boolean processCustomRouteDocumentBusinessRules(MaintenanceDocument document) {
098            LOG.info("Entering processCustomRouteDocumentBusinessRules()");
099    
100            setupConvenienceObjects(document);
101    
102            // check simple rules
103            boolean success = checkSimpleRules();
104            
105            // disallow more than one PrimaryRoute for a given Chart/Account/Doctype
106            success &= checkOnlyOnePrimaryRoute(document);
107    
108            // delegate user must be Active and Professional
109            success &= checkDelegateUserRules(document);
110            
111            return success;
112        }
113    
114        /**
115         * This runs specific rules that are called when a document is approved:
116         * <ul>
117         * <li>{@link DelegateRule#checkSimpleRules()}</li>
118         * <li>{@link DelegateRule#checkOnlyOnePrimaryRoute(MaintenanceDocument)}</li>
119         * <li>{@link DelegateRule#checkDelegateUserRules(MaintenanceDocument)}</li>
120         * </ul>
121         * 
122         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomApproveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
123         */
124        protected boolean processCustomApproveDocumentBusinessRules(MaintenanceDocument document) {
125    
126            boolean success = true;
127    
128            LOG.info("Entering processCustomApproveDocumentBusinessRules()");
129            setupConvenienceObjects(document);
130    
131            // check simple rules
132            success &= checkSimpleRules();
133    
134            success &= checkOnlyOnePrimaryRoute(document);
135    
136            // delegate user must be Active and Professional
137            success &= checkDelegateUserRules(document);
138    
139            return success;
140        }
141    
142        /**
143         * This method sets the convenience objects like newAccount and oldAccount, so you have short and easy handles to the new and
144         * old objects contained in the maintenance document. It also calls the BusinessObjectBase.refresh(), which will attempt to load
145         * all sub-objects from the DB by their primary keys, if available.
146         * 
147         * @param document - the maintenanceDocument being evaluated
148         */
149        protected void setupConvenienceObjects(MaintenanceDocument document) {
150    
151            // setup oldAccount convenience objects, make sure all possible sub-objects are populated
152            oldDelegate = (AccountDelegate) super.getOldBo();
153    
154            // setup newAccount convenience objects, make sure all possible sub-objects are populated
155            newDelegate = (AccountDelegate) super.getNewBo();
156        }
157    
158    
159        /**
160         * This checks to see if
161         * <ul>
162         * <li>the delegate start date is valid and they are active</li>
163         * <li>from amount is >= 0</li>
164         * <li>to amount cannot be empty when from amount is filled out</li>
165         * <li>to amount is >= from amount</li>
166         * <li>account cannot be closed</li>
167         * </ul>
168         * 
169         * @return
170         */
171        protected boolean checkSimpleRules() {
172            boolean success = true;
173    
174            Map<String, String> fieldValues = new HashMap<String, String>();
175            fieldValues.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, newDelegate.getChartOfAccountsCode());
176            fieldValues.put(KFSPropertyConstants.ACCOUNT_NUMBER, newDelegate.getAccountNumber());
177            
178            int accountExist = getBoService().countMatching(Account.class, fieldValues);
179            if (accountExist<=0) {
180                putFieldError(KFSPropertyConstants.ACCOUNT_NUMBER, KFSKeyConstants.ERROR_EXISTENCE, newDelegate.getAccountNumber());
181                success &= false;
182            }
183    
184            // start date must be greater than or equal to today if active
185            boolean newActive = newDelegate.isActive();
186            if ((ObjectUtils.isNotNull(newDelegate.getAccountDelegateStartDate())) && newActive) {
187                Timestamp today = getDateTimeService().getCurrentTimestamp();
188                today.setTime(DateUtils.truncate(today, Calendar.DAY_OF_MONTH).getTime());
189                if (newDelegate.getAccountDelegateStartDate().before(today)) {
190                    putFieldError(KFSPropertyConstants.ACCOUNT_DELEGATE_START_DATE, KFSKeyConstants.ERROR_DOCUMENT_ACCTDELEGATEMAINT_STARTDATE_IN_PAST);
191                    success &= false;
192                }
193            }
194    
195            // FROM amount must be >= 0 (may not be negative)
196            KualiDecimal fromAmount = newDelegate.getFinDocApprovalFromThisAmt();
197            if (ObjectUtils.isNotNull(fromAmount)) {
198                if (fromAmount.isLessThan(KualiDecimal.ZERO)) {
199                    putFieldError(KFSPropertyConstants.FIN_DOC_APPROVAL_FROM_THIS_AMT, KFSKeyConstants.ERROR_DOCUMENT_ACCTDELEGATEMAINT_FROM_AMOUNT_NONNEGATIVE);
200                    success &= false;
201                }
202            }
203    
204            // TO amount must be >= FROM amount or Zero
205            KualiDecimal toAmount = newDelegate.getFinDocApprovalToThisAmount();
206            if (ObjectUtils.isNotNull(toAmount) && !toAmount.equals(KualiDecimal.ZERO)) {
207                // case if FROM amount is non-null and positive, disallow TO amount being less
208                if (fromAmount != null && toAmount.isLessThan(fromAmount)) {
209                    putFieldError(KFSPropertyConstants.FIN_DOC_APPROVAL_TO_THIS_AMOUNT, KFSKeyConstants.ERROR_DOCUMENT_ACCTDELEGATEMAINT_TO_AMOUNT_MORE_THAN_FROM_OR_ZERO);
210                    success &= false;
211                } else if (toAmount.isLessThan(KualiDecimal.ZERO)) {
212                    putFieldError(KFSPropertyConstants.FIN_DOC_APPROVAL_TO_THIS_AMOUNT, KFSKeyConstants.ERROR_DOCUMENT_ACCTDELEGATEMAINT_TO_AMOUNT_MORE_THAN_FROM_OR_ZERO);
213                    success &= false;
214                }
215            }
216            
217            // do we have a good document type?
218            final FinancialSystemDocumentTypeService documentService = SpringContext.getBean(FinancialSystemDocumentTypeService.class);
219            if (!documentService.isFinancialSystemDocumentType(newDelegate.getFinancialDocumentTypeCode())) {
220                putFieldError(KFSPropertyConstants.FINANCIAL_DOCUMENT_TYPE_CODE, KFSKeyConstants.ERROR_DOCUMENT_ACCTDELEGATEMAINT_INVALID_DOC_TYPE, new String[] { newDelegate.getFinancialDocumentTypeCode(), KFSConstants.ROOT_DOCUMENT_TYPE });
221                success = false;
222            }
223    
224            return success;
225        }
226    
227        /**
228         * This checks to see if there is already a record for the primary route
229         * 
230         * @param document
231         * @return false if there is a record
232         */
233        protected boolean checkOnlyOnePrimaryRoute(MaintenanceDocument document) {
234    
235            boolean success = true;
236            boolean checkDb = false;
237            boolean newPrimary;
238            boolean newActive;
239            boolean blockingDocumentExists;
240    
241            // exit out immediately if this doc is not requesting a primary route
242            newPrimary = newDelegate.isAccountsDelegatePrmrtIndicator();
243            if (!newPrimary) {
244                return success;
245            }
246    
247            // exit if new document not active
248            newActive = newDelegate.isActive();
249            if (!newActive) {
250                return success;
251            }
252    
253            // if its a new document, we are only interested if they have chosen this one
254            // to be a primary route
255            if (document.isNew()) {
256                if (newPrimary) {
257                    checkDb = true;
258                }
259            }
260    
261            // handle an edit, where all we care about is that someone might change it
262            // from NOT a primary TO a primary
263            if (document.isEdit()) {
264                boolean oldPrimary = oldDelegate.isAccountsDelegatePrmrtIndicator();
265                if (!oldPrimary && newPrimary) {
266                    checkDb = true;
267                }
268            }
269    
270            // if we dont want to check the db for another primary, then exit
271            if (!checkDb) {
272                return success;
273            }
274    
275            // if a primary already exists for a document type (including ALL), throw an error. However, current business rules
276            // should allow a primary for other document types, even if a primary for ALL already exists.
277    
278            Map whereMap = new HashMap();
279            whereMap.put("chartOfAccountsCode", newDelegate.getChartOfAccountsCode());
280            whereMap.put("accountNumber", newDelegate.getAccountNumber());
281            whereMap.put("accountsDelegatePrmrtIndicator", Boolean.valueOf(true));
282            whereMap.put("financialDocumentTypeCode", newDelegate.getFinancialDocumentTypeCode());
283            whereMap.put("active", Boolean.valueOf(true));
284    
285            // find all the matching records
286            Collection primaryRoutes = getBoService().findMatching(AccountDelegate.class, whereMap);
287    
288            // if there is at least one result, then this business rule is tripped
289            if (primaryRoutes.size() > 0) {
290                putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_ACCTDELEGATEMAINT_PRIMARY_ROUTE_ALREADY_EXISTS_FOR_DOCTYPE);
291                success &= false;
292            }
293    
294            return success;
295        }
296    
297        /**
298         * This checks to see if the user is valid and active for this module
299         * 
300         * @param document
301         * @return false if this user is not valid or active for being a delegate
302         */
303        protected boolean checkDelegateUserRules(MaintenanceDocument document) {
304            boolean success = true;
305            final Person accountDelegate = newDelegate.getAccountDelegate();
306    
307            // if the user doesn't exist, then do nothing, it'll fail the existence test elsewhere
308            if (ObjectUtils.isNull(accountDelegate)) {
309                return success;
310            }
311            
312            // if the document is inactivating an account delegate, don't bother validating the user
313            if (document.getOldMaintainableObject() != null && document.getOldMaintainableObject().getBusinessObject() != null && ((AccountDelegate)document.getOldMaintainableObject().getBusinessObject()).isActive() && !((AccountDelegate)document.getNewMaintainableObject().getBusinessObject()).isActive()) {
314                return success;
315            }
316    
317            if (StringUtils.isBlank(accountDelegate.getEntityId()) || !getDocumentHelperService().getDocumentAuthorizer(document).isAuthorized(document, KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_FISCAL_OFFICER_DELEGATE, accountDelegate.getPrincipalId())) {
318                super.putFieldError("accountDelegate.principalName", KFSKeyConstants.ERROR_USER_MISSING_PERMISSION, new String[] {accountDelegate.getName(), KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_FISCAL_OFFICER_DELEGATE});
319                success = false;
320            }
321            return success;
322        }
323    }