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 }