0001: /*
0002: * Copyright 2007 The Kuali Foundation.
0003: *
0004: * Licensed under the Educational Community License, Version 1.0 (the "License");
0005: * you may not use this file except in compliance with the License.
0006: * You may obtain a copy of the License at
0007: *
0008: * http://www.opensource.org/licenses/ecl1.php
0009: *
0010: * Unless required by applicable law or agreed to in writing, software
0011: * distributed under the License is distributed on an "AS IS" BASIS,
0012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0013: * See the License for the specific language governing permissions and
0014: * limitations under the License.
0015: */
0016: package org.kuali.module.labor.service.impl;
0017:
0018: import java.sql.Date;
0019: import java.text.NumberFormat;
0020: import java.util.ArrayList;
0021: import java.util.Calendar;
0022: import java.util.Collection;
0023: import java.util.HashMap;
0024: import java.util.Iterator;
0025: import java.util.List;
0026: import java.util.Map;
0027: import java.util.StringTokenizer;
0028:
0029: import org.apache.commons.lang.ArrayUtils;
0030: import org.apache.commons.lang.StringUtils;
0031: import org.kuali.core.service.DateTimeService;
0032: import org.kuali.core.service.DocumentTypeService;
0033: import org.kuali.core.service.KualiConfigurationService;
0034: import org.kuali.core.service.PersistenceService;
0035: import org.kuali.core.util.KualiDecimal;
0036: import org.kuali.kfs.KFSConstants;
0037: import org.kuali.kfs.KFSKeyConstants;
0038: import org.kuali.kfs.context.SpringContext;
0039: import org.kuali.kfs.service.ParameterEvaluator;
0040: import org.kuali.kfs.service.ParameterService;
0041: import org.kuali.module.chart.service.ObjectCodeService;
0042: import org.kuali.module.chart.service.OffsetDefinitionService;
0043: import org.kuali.module.financial.service.FlexibleOffsetAccountService;
0044: import org.kuali.module.gl.GLConstants;
0045: import org.kuali.module.gl.batch.ScrubberStep;
0046: import org.kuali.module.gl.bo.OriginEntryGroup;
0047: import org.kuali.module.gl.bo.OriginEntrySource;
0048: import org.kuali.module.gl.bo.Transaction;
0049: import org.kuali.module.gl.bo.UniversityDate;
0050: import org.kuali.module.gl.dao.UniversityDateDao;
0051: import org.kuali.module.gl.service.OriginEntryGroupService;
0052: import org.kuali.module.gl.service.OriginEntryLookupService;
0053: import org.kuali.module.gl.service.ScrubberValidator;
0054: import org.kuali.module.gl.service.impl.scrubber.DemergerReportData;
0055: import org.kuali.module.gl.service.impl.scrubber.ScrubberReportData;
0056: import org.kuali.module.gl.util.CachingLookup;
0057: import org.kuali.module.gl.util.Message;
0058: import org.kuali.module.gl.util.ObjectHelper;
0059: import org.kuali.module.gl.util.OriginEntryStatistics;
0060: import org.kuali.module.labor.bo.LaborOriginEntry;
0061: import org.kuali.module.labor.service.LaborOriginEntryService;
0062: import org.kuali.module.labor.service.LaborReportService;
0063: import org.kuali.module.labor.util.ReportRegistry;
0064:
0065: /**
0066: * This class has the logic for the scrubber. It is required because the scrubber process needs instance variables. Instance
0067: * variables in a spring service are shared between all code calling the service. This will make sure each run of the scrubber has
0068: * it's own instance variables instead of being shared.
0069: */
0070: public class LaborScrubberProcess {
0071: private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger
0072: .getLogger(LaborScrubberProcess.class);
0073:
0074: // 40 spaces - used for filling in descriptions with spaces
0075: private static String SPACES = " ";
0076:
0077: /* Services required */
0078: private FlexibleOffsetAccountService flexibleOffsetAccountService;
0079: private DocumentTypeService documentTypeService;
0080: private LaborOriginEntryService laborOriginEntryService;
0081: private OriginEntryGroupService originEntryGroupService;
0082: private DateTimeService dateTimeService;
0083: private OffsetDefinitionService offsetDefinitionService;
0084: private ObjectCodeService objectCodeService;
0085: private KualiConfigurationService kualiConfigurationService;
0086: private UniversityDateDao universityDateDao;
0087: private PersistenceService persistenceService;
0088: private LaborReportService laborReportService;
0089: private ScrubberValidator scrubberValidator;
0090:
0091: /* These are all different forms of the run date for this job */
0092: private Date runDate;
0093: private Calendar runCal;
0094: private UniversityDate universityRunDate;
0095: private String offsetString;
0096:
0097: /*
0098: * These fields are used to control whether the job was run before some set time, if so, the rundate of the job will be set to
0099: * 11:59 PM of the previous day
0100: */
0101: private Integer cutoffHour;
0102: private Integer cutoffMinute;
0103: private Integer cutoffSecond;
0104:
0105: /* These are the output groups */
0106: private OriginEntryGroup validGroup;
0107: private OriginEntryGroup errorGroup;
0108: private OriginEntryGroup expiredGroup;
0109:
0110: /* Unit Of Work info */
0111: private UnitOfWorkInfo unitOfWork;
0112: private KualiDecimal scrubCostShareAmount;
0113:
0114: /* Statistics for the reports */
0115: private ScrubberReportData scrubberReport;
0116: private Map<Transaction, List<Message>> scrubberReportErrors;
0117: private List<Message> transactionErrors;
0118:
0119: /* Description names */
0120: private String offsetDescription;
0121: private String capitalizationDescription;
0122: private String liabilityDescription;
0123: private String transferDescription;
0124: private String costShareDescription;
0125:
0126: /* Misc stuff */
0127: private boolean reportOnlyMode;
0128:
0129: /**
0130: * These parameters are all the dependencies.
0131: */
0132: public LaborScrubberProcess(
0133: FlexibleOffsetAccountService flexibleOffsetAccountService,
0134: DocumentTypeService documentTypeService,
0135: LaborOriginEntryService laborOriginEntryService,
0136: OriginEntryGroupService originEntryGroupService,
0137: DateTimeService dateTimeService,
0138: OffsetDefinitionService offsetDefinitionService,
0139: ObjectCodeService objectCodeService,
0140: KualiConfigurationService kualiConfigurationService,
0141: UniversityDateDao universityDateDao,
0142: PersistenceService persistenceService,
0143: LaborReportService laborReportService,
0144: ScrubberValidator scrubberValidator) {
0145: super ();
0146: this .flexibleOffsetAccountService = flexibleOffsetAccountService;
0147: this .documentTypeService = documentTypeService;
0148: this .laborOriginEntryService = laborOriginEntryService;
0149: this .originEntryGroupService = originEntryGroupService;
0150: this .dateTimeService = dateTimeService;
0151: this .offsetDefinitionService = offsetDefinitionService;
0152: this .objectCodeService = objectCodeService;
0153: this .kualiConfigurationService = kualiConfigurationService;
0154: this .universityDateDao = universityDateDao;
0155: this .persistenceService = persistenceService;
0156: this .laborReportService = laborReportService;
0157: this .scrubberValidator = scrubberValidator;
0158:
0159: cutoffHour = null;
0160: cutoffMinute = null;
0161: cutoffSecond = null;
0162:
0163: initCutoffTime();
0164: }
0165:
0166: /**
0167: * Scrub this single group read only. This will only output the scrubber report. It won't output any other groups.
0168: *
0169: * @param group
0170: */
0171: public void scrubGroupReportOnly(OriginEntryGroup group,
0172: String documentNumber) {
0173: LOG.debug("scrubGroupReportOnly() started");
0174:
0175: scrubEntries(group, documentNumber);
0176: }
0177:
0178: public void scrubEntries() {
0179: scrubEntries(null, null);
0180: }
0181:
0182: /**
0183: * Scrub all entries that need it in origin entry. Put valid scrubbed entries in a scrubber valid group, put errors in a
0184: * scrubber error group, and transactions with an expired account in the scrubber expired account group.
0185: */
0186: public void scrubEntries(OriginEntryGroup group,
0187: String documentNumber) {
0188: LOG.debug("scrubEntries() started");
0189:
0190: // We are in report only mode if we pass a group to this method.
0191: // if not, we are in batch mode and we scrub the backup group
0192: reportOnlyMode = (group != null);
0193:
0194: // get reportDirectory
0195: String reportsDirectory = ReportRegistry.getReportsDirectory();
0196:
0197: scrubberReportErrors = new HashMap<Transaction, List<Message>>();
0198:
0199: // setup an object to hold the "default" date information
0200: runDate = calculateRunDate(dateTimeService.getCurrentDate());
0201: runCal = Calendar.getInstance();
0202: runCal.setTime(runDate);
0203:
0204: universityRunDate = universityDateDao.getByPrimaryKey(runDate);
0205: if (universityRunDate == null) {
0206: throw new IllegalStateException(
0207: kualiConfigurationService
0208: .getPropertyString(KFSKeyConstants.ERROR_UNIV_DATE_NOT_FOUND));
0209: }
0210:
0211: setOffsetString();
0212: setDescriptions();
0213:
0214: // Create the groups that will store the valid and error entries that come out of the scrubber
0215: // We don't need groups for the reportOnlyMode
0216: if (!reportOnlyMode) {
0217: validGroup = originEntryGroupService.createGroup(runDate,
0218: OriginEntrySource.LABOR_SCRUBBER_VALID, true, true,
0219: false);
0220: errorGroup = originEntryGroupService.createGroup(runDate,
0221: OriginEntrySource.LABOR_SCRUBBER_ERROR, false,
0222: true, false);
0223: expiredGroup = originEntryGroupService.createGroup(runDate,
0224: OriginEntrySource.LABOR_SCRUBBER_EXPIRED, false,
0225: true, false);
0226: }
0227:
0228: // get the origin entry groups to be processed by Scrubber
0229: Collection groupsToScrub = null;
0230: if (reportOnlyMode) {
0231: groupsToScrub = new ArrayList();
0232: groupsToScrub.add(group);
0233: } else {
0234: groupsToScrub = originEntryGroupService
0235: .getLaborBackupGroups(runDate);
0236: }
0237: LOG.debug("scrubEntries() number of groups to scrub: "
0238: + groupsToScrub.size());
0239:
0240: // generate the reports based on the origin entries to be processed by scrubber
0241: if (reportOnlyMode) {
0242: laborReportService
0243: .generateScrubberLedgerSummaryReportOnline(group,
0244: documentNumber, reportsDirectory, runDate);
0245: } else {
0246: laborReportService
0247: .generateScrubberLedgerSummaryReportBatch(
0248: groupsToScrub, reportsDirectory, runDate);
0249: }
0250:
0251: // Scrub all of the OriginEntryGroups waiting to be scrubbed as of runDate.
0252: scrubberReport = new ScrubberReportData();
0253: for (Iterator iteratorOverGroups = groupsToScrub.iterator(); iteratorOverGroups
0254: .hasNext();) {
0255: OriginEntryGroup originEntryGroup = (OriginEntryGroup) iteratorOverGroups
0256: .next();
0257: LOG.debug("scrubEntries() Scrubbing group "
0258: + originEntryGroup.getId());
0259:
0260: processGroup(originEntryGroup);
0261:
0262: if (!reportOnlyMode) {
0263: // Mark the origin entry group as being processed ...
0264: originEntryGroup.setProcess(Boolean.FALSE);
0265:
0266: // ... and save the origin entry group with the new process flag.
0267: originEntryGroupService.save(originEntryGroup);
0268: }
0269: }
0270:
0271: // generate the scrubber status summary report
0272: if (reportOnlyMode) {
0273: laborReportService.generateOnlineScrubberStatisticsReport(
0274: group.getId(), scrubberReport,
0275: scrubberReportErrors, documentNumber,
0276: reportsDirectory, runDate);
0277: } else {
0278: laborReportService.generateBatchScrubberStatisticsReport(
0279: scrubberReport, scrubberReportErrors,
0280: reportsDirectory, runDate);
0281: }
0282:
0283: // run the demerger
0284: if (!reportOnlyMode) {
0285: performDemerger(errorGroup, validGroup);
0286: }
0287:
0288: // Run the reports
0289: if (reportOnlyMode) {
0290: // Run transaction list
0291: laborReportService.generateScrubberTransactionsOnline(
0292: group, documentNumber, reportsDirectory, runDate);
0293: } else {
0294: // Run bad balance type report and removed transaction report
0295: laborReportService
0296: .generateScrubberBadBalanceTypeListingReport(
0297: groupsToScrub, reportsDirectory, runDate);
0298:
0299: laborReportService.generateScrubberRemovedTransactions(
0300: errorGroup, reportsDirectory, runDate);
0301: }
0302: }
0303:
0304: /**
0305: * Determine the type of the transaction by looking at attributes
0306: *
0307: * @param transaction Transaction to identify
0308: * @return CE (Cost share encumbrance, O (Offset), C (apitalization), L (Liability), T (Transfer), CS (Cost Share), X (Other)
0309: */
0310: private String getTransactionType(LaborOriginEntry transaction) {
0311: if ("CE".equals(transaction.getFinancialBalanceTypeCode())) {
0312: return "CE";
0313: }
0314: String desc = transaction
0315: .getTransactionLedgerEntryDescription();
0316:
0317: if (desc == null) {
0318: return "X";
0319: }
0320:
0321: if (desc.startsWith(offsetDescription)
0322: && desc.indexOf("***") > -1) {
0323: return "CS";
0324: }
0325: if (desc.startsWith(costShareDescription)
0326: && desc.indexOf("***") > -1) {
0327: return "CS";
0328: }
0329: if (desc.startsWith(offsetDescription)) {
0330: return "O";
0331: }
0332: if (desc.startsWith(capitalizationDescription)) {
0333: return "C";
0334: }
0335: if (desc.startsWith(liabilityDescription)) {
0336: return "L";
0337: }
0338: if (desc.startsWith(transferDescription)) {
0339: return "T";
0340: }
0341: return "X";
0342: }
0343:
0344: /**
0345: * This will process a group of origin entries. The COBOL code was refactored a lot to get this so there isn't a 1 to 1 section
0346: * of Cobol relating to this.
0347: *
0348: * @param originEntryGroup Group to process
0349: */
0350: private void processGroup(OriginEntryGroup originEntryGroup) {
0351:
0352: LaborOriginEntry lastEntry = null;
0353: scrubCostShareAmount = KualiDecimal.ZERO;
0354: unitOfWork = new UnitOfWorkInfo();
0355: OriginEntryLookupService refLookup = SpringContext
0356: .getBean(OriginEntryLookupService.class);
0357: refLookup.setLookupService(SpringContext
0358: .getBean(CachingLookup.class));
0359: scrubberValidator.setReferenceLookup(refLookup);
0360:
0361: Iterator entries = laborOriginEntryService
0362: .getEntriesByGroup(originEntryGroup);
0363: while (entries.hasNext()) {
0364: LaborOriginEntry unscrubbedEntry = (LaborOriginEntry) entries
0365: .next();
0366: unscrubbedEntry.refresh();
0367: scrubberReport.incrementUnscrubbedRecordsRead();
0368:
0369: transactionErrors = new ArrayList<Message>();
0370:
0371: // This is done so if the code modifies this row, then saves it, it will be an insert,
0372: // and it won't touch the original. The Scrubber never modifies input rows/groups.
0373: unscrubbedEntry.setGroup(null);
0374: unscrubbedEntry.setVersionNumber(null);
0375: unscrubbedEntry.setEntryId(null);
0376:
0377: boolean saveErrorTransaction = false;
0378: boolean saveValidTransaction = false;
0379:
0380: // Build a scrubbed entry
0381: // Labor has more fields
0382: LaborOriginEntry scrubbedEntry = new LaborOriginEntry();
0383: buildScrubbedEntry(unscrubbedEntry, scrubbedEntry);
0384:
0385: // For Labor Scrubber
0386: boolean laborIndicator = true;
0387:
0388: List<Message> tmperrors = scrubberValidator
0389: .validateTransaction(unscrubbedEntry,
0390: scrubbedEntry, universityRunDate,
0391: laborIndicator);
0392: transactionErrors.addAll(tmperrors);
0393:
0394: // Expired account?
0395: if ((unscrubbedEntry.getAccount() != null)
0396: && (unscrubbedEntry.getAccount()
0397: .isAccountClosedIndicator())) {
0398: // Make a copy of it so OJB doesn't just update the row in the original
0399: // group. It needs to make a new one in the expired group
0400: LaborOriginEntry expiredEntry = new LaborOriginEntry(
0401: scrubbedEntry);
0402:
0403: createOutputEntry(expiredEntry, expiredGroup);
0404: scrubberReport.incrementExpiredAccountFound();
0405: }
0406:
0407: if (!isFatal(transactionErrors)) {
0408: saveValidTransaction = true;
0409:
0410: // See if unit of work has changed
0411: if (!unitOfWork.isSameUnitOfWork(scrubbedEntry)) {
0412: // Generate offset for last unit of work
0413:
0414: // generateOffset(lastEntry);
0415:
0416: unitOfWork = new UnitOfWorkInfo(scrubbedEntry);
0417: }
0418:
0419: KualiDecimal transactionAmount = scrubbedEntry
0420: .getTransactionLedgerEntryAmount();
0421:
0422: ParameterEvaluator offsetFiscalPeriods = SpringContext
0423: .getBean(ParameterService.class)
0424: .getParameterEvaluator(
0425: ScrubberStep.class,
0426: GLConstants.GlScrubberGroupRules.OFFSET_FISCAL_PERIOD_CODES,
0427: scrubbedEntry
0428: .getUniversityFiscalPeriodCode());
0429:
0430: if (scrubbedEntry.getBalanceType()
0431: .isFinancialOffsetGenerationIndicator()
0432: && offsetFiscalPeriods.evaluationSucceeds()) {
0433: if (scrubbedEntry.isDebit()) {
0434: unitOfWork.offsetAmount = unitOfWork.offsetAmount
0435: .add(transactionAmount);
0436: } else {
0437: unitOfWork.offsetAmount = unitOfWork.offsetAmount
0438: .subtract(transactionAmount);
0439: }
0440: }
0441:
0442: // The sub account type code will only exist if there is a valid sub account
0443: String subAccountTypeCode = " ";
0444: if (scrubbedEntry.getA21SubAccount() != null) {
0445: subAccountTypeCode = scrubbedEntry
0446: .getA21SubAccount().getSubAccountTypeCode();
0447: }
0448:
0449: ParameterService parameterService = SpringContext
0450: .getBean(ParameterService.class);
0451: ParameterEvaluator costShareObjectTypeCodes = parameterService
0452: .getParameterEvaluator(
0453: ScrubberStep.class,
0454: GLConstants.GlScrubberGroupRules.COST_SHARE_OBJ_TYPE_CODES,
0455: scrubbedEntry
0456: .getFinancialObjectTypeCode());
0457: ParameterEvaluator costShareEncBalanceTypeCodes = parameterService
0458: .getParameterEvaluator(
0459: ScrubberStep.class,
0460: GLConstants.GlScrubberGroupRules.COST_SHARE_ENC_BAL_TYP_CODES,
0461: scrubbedEntry
0462: .getFinancialBalanceTypeCode());
0463: ParameterEvaluator costShareEncFiscalPeriodCodes = parameterService
0464: .getParameterEvaluator(
0465: ScrubberStep.class,
0466: GLConstants.GlScrubberGroupRules.COST_SHARE_ENC_FISCAL_PERIOD_CODES,
0467: scrubbedEntry
0468: .getUniversityFiscalPeriodCode());
0469: ParameterEvaluator costShareEncDocTypeCodes = parameterService
0470: .getParameterEvaluator(
0471: ScrubberStep.class,
0472: GLConstants.GlScrubberGroupRules.COST_SHARE_ENC_DOC_TYPE_CODES,
0473: scrubbedEntry
0474: .getFinancialDocumentTypeCode()
0475: .trim());
0476: ParameterEvaluator costShareFiscalPeriodCodes = parameterService
0477: .getParameterEvaluator(
0478: ScrubberStep.class,
0479: GLConstants.GlScrubberGroupRules.COST_SHARE_FISCAL_PERIOD_CODES,
0480: scrubbedEntry
0481: .getUniversityFiscalPeriodCode());
0482:
0483: if (scrubbedEntry.getAccount() != null) {
0484: if (costShareObjectTypeCodes.evaluationSucceeds()
0485: && scrubbedEntry
0486: .getOption()
0487: .getActualFinancialBalanceTypeCd()
0488: .equals(
0489: scrubbedEntry
0490: .getFinancialBalanceTypeCode())
0491: && scrubbedEntry.getAccount()
0492: .isForContractsAndGrants()
0493: && KFSConstants.COST_SHARE
0494: .equals(subAccountTypeCode)
0495: && costShareFiscalPeriodCodes
0496: .evaluationSucceeds()
0497: && costShareEncDocTypeCodes
0498: .evaluationSucceeds()) {
0499: if (scrubbedEntry.isDebit()) {
0500: scrubCostShareAmount = scrubCostShareAmount
0501: .subtract(transactionAmount);
0502: } else {
0503: scrubCostShareAmount = scrubCostShareAmount
0504: .add(transactionAmount);
0505: }
0506: }
0507: }
0508:
0509: if (transactionErrors.size() > 0) {
0510: scrubberReportErrors.put(scrubbedEntry,
0511: transactionErrors);
0512: }
0513:
0514: lastEntry = scrubbedEntry;
0515: } else {
0516: // Error transaction
0517: saveErrorTransaction = true;
0518:
0519: scrubberReportErrors.put(unscrubbedEntry,
0520: transactionErrors);
0521: }
0522:
0523: if (saveValidTransaction) {
0524: scrubbedEntry
0525: .setTransactionScrubberOffsetGenerationIndicator(false);
0526: createOutputEntry(scrubbedEntry, validGroup);
0527: scrubberReport.incrementScrubbedRecordWritten();
0528: }
0529:
0530: if (saveErrorTransaction) {
0531: // Make a copy of it so OJB doesn't just update the row in the original
0532: // group. It needs to make a new one in the error group
0533: LaborOriginEntry errorEntry = new LaborOriginEntry(
0534: unscrubbedEntry);
0535: errorEntry
0536: .setTransactionScrubberOffsetGenerationIndicator(false);
0537: createOutputEntry(errorEntry, errorGroup);
0538: scrubberReport.incrementErrorRecordWritten();
0539: }
0540: }
0541:
0542: // Generate last offset (if necessary)
0543: // generateOffset(lastEntry);
0544: }
0545:
0546: private boolean isFatal(List<Message> errors) {
0547: for (Iterator<Message> iter = errors.iterator(); iter.hasNext();) {
0548: Message element = iter.next();
0549: if (element.getType() == Message.TYPE_FATAL) {
0550: return true;
0551: }
0552: }
0553: return false;
0554: }
0555:
0556: /**
0557: * Get all the transaction descriptions from the param table
0558: */
0559: private void setDescriptions() {
0560: offsetDescription = kualiConfigurationService
0561: .getPropertyString(KFSKeyConstants.MSG_GENERATED_OFFSET);
0562: capitalizationDescription = kualiConfigurationService
0563: .getPropertyString(KFSKeyConstants.MSG_GENERATED_CAPITALIZATION);
0564: liabilityDescription = kualiConfigurationService
0565: .getPropertyString(KFSKeyConstants.MSG_GENERATED_LIABILITY);
0566: costShareDescription = kualiConfigurationService
0567: .getPropertyString(KFSKeyConstants.MSG_GENERATED_COST_SHARE);
0568: transferDescription = kualiConfigurationService
0569: .getPropertyString(KFSKeyConstants.MSG_GENERATED_TRANSFER);
0570: }
0571:
0572: /**
0573: * Generate the flag for the end of specific descriptions. This will be used in the demerger step
0574: */
0575: private void setOffsetString() {
0576:
0577: NumberFormat nf = NumberFormat.getInstance();
0578: nf.setMaximumFractionDigits(0);
0579: nf.setMaximumIntegerDigits(2);
0580: nf.setMinimumFractionDigits(0);
0581: nf.setMinimumIntegerDigits(2);
0582:
0583: offsetString = "***"
0584: + nf.format(runCal.get(Calendar.MONTH) + 1)
0585: + nf.format(runCal.get(Calendar.DAY_OF_MONTH));
0586: }
0587:
0588: /**
0589: * Generate the offset message with the flag at the end
0590: *
0591: * @return Offset message
0592: */
0593: private String getOffsetMessage() {
0594: String msg = offsetDescription + SPACES;
0595:
0596: return msg.substring(0, 33) + offsetString;
0597: }
0598:
0599: /**
0600: * Save an entry in origin entry
0601: *
0602: * @param entry Entry to save
0603: * @param group Group to save it in
0604: */
0605: private void createOutputEntry(LaborOriginEntry entry,
0606: OriginEntryGroup group) {
0607: // Write the entry if we aren't running in report only mode.
0608: if (reportOnlyMode) {
0609: // If the group is null don't write it because the error and expired groups aren't created in reportOnlyMode
0610: if (group != null) {
0611: entry.setGroup(group);
0612: laborOriginEntryService.save(entry);
0613: }
0614: } else {
0615: entry.setGroup(group);
0616: laborOriginEntryService.save(entry);
0617: }
0618: }
0619:
0620: /**
0621: * If object is null, generate an error
0622: *
0623: * @param glObject object to test
0624: * @param errorMessage error message if glObject is null
0625: * @param errorValue value of glObject to print in the error message
0626: * @param type Type of message (fatal or warning)
0627: * @return true of glObject is null
0628: */
0629: private boolean ifNullAddTransactionErrorAndReturnFalse(
0630: Object glObject, String errorMessage, String errorValue,
0631: int type) {
0632: if (glObject == null) {
0633: if (StringUtils.isNotBlank(errorMessage)) {
0634: addTransactionError(errorMessage, errorValue, type);
0635: } else {
0636: addTransactionError("Unexpected null object", glObject
0637: .getClass().getName(), type);
0638: }
0639: return false;
0640: }
0641: return true;
0642: }
0643:
0644: /**
0645: * Add an error message to the list of messages for this transaction
0646: *
0647: * @param errorMessage Error message
0648: * @param errorValue Value that is in error
0649: * @param type Type of error (Fatal or Warning)
0650: */
0651: private void addTransactionError(String errorMessage,
0652: String errorValue, int type) {
0653: transactionErrors.add(new Message(errorMessage + " ("
0654: + errorValue + ")", type));
0655: }
0656:
0657: private void putTransactionError(Transaction s,
0658: String errorMessage, String errorValue, int type) {
0659: List te = new ArrayList();
0660: te
0661: .add(new Message(errorMessage + "(" + errorValue + ")",
0662: type));
0663: scrubberReportErrors.put(s, te);
0664: }
0665:
0666: class UnitOfWorkInfo {
0667: // Unit of work key
0668: public Integer univFiscalYr = 0;
0669: public String finCoaCd = "";
0670: public String accountNbr = "";
0671: public String subAcctNbr = "";
0672: public String finBalanceTypCd = "";
0673: public String fdocTypCd = "";
0674: public String fsOriginCd = "";
0675: public String fdocNbr = "";
0676: public Date fdocReversalDt = new Date(dateTimeService
0677: .getCurrentDate().getTime());
0678: public String univFiscalPrdCd = "";
0679:
0680: // Data about unit of work
0681: public boolean entryMode = true;
0682: public KualiDecimal offsetAmount = KualiDecimal.ZERO;
0683: public String scrbFinCoaCd;
0684: public String scrbAccountNbr;
0685:
0686: public UnitOfWorkInfo() {
0687: }
0688:
0689: public UnitOfWorkInfo(LaborOriginEntry e) {
0690: univFiscalYr = e.getUniversityFiscalYear();
0691: finCoaCd = e.getChartOfAccountsCode();
0692: accountNbr = e.getAccountNumber();
0693: subAcctNbr = e.getSubAccountNumber();
0694: finBalanceTypCd = e.getFinancialBalanceTypeCode();
0695: fdocTypCd = e.getFinancialDocumentTypeCode();
0696: fsOriginCd = e.getFinancialSystemOriginationCode();
0697: fdocNbr = e.getDocumentNumber();
0698: fdocReversalDt = e.getFinancialDocumentReversalDate();
0699: univFiscalPrdCd = e.getUniversityFiscalPeriodCode();
0700: }
0701:
0702: public boolean isSameUnitOfWork(LaborOriginEntry e) {
0703: // Compare the key fields
0704: return univFiscalYr.equals(e.getUniversityFiscalYear())
0705: && finCoaCd.equals(e.getChartOfAccountsCode())
0706: && accountNbr.equals(e.getAccountNumber())
0707: && subAcctNbr.equals(e.getSubAccountNumber())
0708: && finBalanceTypCd.equals(e
0709: .getFinancialBalanceTypeCode())
0710: && fdocTypCd.equals(e
0711: .getFinancialDocumentTypeCode())
0712: && fsOriginCd.equals(e
0713: .getFinancialSystemOriginationCode())
0714: && fdocNbr.equals(e.getDocumentNumber())
0715: && ObjectHelper.isEqual(fdocReversalDt, e
0716: .getFinancialDocumentReversalDate())
0717: && univFiscalPrdCd.equals(e
0718: .getUniversityFiscalPeriodCode());
0719: }
0720:
0721: public String toString() {
0722: return univFiscalYr + finCoaCd + accountNbr + subAcctNbr
0723: + finBalanceTypCd + fdocTypCd + fsOriginCd
0724: + fdocNbr + fdocReversalDt + univFiscalPrdCd;
0725: }
0726:
0727: public LaborOriginEntry getOffsetTemplate() {
0728: LaborOriginEntry e = new LaborOriginEntry();
0729: e.setUniversityFiscalYear(univFiscalYr);
0730: e.setChartOfAccountsCode(finCoaCd);
0731: e.setAccountNumber(accountNbr);
0732: e.setSubAccountNumber(subAcctNbr);
0733: e.setFinancialBalanceTypeCode(finBalanceTypCd);
0734: e.setFinancialDocumentTypeCode(fdocTypCd);
0735: e.setFinancialSystemOriginationCode(fsOriginCd);
0736: e.setDocumentNumber(fdocNbr);
0737: e.setFinancialDocumentReversalDate(fdocReversalDt);
0738: e.setUniversityFiscalPeriodCode(univFiscalPrdCd);
0739: return e;
0740: }
0741: }
0742:
0743: class TransactionError {
0744: public Transaction transaction;
0745: public Message message;
0746:
0747: public TransactionError(Transaction t, Message m) {
0748: transaction = t;
0749: message = m;
0750: }
0751: }
0752:
0753: protected void setCutoffTimeForPreviousDay(int hourOfDay,
0754: int minuteOfDay, int secondOfDay) {
0755: this .cutoffHour = hourOfDay;
0756: this .cutoffMinute = minuteOfDay;
0757: this .cutoffSecond = secondOfDay;
0758:
0759: LOG.info("Setting cutoff time to hour: " + hourOfDay
0760: + ", minute: " + minuteOfDay + ", second: "
0761: + secondOfDay);
0762: }
0763:
0764: protected void setCutoffTime(String cutoffTime) {
0765: if (StringUtils.isBlank(cutoffTime)) {
0766: LOG.debug("Cutoff time is blank");
0767: unsetCutoffTimeForPreviousDay();
0768: } else {
0769: cutoffTime = cutoffTime.trim();
0770: LOG.debug("Cutoff time value found: " + cutoffTime);
0771: StringTokenizer st = new StringTokenizer(cutoffTime, ":",
0772: false);
0773:
0774: try {
0775: String hourStr = st.nextToken();
0776: String minuteStr = st.nextToken();
0777: String secondStr = st.nextToken();
0778:
0779: int hourInt = Integer.parseInt(hourStr, 10);
0780: int minuteInt = Integer.parseInt(minuteStr, 10);
0781: int secondInt = Integer.parseInt(secondStr, 10);
0782:
0783: if (hourInt < 0 || hourInt > 23 || minuteInt < 0
0784: || minuteInt > 59 || secondInt < 0
0785: || secondInt > 59) {
0786: throw new IllegalArgumentException(
0787: "Cutoff time must be in the format \"HH:mm:ss\", where HH, mm, ss are defined in the java.text.SimpleDateFormat class. In particular, 0 <= hour <= 23, 0 <= minute <= 59, and 0 <= second <= 59");
0788: }
0789: setCutoffTimeForPreviousDay(hourInt, minuteInt,
0790: secondInt);
0791: } catch (Exception e) {
0792: throw new IllegalArgumentException(
0793: "Cutoff time should either be null, or in the format \"HH:mm:ss\", where HH, mm, ss are defined in the java.text.SimpleDateFormat class.");
0794: }
0795: }
0796: }
0797:
0798: public void unsetCutoffTimeForPreviousDay() {
0799: this .cutoffHour = null;
0800: this .cutoffMinute = null;
0801: this .cutoffSecond = null;
0802: }
0803:
0804: /**
0805: * This method modifies the run date if it is before the cutoff time specified by calling the setCutoffTimeForPreviousDay
0806: * method. See KULRNE-70 This method is public to facilitate unit testing
0807: *
0808: * @param currentDate
0809: * @return
0810: */
0811: public java.sql.Date calculateRunDate(java.util.Date currentDate) {
0812: Calendar currentCal = Calendar.getInstance();
0813: currentCal.setTime(currentDate);
0814:
0815: if (isCurrentDateBeforeCutoff(currentCal)) {
0816: // time to set the date to the previous day's last minute/second
0817: currentCal.add(Calendar.DAY_OF_MONTH, -1);
0818: // per old COBOL code (see KULRNE-70),
0819: // the time is set to 23:59:59 (assuming 0 ms)
0820: currentCal.set(Calendar.HOUR_OF_DAY, 23);
0821: currentCal.set(Calendar.MINUTE, 59);
0822: currentCal.set(Calendar.SECOND, 59);
0823: currentCal.set(Calendar.MILLISECOND, 0);
0824: return new java.sql.Date(currentCal.getTimeInMillis());
0825: }
0826: return new java.sql.Date(currentDate.getTime());
0827: }
0828:
0829: protected boolean isCurrentDateBeforeCutoff(Calendar currentCal) {
0830: if (cutoffHour != null && cutoffMinute != null
0831: && cutoffSecond != null) {
0832: // if cutoff date is not properly defined
0833: // 24 hour clock (i.e. hour is 0 - 23)
0834:
0835: // clone the calendar so we get the same month, day, year
0836: // then change the hour, minute, second fields
0837: // then see if the cutoff is before or after
0838: Calendar cutoffTime = (Calendar) currentCal.clone();
0839: cutoffTime.setLenient(false);
0840: cutoffTime.set(Calendar.HOUR_OF_DAY, cutoffHour);
0841: cutoffTime.set(Calendar.MINUTE, cutoffMinute);
0842: cutoffTime.set(Calendar.SECOND, cutoffSecond);
0843: cutoffTime.set(Calendar.MILLISECOND, 0);
0844:
0845: return currentCal.before(cutoffTime);
0846: }
0847: // if cutoff date is not properly defined, then it is considered to be after the cutoff
0848: return false;
0849: }
0850:
0851: protected void initCutoffTime() {
0852: String cutoffTime = SpringContext
0853: .getBean(ParameterService.class)
0854: .getParameterValue(
0855: ScrubberStep.class,
0856: GLConstants.GlScrubberGroupParameters.SCRUBBER_CUTOFF_TIME);
0857: if (StringUtils.isBlank(cutoffTime)) {
0858: LOG.debug("Cutoff time system parameter not found");
0859: unsetCutoffTimeForPreviousDay();
0860: return;
0861: }
0862: setCutoffTime(cutoffTime);
0863: }
0864:
0865: protected void buildScrubbedEntry(LaborOriginEntry unscrubbedEntry,
0866: LaborOriginEntry scrubbedEntry) {
0867: scrubbedEntry.setDocumentNumber(unscrubbedEntry
0868: .getDocumentNumber());
0869: scrubbedEntry.setOrganizationDocumentNumber(unscrubbedEntry
0870: .getOrganizationDocumentNumber());
0871: scrubbedEntry.setOrganizationReferenceId(unscrubbedEntry
0872: .getOrganizationReferenceId());
0873: scrubbedEntry
0874: .setReferenceFinancialDocumentNumber(unscrubbedEntry
0875: .getReferenceFinancialDocumentNumber());
0876:
0877: Integer transactionNumber = unscrubbedEntry
0878: .getTransactionLedgerEntrySequenceNumber();
0879: scrubbedEntry
0880: .setTransactionLedgerEntrySequenceNumber(null == transactionNumber ? new Integer(
0881: 0)
0882: : transactionNumber);
0883: scrubbedEntry
0884: .setTransactionLedgerEntryDescription(unscrubbedEntry
0885: .getTransactionLedgerEntryDescription());
0886: scrubbedEntry.setTransactionLedgerEntryAmount(unscrubbedEntry
0887: .getTransactionLedgerEntryAmount());
0888: scrubbedEntry.setTransactionDebitCreditCode(unscrubbedEntry
0889: .getTransactionDebitCreditCode());
0890:
0891: // For Labor's more fields
0892: // It might be changed based on Labor Scrubber's business rule
0893:
0894: scrubbedEntry.setPositionNumber(unscrubbedEntry
0895: .getPositionNumber());
0896: scrubbedEntry.setTransactionPostingDate(unscrubbedEntry
0897: .getTransactionPostingDate());
0898: scrubbedEntry.setPayPeriodEndDate(unscrubbedEntry
0899: .getPayPeriodEndDate());
0900: scrubbedEntry.setTransactionTotalHours(unscrubbedEntry
0901: .getTransactionTotalHours());
0902: scrubbedEntry.setPayrollEndDateFiscalYear(unscrubbedEntry
0903: .getPayrollEndDateFiscalYear());
0904: scrubbedEntry.setPayrollEndDateFiscalPeriodCode(unscrubbedEntry
0905: .getPayrollEndDateFiscalPeriodCode());
0906: scrubbedEntry.setFinancialDocumentApprovedCode(unscrubbedEntry
0907: .getFinancialDocumentApprovedCode());
0908: scrubbedEntry.setTransactionEntryOffsetCode(unscrubbedEntry
0909: .getTransactionEntryOffsetCode());
0910: scrubbedEntry
0911: .setTransactionEntryProcessedTimestamp(unscrubbedEntry
0912: .getTransactionEntryProcessedTimestamp());
0913: scrubbedEntry.setEmplid(unscrubbedEntry.getEmplid());
0914: scrubbedEntry.setEmployeeRecord(unscrubbedEntry
0915: .getEmployeeRecord());
0916: scrubbedEntry.setEarnCode(unscrubbedEntry.getEarnCode());
0917: scrubbedEntry.setPayGroup(unscrubbedEntry.getPayGroup());
0918: scrubbedEntry.setSalaryAdministrationPlan(unscrubbedEntry
0919: .getSalaryAdministrationPlan());
0920: scrubbedEntry.setGrade(unscrubbedEntry.getGrade());
0921: scrubbedEntry.setRunIdentifier(unscrubbedEntry
0922: .getRunIdentifier());
0923: scrubbedEntry
0924: .setLaborLedgerOriginalChartOfAccountsCode(unscrubbedEntry
0925: .getLaborLedgerOriginalChartOfAccountsCode());
0926: scrubbedEntry
0927: .setLaborLedgerOriginalAccountNumber(unscrubbedEntry
0928: .getLaborLedgerOriginalAccountNumber());
0929: scrubbedEntry
0930: .setLaborLedgerOriginalSubAccountNumber(unscrubbedEntry
0931: .getLaborLedgerOriginalSubAccountNumber());
0932: scrubbedEntry
0933: .setLaborLedgerOriginalFinancialObjectCode(unscrubbedEntry
0934: .getLaborLedgerOriginalFinancialObjectCode());
0935: scrubbedEntry
0936: .setLaborLedgerOriginalFinancialSubObjectCode(unscrubbedEntry
0937: .getLaborLedgerOriginalFinancialSubObjectCode());
0938: scrubbedEntry.setHrmsCompany(unscrubbedEntry.getHrmsCompany());
0939: scrubbedEntry.setSetid(unscrubbedEntry.getSetid());
0940: scrubbedEntry.setTransactionDateTimeStamp(unscrubbedEntry
0941: .getTransactionDateTimeStamp());
0942: scrubbedEntry.setReferenceFinancialDocumentType(unscrubbedEntry
0943: .getReferenceFinancialDocumentType());
0944: scrubbedEntry
0945: .setReferenceFinancialSystemOrigination(unscrubbedEntry
0946: .getReferenceFinancialSystemOrigination());
0947: scrubbedEntry.setPayrollEndDateFiscalPeriod(unscrubbedEntry
0948: .getPayrollEndDateFiscalPeriod());
0949:
0950: }
0951:
0952: /**
0953: * The demerger process reads all of the documents in the error group, then moves all of the original entries for that document
0954: * from the valid group to the error group. It does not move generated entries to the error group. Those are deleted. It also
0955: * modifies the doc number and origin code of cost share transfers.
0956: *
0957: * @param errorGroup
0958: * @param validGroup
0959: */
0960: private void performDemerger(OriginEntryGroup errorGroup,
0961: OriginEntryGroup validGroup) {
0962: LOG.debug("performDemerger() started");
0963:
0964: // Without this step, the job fails with Optimistic Lock Exceptions
0965: persistenceService.clearCache();
0966:
0967: DemergerReportData demergerReport = new DemergerReportData();
0968: OriginEntryStatistics eOes = laborOriginEntryService
0969: .getStatistics(errorGroup.getId());
0970: demergerReport.setErrorTransactionsRead(eOes.getRowCount());
0971:
0972: String[] documentTypesBeProcessed = { "BT", "YEBT", "ST",
0973: "YEST" };
0974:
0975: // Read all the documents from the error group and move all non-generated
0976: // transactions for these documents from the valid group into the error group
0977: Iterator<LaborOriginEntry> errorEntryIterator = laborOriginEntryService
0978: .getEntriesByGroup(errorGroup);
0979: while (errorEntryIterator.hasNext()) {
0980: LaborOriginEntry errorEntry = errorEntryIterator.next();
0981: String documentTypeCode = errorEntry
0982: .getFinancialDocumentTypeCode().trim();
0983:
0984: // Check each entry is from Benefit Expense Transfer or Salary Expense Transfer
0985: Collection<LaborOriginEntry> transactions = null;
0986: if (ArrayUtils.contains(documentTypesBeProcessed,
0987: documentTypeCode)) {
0988: String documentNumber = errorEntry.getDocumentNumber();
0989: String originationCode = errorEntry
0990: .getFinancialSystemOriginationCode();
0991:
0992: // if so, get entry from valid group. It should be Source or Target accounting line
0993: transactions = laborOriginEntryService
0994: .getEntriesByDocument(validGroup,
0995: documentNumber, documentTypeCode,
0996: originationCode);
0997: }
0998:
0999: // put the transactions into an error group
1000: if (transactions != null) {
1001: for (LaborOriginEntry transaction : transactions) {
1002: demergerReport.incrementErrorTransactionsSaved();
1003: transaction.setGroup(errorGroup);
1004: laborOriginEntryService.save(transaction);
1005: }
1006: }
1007: }
1008:
1009: Collection<OriginEntryGroup> validGroups = new ArrayList<OriginEntryGroup>();
1010: validGroups.add(validGroup);
1011:
1012: int validTransactionsSaved = laborOriginEntryService
1013: .getCountOfEntriesInGroups(validGroups);
1014: demergerReport
1015: .setValidTransactionsSaved(validTransactionsSaved);
1016:
1017: eOes = laborOriginEntryService
1018: .getStatistics(errorGroup.getId());
1019: demergerReport.setErrorTransactionWritten(eOes.getRowCount());
1020:
1021: String reportsDirectory = ReportRegistry.getReportsDirectory();
1022: laborReportService.generateScrubberDemergerStatisticsReports(
1023: demergerReport, reportsDirectory, runDate);
1024: }
1025: }
|