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.purap.document.service.impl;
017    
018    import java.util.HashMap;
019    import java.util.Iterator;
020    import java.util.List;
021    import java.util.Map;
022    import java.util.Set;
023    
024    import org.kuali.kfs.coa.businessobject.Account;
025    import org.kuali.kfs.coa.service.AccountService;
026    import org.kuali.kfs.gl.batch.ScrubberStep;
027    import org.kuali.kfs.module.purap.PurapConstants;
028    import org.kuali.kfs.module.purap.PurapKeyConstants;
029    import org.kuali.kfs.module.purap.PurapPropertyConstants;
030    import org.kuali.kfs.module.purap.PurapConstants.AccountsPayableSharedStatuses;
031    import org.kuali.kfs.module.purap.PurapConstants.PaymentRequestStatuses;
032    import org.kuali.kfs.module.purap.businessobject.CreditMemoItem;
033    import org.kuali.kfs.module.purap.businessobject.ItemType;
034    import org.kuali.kfs.module.purap.businessobject.PaymentRequestItem;
035    import org.kuali.kfs.module.purap.businessobject.PurApAccountingLineBase;
036    import org.kuali.kfs.module.purap.businessobject.PurchaseOrderItem;
037    import org.kuali.kfs.module.purap.document.AccountsPayableDocument;
038    import org.kuali.kfs.module.purap.document.PaymentRequestDocument;
039    import org.kuali.kfs.module.purap.document.PurchaseOrderDocument;
040    import org.kuali.kfs.module.purap.document.PurchasingAccountsPayableDocument;
041    import org.kuali.kfs.module.purap.document.VendorCreditMemoDocument;
042    import org.kuali.kfs.module.purap.document.service.AccountsPayableDocumentSpecificService;
043    import org.kuali.kfs.module.purap.document.service.AccountsPayableService;
044    import org.kuali.kfs.module.purap.document.service.PurapService;
045    import org.kuali.kfs.module.purap.document.service.PurchaseOrderService;
046    import org.kuali.kfs.module.purap.service.PurapAccountingService;
047    import org.kuali.kfs.module.purap.service.PurapGeneralLedgerService;
048    import org.kuali.kfs.module.purap.util.ExpiredOrClosedAccount;
049    import org.kuali.kfs.module.purap.util.ExpiredOrClosedAccountEntry;
050    import org.kuali.kfs.sys.KFSConstants;
051    import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
052    import org.kuali.kfs.sys.context.SpringContext;
053    import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
054    import org.kuali.rice.kim.bo.Person;
055    import org.kuali.rice.kim.service.PersonService;
056    import org.kuali.rice.kns.UserSession;
057    import org.kuali.rice.kns.bo.Note;
058    import org.kuali.rice.kns.service.BusinessObjectService;
059    import org.kuali.rice.kns.service.DateTimeService;
060    import org.kuali.rice.kns.service.DocumentService;
061    import org.kuali.rice.kns.service.NoteService;
062    import org.kuali.rice.kns.service.ParameterService;
063    import org.kuali.rice.kns.util.GlobalVariables;
064    import org.kuali.rice.kns.util.KualiDecimal;
065    import org.kuali.rice.kns.util.ObjectUtils;
066    import org.kuali.rice.kns.workflow.service.KualiWorkflowDocument;
067    import org.kuali.rice.kns.workflow.service.WorkflowDocumentService;
068    import org.springframework.transaction.annotation.Transactional;
069    
070    
071    @Transactional
072    public class AccountsPayableServiceImpl implements AccountsPayableService {
073    
074        protected PurapAccountingService purapAccountingService;
075        protected PurapGeneralLedgerService purapGeneralLedgerService;
076        protected DocumentService documentService;
077        protected PurapService purapService;
078        protected ParameterService parameterService;
079        protected DateTimeService dateTimeService;
080        protected PurchaseOrderService purchaseOrderService;
081        protected AccountService accountService;           
082    
083        public void setParameterService(ParameterService parameterService) {
084            this.parameterService = parameterService;
085        }
086       
087        public void setPurapService(PurapService purapService) {
088            this.purapService = purapService;
089        }
090    
091        public void setPurapAccountingService(PurapAccountingService purapAccountingService) {
092            this.purapAccountingService = purapAccountingService;
093        }
094    
095        public void setPurapGeneralLedgerService(PurapGeneralLedgerService purapGeneralLedgerService) {
096            this.purapGeneralLedgerService = purapGeneralLedgerService;
097        }
098    
099        public void setDocumentService(DocumentService documentService) {
100            this.documentService = documentService;
101        }
102    
103        public void setDateTimeService(DateTimeService dateTimeService) {
104            this.dateTimeService = dateTimeService;
105        }
106    
107        public void setPurchaseOrderService(PurchaseOrderService purchaseOrderService) {
108            this.purchaseOrderService = purchaseOrderService;
109        }
110    
111        public void setAccountService(AccountService accountService) {
112            this.accountService = accountService;
113        }
114    
115        /**
116         * @see org.kuali.kfs.module.purap.document.service.AccountsPayableService#getExpiredOrClosedAccountList(org.kuali.kfs.module.purap.document.AccountsPayableDocument)
117         */
118        public HashMap<String, ExpiredOrClosedAccountEntry> getExpiredOrClosedAccountList(AccountsPayableDocument document) {
119    
120            // Retrieve a list of accounts and replacement accounts, where accounts or closed or expired.
121            HashMap<String, ExpiredOrClosedAccountEntry> expiredOrClosedAccounts = expiredOrClosedAccountsList(document);
122    
123            return expiredOrClosedAccounts;
124        }
125    
126        /**
127         * @see org.kuali.kfs.module.purap.document.service.AccountsPayableService#generateExpiredOrClosedAccountNote(org.kuali.kfs.module.purap.document.AccountsPayableDocument,
128         *      java.util.HashMap)
129         */
130        public void generateExpiredOrClosedAccountNote(AccountsPayableDocument document, HashMap<String, ExpiredOrClosedAccountEntry> expiredOrClosedAccountList) {
131    
132            // create a note of all the replacement accounts
133            if (ObjectUtils.isNotNull(expiredOrClosedAccountList) && !expiredOrClosedAccountList.isEmpty()) {
134                addContinuationAccountsNote(document, expiredOrClosedAccountList);
135            }
136    
137        }
138    
139        /**
140         * @see org.kuali.kfs.module.purap.document.service.AccountsPayableService#generateExpiredOrClosedAccountWarning(org.kuali.kfs.module.purap.document.AccountsPayableDocument)
141         */
142        public void generateExpiredOrClosedAccountWarning(AccountsPayableDocument document) {
143    
144            // get user
145            Person user = GlobalVariables.getUserSession().getPerson();
146    
147            // get parameter to see if fiscal officers may see the continuation account warning
148            String showContinuationAccountWaringFO = parameterService.getParameterValue(KfsParameterConstants.PURCHASING_DOCUMENT.class, PurapConstants.PURAP_AP_SHOW_CONTINUATION_ACCOUNT_WARNING_FISCAL_OFFICERS);
149    
150            // get parameter to see if ap users may see the continuation account warning
151            String showContinuationAccountWaringAP = parameterService.getParameterValue(KfsParameterConstants.PURCHASING_DOCUMENT.class, PurapConstants.PURAP_AP_SHOW_CONTINUATION_ACCOUNT_WARNING_AP_USERS);
152    
153            // versus doing it in their respective documents (preq, credit memo)
154            // document is past full entry and
155            // user is a fiscal officer and a system parameter is set to allow viewing
156            // and if the continuation account indicator is set
157            if (isFiscalUser(document, user) && "Y".equals(showContinuationAccountWaringFO) && (document.isContinuationAccountIndicator())) {
158                GlobalVariables.getMessageList().add(PurapKeyConstants.MESSAGE_CLOSED_OR_EXPIRED_ACCOUNTS_REPLACED);
159            }
160    
161            // document is past full entry and
162            // user is an AP User and a system parameter is set to allow viewing
163            // and if the continuation account indicator is set
164            if (isAPUser(document, user) && "Y".equals(showContinuationAccountWaringAP) && (document.isContinuationAccountIndicator())) {
165                GlobalVariables.getMessageList().add(PurapKeyConstants.MESSAGE_CLOSED_OR_EXPIRED_ACCOUNTS_REPLACED);
166            }
167    
168        }
169    
170        /**
171         * @see org.kuali.kfs.module.purap.document.service.AccountsPayableService#processExpiredOrClosedAccount(org.kuali.kfs.module.purap.businessobject.PurApAccountingLineBase,
172         *      java.util.HashMap)
173         */
174        public void processExpiredOrClosedAccount(PurApAccountingLineBase acctLineBase, HashMap<String, ExpiredOrClosedAccountEntry> expiredOrClosedAccountList) {
175    
176            ExpiredOrClosedAccountEntry accountEntry = null;
177            String acctKey = acctLineBase.getChartOfAccountsCode() + "-" + acctLineBase.getAccountNumber();
178    
179            if (expiredOrClosedAccountList.containsKey(acctKey)) {
180    
181                accountEntry = expiredOrClosedAccountList.get(acctKey);
182    
183                if (accountEntry.getOriginalAccount().isContinuationAccountMissing() == false) {
184                    acctLineBase.setChartOfAccountsCode(accountEntry.getReplacementAccount().getChartOfAccountsCode());
185                    acctLineBase.setAccountNumber(accountEntry.getReplacementAccount().getAccountNumber());
186                    acctLineBase.refreshReferenceObject("chart");
187                    acctLineBase.refreshReferenceObject("account");
188                }
189            }
190        }
191    
192        /**
193         * Creates and adds a note indicating accounts replaced and what they replaced and attaches it to the document.
194         * 
195         * @param document  The accounts payable document to which we're adding the note.
196         * @param accounts  The HashMap where the keys are the string representations of the chart and account of the 
197         *                  original account and the values are the ExpiredOrClosedAccountEntry.
198         */
199        protected void addContinuationAccountsNote(AccountsPayableDocument document, HashMap<String, ExpiredOrClosedAccountEntry> accounts) {
200            String noteText;
201            StringBuffer sb = new StringBuffer("");
202            ExpiredOrClosedAccountEntry accountEntry = null;
203            ExpiredOrClosedAccount originalAccount = null;
204            ExpiredOrClosedAccount replacementAccount = null;
205    
206            // List the entries using entrySet()
207            Set entries = accounts.entrySet();
208            Iterator it = entries.iterator();
209    
210            // loop through the accounts found to be expired/closed and add if they have a continuation account
211            while (it.hasNext()) {
212                Map.Entry entry = (Map.Entry) it.next();
213                accountEntry = (ExpiredOrClosedAccountEntry) entry.getValue();
214                originalAccount = accountEntry.getOriginalAccount();
215                replacementAccount = accountEntry.getReplacementAccount();
216    
217                // only print out accounts that were replaced and not missing a continuation account
218                if (originalAccount.isContinuationAccountMissing() == false) {
219                    sb.append(" Account " + originalAccount.getAccountString() + " was replaced with account " + replacementAccount.getAccountString() + " ; ");
220                }
221    
222            }
223    
224            // if a note was created, add it to the document
225            if (sb.toString().length() > 0) {
226                try {
227                    Note resetNote = documentService.createNoteFromDocument(document, sb.toString());
228                    documentService.addNoteToDocument(document, resetNote);
229                }
230                catch (Exception e) {
231                    throw new RuntimeException(PurapConstants.REQ_UNABLE_TO_CREATE_NOTE + " " + e);
232                }
233            }
234        }
235    
236        /**
237         * Gets the replacement account for the specified closed account. 
238         * In this case it's the continuation account of the the specified account.
239         * 
240         * @param account the specified account which is closed.
241         * @document the document the account is associated with.    
242         * @return the replacement account for the specified account.
243         */
244        protected Account getReplaceAccountForClosedAccount(Account account, AccountsPayableDocument document) {
245            if (account == null) return null; // this should never happen        
246            Account continueAccount = accountService.getByPrimaryId(account.getContinuationFinChrtOfAcctCd(), account.getContinuationAccountNumber());
247            return continueAccount;
248        }
249    
250        /**
251         * Gets the replacement account for the specified expired account. 
252         * In this case it's the continuation account of the the specified account.
253         * 
254         * @param account the specified account which is expired.
255         * @document the document the account is associated with.    
256         * @return the replacement account for the specified account.
257         */
258        protected Account getReplaceAccountForExpiredAccount(Account account, AccountsPayableDocument document) {
259            if (account == null) return null; // this should never happen        
260            Account continueAccount = accountService.getByPrimaryId(account.getContinuationFinChrtOfAcctCd(), account.getContinuationAccountNumber());
261            return continueAccount;
262        }
263        
264        /**
265         * Generates a list of replacement accounts for expired or closed accounts, as well as for expired/closed accounts without a continuation account.
266         * 
267         * @param document  The accounts payable document from which we're obtaining the purchase order id to be used
268         *                  to obtain the purchase order document, whose accounts we'll use to generate the list of
269         *                  replacement accounts for expired or closed accounts.
270         * @return          The HashMap where the keys are the string representations of the chart and account 
271         *                  of the original account and the values are the ExpiredOrClosedAccountEntry.
272         */
273        protected HashMap<String, ExpiredOrClosedAccountEntry> expiredOrClosedAccountsList(AccountsPayableDocument document) {     
274            // retrieve po from apdoc
275            PurchaseOrderDocument po = document.getPurchaseOrderDocument();
276            if (po == null && document instanceof VendorCreditMemoDocument) {
277                PaymentRequestDocument preq = ((VendorCreditMemoDocument)document).getPaymentRequestDocument();
278                if (preq == null)   return null; // this should never happen 
279                po = ((VendorCreditMemoDocument)document).getPaymentRequestDocument().getPurchaseOrderDocument();
280            }
281            if (po == null) return null; // this should never happen 
282    
283            // initialize
284            List<SourceAccountingLine> acctLines = purapAccountingService.generateSummary(po.getItemsActiveOnly());            
285            HashMap<String, ExpiredOrClosedAccountEntry> eocAcctMap = new HashMap<String, ExpiredOrClosedAccountEntry>();
286    
287            // loop through accounting lines
288            for (SourceAccountingLine acctLine : acctLines) {
289                Account account = accountService.getByPrimaryId(acctLine.getChartOfAccountsCode(), acctLine.getAccountNumber());
290                Account repAccount = null;
291                boolean replace = false;            
292                
293                // 1. if the account is closed, get the continuation account as replacement
294                if (!account.isActive()) {
295                    repAccount = getReplaceAccountForClosedAccount(account, document);         
296                    replace = true;
297                }            
298                // 2. if the account is C&G and is expired for more than 90 days, get the continuation account as replacement
299                else if (account.isExpired()) {
300                    // retrieve extension limit (grace period)
301                    String expirationExtensionDays = parameterService.getParameterValue(ScrubberStep.class, KFSConstants.SystemGroupParameterNames.GL_SCRUBBER_VALIDATION_DAYS_OFFSET);
302                    int expirationExtensionDaysInt = 90; // default to 90 days (approximately 3 months)
303                    if (expirationExtensionDays.trim().length() > 0) {
304                        expirationExtensionDaysInt = new Integer(expirationExtensionDays).intValue();
305                    }
306                    // if account is C&G and expired beyond grace period then get replacement
307                    if ((account.isForContractsAndGrants() && dateTimeService.dateDiff(account.getAccountExpirationDate(), dateTimeService.getCurrentDate(), true) > expirationExtensionDaysInt)) {
308                        repAccount = getReplaceAccountForExpiredAccount(account, document); 
309                        replace = true;
310                    }
311                    // otherwise if the account is not C&G, or it's expired within the grace period, do nothing         
312                }
313                
314                // if replacement needed, set up ExpiredOrClosedAccount entry and add it to the eocAcctMap
315                if (replace) {
316                    ExpiredOrClosedAccountEntry eocAcctEntry = new ExpiredOrClosedAccountEntry();
317                    ExpiredOrClosedAccount originAcct = new ExpiredOrClosedAccount(acctLine.getChartOfAccountsCode(), acctLine.getAccountNumber(), acctLine.getSubAccountNumber());
318                    ExpiredOrClosedAccount replaceAcct = null;
319                    if (repAccount == null) {
320                        replaceAcct = new ExpiredOrClosedAccount();
321                        originAcct.setContinuationAccountMissing(true);
322                    }
323                    else {
324                        replaceAcct = new ExpiredOrClosedAccount(repAccount.getChartOfAccountsCode(), repAccount.getAccountNumber(), acctLine.getSubAccountNumber());
325                    }
326                    eocAcctEntry.setOriginalAccount(originAcct);
327                    eocAcctEntry.setReplacementAccount(replaceAcct);
328                    eocAcctMap.put(createChartAccountString(originAcct), eocAcctEntry);                 
329                }               
330            }           
331    
332            return eocAcctMap;      
333        }
334    
335        /**
336         * Generates a list of replacement accounts for expired or closed accounts, as well as for expired/closed accounts without a continuation account.
337         * 
338         * @param document  The purchase order document whose accounts we'll use to generate the list of
339         *                  replacement accounts for expired or closed accounts.
340         * @return          The HashMap where the keys are the string representations of the chart and account 
341         *                  of the original account and the values are the ExpiredOrClosedAccountEntry.
342         */
343        public HashMap<String, ExpiredOrClosedAccountEntry> expiredOrClosedAccountsList(PurchaseOrderDocument po) {
344            HashMap<String, ExpiredOrClosedAccountEntry> list = new HashMap<String, ExpiredOrClosedAccountEntry>();
345            ExpiredOrClosedAccountEntry entry = null;
346            ExpiredOrClosedAccount originalAcct = null;
347            ExpiredOrClosedAccount replaceAcct = null;
348            String chartAccount = null;
349    
350            if (po != null) {
351                // get list of active accounts
352                List<SourceAccountingLine> accountList = purapAccountingService.generateSummary(po.getItemsActiveOnly());            
353    
354                // loop through accounts
355                for (SourceAccountingLine poAccountingLine : accountList) {
356                    Account account = accountService.getByPrimaryId(poAccountingLine.getChartOfAccountsCode(), poAccountingLine.getAccountNumber());
357                    entry = new ExpiredOrClosedAccountEntry();
358                    originalAcct = new ExpiredOrClosedAccount(poAccountingLine.getChartOfAccountsCode(), poAccountingLine.getAccountNumber(), poAccountingLine.getSubAccountNumber());
359    
360                    if (!account.isActive()) {
361                        // 1. if the account is closed, get the continuation account and add it to the list
362                        Account continuationAccount = accountService.getByPrimaryId(account.getContinuationFinChrtOfAcctCd(), account.getContinuationAccountNumber());
363    
364                        if (continuationAccount == null) {
365                            replaceAcct = new ExpiredOrClosedAccount();
366                            originalAcct.setContinuationAccountMissing(true);
367    
368                            entry.setOriginalAccount(originalAcct);
369                            entry.setReplacementAccount(replaceAcct);
370    
371                            list.put(createChartAccountString(originalAcct), entry);
372                        }
373                        else {
374                            replaceAcct = new ExpiredOrClosedAccount(continuationAccount.getChartOfAccountsCode(), continuationAccount.getAccountNumber(), poAccountingLine.getSubAccountNumber());
375    
376                            entry.setOriginalAccount(originalAcct);
377                            entry.setReplacementAccount(replaceAcct);
378    
379                            list.put(createChartAccountString(originalAcct), entry);
380                        }
381                        // 2. if the account is expired and the current date is <= 90 days from the expiration date, do nothing
382                        // 3. if the account is expired and the current date is > 90 days from the expiration date, get the continuation
383                        // account and add it to the list
384                    }
385                    else if (account.isExpired()) {
386                        Account continuationAccount = accountService.getByPrimaryId(account.getContinuationFinChrtOfAcctCd(), account.getContinuationAccountNumber());
387                        String expirationExtensionDays = parameterService.getParameterValue(ScrubberStep.class, KFSConstants.SystemGroupParameterNames.GL_SCRUBBER_VALIDATION_DAYS_OFFSET);
388                        int expirationExtensionDaysInt = 3 * 30; // default to 90 days (approximately 3 months)
389    
390                        if (expirationExtensionDays.trim().length() > 0) {
391    
392                            expirationExtensionDaysInt = new Integer(expirationExtensionDays).intValue();
393                        }
394    
395                        // if account is C&G and expired then add to list.
396                        if ((account.isForContractsAndGrants() && dateTimeService.dateDiff(account.getAccountExpirationDate(), dateTimeService.getCurrentDate(), true) > expirationExtensionDaysInt)) {
397    
398                            if (continuationAccount == null) {
399                                replaceAcct = new ExpiredOrClosedAccount();
400                                originalAcct.setContinuationAccountMissing(true);
401    
402                                entry.setOriginalAccount(originalAcct);
403                                entry.setReplacementAccount(replaceAcct);
404    
405                                list.put(createChartAccountString(originalAcct), entry);
406                            }
407                            else {
408                                replaceAcct = new ExpiredOrClosedAccount(continuationAccount.getChartOfAccountsCode(), continuationAccount.getAccountNumber(), poAccountingLine.getSubAccountNumber());
409    
410                                entry.setOriginalAccount(originalAcct);
411                                entry.setReplacementAccount(replaceAcct);
412    
413                                list.put(createChartAccountString(originalAcct), entry);
414                            }
415                        }
416    
417                        // if account is not C&G, use the same account, do not replace
418                    }
419                }
420            }
421            return list;
422        }
423        
424        /**
425         * Creates a chart-account string.
426         * 
427         * @param ecAccount  The account whose chart and account number we're going to use to create the resulting String for this method.
428         * @return           The string representing the chart and account number of the given ecAccount.
429         */
430        protected String createChartAccountString(ExpiredOrClosedAccount ecAccount) {
431            StringBuffer buff = new StringBuffer("");
432    
433            buff.append(ecAccount.getChartOfAccountsCode());
434            buff.append("-");
435            buff.append(ecAccount.getAccountNumber());
436    
437            return buff.toString();
438        }
439    
440        /**
441         * Determines if the user is a fiscal officer.  Currently this only checks the doc and workflow status for approval requested
442         * 
443         * @param document  The document to be used to check the status code and whether the workflow approval is requested.
444         * @param user      The current user.
445         * @return          boolean true if the user is a fiscal officer.
446         */
447        protected boolean isFiscalUser(AccountsPayableDocument document, Person user) {
448            boolean isFiscalUser = false;
449    
450            if (PaymentRequestStatuses.AWAITING_FISCAL_REVIEW.equals(document.getStatusCode()) && document.getDocumentHeader().getWorkflowDocument().isApprovalRequested()) {
451                isFiscalUser = true;
452            }
453    
454            return isFiscalUser;
455        }
456    
457        /**
458         * Determines if the user is an AP user.  Currently this only checks the doc and workflow status for approval requested
459         * 
460         * @param document  The document to be used to check the status code and whether the workflow approval is requested.
461         * @param user      The current user.
462         * @return          boolean true if the user is an AP User.
463         */
464        protected boolean isAPUser(AccountsPayableDocument document, Person user) {
465            boolean isFiscalUser = false;
466    
467            if ((PaymentRequestStatuses.AWAITING_ACCOUNTS_PAYABLE_REVIEW.equals(document.getStatusCode()) && 
468                 document.getDocumentHeader().getWorkflowDocument().isApprovalRequested()) ||
469                 PaymentRequestStatuses.IN_PROCESS.equals(document.getStatusCode())) {
470                    isFiscalUser = true;
471            }
472    
473            return isFiscalUser;
474        }    
475        
476        /**
477         * @see org.kuali.kfs.module.purap.document.service.AccountsPayableService#cancelAccountsPayableDocument(org.kuali.kfs.module.purap.document.AccountsPayableDocument, java.lang.String)
478         */
479        public void cancelAccountsPayableDocument(AccountsPayableDocument apDocument, String currentNodeName) {
480            if (purapService.isFullDocumentEntryCompleted(apDocument)) {
481                purapGeneralLedgerService.generateEntriesCancelAccountsPayableDocument(apDocument);
482            }
483            AccountsPayableDocumentSpecificService accountsPayableDocumentSpecificService = apDocument.getDocumentSpecificService();
484            accountsPayableDocumentSpecificService.updateStatusByNode(currentNodeName, apDocument);
485            apDocument.refreshReferenceObject(PurapPropertyConstants.STATUS);
486    
487            // close/reopen purchase order.
488            accountsPayableDocumentSpecificService.takePurchaseOrderCancelAction(apDocument);
489        }
490        
491        /**
492         * @see org.kuali.kfs.module.purap.document.service.AccountsPayableService#cancelAccountsPayableDocumentByCheckingDocumentStatus(org.kuali.kfs.module.purap.document.AccountsPayableDocument, java.lang.String)
493         */
494        public void cancelAccountsPayableDocumentByCheckingDocumentStatus(AccountsPayableDocument document, String noteText) throws Exception {
495            DocumentService documentService = SpringContext.getBean(DocumentService.class);
496    
497            if (AccountsPayableSharedStatuses.IN_PROCESS.equals(document.getStatusCode())) {
498                //prior to submit, just call regular cancel logic
499                documentService.cancelDocument(document, noteText);
500            }
501            else if (AccountsPayableSharedStatuses.AWAITING_ACCOUNTS_PAYABLE_REVIEW.equals(document.getStatusCode())) {
502                //while awaiting AP approval, just call regular disapprove logic as user will have action request
503                documentService.disapproveDocument(document, noteText);
504            }
505            else {
506                UserSession originalUserSession = GlobalVariables.getUserSession();
507                KualiWorkflowDocument originalWorkflowDocument = document.getDocumentHeader().getWorkflowDocument();
508                //any other time, perform special logic to cancel the document
509                if (!document.getDocumentHeader().getWorkflowDocument().stateIsFinal()) {
510                    try {
511                        // person canceling may not have an action requested on the document
512                        Person userRequestedCancel = SpringContext.getBean(PersonService.class).getPerson(document.getLastActionPerformedByPersonId());
513                        GlobalVariables.setUserSession(new UserSession(KFSConstants.SYSTEM_USER));
514                        
515                        WorkflowDocumentService workflowDocumentService =  SpringContext.getBean(WorkflowDocumentService.class);
516                        KualiWorkflowDocument newWorkflowDocument = workflowDocumentService.createWorkflowDocument(Long.valueOf(document.getDocumentNumber()), GlobalVariables.getUserSession().getPerson());
517                        document.getDocumentHeader().setWorkflowDocument(newWorkflowDocument);
518                        documentService.superUserDisapproveDocument(document, "Document Cancelled by user " + originalUserSession.getPerson().getName() + " (" + originalUserSession.getPerson().getPrincipalName() + ") per request of user " + userRequestedCancel.getName() + " (" + userRequestedCancel.getPrincipalName() + ")");
519                    }
520                    finally {
521                        GlobalVariables.setUserSession(originalUserSession);
522                        document.getDocumentHeader().setWorkflowDocument(originalWorkflowDocument);
523                    }
524                }
525                else {
526                    // call gl method here (no reason for post processing since workflow done)
527                    SpringContext.getBean(AccountsPayableService.class).cancelAccountsPayableDocument(document, "");
528                    document.getDocumentHeader().getWorkflowDocument().logDocumentAction("Document Cancelled by user " + originalUserSession.getPerson().getName() + " (" + originalUserSession.getPerson().getPrincipalName() + ")");
529                }
530            }
531                        
532            Note noteObj = documentService.createNoteFromDocument(document, noteText);
533            documentService.addNoteToDocument(document, noteObj);
534            SpringContext.getBean(NoteService.class).save(noteObj);      
535        }
536    
537        /**
538         * @see org.kuali.kfs.module.purap.document.service.AccountsPayableService#performLogicForFullEntryCompleted(org.kuali.kfs.module.purap.document.PurchasingAccountsPayableDocument)
539         */
540        public void performLogicForFullEntryCompleted(PurchasingAccountsPayableDocument purapDocument) {
541            AccountsPayableDocument apDocument = (AccountsPayableDocument)purapDocument;
542            AccountsPayableDocumentSpecificService accountsPayableDocumentSpecificService = apDocument.getDocumentSpecificService();
543            
544            // eliminate unentered items
545            purapService.deleteUnenteredItems(apDocument);
546            
547            // change accounts from percents to dollars
548            purapAccountingService.updateAccountAmounts(apDocument);
549            
550            //set the AP approval date always when the GL entries are created (treated more of an AP processed date)
551            apDocument.setAccountsPayableApprovalTimestamp(dateTimeService.getCurrentTimestamp());
552    
553            // save for persistence
554            SpringContext.getBean(BusinessObjectService.class).save(apDocument);
555    
556            // do GL entries for document creation
557            accountsPayableDocumentSpecificService.generateGLEntriesCreateAccountsPayableDocument(apDocument);
558            
559        }
560    
561        /**
562         * @see org.kuali.kfs.module.purap.document.service.AccountsPayableService#updateItemList(org.kuali.kfs.module.purap.document.AccountsPayableDocument)
563         */
564        public void updateItemList(AccountsPayableDocument apDocument) {
565            // don't run the following if past full entry
566            if (purapService.isFullDocumentEntryCompleted(apDocument)) {
567                return;
568            }
569            if (apDocument instanceof VendorCreditMemoDocument) {
570                VendorCreditMemoDocument cm = (VendorCreditMemoDocument) apDocument;
571                if (cm.isSourceDocumentPaymentRequest()) {
572                    // just update encumberances, items shouldn't change, get to them through po (or through preq)
573                    List<PaymentRequestItem> items = cm.getPaymentRequestDocument().getItems();
574                    for (PaymentRequestItem preqItem : items) {
575                        // skip inactive and below the line
576                        if (preqItem.getItemType().isAdditionalChargeIndicator()) {
577                            continue;
578                        }
579                        PurchaseOrderItem poItem = preqItem.getPurchaseOrderItem();
580                        CreditMemoItem cmItem = (CreditMemoItem) cm.getAPItemFromPOItem(poItem);
581                        // take invoiced quantities from the lower of the preq and po if different
582                        updateEncumberances(preqItem, poItem, cmItem);
583                    }
584                }
585                else if (cm.isSourceDocumentPurchaseOrder()) {
586                    PurchaseOrderDocument po = purchaseOrderService.getCurrentPurchaseOrder(apDocument.getPurchaseOrderIdentifier());
587                    List<PurchaseOrderItem> poItems = po.getItems();
588                    List<CreditMemoItem> cmItems = cm.getItems();
589                    // iterate through the above the line poItems to find matching
590                    for (PurchaseOrderItem purchaseOrderItem : poItems) {
591                        // skip inactive and below the line
592                        if (purchaseOrderItem.getItemType().isAdditionalChargeIndicator()) {
593                            continue;
594                        }
595    
596                        CreditMemoItem cmItem = (CreditMemoItem) cm.getAPItemFromPOItem(purchaseOrderItem);
597                        // check if any action needs to be taken on the items (i.e. add for new eligible items or remove for ineligible)
598                        if (apDocument.getDocumentSpecificService().poItemEligibleForAp(apDocument, purchaseOrderItem)) {
599                            // if eligible and not there - add
600                            if (ObjectUtils.isNull(cmItem)) {
601                                CreditMemoItem cmi = new CreditMemoItem(cm, purchaseOrderItem);
602                                cmi.setPurapDocument(apDocument);
603                                cmItems.add(cmi);
604                            }
605                            else {
606                                // is eligible and on doc, update encumberances
607                                // (this is only qty and amount for now NOTE we should also update other key fields, like description
608                                // etc in case ammendment modified a line
609                                updateEncumberance(purchaseOrderItem, cmItem);
610                            }
611                        }
612                        else { // if not eligible and there - remove
613                            if (ObjectUtils.isNotNull(cmItem)) {
614                                cmItems.remove(cmItem);
615                                // don't update encumberance
616                                continue;
617                            }
618                        }
619    
620                    }
621                } // else do nothing
622                return;
623    
624                // finally update encumbrances
625            }
626            else if (apDocument instanceof PaymentRequestDocument) {
627    
628                // get a fresh purchase order
629                PurchaseOrderDocument po = purchaseOrderService.getCurrentPurchaseOrder(apDocument.getPurchaseOrderIdentifier());
630                PaymentRequestDocument preq = (PaymentRequestDocument) apDocument;
631    
632                List<PurchaseOrderItem> poItems = po.getItems();
633                List<PaymentRequestItem> preqItems = preq.getItems();
634                // iterate through the above the line poItems to find matching
635                for (PurchaseOrderItem purchaseOrderItem : poItems) {
636                    // skip below the line
637                    if (purchaseOrderItem.getItemType().isAdditionalChargeIndicator()) {
638                        continue;
639                    }
640                    PaymentRequestItem preqItem = (PaymentRequestItem) preq.getAPItemFromPOItem(purchaseOrderItem);
641                    // check if any action needs to be taken on the items (i.e. add for new eligible items or remove for ineligible)
642                    if (apDocument.getDocumentSpecificService().poItemEligibleForAp(apDocument, purchaseOrderItem)) {
643                        // if eligible and not there - add
644                        if (ObjectUtils.isNull(preqItem)) {
645                            PaymentRequestItem pri = new PaymentRequestItem(purchaseOrderItem, preq);
646                            pri.setPurapDocument(apDocument);
647                            preqItems.add(pri);
648                        }
649                        else {
650                            updatePossibleAmmendedFields(purchaseOrderItem, preqItem);
651                        }
652                    }
653                    else { // if not eligible and there - remove
654                        if (ObjectUtils.isNotNull(preqItem)) {
655                            preqItems.remove(preqItem);
656                        }
657                    }
658    
659                }
660            }
661        }
662    
663        /**
664         * Updates fields that could've been changed on amendment.
665         * 
666         * @param sourceItem   The purchase order item from which we're getting the unit price, catalog number and description to be set in the destItem.
667         * @param destItem     The payment request item to which we're setting the unit price, catalog number and description.
668         */
669        protected void updatePossibleAmmendedFields(PurchaseOrderItem sourceItem, PaymentRequestItem destItem) {
670            destItem.setPurchaseOrderItemUnitPrice(sourceItem.getItemUnitPrice());
671            destItem.setItemCatalogNumber(sourceItem.getItemCatalogNumber());
672            destItem.setItemDescription(sourceItem.getItemDescription());
673        }
674    
675        /**
676         * Updates encumberances.
677         * 
678         * @param preqItem  The payment request item from which we're obtaining the item quantity, unit price and extended price.
679         * @param poItem    The purchase order item from which we're obtaining the invoice total quantity, unit price and invoice total amount.
680         * @param cmItem    The credit memo item whose invoice total quantity, unit price and extended price are to be updated.
681         */
682        protected void updateEncumberances(PaymentRequestItem preqItem, PurchaseOrderItem poItem, CreditMemoItem cmItem) {
683            if (poItem.getItemInvoicedTotalQuantity() != null && preqItem.getItemQuantity() != null && poItem.getItemInvoicedTotalQuantity().isLessThan(preqItem.getItemQuantity())) {
684                cmItem.setPreqInvoicedTotalQuantity(poItem.getItemInvoicedTotalQuantity());
685                cmItem.setPreqUnitPrice(poItem.getItemUnitPrice());
686                cmItem.setPreqTotalAmount(poItem.getItemInvoicedTotalAmount());
687            }
688            else {
689                cmItem.setPreqInvoicedTotalQuantity(preqItem.getItemQuantity());
690                cmItem.setPreqUnitPrice(preqItem.getItemUnitPrice());
691                cmItem.setPreqTotalAmount(preqItem.getTotalAmount());
692            }
693        }
694    
695        /**
696         * Updates the encumberance related fields.
697         * 
698         * @param purchaseOrderItem  The purchase order item from which we're obtaining the invoice total quantity, unit price and invoice total amount.
699         * @param cmItem             The credit memo item whose invoice total quantity, unit price and extended price are to be updated.
700         */
701        protected void updateEncumberance(PurchaseOrderItem purchaseOrderItem, CreditMemoItem cmItem) {
702            cmItem.setPoInvoicedTotalQuantity(purchaseOrderItem.getItemInvoicedTotalQuantity());
703            cmItem.setPreqUnitPrice(purchaseOrderItem.getItemUnitPrice());
704            cmItem.setPoTotalAmount(purchaseOrderItem.getItemInvoicedTotalAmount());
705        }
706    
707        /**
708         * @see org.kuali.kfs.module.purap.document.service.AccountsPayableService#purchaseOrderItemEligibleForPayment(org.kuali.kfs.module.purap.businessobject.PurchaseOrderItem)
709         */
710        public boolean purchaseOrderItemEligibleForPayment(PurchaseOrderItem poi) {
711            if (ObjectUtils.isNull(poi)) {
712                throw new RuntimeException("item null in purchaseOrderItemEligibleForPayment ... this should never happen");
713            }
714    
715            // if the po item is not active... skip it
716            if (!poi.isItemActiveIndicator()) {
717                return false;
718            }
719    
720            ItemType poiType = poi.getItemType();
721    
722            if (poiType.isQuantityBasedGeneralLedgerIndicator()) {
723                if (poi.getItemQuantity().isGreaterThan(poi.getItemInvoicedTotalQuantity())) {
724                    return true;
725                }
726                return false;
727            }
728            else { // not quantity based
729                if (poi.getItemOutstandingEncumberedAmount().isGreaterThan(KualiDecimal.ZERO)) {
730                    return true;
731                }
732                return false;
733            }
734        }
735        
736    }
737