/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 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.document.service.impl;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.integration.cam.CapitalAssetManagementModuleService;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.cab.CabConstants;
import org.kuali.kfs.module.cab.CabPropertyConstants;
import org.kuali.kfs.module.cab.businessobject.GeneralLedgerEntry;
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.dataaccess.PurApLineDao;
import org.kuali.kfs.module.cab.document.service.PurApInfoService;
import org.kuali.kfs.module.cab.document.service.PurApLineService;
import org.kuali.kfs.module.cab.document.web.PurApLineSession;
import org.kuali.kfs.module.cam.CamsConstants;
import org.kuali.kfs.module.cam.document.service.AssetService;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;


/**
 * This class provides default implementations of {@link PurApLineService}
 */
@Transactional
public class PurApLineServiceImpl implements PurApLineService {
    private static final Logger LOG = Logger.getLogger(PurApLineServiceImpl.class);
    private BusinessObjectService businessObjectService;
    private PurApLineDao purApLineDao;
    private PurApInfoService purApInfoService;
    private CapitalAssetManagementModuleService capitalAssetManagementModuleService;

    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#mergeLinesHasDifferentObjectSubTypes(java.util.List)
     */
    @Override
    public boolean mergeLinesHasDifferentObjectSubTypes(List<PurchasingAccountsPayableItemAsset> mergeLines) {
        boolean invalid = false;
        List<String> objectSubTypeList = new ArrayList<String>();

        // collect all objectSubTypes from item lines
        for (PurchasingAccountsPayableItemAsset itemAsset : mergeLines) {
            for (PurchasingAccountsPayableLineAssetAccount account : itemAsset.getPurchasingAccountsPayableLineAssetAccounts()) {
                account.getGeneralLedgerEntry().refreshReferenceObject(CabPropertyConstants.GeneralLedgerEntry.FINANCIAL_OBJECT);
                ObjectCode objCode = account.getGeneralLedgerEntry().getFinancialObject();
                if (ObjectUtils.isNotNull(objCode) && StringUtils.isNotEmpty(objCode.getFinancialObjectSubTypeCode())) {
                    objectSubTypeList.add(objCode.getFinancialObjectSubTypeCode());
                }
            }
        }

        // check if different objectSubTypes exist
        if (!getAssetService().isObjectSubTypeCompatible(objectSubTypeList)) {
            invalid = true;
        }
        return invalid;
    }

    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#allocateLinesHasDifferentObjectSubTypes(java.util.List,
     * org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset)
     */
    @Override
    public boolean allocateLinesHasDifferentObjectSubTypes(List<PurchasingAccountsPayableItemAsset> targetLines, PurchasingAccountsPayableItemAsset sourceLine) {
        boolean invalid = false;
        List<String> objectSubTypeList = new ArrayList<String>();

        // collect objectSubTypes from target item lines
        for (PurchasingAccountsPayableItemAsset itemAsset : targetLines) {
            for (PurchasingAccountsPayableLineAssetAccount account : itemAsset.getPurchasingAccountsPayableLineAssetAccounts()) {
                account.getGeneralLedgerEntry().refreshReferenceObject(CabPropertyConstants.GeneralLedgerEntry.FINANCIAL_OBJECT);
                ObjectCode objCode = account.getGeneralLedgerEntry().getFinancialObject();
                if (ObjectUtils.isNotNull(objCode) && StringUtils.isNotEmpty(objCode.getFinancialObjectSubTypeCode())) {
                    objectSubTypeList.add(objCode.getFinancialObjectSubTypeCode());
                }
            }
        }

        // collect objectSubTypes from source item line
        if (ObjectUtils.isNotNull(sourceLine)) {
            for (PurchasingAccountsPayableLineAssetAccount account : sourceLine.getPurchasingAccountsPayableLineAssetAccounts()) {
                account.getGeneralLedgerEntry().refreshReferenceObject(CabPropertyConstants.GeneralLedgerEntry.FINANCIAL_OBJECT);
                ObjectCode objCode = account.getGeneralLedgerEntry().getFinancialObject();
                if (ObjectUtils.isNotNull(objCode) && StringUtils.isNotEmpty(objCode.getFinancialObjectSubTypeCode())) {
                    objectSubTypeList.add(objCode.getFinancialObjectSubTypeCode());
                }
            }
        }

        // check if different objectSubTypes exist
        if (!getAssetService().isObjectSubTypeCompatible(objectSubTypeList)) {
            invalid = true;
        }
        return invalid;
    }

    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineDocumentService#inActivateDocument(org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableDocument)
     */
    @Override
    public void conditionallyUpdateDocumentStatusAsProcessed(PurchasingAccountsPayableDocument selectedDoc) {
        for (PurchasingAccountsPayableItemAsset item : selectedDoc.getPurchasingAccountsPayableItemAssets()) {
            if (item.isActive()) {
                return;
            }
        }
        // set as processed when allocate/merge and all its items are in CAMs
        selectedDoc.setActivityStatusCode(CabConstants.ActivityStatusCode.PROCESSED_IN_CAMS);
    }

    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#resetSelectedValue(java.util.List)
     */
    @Override
    public void resetSelectedValue(List<PurchasingAccountsPayableDocument> purApDocs) {
        for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
            for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
                item.setSelectedValue(false);
            }
        }
    }

    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#processAllocate(org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset,
     * java.util.List, org.kuali.kfs.module.cab.document.web.PurApLineSession, java.util.List)
     */
    @Override
    public boolean processAllocate(PurchasingAccountsPayableItemAsset allocateSourceLine,
                                   List<PurchasingAccountsPayableItemAsset> allocateTargetLines,
                                   List<PurchasingAccountsPayableActionHistory> actionsTakeHistory,
                                   List<PurchasingAccountsPayableDocument> purApDocs,
                                   boolean initiateFromBatch) {

        boolean allocatedIndicator = true;
        // indicator of additional charge allocation
        boolean allocateAddlChrgIndicator = allocateSourceLine.isAdditionalChargeNonTradeInIndicator() | allocateSourceLine.isTradeInAllowance();
        // Maintain this account List for update. So accounts already allocated won't take effect for account not allocated yet.
        List<PurchasingAccountsPayableLineAssetAccount> newAccountList = new ArrayList<PurchasingAccountsPayableLineAssetAccount>();

        // For each account in the source item, allocate it to the target items.
        for (PurchasingAccountsPayableLineAssetAccount sourceAccount : allocateSourceLine.getPurchasingAccountsPayableLineAssetAccounts()) {
            sourceAccount.refresh();
            // Get allocate to target account list
            List<PurchasingAccountsPayableLineAssetAccount> targetAccounts = getAllocateTargetAccounts(sourceAccount, allocateTargetLines, allocateAddlChrgIndicator);
            if (!targetAccounts.isEmpty()) {
                // Percentage amount to each target account
                allocateByItemAccountAmount(sourceAccount, targetAccounts, newAccountList, actionsTakeHistory);
            } else {
                allocatedIndicator = false;
                break;
            }
        }

        if (allocatedIndicator) {
            postAllocateProcess(allocateSourceLine, allocateTargetLines, purApDocs, newAccountList, initiateFromBatch);
        }

        return allocatedIndicator;
    }


    /**
     * Process after allocate.
     *
     * @param selectedLineItem
     * @param allocateTargetLines
     * @param purApDocs
     * @param newAccountList
     */
    protected void postAllocateProcess(PurchasingAccountsPayableItemAsset selectedLineItem, List<PurchasingAccountsPayableItemAsset> allocateTargetLines, List<PurchasingAccountsPayableDocument> purApDocs, List<PurchasingAccountsPayableLineAssetAccount> newAccountList, boolean initiateFromBatch) {
        // add new account into each item list
        addNewAccountToItemList(newAccountList);

        // update total cost and unit cost.
        updateLineItemsCost(allocateTargetLines);

        if (ObjectUtils.isNotNull(selectedLineItem.getPurchasingAccountsPayableDocument())) {
            // remove allocate source line item.
            selectedLineItem.getPurchasingAccountsPayableDocument().getPurchasingAccountsPayableItemAssets().remove(selectedLineItem);
            // when an line is removed from the document, we should check if it is the only active line in the document and
            // in-activate document if yes.
            conditionallyUpdateDocumentStatusAsProcessed(selectedLineItem.getPurchasingAccountsPayableDocument());
        }

        // Adjust create asset and apply payment indicator only when allocate additional charges.
        if (!initiateFromBatch && (selectedLineItem.isAdditionalChargeNonTradeInIndicator() || selectedLineItem.isTradeInAllowance())) {
            setAssetIndicator(purApDocs);
        }

        // update status code as user modified for allocate target lines
        if (!initiateFromBatch) {
            for (PurchasingAccountsPayableItemAsset allocateTargetItem : allocateTargetLines) {
                updateItemStatusAsUserModified(allocateTargetItem);
            }
        }
    }

    /**
     * Build removable asset lock map from the processedItems list. We need to remove all asset locks hold be items which has been
     * merged or allocated to other lines.
     *
     * @param processedItems
     * @return
     */
    protected Map<String, Set> getRemovableAssetLocks(List<PurchasingAccountsPayableItemAsset> processedItems) {
        Map<String, Set> removableAssetLocks = new HashMap<String, Set>();

        for (PurchasingAccountsPayableItemAsset processedItem : processedItems) {
            // For the time being, only for individual system, each item has its own asset numbers.
            if (processedItem.getLockingInformation() != null && !CamsConstants.defaultLockingInformation.equals(processedItem.getLockingInformation())) {
                addAssetLock(removableAssetLocks, processedItem);
            } else if (ObjectUtils.isNotNull(processedItem.getPurchasingAccountsPayableDocument())) {
                // check other items if they are fully processed and can release the lock
                List<PurchasingAccountsPayableItemAsset> remainingItems = processedItem.getPurchasingAccountsPayableDocument().getPurchasingAccountsPayableItemAssets();
                boolean fullyProcessed = true;
                for (PurchasingAccountsPayableItemAsset itemAsset : remainingItems) {
                    if (!CabConstants.ActivityStatusCode.PROCESSED_IN_CAMS.equalsIgnoreCase(itemAsset.getActivityStatusCode())) {
                        fullyProcessed = false;
                        break;
                    }
                }
                if (fullyProcessed) {
                    // All the items are either merged or allocated to other document item. We should remove the asset lock
                    // retained by this document.
                    addAssetLock(removableAssetLocks, processedItem);
                }
            }
        }

        processedItems.clear();
        return removableAssetLocks;
    }

    protected void addAssetLock(Map<String, Set> removableAssetLocks, PurchasingAccountsPayableItemAsset processedItem) {
        if (processedItem.getLockingInformation() == null) {
            processedItem.setLockingInformation(CamsConstants.defaultLockingInformation);
        }
        if (removableAssetLocks.containsKey(processedItem.getDocumentNumber())) {
            Set lockingInfoList = removableAssetLocks.get(processedItem.getDocumentNumber());
            lockingInfoList.add(processedItem.getLockingInformation());
        } else {
            Set lockingInfoList = new HashSet<String>();
            lockingInfoList.add(processedItem.getLockingInformation());
            removableAssetLocks.put(processedItem.getDocumentNumber(), lockingInfoList);
        }
    }

    /**
     * Reset item total cost and unit cost for each item.
     *
     * @param allocateTargetLines
     */
    protected void updateLineItemsCost(List<PurchasingAccountsPayableItemAsset> lineItems) {
        // update target item unit cost and total cost
        for (PurchasingAccountsPayableItemAsset item : lineItems) {
            setLineItemCost(item);
        }
    }

    /**
     * update account list for each line item
     *
     * @param updatedAccountList
     */
    protected void addNewAccountToItemList(List<PurchasingAccountsPayableLineAssetAccount> newAccountList) {
        PurchasingAccountsPayableItemAsset lineItem = null;
        for (PurchasingAccountsPayableLineAssetAccount newAccount : newAccountList) {
            lineItem = newAccount.getPurchasingAccountsPayableItemAsset();
            if (ObjectUtils.isNotNull(lineItem) && ObjectUtils.isNotNull(lineItem.getPurchasingAccountsPayableLineAssetAccounts())) {
                lineItem.getPurchasingAccountsPayableLineAssetAccounts().add(newAccount);
            }
        }
    }

    /**
     * Set line item total cost and unit cost.
     *
     * @param item
     */
    protected void setLineItemCost(PurchasingAccountsPayableItemAsset item) {
        KualiDecimal totalCost = calculateItemAssetTotalCost(item);
        item.setTotalCost(totalCost);
        setItemAssetUnitCost(item, totalCost);
    }

    /**
     * Allocate one account line to target account lines percentage based on the account line amount.
     *
     * @param sourceAccount  Account line to be allocated.
     * @param targetAccounts Account lines which accept amount.
     */
    protected void allocateByItemAccountAmount(PurchasingAccountsPayableLineAssetAccount sourceAccount, List<PurchasingAccountsPayableLineAssetAccount> targetAccounts, List<PurchasingAccountsPayableLineAssetAccount> newAccountList, List<PurchasingAccountsPayableActionHistory> actionsTakeHistory) {
        KualiDecimal targetAccountsTotalAmount = KualiDecimal.ZERO;
        KualiDecimal sourceAccountTotalAmount = sourceAccount.getItemAccountTotalAmount();
        KualiDecimal amountAllocated = KualiDecimal.ZERO;
        KualiDecimal additionalAmount = null;

        // Calculate the targetAccountTotalAmount. Ignore the sign of the account amount when proportionally allocate based on
        // account amount
        for (PurchasingAccountsPayableLineAssetAccount targetAccount : targetAccounts) {
            targetAccountsTotalAmount = targetAccountsTotalAmount.add(targetAccount.getItemAccountTotalAmount().abs());
        }

        for (Iterator<PurchasingAccountsPayableLineAssetAccount> iterator = targetAccounts.iterator(); iterator.hasNext(); ) {
            PurchasingAccountsPayableLineAssetAccount targetAccount = iterator.next();
            if (iterator.hasNext()) {
                // Not working on the last node. Calculate additional charge amount by percentage. Ignore the sign of the account
                // amount when proportionally allocate based on account amount
                additionalAmount = targetAccount.getItemAccountTotalAmount().abs().multiply(sourceAccountTotalAmount).divide(targetAccountsTotalAmount);
                amountAllocated = amountAllocated.add(additionalAmount);
            } else {
                // Working on the last node, set the additional charge amount to the rest of sourceAccountTotalAmount.
                additionalAmount = sourceAccountTotalAmount.subtract(amountAllocated);
            }

            // Code below mainly handle grouping account lines if they're from the same GL.
            PurchasingAccountsPayableLineAssetAccount newAccount = getMatchingFromAccountList(targetAccounts, sourceAccount.getGeneralLedgerAccountIdentifier(), targetAccount);
            if (newAccount != null) {
                // If exists the same account line and GL entry, update its itemAccountTotalAmount. This account line could be other
                // than targetAccount, but must belong to the same line item.
                updateAccountAmount(additionalAmount, newAccount);
            } else {
                // If exist account just created, grouping them and update the account amount.
                newAccount = getMatchingFromAccountList(newAccountList, sourceAccount.getGeneralLedgerAccountIdentifier(), targetAccount);
                if (newAccount != null) {
                    updateAccountAmount(additionalAmount, newAccount);
                } else {
                    // If the target account is a different GL entry, create a new account and attach to this line item.
                    newAccount = new PurchasingAccountsPayableLineAssetAccount(targetAccount.getPurchasingAccountsPayableItemAsset(), sourceAccount.getGeneralLedgerAccountIdentifier());
                    newAccount.setItemAccountTotalAmount(additionalAmount);
                    newAccount.setGeneralLedgerEntry(sourceAccount.getGeneralLedgerEntry());
                    newAccountList.add(newAccount);
                }
            }

            // add to action history
            addAllocateHistory(sourceAccount, actionsTakeHistory, additionalAmount, newAccount);
        }
    }

    /**
     * Save allocate action into session object.
     *
     * @param sourceAccount
     * @param actionsTakeHistory
     * @param additionalAmount
     * @param newAccount
     */
    protected void addAllocateHistory(PurchasingAccountsPayableLineAssetAccount sourceAccount, List<PurchasingAccountsPayableActionHistory> actionsTakeHistory, KualiDecimal additionalAmount, PurchasingAccountsPayableLineAssetAccount newAccount) {
        PurchasingAccountsPayableActionHistory newAction = new PurchasingAccountsPayableActionHistory(sourceAccount.getPurchasingAccountsPayableItemAsset(), newAccount.getPurchasingAccountsPayableItemAsset(), CabConstants.Actions.ALLOCATE);
        newAction.setGeneralLedgerAccountIdentifier(sourceAccount.getGeneralLedgerAccountIdentifier());
        newAction.setItemAccountTotalAmount(additionalAmount);
        newAction.setAccountsPayableItemQuantity(sourceAccount.getPurchasingAccountsPayableItemAsset().getAccountsPayableItemQuantity());
        actionsTakeHistory.add(newAction);
    }


    /**
     * Search matching account in targetAccounts by glIdentifier.
     *
     * @param targetAccounts
     * @param glIdentifier
     * @return
     */
    protected PurchasingAccountsPayableLineAssetAccount getFromTargetAccountList(List<PurchasingAccountsPayableLineAssetAccount> targetAccounts, Long glIdentifier) {
        for (PurchasingAccountsPayableLineAssetAccount account : targetAccounts) {
            if (account.getGeneralLedgerAccountIdentifier().equals(glIdentifier)) {
                return account;
            }
        }
        return null;
    }

    /**
     * Update targetAccount by additionalAmount.
     *
     * @param additionalAmount
     * @param targetAccount
     */
    protected void updateAccountAmount(KualiDecimal additionalAmount, PurchasingAccountsPayableLineAssetAccount targetAccount) {
        KualiDecimal baseAmount = targetAccount.getItemAccountTotalAmount();
        targetAccount.setItemAccountTotalAmount(baseAmount != null ? baseAmount.add(additionalAmount) : additionalAmount);
    }


    /**
     * Searching in accountList by glIdentifier for matching account which associated with the same item as targetAccount.
     *
     * @param accountList
     * @param glIdentifier
     * @param targetAccount
     * @return
     */
    protected PurchasingAccountsPayableLineAssetAccount getMatchingFromAccountList(List<PurchasingAccountsPayableLineAssetAccount> accountList, Long glIdentifier, PurchasingAccountsPayableLineAssetAccount targetAccount) {
        for (PurchasingAccountsPayableLineAssetAccount account : accountList) {
            if (StringUtils.equalsIgnoreCase(targetAccount.getDocumentNumber(), account.getDocumentNumber()) && targetAccount.getAccountsPayableLineItemIdentifier().equals(account.getAccountsPayableLineItemIdentifier()) && targetAccount.getCapitalAssetBuilderLineNumber().equals(account.getCapitalAssetBuilderLineNumber()) && glIdentifier.equals(account.getGeneralLedgerAccountIdentifier())) {
                return account;
            }
        }
        return null;
    }

    /**
     * Get the target account lines which will be used for allocate.
     *
     * @param sourceAccount
     * @param lineItems
     * @param addtionalCharge
     * @return
     */
    protected List<PurchasingAccountsPayableLineAssetAccount> getAllocateTargetAccounts(PurchasingAccountsPayableLineAssetAccount sourceAccount, List<PurchasingAccountsPayableItemAsset> allocateTargetLines, boolean addtionalCharge) {
        GeneralLedgerEntry candidateEntry = null;
        GeneralLedgerEntry sourceEntry = sourceAccount.getGeneralLedgerEntry();
        List<PurchasingAccountsPayableLineAssetAccount> matchingAccounts = new ArrayList<PurchasingAccountsPayableLineAssetAccount>();
        List<PurchasingAccountsPayableLineAssetAccount> allAccounts = new ArrayList<PurchasingAccountsPayableLineAssetAccount>();

        // For additional charge allocation, target account selection is based on account lines with the same account number and
        // object code. If no matching, select all account lines. For line item to line items, select all account lines as target.
        for (PurchasingAccountsPayableItemAsset item : allocateTargetLines) {
            for (PurchasingAccountsPayableLineAssetAccount account : item.getPurchasingAccountsPayableLineAssetAccounts()) {
                //KFSMI-5122: We need to refresh account general ledger entry so that the gl entries become visible as candidateEntry
                account.refreshReferenceObject("generalLedgerEntry");
                candidateEntry = account.getGeneralLedgerEntry();

                if (ObjectUtils.isNotNull(candidateEntry)) {
                    // For additional charge, select matching account when account number and object code both match.
                    if (addtionalCharge && StringUtils.equalsIgnoreCase(sourceEntry.getChartOfAccountsCode(), candidateEntry.getChartOfAccountsCode()) && StringUtils.equalsIgnoreCase(sourceEntry.getAccountNumber(), candidateEntry.getAccountNumber()) && StringUtils.equalsIgnoreCase(sourceEntry.getFinancialObjectCode(), candidateEntry.getFinancialObjectCode())) {
                        matchingAccounts.add(account);
                    }
                }

                allAccounts.add(account);
            }
        }

        return matchingAccounts.isEmpty() ? allAccounts : matchingAccounts;
    }

    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#getAllocateTargetLines(org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset,
     * java.util.List)
     */
    @Override
    public List<PurchasingAccountsPayableItemAsset> getAllocateTargetLines(PurchasingAccountsPayableItemAsset selectedLineItem, List<PurchasingAccountsPayableDocument> purApDocs) {
        List<PurchasingAccountsPayableItemAsset> targetLineItems = new ArrayList<PurchasingAccountsPayableItemAsset>();

        for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
            for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
                // If selected Line is additional charge line, get target lines from the same document.
                // Else if selected Line is trade in allowance, target lines are item lines with TI indicator set.
                // Otherwise, select items with check box set.
                if (item.isActive() && item != selectedLineItem && ((selectedLineItem.isAdditionalChargeNonTradeInIndicator() && !item.isAdditionalChargeNonTradeInIndicator() && !item.isTradeInAllowance() && StringUtils.equalsIgnoreCase(selectedLineItem.getDocumentNumber(), item.getDocumentNumber())) || (selectedLineItem.isTradeInAllowance() && item.isItemAssignedToTradeInIndicator()) || (item.isSelectedValue()))) {
                    targetLineItems.add(item);
                }
            }
        }
        return targetLineItems;
    }

    /**
     * Get selected merge lines. If this is merge all action, we need to manually append all additional charge lines since no select
     * box associated with them.
     *
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#getSelectedMergeLines(boolean, java.util.List)
     */
    @Override
    public List<PurchasingAccountsPayableItemAsset> getSelectedMergeLines(boolean isMergeAll, List<PurchasingAccountsPayableDocument> purApDocs) {
        List<PurchasingAccountsPayableItemAsset> mergeLines = new ArrayList<PurchasingAccountsPayableItemAsset>();
        boolean excludeTradeInAllowance = false;

        // Handle one exception for merge all: when we have TI allowance but no TI indicator line, we should exclude
        if (isMergeAll && !isTradeInIndicatorExistInAllLines(purApDocs) && isTradeInAllowanceExist(purApDocs)) {
            excludeTradeInAllowance = true;
        }

        for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
            for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
                // If not merge all action, select items are the merge lines. If it is merge all action, all lines should be
                // candidate merge lines except trade-in allowance line when there is no trade-in indicator line.
                if ((!isMergeAll && item.isSelectedValue()) || (isMergeAll && (!excludeTradeInAllowance || !item.isTradeInAllowance()))) {
                    mergeLines.add(item);
                    // setup non-persistent relationship from item to document.
                    item.setPurchasingAccountsPayableDocument(purApDoc);
                }
            }
        }
        return mergeLines;
    }

    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isTradeInAllowanceExist(java.util.List)
     */
    @Override
    public boolean isTradeInAllowanceExist(List<PurchasingAccountsPayableDocument> purApDocs) {
        boolean tradeInAllowance = false;
        for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
            for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
                // KFSCNTRB-1676/FSKD-5487
                if (item.isTradeInAllowance() && item.isActive()) {
                    tradeInAllowance = true;
                    break;
                }
            }
        }
        return tradeInAllowance;
    }

    /**
     * Check if TI indicator exists in all form lines
     *
     * @param purApDocs
     * @return
     */
    protected boolean isTradeInIndicatorExistInAllLines(List<PurchasingAccountsPayableDocument> purApDocs) {
        boolean tradeInIndicator = false;
        for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
            for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
                // KFSCNTRB-1676/FSKD-5487
                if (item.isItemAssignedToTradeInIndicator() && item.isActive()) {
                    tradeInIndicator = true;
                    break;
                }
            }
        }
        return tradeInIndicator;
    }

    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isTradeInIndicatorExist(java.util.List)
     */
    @Override
    public boolean isTradeInIndExistInSelectedLines(List<PurchasingAccountsPayableItemAsset> itemAssets) {
        boolean tradeInIndicator = false;
        for (PurchasingAccountsPayableItemAsset item : itemAssets) {
            if (item.isItemAssignedToTradeInIndicator()) {
                tradeInIndicator = true;
                break;
            }
        }
        return tradeInIndicator;
    }

    /**
     * If item assets are from the same document, we can ignore additional charges pending.
     *
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isAdditionalChargePending(java.util.List)
     */
    @Override
    public boolean isAdditionalChargePending(List<PurchasingAccountsPayableItemAsset> itemAssets) {
        boolean additionalChargePending = false;
        Boolean diffDocment = false;
        PurchasingAccountsPayableItemAsset firstAsset = itemAssets.get(0);
        PurchasingAccountsPayableItemAsset lastAsset = itemAssets.get(itemAssets.size() - 1);

        // Check if itemAssets are in different PurAp Document. itemAssets is a sorted list which has item assets from the same
        // document grouping together.
        if (ObjectUtils.isNotNull(firstAsset) && ObjectUtils.isNotNull(lastAsset) && !firstAsset.getDocumentNumber().equalsIgnoreCase(lastAsset.getDocumentNumber())) {
            diffDocment = true;
        }

        // check if item assets from different document have additional charges not allocated yet. Bring all lines in
        // the same document as checking candidate.
        if (diffDocment) {
            for (PurchasingAccountsPayableItemAsset item : itemAssets) {
                if (ObjectUtils.isNotNull(item.getPurchasingAccountsPayableDocument())) {
                    for (PurchasingAccountsPayableItemAsset itemLine : item.getPurchasingAccountsPayableDocument().getPurchasingAccountsPayableItemAssets()) {
                        if (itemLine.isAdditionalChargeNonTradeInIndicator()) {
                            additionalChargePending = true;
                            return additionalChargePending;
                        }
                    }
                }
            }
        }
        return additionalChargePending;
    }

    /**
     * Check if the merge action is merge all.
     *
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isMergeAllAction(java.util.List)
     */
    @Override
    public boolean isMergeAllAction(List<PurchasingAccountsPayableDocument> purApDocs) {
        for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
            for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
                // When there is one item line not selected, mergeAll is false
                if (!item.isAdditionalChargeNonTradeInIndicator() && !item.isTradeInAllowance() && !item.isSelectedValue()) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isAdditionalChargeExistInAllLines(java.util.List)
     */
    @Override
    public boolean isAdditionalChargeExistInAllLines(List<PurchasingAccountsPayableDocument> purApDocs) {
        for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
            for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
                // KFSCNTRB-1676/FSKD-5487
                if (item.isAdditionalChargeNonTradeInIndicator() && item.isActive()) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#processMerge(java.util.List)
     */
    @Override
    public void processMerge(List<PurchasingAccountsPayableItemAsset> mergeLines, List<PurchasingAccountsPayableActionHistory> actionsTakeHistory, boolean isMergeAll) {
        PurchasingAccountsPayableItemAsset sourceItem = null;
        PurchasingAccountsPayableLineAssetAccount targetAccount = null;
        // use the first item and its accounts as the target
        PurchasingAccountsPayableItemAsset firstItem = mergeLines.get(0);
        List<PurchasingAccountsPayableLineAssetAccount> firstAccountList = firstItem.getPurchasingAccountsPayableLineAssetAccounts();

        // Merge accounts starting from the second item to the last one.
        for (int i = 1; i < mergeLines.size(); i++) {
            sourceItem = mergeLines.get(i);
            for (PurchasingAccountsPayableLineAssetAccount account : sourceItem.getPurchasingAccountsPayableLineAssetAccounts()) {
                // Check if we can grouping the accounts. If yes, update the account amount without moving account line.
                targetAccount = getFromTargetAccountList(firstAccountList, account.getGeneralLedgerAccountIdentifier());
                if (targetAccount != null) {
                    updateAccountAmount(account.getItemAccountTotalAmount(), targetAccount);
                } else {
                    // move account from source to target
                    account.setDocumentNumber(firstItem.getDocumentNumber());
                    account.setAccountsPayableLineItemIdentifier(firstItem.getAccountsPayableLineItemIdentifier());
                    account.setCapitalAssetBuilderLineNumber(firstItem.getCapitalAssetBuilderLineNumber());
                    account.setPurchasingAccountsPayableItemAsset(firstItem);
                    firstAccountList.add(account);
                }
            }
        }

        // update action history, remove lines before merge and clean up the user input
        postMergeProcess(mergeLines, actionsTakeHistory, isMergeAll);
    }

    /**
     * Process after merge.
     *
     * @param mergeLines
     * @param purApLineSession
     * @param isMergeAll
     */
    protected void postMergeProcess(List<PurchasingAccountsPayableItemAsset> mergeLines, List<PurchasingAccountsPayableActionHistory> actionsTakeHistory, boolean isMergeAll) {
        String actionTypeCode = isMergeAll ? CabConstants.Actions.MERGE_ALL : CabConstants.Actions.MERGE;
        PurchasingAccountsPayableItemAsset targetItem = mergeLines.get(0);

        // set unit cost and total cost
        setLineItemCost(targetItem);
        targetItem.setItemAssignedToTradeInIndicator(false);

        // For all merge source lines(the first line is considered as target technically), remove it from the document and update in
        // the action history.
        for (int i = 1; i < mergeLines.size(); i++) {
            PurchasingAccountsPayableItemAsset sourceItem = mergeLines.get(i);

            // Update the action history.
            addMergeHistory(actionsTakeHistory, actionTypeCode, sourceItem, targetItem);

            if (ObjectUtils.isNotNull(sourceItem.getPurchasingAccountsPayableDocument())) {
                // remove mergeLines from the document
                sourceItem.getPurchasingAccountsPayableDocument().getPurchasingAccountsPayableItemAssets().remove(sourceItem);
                // if all active lines are merged to other line, we need to in-activate the current document
                conditionallyUpdateDocumentStatusAsProcessed(sourceItem.getPurchasingAccountsPayableDocument());
            }
        }
        // set the target item itemLineNumber for pre-tagging
        Integer poId = targetItem.getPurchasingAccountsPayableDocument().getPurchaseOrderIdentifier();
        if (poId != null) {
            Pretag targetPretag = getTargetPretag(mergeLines, poId);
            if (targetPretag != null) {
                targetItem.setItemLineNumber(targetPretag.getItemLineNumber());
            }
        }

        // update create asset/ apply payment indicator if any of the merged lines has the indicator set.
        updateAssetIndicatorAfterMerge(mergeLines);

        // update activity status code as modified
        updateItemStatusAsUserModified(targetItem);
    }

    /**
     * Update create asset and apply payment indicators after merge.
     *
     * @param mergeLines
     */
    protected void updateAssetIndicatorAfterMerge(List<PurchasingAccountsPayableItemAsset> mergeLines) {
        boolean existCreateAsset = false;
        boolean existApplyPayment = false;
        PurchasingAccountsPayableItemAsset targetItem = mergeLines.get(0);
        // set indicator if any of the source item set it or target item set it.
        for (int i = 1; i < mergeLines.size(); i++) {
            PurchasingAccountsPayableItemAsset sourceItem = mergeLines.get(i);
            existCreateAsset |= sourceItem.isCreateAssetIndicator();
            existApplyPayment |= sourceItem.isApplyPaymentIndicator();
        }
        targetItem.setCreateAssetIndicator(targetItem.isCreateAssetIndicator() | existCreateAsset);
        targetItem.setApplyPaymentIndicator(targetItem.isApplyPaymentIndicator() | existApplyPayment);
    }

    /**
     * Get the first pre-tag for given itemLines
     *
     * @param itemLines
     * @param purchaseOrderIdentifier
     * @return
     */
    protected Pretag getTargetPretag(List<PurchasingAccountsPayableItemAsset> itemLines, Integer purchaseOrderIdentifier) {
        for (PurchasingAccountsPayableItemAsset item : itemLines) {
            Pretag newTag = getPreTagLineItem(purchaseOrderIdentifier, item.getItemLineNumber());

            if (isPretaggingExisting(newTag)) {
                return newTag;
            }
        }

        return null;
    }

    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isPretaggingExisting(org.kuali.kfs.module.cab.businessobject.Pretag)
     */
    @Override
    public boolean isPretaggingExisting(Pretag newTag) {
        return ObjectUtils.isNotNull(newTag) && newTag.getPretagDetails() != null && !newTag.getPretagDetails().isEmpty();
    }


    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#isMultipleTagExisting(java.lang.Integer, java.util.Set)
     */
    @Override
    public boolean isMultipleTagExisting(Integer purchaseOrderIdentifier, Set<Integer> itemLineNumbers) {
        Pretag firstTag = null;
        for (Iterator iterator = itemLineNumbers.iterator(); iterator.hasNext(); ) {
            Integer itemLineNumber = (Integer) iterator.next();
            Pretag newTag = getPreTagLineItem(purchaseOrderIdentifier, itemLineNumber);

            if (isPretaggingExisting(newTag)) {
                if (firstTag != null) {
                    // find the second preTagging item
                    return true;
                } else {
                    firstTag = newTag;
                }
            }
        }

        return false;
    }

    /**
     * Add merge action to the action history.
     *
     * @param purApLineSession
     * @param isMergeAllAction
     * @param firstItem
     * @param item
     */
    protected void addMergeHistory(List<PurchasingAccountsPayableActionHistory> actionsTakenHistory, String actionTypeCode, PurchasingAccountsPayableItemAsset sourceItem, PurchasingAccountsPayableItemAsset targetItem) {
        // create action history records for each account from the source lines.
        for (PurchasingAccountsPayableLineAssetAccount sourceAccount : sourceItem.getPurchasingAccountsPayableLineAssetAccounts()) {
            PurchasingAccountsPayableActionHistory newAction = new PurchasingAccountsPayableActionHistory(sourceItem, targetItem, actionTypeCode);
            newAction.setAccountsPayableItemQuantity(sourceItem.getAccountsPayableItemQuantity());
            newAction.setItemAccountTotalAmount(sourceAccount.getItemAccountTotalAmount());
            newAction.setGeneralLedgerAccountIdentifier(sourceAccount.getGeneralLedgerAccountIdentifier());
            actionsTakenHistory.add(newAction);
        }
    }


    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#processPercentPayment(org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset)
     */
    @Override
    public void processPercentPayment(PurchasingAccountsPayableItemAsset itemAsset, List<PurchasingAccountsPayableActionHistory> actionsTakenHistory) {
        KualiDecimal oldQty = itemAsset.getAccountsPayableItemQuantity();
        KualiDecimal newQty = new KualiDecimal(1);
        // update quantity, total cost and unit cost.
        if (oldQty.isLessThan(newQty)) {
            itemAsset.setAccountsPayableItemQuantity(newQty);
            setLineItemCost(itemAsset);
            // add to action history
            addPercentPaymentHistory(actionsTakenHistory, itemAsset, oldQty);
            // update status code
            updateItemStatusAsUserModified(itemAsset);
        }
    }

    /**
     * Updates activity status code when percent payment/split/allocate/merge action taken.
     *
     * @param itemAsset itemAsset for which action status is to be modified
     */
    protected void updateItemStatusAsUserModified(PurchasingAccountsPayableItemAsset itemAsset) {
        itemAsset.setActivityStatusCode(CabConstants.ActivityStatusCode.MODIFIED);

        for (PurchasingAccountsPayableLineAssetAccount account : itemAsset.getPurchasingAccountsPayableLineAssetAccounts()) {
            account.setActivityStatusCode(CabConstants.ActivityStatusCode.MODIFIED);
        }

        itemAsset.getPurchasingAccountsPayableDocument().setActivityStatusCode(CabConstants.ActivityStatusCode.MODIFIED);

    }

    /**
     * Update action history for the percent payment action.
     *
     * @param actionsTaken
     * @param item
     * @param oldQty
     */
    protected void addPercentPaymentHistory(List<PurchasingAccountsPayableActionHistory> actionsTakenHistory, PurchasingAccountsPayableItemAsset item, KualiDecimal oldQty) {
        // create and set up one action history record for this action
        PurchasingAccountsPayableActionHistory newAction = new PurchasingAccountsPayableActionHistory(item, item, CabConstants.Actions.PERCENT_PAYMENT);
        // record quantity before percent payment into action history
        newAction.setAccountsPayableItemQuantity(oldQty);
        actionsTakenHistory.add(newAction);
    }

    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#processSplit(org.kuali.kfs.module.cab.businessobject.PurchasingAccountsPayableItemAsset)
     */
    @Override
    public void processSplit(PurchasingAccountsPayableItemAsset splitItemAsset, List<PurchasingAccountsPayableActionHistory> actionsTakeHistory) {
        PurchasingAccountsPayableDocument purApDoc = splitItemAsset.getPurchasingAccountsPayableDocument();
        // update activity status code for split item. it will be propogated to new created item and its accounts.
        updateItemStatusAsUserModified(splitItemAsset);

        // create a new item asset from the current item asset.
        PurchasingAccountsPayableItemAsset newItemAsset = new PurchasingAccountsPayableItemAsset(splitItemAsset);

        // set cab line number
        newItemAsset.setCapitalAssetBuilderLineNumber(getMaxCabLineNumber(splitItemAsset, purApDoc) + 1);

        newItemAsset.setAccountsPayableItemQuantity(splitItemAsset.getSplitQty());

        // Set account list for new item asset and update current account amount value.
        createAccountsForNewItemAsset(splitItemAsset, newItemAsset);
        // set unit cost and total cost in new item
        setLineItemCost(newItemAsset);

        // Adjust current item asset quantity, total cost and unit cost
        splitItemAsset.setAccountsPayableItemQuantity(splitItemAsset.getAccountsPayableItemQuantity().subtract(splitItemAsset.getSplitQty()));
        setLineItemCost(splitItemAsset);

        // add the new item into document and sort.
        purApDoc.getPurchasingAccountsPayableItemAssets().add(newItemAsset);
        Collections.sort(purApDoc.getPurchasingAccountsPayableItemAssets());

        // Add to action history
        addSplitHistory(splitItemAsset, newItemAsset, actionsTakeHistory);

        // clear up user input
        splitItemAsset.setSplitQty(null);
    }


    /**
     * Get the max cab line #. As part of the primary key, it should be the max value among the form item list and DB.
     *
     * @param splitItemAsset
     * @param purApDoc
     * @return
     */
    protected int getMaxCabLineNumber(PurchasingAccountsPayableItemAsset splitItemAsset, PurchasingAccountsPayableDocument purApDoc) {
        // get the max CAB line number in DB.
        Integer maxDBCabLineNbr = purApLineDao.getMaxCabLineNumber(splitItemAsset.getDocumentNumber(), splitItemAsset.getAccountsPayableLineItemIdentifier());
        // get the max CAB line number in form.
        int availableCabLineNbr = getMaxCabLineNbrForItemInForm(purApDoc, splitItemAsset);

        if (maxDBCabLineNbr.intValue() > availableCabLineNbr) {
            availableCabLineNbr = maxDBCabLineNbr.intValue();
        }
        return availableCabLineNbr;
    }

    /**
     * Search the current active items and return the max CAB line # for split new item .
     *
     * @param purApDoc
     * @param currentItemAsset
     * @return
     */
    protected int getMaxCabLineNbrForItemInForm(PurchasingAccountsPayableDocument purApDoc, PurchasingAccountsPayableItemAsset currentItemAsset) {
        int maxCabLineNbr = 0;
        for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {
            if (item.getDocumentNumber().equalsIgnoreCase(currentItemAsset.getDocumentNumber()) && item.getAccountsPayableLineItemIdentifier().equals(currentItemAsset.getAccountsPayableLineItemIdentifier()) && item.getCapitalAssetBuilderLineNumber().intValue() > maxCabLineNbr) {
                maxCabLineNbr = item.getCapitalAssetBuilderLineNumber().intValue();
            }

        }
        return maxCabLineNbr;
    }

    /**
     * Update action history for a split action.
     *
     * @param currentItemAsset
     * @param newItemAsset
     * @param actionsTaken
     */
    protected void addSplitHistory(PurchasingAccountsPayableItemAsset currentItemAsset, PurchasingAccountsPayableItemAsset newItemAsset, List<PurchasingAccountsPayableActionHistory> actionsTakenHistory) {
        // for each account moved from original item to new item, create one action history record
        for (PurchasingAccountsPayableLineAssetAccount account : newItemAsset.getPurchasingAccountsPayableLineAssetAccounts()) {
            PurchasingAccountsPayableActionHistory newAction = new PurchasingAccountsPayableActionHistory(currentItemAsset, newItemAsset, CabConstants.Actions.SPLIT);
            newAction.setGeneralLedgerAccountIdentifier(account.getGeneralLedgerAccountIdentifier());
            // quantity moved from original item to new item
            newAction.setAccountsPayableItemQuantity(newItemAsset.getAccountsPayableItemQuantity());
            // account amount moved to new item
            newAction.setItemAccountTotalAmount(account.getItemAccountTotalAmount());
            actionsTakenHistory.add(newAction);
        }
    }


    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#processSaveBusinessObjects(java.util.List,
     * org.kuali.kfs.module.cab.document.web.PurApLineSession)
     */
    @Override
    public void processSaveBusinessObjects(List<PurchasingAccountsPayableDocument> purApDocs, PurApLineSession purApLineSession) {
        // Get removable asset locks which could be generated by allocate or merge when items removed and the lock should be
        // released.
        Map<String, Set> removableAssetLocks = getRemovableAssetLocks(purApLineSession.getProcessedItems());
        for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
            // auto save items(including deleted items) and accounts due to auto-update setting in OJB.
            businessObjectService.save(purApDoc);
        }
        // remove asset locks
        for (String lockingDocumentNbr : removableAssetLocks.keySet()) {
            Set<String> lockingInfoList = removableAssetLocks.get(lockingDocumentNbr);
            for (String lockingInfo : lockingInfoList) {
                if (this.getCapitalAssetManagementModuleService().isAssetLockedByCurrentDocument(lockingDocumentNbr, lockingInfo)) {
                    this.getCapitalAssetManagementModuleService().deleteAssetLocks(lockingDocumentNbr, lockingInfo);
                }
            }
        }

        if (purApLineSession != null) {
            // save to action history table
            List<PurchasingAccountsPayableActionHistory> historyList = purApLineSession.getActionsTakenHistory();
            if (historyList != null && !historyList.isEmpty()) {
                businessObjectService.save(historyList);
                historyList.clear();
            }

            // save to generalLedgerEntry table
            List<GeneralLedgerEntry> glUpdateList = purApLineSession.getGlEntryUpdateList();
            if (glUpdateList != null && !glUpdateList.isEmpty()) {
                businessObjectService.save(glUpdateList);
                glUpdateList.clear();
            }

        }
    }


    /**
     * Create asset account list for new item asset and update the current account amount.
     *
     * @param oldItemAsset old line item.
     * @param newItemAsset new line item.
     */
    protected void createAccountsForNewItemAsset(PurchasingAccountsPayableItemAsset currentItemAsset, PurchasingAccountsPayableItemAsset newItemAsset) {
        KualiDecimal currentQty = currentItemAsset.getAccountsPayableItemQuantity();
        KualiDecimal splitQty = currentItemAsset.getSplitQty();
        List<PurchasingAccountsPayableLineAssetAccount> newAccountsList = newItemAsset.getPurchasingAccountsPayableLineAssetAccounts();
        PurchasingAccountsPayableLineAssetAccount newAccount;
        for (PurchasingAccountsPayableLineAssetAccount currentAccount : currentItemAsset.getPurchasingAccountsPayableLineAssetAccounts()) {
            // create accounts for new item asset.
            newAccount = new PurchasingAccountsPayableLineAssetAccount(newItemAsset, currentAccount.getGeneralLedgerAccountIdentifier());
            newAccount.setItemAccountTotalAmount(currentAccount.getItemAccountTotalAmount().multiply(splitQty).divide(currentQty));
            newAccount.setGeneralLedgerEntry(currentAccount.getGeneralLedgerEntry());
            newAccountsList.add(newAccount);

            // Adjust current account amount for split item( subtract new account amount for original amount)
            currentAccount.setItemAccountTotalAmount(currentAccount.getItemAccountTotalAmount().subtract(newAccount.getItemAccountTotalAmount()));
        }
    }


    /**
     * Set object code by the first one from the accounting lines.
     *
     * @param item Selected line item.
     */
    protected void setFirstFinancialObjectCode(PurchasingAccountsPayableItemAsset item) {
        String firstFinancialObjectCode = null;
        for (PurchasingAccountsPayableLineAssetAccount account : item.getPurchasingAccountsPayableLineAssetAccounts()) {
            if (ObjectUtils.isNotNull(account.getGeneralLedgerEntry())) {
                firstFinancialObjectCode = account.getGeneralLedgerEntry().getFinancialObjectCode();
                break;
            }
        }
        item.setFirstFincialObjectCode(firstFinancialObjectCode);
    }


    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#buildPurApItemAssetList(java.util.List)
     */
    @Override
    public void buildPurApItemAssetList(List<PurchasingAccountsPayableDocument> purApDocs) {
        for (PurchasingAccountsPayableDocument purApDoc : purApDocs) {
            for (PurchasingAccountsPayableItemAsset item : purApDoc.getPurchasingAccountsPayableItemAssets()) {

                // set item non-persistent fields from PurAp PREQ/CM item tables
                purApInfoService.setAccountsPayableItemsFromPurAp(item, purApDoc.getDocumentTypeCode());

                // set line item unit cost and total cost
                setLineItemCost(item);

                // set financial object code
                setFirstFinancialObjectCode(item);

                // KFSMI-5337
                // Adding the following code to populate item description from PreAsset tagging
                updateAssetDescriptionFromPreTag(item, purApDoc);
            }
            // For display purpose, move additional charges including trade-in below item lines.
            Collections.sort(purApDoc.getPurchasingAccountsPayableItemAssets());
        }

        // set CAMS Transaction type from PurAp
        purApInfoService.setCamsTransactionFromPurAp(purApDocs);

        // set create asset/apply payment indicator which are used to control display two buttons.
        setAssetIndicator(purApDocs);
    }

    private void updateAssetDescriptionFromPreTag(PurchasingAccountsPayableItemAsset item, PurchasingAccountsPayableDocument purApDoc) {
        Pretag preTag = null;
        if (item.isActive()) {
            preTag = getPreTagLineItem(purApDoc.getPurchaseOrderIdentifier(), item.getItemLineNumber());
            if (ObjectUtils.isNotNull(preTag) && StringUtils.isNotBlank(preTag.getAssetTopsDescription())) {
                item.setAccountsPayableLineItemDescription(preTag.getAssetTopsDescription());
            }
        }
    }


    /**
     * @see org.kuali.kfs.module.cab.document.service.PurApLineService#getPreTagLineItem(java.lang.String, java.lang.Integer)
     */
    @Override
    public Pretag getPreTagLineItem(Integer purchaseOrderIdentifier, Integer lineItemNumber) {

        if (purchaseOrderIdentifier == null || lineItemNumber == null) {
            return null;
        }

        Map<String, Object> pKeys = new HashMap<String, Object>();

        pKeys.put(CabPropertyConstants.Pretag.PURCHASE_ORDER_NUMBER, purchaseOrderIdentifier);
        pKeys.put(CabPropertyConstants.Pretag.ITEM_LINE_NUMBER, lineItemNumber);
        return businessObjectService.findByPrimaryKey(Pretag.class, pKeys);
    }

    /**
     * KFSCNTRB-1676/FSKD-5487
     * Sets create asset and apply payment indicators. These two indicators are referenced by jsp to control display of these two
     * action links. How to set these two indicators is based on the business rules. We need to put the following situations into
     * consideration. Since we move allocate additional charge allocation to CAB batch, we bring over additional charge lines only
     * when they are the only items in the document, or they are trade-in allowances, or they are from cancelled AP docs or FO changes.
     * To accommodate this, we relax the rules and defined as:
     * 1. Throughout the AP document list, if there're both unallocated TRDI additional charges and active trade-in ITEM lines,
     * then disable the process action links on these lines across the document list;
     * 2. Within each AP document in the list, if there're both unallocated non-TRDI additional charges and active ITEM lines,
     * then disable the process action links on all lines in this document;
     * 3. Except for above cases, display action links for other lines.
     * NOTE: Above, AP document list refer to all active PREQs or CMs extracted into CAB for the same PO; they are processed on the same CAB screen.
     *
     * @param apDocs AP document list containing all active PREQs or CMs extracted into CAB for the same PO.
     */
    protected void setAssetIndicator(List<PurchasingAccountsPayableDocument> apDocs) {
        /* For un-allocated TRDI and active trade-in ITEM lines, we need to check throughout the entire active AP documents list,
         * because a TRDI line can only be allocated to trade-in ITEM lines, which could exist in other AP documents in the list.
         * Consider the following scenario: some PREQ with only trade-in ITEMs but no TRDI lines was extracted and submitted first
         * and become inactive; later another PREQ with only TRDI lines but no trade-in ITEMs for the same PO are created and extracted.
         * At this point, the later PREQ would be stuck if we force allocation, because the TRDI lines have no target trade-in ITEM lines
         * to allocate to, while the ITEM lines are waiting for allocation before they can be processed. To resolve this issue,
         * first of all, we should prevent trade-in ITEMs to be submitted before TRDI lines are allocated across all active documents;
         * further more, if the undesirable scenario above happens, we have to relax the rule of forcing allocation, i.e.
         * in order to proceed, we need to allow both the TRDI and the ITEM lines to be processed without allocation.
         */
        boolean existUnallocatedAdditionalTRDI = existUnallocatedAdditionalTRDI(apDocs);
        boolean existActiveItemTradeIn = existActiveItemTradeIn(apDocs);

        for (PurchasingAccountsPayableDocument apDoc : apDocs) {
            /* For non-TRDI additional charges pending allocation, we should check within each document instead of across the document list,
             * because non-TRDI can allocate to any active ITEM within the same document, no need to worry about the stuck scenario.
             * Meanwhile we don't want to prevent one document from processing just because some other document has non-TRDI pending allocation.
             */
            boolean existUnallocatedAdditionalNonTRDI = existUnallocatedAdditionalNonTRDI(apDoc);
            boolean existActiveItemLines = existActiveItemLines(apDoc);

            // if within the AP doc, there're both unallocated non-TRDI additional charge and active ITEM lines,
            // we need to disable process actions on all lines to force allocation first
            if (existUnallocatedAdditionalNonTRDI && existActiveItemLines) {
                continue;
            }

            // otherwise, enable/disable process actions in the document based on the allocation status of TRDI/trade-in lines across the document list
            for (PurchasingAccountsPayableItemAsset item : apDoc.getPurchasingAccountsPayableItemAssets()) {
                // skip inactive lines
                if (!item.isActive()) {
                    continue;
                }

                // if we are NOT in the situation where there're both unallocated TRDI additional charge and active trade-in ITEM lines
                // throughout the AP document list, then we can enable process actions on all active lines
                if (!(existUnallocatedAdditionalTRDI && existActiveItemTradeIn)) {
                    item.setCreateAssetIndicator(true);
                    item.setApplyPaymentIndicator(true);
                }
                // otherwise, disable process actions only on the unallocated TRDI additional charge and active trade-in ITEM lines,
                // while enable the actions on all other active lines in the AP document.
                // Note: Since allocated lines are removed from the list, all remaining active lines are unallocated.
                else if (!item.isActiveAdditionalTRDI() && !item.isActiveItemTradeIn()) {
                    item.setCreateAssetIndicator(true);
                    item.setApplyPaymentIndicator(true);
                }
            }
        }

    }

    /**
     * KFSCNTRB-1676/FSKD-5487
     * Checks whether there exists any unallocated TRDI additional charge asset line throughout the AP document list.
     *
     * @param apDocs AP document list containing all active PREQs/CMs extracted into CAB for the same PO.
     */
    protected boolean existUnallocatedAdditionalTRDI(List<PurchasingAccountsPayableDocument> apDocs) {
        for (PurchasingAccountsPayableDocument apDoc : apDocs) {
            for (PurchasingAccountsPayableItemAsset item : apDoc.getPurchasingAccountsPayableItemAssets()) {
                // Each time an asset line is allocated it will be removed from the list, so we can assume that all remaining lines in the doc
                // are unallocated; thus we don't need to further distinguish the action status code except that the line is active.
                if (item.isActiveAdditionalTRDI()) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * KFSCNTRB-1676/FSKD-5487
     * Checks whether there exists any unallocated non-TRDI additional charge asset line in the specified AP document.
     *
     * @param apDocs the specified PREQ/CM document.
     */
    protected boolean existUnallocatedAdditionalNonTRDI(PurchasingAccountsPayableDocument apDoc) {
        for (PurchasingAccountsPayableItemAsset item : apDoc.getPurchasingAccountsPayableItemAssets()) {
            // Each time an asset line is allocated it will be removed from the list, so we can assume that all remaining lines in the doc
            // are unallocated; thus we don't need to further distinguish the action status code except that the line is active.
            if (item.isActiveAdditionalNonTRDI()) {
                return true;
            }
        }
        return false;
    }

    /**
     * KFSCNTRB-1676/FSKD-5487
     * Checks whether there exists any active trade-in ITEM asset line throughout the AP document list.
     *
     * @param apDocs AP document list containing all active PREQs/CMs extracted into CAB for the same PO.
     */
    protected boolean existActiveItemTradeIn(List<PurchasingAccountsPayableDocument> apDocs) {
        for (PurchasingAccountsPayableDocument apDoc : apDocs) {
            for (PurchasingAccountsPayableItemAsset item : apDoc.getPurchasingAccountsPayableItemAssets()) {
                if (item.isActiveItemTradeIn()) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * KFSCNTRB-1676/FSKD-5487
     * Checks whether there exists any active ITEM  asset line in the specified AP document.
     *
     * @param apDocs the specified PREQ/CM document.
     */
    protected boolean existActiveItemLines(PurchasingAccountsPayableDocument apDoc) {
        for (PurchasingAccountsPayableItemAsset item : apDoc.getPurchasingAccountsPayableItemAssets()) {
            if (item.isActiveItemLine()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Set item asset unit cost.
     *
     * @param item      line item
     * @param totalCost total cost for this line item.
     */
    protected void setItemAssetUnitCost(PurchasingAccountsPayableItemAsset item, KualiDecimal totalCost) {
        // set unit cost
        KualiDecimal quantity = item.getAccountsPayableItemQuantity();
        if (quantity != null && quantity.isNonZero()) {
            item.setUnitCost(totalCost.divide(quantity));
        }
    }

    /**
     * Calculate item asset total cost
     *
     * @param item
     * @return line item total cost
     */
    public KualiDecimal calculateItemAssetTotalCost(PurchasingAccountsPayableItemAsset item) {
        // Calculate and set total cost
        KualiDecimal totalCost = KualiDecimal.ZERO;
        for (PurchasingAccountsPayableLineAssetAccount account : item.getPurchasingAccountsPayableLineAssetAccounts()) {
            if (account.getItemAccountTotalAmount() != null) {
                totalCost = totalCost.add(account.getItemAccountTotalAmount());
            }
        }
        return totalCost;
    }


    /**
     * Gets the businessObjectService attribute.
     *
     * @return Returns the businessObjectService.
     */
    public BusinessObjectService getBusinessObjectService() {
        return businessObjectService;
    }


    /**
     * Sets the businessObjectService attribute value.
     *
     * @param businessObjectService The businessObjectService to set.
     */
    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }


    /**
     * Gets the purApLineDao attribute.
     *
     * @return Returns the purApLineDao.
     */
    public PurApLineDao getPurApLineDao() {
        return purApLineDao;
    }


    /**
     * Sets the purApLineDao attribute value.
     *
     * @param purApLineDao The purApLineDao to set.
     */
    public void setPurApLineDao(PurApLineDao purApLineDao) {
        this.purApLineDao = purApLineDao;
    }

    /**
     * Gets the purApInfoService attribute.
     *
     * @return Returns the purApInfoService.
     */
    public PurApInfoService getPurApInfoService() {
        return purApInfoService;
    }

    /**
     * Sets the purApInfoService attribute value.
     *
     * @param purApInfoService The purApInfoService to set.
     */
    public void setPurApInfoService(PurApInfoService purApInfoService) {
        this.purApInfoService = purApInfoService;
    }

    /**
     * get CAMS AssetService.
     *
     * @return
     */
    protected AssetService getAssetService() {
        return SpringContext.getBean(AssetService.class);
    }

    /**
     * Gets the capitalAssetManagementModuleService attribute.
     *
     * @return Returns the capitalAssetManagementModuleService.
     */
    public CapitalAssetManagementModuleService getCapitalAssetManagementModuleService() {
        return capitalAssetManagementModuleService;
    }

    /**
     * Sets the capitalAssetManagementModuleService attribute value.
     *
     * @param capitalAssetManagementModuleService The capitalAssetManagementModuleService to set.
     */
    public void setCapitalAssetManagementModuleService(CapitalAssetManagementModuleService capitalAssetManagementModuleService) {
        this.capitalAssetManagementModuleService = capitalAssetManagementModuleService;
    }


}
