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.gl.batch.service.impl;
017
018 import java.io.File;
019 import java.io.FileInputStream;
020 import java.io.FileNotFoundException;
021 import java.io.IOException;
022 import java.io.InputStream;
023 import java.io.PrintStream;
024 import java.util.Collection;
025 import java.util.HashMap;
026 import java.util.HashSet;
027 import java.util.Iterator;
028 import java.util.List;
029 import java.util.Map;
030 import java.util.Set;
031
032 import org.apache.commons.collections.IteratorUtils;
033 import org.apache.commons.io.IOUtils;
034 import org.apache.commons.lang.StringUtils;
035 import org.apache.log4j.Logger;
036 import org.kuali.kfs.coa.businessobject.Account;
037 import org.kuali.kfs.coa.businessobject.BalanceType;
038 import org.kuali.kfs.coa.businessobject.ObjectType;
039 import org.kuali.kfs.coa.service.AccountService;
040 import org.kuali.kfs.gl.GeneralLedgerConstants;
041 import org.kuali.kfs.gl.batch.CollectorBatch;
042 import org.kuali.kfs.gl.batch.CollectorStep;
043 import org.kuali.kfs.gl.batch.service.CollectorHelperService;
044 import org.kuali.kfs.gl.batch.service.CollectorScrubberService;
045 import org.kuali.kfs.gl.businessobject.CollectorDetail;
046 import org.kuali.kfs.gl.businessobject.CollectorHeader;
047 import org.kuali.kfs.gl.businessobject.OriginEntryFull;
048 import org.kuali.kfs.gl.businessobject.OriginEntryInformation;
049 import org.kuali.kfs.gl.report.CollectorReportData;
050 import org.kuali.kfs.gl.report.PreScrubberReportData;
051 import org.kuali.kfs.gl.service.CollectorDetailService;
052 import org.kuali.kfs.gl.service.OriginEntryGroupService;
053 import org.kuali.kfs.gl.service.OriginEntryService;
054 import org.kuali.kfs.gl.service.PreScrubberService;
055 import org.kuali.kfs.gl.service.impl.CollectorScrubberStatus;
056 import org.kuali.kfs.sys.KFSConstants;
057 import org.kuali.kfs.sys.KFSKeyConstants;
058 import org.kuali.kfs.sys.KFSPropertyConstants;
059 import org.kuali.kfs.sys.KFSConstants.SystemGroupParameterNames;
060 import org.kuali.kfs.sys.batch.BatchInputFileType;
061 import org.kuali.kfs.sys.batch.service.BatchInputFileService;
062 import org.kuali.kfs.sys.context.SpringContext;
063 import org.kuali.kfs.sys.exception.ParseException;
064 import org.kuali.rice.kns.service.BusinessObjectService;
065 import org.kuali.rice.kns.service.DateTimeService;
066 import org.kuali.rice.kns.service.KualiConfigurationService;
067 import org.kuali.rice.kns.service.ParameterService;
068 import org.kuali.rice.kns.util.GlobalVariables;
069 import org.kuali.rice.kns.util.KualiDecimal;
070 import org.kuali.rice.kns.util.MessageMap;
071 import org.kuali.rice.kns.util.ObjectUtils;
072
073 /**
074 * The base implementation of CollectorHelperService
075 * @see org.kuali.kfs.gl.batch.service.CollectorService
076 */
077 public class CollectorHelperServiceImpl implements CollectorHelperService {
078 private static Logger LOG = Logger.getLogger(CollectorHelperServiceImpl.class);
079
080 private static final String CURRENCY_SYMBOL = "$";
081
082 private CollectorDetailService collectorDetailService;
083 private OriginEntryService originEntryService;
084 private OriginEntryGroupService originEntryGroupService;
085 private ParameterService parameterService;
086 private KualiConfigurationService configurationService;
087 private DateTimeService dateTimeService;
088 private BatchInputFileService batchInputFileService;
089 private CollectorScrubberService collectorScrubberService;
090 private AccountService accountService;
091 private PreScrubberService preScrubberService;
092 private String batchFileDirectoryName;
093
094 /**
095 * Parses the given file, validates the batch, stores the entries, and sends email.
096 * @param fileName - name of file to load (including path)
097 * @param group the group into which to persist the origin entries for the collector batch/file
098 * @param collectorReportData the object used to store all of the collector status information for reporting
099 * @param collectorScrubberStatuses if the collector scrubber is able to be invoked upon this collector batch, then the status
100 * info of the collector status run is added to the end of this list
101 * @param the output stream to which to store origin entries that properly pass validation
102 * @return boolean - true if load was successful, false if errors were encountered
103 * @see org.kuali.kfs.gl.batch.service.CollectorService#loadCollectorFile(java.lang.String)
104 */
105 public boolean loadCollectorFile(String fileName, CollectorReportData collectorReportData, List<CollectorScrubberStatus> collectorScrubberStatuses, BatchInputFileType collectorInputFileType, PrintStream originEntryOutputPs) {
106 boolean isValid = true;
107
108 MessageMap fileMessageMap = collectorReportData.getMessageMapForFileName(fileName);
109
110 List<CollectorBatch> batches = doCollectorFileParse(fileName, fileMessageMap, collectorInputFileType, collectorReportData);
111 for (int i = 0; i < batches.size(); i++) {
112 CollectorBatch collectorBatch = batches.get(i);
113
114 collectorBatch.setBatchName(fileName + " Batch " + String.valueOf(i + 1));
115 collectorReportData.addBatch(collectorBatch);
116
117 isValid &= loadCollectorBatch(collectorBatch, fileName, i + 1, collectorReportData, collectorScrubberStatuses, collectorInputFileType, originEntryOutputPs);
118 }
119 return isValid;
120 }
121
122 protected boolean loadCollectorBatch(CollectorBatch batch, String fileName, int batchIndex, CollectorReportData collectorReportData, List<CollectorScrubberStatus> collectorScrubberStatuses, BatchInputFileType collectorInputFileType, PrintStream originEntryOutputPs) {
123 boolean isValid = true;
124
125 MessageMap messageMap = batch.getMessageMap();
126 // terminate if there were parse errors
127 if (!messageMap.isEmpty()) {
128 isValid = false;
129 }
130
131 if (isValid) {
132 collectorReportData.setNumInputDetails(batch);
133 // check totals
134 isValid = checkTrailerTotals(batch, collectorReportData, messageMap);
135 }
136
137 // do validation, base collector files rules and total checks
138 if (isValid) {
139 isValid = performValidation(batch, messageMap);
140 }
141
142 if (isValid) {
143 // mark batch as valid
144 collectorReportData.markValidationStatus(batch, true);
145
146 prescrubParsedCollectorBatch(batch, collectorReportData);
147
148 String collectorFileDirectoryName = collectorInputFileType.getDirectoryPath();
149 // create a input file for scrubber
150 String collectorInputFileNameForScrubber = batchFileDirectoryName + File.separator + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_BACKUP_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
151 PrintStream inputFilePs = null;
152 try {
153 inputFilePs = new PrintStream(collectorInputFileNameForScrubber);
154
155 for (OriginEntryFull entry : batch.getOriginEntries()){
156 inputFilePs.printf("%s\n", entry.getLine());
157 }
158 } catch (IOException e) {
159 throw new RuntimeException("loadCollectorFile Stopped: " + e.getMessage(), e);
160 } finally {
161 IOUtils.closeQuietly(inputFilePs);
162 }
163
164 CollectorScrubberStatus collectorScrubberStatus = collectorScrubberService.scrub(batch, collectorReportData, collectorFileDirectoryName);
165 collectorScrubberStatuses.add(collectorScrubberStatus);
166 processInterDepartmentalBillingAmounts(batch);
167
168 // store origin group, entries, and collector detairs
169 String collectorDemergerOutputFileName = batchFileDirectoryName + File.separator + GeneralLedgerConstants.BatchFileSystem.COLLECTOR_DEMERGER_VAILD_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
170 batch.setDefaultsAndStore(collectorReportData, collectorDemergerOutputFileName, originEntryOutputPs);
171 collectorReportData.incrementNumPersistedBatches();
172 }
173 else {
174 collectorReportData.incrementNumNonPersistedBatches();
175 collectorReportData.incrementNumNotPersistedOriginEntryRecords(batch.getOriginEntries().size());
176 collectorReportData.incrementNumNotPersistedCollectorDetailRecords(batch.getCollectorDetails().size());
177 // mark batch as invalid
178 collectorReportData.markValidationStatus(batch, false);
179 }
180
181 return isValid;
182 }
183
184 /**
185 * After a parse error, tries to go through the file to see if the email address can be determined. This method will not throw
186 * an exception.
187 *
188 * It's not doing much right now, just returning null
189 *
190 * @param fileName the name of the file that a parsing error occurred on
191 * @return the email from the file
192 */
193 protected String attemptToParseEmailAfterParseError(String fileName) {
194 return null;
195 }
196
197 /**
198 * Calls batch input service to parse the xml contents into an object. Any errors will be contained in GlobalVariables.MessageMap
199 *
200 * @param fileName the name of the file to parse
201 * @param MessageMap a map of errors resultant from the parsing
202 * @return the CollectorBatch of details parsed from the file
203 */
204 protected List<CollectorBatch> doCollectorFileParse(String fileName, MessageMap messageMap, BatchInputFileType collectorInputFileType, CollectorReportData collectorReportData) {
205
206 InputStream inputStream = null;
207 try {
208 inputStream = new FileInputStream(fileName);
209 }
210 catch (FileNotFoundException e) {
211 LOG.error("file to parse not found " + fileName, e);
212 collectorReportData.markUnparsableFileNames(fileName);
213 throw new RuntimeException("Cannot find the file requested to be parsed " + fileName + " " + e.getMessage(), e);
214 }
215 catch (RuntimeException e) {
216 collectorReportData.markUnparsableFileNames(fileName);
217 throw e;
218 }
219
220 List<CollectorBatch> parsedObject = null;
221 try {
222 byte[] fileByteContent = IOUtils.toByteArray(inputStream);
223 parsedObject = (List<CollectorBatch>) batchInputFileService.parse(collectorInputFileType, fileByteContent);
224 }
225 catch (IOException e) {
226 LOG.error("error while getting file bytes: " + e.getMessage(), e);
227 collectorReportData.markUnparsableFileNames(fileName);
228 throw new RuntimeException("Error encountered while attempting to get file bytes: " + e.getMessage(), e);
229 }
230 catch (ParseException e1) {
231 LOG.error("errors parsing file " + e1.getMessage(), e1);
232 collectorReportData.markUnparsableFileNames(fileName);
233 messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.ERROR_BATCH_UPLOAD_PARSING_XML, new String[] { e1.getMessage() });
234 }
235 catch (RuntimeException e) {
236 collectorReportData.markUnparsableFileNames(fileName);
237 throw e;
238 }
239
240 return parsedObject;
241 }
242
243 protected void prescrubParsedCollectorBatch(CollectorBatch collectorBatch, CollectorReportData collectorReportData) {
244 if (preScrubberService.deriveChartOfAccountsCodeIfSpaces()) {
245 PreScrubberReportData preScrubberReportData = collectorReportData.getPreScrubberReportData();
246
247 int inputRecords = collectorBatch.getOriginEntries().size();
248 Set<String> noChartCodesCache = new HashSet<String>();
249 Set<String> multipleChartCodesCache = new HashSet<String>();
250 Map<String, String> accountNumberToChartCodeCache = new HashMap<String, String>();
251
252 Iterator<Object> originEntryAndDetailIterator = IteratorUtils.chainedIterator(collectorBatch.getOriginEntries().iterator(), collectorBatch.getCollectorDetails().iterator());
253 while (originEntryAndDetailIterator.hasNext()) {
254 Object originEntryOrDetail = originEntryAndDetailIterator.next();
255 if (StringUtils.isBlank(extractChartOfAccountsCode(originEntryOrDetail))) {
256 String accountNumber = extractAccountNumber(originEntryOrDetail);
257
258 boolean nonExistent = false;
259 boolean multipleFound = false;
260 String chartOfAccountsCode = null;
261
262 if (noChartCodesCache.contains(accountNumber)) {
263 nonExistent = true;
264 }
265 else if (multipleChartCodesCache.contains(accountNumber)) {
266 multipleFound = true;
267 }
268 else if (accountNumberToChartCodeCache.containsKey(accountNumber)) {
269 chartOfAccountsCode = accountNumberToChartCodeCache.get(accountNumber);
270 }
271 else {
272 Collection<Account> accounts = accountService.getAccountsForAccountNumber(accountNumber);
273 if (accounts.size() == 1) {
274 chartOfAccountsCode = accounts.iterator().next().getChartOfAccountsCode();
275 accountNumberToChartCodeCache.put(accountNumber, chartOfAccountsCode);
276 }
277 else if (accounts.size() == 0) {
278 noChartCodesCache.add(accountNumber);
279 nonExistent = true;
280 }
281 else {
282 multipleChartCodesCache.add(accountNumber);
283 multipleFound = true;
284 }
285 }
286
287 if (!nonExistent && !multipleFound) {
288 setChartOfAccountsCode(originEntryOrDetail, chartOfAccountsCode);
289 }
290 }
291 }
292
293 preScrubberReportData.getAccountsWithMultipleCharts().addAll(multipleChartCodesCache);
294 preScrubberReportData.getAccountsWithNoCharts().addAll(noChartCodesCache);
295 preScrubberReportData.setInputRecords(preScrubberReportData.getInputRecords() + inputRecords);
296 preScrubberReportData.setOutputRecords(preScrubberReportData.getOutputRecords() + inputRecords);
297 }
298 }
299
300 protected String extractChartOfAccountsCode(Object originEntryOrDetail) {
301 if (originEntryOrDetail instanceof OriginEntryInformation)
302 return ((OriginEntryInformation) originEntryOrDetail).getChartOfAccountsCode();
303 return ((CollectorDetail) originEntryOrDetail).getChartOfAccountsCode();
304 }
305
306 protected String extractAccountNumber(Object originEntryOrDetail) {
307 if (originEntryOrDetail instanceof OriginEntryInformation)
308 return ((OriginEntryInformation) originEntryOrDetail).getAccountNumber();
309 return ((CollectorDetail) originEntryOrDetail).getAccountNumber();
310 }
311
312 protected void setChartOfAccountsCode(Object originEntryOrDetail, String chartOfAccountsCode) {
313 if (originEntryOrDetail instanceof OriginEntryInformation)
314 ((OriginEntryInformation) originEntryOrDetail).setChartOfAccountsCode(chartOfAccountsCode);
315 else
316 ((CollectorDetail) originEntryOrDetail).setChartOfAccountsCode(chartOfAccountsCode);
317 }
318
319 /**
320 * Validates the contents of a parsed file.
321 *
322 * @param batch - batch to validate
323 * @return boolean - true if validation was OK, false if there were errors
324 * @see org.kuali.kfs.gl.batch.service.CollectorHelperService#performValidation(org.kuali.kfs.gl.batch.CollectorBatch)
325 */
326 public boolean performValidation(CollectorBatch batch) {
327 return performValidation(batch, GlobalVariables.getMessageMap());
328 }
329
330 /**
331 * Performs the following checks on the collector batch: Any errors will be contained in GlobalVariables.MessageMap
332 *
333 * @param batch - batch to validate
334 * @param MessageMap the map into which to put errors encountered during validation
335 * @return boolean - true if validation was successful, false it not
336 */
337 protected boolean performValidation(CollectorBatch batch, MessageMap MessageMap) {
338 boolean valid = performCollectorHeaderValidation(batch, MessageMap);
339
340 performUppercasing(batch);
341
342 boolean performDuplicateHeaderCheck = parameterService.getIndicatorParameter(CollectorStep.class, SystemGroupParameterNames.COLLECTOR_PERFORM_DUPLICATE_HEADER_CHECK);
343 if (valid && performDuplicateHeaderCheck) {
344 valid = duplicateHeaderCheck(batch, MessageMap);
345 }
346 if (valid) {
347 valid = checkForMixedDocumentTypes(batch, MessageMap);
348 }
349
350 if (valid) {
351 valid = checkForMixedBalanceTypes(batch, MessageMap);
352 }
353
354 if (valid) {
355 valid = checkDetailKeys(batch, MessageMap);
356 }
357
358 return valid;
359 }
360
361 /**
362 * Uppercases sub-account, sub-object, and project fields
363 *
364 * @param batch CollectorBatch with data to uppercase
365 */
366 protected void performUppercasing(CollectorBatch batch) {
367 for (OriginEntryFull originEntry : batch.getOriginEntries()) {
368 if (StringUtils.isNotBlank(originEntry.getSubAccountNumber())) {
369 originEntry.setSubAccountNumber(originEntry.getSubAccountNumber().toUpperCase());
370 }
371
372 if (StringUtils.isNotBlank(originEntry.getFinancialSubObjectCode())) {
373 originEntry.setFinancialSubObjectCode(originEntry.getFinancialSubObjectCode().toUpperCase());
374 }
375
376 if (StringUtils.isNotBlank(originEntry.getProjectCode())) {
377 originEntry.setProjectCode(originEntry.getProjectCode().toUpperCase());
378 }
379 }
380
381 for (CollectorDetail detail : batch.getCollectorDetails()) {
382 if (StringUtils.isNotBlank(detail.getSubAccountNumber())) {
383 detail.setSubAccountNumber(detail.getSubAccountNumber().toUpperCase());
384 }
385
386 if (StringUtils.isNotBlank(detail.getFinancialSubObjectCode())) {
387 detail.setFinancialSubObjectCode(detail.getFinancialSubObjectCode().toUpperCase());
388 }
389 }
390 }
391
392 protected boolean performCollectorHeaderValidation(CollectorBatch batch, MessageMap messageMap) {
393 if (batch.isHeaderlessBatch()) {
394 // if it's a headerless batch, don't validate the header, but it's still an error
395 return false;
396 }
397 boolean valid = true;
398 if (StringUtils.isBlank(batch.getChartOfAccountsCode())) {
399 valid = false;
400 messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.HEADER_CHART_CODE_REQUIRED);
401 }
402 if (StringUtils.isBlank(batch.getOrganizationCode())) {
403 valid = false;
404 messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.HEADER_ORGANIZATION_CODE_REQUIRED);
405 }
406 if (StringUtils.isBlank(batch.getCampusCode())) {
407 valid = false;
408 messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.HEADER_CAMPUS_CODE_REQUIRED);
409 }
410 if (StringUtils.isBlank(batch.getPhoneNumber())) {
411 valid = false;
412 messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.HEADER_PHONE_NUMBER_REQUIRED);
413 }
414 if (StringUtils.isBlank(batch.getMailingAddress())) {
415 valid = false;
416 messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.HEADER_MAILING_ADDRESS_REQUIRED);
417 }
418 if (StringUtils.isBlank(batch.getDepartmentName())) {
419 valid = false;
420 messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.HEADER_DEPARTMENT_NAME_REQUIRED);
421 }
422 return valid;
423 }
424
425 /**
426 * Modifies the amounts in the ID Billing Detail rows, depending on specific business rules. For this default implementation,
427 * see the {@link #negateAmountIfNecessary(InterDepartmentalBilling, BalanceTyp, ObjectType, CollectorBatch)} method to see how
428 * the billing detail amounts are modified.
429 *
430 * @param batch a CollectorBatch to process
431 */
432 protected void processInterDepartmentalBillingAmounts(CollectorBatch batch) {
433 for (CollectorDetail collectorDetail : batch.getCollectorDetails()) {
434 String balanceTypeCode = getBalanceTypeCode(collectorDetail, batch);
435
436 BalanceType balanceTyp = new BalanceType();
437 balanceTyp.setFinancialBalanceTypeCode(balanceTypeCode);
438 balanceTyp = (BalanceType) SpringContext.getBean(BusinessObjectService.class).retrieve(balanceTyp);
439 if (balanceTyp == null) {
440 // no balance type in db
441 LOG.info("No balance type code found for ID billing record. " + collectorDetail);
442 continue;
443 }
444
445 collectorDetail.refreshReferenceObject(KFSPropertyConstants.FINANCIAL_OBJECT);
446 if (collectorDetail.getFinancialObject() == null) {
447 // no object code in db
448 LOG.info("No object code found for ID billing record. " + collectorDetail);
449 continue;
450 }
451 ObjectType objectType = collectorDetail.getFinancialObject().getFinancialObjectType();
452
453 /** Commented out for KULRNE-5922 */
454 // negateAmountIfNecessary(collectorDetail, balanceTyp, objectType, batch);
455 }
456 }
457
458 /**
459 * Negates the amount of the internal departmental billing detail record if necessary. For this default implementation, if the
460 * balance type's offset indicator is yes and the object type has a debit indicator, then the amount is negated.
461 *
462 * @param collectorDetail the collector detail
463 * @param balanceTyp the balance type
464 * @param objectType the object type
465 * @param batch the patch to which the interDepartmentalBilling parameter belongs
466 */
467 protected void negateAmountIfNecessary(CollectorDetail collectorDetail, BalanceType balanceTyp, ObjectType objectType, CollectorBatch batch) {
468 if (balanceTyp != null && objectType != null) {
469 if (balanceTyp.isFinancialOffsetGenerationIndicator()) {
470 if (KFSConstants.GL_DEBIT_CODE.equals(objectType.getFinObjectTypeDebitcreditCd())) {
471 KualiDecimal amount = collectorDetail.getCollectorDetailItemAmount();
472 amount = amount.negated();
473 collectorDetail.setCollectorDetailItemAmount(amount);
474 }
475 }
476 }
477 }
478
479 /**
480 * Returns the balance type code for the interDepartmentalBilling record. This default implementation will look into the system
481 * parameters to determine the balance type
482 *
483 * @param interDepartmentalBilling a inter departmental billing detail record
484 * @param batch the batch to which the interDepartmentalBilling billing belongs
485 * @return the balance type code for the billing detail
486 */
487 protected String getBalanceTypeCode(CollectorDetail collectorDetail, CollectorBatch batch) {
488 return collectorDetail.getFinancialBalanceTypeCode();
489 }
490
491 /**
492 * Checks header against previously loaded batch headers for a duplicate submission.
493 *
494 * @param batch - batch to check
495 * @return true if header if OK, false if header was used previously
496 */
497 protected boolean duplicateHeaderCheck(CollectorBatch batch, MessageMap messageMap) {
498 boolean validHeader = true;
499
500 CollectorHeader foundHeader = batch.retrieveDuplicateHeader();
501
502 if (foundHeader != null) {
503 LOG.error("batch header was matched to a previously loaded batch");
504 messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.DUPLICATE_BATCH_HEADER);
505
506 validHeader = false;
507 }
508
509 return validHeader;
510 }
511
512 /**
513 * Iterates through the origin entries and builds a map on the document types. Then checks there was only one document type
514 * found.
515 *
516 * @param batch - batch to check document types
517 * @return true if there is only one document type, false if multiple document types were found.
518 */
519 protected boolean checkForMixedDocumentTypes(CollectorBatch batch, MessageMap MessageMap) {
520 boolean docTypesNotMixed = true;
521
522 Set batchDocumentTypes = new HashSet();
523 for (OriginEntryFull entry : batch.getOriginEntries()) {
524 batchDocumentTypes.add(entry.getFinancialDocumentTypeCode());
525 }
526
527 if (batchDocumentTypes.size() > 1) {
528 LOG.error("mixed document types found in batch");
529 MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.MIXED_DOCUMENT_TYPES);
530
531 docTypesNotMixed = false;
532 }
533
534 return docTypesNotMixed;
535 }
536
537 /**
538 * Iterates through the origin entries and builds a map on the balance types. Then checks there was only one balance type found.
539 *
540 * @param batch - batch to check balance types
541 * @return true if there is only one balance type, false if multiple balance types were found
542 */
543 protected boolean checkForMixedBalanceTypes(CollectorBatch batch, MessageMap MessageMap) {
544 boolean balanceTypesNotMixed = true;
545
546 Set balanceTypes = new HashSet();
547 for (OriginEntryFull entry : batch.getOriginEntries()) {
548 balanceTypes.add(entry.getFinancialBalanceTypeCode());
549 }
550
551 if (balanceTypes.size() > 1) {
552 LOG.error("mixed balance types found in batch");
553 MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.MIXED_BALANCE_TYPES);
554
555 balanceTypesNotMixed = false;
556 }
557
558 return balanceTypesNotMixed;
559 }
560
561 /**
562 * Verifies each detail (id billing) record key has an corresponding gl entry in the same batch. The key is built by joining the
563 * values of chart of accounts code, account number, sub account number, object code, and sub object code.
564 *
565 * @param batch - batch to validate
566 * @return true if all detail records had matching keys, false otherwise
567 */
568 protected boolean checkDetailKeys(CollectorBatch batch, MessageMap MessageMap) {
569 boolean detailKeysFound = true;
570
571 // build a Set of keys from the gl entries to compare with
572 Set<String> glEntryKeys = new HashSet<String>();
573 for (OriginEntryFull entry : batch.getOriginEntries()) {
574 glEntryKeys.add(generateOriginEntryMatchingKey(entry, ", "));
575 }
576
577 for (CollectorDetail collectorDetail : batch.getCollectorDetails()) {
578 String collectorDetailKey = generateCollectorDetailMatchingKey(collectorDetail, ", ");
579 if (!glEntryKeys.contains(collectorDetailKey)) {
580 LOG.error("found detail key without a matching gl entry key " + collectorDetailKey);
581 MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.NONMATCHING_DETAIL_KEY, collectorDetailKey);
582
583 detailKeysFound = false;
584 }
585 }
586
587 return detailKeysFound;
588 }
589
590 /**
591 * Generates a String representation of the OriginEntryFull's primary key
592 *
593 * @param entry origin entry to get key from
594 * @param delimiter the String delimiter to separate parts of the key
595 * @return the key as a String
596 */
597 protected String generateOriginEntryMatchingKey(OriginEntryFull entry, String delimiter) {
598 return StringUtils.join(new String[] { ObjectUtils.isNull(entry.getUniversityFiscalYear()) ? "" : entry.getUniversityFiscalYear().toString(), entry.getUniversityFiscalPeriodCode(), entry.getChartOfAccountsCode(), entry.getAccountNumber(), entry.getSubAccountNumber(), entry.getFinancialObjectCode(), entry.getFinancialSubObjectCode(), entry.getFinancialObjectTypeCode(), entry.getDocumentNumber(), entry.getFinancialDocumentTypeCode(), entry.getFinancialSystemOriginationCode() }, delimiter);
599 }
600
601 /**
602 * Generates a String representation of the CollectorDetail's primary key
603 *
604 * @param collectorDetail collector detail to get key from
605 * @param delimiter the String delimiter to separate parts of the key
606 * @return the key as a String
607 */
608 protected String generateCollectorDetailMatchingKey(CollectorDetail collectorDetail, String delimiter) {
609 return StringUtils.join(new String[] { ObjectUtils.isNull(collectorDetail.getUniversityFiscalYear()) ? "" : collectorDetail.getUniversityFiscalYear().toString(), collectorDetail.getUniversityFiscalPeriodCode(), collectorDetail.getChartOfAccountsCode(), collectorDetail.getAccountNumber(), collectorDetail.getSubAccountNumber(), collectorDetail.getFinancialObjectCode(), collectorDetail.getFinancialSubObjectCode(), collectorDetail.getFinancialObjectTypeCode(), collectorDetail.getDocumentNumber(), collectorDetail.getFinancialDocumentTypeCode(), collectorDetail.getFinancialSystemOriginationCode() }, delimiter);
610 }
611
612 /**
613 * Checks the batch total line count and amounts against the trailer. Any errors will be contained in GlobalVariables.MessageMap
614 *
615 * @param batch batch to check totals for
616 * @param collectorReportData collector report data (optional)
617 * @see org.kuali.kfs.gl.batch.service.CollectorHelperService#checkTrailerTotals(org.kuali.kfs.gl.batch.CollectorBatch,
618 * org.kuali.kfs.gl.report.CollectorReportData)
619 */
620 public boolean checkTrailerTotals(CollectorBatch batch, CollectorReportData collectorReportData) {
621 return checkTrailerTotals(batch, collectorReportData, GlobalVariables.getMessageMap());
622 }
623
624 /**
625 * Checks the batch total line count and amounts against the trailer. Any errors will be contained in GlobalVariables.MessageMap
626 *
627 * @param batch - batch to check totals for
628 * @return boolean - true if validation was successful, false it not
629 */
630 protected boolean checkTrailerTotals(CollectorBatch batch, CollectorReportData collectorReportData, MessageMap MessageMap) {
631 boolean trailerTotalsMatch = true;
632
633 int actualRecordCount = batch.getOriginEntries().size() + batch.getCollectorDetails().size();
634 if (actualRecordCount != batch.getTotalRecords()) {
635 LOG.error("trailer check on total count did not pass, expected count: " + String.valueOf(batch.getTotalRecords()) + ", actual count: " + String.valueOf(actualRecordCount));
636 MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.TRAILER_ERROR_COUNTNOMATCH, String.valueOf(batch.getTotalRecords()), String.valueOf(actualRecordCount));
637 trailerTotalsMatch = false;
638 }
639
640 OriginEntryTotals totals = batch.getOriginEntryTotals();
641
642 if (batch.getOriginEntries().size() == 0) {
643 if (!KualiDecimal.ZERO.equals(batch.getTotalAmount())) {
644 LOG.error("trailer total should be zero when there are no origin entries");
645 MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.TRAILER_ERROR_AMOUNT_SHOULD_BE_ZERO);
646 }
647 return false;
648 }
649
650 // retrieve document types that balance by equal debits and credits
651 String[] documentTypes = parameterService.getParameterValues(CollectorStep.class, KFSConstants.SystemGroupParameterNames.COLLECTOR_EQUAL_DC_TOTAL_DOCUMENT_TYPES).toArray(new String[] {});
652
653 boolean equalDebitCreditTotal = false;
654 for (int i = 0; i < documentTypes.length; i++) {
655 String documentType = StringUtils.remove(documentTypes[i], "*");
656 if (batch.getOriginEntries().get(0).getFinancialDocumentTypeCode().startsWith(documentType.toUpperCase()) && KFSConstants.BALANCE_TYPE_ACTUAL.equals(batch.getOriginEntries().get(0).getFinancialBalanceTypeCode())) {
657 equalDebitCreditTotal = true;
658 }
659 }
660
661 if (equalDebitCreditTotal) {
662 // credits must equal debits must equal total trailer amount
663 if (!totals.getCreditAmount().equals(totals.getDebitAmount()) || !totals.getCreditAmount().equals(batch.getTotalAmount())) {
664 LOG.error("trailer check on total amount did not pass, debit should equal credit, should equal trailer total");
665 MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.TRAILER_ERROR_AMOUNTNOMATCH1, totals.getCreditAmount().toString(), totals.getDebitAmount().toString(), batch.getTotalAmount().toString());
666 trailerTotalsMatch = false;
667 }
668 }
669 else {
670 // credits plus debits plus other amount must equal trailer
671 KualiDecimal totalGlEntries = totals.getCreditAmount().add(totals.getDebitAmount()).add(totals.getOtherAmount());
672 if (!totalGlEntries.equals(batch.getTotalAmount())) {
673 LOG.error("trailer check on total amount did not pass, sum of gl entry amounts should equal trailer total");
674 MessageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.TRAILER_ERROR_AMOUNTNOMATCH2, totalGlEntries.toString(), batch.getTotalAmount().toString());
675 trailerTotalsMatch = false;
676 }
677 }
678
679 return trailerTotalsMatch;
680 }
681
682 public void setCollectorDetailService(CollectorDetailService collectorDetailService) {
683 this.collectorDetailService = collectorDetailService;
684 }
685
686 public void setOriginEntryGroupService(OriginEntryGroupService originEntryGroupService) {
687 this.originEntryGroupService = originEntryGroupService;
688 }
689
690 public void setOriginEntryService(OriginEntryService originEntryService) {
691 this.originEntryService = originEntryService;
692 }
693
694 /**
695 * Returns the name of the directory where Collector files are saved
696 *
697 * @return the name of the staging directory
698 */
699 public String getStagingDirectory() {
700 return configurationService.getPropertyString(KFSConstants.GL_COLLECTOR_STAGING_DIRECTORY);
701 }
702
703 public void setDateTimeService(DateTimeService dateTimeService) {
704 this.dateTimeService = dateTimeService;
705 }
706
707 public void setBatchInputFileService(BatchInputFileService batchInputFileService) {
708 this.batchInputFileService = batchInputFileService;
709 }
710
711 /**
712 * Sets the collectorScrubberService attribute value.
713 *
714 * @param collectorScrubberService The collectorScrubberService to set.
715 */
716 public void setCollectorScrubberService(CollectorScrubberService collectorScrubberService) {
717 this.collectorScrubberService = collectorScrubberService;
718 }
719
720 public void setConfigurationService(KualiConfigurationService configurationService) {
721 this.configurationService = configurationService;
722 }
723
724 public void setParameterService(ParameterService parameterService) {
725 this.parameterService = parameterService;
726 }
727
728 /**
729 * Sets the batchFileDirectoryName attribute value.
730 * @param batchFileDirectoryName The batchFileDirectoryName to set.
731 */
732 public void setBatchFileDirectoryName(String batchFileDirectoryName) {
733 this.batchFileDirectoryName = batchFileDirectoryName;
734 }
735
736 /**
737 * Sets the accountService attribute value.
738 * @param accountService The accountService to set.
739 */
740 public void setAccountService(AccountService accountService) {
741 this.accountService = accountService;
742 }
743
744 /**
745 * Sets the preScrubberService attribute value.
746 * @param preScrubberService The preScrubberService to set.
747 */
748 public void setPreScrubberService(PreScrubberService preScrubberService) {
749 this.preScrubberService = preScrubberService;
750 }
751 }