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 }