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 }