/* * 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.math.BigDecimal; import java.sql.Timestamp; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang.time.DateUtils; import org.apache.log4j.Logger; import org.kuali.kfs.gl.businessobject.Entry; import org.kuali.kfs.integration.cam.CapitalAssetManagementModuleService; import org.kuali.kfs.module.cab.CabConstants; import org.kuali.kfs.module.cab.CabPropertyConstants; import org.kuali.kfs.module.cab.batch.ExtractProcessLog; import org.kuali.kfs.module.cab.batch.PreAssetTaggingStep; import org.kuali.kfs.module.cab.batch.dataaccess.ExtractDao; import org.kuali.kfs.module.cab.batch.dataaccess.PurchasingAccountsPayableItemAssetDao; import org.kuali.kfs.module.cab.batch.service.BatchExtractService; import org.kuali.kfs.module.cab.batch.service.ReconciliationService; import org.kuali.kfs.module.cab.businessobject.BatchParameters; import org.kuali.kfs.module.cab.businessobject.GeneralLedgerEntry; import org.kuali.kfs.module.cab.businessobject.GlAccountLineGroup; import org.kuali.kfs.module.cab.businessobject.Pretag; import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableActionHistory; import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableDocument; import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset; import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableLineAssetAccount; import org.kuali.kfs.module.cab.document.service.PurApInfoService; import org.kuali.kfs.module.cab.document.service.PurApLineService; import org.kuali.kfs.module.cam.CamsConstants; import org.kuali.kfs.module.cam.businessobject.AssetGlobal; import org.kuali.kfs.module.purap.PurapConstants; import org.kuali.kfs.module.purap.businessobject.CreditMemoAccountRevision; import org.kuali.kfs.module.purap.businessobject.PaymentRequestAccountRevision; import org.kuali.kfs.module.purap.businessobject.PurApAccountingLineBase; import org.kuali.kfs.module.purap.businessobject.PurApItem; import org.kuali.kfs.module.purap.businessobject.PurchaseOrderAccount; import org.kuali.kfs.module.purap.businessobject.PurchaseOrderItem; import org.kuali.kfs.module.purap.document.AccountsPayableDocumentBase; import org.kuali.kfs.module.purap.document.PaymentRequestDocument; import org.kuali.kfs.module.purap.document.PurchaseOrderDocument; import org.kuali.kfs.module.purap.document.VendorCreditMemoDocument; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.context.SpringContext; import org.kuali.kfs.sys.document.service.FinancialSystemDocumentService; import org.kuali.kfs.sys.service.NonTransactional; import org.kuali.kfs.sys.service.impl.KfsParameterConstants; import org.kuali.rice.core.api.datetime.DateTimeService; import org.kuali.rice.core.api.util.type.KualiDecimal; import org.kuali.rice.coreservice.api.parameter.Parameter; import org.kuali.rice.coreservice.framework.parameter.ParameterService; import org.kuali.rice.kew.api.exception.WorkflowException; import org.kuali.rice.krad.service.BusinessObjectService; import org.kuali.rice.krad.util.ObjectUtils; import org.springframework.transaction.annotation.Transactional; /** * This class provides default implementation of {@link BatchExtractService} */ public class BatchExtractServiceImpl implements BatchExtractService { protected static final Logger LOG = Logger.getLogger(BatchExtractServiceImpl.class); protected BusinessObjectService businessObjectService; protected ExtractDao extractDao; protected DateTimeService dateTimeService; protected ParameterService parameterService; protected PurApLineService purApLineService; protected PurApInfoService purApInfoService; protected PurchasingAccountsPayableItemAssetDao purchasingAccountsPayableItemAssetDao; @Override @Transactional public void performExtract(ExtractProcessLog processLog) { Collection<Entry> elgibleGLEntries = findElgibleGLEntries(processLog); if (elgibleGLEntries != null && !elgibleGLEntries.isEmpty()) { List<Entry> fpLines = new ArrayList<Entry>(); List<Entry> purapLines = new ArrayList<Entry>(); // Separate PO lines separatePOLines(fpLines, purapLines, elgibleGLEntries); // Save the Non-PO lines saveFPLines(fpLines, processLog); // Save the PO lines HashSet<PurchasingAccountsPayableDocument> purApDocuments = savePOLines(purapLines, processLog); // call allocate additional charges( not including trade-in lines) during batch. if comment out this line, CAB users // will see them on the screen and they will have to manually allocate the additional charge lines. allocateAdditionalCharges(purApDocuments); // Set the log values processLog.setTotalGlCount(elgibleGLEntries.size()); processLog.setNonPurApGlCount(fpLines.size()); processLog.setPurApGlCount(purapLines.size()); // Update the last extract time stamp updateLastExtractTime(processLog.getStartTime()); if (LOG.isDebugEnabled()) { LOG.debug("CAB batch finished at " + dateTimeService.getCurrentTimestamp()); } processLog.setFinishTime(dateTimeService.getCurrentTimestamp()); processLog.setSuccess(true); } else { LOG.warn("****** No records processed during CAB Extract *******"); processLog.setSuccess(false); processLog.setErrorMessage("No GL records were found for CAB processing."); } } @Override @NonTransactional public void allocateAdditionalCharges(HashSet<PurchasingAccountsPayableDocument> purApDocuments) { List<PurchasingAccountsPayableActionHistory> actionsTakenHistory = new ArrayList<PurchasingAccountsPayableActionHistory>(); List<PurchasingAccountsPayableDocument> candidateDocs = new ArrayList<PurchasingAccountsPayableDocument>(); List<PurchasingAccountsPayableItemAsset> allocateTargetLines = null; List<PurchasingAccountsPayableItemAsset> initialItems = new ArrayList<PurchasingAccountsPayableItemAsset>(); boolean documentUpdated = false; for (PurchasingAccountsPayableDocument purApDoc : purApDocuments) { documentUpdated = false; // Refresh to get the referenced GLEntry BO. This is required to call purApLineService.processAllocate(). Map<String, String> primaryKeys = new HashMap<String, String>(); primaryKeys.put(CabPropertyConstants.PurchasingAccountsPayableDocument.DOCUMENT_NUMBER, purApDoc.getDocumentNumber()); // check if doc is already in CAB PurchasingAccountsPayableDocument cabPurapDoc = businessObjectService.findByPrimaryKey(PurchasingAccountsPayableDocument.class, primaryKeys); candidateDocs.add(cabPurapDoc); // keep the original list of items for iteration since purApLineService.processAllocate() may remove the line item when // it's additional charges line. initialItems.addAll(cabPurapDoc.getPurchasingAccountsPayableItemAssets()); // set additional charge indicator for each line item from PurAp. we need to set them before hand for use in // purApLineService.getAllocateTargetLines(), in which method to select line items as the target allocating lines. for (PurchasingAccountsPayableItemAsset ititialItem : initialItems) { purApInfoService.setAccountsPayableItemsFromPurAp(ititialItem, cabPurapDoc.getDocumentTypeCode()); } // allocate additional charge lines if it's active line for (PurchasingAccountsPayableItemAsset allocateSourceLine : initialItems) { if (allocateSourceLine.isAdditionalChargeNonTradeInIndicator() && allocateSourceLine.isActive()) { // get the allocate additional charge target lines. allocateTargetLines = purApLineService.getAllocateTargetLines(allocateSourceLine, candidateDocs); if (allocateTargetLines != null && !allocateTargetLines.isEmpty()) { // setup the bidirectional relationship between objects. setupObjectRelationship(candidateDocs); purApLineService.processAllocate(allocateSourceLine, allocateTargetLines, actionsTakenHistory, candidateDocs, true); documentUpdated = true; } } } if (documentUpdated) { businessObjectService.save(cabPurapDoc); } candidateDocs.clear(); initialItems.clear(); } } /** * Setup relationship from account to item and item to doc. In this way, we keep all working objects in the same view as form. * * @param purApDocs */ protected void setupObjectRelationship(List<PurchasingAccountsPayableDocument> purApDocs) { for (PurchasingAccountsPayableDocument purApDoc : purApDocs) { for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) { item.setPurchasingAccountsPayableDocument(purApDoc); for (PurchasingAccountsPayableLineAssetAccount account : item.getPurchasingAccountsPayableLineAssetAccounts()) { account.setPurchasingAccountsPayableItemAsset(item); } } } } /** * Creates a batch parameters object reading values from configured system parameters for CAB Extract * * @return BatchParameters */ protected BatchParameters createCabBatchParameters() { BatchParameters parameters = new BatchParameters(); parameters.setLastRunTime(getCabLastRunTimestamp()); parameters.setIncludedFinancialBalanceTypeCodes(parameterService.getParameterValuesAsString(KfsParameterConstants.CAPITAL_ASSET_BUILDER_BATCH.class, CabConstants.Parameters.BALANCE_TYPES)); parameters.setIncludedFinancialObjectSubTypeCodes(parameterService.getParameterValuesAsString(KfsParameterConstants.CAPITAL_ASSET_BUILDER_BATCH.class, CabConstants.Parameters.OBJECT_SUB_TYPES)); parameters.setExcludedChartCodes(parameterService.getParameterValuesAsString(KfsParameterConstants.CAPITAL_ASSET_BUILDER_BATCH.class, CabConstants.Parameters.CHARTS)); parameters.setExcludedDocTypeCodes(parameterService.getParameterValuesAsString(KfsParameterConstants.CAPITAL_ASSET_BUILDER_BATCH.class, CabConstants.Parameters.DOCUMENT_TYPES)); parameters.setExcludedFiscalPeriods(parameterService.getParameterValuesAsString(KfsParameterConstants.CAPITAL_ASSET_BUILDER_BATCH.class, CabConstants.Parameters.FISCAL_PERIODS)); parameters.setExcludedSubFundCodes(parameterService.getParameterValuesAsString(KfsParameterConstants.CAPITAL_ASSET_BUILDER_BATCH.class, CabConstants.Parameters.SUB_FUND_GROUPS)); return parameters; } /** * Creates a batch parameters object reading values from configured system parameters for Pre Tagging Extract * * @return BatchParameters */ protected BatchParameters createPreTagBatchParameters() { BatchParameters parameters = new BatchParameters(); parameters.setLastRunDate(getPreTagLastRunDate()); parameters.setIncludedFinancialObjectSubTypeCodes(parameterService.getParameterValuesAsString(PreAssetTaggingStep.class, CabConstants.Parameters.OBJECT_SUB_TYPES)); parameters.setExcludedChartCodes(parameterService.getParameterValuesAsString(KfsParameterConstants.CAPITAL_ASSET_BUILDER_BATCH.class, CabConstants.Parameters.CHARTS)); parameters.setExcludedSubFundCodes(parameterService.getParameterValuesAsString(KfsParameterConstants.CAPITAL_ASSET_BUILDER_BATCH.class, CabConstants.Parameters.SUB_FUND_GROUPS)); parameters.setCapitalizationLimitAmount(new BigDecimal(parameterService.getParameterValueAsString(AssetGlobal.class, CamsConstants.Parameters.CAPITALIZATION_LIMIT_AMOUNT))); return parameters; } /** * Retrieves a credit memo document for a specific document number * * @param entry GL Line * @return CreditMemoDocument */ protected VendorCreditMemoDocument findCreditMemoDocument(Entry entry) { VendorCreditMemoDocument creditMemoDocument = null; Map<String, String> keys = new LinkedHashMap<String, String>(); keys.put(CabPropertyConstants.DOCUMENT_NUMBER, entry.getDocumentNumber()); Collection<VendorCreditMemoDocument> matchingCms = businessObjectService.findMatching(VendorCreditMemoDocument.class, keys); if (matchingCms != null && matchingCms.size() == 1) { creditMemoDocument = matchingCms.iterator().next(); } return creditMemoDocument; } /** * @see org.kuali.kfs.module.cab.batch.service.BatchExtractService#findElgibleGLEntries() */ @Override @NonTransactional public Collection<Entry> findElgibleGLEntries(ExtractProcessLog processLog) { BatchParameters parameters = createCabBatchParameters(); processLog.setLastExtractTime(parameters.getLastRunTime()); return extractDao.findMatchingGLEntries(parameters); } /** * @see org.kuali.kfs.module.cab.batch.service.BatchExtractService#findPreTaggablePOAccounts() */ @Override @NonTransactional public Collection<PurchaseOrderAccount> findPreTaggablePOAccounts() { BatchParameters parameters = createPreTagBatchParameters(); return extractDao.findPreTaggablePOAccounts(parameters, getDocumentsNumbersAwaitingPurchaseOrderOpenStatus()); } /** * Retrieves a payment request document for a specific document number * * @param entry GL Line * @return PaymentRequestDocument */ protected PaymentRequestDocument findPaymentRequestDocument(Entry entry) { PaymentRequestDocument paymentRequestDocument = null; Map<String, String> keys = new LinkedHashMap<String, String>(); keys.put(CabPropertyConstants.DOCUMENT_NUMBER, entry.getDocumentNumber()); Collection<PaymentRequestDocument> matchingPreqs = businessObjectService.findMatching(PaymentRequestDocument.class, keys); if (matchingPreqs != null && matchingPreqs.size() == 1) { paymentRequestDocument = matchingPreqs.iterator().next(); } return paymentRequestDocument; } /** * Computes the last run time stamp, if null then it gives yesterday * * @return Last run time stamp */ protected Timestamp getCabLastRunTimestamp() { Timestamp lastRunTime; String lastRunTS = parameterService.getParameterValueAsString(KfsParameterConstants.CAPITAL_ASSET_BUILDER_BATCH.class, CabConstants.Parameters.LAST_EXTRACT_TIME); java.util.Date yesterday = DateUtils.add(dateTimeService.getCurrentDate(), Calendar.DAY_OF_MONTH, -1); try { lastRunTime = lastRunTS == null ? new Timestamp(yesterday.getTime()) : new Timestamp(DateUtils.parseDate(lastRunTS, new String[] { CabConstants.DateFormats.MONTH_DAY_YEAR + " " + CabConstants.DateFormats.MILITARY_TIME }).getTime()); } catch (ParseException e) { throw new RuntimeException(e); } return lastRunTime; } /** * Gets the last pre tag extract run date from system parameter * * @return */ protected java.sql.Date getPreTagLastRunDate() { java.sql.Date lastRunDt; String lastRunTS = parameterService.getParameterValueAsString(PreAssetTaggingStep.class, CabConstants.Parameters.LAST_EXTRACT_DATE); java.util.Date yesterday = DateUtils.add(dateTimeService.getCurrentDate(), Calendar.DAY_OF_MONTH, -1); try { lastRunDt = lastRunTS == null ? new java.sql.Date(yesterday.getTime()) : new java.sql.Date(DateUtils.parseDate(lastRunTS, new String[] { CabConstants.DateFormats.MONTH_DAY_YEAR }).getTime()); } catch (ParseException e) { throw new RuntimeException(e); } return lastRunDt; } /** * @see org.kuali.kfs.module.cab.batch.service.BatchExtractService#saveFPLines(java.util.List) */ @Override @Transactional public void saveFPLines(List<Entry> fpLines, ExtractProcessLog processLog) { for (Entry fpLine : fpLines) { // If entry is not duplicate, non-null and non-zero, then insert into CAB ReconciliationService reconciliationService = SpringContext.getBean(ReconciliationService.class); if (fpLine.getTransactionLedgerEntryAmount() == null || fpLine.getTransactionLedgerEntryAmount().isZero()) { // amount is zero or null processLog.addIgnoredGLEntry(fpLine); } else if (reconciliationService.isDuplicateEntry(fpLine)) { // GL is duplicate processLog.addDuplicateGLEntry(fpLine); } else { GeneralLedgerEntry glEntry = new GeneralLedgerEntry(fpLine); businessObjectService.save(glEntry); } } } /** * @see org.kuali.kfs.module.cab.batch.service.BatchExtractService#savePOLines(java.util.List) */ @Transactional @Override public HashSet<PurchasingAccountsPayableDocument> savePOLines(List<Entry> poLines, ExtractProcessLog processLog) { HashSet<PurchasingAccountsPayableDocument> purApDocuments = new HashSet<PurchasingAccountsPayableDocument>(); ReconciliationService reconciliationService = SpringContext.getBean(ReconciliationService.class); // This is a list of pending GL entries created after last GL process and Cab Batch extract // PurAp Account Line history comes from PURAP module Collection<PurApAccountingLineBase> purapAcctLines = findPurapAccountRevisions(); // Pass the records to reconciliation service method reconciliationService.reconcile(poLines, purapAcctLines); // for each valid GL entry there is a collection of valid PO Doc and Account Lines Collection<GlAccountLineGroup> matchedGroups = reconciliationService.getMatchedGroups(); // Keep track of unique item lines HashMap<String, PurchasingAccountsPayableItemAsset> assetItems = new HashMap<String, PurchasingAccountsPayableItemAsset>(); // Keep track of unique account lines HashMap<String, PurchasingAccountsPayableLineAssetAccount> assetAcctLines = new HashMap<String, PurchasingAccountsPayableLineAssetAccount>(); // Keep track of asset lock HashMap<String, Object> assetLockMap = new HashMap<String, Object>(); // Keep track of purchaseOrderDocument HashMap<Integer, PurchaseOrderDocument> poDocMap = new HashMap<Integer, PurchaseOrderDocument>(); // KFSMI-7214, add document map for processing multiple items from the same AP doc HashMap<String, PurchasingAccountsPayableDocument> papdMap = new HashMap<String, PurchasingAccountsPayableDocument>(); for (GlAccountLineGroup group : matchedGroups) { Entry entry = group.getTargetEntry(); GeneralLedgerEntry generalLedgerEntry = new GeneralLedgerEntry(entry); GeneralLedgerEntry debitEntry = null; GeneralLedgerEntry creditEntry = null; KualiDecimal transactionLedgerEntryAmount = generalLedgerEntry.getTransactionLedgerEntryAmount(); List<PurApAccountingLineBase> matchedPurApAcctLines = group.getMatchedPurApAcctLines(); boolean hasPositiveAndNegative = hasPositiveAndNegative(matchedPurApAcctLines); boolean nonZero = ObjectUtils.isNotNull(transactionLedgerEntryAmount) && transactionLedgerEntryAmount.isNonZero(); // generally for non-zero transaction ledger amount we should create a single GL entry with that amount, if (nonZero && !hasPositiveAndNegative) { businessObjectService.save(generalLedgerEntry); } // but if there is FO revision or negative amount lines such as discount, create and save the set of debit(positive) and credit(negative) entries initialized with zero transaction amounts else { debitEntry = createPositiveGlEntry(entry); businessObjectService.save(debitEntry); creditEntry = createNegativeGlEntry(entry); businessObjectService.save(creditEntry); } // KFSMI-7214, create an active document reference map boolean newApDoc = false; // KFSMI-7214, find from active document reference map first PurchasingAccountsPayableDocument cabPurapDoc = papdMap.get(entry.getDocumentNumber()); if (ObjectUtils.isNull(cabPurapDoc)) { // find from DB cabPurapDoc = findPurchasingAccountsPayableDocument(entry); } // if document is found already, update the active flag if (ObjectUtils.isNull(cabPurapDoc)) { cabPurapDoc = createPurchasingAccountsPayableDocument(entry); newApDoc = true; } if (cabPurapDoc != null) { // KFSMI-7214, add to the cached document map papdMap.put(entry.getDocumentNumber(), cabPurapDoc); // we only deal with PREQ or CM, so isPREQ = !isCM, isCM = !PREQ boolean isPREQ = CabConstants.PREQ.equals(entry.getFinancialDocumentTypeCode()); boolean hasRevisionWithMixedLines = isPREQ && hasRevisionWithMixedLines(matchedPurApAcctLines); for (PurApAccountingLineBase purApAccountingLine : matchedPurApAcctLines) { // KFSMI-7214,tracking down changes on CAB item. boolean newAssetItem = false; PurApItem purapItem = purApAccountingLine.getPurapItem(); String itemAssetKey = cabPurapDoc.getDocumentNumber() + "-" + purapItem.getItemIdentifier(); // KFSMI-7214, search CAB item from active object reference map first PurchasingAccountsPayableItemAsset itemAsset = assetItems.get(itemAssetKey); if (ObjectUtils.isNull(itemAsset)) { itemAsset = findMatchingPurapAssetItem(cabPurapDoc, purapItem); } // if new item, create and add to the list if (ObjectUtils.isNull(itemAsset)) { itemAsset = createPurchasingAccountsPayableItemAsset(cabPurapDoc, purapItem); cabPurapDoc.getPurchasingAccountsPayableItemAssets().add(itemAsset); newAssetItem = true; } assetItems.put(itemAssetKey, itemAsset); String acctLineKey = cabPurapDoc.getDocumentNumber() + "-" + itemAsset.getAccountsPayableLineItemIdentifier() + "-" + itemAsset.getCapitalAssetBuilderLineNumber() + "-" + generalLedgerEntry.getGeneralLedgerAccountIdentifier(); PurchasingAccountsPayableLineAssetAccount assetAccount = assetAcctLines.get(acctLineKey); if (ObjectUtils.isNull(assetAccount) && nonZero && !hasPositiveAndNegative) { // if new unique account line within GL, then create a new account line assetAccount = createPurchasingAccountsPayableLineAssetAccount(generalLedgerEntry, cabPurapDoc, purApAccountingLine, itemAsset); assetAcctLines.put(acctLineKey, assetAccount); itemAsset.getPurchasingAccountsPayableLineAssetAccounts().add(assetAccount); } else if (!nonZero || hasPositiveAndNegative) { // if amount is zero, means canceled doc, then create a copy and retain the account line KualiDecimal purapAmount = purApAccountingLine.getAmount(); /* * KFSMI-9760 / KFSCNTRB-???(FSKD-5097) * 1. Usually, we consolidate matched accounting lines (for the same account) based on positive/negative amount, i.e. * 1.1 For PREQ, positive -> debit, negative -> credit; * That means charges (positive amount) are debit, trade-ins/discounts (negative amount) are credit. * 1.2. For CM, the opposite, positive -> credit, negative -> debit * That means payments (positive amount) are credit, Less Restocking Fees (negative amount) are debit. * 2. However when there is a FO revision on PREQ (CMs don't have revisions), it's more complicated: * 2.1 If the matched accounting lines are either all for non trade-in/discount items, or all for trade-in/discount items, * then we still could base the debit/credit on positive/negative amount; * That means reverse of charges (negative amount) are credit, reverse of trade-ins/discounts (positive amount) are debit. * 2.2 Otherwise, i.e. the matched accounting lines cover both non trade-in/discount items and trade-in/discount items, * In this case we prefer to consolidate based on revision, * that means the original charges and trade-in/discounts are combined together, * while the reversed charges and trade-in/discounts are combined together; * So: original charge + original trade-in/discount -> debit, reversed charge + reversed trade-in/discount -> credit * 3. On top of these, we ensure that the final cab GL entries created is a debit if the consolidated amount is positive, and vice versa. * Note: In general, the consolidated amount for debit entry should already be positive, and vice versa. But there could be special cases, * for ex, in the case of 2.2, if the revision is only on discount, then the credit entry for the reverse would come out as positive, so we need * to swap it into a debit entry. This means, we will have 2 debit entries, one for the original lines, the other for the reversed discount line. */ // note that PurAp Doc accounting lines won't have zero amount, so !isPositive = isNegative boolean isPositive = purapAmount.isPositive(); // trade-in and discount items on PREQ usually have negative amount (unless it's a revision) boolean usuallyNegative = isItemTypeUsuallyOfNegativeAmount(purapItem.getItemTypeCode()); // decide if current accounting line should be consolidated into debit or credit entry based on the above criteria boolean isDebitEntry = hasRevisionWithMixedLines ? (usuallyNegative ? !isPositive : isPositive) : // case 2.2 (isPREQ ? isPositive : !isPositive); // case 1.1/1.2/2.1 GeneralLedgerEntry currentEntry = isDebitEntry ? debitEntry : creditEntry; // during calculation, regard D/C code as a +/- sign in front of the amount KualiDecimal oldAmount = currentEntry.getTransactionLedgerEntryAmount(); oldAmount = isDebitEntry ? oldAmount : oldAmount.negated(); KualiDecimal newAmount = oldAmount.add(purapAmount); newAmount = isDebitEntry ? newAmount : newAmount.negated(); currentEntry.setTransactionLedgerEntryAmount(newAmount); assetAccount = createPurchasingAccountsPayableLineAssetAccount(currentEntry, cabPurapDoc, purApAccountingLine, itemAsset); itemAsset.getPurchasingAccountsPayableLineAssetAccounts().add(assetAccount); } else if (ObjectUtils.isNotNull(assetAccount)) { // if account line key matches within same GL Entry, combine the amount assetAccount.setItemAccountTotalAmount(assetAccount.getItemAccountTotalAmount().add(purApAccountingLine.getAmount())); } // KFSMI-7214: fixed OJB auto-update object issue. if (!newAssetItem) { businessObjectService.save(itemAsset); } businessObjectService.save(cabPurapDoc); // Add to the asset lock table if purap has asset number information addAssetLocks(assetLockMap, cabPurapDoc, purapItem, itemAsset.getAccountsPayableLineItemIdentifier(), poDocMap); } // Update and save the debit/credit entry if needed; // Ensure that the entry always carries a positive TransactionLedgerEntryAmount, // otherwise need to swap the D/C code, (see item #3 in the above KFSMI-9760 / KFSCNTRB-???(FSKD-5097) comment) // since the real amount being positive/negative shall be solely indicated by the D/C code. if (debitEntry != null) { KualiDecimal amount = debitEntry.getTransactionLedgerEntryAmount(); if (amount.isNegative()) { debitEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE); debitEntry.setTransactionLedgerEntryAmount(amount.negated()); } businessObjectService.save(debitEntry); } if (creditEntry != null) { KualiDecimal amount = creditEntry.getTransactionLedgerEntryAmount(); if (amount.isNegative()) { creditEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE); creditEntry.setTransactionLedgerEntryAmount(amount.negated()); } businessObjectService.save(creditEntry); } // Add to the doc collection which will be used for additional charge allocating. This will be the next step during // batch. if (newApDoc) { purApDocuments.add(cabPurapDoc); } } else { LOG.error("Could not create a valid PurchasingAccountsPayableDocument object for document number " + entry.getDocumentNumber()); } } updateProcessLog(processLog, reconciliationService); return purApDocuments; } /** * Returns true if the item type code is trade-in or discount, since items with these types usually have negative amounts. */ private boolean isItemTypeUsuallyOfNegativeAmount(String itemTypeCode) { return PurapConstants.ItemTypeCodes.ITEM_TYPE_TRADE_IN_CODE.equals(itemTypeCode) || PurapConstants.ItemTypeCodes.ITEM_TYPE_ORDER_DISCOUNT_CODE.equals(itemTypeCode) || //TODO remove the following logic about MISC item when bug in KFSMI-10170 is fixed //MISC is included here temporarily for testing, since it's used as TRDI and ORDS, which don't work due to bug PurapConstants.ItemTypeCodes.ITEM_TYPE_MISC_CODE.equals(itemTypeCode); } /** * Determines if the matched PurAp accounting lines have revisions and the account is used in both line items and trade-in/discount items. * If so, the trade-in/discount accounting lines need to be consolidated differently than simply by positive/negative amount. * Note: This method only applies to PREQ, since no revision could happen to CM. * * @param matchedPurApAcctLines List of matched PurAp accounting lines to check for multiple discount items * @return true if multiple discount items, false otherwise */ private boolean hasRevisionWithMixedLines(List<PurApAccountingLineBase> matchedPurApAcctLines) { boolean hasItemsUsuallyNegative = false; boolean hasOthers = false; boolean hasRevision = false; HashSet<Integer> itemIdentifiers = new HashSet<Integer>(); for (PurApAccountingLineBase purApAccountingLine : matchedPurApAcctLines) { PurApItem purapItem = purApAccountingLine.getPurapItem(); if (isItemTypeUsuallyOfNegativeAmount(purapItem.getItemTypeCode())) { hasItemsUsuallyNegative = true; } else { hasOthers = true; } // when we hit the same item twice within the matched lines, which share the same account, then we find a revision if (itemIdentifiers.contains(purApAccountingLine.getItemIdentifier())) { hasRevision = true; } else { itemIdentifiers.add(purApAccountingLine.getItemIdentifier()); } if (hasRevision && hasItemsUsuallyNegative && hasOthers) { return true; } } return false; } /** * Determines if there are both positive amounts AND negative amounts among the matched PurAp accounting lines. * This usually could happen in the following cases: * 1. The account was revised by Financial Officer: positive amount is for charge, negative amount is for refund; * 2. The account is for Trade-in or Discount item for PREQ, or Less Restocking Fee on Credit Memo. * 3. Both 1 and 2 happens at same time (for PREQ only; as for CM no revision could happen). * * @param matchedPurApAcctLines * @return */ private boolean hasPositiveAndNegative(List<PurApAccountingLineBase> matchedPurApAcctLines) { boolean hasPostive = false; boolean hasNegative = false; for (PurApAccountingLineBase line : matchedPurApAcctLines) { hasPostive = hasPostive || line.getAmount().isPositive(); hasNegative = hasNegative || line.getAmount().isNegative(); if (hasPostive && hasNegative) { return true; } } return false; } /** * Add asset lock to prohibit CAMS user modify the asset payment line. * * @param assetLockMap * @param cabPurapDoc * @param purapItem * @param itemAsset * @param poDocMap */ protected void addAssetLocks(HashMap<String, Object> assetLockMap, PurchasingAccountsPayableDocument cabPurapDoc, PurApItem purapItem, Integer accountsPaymentItemId, HashMap<Integer, PurchaseOrderDocument> poDocMap) { PurchaseOrderDocument purApdocument = null; if (poDocMap.containsKey(cabPurapDoc.getPurchaseOrderIdentifier())) { purApdocument = poDocMap.get(cabPurapDoc.getPurchaseOrderIdentifier()); } else { purApdocument = purApInfoService.getCurrentDocumentForPurchaseOrderIdentifier(cabPurapDoc.getPurchaseOrderIdentifier()); poDocMap.put(cabPurapDoc.getPurchaseOrderIdentifier(), purApdocument); } String assetLockKey = cabPurapDoc.getDocumentNumber(); // Only individual system will lock on item line number. other system will using preq/cm doc nbr as the locking // key String lockingInformation = null; if (PurapConstants.CapitalAssetTabStrings.INDIVIDUAL_ASSETS.equalsIgnoreCase(purApdocument.getCapitalAssetSystemTypeCode())) { lockingInformation = accountsPaymentItemId.toString(); assetLockKey = cabPurapDoc.getDocumentNumber() + "-" + lockingInformation; } // set asset locks if the locks does not exist in HashMap and not in asset lock table either. if (!assetLockMap.containsKey(assetLockKey) && !getCapitalAssetManagementModuleService().isAssetLockedByCurrentDocument(cabPurapDoc.getDocumentNumber(), lockingInformation)) { // the below method need several PurAp service calls which may take long time to run. List capitalAssetNumbers = getAssetNumbersForLocking(purApdocument, purapItem); if (capitalAssetNumbers != null && !capitalAssetNumbers.isEmpty()) { boolean lockingResult = this.getCapitalAssetManagementModuleService().storeAssetLocks(capitalAssetNumbers, cabPurapDoc.getDocumentNumber(), cabPurapDoc.getDocumentTypeCode(), lockingInformation); // add into cache assetLockMap.put(assetLockKey, lockingResult); } else { // remember the decision... assetLockMap.put(assetLockKey, false); } } } protected List getAssetNumbersForLocking(PurchaseOrderDocument purApdocument, PurApItem purapItem) { String capitalAssetSystemTypeCode = purApdocument.getCapitalAssetSystemTypeCode(); if (!PurapConstants.CapitalAssetSystemStates.MODIFY.equalsIgnoreCase(purApdocument.getCapitalAssetSystemStateCode())) { return null; } return purApInfoService.retrieveValidAssetNumberForLocking(purApdocument.getPurapDocumentIdentifier(), purApdocument.getCapitalAssetSystemTypeCode(), purapItem); } protected CapitalAssetManagementModuleService getCapitalAssetManagementModuleService() { return SpringContext.getBean(CapitalAssetManagementModuleService.class); } /** * Force created entry with zero transaction amount * * @param entry * @return */ protected GeneralLedgerEntry createPositiveGlEntry(Entry entry) { GeneralLedgerEntry copyEntry = new GeneralLedgerEntry(entry); copyEntry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE); copyEntry.setTransactionLedgerEntryAmount(KualiDecimal.ZERO); return copyEntry; } /** * Force created entry with zero transaction amount * * @param entry * @return */ protected GeneralLedgerEntry createNegativeGlEntry(Entry entry) { GeneralLedgerEntry copyEntry = new GeneralLedgerEntry(entry); copyEntry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE); copyEntry.setTransactionLedgerEntryAmount(KualiDecimal.ZERO); return copyEntry; } /** * Retrieves Payment Request Account History and Credit Memo account history, combines them into a single list * * @see org.kuali.kfs.module.cab.batch.service.BatchExtractService#findPurapAccountHistory() */ @Override @NonTransactional public Collection<PurApAccountingLineBase> findPurapAccountRevisions() { Collection<PurApAccountingLineBase> purapAcctLines = new ArrayList<PurApAccountingLineBase>(); Collection<CreditMemoAccountRevision> cmAccountHistory = extractDao.findCreditMemoAccountRevisions(createCabBatchParameters()); Collection<PaymentRequestAccountRevision> preqAccountHistory = extractDao.findPaymentRequestAccountRevisions(createCabBatchParameters()); if (cmAccountHistory != null) { purapAcctLines.addAll(cmAccountHistory); } if (preqAccountHistory != null) { purapAcctLines.addAll(preqAccountHistory); } return purapAcctLines; } /** * Creates a new instance of PurchasingAccountsPayableLineAssetAccount using values provided from dependent objects * * @param generalLedgerEntry General Ledger Entry record * @param cabPurapDoc CAB PurAp Document * @param purApAccountingLine PurAp accounting line * @param itemAsset CAB PurAp Item Asset * @return New PurchasingAccountsPayableLineAssetAccount */ protected PurchasingAccountsPayableLineAssetAccount createPurchasingAccountsPayableLineAssetAccount(GeneralLedgerEntry generalLedgerEntry, PurchasingAccountsPayableDocument cabPurapDoc, PurApAccountingLineBase purApAccountingLine, PurchasingAccountsPayableItemAsset itemAsset) { PurchasingAccountsPayableLineAssetAccount assetAccount = new PurchasingAccountsPayableLineAssetAccount(); assetAccount.setDocumentNumber(cabPurapDoc.getDocumentNumber()); assetAccount.setAccountsPayableLineItemIdentifier(itemAsset.getAccountsPayableLineItemIdentifier()); assetAccount.setCapitalAssetBuilderLineNumber(itemAsset.getCapitalAssetBuilderLineNumber()); assetAccount.setGeneralLedgerAccountIdentifier(generalLedgerEntry.getGeneralLedgerAccountIdentifier()); if (CabConstants.CM.equals(generalLedgerEntry.getFinancialDocumentTypeCode())) { assetAccount.setItemAccountTotalAmount(purApAccountingLine.getAmount().negated()); } else { assetAccount.setItemAccountTotalAmount(purApAccountingLine.getAmount()); } assetAccount.setActivityStatusCode(CabConstants.ActivityStatusCode.NEW); assetAccount.setVersionNumber(0L); return assetAccount; } /** * Updates the entries into process log * * @param processLog Extract Process Log * @param reconciliationService Reconciliation Service data */ protected void updateProcessLog(ExtractProcessLog processLog, ReconciliationService reconciliationService) { processLog.addIgnoredGLEntries(reconciliationService.getIgnoredEntries()); processLog.addDuplicateGLEntries(reconciliationService.getDuplicateEntries()); Collection<GlAccountLineGroup> misMatchedGroups = reconciliationService.getMisMatchedGroups(); for (GlAccountLineGroup glAccountLineGroup : misMatchedGroups) { processLog.addMismatchedGLEntries(glAccountLineGroup.getSourceEntries()); } } /** * Finds PurchasingAccountsPayableDocument using document number * * @param entry GL Entry * @return PurchasingAccountsPayableDocument */ protected PurchasingAccountsPayableDocument findPurchasingAccountsPayableDocument(Entry entry) { Map<String, String> primaryKeys = new HashMap<String, String>(); primaryKeys.put(CabPropertyConstants.PurchasingAccountsPayableDocument.DOCUMENT_NUMBER, entry.getDocumentNumber()); // check if doc is already in CAB PurchasingAccountsPayableDocument cabPurapDoc = businessObjectService.findByPrimaryKey(PurchasingAccountsPayableDocument.class, primaryKeys); return cabPurapDoc; } /** * Creates a new PurchasingAccountsPayableItemAsset using Purchasing Accounts payable item * * @param cabPurapDoc Cab Purap Document * @param apItem Accounts Payable Item * @return PurchasingAccountsPayableItemAsset */ protected PurchasingAccountsPayableItemAsset createPurchasingAccountsPayableItemAsset(PurchasingAccountsPayableDocument cabPurapDoc, PurApItem apItem) { PurchasingAccountsPayableItemAsset itemAsset = new PurchasingAccountsPayableItemAsset(); itemAsset.setDocumentNumber(cabPurapDoc.getDocumentNumber()); itemAsset.setAccountsPayableLineItemIdentifier(apItem.getItemIdentifier()); // KFSMI-5337 itemAsset.setCapitalAssetBuilderLineNumber(purchasingAccountsPayableItemAssetDao.findMaxCabLineNumber(cabPurapDoc.getDocumentNumber(), apItem.getItemIdentifier()) + 1); // replacing the above line by following code which can populate item description from PO for normal line item if (ObjectUtils.isNotNull(cabPurapDoc) && ObjectUtils.isNotNull(apItem.getItemType()) && apItem.getItemType().isLineItemIndicator()) { PurchaseOrderDocument poDoc = purApInfoService.getCurrentDocumentForPurchaseOrderIdentifier(cabPurapDoc.getPurchaseOrderIdentifier()); // get corresponding poItem if (ObjectUtils.isNotNull(poDoc)) { PurApItem poItem = poDoc.getItemByLineNumber(apItem.getItemLineNumber()); if (ObjectUtils.isNotNull(poItem)) { itemAsset.setAccountsPayableLineItemDescription(poItem.getItemDescription()); } } } //itemAsset.setAccountsPayableLineItemDescription(apItem.getItemDescription()); itemAsset.setAccountsPayableItemQuantity(apItem.getItemQuantity() == null ? new KualiDecimal(1) : apItem.getItemQuantity()); itemAsset.setActivityStatusCode(CabConstants.ActivityStatusCode.NEW); itemAsset.setVersionNumber(0L); return itemAsset; } /** * This method creates PurchasingAccountsPayableDocument from a GL Entry and AP Document * * @param entry GL Entry * @param apDoc AP Document * @return PurchasingAccountsPayableDocument */ protected PurchasingAccountsPayableDocument createPurchasingAccountsPayableDocument(Entry entry) { AccountsPayableDocumentBase apDoc = null; PurchasingAccountsPayableDocument cabPurapDoc = null; // If document is not in CAB, create a new document to save in CAB if (CabConstants.PREQ.equals(entry.getFinancialDocumentTypeCode())) { // find PREQ apDoc = findPaymentRequestDocument(entry); } else if (CabConstants.CM.equals(entry.getFinancialDocumentTypeCode())) { // find CM apDoc = findCreditMemoDocument(entry); } if (apDoc == null) { LOG.error("A valid Purchasing Document (PREQ or CM) could not be found for this document number " + entry.getDocumentNumber()); } else { cabPurapDoc = new PurchasingAccountsPayableDocument(); cabPurapDoc.setDocumentNumber(entry.getDocumentNumber()); cabPurapDoc.setPurapDocumentIdentifier(apDoc.getPurapDocumentIdentifier()); cabPurapDoc.setPurchaseOrderIdentifier(apDoc.getPurchaseOrderIdentifier()); cabPurapDoc.setDocumentTypeCode(entry.getFinancialDocumentTypeCode()); cabPurapDoc.setActivityStatusCode(CabConstants.ActivityStatusCode.NEW); cabPurapDoc.setVersionNumber(0L); } return cabPurapDoc; } /** * Finds out the active CAB Asset Item matching the line from PurAP. * * @param cabPurapDoc CAB PurAp document * @param apItem AP Item * @return PurchasingAccountsPayableItemAsset */ protected PurchasingAccountsPayableItemAsset findMatchingPurapAssetItem(PurchasingAccountsPayableDocument cabPurapDoc, PurApItem apItem) { // KFSMI-7214: fixed OJB proxy object issue. Retrieve object from OJB cache instead of from table. if (ObjectUtils.isNotNull(cabPurapDoc)) { for (PurchasingAccountsPayableItemAsset assetItem : cabPurapDoc.getPurchasingAccountsPayableItemAssets()) { if (assetItem.getAccountsPayableLineItemIdentifier() != null && assetItem.getAccountsPayableLineItemIdentifier().equals(apItem.getItemIdentifier())) { // if still active and never split or submitted to CAMS if (ObjectUtils.isNotNull(assetItem) && CabConstants.ActivityStatusCode.NEW.equalsIgnoreCase(assetItem.getActivityStatusCode())) { // KFSMI-7214: return the proxy object if it's already loaded. return assetItem; } } } } else { LOG.error("expecting the CAB AP document not null"); } return null; } /** * @see org.kuali.kfs.module.cab.batch.service.BatchExtractService#separatePOLines(java.util.List, java.util.List, * java.util.Collection) */ @Override @NonTransactional public void separatePOLines(List<Entry> fpLines, List<Entry> purapLines, Collection<Entry> elgibleGLEntries) { for (Entry entry : elgibleGLEntries) { if (CabConstants.PREQ.equals(entry.getFinancialDocumentTypeCode())) { purapLines.add(entry); } else if (!CabConstants.CM.equals(entry.getFinancialDocumentTypeCode())) { fpLines.add(entry); } else if (CabConstants.CM.equals(entry.getFinancialDocumentTypeCode())) { Map<String, String> fieldValues = new HashMap<String, String>(); fieldValues.put(CabPropertyConstants.GeneralLedgerEntry.DOCUMENT_NUMBER, entry.getDocumentNumber()); // check if vendor credit memo, then include as FP line Collection<VendorCreditMemoDocument> matchingCreditMemos = businessObjectService.findMatching(VendorCreditMemoDocument.class, fieldValues); for (VendorCreditMemoDocument creditMemoDocument : matchingCreditMemos) { if (creditMemoDocument.getPurchaseOrderIdentifier() == null) { fpLines.add(entry); } else { purapLines.add(entry); } } } } } /** * @see org.kuali.kfs.module.cab.batch.service.BatchExtractService#updateLastExtractTime(java.sql.Timestamp) */ @Override @NonTransactional public void updateLastExtractTime(Timestamp time) { Parameter parameter = parameterService.getParameter(CabConstants.Parameters.NAMESPACE, CabConstants.Parameters.DETAIL_TYPE_BATCH, CabConstants.Parameters.LAST_EXTRACT_TIME); if (parameter != null) { SimpleDateFormat format = new SimpleDateFormat(CabConstants.DateFormats.MONTH_DAY_YEAR + " " + CabConstants.DateFormats.MILITARY_TIME); Parameter.Builder updatedParameter = Parameter.Builder.create(parameter); updatedParameter.setValue(format.format(time)); parameterService.updateParameter(updatedParameter.build()); } } /** * @see org.kuali.kfs.module.cab.batch.service.BatchExtractService#savePreTagLines(java.util.Collection) */ @Override @Transactional public void savePreTagLines(Collection<PurchaseOrderAccount> preTaggablePOAccounts) { HashSet<String> savedLines = new HashSet<String>(); for (PurchaseOrderAccount purchaseOrderAccount : preTaggablePOAccounts) { purchaseOrderAccount.refresh(); PurchaseOrderItem purapItem = purchaseOrderAccount.getPurapItem(); PurchaseOrderDocument purchaseOrder = purapItem.getPurchaseOrder(); if (ObjectUtils.isNotNull(purchaseOrder)) { Integer poId = purchaseOrder.getPurapDocumentIdentifier(); Integer itemLineNumber = purapItem.getItemLineNumber(); if (poId != null && itemLineNumber != null) { Map<String, Object> primaryKeys = new HashMap<String, Object>(); primaryKeys.put(CabPropertyConstants.Pretag.PURCHASE_ORDER_NUMBER, poId); primaryKeys.put(CabPropertyConstants.Pretag.ITEM_LINE_NUMBER, itemLineNumber); // check if already in pre-tag table Pretag pretag = businessObjectService.findByPrimaryKey(Pretag.class, primaryKeys); if (ObjectUtils.isNull(pretag) && savedLines.add("" + poId + "-" + itemLineNumber)) { pretag = new Pretag(); pretag.setPurchaseOrderNumber(poId.toString()); pretag.setItemLineNumber(itemLineNumber); KualiDecimal quantity = purapItem.getItemQuantity(); pretag.setQuantityInvoiced(quantity != null ? quantity : new KualiDecimal(1)); pretag.setVendorName(purchaseOrder.getVendorName()); pretag.setAssetTopsDescription(purapItem.getItemDescription()); pretag.setPretagCreateDate(new java.sql.Date(purchaseOrder.getPurchaseOrderInitialOpenTimestamp().getTime())); pretag.setChartOfAccountsCode(purchaseOrder.getChartOfAccountsCode()); pretag.setOrganizationCode(purchaseOrder.getOrganizationCode()); pretag.setActive(true); businessObjectService.save(pretag); } } } } } protected List<String> getDocumentsNumbersAwaitingPurchaseOrderOpenStatus() { List<String> poDocumentNumbers = new ArrayList<String>(); List<PurchaseOrderDocument> poDocuments = new ArrayList<PurchaseOrderDocument>(); try { // This should pick up all types of POs (Amendments, Voids, etc) poDocuments = (List<PurchaseOrderDocument>) SpringContext.getBean(FinancialSystemDocumentService.class).findByApplicationDocumentStatus( PurchaseOrderDocument.class, CabConstants.PO_STATUS_CODE_OPEN); } catch (WorkflowException we) { throw new RuntimeException(we); } for (PurchaseOrderDocument poDocument : poDocuments) { poDocumentNumbers.add(poDocument.getDocumentNumber()); } return poDocumentNumbers; } /** * @see org.kuali.kfs.module.cab.batch.service.BatchExtractService#updateLastExtractDate(java.sql.Date) */ @Override @NonTransactional public void updateLastExtractDate(java.sql.Date dt) { Parameter parameter = parameterService.getParameter(CabConstants.Parameters.NAMESPACE, CabConstants.Parameters.DETAIL_TYPE_PRE_ASSET_TAGGING_STEP, CabConstants.Parameters.LAST_EXTRACT_DATE); if (parameter != null) { SimpleDateFormat format = new SimpleDateFormat(CabConstants.DateFormats.MONTH_DAY_YEAR); Parameter.Builder updatedParameter = Parameter.Builder.create(parameter); updatedParameter.setValue(format.format(dt)); parameterService.updateParameter(updatedParameter.build()); } } /** * Sets the businessObjectService attribute value. * * @param businessObjectService The businessObjectService to set. */ @NonTransactional public void setBusinessObjectService(BusinessObjectService businessObjectService) { this.businessObjectService = businessObjectService; } /** * Sets the extractDao attribute value. * * @param extractDao The extractDao to set. */ @NonTransactional public void setExtractDao(ExtractDao extractDao) { this.extractDao = extractDao; } /** * Sets the dateTimeService attribute value. * * @param dateTimeService The dateTimeService to set. */ @NonTransactional public void setDateTimeService(DateTimeService dateTimeService) { this.dateTimeService = dateTimeService; } /** * Sets the parameterService attribute value. * * @param parameterService The parameterService to set. */ @NonTransactional public void setParameterService(ParameterService parameterService) { this.parameterService = parameterService; } /** * Sets the purchasingAccountsPayableItemAssetDao attribute value. * * @param purchasingAccountsPayableItemAssetDao The purchasingAccountsPayableItemAssetDao to set. */ @NonTransactional public void setPurchasingAccountsPayableItemAssetDao(PurchasingAccountsPayableItemAssetDao purchasingAccountsPayableItemAssetDao) { this.purchasingAccountsPayableItemAssetDao = purchasingAccountsPayableItemAssetDao; } /** * Sets the purApLineService attribute value. * * @param purApLineService The purApLineService to set. */ @NonTransactional public void setPurApLineService(PurApLineService purApLineService) { this.purApLineService = purApLineService; } /** * Sets the purApInfoService attribute value. * * @param purApInfoService The purApInfoService to set. */ @NonTransactional public void setPurApInfoService(PurApInfoService purApInfoService) { this.purApInfoService = purApInfoService; } }