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.document.service.impl;
017    
018    import java.util.ArrayList;
019    import java.util.Collections;
020    import java.util.HashMap;
021    import java.util.HashSet;
022    import java.util.Iterator;
023    import java.util.List;
024    import java.util.Map;
025    import java.util.Set;
026    
027    import org.apache.commons.lang.StringUtils;
028    import org.apache.log4j.Logger;
029    import org.kuali.kfs.coa.businessobject.ObjectCode;
030    import org.kuali.kfs.integration.cam.CapitalAssetManagementModuleService;
031    import org.kuali.kfs.module.cab.CabConstants;
032    import org.kuali.kfs.module.cab.CabPropertyConstants;
033    import org.kuali.kfs.module.cab.businessobject.GeneralLedgerEntry;
034    import org.kuali.kfs.module.cab.businessobject.Pretag;
035    import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableActionHistory;
036    import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableDocument;
037    import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset;
038    import org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableLineAssetAccount;
039    import org.kuali.kfs.module.cab.dataaccess.PurApLineDao;
040    import org.kuali.kfs.module.cab.document.service.PurApInfoService;
041    import org.kuali.kfs.module.cab.document.service.PurApLineService;
042    import org.kuali.kfs.module.cab.document.web.PurApLineSession;
043    import org.kuali.kfs.module.cam.CamsConstants;
044    import org.kuali.kfs.module.cam.document.service.AssetService;
045    import org.kuali.kfs.sys.context.SpringContext;
046    import org.kuali.rice.kns.service.BusinessObjectService;
047    import org.kuali.rice.kns.util.KualiDecimal;
048    import org.kuali.rice.kns.util.ObjectUtils;
049    import org.kuali.rice.kns.util.TypedArrayList;
050    import org.springframework.transaction.annotation.Transactional;
051    
052    
053    /**
054     * This class provides default implementations of {@link PurApLineService}
055     */
056    @Transactional
057    public class PurApLineServiceImpl implements PurApLineService {
058        private static final Logger LOG = Logger.getLogger(PurApLineServiceImpl.class);
059        private BusinessObjectService businessObjectService;
060        private PurApLineDao purApLineDao;
061        private PurApInfoService purApInfoService;
062        private CapitalAssetManagementModuleService capitalAssetManagementModuleService;
063    
064        /**
065         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#mergeLinesHasDifferentObjectSubTypes(java.util.List)
066         */
067        public boolean mergeLinesHasDifferentObjectSubTypes(List<PurchasingAccountsPayableItemAsset> mergeLines) {
068            boolean invalid = false;
069            List<String> objectSubTypeList = new ArrayList<String>();
070    
071            // collect all objectSubTypes from item lines
072            for (PurchasingAccountsPayableItemAsset itemAsset : mergeLines) {
073                for (PurchasingAccountsPayableLineAssetAccount account : itemAsset.getPurchasingAccountsPayableLineAssetAccounts()) {
074                    account.getGeneralLedgerEntry().refreshReferenceObject(CabPropertyConstants.GeneralLedgerEntry.FINANCIAL_OBJECT);
075                    ObjectCode objCode = account.getGeneralLedgerEntry().getFinancialObject();
076                    if (ObjectUtils.isNotNull(objCode) && StringUtils.isNotEmpty(objCode.getFinancialObjectSubTypeCode())) {
077                        objectSubTypeList.add(objCode.getFinancialObjectSubTypeCode());
078                    }
079                }
080            }
081    
082            // check if different objectSubTypes exist
083            if (!getAssetService().isObjectSubTypeCompatible(objectSubTypeList)) {
084                invalid = true;
085            }
086            return invalid;
087        }
088    
089        /**
090         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#allocateLinesHasDifferentObjectSubTypes(java.util.List,
091         *      org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset)
092         */
093        public boolean allocateLinesHasDifferentObjectSubTypes(List<PurchasingAccountsPayableItemAsset> targetLines, PurchasingAccountsPayableItemAsset sourceLine) {
094            boolean invalid = false;
095            List<String> objectSubTypeList = new ArrayList<String>();
096    
097            // collect objectSubTypes from target item lines
098            for (PurchasingAccountsPayableItemAsset itemAsset : targetLines) {
099                for (PurchasingAccountsPayableLineAssetAccount account : itemAsset.getPurchasingAccountsPayableLineAssetAccounts()) {
100                    account.getGeneralLedgerEntry().refreshReferenceObject(CabPropertyConstants.GeneralLedgerEntry.FINANCIAL_OBJECT);
101                    ObjectCode objCode = account.getGeneralLedgerEntry().getFinancialObject();
102                    if (ObjectUtils.isNotNull(objCode) && StringUtils.isNotEmpty(objCode.getFinancialObjectSubTypeCode())) {
103                        objectSubTypeList.add(objCode.getFinancialObjectSubTypeCode());
104                    }
105                }
106            }
107    
108            // collect objectSubTypes from source item line
109            if (ObjectUtils.isNotNull(sourceLine)) {
110                for (PurchasingAccountsPayableLineAssetAccount account : sourceLine.getPurchasingAccountsPayableLineAssetAccounts()) {
111                    account.getGeneralLedgerEntry().refreshReferenceObject(CabPropertyConstants.GeneralLedgerEntry.FINANCIAL_OBJECT);
112                    ObjectCode objCode = account.getGeneralLedgerEntry().getFinancialObject();
113                    if (ObjectUtils.isNotNull(objCode) && StringUtils.isNotEmpty(objCode.getFinancialObjectSubTypeCode())) {
114                        objectSubTypeList.add(objCode.getFinancialObjectSubTypeCode());
115                    }
116                }
117            }
118    
119            // check if different objectSubTypes exist
120            if (!getAssetService().isObjectSubTypeCompatible(objectSubTypeList)) {
121                invalid = true;
122            }
123            return invalid;
124        }
125    
126        /**
127         * @see org.kuali.kfs.module.cab.document.service.PurApLineDocumentService#inActivateDocument(org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableDocument)
128         */
129        public void conditionallyUpdateDocumentStatusAsProcessed(PurchasingAccountsPayableDocument selectedDoc) {
130            for (PurchasingAccountsPayableItemAsset item : selectedDoc.getPurchasingAccountsPayableItemAssets()) {
131                if (item.isActive()) {
132                    return;
133                }
134            }
135            // set as processed when allocate/merge and all its items are in CAMs
136            selectedDoc.setActivityStatusCode(CabConstants.ActivityStatusCode.PROCESSED_IN_CAMS);
137        }
138    
139        /**
140         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#resetSelectedValue(java.util.List)
141         */
142        public void resetSelectedValue(List<PurchasingAccountsPayableDocument> purApDocs) {
143            for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
144                for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
145                    item.setSelectedValue(false);
146                }
147            }
148        }
149    
150        /**
151         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#processAllocate(org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset,
152         *      java.util.List, org.kuali.kfs.module.cab.document.web.PurApLineSession, java.util.List)
153         */
154        public boolean processAllocate(PurchasingAccountsPayableItemAsset allocateSourceLine, 
155                List<PurchasingAccountsPayableItemAsset> allocateTargetLines, 
156                List<PurchasingAccountsPayableActionHistory> actionsTakeHistory, 
157                List<PurchasingAccountsPayableDocument> purApDocs, 
158                boolean initiateFromBatch) {
159            
160            boolean allocatedIndicator = true;
161            // indicator of additional charge allocation
162            boolean allocateAddlChrgIndicator = allocateSourceLine.isAdditionalChargeNonTradeInIndicator() | allocateSourceLine.isTradeInAllowance();
163            // Maintain this account List for update. So accounts already allocated won't take effect for account not allocated yet.
164            List<PurchasingAccountsPayableLineAssetAccount> newAccountList = new ArrayList<PurchasingAccountsPayableLineAssetAccount>();
165    
166            // For each account in the source item, allocate it to the target items.
167            for (PurchasingAccountsPayableLineAssetAccount sourceAccount : allocateSourceLine.getPurchasingAccountsPayableLineAssetAccounts()) {
168                sourceAccount.refresh();           
169                // Get allocate to target account list
170                List<PurchasingAccountsPayableLineAssetAccount> targetAccounts = getAllocateTargetAccounts(sourceAccount, allocateTargetLines, allocateAddlChrgIndicator);
171                if (!targetAccounts.isEmpty()) {
172                    // Percentage amount to each target account
173                    allocateByItemAccountAmount(sourceAccount, targetAccounts, newAccountList, actionsTakeHistory);
174                }
175                else {
176                    allocatedIndicator = false;
177                    break;
178                }
179            }
180    
181            if (allocatedIndicator) {
182                postAllocateProcess(allocateSourceLine, allocateTargetLines, purApDocs, newAccountList, initiateFromBatch);
183            }
184    
185            return allocatedIndicator;
186        }
187    
188    
189        /**
190         * Process after allocate.
191         * 
192         * @param selectedLineItem
193         * @param allocateTargetLines
194         * @param purApDocs
195         * @param newAccountList
196         */
197        protected void postAllocateProcess(PurchasingAccountsPayableItemAsset selectedLineItem, List<PurchasingAccountsPayableItemAsset> allocateTargetLines, List<PurchasingAccountsPayableDocument> purApDocs, List<PurchasingAccountsPayableLineAssetAccount> newAccountList, boolean initiateFromBatch) {
198            // add new account into each item list
199            addNewAccountToItemList(newAccountList);
200    
201            // update total cost and unit cost.
202            updateLineItemsCost(allocateTargetLines);
203    
204            if (ObjectUtils.isNotNull(selectedLineItem.getPurchasingAccountsPayableDocument())) {
205                // remove allocate source line item.
206                selectedLineItem.getPurchasingAccountsPayableDocument().getPurchasingAccountsPayableItemAssets().remove(selectedLineItem);
207                // when an line is removed from the document, we should check if it is the only active line in the document and
208                // in-activate document if yes.
209                conditionallyUpdateDocumentStatusAsProcessed(selectedLineItem.getPurchasingAccountsPayableDocument());
210            }
211    
212            // Adjust create asset and apply payment indicator only when allocate additional charges.
213            if (!initiateFromBatch && (selectedLineItem.isAdditionalChargeNonTradeInIndicator() || selectedLineItem.isTradeInAllowance())) {
214                setAssetIndicator(purApDocs);
215            }
216    
217            // update status code as user modified for allocate target lines
218            if (!initiateFromBatch) {
219                for (PurchasingAccountsPayableItemAsset allocateTargetItem : allocateTargetLines) {
220                    updateItemStatusAsUserModified(allocateTargetItem);
221                }
222            }
223        }
224    
225        /**
226         * Build removable asset lock map from the processedItems list. We need to remove all asset locks hold be items which has been
227         * merged or allocated to other lines.
228         * 
229         * @param processedItems
230         * @return
231         */
232        protected Map<String, Set> getRemovableAssetLocks(List<PurchasingAccountsPayableItemAsset> processedItems) {
233            Map<String, Set> removableAssetLocks = new HashMap<String, Set>();
234    
235            for (PurchasingAccountsPayableItemAsset processedItem : processedItems) {
236                // For the time being, only for individual system, each item has its own asset numbers.
237                if (processedItem.getLockingInformation() != null && !CamsConstants.defaultLockingInformation.equals(processedItem.getLockingInformation())) {
238                    addAssetLock(removableAssetLocks, processedItem);
239                }
240                else if (ObjectUtils.isNotNull(processedItem.getPurchasingAccountsPayableDocument())) {
241                    // check other items if they are fully processed and can release the lock
242                    List<PurchasingAccountsPayableItemAsset> remainingItems = processedItem.getPurchasingAccountsPayableDocument().getPurchasingAccountsPayableItemAssets();
243                    boolean fullyProcessed = true;
244                    for (PurchasingAccountsPayableItemAsset itemAsset : remainingItems) {
245                        if (!CabConstants.ActivityStatusCode.PROCESSED_IN_CAMS.equalsIgnoreCase(itemAsset.getActivityStatusCode())) {
246                            fullyProcessed = false;
247                            break;
248                        }
249                    }
250                    if (fullyProcessed) {
251                        // All the items are either merged or allocated to other document item. We should remove the asset lock
252                        // retained by this document.
253                        addAssetLock(removableAssetLocks, processedItem);
254                    }
255                }
256            }
257    
258            processedItems.clear();
259            return removableAssetLocks;
260        }
261    
262        protected void addAssetLock(Map<String, Set> removableAssetLocks, PurchasingAccountsPayableItemAsset processedItem) {
263            if (processedItem.getLockingInformation() == null) {
264                processedItem.setLockingInformation(CamsConstants.defaultLockingInformation);
265            }
266            if (removableAssetLocks.containsKey(processedItem.getDocumentNumber())) {
267                Set lockingInfoList = removableAssetLocks.get(processedItem.getDocumentNumber());
268                lockingInfoList.add(processedItem.getLockingInformation());
269            }
270            else {
271                Set lockingInfoList = new HashSet<String>();
272                lockingInfoList.add(processedItem.getLockingInformation());
273                removableAssetLocks.put(processedItem.getDocumentNumber(), lockingInfoList);
274            }
275        }
276    
277        /**
278         * Reset item total cost and unit cost for each item.
279         * 
280         * @param allocateTargetLines
281         */
282        protected void updateLineItemsCost(List<PurchasingAccountsPayableItemAsset> lineItems) {
283            // update target item unit cost and total cost
284            for (PurchasingAccountsPayableItemAsset item : lineItems) {
285                setLineItemCost(item);
286            }
287        }
288    
289        /**
290         * update account list for each line item
291         * 
292         * @param updatedAccountList
293         */
294        protected void addNewAccountToItemList(List<PurchasingAccountsPayableLineAssetAccount> newAccountList) {
295            PurchasingAccountsPayableItemAsset lineItem = null;
296            for (PurchasingAccountsPayableLineAssetAccount newAccount : newAccountList) {
297                lineItem = newAccount.getPurchasingAccountsPayableItemAsset();
298                if (ObjectUtils.isNotNull(lineItem) && ObjectUtils.isNotNull(lineItem.getPurchasingAccountsPayableLineAssetAccounts())) {
299                    lineItem.getPurchasingAccountsPayableLineAssetAccounts().add(newAccount);
300                }
301            }
302        }
303    
304        /**
305         * Set line item total cost and unit cost.
306         * 
307         * @param item
308         */
309        protected void setLineItemCost(PurchasingAccountsPayableItemAsset item) {
310            KualiDecimal totalCost = calculateItemAssetTotalCost(item);
311            item.setTotalCost(totalCost);
312            setItemAssetUnitCost(item, totalCost);
313        }
314    
315        /**
316         * Allocate one account line to target account lines percentage based on the account line amount.
317         * 
318         * @param sourceAccount Account line to be allocated.
319         * @param targetAccounts Account lines which accept amount.
320         */
321        protected void allocateByItemAccountAmount(PurchasingAccountsPayableLineAssetAccount sourceAccount, List<PurchasingAccountsPayableLineAssetAccount> targetAccounts, List<PurchasingAccountsPayableLineAssetAccount> newAccountList, List<PurchasingAccountsPayableActionHistory> actionsTakeHistory) {
322            KualiDecimal targetAccountsTotalAmount = KualiDecimal.ZERO;
323            KualiDecimal sourceAccountTotalAmount = sourceAccount.getItemAccountTotalAmount();
324            KualiDecimal amountAllocated = KualiDecimal.ZERO;
325            KualiDecimal additionalAmount = null;
326    
327            // Calculate the targetAccountTotalAmount. Ignore the sign of the account amount when proportionally allocate based on
328            // account amount
329            for (PurchasingAccountsPayableLineAssetAccount targetAccount : targetAccounts) {
330                targetAccountsTotalAmount = targetAccountsTotalAmount.add(targetAccount.getItemAccountTotalAmount().abs());
331            }
332    
333            for (Iterator<PurchasingAccountsPayableLineAssetAccount> iterator = targetAccounts.iterator(); iterator.hasNext();) {
334                PurchasingAccountsPayableLineAssetAccount targetAccount = (PurchasingAccountsPayableLineAssetAccount) iterator.next();
335                if (iterator.hasNext()) {
336                    // Not working on the last node. Calculate additional charge amount by percentage. Ignore the sign of the account
337                    // amount when proportionally allocate based on account amount
338                    additionalAmount = targetAccount.getItemAccountTotalAmount().abs().multiply(sourceAccountTotalAmount).divide(targetAccountsTotalAmount);
339                    amountAllocated = amountAllocated.add(additionalAmount);
340                }
341                else {
342                    // Working on the last node, set the additional charge amount to the rest of sourceAccountTotalAmount.
343                    additionalAmount = sourceAccountTotalAmount.subtract(amountAllocated);
344                }
345    
346                // Code below mainly handle grouping account lines if they're from the same GL.
347                PurchasingAccountsPayableLineAssetAccount newAccount = getMatchingFromAccountList(targetAccounts, sourceAccount.getGeneralLedgerAccountIdentifier(), targetAccount);
348                if (newAccount != null) {               
349                    // If exists the same account line and GL entry, update its itemAccountTotalAmount. This account line could be other
350                    // than targetAccount, but must belong to the same line item.
351                    updateAccountAmount(additionalAmount, newAccount);
352                }
353                else {
354                    // If exist account just created, grouping them and update the account amount.
355                    newAccount = getMatchingFromAccountList(newAccountList, sourceAccount.getGeneralLedgerAccountIdentifier(), targetAccount);
356                    if (newAccount != null) {
357                        updateAccountAmount(additionalAmount, newAccount);
358                    }
359                    else {
360                        // If the target account is a different GL entry, create a new account and attach to this line item.
361                        newAccount = new PurchasingAccountsPayableLineAssetAccount(targetAccount.getPurchasingAccountsPayableItemAsset(), sourceAccount.getGeneralLedgerAccountIdentifier());
362                        newAccount.setItemAccountTotalAmount(additionalAmount);
363                        newAccount.setGeneralLedgerEntry(sourceAccount.getGeneralLedgerEntry());
364                        newAccountList.add(newAccount);
365                    }
366                }
367    
368                // add to action history
369                addAllocateHistory(sourceAccount, actionsTakeHistory, additionalAmount, newAccount);
370            }
371        }
372    
373        /**
374         * Save allocate action into session object.
375         * 
376         * @param sourceAccount
377         * @param actionsTakeHistory
378         * @param additionalAmount
379         * @param newAccount
380         */
381        protected void addAllocateHistory(PurchasingAccountsPayableLineAssetAccount sourceAccount, List<PurchasingAccountsPayableActionHistory> actionsTakeHistory, KualiDecimal additionalAmount, PurchasingAccountsPayableLineAssetAccount newAccount) {
382            PurchasingAccountsPayableActionHistory newAction = new PurchasingAccountsPayableActionHistory(sourceAccount.getPurchasingAccountsPayableItemAsset(), newAccount.getPurchasingAccountsPayableItemAsset(), CabConstants.Actions.ALLOCATE);
383            newAction.setGeneralLedgerAccountIdentifier(sourceAccount.getGeneralLedgerAccountIdentifier());
384            newAction.setItemAccountTotalAmount(additionalAmount);
385            newAction.setAccountsPayableItemQuantity(sourceAccount.getPurchasingAccountsPayableItemAsset().getAccountsPayableItemQuantity());
386            actionsTakeHistory.add(newAction);
387        }
388    
389    
390        /**
391         * Search matching account in targetAccounts by glIdentifier.
392         * 
393         * @param targetAccounts
394         * @param glIdentifier
395         * @return
396         */
397        protected PurchasingAccountsPayableLineAssetAccount getFromTargetAccountList(List<PurchasingAccountsPayableLineAssetAccount> targetAccounts, Long glIdentifier) {
398            for (PurchasingAccountsPayableLineAssetAccount account : targetAccounts) {
399                if (account.getGeneralLedgerAccountIdentifier().equals(glIdentifier)) {
400                    return account;
401                }
402            }
403            return null;
404        }
405    
406        /**
407         * Update targetAccount by additionalAmount.
408         * 
409         * @param additionalAmount
410         * @param targetAccount
411         */
412        protected void updateAccountAmount(KualiDecimal additionalAmount, PurchasingAccountsPayableLineAssetAccount targetAccount) {
413            KualiDecimal baseAmount = targetAccount.getItemAccountTotalAmount();
414            targetAccount.setItemAccountTotalAmount(baseAmount != null ? baseAmount.add(additionalAmount) : additionalAmount);
415        }
416    
417    
418        /**
419         * Searching in accountList by glIdentifier for matching account which associated with the same item as targetAccount.
420         * 
421         * @param accountList
422         * @param glIdentifier
423         * @param targetAccount
424         * @return
425         */
426        protected PurchasingAccountsPayableLineAssetAccount getMatchingFromAccountList(List<PurchasingAccountsPayableLineAssetAccount> accountList, Long glIdentifier, PurchasingAccountsPayableLineAssetAccount targetAccount) {
427            for (PurchasingAccountsPayableLineAssetAccount account : accountList) {
428                if (StringUtils.equalsIgnoreCase(targetAccount.getDocumentNumber(), account.getDocumentNumber()) && targetAccount.getAccountsPayableLineItemIdentifier().equals(account.getAccountsPayableLineItemIdentifier()) && targetAccount.getCapitalAssetBuilderLineNumber().equals(account.getCapitalAssetBuilderLineNumber()) && glIdentifier.equals(account.getGeneralLedgerAccountIdentifier())) {
429                    return account;
430                }
431            }
432            return null;
433        }
434    
435        /**
436         * Get the target account lines which will be used for allocate.
437         * 
438         * @param sourceAccount
439         * @param lineItems
440         * @param addtionalCharge
441         * @return
442         */
443        protected List<PurchasingAccountsPayableLineAssetAccount> getAllocateTargetAccounts(PurchasingAccountsPayableLineAssetAccount sourceAccount, List<PurchasingAccountsPayableItemAsset> allocateTargetLines, boolean addtionalCharge) {
444            GeneralLedgerEntry candidateEntry = null;
445            GeneralLedgerEntry sourceEntry = sourceAccount.getGeneralLedgerEntry();
446            List<PurchasingAccountsPayableLineAssetAccount> matchingAccounts = new ArrayList<PurchasingAccountsPayableLineAssetAccount>();
447            List<PurchasingAccountsPayableLineAssetAccount> allAccounts = new ArrayList<PurchasingAccountsPayableLineAssetAccount>();
448    
449            // For additional charge allocation, target account selection is based on account lines with the same account number and
450            // object code. If no matching, select all account lines. For line item to line items, select all account lines as target.
451            for (PurchasingAccountsPayableItemAsset item : allocateTargetLines) {
452                for (PurchasingAccountsPayableLineAssetAccount account : item.getPurchasingAccountsPayableLineAssetAccounts()) {
453                    //KFSMI-5122: We need to refresh account general ledger entry so that the gl entries become visible as candidateEntry
454                    account.refreshReferenceObject("generalLedgerEntry");
455                    candidateEntry = account.getGeneralLedgerEntry();
456                    
457                    if (ObjectUtils.isNotNull(candidateEntry)) {
458                        // For additional charge, select matching account when account number and object code both match.
459                        if (addtionalCharge && StringUtils.equalsIgnoreCase(sourceEntry.getChartOfAccountsCode(), candidateEntry.getChartOfAccountsCode()) && StringUtils.equalsIgnoreCase(sourceEntry.getAccountNumber(), candidateEntry.getAccountNumber()) && StringUtils.equalsIgnoreCase(sourceEntry.getFinancialObjectCode(), candidateEntry.getFinancialObjectCode())) {
460                            matchingAccounts.add(account);
461                        }
462                    }
463                    
464                    allAccounts.add(account);
465                }
466            }
467    
468            return matchingAccounts.isEmpty() ? allAccounts : matchingAccounts;
469        }
470    
471        /**
472         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#getAllocateTargetLines(org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset,
473         *      java.util.List)
474         */
475        public List<PurchasingAccountsPayableItemAsset> getAllocateTargetLines(PurchasingAccountsPayableItemAsset selectedLineItem, List<PurchasingAccountsPayableDocument> purApDocs) {
476            List<PurchasingAccountsPayableItemAsset> targetLineItems = new TypedArrayList(PurchasingAccountsPayableItemAsset.class);
477    
478            for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
479                for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
480                    // If selected Line is additional charge line, get target lines from the same document.
481                    // Else if selected Line is trade in allowance, target lines are item lines with TI indicator set.
482                    // Otherwise, select items with check box set.
483                    if (item.isActive() && item != selectedLineItem && ((selectedLineItem.isAdditionalChargeNonTradeInIndicator() && !item.isAdditionalChargeNonTradeInIndicator() && !item.isTradeInAllowance() && StringUtils.equalsIgnoreCase(selectedLineItem.getDocumentNumber(), item.getDocumentNumber())) || (selectedLineItem.isTradeInAllowance() && item.isItemAssignedToTradeInIndicator()) || (item.isSelectedValue()))) {
484                        targetLineItems.add(item);
485                    }
486                }
487            }
488            return targetLineItems;
489        }
490    
491        /**
492         * Get selected merge lines. If this is merge all action, we need to manually append all additional charge lines since no select
493         * box associated with them.
494         * 
495         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#getSelectedMergeLines(boolean, java.util.List)
496         */
497        public List<PurchasingAccountsPayableItemAsset> getSelectedMergeLines(boolean isMergeAll, List<PurchasingAccountsPayableDocument> purApDocs) {
498            List<PurchasingAccountsPayableItemAsset> mergeLines = new TypedArrayList(PurchasingAccountsPayableItemAsset.class);
499            boolean excludeTradeInAllowance = false;
500    
501            // Handle one exception for merge all: when we have TI allowance but no TI indicator line, we should exclude
502            if (isMergeAll && !isTradeInIndicatorExistInAllLines(purApDocs) && isTradeInAllowanceExist(purApDocs)) {
503                excludeTradeInAllowance = true;
504            }
505    
506            for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
507                for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
508                    // If not merge all action, select items are the merge lines. If it is merge all action, all lines should be
509                    // candidate merge lines except trade-in allowance line when there is no trade-in indicator line.
510                    if ((!isMergeAll && item.isSelectedValue()) || (isMergeAll && (!excludeTradeInAllowance || !item.isTradeInAllowance()))) {
511                        mergeLines.add(item);
512                        // setup non-persistent relationship from item to document.
513                        item.setPurchasingAccountsPayableDocument(purApDoc);
514                    }
515                }
516            }
517            return mergeLines;
518        }
519    
520        /**
521         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isTradeInAllowanceExist(java.util.List)
522         */
523        public boolean isTradeInAllowanceExist(List<PurchasingAccountsPayableDocument> purApDocs) {
524            boolean tradeInAllowance = false;
525            for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
526                for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
527                    if (item.isTradeInAllowance()) {
528                        tradeInAllowance = true;
529                        break;
530                    }
531                }
532            }
533            return tradeInAllowance;
534        }
535    
536        /**
537         * Check if TI indicator exists in all form lines
538         * 
539         * @param purApDocs
540         * @return
541         */
542        protected boolean isTradeInIndicatorExistInAllLines(List<PurchasingAccountsPayableDocument> purApDocs) {
543            boolean tradeInIndicator = false;
544            for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
545                for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
546                    if (item.isItemAssignedToTradeInIndicator()) {
547                        tradeInIndicator = true;
548                        break;
549                    }
550                }
551            }
552            return tradeInIndicator;
553        }
554    
555        /**
556         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isTradeInIndicatorExist(java.util.List)
557         */
558        public boolean isTradeInIndExistInSelectedLines(List<PurchasingAccountsPayableItemAsset> itemAssets) {
559            boolean tradeInIndicator = false;
560            for (PurchasingAccountsPayableItemAsset item : itemAssets) {
561                if (item.isItemAssignedToTradeInIndicator()) {
562                    tradeInIndicator = true;
563                    break;
564                }
565            }
566            return tradeInIndicator;
567        }
568    
569        /**
570         * If item assets are from the same document, we can ignore additional charges pending.
571         * 
572         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isAdditionalChargePending(java.util.List)
573         */
574        public boolean isAdditionalChargePending(List<PurchasingAccountsPayableItemAsset> itemAssets) {
575            boolean additionalChargePending = false;
576            Boolean diffDocment = false;
577            PurchasingAccountsPayableItemAsset firstAsset = itemAssets.get(0);
578            PurchasingAccountsPayableItemAsset lastAsset = itemAssets.get(itemAssets.size() - 1);
579    
580            // Check if itemAssets are in different PurAp Document. itemAssets is a sorted list which has item assets from the same
581            // document grouping together.
582            if (ObjectUtils.isNotNull(firstAsset) && ObjectUtils.isNotNull(lastAsset) && !firstAsset.getDocumentNumber().equalsIgnoreCase(lastAsset.getDocumentNumber())) {
583                diffDocment = true;
584            }
585    
586            // check if item assets from different document have additional charges not allocated yet. Bring all lines in
587            // the same document as checking candidate.
588            if (diffDocment) {
589                for (PurchasingAccountsPayableItemAsset item : itemAssets) {
590                    if (ObjectUtils.isNotNull(item.getPurchasingAccountsPayableDocument())) {
591                        for (PurchasingAccountsPayableItemAsset itemLine : item.getPurchasingAccountsPayableDocument().getPurchasingAccountsPayableItemAssets()) {
592                            if (itemLine.isAdditionalChargeNonTradeInIndicator()) {
593                                additionalChargePending = true;
594                                return additionalChargePending;
595                            }
596                        }
597                    }
598                }
599            }
600            return additionalChargePending;
601        }
602    
603        /**
604         * Check if the merge action is merge all.
605         * 
606         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isMergeAllAction(java.util.List)
607         */
608        public boolean isMergeAllAction(List<PurchasingAccountsPayableDocument> purApDocs) {
609            for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
610                for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
611                    // When there is one item line not selected, mergeAll is false
612                    if (!item.isAdditionalChargeNonTradeInIndicator() && !item.isTradeInAllowance() && !item.isSelectedValue()) {
613                        return false;
614                    }
615                }
616            }
617            return true;
618        }
619    
620        /**
621         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isAdditionalChargeExistInAllLines(java.util.List)
622         */
623        public boolean isAdditionalChargeExistInAllLines(List<PurchasingAccountsPayableDocument> purApDocs) {
624            for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
625                for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
626                    if (item.isAdditionalChargeNonTradeInIndicator()) {
627                        return true;
628                    }
629                }
630            }
631            return false;
632        }
633    
634        /**
635         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#processMerge(java.util.List)
636         */
637        public void processMerge(List<PurchasingAccountsPayableItemAsset> mergeLines, List<PurchasingAccountsPayableActionHistory> actionsTakeHistory, boolean isMergeAll) {
638            PurchasingAccountsPayableItemAsset sourceItem = null;
639            PurchasingAccountsPayableLineAssetAccount targetAccount = null;
640            // use the first item and its accounts as the target
641            PurchasingAccountsPayableItemAsset firstItem = mergeLines.get(0);
642            List<PurchasingAccountsPayableLineAssetAccount> firstAccountList = firstItem.getPurchasingAccountsPayableLineAssetAccounts();
643    
644            // Merge accounts starting from the second item to the last one.
645            for (int i = 1; i < mergeLines.size(); i++) {
646                sourceItem = mergeLines.get(i);
647                for (PurchasingAccountsPayableLineAssetAccount account : sourceItem.getPurchasingAccountsPayableLineAssetAccounts()) {
648                    // Check if we can grouping the accounts. If yes, update the account amount without moving account line.
649                    targetAccount = getFromTargetAccountList(firstAccountList, account.getGeneralLedgerAccountIdentifier());
650                    if (targetAccount != null) {
651                        updateAccountAmount(account.getItemAccountTotalAmount(), targetAccount);
652                    }
653                    else {
654                        // move account from source to target
655                        account.setDocumentNumber(firstItem.getDocumentNumber());
656                        account.setAccountsPayableLineItemIdentifier(firstItem.getAccountsPayableLineItemIdentifier());
657                        account.setCapitalAssetBuilderLineNumber(firstItem.getCapitalAssetBuilderLineNumber());
658                        account.setPurchasingAccountsPayableItemAsset(firstItem);
659                        firstAccountList.add(account);
660                    }
661                }
662            }
663    
664            // update action history, remove lines before merge and clean up the user input
665            postMergeProcess(mergeLines, actionsTakeHistory, isMergeAll);
666        }
667    
668        /**
669         * Process after merge.
670         * 
671         * @param mergeLines
672         * @param purApLineSession
673         * @param isMergeAll
674         */
675        protected void postMergeProcess(List<PurchasingAccountsPayableItemAsset> mergeLines, List<PurchasingAccountsPayableActionHistory> actionsTakeHistory, boolean isMergeAll) {
676            String actionTypeCode = isMergeAll ? CabConstants.Actions.MERGE_ALL : CabConstants.Actions.MERGE;
677            PurchasingAccountsPayableItemAsset targetItem = mergeLines.get(0);
678    
679            // set unit cost and total cost
680            setLineItemCost(targetItem);
681            targetItem.setItemAssignedToTradeInIndicator(false);
682    
683            // For all merge source lines(the first line is considered as target technically), remove it from the document and update in
684            // the action history.
685            for (int i = 1; i < mergeLines.size(); i++) {
686                PurchasingAccountsPayableItemAsset sourceItem = mergeLines.get(i);
687    
688                // Update the action history.
689                addMergeHistory(actionsTakeHistory, actionTypeCode, sourceItem, targetItem);
690    
691                if (ObjectUtils.isNotNull(sourceItem.getPurchasingAccountsPayableDocument())) {
692                    // remove mergeLines from the document
693                    sourceItem.getPurchasingAccountsPayableDocument().getPurchasingAccountsPayableItemAssets().remove(sourceItem);
694                    // if all active lines are merged to other line, we need to in-activate the current document
695                    conditionallyUpdateDocumentStatusAsProcessed(sourceItem.getPurchasingAccountsPayableDocument());
696                }
697            }
698            // set the target item itemLineNumber for pre-tagging
699            Integer poId = targetItem.getPurchasingAccountsPayableDocument().getPurchaseOrderIdentifier();
700            if (poId != null) {
701                Pretag targetPretag = getTargetPretag(mergeLines, poId);
702                if (targetPretag != null) {
703                    targetItem.setItemLineNumber(targetPretag.getItemLineNumber());
704                }
705            }
706    
707            // update create asset/ apply payment indicator if any of the merged lines has the indicator set.
708            updateAssetIndicatorAfterMerge(mergeLines);
709    
710            // update activity status code as modified
711            updateItemStatusAsUserModified(targetItem);
712        }
713    
714        /**
715         * Update create asset and apply payment indicators after merge.
716         * 
717         * @param mergeLines
718         */
719        protected void updateAssetIndicatorAfterMerge(List<PurchasingAccountsPayableItemAsset> mergeLines) {
720            boolean existCreateAsset = false;
721            boolean existApplyPayment = false;
722            PurchasingAccountsPayableItemAsset targetItem = mergeLines.get(0);
723            // set indicator if any of the source item set it or target item set it.
724            for (int i = 1; i < mergeLines.size(); i++) {
725                PurchasingAccountsPayableItemAsset sourceItem = mergeLines.get(i);
726                existCreateAsset |= sourceItem.isCreateAssetIndicator();
727                existApplyPayment |= sourceItem.isApplyPaymentIndicator();
728            }
729            targetItem.setCreateAssetIndicator(targetItem.isCreateAssetIndicator() | existCreateAsset);
730            targetItem.setApplyPaymentIndicator(targetItem.isApplyPaymentIndicator() | existApplyPayment);
731        }
732    
733        /**
734         * Get the first pre-tag for given itemLines
735         * 
736         * @param itemLines
737         * @param purchaseOrderIdentifier
738         * @return
739         */
740        protected Pretag getTargetPretag(List<PurchasingAccountsPayableItemAsset> itemLines, Integer purchaseOrderIdentifier) {
741            for (PurchasingAccountsPayableItemAsset item : itemLines) {
742                Pretag newTag = getPreTagLineItem(purchaseOrderIdentifier, item.getItemLineNumber());
743    
744                if (isPretaggingExisting(newTag))
745                    return newTag;
746            }
747    
748            return null;
749        }
750    
751        /**
752         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isPretaggingExisting(org.kuali.kfs.module.cab.businessobject.Pretag)
753         */
754        public boolean isPretaggingExisting(Pretag newTag) {
755            return ObjectUtils.isNotNull(newTag) && newTag.getPretagDetails() != null && !newTag.getPretagDetails().isEmpty();
756        }
757    
758    
759        /**
760         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isMultipleTagExisting(java.lang.Integer, java.util.Set)
761         */
762        public boolean isMultipleTagExisting(Integer purchaseOrderIdentifier, Set<Integer> itemLineNumbers) {
763            Pretag firstTag = null;
764            for (Iterator iterator = itemLineNumbers.iterator(); iterator.hasNext();) {
765                Integer itemLineNumber = (Integer) iterator.next();
766                Pretag newTag = getPreTagLineItem(purchaseOrderIdentifier, itemLineNumber);
767    
768                if (isPretaggingExisting(newTag))
769                    if (firstTag != null) {
770                        // find the second preTagging item
771                        return true;
772                    }
773                    else {
774                        firstTag = newTag;
775                    }
776            }
777    
778            return false;
779        }
780    
781        /**
782         * Add merge action to the action history.
783         * 
784         * @param purApLineSession
785         * @param isMergeAllAction
786         * @param firstItem
787         * @param item
788         */
789        protected void addMergeHistory(List<PurchasingAccountsPayableActionHistory> actionsTakenHistory, String actionTypeCode, PurchasingAccountsPayableItemAsset sourceItem, PurchasingAccountsPayableItemAsset targetItem) {
790            // create action history records for each account from the source lines.
791            for (PurchasingAccountsPayableLineAssetAccount sourceAccount : sourceItem.getPurchasingAccountsPayableLineAssetAccounts()) {
792                PurchasingAccountsPayableActionHistory newAction = new PurchasingAccountsPayableActionHistory(sourceItem, targetItem, actionTypeCode);
793                newAction.setAccountsPayableItemQuantity(sourceItem.getAccountsPayableItemQuantity());
794                newAction.setItemAccountTotalAmount(sourceAccount.getItemAccountTotalAmount());
795                newAction.setGeneralLedgerAccountIdentifier(sourceAccount.getGeneralLedgerAccountIdentifier());
796                actionsTakenHistory.add(newAction);
797            }
798        }
799    
800    
801        /**
802         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#processPercentPayment(org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset)
803         */
804        public void processPercentPayment(PurchasingAccountsPayableItemAsset itemAsset, List<PurchasingAccountsPayableActionHistory> actionsTakenHistory) {
805            KualiDecimal oldQty = itemAsset.getAccountsPayableItemQuantity();
806            KualiDecimal newQty = new KualiDecimal(1);
807            // update quantity, total cost and unit cost.
808            if (oldQty.isLessThan(newQty)) {
809                itemAsset.setAccountsPayableItemQuantity(newQty);
810                setLineItemCost(itemAsset);
811                // add to action history
812                addPercentPaymentHistory(actionsTakenHistory, itemAsset, oldQty);
813                // update status code
814                updateItemStatusAsUserModified(itemAsset);
815            }
816        }
817    
818    
819        /**
820         * Update activity status code when percent payment/split/allocate/merge action taken.
821         * 
822         * @param itemAsset
823         */
824        protected void updateItemStatusAsUserModified(PurchasingAccountsPayableItemAsset itemAsset) {
825            itemAsset.setActivityStatusCode(CabConstants.ActivityStatusCode.MODIFIED);
826            for (PurchasingAccountsPayableLineAssetAccount account : itemAsset.getPurchasingAccountsPayableLineAssetAccounts()) {
827                account.setActivityStatusCode(CabConstants.ActivityStatusCode.MODIFIED);
828            }
829    
830            itemAsset.getPurchasingAccountsPayableDocument().setActivityStatusCode(CabConstants.ActivityStatusCode.MODIFIED);
831    
832        }
833    
834        /**
835         * Update action history for the percent payment action.
836         * 
837         * @param actionsTaken
838         * @param item
839         * @param oldQty
840         */
841        protected void addPercentPaymentHistory(List<PurchasingAccountsPayableActionHistory> actionsTakenHistory, PurchasingAccountsPayableItemAsset item, KualiDecimal oldQty) {
842            // create and set up one action history record for this action
843            PurchasingAccountsPayableActionHistory newAction = new PurchasingAccountsPayableActionHistory(item, item, CabConstants.Actions.PERCENT_PAYMENT);
844            // record quantity before percent payment into action history
845            newAction.setAccountsPayableItemQuantity(oldQty);
846            actionsTakenHistory.add(newAction);
847        }
848    
849        /**
850         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#processSplit(org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset)
851         */
852        public void processSplit(PurchasingAccountsPayableItemAsset splitItemAsset, List<PurchasingAccountsPayableActionHistory> actionsTakeHistory) {
853            PurchasingAccountsPayableDocument purApDoc = splitItemAsset.getPurchasingAccountsPayableDocument();
854            // update activity status code for split item. it will be propogated to new created item and its accounts.
855            updateItemStatusAsUserModified(splitItemAsset);
856    
857            // create a new item asset from the current item asset.
858            PurchasingAccountsPayableItemAsset newItemAsset = new PurchasingAccountsPayableItemAsset(splitItemAsset);
859    
860            // set cab line number
861            newItemAsset.setCapitalAssetBuilderLineNumber(getMaxCabLineNumber(splitItemAsset, purApDoc) + 1);
862    
863            newItemAsset.setAccountsPayableItemQuantity(splitItemAsset.getSplitQty());
864    
865            // Set account list for new item asset and update current account amount value.
866            createAccountsForNewItemAsset(splitItemAsset, newItemAsset);
867            // set unit cost and total cost in new item
868            setLineItemCost(newItemAsset);
869    
870            // Adjust current item asset quantity, total cost and unit cost
871            splitItemAsset.setAccountsPayableItemQuantity(splitItemAsset.getAccountsPayableItemQuantity().subtract(splitItemAsset.getSplitQty()));
872            setLineItemCost(splitItemAsset);
873    
874            // add the new item into document and sort.
875            purApDoc.getPurchasingAccountsPayableItemAssets().add(newItemAsset);
876            Collections.sort(purApDoc.getPurchasingAccountsPayableItemAssets());
877    
878            // Add to action history
879            addSplitHistory(splitItemAsset, newItemAsset, actionsTakeHistory);
880    
881            // clear up user input
882            splitItemAsset.setSplitQty(null);
883        }
884    
885    
886        /**
887         * Get the max cab line #. As part of the primary key, it should be the max value among the form item list and DB.
888         * 
889         * @param splitItemAsset
890         * @param purApDoc
891         * @return
892         */
893        protected int getMaxCabLineNumber(PurchasingAccountsPayableItemAsset splitItemAsset, PurchasingAccountsPayableDocument purApDoc) {
894            // get the max CAB line number in DB.
895            Integer maxDBCabLineNbr = purApLineDao.getMaxCabLineNumber(splitItemAsset.getDocumentNumber(), splitItemAsset.getAccountsPayableLineItemIdentifier());
896            // get the max CAB line number in form.
897            int availableCabLineNbr = getMaxCabLineNbrForItemInForm(purApDoc, splitItemAsset);
898    
899            if (maxDBCabLineNbr.intValue() > availableCabLineNbr) {
900                availableCabLineNbr = maxDBCabLineNbr.intValue();
901            }
902            return availableCabLineNbr;
903        }
904    
905        /**
906         * Search the current active items and return the max CAB line # for split new item .
907         * 
908         * @param purApDoc
909         * @param currentItemAsset
910         * @return
911         */
912        protected int getMaxCabLineNbrForItemInForm(PurchasingAccountsPayableDocument purApDoc, PurchasingAccountsPayableItemAsset currentItemAsset) {
913            int maxCabLineNbr = 0;
914            for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
915                if (item.getDocumentNumber().equalsIgnoreCase(currentItemAsset.getDocumentNumber()) && item.getAccountsPayableLineItemIdentifier().equals(currentItemAsset.getAccountsPayableLineItemIdentifier()) && item.getCapitalAssetBuilderLineNumber().intValue() > maxCabLineNbr) {
916                    maxCabLineNbr = item.getCapitalAssetBuilderLineNumber().intValue();
917                }
918    
919            }
920            return maxCabLineNbr;
921        }
922    
923        /**
924         * Update action history for a split action.
925         * 
926         * @param currentItemAsset
927         * @param newItemAsset
928         * @param actionsTaken
929         */
930        protected void addSplitHistory(PurchasingAccountsPayableItemAsset currentItemAsset, PurchasingAccountsPayableItemAsset newItemAsset, List<PurchasingAccountsPayableActionHistory> actionsTakenHistory) {
931            // for each account moved from original item to new item, create one action history record
932            for (PurchasingAccountsPayableLineAssetAccount account : newItemAsset.getPurchasingAccountsPayableLineAssetAccounts()) {
933                PurchasingAccountsPayableActionHistory newAction = new PurchasingAccountsPayableActionHistory(currentItemAsset, newItemAsset, CabConstants.Actions.SPLIT);
934                newAction.setGeneralLedgerAccountIdentifier(account.getGeneralLedgerAccountIdentifier());
935                // quantity moved from original item to new item
936                newAction.setAccountsPayableItemQuantity(newItemAsset.getAccountsPayableItemQuantity());
937                // account amount moved to new item
938                newAction.setItemAccountTotalAmount(account.getItemAccountTotalAmount());
939                actionsTakenHistory.add(newAction);
940            }
941        }
942    
943    
944        /**
945         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#processSaveBusinessObjects(java.util.List,
946         *      org.kuali.kfs.module.cab.document.web.PurApLineSession)
947         */
948        public void processSaveBusinessObjects(List<PurchasingAccountsPayableDocument> purApDocs, PurApLineSession purApLineSession) {
949            // Get removable asset locks which could be generated by allocate or merge when items removed and the lock should be
950            // released.
951            Map<String, Set> removableAssetLocks = getRemovableAssetLocks(purApLineSession.getProcessedItems());
952            for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
953                // auto save items(including deleted items) and accounts due to auto-update setting in OJB.
954                businessObjectService.save(purApDoc);
955            }
956            // remove asset locks
957            for (String lockingDocumentNbr : removableAssetLocks.keySet()) {
958                Set<String> lockingInfoList = removableAssetLocks.get(lockingDocumentNbr);
959                for (String lockingInfo : lockingInfoList) {
960                    if (this.getCapitalAssetManagementModuleService().isAssetLockedByCurrentDocument(lockingDocumentNbr, lockingInfo)) {
961                        this.getCapitalAssetManagementModuleService().deleteAssetLocks(lockingDocumentNbr, lockingInfo);
962                    }
963                }
964            }
965    
966            if (purApLineSession != null) {
967                // save to action history table
968                List<PurchasingAccountsPayableActionHistory> historyList = purApLineSession.getActionsTakenHistory();
969                if (historyList != null && !historyList.isEmpty()) {
970                    businessObjectService.save(historyList);
971                    historyList.clear();
972                }
973    
974                // save to generalLedgerEntry table
975                List<GeneralLedgerEntry> glUpdateList = purApLineSession.getGlEntryUpdateList();
976                if (glUpdateList != null && !glUpdateList.isEmpty()) {
977                    businessObjectService.save(glUpdateList);
978                    glUpdateList.clear();
979                }
980    
981            }
982        }
983    
984    
985        /**
986         * Create asset account list for new item asset and update the current account amount.
987         * 
988         * @param oldItemAsset old line item.
989         * @param newItemAsset new line item.
990         */
991        protected void createAccountsForNewItemAsset(PurchasingAccountsPayableItemAsset currentItemAsset, PurchasingAccountsPayableItemAsset newItemAsset) {
992            KualiDecimal currentQty = currentItemAsset.getAccountsPayableItemQuantity();
993            KualiDecimal splitQty = currentItemAsset.getSplitQty();
994            List<PurchasingAccountsPayableLineAssetAccount> newAccountsList = newItemAsset.getPurchasingAccountsPayableLineAssetAccounts();
995            PurchasingAccountsPayableLineAssetAccount newAccount;
996            for (PurchasingAccountsPayableLineAssetAccount currentAccount : currentItemAsset.getPurchasingAccountsPayableLineAssetAccounts()) {
997                // create accounts for new item asset.
998                newAccount = new PurchasingAccountsPayableLineAssetAccount(newItemAsset, currentAccount.getGeneralLedgerAccountIdentifier());
999                newAccount.setItemAccountTotalAmount(currentAccount.getItemAccountTotalAmount().multiply(splitQty).divide(currentQty));
1000                newAccount.setGeneralLedgerEntry(currentAccount.getGeneralLedgerEntry());
1001                newAccountsList.add(newAccount);
1002    
1003                // Adjust current account amount for split item( subtract new account amount for original amount)
1004                currentAccount.setItemAccountTotalAmount(currentAccount.getItemAccountTotalAmount().subtract(newAccount.getItemAccountTotalAmount()));
1005            }
1006        }
1007    
1008    
1009        /**
1010         * Set object code by the first one from the accounting lines.
1011         * 
1012         * @param item Selected line item.
1013         */
1014        protected void setFirstFinancialObjectCode(PurchasingAccountsPayableItemAsset item) {
1015            String firstFinancialObjectCode = null;
1016            for (PurchasingAccountsPayableLineAssetAccount account : item.getPurchasingAccountsPayableLineAssetAccounts()) {
1017                if (ObjectUtils.isNotNull(account.getGeneralLedgerEntry())) {
1018                    firstFinancialObjectCode = account.getGeneralLedgerEntry().getFinancialObjectCode();
1019                    break;
1020                }
1021            }
1022            item.setFirstFincialObjectCode(firstFinancialObjectCode);
1023        }
1024    
1025    
1026        /**
1027         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#buildPurApItemAssetList(java.util.List)
1028         */
1029        public void buildPurApItemAssetList(List<PurchasingAccountsPayableDocument> purApDocs) {
1030            for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
1031                for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
1032                    // set item non-persistent fields from PurAp PREQ/CM item tables
1033                    purApInfoService.setAccountsPayableItemsFromPurAp(item, purApDoc.getDocumentTypeCode());
1034    
1035                    // set line item unit cost and total cost
1036                    setLineItemCost(item);
1037    
1038                    // set financial object code
1039                    setFirstFinancialObjectCode(item);
1040                }
1041                // For display purpose, move additional charges including trade-in below item lines.
1042                Collections.sort(purApDoc.getPurchasingAccountsPayableItemAssets());
1043            }
1044    
1045            // set CAMS Transaction type from PurAp
1046            purApInfoService.setCamsTransactionFromPurAp(purApDocs);
1047    
1048            // set create asset/apply payment indicator which are used to control display two buttons.
1049            setAssetIndicator(purApDocs);
1050    
1051        }
1052    
1053    
1054        /**
1055         * @see org.kuali.kfs.module.cab.document.service.PurApLineService#getPreTagLineItem(java.lang.String, java.lang.Integer)
1056         */
1057        public Pretag getPreTagLineItem(Integer purchaseOrderIdentifier, Integer lineItemNumber) {
1058    
1059            if (purchaseOrderIdentifier == null || lineItemNumber == null) {
1060                return null;
1061            }
1062    
1063            Map<String, Object> pKeys = new HashMap<String, Object>();
1064    
1065            pKeys.put(CabPropertyConstants.Pretag.PURCHASE_ORDER_NUMBER, purchaseOrderIdentifier);
1066            pKeys.put(CabPropertyConstants.Pretag.ITEM_LINE_NUMBER, lineItemNumber);
1067            return (Pretag) businessObjectService.findByPrimaryKey(Pretag.class, pKeys);
1068        }
1069    
1070        /**
1071         * Set create asset and apply payment indicator. These two indicators are referenced by jsp to control display of these two
1072         * buttons. How to set these two indicators is based on the business rules. We need to put the following situations into
1073         * consideration. Since we move allocate additional charge allocation to CAB batch, we bring over addl charge lines only when
1074         * they are the only items in the document or they are from the FO changes. To accommodate this, we relax the rules and defined
1075         * as:
1076         * <p>
1077         * 1. If the line is trade-in allowance and without trade-in indicator items, we open these two buttons.
1078         * <p>
1079         * 2. For line items, if it has trade-in indicator set, there must be no trade-in allowance pending for allocation. Trade-in
1080         * allowance could from other document but share the same po_id.
1081         * 
1082         * @param purApDocs
1083         */
1084        protected void setAssetIndicator(List<PurchasingAccountsPayableDocument> purApDocs) {
1085            // get the trade-in allowance in the form-wise
1086            boolean existTradeInAllowance = isTradeInAllowanceExist(purApDocs);
1087            // get the trade-in indicator in the form-wise
1088            boolean existTradeInIndicator = isTradeInIndicatorExistInAllLines(purApDocs);
1089            for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
1090                boolean existAdditionalCharge = false;
1091                // check if within the same document, exist both additional charge lines and normal line items.
1092                for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
1093                    existAdditionalCharge |= item.isAdditionalChargeNonTradeInIndicator();
1094                }
1095                for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
1096                    // when the indicator is not set yet...
1097                    if (!item.isCreateAssetIndicator() || !item.isApplyPaymentIndicator()) {
1098                        // If the lines on the purchase order are all additional charge lines, or trade-in allowance then we can apply
1099                        // payment, or create asset.
1100                        existTradeInIndicator = item.isItemAssignedToTradeInIndicator();
1101                        
1102                        if (item.isAdditionalChargeNonTradeInIndicator() || (item.isTradeInAllowance() && !existTradeInIndicator)) {
1103                            item.setCreateAssetIndicator(true);
1104                            item.setApplyPaymentIndicator(true);
1105                        }
1106                        // For line item(not additional charge line), if there is no pending additional charges and itself not a
1107                        // trade-in indicator or it has trade-in indicator but no trade-in allowance, we allow apply payment or create
1108                        // asset.
1109                        else if (!existAdditionalCharge && (!item.isAdditionalChargeNonTradeInIndicator() && !item.isTradeInAllowance() && (!item.isItemAssignedToTradeInIndicator() || !existTradeInAllowance))) {
1110                            item.setCreateAssetIndicator(true);
1111                            item.setApplyPaymentIndicator(true);
1112                        }
1113                    }
1114                }
1115            }
1116    
1117        }
1118    
1119    
1120        /**
1121         * Set item asset unit cost.
1122         * 
1123         * @param item line item
1124         * @param totalCost total cost for this line item.
1125         */
1126        protected void setItemAssetUnitCost(PurchasingAccountsPayableItemAsset item, KualiDecimal totalCost) {
1127            // set unit cost
1128            KualiDecimal quantity = item.getAccountsPayableItemQuantity();
1129            if (quantity != null && quantity.isNonZero()) {
1130                item.setUnitCost(totalCost.divide(quantity));
1131            }
1132        }
1133    
1134        /**
1135         * Calculate item asset total cost
1136         * 
1137         * @param item
1138         * @return line item total cost
1139         */
1140        public KualiDecimal calculateItemAssetTotalCost(PurchasingAccountsPayableItemAsset item) {
1141            // Calculate and set total cost
1142            KualiDecimal totalCost = KualiDecimal.ZERO;
1143            for (PurchasingAccountsPayableLineAssetAccount account : item.getPurchasingAccountsPayableLineAssetAccounts()) {
1144                if (account.getItemAccountTotalAmount() != null) {
1145                    totalCost = totalCost.add(account.getItemAccountTotalAmount());
1146                }
1147            }
1148            return totalCost;
1149        }
1150    
1151    
1152        /**
1153         * Gets the businessObjectService attribute.
1154         * 
1155         * @return Returns the businessObjectService.
1156         */
1157        public BusinessObjectService getBusinessObjectService() {
1158            return businessObjectService;
1159        }
1160    
1161    
1162        /**
1163         * Sets the businessObjectService attribute value.
1164         * 
1165         * @param businessObjectService The businessObjectService to set.
1166         */
1167        public void setBusinessObjectService(BusinessObjectService businessObjectService) {
1168            this.businessObjectService = businessObjectService;
1169        }
1170    
1171    
1172        /**
1173         * Gets the purApLineDao attribute.
1174         * 
1175         * @return Returns the purApLineDao.
1176         */
1177        public PurApLineDao getPurApLineDao() {
1178            return purApLineDao;
1179        }
1180    
1181    
1182        /**
1183         * Sets the purApLineDao attribute value.
1184         * 
1185         * @param purApLineDao The purApLineDao to set.
1186         */
1187        public void setPurApLineDao(PurApLineDao purApLineDao) {
1188            this.purApLineDao = purApLineDao;
1189        }
1190    
1191        /**
1192         * Gets the purApInfoService attribute.
1193         * 
1194         * @return Returns the purApInfoService.
1195         */
1196        public PurApInfoService getPurApInfoService() {
1197            return purApInfoService;
1198        }
1199    
1200        /**
1201         * Sets the purApInfoService attribute value.
1202         * 
1203         * @param purApInfoService The purApInfoService to set.
1204         */
1205        public void setPurApInfoService(PurApInfoService purApInfoService) {
1206            this.purApInfoService = purApInfoService;
1207        }
1208    
1209        /**
1210         * get CAMS AssetService.
1211         * 
1212         * @return
1213         */
1214        protected AssetService getAssetService() {
1215            return SpringContext.getBean(AssetService.class);
1216        }
1217    
1218        /**
1219         * Gets the capitalAssetManagementModuleService attribute.
1220         * 
1221         * @return Returns the capitalAssetManagementModuleService.
1222         */
1223        public CapitalAssetManagementModuleService getCapitalAssetManagementModuleService() {
1224            return capitalAssetManagementModuleService;
1225        }
1226    
1227        /**
1228         * Sets the capitalAssetManagementModuleService attribute value.
1229         * 
1230         * @param capitalAssetManagementModuleService The capitalAssetManagementModuleService to set.
1231         */
1232        public void setCapitalAssetManagementModuleService(CapitalAssetManagementModuleService capitalAssetManagementModuleService) {
1233            this.capitalAssetManagementModuleService = capitalAssetManagementModuleService;
1234        }
1235    
1236    
1237    }