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.cab.batch.service.impl;
017    
018    import java.util.ArrayList;
019    import java.util.Collection;
020    import java.util.HashMap;
021    import java.util.HashSet;
022    import java.util.LinkedHashMap;
023    import java.util.List;
024    import java.util.Map;
025    
026    import org.apache.log4j.Logger;
027    import org.kuali.kfs.coa.businessobject.Account;
028    import org.kuali.kfs.gl.businessobject.Entry;
029    import org.kuali.kfs.module.cab.CabPropertyConstants;
030    import org.kuali.kfs.module.cab.batch.service.ReconciliationService;
031    import org.kuali.kfs.module.cab.businessobject.AccountLineGroup;
032    import org.kuali.kfs.module.cab.businessobject.GeneralLedgerEntry;
033    import org.kuali.kfs.module.cab.businessobject.GlAccountLineGroup;
034    import org.kuali.kfs.module.cab.businessobject.PurApAccountLineGroup;
035    import org.kuali.kfs.module.purap.businessobject.PurApAccountingLineBase;
036    import org.kuali.rice.kns.service.BusinessObjectService;
037    import org.kuali.rice.kns.util.KualiDecimal;
038    import org.springframework.transaction.annotation.Transactional;
039    
040    /**
041     * Default implementation of {@link ReconciliationService}
042     */
043    @Transactional
044    public class ReconciliationServiceImpl implements ReconciliationService {
045        private static final Logger LOG = Logger.getLogger(ReconciliationServiceImpl.class);
046        protected BusinessObjectService businessObjectService;
047        protected List<Entry> ignoredEntries = new ArrayList<Entry>();
048        protected List<Entry> duplicateEntries = new ArrayList<Entry>();
049        protected Collection<GlAccountLineGroup> matchedGroups = new HashSet<GlAccountLineGroup>();
050        protected Collection<GlAccountLineGroup> misMatchedGroups = new HashSet<GlAccountLineGroup>();
051        protected HashMap<GlAccountLineGroup, GlAccountLineGroup> glEntryGroupMap = new HashMap<GlAccountLineGroup, GlAccountLineGroup>();
052        protected HashMap<PurApAccountLineGroup, PurApAccountLineGroup> purapAcctGroupMap = new HashMap<PurApAccountLineGroup, PurApAccountLineGroup>();
053    
054        /**
055         * @see org.kuali.kfs.module.cab.batch.service.ReconciliationService#reconcile(java.util.Collection, java.util.Collection,
056         *      java.util.Collection)
057         */
058        public void reconcile(Collection<Entry> glEntries, Collection<PurApAccountingLineBase> purapAcctEntries) {
059            /**
060             * FORMULA is to equate amount value (GL_ENTRY_T + GL_PEND_ENTRY_T = AP_ACCT_LINE_HIST)
061             */
062            LOG.info("Reconcile started");
063            groupGLEntries(glEntries);
064            groupPurapAccountEntries(purapAcctEntries);
065            reconcileGroups(glEntryGroupMap.values());
066    
067            // check for continuation account numbers
068            if (!misMatchedGroups.isEmpty()) {
069                LOG.info("Checking for continuation account");
070                checkGroupByContinuationAccount();
071                reconcileGroups(misMatchedGroups);
072            }
073            LOG.info("Reconcile finished");
074        }
075    
076        /**
077         * This method will run through all PO Accounting lines and Pending GL Lines for which a match was not found. Then check if
078         * account number is expired and continuation account is available. If true then reassign the account group with this new
079         * continuation account number.
080         */
081        protected void checkGroupByContinuationAccount() {
082            // get the keys first to avoid concurrent modification issues
083            List<PurApAccountLineGroup> purapGroups = new ArrayList<PurApAccountLineGroup>();
084            purapGroups.addAll(purapAcctGroupMap.keySet());
085    
086            for (PurApAccountLineGroup purapAcctLineGroup : purapGroups) {
087                // if not matched already, check and replace with continuation account
088                if (!matchedGroups.contains(purapAcctLineGroup)) {
089                    Account account = findAccount(purapAcctLineGroup);
090                    // find the account and check expiration date and continuation
091                    String continuationAcctNum = null;
092                    if (account.isExpired() && (continuationAcctNum = account.getContinuationAccountNumber()) != null) {
093                        LOG.debug("Continutation account found for " + account.getAccountNumber() + " is " + account.getContinuationAccountNumber());
094                        purapAcctGroupMap.remove(purapAcctLineGroup);
095                        purapAcctLineGroup.setAccountNumber(continuationAcctNum);
096                        purapAcctGroupMap.put(purapAcctLineGroup, purapAcctLineGroup);
097                    }
098                }
099            }
100    
101        }
102    
103        /**
104         * Finds an account object using its primary key
105         * 
106         * @param acctLineGroup AcctLineGroup
107         * @return Account
108         */
109        protected Account findAccount(AccountLineGroup acctLineGroup) {
110            Map<String, String> keys = new HashMap<String, String>();
111            keys.put(CabPropertyConstants.Account.CHART_OF_ACCOUNTS_CODE, acctLineGroup.getChartOfAccountsCode());
112            keys.put(CabPropertyConstants.Account.ACCOUNT_NUMBER, acctLineGroup.getAccountNumber());
113            Account account = (Account) businessObjectService.findByPrimaryKey(Account.class, keys);
114            return account;
115        }
116    
117        /**
118         * Identify and separate the matching and mismatching account line groups
119         * 
120         * @param glKeySet GL Account Line groups
121         */
122        protected void reconcileGroups(Collection<GlAccountLineGroup> glKeySet) {
123            for (GlAccountLineGroup glAccountLineGroup : glKeySet) {
124                PurApAccountLineGroup purapAccountLineGroup = this.purapAcctGroupMap.get(glAccountLineGroup);
125                KualiDecimal glAmt = this.glEntryGroupMap.get(glAccountLineGroup).getAmount();
126    
127                if (purapAccountLineGroup == null || !glAmt.equals(purapAccountLineGroup.getAmount())) {
128                    LOG.debug("GL account line " + glAccountLineGroup.toString() + " did not find a matching purchasing account line group");
129                    misMatchedGroups.add(glAccountLineGroup);
130                }
131                else {
132                    LOG.debug("GL account line " + glAccountLineGroup.toString() + " found a matching Purchasing account line group ");
133                    glAccountLineGroup.setMatchedPurApAcctLines(purapAccountLineGroup.getSourceEntries());
134                    matchedGroups.add(glAccountLineGroup);
135                    misMatchedGroups.remove(glAccountLineGroup);
136                }
137            }
138        }
139    
140        /**
141         * Groups GL entries by fields by univ_fiscal_yr, fin_coa_cd, account_nbr, sub_acct_nbr, fin_object_cd, fin_sub_obj_cd,
142         * univ_fiscal_prd_cd, fdoc_nbr, fdoc_ref_nbr
143         * 
144         * @param glEntries GL Entries
145         */
146        protected void groupGLEntries(Collection<Entry> glEntries) {
147            for (Entry glEntry : glEntries) {
148                // Step-1 Ignore zero or null amounts
149                if (glEntry.getTransactionLedgerEntryAmount() == null || glEntry.getTransactionLedgerEntryAmount().isZero()) {
150                    this.ignoredEntries.add(glEntry);
151                }
152                else if (isDuplicateEntry(glEntry)) {
153                    // Ignore the duplicate entries
154                    this.duplicateEntries.add(glEntry);
155                }
156                else {
157                    // Step-2 Group by univ_fiscal_yr, fin_coa_cd, account_nbr, sub_acct_nbr, fin_object_cd, fin_sub_obj_cd,
158                    // univ_fiscal_prd_cd, fdoc_nbr, fdoc_ref_nbr
159                    GlAccountLineGroup accountLineGroup = new GlAccountLineGroup(glEntry);
160                    GlAccountLineGroup targetAccountLineGroup = glEntryGroupMap.get(accountLineGroup);
161                    if (targetAccountLineGroup == null) {
162                        glEntryGroupMap.put(accountLineGroup, accountLineGroup);
163                    }
164                    else {
165                        // group GL entries
166                        targetAccountLineGroup.combineEntry(glEntry);
167                    }
168                }
169            }
170        }
171    
172        /**
173         * Groups Purap Account Line entries by fields by univ_fiscal_yr, fin_coa_cd, account_nbr, sub_acct_nbr, fin_object_cd,
174         * fin_sub_obj_cd, univ_fiscal_prd_cd, fdoc_nbr, fdoc_ref_nbr
175         * 
176         * @param purapAcctEntries Purap account entries
177         */
178        protected void groupPurapAccountEntries(Collection<PurApAccountingLineBase> purapAcctEntries) {
179            for (PurApAccountingLineBase entry : purapAcctEntries) {
180                if (entry.getAmount() != null && !entry.getAmount().isZero()) {
181                    // Step-2 Group by univ_fiscal_yr, fin_coa_cd, account_nbr, sub_acct_nbr, fin_object_cd, fin_sub_obj_cd,
182                    // univ_fiscal_prd_cd, fdoc_nbr, fdoc_ref_nbr
183                    PurApAccountLineGroup accountLineGroup = new PurApAccountLineGroup(entry);
184                    PurApAccountLineGroup targetAccountLineGroup = purapAcctGroupMap.get(accountLineGroup);
185                    if (targetAccountLineGroup == null) {
186                        purapAcctGroupMap.put(accountLineGroup, accountLineGroup);
187                    }
188                    else {
189                        // group GL entries
190                        targetAccountLineGroup.combineEntry(entry);
191                    }
192                }
193            }
194        }
195    
196        /**
197         * @see org.kuali.kfs.module.cab.batch.service.ReconciliationService#isDuplicateEntry(org.kuali.kfs.gl.businessobject.Entry)
198         */
199        public boolean isDuplicateEntry(Entry glEntry) {
200            // find matching entry from CB_GL_ENTRY_T
201            Map<String, Object> glKeys = new LinkedHashMap<String, Object>();
202            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.UNIVERSITY_FISCAL_YEAR, glEntry.getUniversityFiscalYear());
203            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.CHART_OF_ACCOUNTS_CODE, glEntry.getChartOfAccountsCode());
204            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.ACCOUNT_NUMBER, glEntry.getAccountNumber());
205            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.SUB_ACCOUNT_NUMBER, glEntry.getSubAccountNumber());
206            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.FINANCIAL_OBJECT_CODE, glEntry.getFinancialObjectCode());
207            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.FINANCIAL_SUB_OBJECT_CODE, glEntry.getFinancialSubObjectCode());
208            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.FINANCIAL_BALANCE_TYPE_CODE, glEntry.getFinancialBalanceTypeCode());
209            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.FINANCIAL_OBJECT_TYPE_CODE, glEntry.getFinancialObjectTypeCode());
210            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.UNIVERSITY_FISCAL_PERIOD_CODE, glEntry.getUniversityFiscalPeriodCode());
211            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.FINANCIAL_DOCUMENT_TYPE_CODE, glEntry.getFinancialDocumentTypeCode());
212            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.FINANCIAL_SYSTEM_ORIGINATION_CODE, glEntry.getFinancialSystemOriginationCode());
213            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.DOCUMENT_NUMBER, glEntry.getDocumentNumber());
214            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.TRANSACTION_LEDGER_ENTRY_SEQUENCE_NUMBER, glEntry.getTransactionLedgerEntrySequenceNumber());
215            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.ORGNIZATION_REFERENCE_ID, glEntry.getOrganizationReferenceId());
216            glKeys.put(CabPropertyConstants.GeneralLedgerEntry.PROJECT_CD, glEntry.getProjectCode());
217            Collection<GeneralLedgerEntry> matchingEntries = businessObjectService.findMatching(GeneralLedgerEntry.class, glKeys);
218            // if not found, return false
219            if (matchingEntries == null || matchingEntries.isEmpty()) {
220                return false;
221            }
222            return true;
223        }
224    
225        /**
226         * Gets the businessObjectService attribute.
227         * 
228         * @return Returns the businessObjectService
229         */
230    
231        public BusinessObjectService getBusinessObjectService() {
232            return businessObjectService;
233        }
234    
235        /**
236         * Sets the businessObjectService attribute.
237         * 
238         * @param businessObjectService The businessObjectService to set.
239         */
240    
241        public void setBusinessObjectService(BusinessObjectService businessObjectService) {
242            this.businessObjectService = businessObjectService;
243        }
244    
245        /**
246         * Gets the ignoredEntries attribute.
247         * 
248         * @return Returns the ignoredEntries
249         */
250    
251        public List<Entry> getIgnoredEntries() {
252            return ignoredEntries;
253        }
254    
255        /**
256         * Sets the ignoredEntries attribute.
257         * 
258         * @param ignoredEntries The ignoredEntries to set.
259         */
260    
261        public void setIgnoredEntries(List<Entry> ignoredEntries) {
262            this.ignoredEntries = ignoredEntries;
263        }
264    
265        /**
266         * Gets the duplicateEntries attribute.
267         * 
268         * @return Returns the duplicateEntries
269         */
270    
271        public List<Entry> getDuplicateEntries() {
272            return duplicateEntries;
273        }
274    
275        /**
276         * Sets the duplicateEntries attribute.
277         * 
278         * @param duplicateEntries The duplicateEntries to set.
279         */
280    
281        public void setDuplicateEntries(List<Entry> duplicateEntries) {
282            this.duplicateEntries = duplicateEntries;
283        }
284    
285        public Collection<GlAccountLineGroup> getMatchedGroups() {
286            return this.matchedGroups;
287        }
288    
289        public Collection<GlAccountLineGroup> getMisMatchedGroups() {
290            return this.misMatchedGroups;
291        }
292    
293    
294    }