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.fp.batch.service.impl;
017    
018    import java.sql.Timestamp;
019    import java.text.ParseException;
020    import java.util.ArrayList;
021    import java.util.Collection;
022    import java.util.Date;
023    import java.util.HashSet;
024    import java.util.Iterator;
025    import java.util.List;
026    import java.util.Set;
027    
028    import org.apache.commons.lang.StringUtils;
029    import org.apache.commons.lang.WordUtils;
030    import org.kuali.kfs.fp.batch.DvToPdpExtractStep;
031    import org.kuali.kfs.fp.batch.service.DisbursementVoucherExtractService;
032    import org.kuali.kfs.fp.businessobject.DisbursementVoucherNonEmployeeExpense;
033    import org.kuali.kfs.fp.businessobject.DisbursementVoucherNonEmployeeTravel;
034    import org.kuali.kfs.fp.businessobject.DisbursementVoucherPayeeDetail;
035    import org.kuali.kfs.fp.businessobject.DisbursementVoucherPreConferenceDetail;
036    import org.kuali.kfs.fp.businessobject.DisbursementVoucherPreConferenceRegistrant;
037    import org.kuali.kfs.fp.dataaccess.DisbursementVoucherDao;
038    import org.kuali.kfs.fp.document.DisbursementVoucherConstants;
039    import org.kuali.kfs.fp.document.DisbursementVoucherDocument;
040    import org.kuali.kfs.pdp.PdpConstants;
041    import org.kuali.kfs.pdp.PdpParameterConstants;
042    import org.kuali.kfs.pdp.businessobject.Batch;
043    import org.kuali.kfs.pdp.businessobject.CustomerProfile;
044    import org.kuali.kfs.pdp.businessobject.PaymentAccountDetail;
045    import org.kuali.kfs.pdp.businessobject.PaymentDetail;
046    import org.kuali.kfs.pdp.businessobject.PaymentGroup;
047    import org.kuali.kfs.pdp.businessobject.PaymentNoteText;
048    import org.kuali.kfs.pdp.service.CustomerProfileService;
049    import org.kuali.kfs.pdp.service.PaymentFileService;
050    import org.kuali.kfs.pdp.service.PaymentGroupService;
051    import org.kuali.kfs.pdp.service.PdpEmailService;
052    import org.kuali.kfs.sys.KFSConstants;
053    import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
054    import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper;
055    import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
056    import org.kuali.kfs.sys.context.SpringContext;
057    import org.kuali.kfs.sys.document.service.FinancialSystemDocumentService;
058    import org.kuali.kfs.sys.document.validation.event.AccountingDocumentSaveWithNoLedgerEntryGenerationEvent;
059    import org.kuali.kfs.sys.service.GeneralLedgerPendingEntryService;
060    import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
061    import org.kuali.kfs.vnd.businessobject.VendorDetail;
062    import org.kuali.kfs.vnd.document.service.VendorService;
063    import org.kuali.rice.kew.exception.WorkflowException;
064    import org.kuali.rice.kim.bo.Person;
065    import org.kuali.rice.kim.service.PersonService;
066    import org.kuali.rice.kns.service.BusinessObjectService;
067    import org.kuali.rice.kns.service.DateTimeService;
068    import org.kuali.rice.kns.service.DocumentService;
069    import org.kuali.rice.kns.service.ParameterEvaluator;
070    import org.kuali.rice.kns.service.ParameterService;
071    import org.kuali.rice.kns.util.KualiDecimal;
072    import org.kuali.rice.kns.util.KualiInteger;
073    import org.kuali.rice.kns.util.ObjectUtils;
074    import org.springframework.transaction.annotation.Transactional;
075    
076    /**
077     * This is the default implementation of the DisbursementVoucherExtractService interface.
078     */
079    @Transactional
080    public class DisbursementVoucherExtractServiceImpl implements DisbursementVoucherExtractService {
081        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DisbursementVoucherExtractServiceImpl.class);
082    
083        private PersonService<Person> personService;
084        private ParameterService parameterService;
085        private DisbursementVoucherDao disbursementVoucherDao;
086        private DateTimeService dateTimeService;
087        private CustomerProfileService customerProfileService;
088        private PaymentFileService paymentFileService;
089        private PaymentGroupService paymentGroupService;
090        private BusinessObjectService businessObjectService;
091        private PdpEmailService paymentFileEmailService;
092        private int maxNoteLines;
093    
094        // This should only be set to true when testing this system. Setting this to true will run the code but
095        // won't set the doc status to extracted
096        boolean testMode = false;
097    
098        /**
099         * This method extracts all payments from a disbursement voucher with a status code of "A" and uploads them as a batch for
100         * processing.
101         * 
102         * @return Always returns true if the method completes.
103         * @see org.kuali.kfs.fp.batch.service.DisbursementVoucherExtractService#extractPayments()
104         */
105        public boolean extractPayments() {
106            LOG.debug("extractPayments() started");
107    
108            Date processRunDate = dateTimeService.getCurrentDate();
109    
110            String noteLines = parameterService.getParameterValue(KfsParameterConstants.PRE_DISBURSEMENT_ALL.class, PdpParameterConstants.MAX_NOTE_LINES);
111    
112            try {
113                maxNoteLines = Integer.parseInt(noteLines);
114            }
115            catch (NumberFormatException nfe) {
116                throw new IllegalArgumentException("Invalid Max Notes Lines parameter");
117            }
118    
119            Person uuser = getPersonService().getPersonByPrincipalName(KFSConstants.SYSTEM_USER);
120            if (uuser == null) {
121                LOG.debug("extractPayments() Unable to find user " + KFSConstants.SYSTEM_USER);
122                throw new IllegalArgumentException("Unable to find user " + KFSConstants.SYSTEM_USER);
123            }
124    
125            // Get a list of campuses that have documents with an 'A' (approved) status.
126            Set<String> campusList = getCampusListByDocumentStatusCode(DisbursementVoucherConstants.DocumentStatusCodes.APPROVED);
127    
128            // Process each campus one at a time
129            for (String campusCode : campusList) {
130                extractPaymentsForCampus(campusCode, uuser, processRunDate);
131            }
132    
133            return true;
134        }
135    
136        /**
137         * This method extracts all outstanding payments from all the disbursement vouchers in approved status for a given campus and
138         * adds these payments to a batch file that is uploaded for processing.
139         * 
140         * @param campusCode The id code of the campus the payments will be retrieved for.
141         * @param user The user object used when creating the batch file to upload with outstanding payments.
142         * @param processRunDate This is the date that the batch file is created, often this value will be today's date.
143         */
144        protected void extractPaymentsForCampus(String campusCode, Person user, Date processRunDate) {
145            LOG.debug("extractPaymentsForCampus() started for campus: " + campusCode);
146    
147            Batch batch = createBatch(campusCode, user, processRunDate);
148            Integer count = 0;
149            KualiDecimal totalAmount = KualiDecimal.ZERO;
150    
151            Collection<DisbursementVoucherDocument> dvd = getListByDocumentStatusCodeCampus(DisbursementVoucherConstants.DocumentStatusCodes.APPROVED, campusCode);
152            for (DisbursementVoucherDocument document : dvd) {
153                addPayment(document, batch, processRunDate);
154                count++;
155                totalAmount = totalAmount.add(document.getDisbVchrCheckTotalAmount());
156            }
157    
158            batch.setPaymentCount(new KualiInteger(count));
159            batch.setPaymentTotalAmount(totalAmount);
160    
161            businessObjectService.save(batch);
162            paymentFileEmailService.sendLoadEmail(batch);
163        }
164    
165        /**
166         * This method creates a payment group from the disbursement voucher and batch provided and persists that group to the database.
167         * 
168         * @param document The document used to build a payment group detail.
169         * @param batch The batch file used to build a payment group and detail.
170         * @param processRunDate The date the batch file is to post.
171         */
172        protected void addPayment(DisbursementVoucherDocument document, Batch batch, Date processRunDate) {
173            LOG.debug("addPayment() started");
174    
175            PaymentGroup pg = buildPaymentGroup(document, batch);
176            PaymentDetail pd = buildPaymentDetail(document, batch, processRunDate);
177    
178            pd.setPaymentGroup(pg);
179            pg.addPaymentDetails(pd);
180            this.businessObjectService.save(pg);
181    
182            if (!testMode) {
183                try {
184                    document.getDocumentHeader().setFinancialDocumentStatusCode(DisbursementVoucherConstants.DocumentStatusCodes.EXTRACTED);
185                    document.setExtractDate(new java.sql.Date(processRunDate.getTime()));
186                    SpringContext.getBean(DocumentService.class).saveDocument(document, AccountingDocumentSaveWithNoLedgerEntryGenerationEvent.class);
187                }
188                catch (WorkflowException we) {
189                    LOG.error("Could not save disbursement voucher document #" + document.getDocumentNumber() + ": " + we);
190                    throw new RuntimeException(we);
191                }
192            }
193        }
194    
195        /**
196         * This method creates a PaymentGroup from the disbursement voucher and batch provided. The values provided by the disbursement
197         * voucher are used to assign appropriate attributes to the payment group, including address and vendor detail information. The
198         * information added to the payment group includes tax encoding to identify if taxes should be taken out of the payment. The tax
199         * rules vary depending on the type of individual or entity being paid
200         * 
201         * @param document The document to be used for retrieving the information about the vendor being paid.
202         * @param batch The batch that the payment group will be associated with.
203         * @return A PaymentGroup object fully populated with all the values necessary to make a payment.
204         */
205        protected PaymentGroup buildPaymentGroup(DisbursementVoucherDocument document, Batch batch) {
206            LOG.debug("buildPaymentGroup() started");
207    
208            PaymentGroup pg = new PaymentGroup();
209            pg.setBatch(batch);
210            pg.setCombineGroups(Boolean.TRUE);
211            pg.setCampusAddress(Boolean.FALSE);
212    
213            DisbursementVoucherPayeeDetail pd = document.getDvPayeeDetail();
214            String rc = pd.getDisbVchrPaymentReasonCode();
215    
216            // If the payee is an employee, set these flags accordingly
217            if ((document.getDvPayeeDetail().isVendor() && SpringContext.getBean(VendorService.class).isVendorInstitutionEmployee(pd.getDisbVchrVendorHeaderIdNumberAsInteger())) || document.getDvPayeeDetail().isEmployee()) {
218                pg.setEmployeeIndicator(Boolean.TRUE);
219                pg.setPayeeIdTypeCd(PdpConstants.PayeeIdTypeCodes.EMPLOYEE);
220    
221                // All payments are taxable except research participant, rental & royalties
222                pg.setTaxablePayment(
223                        !parameterService.getParameterEvaluator(DisbursementVoucherDocument.class, DisbursementVoucherConstants.RESEARCH_PAYMENT_REASONS_PARM_NM, rc).evaluationSucceeds()
224                            && !DisbursementVoucherConstants.PaymentReasonCodes.RENTAL_PAYMENT.equals(rc)
225                            && !DisbursementVoucherConstants.PaymentReasonCodes.ROYALTIES.equals(rc));
226            }
227            // Payee is not an employee
228            else {
229    
230                // These are taxable
231                VendorDetail vendDetail = SpringContext.getBean(VendorService.class).getVendorDetail(pd.getDisbVchrVendorHeaderIdNumberAsInteger(), pd.getDisbVchrVendorDetailAssignedIdNumberAsInteger());
232                String vendorOwnerCode = vendDetail.getVendorHeader().getVendorOwnershipCode();
233                String vendorOwnerCategoryCode = vendDetail.getVendorHeader().getVendorOwnershipCategoryCode();
234                String payReasonCode = pd.getDisbVchrPaymentReasonCode();
235                
236                pg.setPayeeIdTypeCd(PdpConstants.PayeeIdTypeCodes.VENDOR_ID);
237                
238                // Assume it is not taxable until proven otherwise
239                pg.setTaxablePayment(Boolean.FALSE);
240                pg.setPayeeOwnerCd(vendorOwnerCode);
241    
242                ParameterEvaluator parameterEvaluator1 = this.parameterService.getParameterEvaluator(DvToPdpExtractStep.class, PdpParameterConstants.TAXABLE_PAYMENT_REASON_CODES_BY_OWNERSHIP_CODES_PARAMETER_NAME, PdpParameterConstants.NON_TAXABLE_PAYMENT_REASON_CODES_BY_OWNERSHIP_CODES_PARAMETER_NAME, vendorOwnerCode, payReasonCode);
243                ParameterEvaluator parameterEvaluator2 = this.parameterService.getParameterEvaluator(DvToPdpExtractStep.class, PdpParameterConstants.TAXABLE_PAYMENT_REASON_CODES_BY_CORPORATION_OWNERSHIP_TYPE_CATEGORY_PARAMETER_NAME, PdpParameterConstants.NON_TAXABLE_PAYMENT_REASON_CODES_BY_CORPORATION_OWNERSHIP_TYPE_CATEGORY_PARAMETER_NAME, vendorOwnerCategoryCode, payReasonCode);
244                
245                if ( parameterEvaluator1.evaluationSucceeds() ) {
246                    pg.setTaxablePayment(Boolean.TRUE);
247                }
248                else if (this.parameterService.getParameterValue(DvToPdpExtractStep.class, PdpParameterConstants.CORPORATION_OWNERSHIP_TYPE_PARAMETER_NAME).equals("CP") &&
249                          StringUtils.isEmpty(vendorOwnerCategoryCode) &&
250                          this.parameterService.getParameterEvaluator(DvToPdpExtractStep.class, PdpParameterConstants.TAXABLE_PAYMENT_REASON_CODES_FOR_BLANK_CORPORATION_OWNERSHIP_TYPE_CATEGORIES_PARAMETER_NAME, payReasonCode).evaluationSucceeds()) {
251                    pg.setTaxablePayment(Boolean.TRUE);
252                }
253                else if (this.parameterService.getParameterValue(DvToPdpExtractStep.class, PdpParameterConstants.CORPORATION_OWNERSHIP_TYPE_PARAMETER_NAME).equals("CP")
254                            && !StringUtils.isEmpty(vendorOwnerCategoryCode)
255                            && parameterEvaluator2.evaluationSucceeds() ) {
256                    pg.setTaxablePayment(Boolean.TRUE);
257                }
258            }
259            
260            pg.setCity(pd.getDisbVchrPayeeCityName());
261            pg.setCountry(pd.getDisbVchrPayeeCountryCode());
262            pg.setLine1Address(pd.getDisbVchrPayeeLine1Addr());
263            pg.setLine2Address(pd.getDisbVchrPayeeLine2Addr());
264            pg.setPayeeName(pd.getDisbVchrPayeePersonName());
265            pg.setPayeeId(pd.getDisbVchrPayeeIdNumber());
266            pg.setState(pd.getDisbVchrPayeeStateCode());
267            pg.setZipCd(pd.getDisbVchrPayeeZipCode());
268            pg.setPaymentDate(document.getDisbursementVoucherDueDate());
269    
270            // It doesn't look like the DV has a way to do immediate processes
271            pg.setProcessImmediate(Boolean.FALSE);
272            pg.setPymtAttachment(document.isDisbVchrAttachmentCode());
273            pg.setPymtSpecialHandling(document.isDisbVchrSpecialHandlingCode());
274            pg.setNraPayment(pd.isDisbVchrAlienPaymentCode());
275    
276            pg.setBankCode(document.getDisbVchrBankCode());
277            pg.setPaymentStatusCode(KFSConstants.PdpConstants.PAYMENT_OPEN_STATUS_CODE);
278    
279            return pg;
280        }
281    
282        /**
283         * This method builds a payment detail object from the disbursement voucher document provided and links that detail file to the
284         * batch and process run date given.
285         * 
286         * @param document The disbursement voucher document to retrieve payment information from to populate the PaymentDetail.
287         * @param batch The batch file associated with the payment.
288         * @param processRunDate The date of the payment detail invoice.
289         * @return A fully populated PaymentDetail instance.
290         */
291        protected PaymentDetail buildPaymentDetail(DisbursementVoucherDocument document, Batch batch, Date processRunDate) {
292            LOG.debug("buildPaymentDetail() started");
293    
294            PaymentDetail pd = new PaymentDetail();
295            if (StringUtils.isNotEmpty(document.getDocumentHeader().getOrganizationDocumentNumber())) {
296                pd.setOrganizationDocNbr(document.getDocumentHeader().getOrganizationDocumentNumber());
297            }
298            pd.setCustPaymentDocNbr(document.getDocumentNumber());
299            pd.setInvoiceDate(new java.sql.Date(processRunDate.getTime()));
300            pd.setOrigInvoiceAmount(document.getDisbVchrCheckTotalAmount());
301            pd.setInvTotDiscountAmount(KualiDecimal.ZERO);
302            pd.setInvTotOtherCreditAmount(KualiDecimal.ZERO);
303            pd.setInvTotOtherDebitAmount(KualiDecimal.ZERO);
304            pd.setInvTotShipAmount(KualiDecimal.ZERO);
305            pd.setNetPaymentAmount(document.getDisbVchrCheckTotalAmount());
306            pd.setPrimaryCancelledPayment(Boolean.FALSE);
307            pd.setFinancialDocumentTypeCode(DisbursementVoucherConstants.DOCUMENT_TYPE_CHECKACH);
308            pd.setFinancialSystemOriginCode(KFSConstants.ORIGIN_CODE_KUALI);
309    
310            // Handle accounts
311            for (Iterator iter = document.getSourceAccountingLines().iterator(); iter.hasNext();) {
312                SourceAccountingLine sal = (SourceAccountingLine) iter.next();
313    
314                PaymentAccountDetail pad = new PaymentAccountDetail();
315                pad.setFinChartCode(sal.getChartOfAccountsCode());
316                pad.setAccountNbr(sal.getAccountNumber());
317                if (StringUtils.isNotEmpty(sal.getSubAccountNumber())) {
318                    pad.setSubAccountNbr(sal.getSubAccountNumber());
319                }
320                else {
321                    pad.setSubAccountNbr(KFSConstants.getDashSubAccountNumber());
322                }
323                pad.setFinObjectCode(sal.getFinancialObjectCode());
324                if (StringUtils.isNotEmpty(sal.getFinancialSubObjectCode())) {
325                    pad.setFinSubObjectCode(sal.getFinancialSubObjectCode());
326                }
327                else {
328                    pad.setFinSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
329                }
330                if (StringUtils.isNotEmpty(sal.getOrganizationReferenceId())) {
331                    pad.setOrgReferenceId(sal.getOrganizationReferenceId());
332                }
333                if (StringUtils.isNotEmpty(sal.getProjectCode())) {
334                    pad.setProjectCode(sal.getProjectCode());
335                }
336                else {
337                    pad.setProjectCode(KFSConstants.getDashProjectCode());
338                }
339                pad.setAccountNetAmount(sal.getAmount());
340                pd.addAccountDetail(pad);
341            }
342    
343            // Handle notes
344            DisbursementVoucherPayeeDetail dvpd = document.getDvPayeeDetail();
345    
346            int line = 0;
347            PaymentNoteText pnt = new PaymentNoteText();
348            pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
349            pnt.setCustomerNoteText("Info: " + document.getDisbVchrContactPersonName() + " " + document.getDisbVchrContactPhoneNumber());
350            pd.addNote(pnt);
351    
352            String dvSpecialHandlingPersonName = null;
353            String dvSpecialHandlingLine1Address = null;
354            String dvSpecialHandlingLine2Address = null;
355            String dvSpecialHandlingCity = null;
356            String dvSpecialHandlingState = null;
357            String dvSpecialHandlingZip = null;
358    
359            dvSpecialHandlingPersonName = dvpd.getDisbVchrSpecialHandlingPersonName();
360            dvSpecialHandlingLine1Address = dvpd.getDisbVchrSpecialHandlingLine1Addr();
361            dvSpecialHandlingLine2Address = dvpd.getDisbVchrSpecialHandlingLine2Addr();
362            dvSpecialHandlingCity = dvpd.getDisbVchrSpecialHandlingCityName();
363            dvSpecialHandlingState = dvpd.getDisbVchrSpecialHandlingStateCode();
364            dvSpecialHandlingZip = dvpd.getDisbVchrSpecialHandlingZipCode();
365    
366            if (StringUtils.isNotEmpty(dvSpecialHandlingPersonName)) {
367                pnt = new PaymentNoteText();
368                pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
369                pnt.setCustomerNoteText("Send Check To: " + dvSpecialHandlingPersonName);
370                if (LOG.isDebugEnabled()) {
371                    LOG.debug("Creating special handling person name note: "+pnt.getCustomerNoteText());
372                }
373                pd.addNote(pnt);
374            }
375            if (StringUtils.isNotEmpty(dvSpecialHandlingLine1Address)) {
376                pnt = new PaymentNoteText();
377                pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
378                pnt.setCustomerNoteText(dvSpecialHandlingLine1Address);
379                if (LOG.isDebugEnabled()) {
380                    LOG.debug("Creating special handling address 1 note: "+pnt.getCustomerNoteText());
381                }
382                pd.addNote(pnt);
383            }
384            if (StringUtils.isNotEmpty(dvSpecialHandlingLine2Address)) {
385                pnt = new PaymentNoteText();
386                pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
387                pnt.setCustomerNoteText(dvSpecialHandlingLine2Address);
388                if (LOG.isDebugEnabled()) {
389                    LOG.debug("Creating special handling address 2 note: "+pnt.getCustomerNoteText());
390                }
391                pd.addNote(pnt);
392            }
393            if (StringUtils.isNotEmpty(dvSpecialHandlingCity)) {
394                pnt = new PaymentNoteText();
395                pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
396                pnt.setCustomerNoteText(dvSpecialHandlingCity + ", " + dvSpecialHandlingState + " " + dvSpecialHandlingZip);
397                if (LOG.isDebugEnabled()) {
398                    LOG.debug("Creating special handling city note: "+pnt.getCustomerNoteText());
399                }
400                pd.addNote(pnt);
401            }
402            if (document.isDisbVchrAttachmentCode()) {
403                pnt = new PaymentNoteText();
404                pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
405                pnt.setCustomerNoteText("Attachment Included");
406                if (LOG.isDebugEnabled()) {
407                    LOG.debug("create attachment note: "+pnt.getCustomerNoteText());
408                }
409                pd.addNote(pnt);
410            }
411    
412            String paymentReasonCode = dvpd.getDisbVchrPaymentReasonCode();
413            if (parameterService.getParameterEvaluator(DisbursementVoucherDocument.class, DisbursementVoucherConstants.NONEMPLOYEE_TRAVEL_PAY_REASONS_PARM_NM, paymentReasonCode).evaluationSucceeds()) {
414                DisbursementVoucherNonEmployeeTravel dvnet = document.getDvNonEmployeeTravel();
415                
416                pnt = new PaymentNoteText();
417                pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
418                pnt.setCustomerNoteText("Reimbursement associated with " + dvnet.getDisbVchrServicePerformedDesc());
419                if (LOG.isDebugEnabled()) {
420                    LOG.debug("Creating non employee travel notes: "+pnt.getCustomerNoteText());
421                }
422                pd.addNote(pnt);
423    
424                pnt = new PaymentNoteText();
425                pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
426                pnt.setCustomerNoteText("The total per diem amount for your daily expenses is " + dvnet.getDisbVchrPerdiemCalculatedAmt());
427                if (LOG.isDebugEnabled()) {
428                    LOG.debug("Creating non employee travel notes: "+pnt.getCustomerNoteText());
429                }
430                pd.addNote(pnt);
431    
432                if (dvnet.getDisbVchrPersonalCarAmount() != null && dvnet.getDisbVchrPersonalCarAmount().compareTo(KualiDecimal.ZERO) != 0) {
433                    pnt = new PaymentNoteText();
434                    pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
435                    pnt.setCustomerNoteText("The total dollar amount for your vehicle mileage is " + dvnet.getDisbVchrPersonalCarAmount());
436                    if (LOG.isDebugEnabled()) {
437                        LOG.debug("Creating non employee travel vehicle note: "+pnt.getCustomerNoteText());
438                    }
439                    pd.addNote(pnt);
440    
441                    for (Iterator iter = dvnet.getDvNonEmployeeExpenses().iterator(); iter.hasNext();) {
442                        DisbursementVoucherNonEmployeeExpense exp = (DisbursementVoucherNonEmployeeExpense) iter.next();
443    
444                        if (line < (maxNoteLines - 8)) {
445                            pnt = new PaymentNoteText();
446                            pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
447                            pnt.setCustomerNoteText(exp.getDisbVchrExpenseCompanyName() + " " + exp.getDisbVchrExpenseAmount());
448                            if (LOG.isDebugEnabled()) {
449                                LOG.debug("Creating non employee travel expense note: "+pnt.getCustomerNoteText());
450                            }
451                            pd.addNote(pnt);
452                        }
453                    }
454                }
455            }
456            else if (parameterService.getParameterEvaluator(DisbursementVoucherDocument.class, DisbursementVoucherConstants.PREPAID_TRAVEL_PAYMENT_REASONS_PARM_NM, paymentReasonCode).evaluationSucceeds()) {
457                pnt = new PaymentNoteText();
458                pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
459                pnt.setCustomerNoteText("Payment is for the following individuals/charges:");
460                pd.addNote(pnt);
461                if (LOG.isDebugEnabled()) {
462                    LOG.info("Creating prepaid travel note note: "+pnt.getCustomerNoteText());
463                }
464    
465                DisbursementVoucherPreConferenceDetail dvpcd = document.getDvPreConferenceDetail();
466    
467                for (Iterator iter = dvpcd.getDvPreConferenceRegistrants().iterator(); iter.hasNext();) {
468                    DisbursementVoucherPreConferenceRegistrant dvpcr = (DisbursementVoucherPreConferenceRegistrant) iter.next();
469    
470                    if (line < (maxNoteLines - 8)) {
471                        pnt = new PaymentNoteText();
472                        pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
473                        pnt.setCustomerNoteText(dvpcr.getDvConferenceRegistrantName() + " " + dvpcr.getDisbVchrExpenseAmount());
474                        if (LOG.isDebugEnabled()) {
475                            LOG.debug("Creating pre-paid conference registrants note: "+pnt.getCustomerNoteText());
476                        }
477                        pd.addNote(pnt);
478                    }
479                }
480            }
481    
482            // Get the original, raw form, note text from the DV document. 
483            String text = document.getDisbVchrCheckStubText();
484            if (text != null && text.length() > 0) {
485                
486                // The WordUtils should be sufficient for the majority of cases.  This method will
487                // word wrap the whole string based on the MAX_NOTE_LINE_SIZE, separating each wrapped
488                // word by a newline character.  The 'wrap' method adds line feeds to the end causing
489                // the character length to exceed the max length by 1, hence the need for the replace
490                // method before splitting.
491                String   wrappedText = WordUtils.wrap(text, DisbursementVoucherConstants.MAX_NOTE_LINE_SIZE);
492                String[] noteLines   = wrappedText.replaceAll("[\r]", "").split("\\n");
493                
494                // Loop through all the note lines.
495                for (String noteLine : noteLines) {
496                    if (line < (maxNoteLines - 3) && !StringUtils.isEmpty(noteLine)) {
497                        
498                        // This should only happen if we encounter a word that is greater than the max length.
499                        // The only concern I have for this occurring is with URLs/email addresses.
500                        if (noteLine.length() > DisbursementVoucherConstants.MAX_NOTE_LINE_SIZE) {
501                            for (String choppedWord : chopWord(noteLine, DisbursementVoucherConstants.MAX_NOTE_LINE_SIZE)) {
502                                
503                                // Make sure we're still under the maximum number of note lines.
504                                if (line < (maxNoteLines - 3) && !StringUtils.isEmpty(choppedWord)) {
505                                    pnt = new PaymentNoteText();
506                                    pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
507                                    pnt.setCustomerNoteText(choppedWord.replaceAll("\\n", "").trim());
508                                }
509                                // We can't add any additional note lines, or we'll exceed the maximum, therefore
510                                // just break out of the loop early - there's nothing left to do.
511                                else {
512                                    break;
513                                }
514                            }
515                        }
516                        // This should be the most common case.  Simply create a new PaymentNoteText,
517                        // add the line at the correct line location.
518                        else {
519                            pnt = new PaymentNoteText();
520                            pnt.setCustomerNoteLineNbr(new KualiInteger(line++));
521                            pnt.setCustomerNoteText(noteLine.replaceAll("\\n", "").trim());
522                        }
523                        
524                        // Logging...
525                        if (LOG.isDebugEnabled()) {
526                            LOG.debug("Creating check stub text note: " + pnt.getCustomerNoteText());
527                        }
528                        pd.addNote(pnt);
529                    }
530                }
531            }
532            
533            return pd;
534        }
535        
536        /**
537         * This method will take a word and simply chop into smaller
538         * text segments that satisfy the limit requirements.  All words
539         * brute force chopped, with no regard to preserving whole words.
540         * 
541         * For example:
542         * 
543         *      "Java is a fun programming language!"
544         * 
545         * Might be chopped into:
546         * 
547         *      "Java is a fun prog"
548         *      "ramming language!"
549         *  
550         * @param word The word that needs chopping
551         * @param limit Number of character that should represent a chopped word
552         * @return String [] of chopped words
553         */
554        private String [] chopWord(String word, int limit)
555        {
556            StringBuilder builder = new StringBuilder();
557            if (word != null && word.trim().length() > 0) {
558                
559                char[] chars = word.toCharArray();
560                int index = 0;
561                
562                // First process all the words that fit into the limit.
563                for (int i = 0; i < chars.length/limit; i++) {
564                    builder.append(String.copyValueOf(chars, index, limit));
565                    builder.append("\n");
566                    
567                    index += limit;
568                }
569                
570                // Not all words will fit perfectly into the limit amount, so
571                // calculate the modulus value to determine any remaining characters.
572                int modValue =  chars.length%limit;
573                if (modValue > 0) {
574                    builder.append(String.copyValueOf(chars, index, modValue));
575                }
576                
577            }
578            
579            // Split the chopped words into individual segments.
580            return builder.toString().split("\\n");
581        }
582    
583        /**
584         * This method creates a Batch instance and populates it with the information provided.
585         * 
586         * @param campusCode The campus code used to retrieve a customer profile to be set on the batch.
587         * @param user The user who submitted the batch.
588         * @param processRunDate The date the batch was submitted and the date the customer profile was generated.
589         * @return A fully populated batch instance.
590         */
591        protected Batch createBatch(String campusCode, Person user, Date processRunDate) {
592            String orgCode = parameterService.getParameterValue(DisbursementVoucherDocument.class, DisbursementVoucherConstants.DvPdpExtractGroup.DV_PDP_ORG_CODE);
593            String subUnitCode = parameterService.getParameterValue(DisbursementVoucherDocument.class, DisbursementVoucherConstants.DvPdpExtractGroup.DV_PDP_SBUNT_CODE);
594            CustomerProfile customer = customerProfileService.get(campusCode, orgCode, subUnitCode);
595            if (customer == null) {
596                throw new IllegalArgumentException("Unable to find customer profile for " + campusCode + "/" + orgCode + "/" + subUnitCode);
597            }
598    
599            // Create the group for this campus
600            Batch batch = new Batch();
601            batch.setCustomerProfile(customer);
602            batch.setCustomerFileCreateTimestamp(new Timestamp(processRunDate.getTime()));
603            batch.setFileProcessTimestamp(new Timestamp(processRunDate.getTime()));
604            batch.setPaymentFileName(KFSConstants.DISBURSEMENT_VOUCHER_PDP_EXTRACT_FILE_NAME);
605            batch.setSubmiterUserId(user.getPrincipalId());
606    
607            // Set these for now, we will update them later
608            batch.setPaymentCount(KualiInteger.ZERO);
609            batch.setPaymentTotalAmount(KualiDecimal.ZERO);
610    
611            businessObjectService.save(batch);
612    
613            return batch;
614        }
615    
616        /**
617         * This method retrieves a collection of campus instances representing all the campuses which currently have disbursement
618         * vouchers with the status code provided.
619         * 
620         * @param statusCode The status code to retrieve disbursement vouchers by.
621         * @return A collection of campus codes of all the campuses with disbursement vouchers in the status given.
622         */
623        protected Set<String> getCampusListByDocumentStatusCode(String statusCode) {
624            LOG.debug("getCampusListByDocumentStatusCode() started");
625    
626            Set<String> campusSet = new HashSet<String>();
627    
628            Collection<DisbursementVoucherDocument> docs = disbursementVoucherDao.getDocumentsByHeaderStatus(statusCode);
629            for (DisbursementVoucherDocument element : docs) {
630                String dvdCampusCode = element.getCampusCode();
631                campusSet.add(dvdCampusCode);
632            }
633    
634            return campusSet;
635        }
636    
637        /**
638         * This method retrieves a list of disbursement voucher documents that are in the status provided for the campus code given.
639         * 
640         * @param statusCode The status of the disbursement vouchers to be retrieved.
641         * @param campusCode The campus code that the disbursement vouchers will be associated with.
642         * @return A collection of disbursement voucher objects that meet the search criteria given.
643         */
644        protected Collection<DisbursementVoucherDocument> getListByDocumentStatusCodeCampus(String statusCode, String campusCode) {
645            LOG.debug("getListByDocumentStatusCodeCampus() started");
646    
647            Collection<DisbursementVoucherDocument> list = new ArrayList<DisbursementVoucherDocument>();
648    
649            try {
650                Collection<DisbursementVoucherDocument> docs = SpringContext.getBean(FinancialSystemDocumentService.class).findByDocumentHeaderStatusCode(DisbursementVoucherDocument.class, statusCode);
651                for (DisbursementVoucherDocument element : docs) {
652                    String dvdCampusCode = element.getCampusCode();
653    
654                    if (dvdCampusCode.equals(campusCode) && DisbursementVoucherConstants.PAYMENT_METHOD_CHECK.equals(element.getDisbVchrPaymentMethodCode())) {
655                        list.add(element);
656                    }
657                }
658            }
659            catch (WorkflowException we) {
660                LOG.error("Could not load Disbursement Voucher Documents with status code = " + statusCode + ": " + we);
661                throw new RuntimeException(we);
662            }
663    
664            return list;
665        }
666    
667        /**
668         * This cancels the disbursement voucher
669         * 
670         * @param dv the disbursement voucher document to cancel
671         * @param processDate the date of the cancelation
672         * @see org.kuali.kfs.fp.batch.service.DisbursementVoucherExtractService#cancelExtractedDisbursementVoucher(org.kuali.kfs.fp.document.DisbursementVoucherDocument)
673         */
674        public void cancelExtractedDisbursementVoucher(DisbursementVoucherDocument dv, java.sql.Date processDate) {
675            if (dv.getCancelDate() == null) {
676                try {
677                    BusinessObjectService boService = SpringContext.getBean(BusinessObjectService.class);
678                    // set the canceled date
679                    dv.setCancelDate(processDate);
680                    dv.refreshReferenceObject("generalLedgerPendingEntries");
681                    if (ObjectUtils.isNull(dv.getGeneralLedgerPendingEntries()) || dv.getGeneralLedgerPendingEntries().size() == 0) {
682                        // generate all the pending entries for the document
683                        SpringContext.getBean(GeneralLedgerPendingEntryService.class).generateGeneralLedgerPendingEntries(dv);
684                        // for each pending entry, opposite-ify it and reattach it to the document
685                        GeneralLedgerPendingEntrySequenceHelper glpeSeqHelper = new GeneralLedgerPendingEntrySequenceHelper();
686                        for (GeneralLedgerPendingEntry glpe : dv.getGeneralLedgerPendingEntries()) {
687                            oppositifyEntry(glpe, boService, glpeSeqHelper);
688                        }
689                    }
690                    else {
691                        List<GeneralLedgerPendingEntry> newGLPEs = new ArrayList<GeneralLedgerPendingEntry>();
692                        GeneralLedgerPendingEntrySequenceHelper glpeSeqHelper = new GeneralLedgerPendingEntrySequenceHelper(dv.getGeneralLedgerPendingEntries().size() + 1);
693                        for (GeneralLedgerPendingEntry glpe : dv.getGeneralLedgerPendingEntries()) {
694                            glpe.refresh();
695                            if (glpe.getFinancialDocumentApprovedCode().equals(KFSConstants.PENDING_ENTRY_APPROVED_STATUS_CODE.PROCESSED)) {
696                                // damn! it got processed! well, make a copy, oppositify, and save
697                                GeneralLedgerPendingEntry undoer = new GeneralLedgerPendingEntry(glpe);
698                                oppositifyEntry(undoer, boService, glpeSeqHelper);
699                                newGLPEs.add(undoer);
700                            }
701                            else {
702                                // just delete the GLPE before anything happens to it
703                                boService.delete(glpe);
704                            }
705                        }
706                        dv.setGeneralLedgerPendingEntries(newGLPEs);
707                    }
708                    // set the financial document status to canceled
709                    dv.getDocumentHeader().setFinancialDocumentStatusCode(KFSConstants.DocumentStatusCodes.CANCELLED);
710                    // save the document
711                    SpringContext.getBean(DocumentService.class).saveDocument(dv, AccountingDocumentSaveWithNoLedgerEntryGenerationEvent.class);
712                }
713                catch (WorkflowException we) {
714                    LOG.error("encountered workflow exception while attempting to save Disbursement Voucher: " + dv.getDocumentNumber() + " " + we);
715                    throw new RuntimeException(we);
716                }
717            }
718        }
719    
720        /**
721         * Updates the given general ledger pending entry so that it will have the opposite effect of what it was created to do; this,
722         * in effect, undoes the entries that were already posted for this document
723         * 
724         * @param glpe the general ledger pending entry to undo
725         */
726        protected void oppositifyEntry(GeneralLedgerPendingEntry glpe, BusinessObjectService boService, GeneralLedgerPendingEntrySequenceHelper glpeSeqHelper) {
727            if (glpe.getTransactionDebitCreditCode().equals(KFSConstants.GL_CREDIT_CODE)) {
728                glpe.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
729            }
730            else if (glpe.getTransactionDebitCreditCode().equals(KFSConstants.GL_DEBIT_CODE)) {
731                glpe.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
732            }
733            glpe.setTransactionLedgerEntrySequenceNumber(glpeSeqHelper.getSequenceCounter());
734            glpeSeqHelper.increment();
735            glpe.setFinancialDocumentApprovedCode(KFSConstants.PENDING_ENTRY_APPROVED_STATUS_CODE.APPROVED);
736            boService.save(glpe);
737        }
738    
739        /**
740         * This updates the disbursement voucher so that when it is re-extracted, information about it will be accurate
741         * 
742         * @param dv the disbursement voucher document to reset
743         * @param processDate the date of the reseting
744         * @see org.kuali.kfs.fp.batch.service.DisbursementVoucherExtractService#resetExtractedDisbursementVoucher(org.kuali.kfs.fp.document.DisbursementVoucherDocument)
745         */
746        public void resetExtractedDisbursementVoucher(DisbursementVoucherDocument dv, java.sql.Date processDate) {
747            try {
748                // 1. reset the extracted date
749                dv.setExtractDate(null);
750                dv.setPaidDate(null);
751                // 2. save the doc
752                SpringContext.getBean(DocumentService.class).saveDocument(dv, AccountingDocumentSaveWithNoLedgerEntryGenerationEvent.class);
753            }
754            catch (WorkflowException we) {
755                LOG.error("encountered workflow exception while attempting to save Disbursement Voucher: " + dv.getDocumentNumber() + " " + we);
756                throw new RuntimeException(we);
757            }
758        }
759    
760        /**
761         * Looks up the document using document service, and deals with any nasty WorkflowException or ClassCastExceptions that pop up
762         * 
763         * @param documentNumber the number of the document to look up
764         * @return the dv doc if found, or null otherwise
765         * @see org.kuali.kfs.fp.batch.service.DisbursementVoucherExtractService#getDocumentById(java.lang.String)
766         */
767        public DisbursementVoucherDocument getDocumentById(String documentNumber) {
768            DisbursementVoucherDocument dv = null;
769            try {
770                dv = (DisbursementVoucherDocument) SpringContext.getBean(DocumentService.class).getByDocumentHeaderId(documentNumber);
771            }
772            catch (WorkflowException we) {
773                LOG.error("encountered workflow exception while attempting to retrieve Disbursement Voucher: " + dv.getDocumentNumber() + " " + we);
774                throw new RuntimeException(we);
775            }
776            return dv;
777        }
778    
779        /**
780         * Marks the disbursement voucher as paid by setting its paid date
781         * 
782         * @param dv the dv document to mark as paid
783         * @param processDate the date when the dv was paid
784         * @see org.kuali.kfs.fp.batch.service.DisbursementVoucherExtractService#markDisbursementVoucherAsPaid(org.kuali.kfs.fp.document.DisbursementVoucherDocument)
785         */
786        public void markDisbursementVoucherAsPaid(DisbursementVoucherDocument dv, java.sql.Date processDate) {
787            try {
788                dv.setPaidDate(processDate);
789                SpringContext.getBean(DocumentService.class).saveDocument(dv, AccountingDocumentSaveWithNoLedgerEntryGenerationEvent.class);
790            }
791            catch (WorkflowException we) {
792                LOG.error("encountered workflow exception while attempting to save Disbursement Voucher: " + dv.getDocumentNumber() + " " + we);
793                throw new RuntimeException(we);
794            }
795        }
796    
797        /**
798         * This method sets the disbursementVoucherDao instance.
799         * 
800         * @param disbursementVoucherDao The DisbursementVoucherDao to be set.
801         */
802        public void setDisbursementVoucherDao(DisbursementVoucherDao disbursementVoucherDao) {
803            this.disbursementVoucherDao = disbursementVoucherDao;
804        }
805    
806        /**
807         * This method sets the ParameterService instance.
808         * 
809         * @param parameterService The ParameterService to be set.
810         */
811        public void setParameterService(ParameterService parameterService) {
812            this.parameterService = parameterService;
813        }
814    
815        /**
816         * This method sets the dateTimeService instance.
817         * 
818         * @param dateTimeService The DateTimeService to be set.
819         */
820        public void setDateTimeService(DateTimeService dateTimeService) {
821            this.dateTimeService = dateTimeService;
822        }
823    
824        /**
825         * This method sets the customerProfileService instance.
826         * 
827         * @param customerProfileService The CustomerProfileService to be set.
828         */
829        public void setCustomerProfileService(CustomerProfileService customerProfileService) {
830            this.customerProfileService = customerProfileService;
831        }
832    
833        /**
834         * This method sets the paymentFileService instance.
835         * 
836         * @param paymentFileService The PaymentFileService to be set.
837         */
838        public void setPaymentFileService(PaymentFileService paymentFileService) {
839            this.paymentFileService = paymentFileService;
840        }
841    
842        /**
843         * This method sets the paymentGroupService instance.
844         * 
845         * @param paymentGroupService The PaymentGroupService to be set.
846         */
847        public void setPaymentGroupService(PaymentGroupService paymentGroupService) {
848            this.paymentGroupService = paymentGroupService;
849        }
850    
851        /**
852         * Sets the businessObjectService attribute value.
853         * 
854         * @param businessObjectService The businessObjectService to set.
855         */
856        public void setBusinessObjectService(BusinessObjectService businessObjectService) {
857            this.businessObjectService = businessObjectService;
858        }
859    
860        /**
861         * Sets the paymentFileEmailService attribute value.
862         * 
863         * @param paymentFileEmailService The paymentFileEmailService to set.
864         */
865        public void setPaymentFileEmailService(PdpEmailService paymentFileEmailService) {
866            this.paymentFileEmailService = paymentFileEmailService;
867        }
868    
869        /**
870         * @return Returns the personService.
871         */
872        protected PersonService<Person> getPersonService() {
873            if(personService==null)
874                personService = SpringContext.getBean(PersonService.class);
875            return personService;
876        }
877    
878    }