/*
* The Kuali Financial System, a comprehensive financial management system for higher education.
*
* Copyright 2005-2014 The Kuali Foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kuali.kfs.module.cab.batch.service.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import org.kuali.kfs.coa.businessobject.Account;
import org.kuali.kfs.gl.businessobject.Entry;
import org.kuali.kfs.module.cab.CabPropertyConstants;
import org.kuali.kfs.module.cab.batch.service.ReconciliationService;
import org.kuali.kfs.module.cab.businessobject.AccountLineGroup;
import org.kuali.kfs.module.cab.businessobject.GlAccountLineGroup;
import org.kuali.kfs.module.cab.businessobject.PurApAccountLineGroup;
import org.kuali.kfs.module.cab.dataaccess.ReconciliationDao;
import org.kuali.kfs.module.purap.businessobject.PurApAccountingLineBase;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.krad.service.BusinessObjectService;
import org.springframework.transaction.annotation.Transactional;
/**
* Default implementation of {@link ReconciliationService}
*/
@Transactional
public class ReconciliationServiceImpl implements ReconciliationService {
private static final Logger LOG = Logger.getLogger(ReconciliationServiceImpl.class);
protected BusinessObjectService businessObjectService;
protected ReconciliationDao reconciliationDao;
protected List<Entry> ignoredEntries = new ArrayList<Entry>();
protected List<Entry> duplicateEntries = new ArrayList<Entry>();
protected Collection<GlAccountLineGroup> matchedGroups = new HashSet<GlAccountLineGroup>();
protected Collection<GlAccountLineGroup> misMatchedGroups = new HashSet<GlAccountLineGroup>();
protected HashMap<GlAccountLineGroup, GlAccountLineGroup> glEntryGroupMap = new HashMap<GlAccountLineGroup, GlAccountLineGroup>();
protected HashMap<PurApAccountLineGroup, PurApAccountLineGroup> purapAcctGroupMap = new HashMap<PurApAccountLineGroup, PurApAccountLineGroup>();
/**
* @see org.kuali.kfs.module.cab.batch.service.ReconciliationService#reconcile(java.util.Collection, java.util.Collection,
* java.util.Collection)
*/
public void reconcile(Collection<Entry> glEntries, Collection<PurApAccountingLineBase> purapAcctEntries) {
/**
* FORMULA is to equate amount value (GL_ENTRY_T + GL_PEND_ENTRY_T = AP_ACCT_LINE_HIST)
*/
LOG.debug("Reconcile started");
groupGLEntries(glEntries);
groupPurapAccountEntries(purapAcctEntries);
reconcileGroups(glEntryGroupMap.values());
// check for continuation account numbers
if (!misMatchedGroups.isEmpty()) {
LOG.info("Checking for continuation account");
checkGroupByContinuationAccount();
reconcileGroups(misMatchedGroups);
}
LOG.debug("Reconcile finished");
}
/**
* This method will run through all PO Accounting lines and Pending GL Lines for which a match was not found. Then check if
* account number is expired and continuation account is available. If true then reassign the account group with this new
* continuation account number.
*/
protected void checkGroupByContinuationAccount() {
// get the keys first to avoid concurrent modification issues
List<PurApAccountLineGroup> purapGroups = new ArrayList<PurApAccountLineGroup>();
purapGroups.addAll(purapAcctGroupMap.keySet());
for (PurApAccountLineGroup purapAcctLineGroup : purapGroups) {
// if not matched already, check and replace with continuation account
if (!matchedGroups.contains(purapAcctLineGroup)) {
Account account = findAccount(purapAcctLineGroup);
// find the account and check expiration date and continuation
String continuationAcctNum = null;
if (account.isExpired() && (continuationAcctNum = account.getContinuationAccountNumber()) != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Continutation account found for " + account.getAccountNumber() + " is " + account.getContinuationAccountNumber());
}
purapAcctGroupMap.remove(purapAcctLineGroup);
purapAcctLineGroup.setAccountNumber(continuationAcctNum);
purapAcctGroupMap.put(purapAcctLineGroup, purapAcctLineGroup);
}
}
}
}
/**
* Finds an account object using its primary key
*
* @param acctLineGroup AcctLineGroup
* @return Account
*/
protected Account findAccount(AccountLineGroup acctLineGroup) {
Map<String, String> keys = new HashMap<String, String>();
keys.put(CabPropertyConstants.Account.CHART_OF_ACCOUNTS_CODE, acctLineGroup.getChartOfAccountsCode());
keys.put(CabPropertyConstants.Account.ACCOUNT_NUMBER, acctLineGroup.getAccountNumber());
Account account = (Account) businessObjectService.findByPrimaryKey(Account.class, keys);
return account;
}
/**
* Identify and separate the matching and mismatching account line groups
*
* @param glKeySet GL Account Line groups
*/
protected void reconcileGroups(Collection<GlAccountLineGroup> glKeySet) {
for (GlAccountLineGroup glAccountLineGroup : glKeySet) {
PurApAccountLineGroup purapAccountLineGroup = this.purapAcctGroupMap.get(glAccountLineGroup);
KualiDecimal glAmt = this.glEntryGroupMap.get(glAccountLineGroup).getAmount();
if (purapAccountLineGroup == null || !glAmt.equals(purapAccountLineGroup.getAmount())) {
if (LOG.isDebugEnabled()) {
LOG.debug("GL account line " + glAccountLineGroup.toString() + " did not find a matching purchasing account line group");
}
misMatchedGroups.add(glAccountLineGroup);
}
else {
if (LOG.isDebugEnabled()) {
LOG.debug("GL account line " + glAccountLineGroup.toString() + " found a matching Purchasing account line group ");
}
glAccountLineGroup.setMatchedPurApAcctLines(purapAccountLineGroup.getSourceEntries());
matchedGroups.add(glAccountLineGroup);
misMatchedGroups.remove(glAccountLineGroup);
}
}
}
/**
* Groups GL entries by fields by univ_fiscal_yr, fin_coa_cd, account_nbr, sub_acct_nbr, fin_object_cd, fin_sub_obj_cd,
* univ_fiscal_prd_cd, fdoc_nbr, fdoc_ref_nbr
*
* @param glEntries GL Entries
*/
protected void groupGLEntries(Collection<Entry> glEntries) {
for (Entry glEntry : glEntries) {
// Step-1 Ignore zero or null amounts
if (glEntry.getTransactionLedgerEntryAmount() == null || glEntry.getTransactionLedgerEntryAmount().isZero()) {
this.ignoredEntries.add(glEntry);
}
else if (isDuplicateEntry(glEntry)) {
// Ignore the duplicate entries
this.duplicateEntries.add(glEntry);
}
else {
// Step-2 Group by univ_fiscal_yr, fin_coa_cd, account_nbr, sub_acct_nbr, fin_object_cd, fin_sub_obj_cd,
// univ_fiscal_prd_cd, fdoc_nbr, fdoc_ref_nbr
GlAccountLineGroup accountLineGroup = new GlAccountLineGroup(glEntry);
GlAccountLineGroup targetAccountLineGroup = glEntryGroupMap.get(accountLineGroup);
if (targetAccountLineGroup == null) {
glEntryGroupMap.put(accountLineGroup, accountLineGroup);
}
else {
// group GL entries
targetAccountLineGroup.combineEntry(glEntry);
}
}
}
}
/**
* Groups Purap Account Line entries by fields by univ_fiscal_yr, fin_coa_cd, account_nbr, sub_acct_nbr, fin_object_cd,
* fin_sub_obj_cd, univ_fiscal_prd_cd, fdoc_nbr, fdoc_ref_nbr
*
* @param purapAcctEntries Purap account entries
*/
protected void groupPurapAccountEntries(Collection<PurApAccountingLineBase> purapAcctEntries) {
for (PurApAccountingLineBase entry : purapAcctEntries) {
if (entry.getAmount() != null && !entry.getAmount().isZero()) {
// Step-2 Group by univ_fiscal_yr, fin_coa_cd, account_nbr, sub_acct_nbr, fin_object_cd, fin_sub_obj_cd,
// univ_fiscal_prd_cd, fdoc_nbr, fdoc_ref_nbr
PurApAccountLineGroup accountLineGroup = new PurApAccountLineGroup(entry);
PurApAccountLineGroup targetAccountLineGroup = purapAcctGroupMap.get(accountLineGroup);
if (targetAccountLineGroup == null) {
purapAcctGroupMap.put(accountLineGroup, accountLineGroup);
}
else {
// group GL entries
targetAccountLineGroup.combineEntry(entry);
}
}
}
}
/**
* @see org.kuali.kfs.module.cab.batch.service.ReconciliationService#isDuplicateEntry(org.kuali.kfs.gl.businessobject.Entry)
*/
public boolean isDuplicateEntry(Entry glEntry) {
// find matching entry from CB_GL_ENTRY_T
return reconciliationDao.isDuplicateEntry(glEntry);
}
/**
* Gets the businessObjectService attribute.
*
* @return Returns the businessObjectService
*/
public BusinessObjectService getBusinessObjectService() {
return businessObjectService;
}
/**
* Sets the businessObjectService attribute.
*
* @param businessObjectService The businessObjectService to set.
*/
public void setBusinessObjectService(BusinessObjectService businessObjectService) {
this.businessObjectService = businessObjectService;
}
public void setReconciliationDao( ReconciliationDao reconDao) {
this.reconciliationDao = reconDao;
}
/**
* Gets the ignoredEntries attribute.
*
* @return Returns the ignoredEntries
*/
public List<Entry> getIgnoredEntries() {
return ignoredEntries;
}
/**
* Sets the ignoredEntries attribute.
*
* @param ignoredEntries The ignoredEntries to set.
*/
public void setIgnoredEntries(List<Entry> ignoredEntries) {
this.ignoredEntries = ignoredEntries;
}
/**
* Gets the duplicateEntries attribute.
*
* @return Returns the duplicateEntries
*/
public List<Entry> getDuplicateEntries() {
return duplicateEntries;
}
/**
* Sets the duplicateEntries attribute.
*
* @param duplicateEntries The duplicateEntries to set.
*/
public void setDuplicateEntries(List<Entry> duplicateEntries) {
this.duplicateEntries = duplicateEntries;
}
public Collection<GlAccountLineGroup> getMatchedGroups() {
return this.matchedGroups;
}
public Collection<GlAccountLineGroup> getMisMatchedGroups() {
return this.misMatchedGroups;
}
}