001 /* 002 * Copyright 2011 The Kuali Foundation. 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.kfs.module.bc.batch.dataaccess.impl; 017 018 import java.math.BigDecimal; 019 import java.math.MathContext; 020 import java.math.RoundingMode; 021 import java.sql.Date; 022 import java.util.ArrayList; 023 import java.util.HashMap; 024 import java.util.HashSet; 025 import java.util.Iterator; 026 import java.util.List; 027 import java.util.Map; 028 029 import org.apache.log4j.Level; 030 import org.apache.log4j.Logger; 031 import org.apache.ojb.broker.query.Criteria; 032 import org.apache.ojb.broker.query.QueryByCriteria; 033 import org.apache.ojb.broker.query.ReportQueryByCriteria; 034 import org.kuali.kfs.coa.businessobject.Account; 035 import org.kuali.kfs.coa.businessobject.ObjectCode; 036 import org.kuali.kfs.coa.businessobject.Organization; 037 import org.kuali.kfs.coa.service.OrganizationService; 038 import org.kuali.kfs.fp.businessobject.FiscalYearFunctionControl; 039 import org.kuali.kfs.fp.businessobject.FunctionControlCode; 040 import org.kuali.kfs.gl.GeneralLedgerConstants.ColumnNames; 041 import org.kuali.kfs.gl.businessobject.Balance; 042 import org.kuali.kfs.integration.ld.LaborLedgerObject; 043 import org.kuali.kfs.module.bc.BCConstants; 044 import org.kuali.kfs.module.bc.BCPropertyConstants; 045 import org.kuali.kfs.module.bc.batch.dataaccess.BudgetConstructionHumanResourcesPayrollInterfaceDao; 046 import org.kuali.kfs.module.bc.batch.dataaccess.GenesisDao; 047 import org.kuali.kfs.module.bc.businessobject.BudgetConstructionAccountOrganizationHierarchy; 048 import org.kuali.kfs.module.bc.businessobject.BudgetConstructionAccountReports; 049 import org.kuali.kfs.module.bc.businessobject.BudgetConstructionAdministrativePost; 050 import org.kuali.kfs.module.bc.businessobject.BudgetConstructionAppointmentFundingReason; 051 import org.kuali.kfs.module.bc.businessobject.BudgetConstructionCalculatedSalaryFoundationTracker; 052 import org.kuali.kfs.module.bc.businessobject.BudgetConstructionFundingLock; 053 import org.kuali.kfs.module.bc.businessobject.BudgetConstructionHeader; 054 import org.kuali.kfs.module.bc.businessobject.BudgetConstructionIntendedIncumbent; 055 import org.kuali.kfs.module.bc.businessobject.BudgetConstructionMonthly; 056 import org.kuali.kfs.module.bc.businessobject.BudgetConstructionOrganizationReports; 057 import org.kuali.kfs.module.bc.businessobject.BudgetConstructionPosition; 058 import org.kuali.kfs.module.bc.businessobject.CalculatedSalaryFoundationTracker; 059 import org.kuali.kfs.module.bc.businessobject.CalculatedSalaryFoundationTrackerOverride; 060 import org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding; 061 import org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionGeneralLedger; 062 import org.kuali.kfs.module.bc.document.BudgetConstructionDocument; 063 import org.kuali.kfs.sys.KFSConstants; 064 import org.kuali.kfs.sys.KFSPropertyConstants; 065 import org.kuali.kfs.sys.KFSConstants.BudgetConstructionConstants; 066 import org.kuali.kfs.sys.KFSConstants.ParameterValues; 067 import org.kuali.kfs.sys.businessobject.FinancialSystemDocumentHeader; 068 import org.kuali.kfs.sys.businessobject.UniversityDate; 069 import org.kuali.kfs.sys.context.SpringContext; 070 import org.kuali.rice.kew.exception.WorkflowException; 071 import org.kuali.rice.kns.dao.DocumentDao; 072 import org.kuali.rice.kns.service.DateTimeService; 073 import org.kuali.rice.kns.service.DocumentService; 074 import org.kuali.rice.kns.service.KualiModuleService; 075 import org.kuali.rice.kns.util.KualiDecimal; 076 import org.kuali.rice.kns.util.KualiInteger; 077 import org.kuali.rice.kns.util.TransactionalServiceUtils; 078 import org.kuali.rice.kns.workflow.service.WorkflowDocumentService; 079 080 081 public class GenesisDaoOjb extends BudgetConstructionBatchHelperDaoOjb implements GenesisDao { 082 /* 083 * December, 2006: 084 * These routines are written to try to mitigate the performance hit that 085 * comes from using OJB as opposed to JDBC (pass-through SQL). Pass-through 086 * SQL in Kuali could lead to database-dependencies in the code, and tie Kuali 087 * to a specific RDBMS. 088 * OJB is not really suited for batch, where rows are fetched, inserted, and 089 * updated in big bunches as opposed to a few at a time. 090 * (1) OJB in "lazy evaluation mode" (the Kuali standard for performance 091 * reasons) will only return the row from the main table regardless of 092 * how many "reference descriptor" joins and/or "collection descriptor" 093 * joins there may be in the OJB repository file. So, if I query table A and 094 * reference table B, my query (in batch) might return 10,000 A rows in 095 * a single call. None of the matching B fields will be filled in in the 096 * DAO. If I then try to access a B field in a given instance of the DAO, 097 * Spring will do a query to fetch the relevant B row. In essence, in batch 098 * I would do a single DB call to get the 10,000 rows of A, and 10,000 DB 099 * calls to fill in the fields from B, one for each row of A. 100 * (2) This routine tries to do joins in java, in memory, by using what Oracle 101 * calls a "hash join". If we want to join A and B on a key, we will get 102 * the relevant fields from A and B on separate DB calls (one for A and one 103 * for B), and create a hash map on the join key from the results. We can 104 * then iterate through either A or B and get the relevant fields from the 105 * other table by employing the hash key. This should be fast, since hash 106 * tables are designed for fast access. 107 * (3) We will only store when absolutely necessary to minimize data base access. 108 * So where in Oracle we would do an UPDATE A.. WHERE (EXISTS (SELECT 1 FROM B 109 * WHERE A matches B) or an INSERT A (SELECT ... FROM A, B WHERE A = B), we will 110 * get all the candidate rows from both A and B, and store individually to do 111 * INSERT or UPDATE. (There seems to be no way in OJB to store more than 112 * one row at a time.) This may lead to a lot of database calls that operate 113 * on a single row. We can only try to minimize this problem. We can't 114 * get around it. 115 * This is the impression of the coder. If anyone has other suggestions, please 116 * let us know. 117 * (One alternative might be to have many different class-desriptor tags in the 118 * OJB repository file representing table A, one for each join to table B. If 119 * we could override lazy evaluation at the class-descriptor level, we could 120 * code some batch-specific joins that would get everything we need in one call. 121 * The problem with this is that the A/B descriptions would then be in multiple 122 * tags, and changing them would be labor-intensive and error-prone. But OJB 123 * repositories allow headers, so we could get around this by using an entity to 124 * describe the A fields. The entity would be in one place, so changes to the A 125 * fields could also be made in one place. The foreignkey field-ref tag B fields 126 * are repeated in every description anyway, so things aren't always in one place 127 * to begin with.) 128 * November, 2008: 129 * JOINs (especially those using one-to-many relationships) are problematic in OJB, as the admit in their docmentation. 130 * But, it is possible, using ReportQuery and qualifiers, to tell OJB how to build efficient JOIN SQL correctly. The code 131 * below, however, seems to be efficient as well. The only disadvantage it has is that it requires additional memory. Based 132 * on tests against realistic data sets (over 400,000 rows written), the OJB cache is a far greater memory hog--and itself uses 133 * hashmaps--than these hashmaps. If you are running batch in your own container with no other threads active, it is easy to 134 * turn off the OJB cache dynamically. This code doesn't really make use of the OJB cache. It attempts to avoid reading the 135 * same row repeatedly. It also uses report queries extensively, and those do not cache results. 136 * 137 */ 138 139 private FiscalYearFunctionControl fiscalYearFunctionControl; 140 private FunctionControlCode functionControlCode; 141 142 /* turn on the logger for the persistence broker */ 143 private static Logger LOG = org.apache.log4j.Logger.getLogger(GenesisDaoOjb.class); 144 145 /* 146 * version number for new rows 147 */ 148 public final static Long DEFAULT_VERSION_NUMBER = new Long(1); 149 /* 150 * code a high value for the limit of the organization reporting chain. we limit this to avoid 151 * infinite loops when for some reason there is a circular reporting chain in the DB 152 */ 153 public final static Integer MAXIMUM_ORGANIZATION_TREE_DEPTH = new Integer(1000); 154 155 private DocumentService documentService; 156 private WorkflowDocumentService workflowDocumentService; 157 private DateTimeService dateTimeService; 158 private DocumentDao documentDao; 159 private KualiModuleService kualiModuleService; 160 private BudgetConstructionHumanResourcesPayrollInterfaceDao budgetConstructionHumanResourcesPayrollInterfaceDao; 161 162 163 public final Map<String, String> getBudgetConstructionControlFlags(Integer universityFiscalYear) { 164 /* return the flag names and the values for all the BC flags for the fiscal year */ 165 166 /* the key to the map returned will be the name of the flag 167 * the entry will be the flag's value 168 */ 169 Map<String, String> controlFlags = new HashMap(); 170 Criteria criteriaID = new Criteria(); 171 criteriaID.addEqualTo(KFSConstants.UNIVERSITY_FISCAL_YEAR_PROPERTY_NAME, universityFiscalYear); 172 String[] queryAttr = { KFSPropertyConstants.FINANCIAL_SYSTEM_FUNCTION_CONTROL_CODE, KFSPropertyConstants.FINANCIAL_SYSTEM_FUNCTION_ACTIVE_INDICATOR }; 173 ReportQueryByCriteria queryID = new ReportQueryByCriteria(FiscalYearFunctionControl.class, queryAttr, criteriaID); 174 Iterator Results = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 175 /* fill in the map */ 176 while (Results.hasNext()) { 177 String[] mapValues = (String[]) ((Object[]) Results.next()); 178 controlFlags.put(mapValues[0], mapValues[1]); 179 } 180 ; 181 return controlFlags; 182 } 183 184 public boolean getBudgetConstructionControlFlag(Integer universityFiscalYear, String FlagID) { 185 /* return true if a flag is on, false if it is not */ 186 Boolean result; 187 Criteria criteriaID = new Criteria(); 188 criteriaID.addEqualTo(KFSConstants.UNIVERSITY_FISCAL_YEAR_PROPERTY_NAME, universityFiscalYear); 189 criteriaID.addEqualTo(KFSPropertyConstants.FINANCIAL_SYSTEM_FUNCTION_CONTROL_CODE, FlagID); 190 String[] queryAttr = { KFSPropertyConstants.FINANCIAL_SYSTEM_FUNCTION_ACTIVE_INDICATOR }; 191 ReportQueryByCriteria queryID = new ReportQueryByCriteria(FiscalYearFunctionControl.class, queryAttr, criteriaID, true); 192 Iterator Results = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 193 result = (Boolean) ((Object[]) Results.next())[0]; 194 return result; 195 } 196 197 198 /* 199 * ******************************************************************************** 200 * This routine returns the fiscal year as of the run date. 201 * Normally, genesis runs in the current fiscal year to build budget construction 202 * for the coming fiscal year. 203 */ 204 public Integer fiscalYearFromToday() { 205 // we look up the fiscal year for today's date, and return it 206 // we return 0 if nothing is found 207 Integer currentFiscalYear = new Integer(0); 208 Date lookUpDate = dateTimeService.getCurrentSqlDateMidnight(); 209 Criteria criteriaID = new Criteria(); 210 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_DATE, lookUpDate); 211 String[] attrb = { KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR }; 212 ReportQueryByCriteria queryID = new ReportQueryByCriteria(UniversityDate.class, attrb, criteriaID); 213 Iterator resultRow = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 214 // there will only be one row. 215 // Oracle will not close the cursor, however, until the iterator has been exhausted 216 if (resultRow.hasNext()) { 217 currentFiscalYear = (Integer) ((Number) ((Object[]) TransactionalServiceUtils.retrieveFirstAndExhaustIterator(resultRow))[0]).intValue(); 218 } 219 LOG.debug(String.format("\nreturned from fiscalYearFromToday: %d", currentFiscalYear)); 220 return currentFiscalYear; 221 } 222 223 224 /* 225 * ****************************************************************************** 226 * (1) these routines are used to create and set the control flags for budget * 227 * construction. Genesis sets flags for both the current fiscal year and the * 228 * fiscal year to be budgeted to a fixed set of initial values. The flags * 229 * are changed after that using maintenance screens. * 230 * ****************************************************************************** 231 */ 232 233 public void setControlFlagsAtTheStartOfGenesis(Integer BaseYear) { 234 Integer RequestYear = BaseYear + 1; 235 // 236 // first we have to eliminate anything for the new year that's there now 237 getPersistenceBrokerTemplate().clearCache(); 238 Criteria criteriaID = new Criteria(); 239 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 240 QueryByCriteria queryID = new QueryByCriteria(FiscalYearFunctionControl.class, criteriaID); 241 getPersistenceBrokerTemplate().deleteByQuery(queryID); 242 getPersistenceBrokerTemplate().clearCache(); 243 // 244 // the default values (except for the BUDGET_CONSTRUCTION_GENESIS_RUNNING flag) 245 // come from the function control code table 246 FiscalYearFunctionControl SLF; 247 criteriaID = QueryByCriteria.CRITERIA_SELECT_ALL; 248 String[] attrQ = { KFSPropertyConstants.FINANCIAL_SYSTEM_FUNCTION_CONTROL_CODE, KFSPropertyConstants.FINANCIAL_SYSTEM_FUNCTION_DEFAULT_INDICATOR }; 249 ReportQueryByCriteria rptQueryID = new ReportQueryByCriteria(FunctionControlCode.class, attrQ, criteriaID); 250 Integer sqlFunctionControlCode = 0; 251 Integer sqlFunctionActiveIndicator = 1; 252 // run the query 253 Iterator Results = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(rptQueryID); 254 while (Results.hasNext()) { 255 SLF = new FiscalYearFunctionControl(); 256 Object[] resultFields = (Object[]) Results.next(); 257 String flagTag = (String) resultFields[sqlFunctionControlCode]; 258 // String flagDefault = (String) resultFields[sqlFunctionActiveIndicator]; 259 // apparently OJB is smart enough to bring this in as a boolean 260 boolean flagDefault = (Boolean) resultFields[sqlFunctionActiveIndicator]; 261 SLF.setUniversityFiscalYear(RequestYear); 262 LOG.debug("\nfiscal year has been set"); 263 SLF.setFinancialSystemFunctionControlCode(flagTag); 264 LOG.debug("\nfunction code has been set"); 265 SLF.setVersionNumber(DEFAULT_VERSION_NUMBER); 266 LOG.debug(String.format("\nversion number set to %d", SLF.getVersionNumber())); 267 if (flagTag.equals(BudgetConstructionConstants.BUDGET_CONSTRUCTION_GENESIS_RUNNING)) { 268 SLF.setFinancialSystemFunctionActiveIndicator(true); 269 } 270 else { 271 // SLF.setFinancialSystemFunctionActiveIndicator( 272 // ((flagDefault == KFSConstants.ParameterValues.YES)? true : false)); 273 SLF.setFinancialSystemFunctionActiveIndicator(flagDefault); 274 } 275 LOG.debug("\nabout to store the result"); 276 getPersistenceBrokerTemplate().store(SLF); 277 } 278 } 279 280 public void setControlFlagsAtTheEndOfGenesis(Integer BaseYear) { 281 Integer RequestYear = BaseYear + 1; 282 resetExistingFlags(BaseYear, BCConstants.CURRENT_FSCL_YR_CTRL_FLAGS); 283 resetExistingFlags(RequestYear, BCConstants.NEXT_FSCL_YR_CTRL_FLAGS_AFTER_GENESIS); 284 } 285 286 // this method just reads the existing flags and changes their values 287 // based on the configuration constants 288 public void resetExistingFlags(Integer Year, HashMap<String, String> configValues) { 289 Criteria criteriaID = new Criteria(); 290 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, Year); 291 QueryByCriteria queryID = new QueryByCriteria(FiscalYearFunctionControl.class, criteriaID); 292 Iterator Results = getPersistenceBrokerTemplate().getIteratorByQuery(queryID); 293 while (Results.hasNext()) { 294 LOG.debug("\nbefore call to next() and cast"); 295 FiscalYearFunctionControl SLF = (FiscalYearFunctionControl) Results.next(); 296 LOG.debug("\nafter call to next()"); 297 String mapKey = SLF.getFinancialSystemFunctionControlCode(); 298 String newValue = configValues.get(mapKey); 299 SLF.setFinancialSystemFunctionActiveIndicator(((newValue.equals(ParameterValues.YES)) ? true : false)); 300 LOG.debug("\nabout to store the result"); 301 getPersistenceBrokerTemplate().store(SLF); 302 LOG.debug("\nafter store"); 303 } 304 } 305 306 /* 307 * **************************************************************** 308 * (2) intialization for genesis * 309 * these methods clean out the PBGL and document tables. * 310 * BC only allows one fiscal year at a time * 311 * (this could be modified to clear things out by fiscal year) * 312 * it should be modified to add more tables * 313 * * 314 * NOTE (IMPORTANT): * 315 * In order to do a bulk delete, we MUST use deleteByQuery. The * 316 * only alternative is to fetch each existing row and delete it, * 317 * one at a time. This is unrealistic in a batch process dealing * 318 * with many rows. deleteByQuery does NOT "synchronize" the cache:* 319 * the deleted rows remain in the cache and will be fetched on a * 320 * subsequent database call if they fit the criteria. The OJB * 321 * documentation explicitly states this. So, after a * 322 * deleteByQuery we need to call clearCache. This will not remove* 323 * instantiated objects but will remove the database rows that * 324 * were used to build them. One consequence is that every store * 325 * of an object that was instantiated before the cache was cleared* 326 * will generate a select on the first key field (to test whether * 327 * the object exists) and then an INSERT if it does not or an * 328 * UPDATE if it does. This same process happens if on changes the* 329 * key (for example, the Fiscal Year) of an instantiated object. * 330 * **************************************************************** 331 */ 332 public void clearDBForGenesis(Integer BaseYear) { 333 clearBudgetConstructionAdministrativePost(); 334 clearBudgetConstructionIntendedIncumbent(); 335 // the order is important because of referential integrity in the database 336 clearBothYearsBCSF(BaseYear); 337 clearBothYearsBudgetConstructionAppointmentFundingReason(BaseYear); 338 clearBothYearsPendingApptFunding(BaseYear); 339 clearBothYearsBCPosition(BaseYear); 340 // the calling order is important because of referential integrity in the 341 // database 342 clearBothYearsPBGL(BaseYear); 343 clearBothYearsHeaders(BaseYear); 344 } 345 346 protected void clearBudgetConstructionAdministrativePost() { 347 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionAdministrativePost.class, QueryByCriteria.CRITERIA_SELECT_ALL); 348 getPersistenceBrokerTemplate().deleteByQuery(queryId); 349 getPersistenceBrokerTemplate().clearCache(); 350 } 351 352 protected void clearBaseYearBudgetConstructionAppointmentFundingReason(Integer BaseYear) { 353 Criteria criteriaId = new Criteria(); 354 criteriaId.addColumnEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 355 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionAppointmentFundingReason.class, criteriaId); 356 getPersistenceBrokerTemplate().deleteByQuery(queryId); 357 getPersistenceBrokerTemplate().clearCache(); 358 } 359 360 protected void clearBothYearsBudgetConstructionAppointmentFundingReason(Integer BaseYear) { 361 Integer RequestYear = BaseYear + 1; 362 Criteria criteriaId = new Criteria(); 363 criteriaId.addBetween(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear, RequestYear); 364 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionAppointmentFundingReason.class, criteriaId); 365 getPersistenceBrokerTemplate().deleteByQuery(queryId); 366 getPersistenceBrokerTemplate().clearCache(); 367 } 368 369 protected void clearBudgetConstructionAppointmentFundingReason() { 370 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionAppointmentFundingReason.class, QueryByCriteria.CRITERIA_SELECT_ALL); 371 getPersistenceBrokerTemplate().deleteByQuery(queryId); 372 getPersistenceBrokerTemplate().clearCache(); 373 } 374 375 protected void clearRequestYearBudgetConstructionAppointmentFundingReason(Integer BaseYear) { 376 Integer RequestYear = BaseYear + 1; 377 Criteria criteriaId = new Criteria(); 378 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 379 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionAppointmentFundingReason.class, criteriaId); 380 getPersistenceBrokerTemplate().deleteByQuery(queryId); 381 getPersistenceBrokerTemplate().clearCache(); 382 } 383 384 protected void clearBaseYearBCSF(Integer BaseYear) { 385 Criteria criteriaId = new Criteria(); 386 criteriaId.addColumnEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 387 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionCalculatedSalaryFoundationTracker.class, criteriaId); 388 getPersistenceBrokerTemplate().deleteByQuery(queryId); 389 getPersistenceBrokerTemplate().clearCache(); 390 } 391 392 protected void clearBothYearsBCSF(Integer BaseYear) { 393 Integer RequestYear = BaseYear + 1; 394 Criteria criteriaId = new Criteria(); 395 criteriaId.addBetween(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear, RequestYear); 396 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionCalculatedSalaryFoundationTracker.class, criteriaId); 397 getPersistenceBrokerTemplate().deleteByQuery(queryId); 398 getPersistenceBrokerTemplate().clearCache(); 399 } 400 401 protected void clearBCSF() { 402 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionCalculatedSalaryFoundationTracker.class, QueryByCriteria.CRITERIA_SELECT_ALL); 403 getPersistenceBrokerTemplate().deleteByQuery(queryId); 404 getPersistenceBrokerTemplate().clearCache(); 405 } 406 407 protected void clearBudgetConstructionIntendedIncumbent() { 408 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionIntendedIncumbent.class, QueryByCriteria.CRITERIA_SELECT_ALL); 409 getPersistenceBrokerTemplate().deleteByQuery(queryId); 410 getPersistenceBrokerTemplate().clearCache(); 411 } 412 413 protected void clearBaseYearBCPosition(Integer BaseYear) { 414 Criteria criteriaId = new Criteria(); 415 criteriaId.addColumnEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 416 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionPosition.class, criteriaId); 417 getPersistenceBrokerTemplate().deleteByQuery(queryId); 418 getPersistenceBrokerTemplate().clearCache(); 419 } 420 421 protected void clearBothYearsBCPosition(Integer BaseYear) { 422 Integer RequestYear = BaseYear + 1; 423 Criteria criteriaId = new Criteria(); 424 criteriaId.addBetween(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear, RequestYear); 425 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionPosition.class, criteriaId); 426 getPersistenceBrokerTemplate().deleteByQuery(queryId); 427 getPersistenceBrokerTemplate().clearCache(); 428 } 429 430 protected void clearBCPosition() { 431 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionPosition.class, QueryByCriteria.CRITERIA_SELECT_ALL); 432 getPersistenceBrokerTemplate().deleteByQuery(queryId); 433 getPersistenceBrokerTemplate().clearCache(); 434 } 435 436 protected void clearRequestYearBCPosition(Integer BaseYear) { 437 Integer RequestYear = BaseYear + 1; 438 Criteria criteriaId = new Criteria(); 439 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 440 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionPosition.class, criteriaId); 441 getPersistenceBrokerTemplate().deleteByQuery(queryId); 442 getPersistenceBrokerTemplate().clearCache(); 443 } 444 445 protected void clearRequestYearBCSF(Integer BaseYear) { 446 Integer RequestYear = BaseYear + 1; 447 Criteria criteriaId = new Criteria(); 448 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 449 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionCalculatedSalaryFoundationTracker.class, criteriaId); 450 getPersistenceBrokerTemplate().deleteByQuery(queryId); 451 getPersistenceBrokerTemplate().clearCache(); 452 } 453 454 protected void clearBaseYearHeaders(Integer BaseYear) { 455 Criteria criteriaId = new Criteria(); 456 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 457 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionHeader.class, criteriaId); 458 getPersistenceBrokerTemplate().deleteByQuery(queryId); 459 getPersistenceBrokerTemplate().clearCache(); 460 } 461 462 protected void clearBothYearsHeaders(Integer BaseYear) { 463 Integer RequestYear = BaseYear + 1; 464 Criteria criteriaId = new Criteria(); 465 criteriaId.addBetween(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear, RequestYear); 466 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionHeader.class, criteriaId); 467 getPersistenceBrokerTemplate().deleteByQuery(queryId); 468 getPersistenceBrokerTemplate().clearCache(); 469 } 470 471 protected void clearHeaders() { 472 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionHeader.class, QueryByCriteria.CRITERIA_SELECT_ALL); 473 getPersistenceBrokerTemplate().deleteByQuery(queryId); 474 getPersistenceBrokerTemplate().clearCache(); 475 } 476 477 protected void clearBaseYearPBGL(Integer BaseYear) { 478 // the order here is mandated by referential integrity 479 // remove rows from the base year from budget construction months 480 Criteria mnCriteriaID = new Criteria(); 481 mnCriteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 482 QueryByCriteria mnQueryID = new QueryByCriteria(BudgetConstructionMonthly.class, mnCriteriaID); 483 getPersistenceBrokerTemplate().deleteByQuery(mnQueryID); 484 // remove rows from the basse year from budget construction general ledger 485 Criteria criteriaID = new Criteria(); 486 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 487 QueryByCriteria queryID = new QueryByCriteria(PendingBudgetConstructionGeneralLedger.class, criteriaID); 488 LOG.debug(String.format("delete PBGL started at %tT for %d", dateTimeService.getCurrentDate(), BaseYear)); 489 getPersistenceBrokerTemplate().deleteByQuery(queryID); 490 LOG.debug(String.format("delete PBGL ended at %tT", dateTimeService.getCurrentDate())); 491 getPersistenceBrokerTemplate().clearCache(); 492 } 493 494 protected void clearBothYearsPBGL(Integer BaseYear) { 495 clearBaseYearPBGL(BaseYear); 496 clearRequestYearPBGL(BaseYear); 497 } 498 499 protected void clearPBGL() { 500 // the order here is mandated by referential integrity 501 QueryByCriteria mnQueryId = new QueryByCriteria(BudgetConstructionMonthly.class, QueryByCriteria.CRITERIA_SELECT_ALL); 502 getPersistenceBrokerTemplate().deleteByQuery(mnQueryId); 503 QueryByCriteria queryId = new QueryByCriteria(PendingBudgetConstructionGeneralLedger.class, QueryByCriteria.CRITERIA_SELECT_ALL); 504 getPersistenceBrokerTemplate().deleteByQuery(queryId); 505 getPersistenceBrokerTemplate().clearCache(); 506 } 507 508 protected void clearRequestYearPBGL(Integer BaseYear) { 509 Integer RequestYear = BaseYear + 1; 510 // the order here is mandated by referential integrity 511 // remove rows from the request year from budget construction months 512 Criteria mnCriteriaID = new Criteria(); 513 mnCriteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 514 QueryByCriteria mnQueryID = new QueryByCriteria(BudgetConstructionMonthly.class, mnCriteriaID); 515 getPersistenceBrokerTemplate().deleteByQuery(mnQueryID); 516 // remove rows from the request year 517 Criteria criteriaID = new Criteria(); 518 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 519 QueryByCriteria queryID = new QueryByCriteria(PendingBudgetConstructionGeneralLedger.class, criteriaID); 520 LOG.debug(String.format("\ndelete PBGL started at %tT for %d", dateTimeService.getCurrentDate(), RequestYear)); 521 getPersistenceBrokerTemplate().deleteByQuery(queryID); 522 LOG.debug(String.format("\ndelete PBGL ended at %tT", dateTimeService.getCurrentDate())); 523 getPersistenceBrokerTemplate().clearCache(); 524 } 525 526 protected void clearBaseYearPendingApptFunding(Integer BaseYear) { 527 Criteria criteriaId = new Criteria(); 528 criteriaId.addColumnEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 529 QueryByCriteria queryId = new QueryByCriteria(PendingBudgetConstructionAppointmentFunding.class, criteriaId); 530 getPersistenceBrokerTemplate().deleteByQuery(queryId); 531 getPersistenceBrokerTemplate().clearCache(); 532 } 533 534 protected void clearBothYearsPendingApptFunding(Integer BaseYear) { 535 Integer RequestYear = BaseYear + 1; 536 Criteria criteriaId = new Criteria(); 537 criteriaId.addBetween(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear, RequestYear); 538 QueryByCriteria queryId = new QueryByCriteria(PendingBudgetConstructionAppointmentFunding.class, criteriaId); 539 getPersistenceBrokerTemplate().deleteByQuery(queryId); 540 getPersistenceBrokerTemplate().clearCache(); 541 } 542 543 protected void clearPendingApptFunding() { 544 QueryByCriteria queryId = new QueryByCriteria(PendingBudgetConstructionAppointmentFunding.class, QueryByCriteria.CRITERIA_SELECT_ALL); 545 getPersistenceBrokerTemplate().deleteByQuery(queryId); 546 getPersistenceBrokerTemplate().clearCache(); 547 } 548 549 protected void clearRequestYearPendingApptFunding(Integer BaseYear) { 550 Integer RequestYear = BaseYear + 1; 551 Criteria criteriaId = new Criteria(); 552 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 553 QueryByCriteria queryId = new QueryByCriteria(PendingBudgetConstructionAppointmentFunding.class, criteriaId); 554 getPersistenceBrokerTemplate().deleteByQuery(queryId); 555 getPersistenceBrokerTemplate().clearCache(); 556 } 557 558 /* 559 * **************************************************************************** 560 * (3) BC Document Creation * 561 * **************************************************************************** 562 */ 563 /* 564 * A document number is created for each account/sub-account involved in budgdet construction. 565 * These "documents" are not routed. They are created and finalized in workflow in a single step. 566 * They serve two purposes. First, the budget construction tool can use the workflow action tables to 567 * track the people who have edited each account/sub-account, and use the workflow notes tables to allow 568 * these editors to leave whatever comments they may wish to make. Second, the general ledger entry 569 * table requires that every entry have a document number attached. Assigning a document number to 570 * each account/sub-account in the budget allows us to load the budget to the general ledger with 571 * a document number that can be tracked in workflow as outlined above. 572 * 573 * An account/subaccount can get into budget construction in three ways. 574 * (1) Someone can create a budget for it using the budget construction on-line tool. Neither genesis nor 575 * the batch update process cares about this case, so it is irrelevant here. 576 * (2) There is an existing base budget in the current fiscal year for the account/subaccount. In this case, 577 * genesis and the batch update process will create a document for it in budget construction if one does not already 578 * exist (much of the code below involves looking at the GL and checking for existing documents for account/subaccounts with 579 * base budget in the GL. 580 * (3) There is no base budget in the GL, but for some reason there is a person in a budgeted position drawing pay in 581 * payroll. In this case, if no document number exists, we will create one. It will point to budget construction CSF tracker 582 * (i.e., payroll) base, but it will not have any base budget amounts in the budget construction general ledger. 583 * 584 * NOTE: Kuali workflow went through a lot of iterations since this code was written. I have tried to remove any comments 585 * that pertain to iterations other than the stable release 3 configuration, but I may have missed something. Earlier iterations 586 * (a) used a lot of memory and (b) were at one point in a separate transaction. 587 * Because this code will have to deal with a remote workflow server in real life, it will be the slowest part of genesis. We did 588 * not have a good test bed for thoroughly researching this issue, but based on extrapolation I believe the solution we came up with 589 * (going to "complete" status immediately in order to by-pass routing, then using workflow routines to read the actual workflow 590 * document tables and set all the dates to final and save the initial "action taken") will give acceptable performance. 591 */ 592 593 private HashSet<String> currentBCHeaderKeys = new HashSet<String>(1); 594 // these routines are used to merge CSF and CSF Override 595 private HashMap<String, String[]> CSFTrackerKeys = new HashMap<String, String[]>(1); 596 597 protected void createNewDocumentsCleanUp() { 598 currentBCHeaderKeys.clear(); 599 CSFTrackerKeys.clear(); 600 } 601 602 // counters 603 Long documentsToCreateinNTS = new Long(0); 604 Long documentsSkippedinNTS = new Long(0); 605 Long documentsCreatedinNTS = new Long(0); 606 Long documentsCSFCreatedinNTS = new Long(0); 607 Long documentsGLCreatedinNTS = new Long(0); 608 609 Long proxyCandidatesReadinTS = new Long(0); 610 Long proxyBCHeadersCreatedinTS = new Long(0); 611 612 // 613 // this is the new document creation mechanism that works with embedded workflow 614 public void createNewBCDocumentsFromGLCSF(Integer BaseYear, boolean GLUpdatesAllowed, boolean CSFUpdatesAllowed) { 615 if ((!GLUpdatesAllowed) && (!CSFUpdatesAllowed)) { 616 // no new documents need to be created 617 return; 618 } 619 // take the count of header keys from the GL 620 setUpCurrentBCHeaderKeys(BaseYear); 621 Integer RequestYear = BaseYear + 1; 622 // fetch the keys currently in budget construction header 623 getCurrentBCHeaderKeys(BaseYear); 624 // 625 // we have to read the GL BALANCE (which is not proxy=true) to create 626 // new BC header objects. we use a report query to avoid triggering 627 // nine separate reads for each row, and to avoid returning the entire 628 // field list when we only need a few fields. 629 if (GLUpdatesAllowed) { 630 getAndStoreCurrentGLBCHeaderCandidates(BaseYear); 631 } 632 // we also have to read CSF for any accounts with no base budget in GL BALANCE 633 // but which pay people in budgeted positions 634 if (CSFUpdatesAllowed) { 635 setUpCSFHashStructures(BaseYear); 636 getCSFCandidateDocumentKeys(BaseYear); 637 getCSFOverrideDeletedKeys(BaseYear); 638 getCSFOverrideCandidateDocumentKeys(BaseYear); 639 getAndStoreCurrentCSFBCHeaderCandidates(BaseYear); 640 } 641 createNewDocumentsCleanUp(); 642 } 643 644 // here are the private methods that go with it 645 protected void getAndStoreCurrentCSFBCHeaderCandidates(Integer BaseYear) { 646 Integer RequestYear = BaseYear + 1; 647 for (Map.Entry<String, String[]> newCSFDocs : CSFTrackerKeys.entrySet()) { 648 // all the CSF keys in the map require new documents 649 proxyCandidatesReadinTS = proxyCandidatesReadinTS + 1; 650 String[] Results = newCSFDocs.getValue(); 651 // set up the Budget Construction Header 652 BudgetConstructionDocument newBCHdr; 653 try { 654 newBCHdr = (BudgetConstructionDocument) documentService.getNewDocument(BCConstants.BUDGET_CONSTRUCTION_DOCUMENT_NAME); 655 } 656 catch (WorkflowException wex) { 657 LOG.warn(String.format("\nskipping creation of document for CSF key: %s %s %s \n(%s)\n", Results[0], Results[1], Results[2], wex.getMessage())); 658 wex.printStackTrace(); 659 documentsSkippedinNTS = documentsSkippedinNTS + 1; 660 continue; 661 } 662 newBCHdr.setUniversityFiscalYear(RequestYear); 663 newBCHdr.setChartOfAccountsCode(Results[0]); 664 newBCHdr.setAccountNumber(Results[1]); 665 newBCHdr.setSubAccountNumber(Results[2]); 666 // store the document 667 try { 668 storeANewBCDocument(newBCHdr); 669 } 670 catch (WorkflowException wex) { 671 LOG.warn(String.format("\nskipping creation of document for CSF key: %s %s %s \n(%s)\n", newBCHdr.getChartOfAccounts(), newBCHdr.getAccountNumber(), newBCHdr.getSubAccountNumber(), wex.getMessage())); 672 wex.printStackTrace(); 673 documentsSkippedinNTS = documentsSkippedinNTS + 1; 674 continue; 675 676 } 677 documentsCSFCreatedinNTS = documentsCSFCreatedinNTS + 1; 678 documentsCreatedinNTS = documentsCreatedinNTS + 1; 679 } 680 } 681 682 protected void getAndStoreCurrentGLBCHeaderCandidates(Integer BaseYear) { 683 Integer RequestYear = BaseYear + 1; 684 // first build a document set from GL BALANCE 685 Criteria criteriaId = new Criteria(); 686 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 687 criteriaId.addEqualTo(KFSPropertyConstants.BALANCE_TYPE_CODE, KFSConstants.BALANCE_TYPE_BASE_BUDGET); 688 String newAttr = ColumnNames.BEGINNING_BALANCE + "+" + ColumnNames.ANNUAL_BALANCE; 689 criteriaId.addNotEqualTo(newAttr, 0); 690 String[] queryAttr = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ACCOUNT_NUMBER, KFSPropertyConstants.SUB_ACCOUNT_NUMBER }; 691 ReportQueryByCriteria queryId = new ReportQueryByCriteria(Balance.class, queryAttr, criteriaId, true); 692 Iterator RowsReturned = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryId); 693 while (RowsReturned.hasNext()) { 694 proxyCandidatesReadinTS = proxyCandidatesReadinTS + 1; 695 Object[] Results = (Object[]) RowsReturned.next(); 696 String testKey = ((String) Results[0]) + ((String) Results[1]) + ((String) Results[2]); 697 if (currentBCHeaderKeys.contains(testKey)) { 698 // don't create a new row for anything with a current header 699 continue; 700 } 701 // set up the Budget Construction Header 702 BudgetConstructionDocument newBCHdr; 703 try { 704 newBCHdr = (BudgetConstructionDocument) documentService.getNewDocument(BCConstants.BUDGET_CONSTRUCTION_DOCUMENT_NAME); 705 } 706 catch (WorkflowException wex) { 707 LOG.warn(String.format("\nskipping creation of document for GL key: %s %s %s \n(%s)\n", (String) Results[0], (String) Results[1], (String) Results[2], wex.getMessage())); 708 wex.printStackTrace(); 709 documentsSkippedinNTS = documentsSkippedinNTS + 1; 710 continue; 711 } 712 newBCHdr.setUniversityFiscalYear(RequestYear); 713 newBCHdr.setChartOfAccountsCode((String) Results[0]); 714 newBCHdr.setAccountNumber((String) Results[1]); 715 newBCHdr.setSubAccountNumber((String) Results[2]); 716 // store the document 717 try { 718 storeANewBCDocument(newBCHdr); 719 } 720 catch (WorkflowException wex) { 721 LOG.warn(String.format("\nskipping creation of document for GL key: %s %s %s \n(%s)\n", newBCHdr.getChartOfAccounts(), newBCHdr.getAccountNumber(), newBCHdr.getSubAccountNumber(), wex.getMessage())); 722 wex.printStackTrace(); 723 documentsSkippedinNTS = documentsSkippedinNTS + 1; 724 continue; 725 726 } 727 documentsGLCreatedinNTS = documentsGLCreatedinNTS + 1; 728 documentsCreatedinNTS = documentsCreatedinNTS + 1; 729 // add this header to the current BC Header map 730 currentBCHeaderKeys.add(testKey); 731 } 732 } 733 734 public void getCSFCandidateDocumentKeys(Integer BaseYear) { 735 Criteria criteriaId = new Criteria(); 736 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 737 criteriaId.addEqualTo(KFSPropertyConstants.CSF_DELETE_CODE, BCConstants.ACTIVE_CSF_DELETE_CODE); 738 String[] queryAttr = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ACCOUNT_NUMBER, KFSPropertyConstants.SUB_ACCOUNT_NUMBER }; 739 ReportQueryByCriteria queryId = new ReportQueryByCriteria(CalculatedSalaryFoundationTracker.class, queryAttr, criteriaId, true); 740 Iterator rowsReturned = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryId); 741 // decide which keys from CSF need to create new documents 742 // we have already created new documents for all the GL keys 743 while (rowsReturned.hasNext()) { 744 Object[] returnedRow = (Object[]) rowsReturned.next(); 745 String testKey = ((String) returnedRow[0]) + ((String) returnedRow[1]) + ((String) returnedRow[2]); 746 if (currentBCHeaderKeys.contains(testKey)) { 747 // there is no need to create a row for this key 748 continue; 749 } 750 String[] valueCSF = { (String) returnedRow[0], (String) returnedRow[1], (String) returnedRow[2] }; 751 CSFTrackerKeys.put(testKey, valueCSF); 752 } 753 } 754 755 public void getCSFOverrideCandidateDocumentKeys(Integer BaseYear) { 756 Criteria criteriaId = new Criteria(); 757 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 758 criteriaId.addEqualTo(KFSPropertyConstants.CSF_DELETE_CODE, BCConstants.ACTIVE_CSF_DELETE_CODE); 759 criteriaId.addEqualTo(KFSPropertyConstants.ACTIVE, true); 760 String[] queryAttr = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ACCOUNT_NUMBER, KFSPropertyConstants.SUB_ACCOUNT_NUMBER }; 761 ReportQueryByCriteria queryId = new ReportQueryByCriteria(CalculatedSalaryFoundationTrackerOverride.class, queryAttr, criteriaId, true); 762 Iterator rowsReturned = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryId); 763 // decide which keys from CSF override need to create new documents 764 // we have already read in the CSF keys--existing keys need not be replaced 765 // new active keys from CSF override should be added 766 while (rowsReturned.hasNext()) { 767 Object[] returnedRow = (Object[]) rowsReturned.next(); 768 String testKey = ((String) returnedRow[0]) + ((String) returnedRow[1]) + ((String) returnedRow[2]); 769 if (currentBCHeaderKeys.contains(testKey)) { 770 // there is no need to create a row for this key 771 // it is already in the base budget in the GL 772 continue; 773 } 774 String[] valueCSF = { (String) returnedRow[0], (String) returnedRow[1], (String) returnedRow[2] }; 775 CSFTrackerKeys.put(testKey, valueCSF); 776 } 777 } 778 779 public void getCSFOverrideDeletedKeys(Integer BaseYear) { 780 Criteria criteriaId = new Criteria(); 781 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 782 criteriaId.addNotEqualTo(KFSPropertyConstants.CSF_DELETE_CODE, BCConstants.ACTIVE_CSF_DELETE_CODE); 783 criteriaId.addEqualTo(KFSPropertyConstants.ACTIVE, true); 784 String[] queryAttr = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ACCOUNT_NUMBER, KFSPropertyConstants.SUB_ACCOUNT_NUMBER }; 785 ReportQueryByCriteria queryId = new ReportQueryByCriteria(CalculatedSalaryFoundationTrackerOverride.class, queryAttr, criteriaId, true); 786 Iterator rowsReturned = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryId); 787 // decide which keys from CSF override need to create new documents 788 // we have already read in the CSF keys--any overrides of existing CSF 789 // which carry a delete code should be tentatively removed CSF key table 790 while (rowsReturned.hasNext()) { 791 Object[] returnedRow = (Object[]) rowsReturned.next(); 792 String testKey = ((String) returnedRow[0]) + ((String) returnedRow[1]) + ((String) returnedRow[2]); 793 if (currentBCHeaderKeys.contains(testKey)) { 794 // this key is in the GL base budget 795 // it should create a document whether anyone is paid from it 796 // or not 797 continue; 798 } 799 if (CSFTrackerKeys.containsKey(testKey)) { 800 // an override row deletes a key in CSF 801 // we tentatively remove this key from the map 802 // if there is an active override row for this key as well, it 803 // will be restored when we read the active override keys 804 CSFTrackerKeys.remove(testKey); 805 } 806 } 807 } 808 809 protected void getCurrentBCHeaderKeys(Integer BaseYear) { 810 Integer RequestYear = BaseYear + 1; 811 Criteria criteriaId = new Criteria(); 812 Iterator<Object[]> Results; 813 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 814 String[] selectList = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ACCOUNT_NUMBER, KFSPropertyConstants.SUB_ACCOUNT_NUMBER }; 815 ReportQueryByCriteria queryId = new ReportQueryByCriteria(BudgetConstructionHeader.class, selectList, criteriaId); 816 Results = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryId); 817 818 while (Results.hasNext()) { 819 Object[] returnedRow = Results.next(); 820 currentBCHeaderKeys.add(((String) returnedRow[0]) + ((String) returnedRow[1]) + ((String) returnedRow[2])); 821 } 822 } 823 824 public void setUpCSFHashStructures(Integer BaseYear) { 825 // these are the potential document keys in the CSF tracker 826 Criteria criteriaId = new Criteria(); 827 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 828 criteriaId.addEqualTo(KFSPropertyConstants.CSF_DELETE_CODE, BCConstants.ACTIVE_CSF_DELETE_CODE); 829 String[] propertyString = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ACCOUNT_NUMBER, KFSPropertyConstants.SUB_ACCOUNT_NUMBER }; 830 CSFTrackerKeys = new HashMap<String, String[]>(hashObjectSize(CalculatedSalaryFoundationTracker.class, criteriaId, propertyString)); 831 } 832 833 public void setUpCurrentBCHeaderKeys(Integer BaseYear) { 834 // the BC header keys should be roughly the same as the GL balance BB keys 835 // if any new keys are introduced from CSF, it means that there is money 836 // in the payroll that has NOT been budgeted. this should be a rare 837 // occurrence. 838 Criteria criteriaID = new Criteria(); 839 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 840 criteriaID.addEqualTo(KFSPropertyConstants.BALANCE_TYPE_CODE, KFSConstants.BALANCE_TYPE_BASE_BUDGET); 841 String[] propertyString = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ACCOUNT_NUMBER, KFSPropertyConstants.SUB_ACCOUNT_NUMBER }; 842 currentBCHeaderKeys = new HashSet<String>(hashObjectSize(Balance.class, criteriaID, propertyString)); 843 } 844 845 public void storeANewBCDocument(BudgetConstructionDocument newBCHdr) throws WorkflowException { 846 newBCHdr.setOrganizationLevelChartOfAccountsCode(BCConstants.INITIAL_ORGANIZATION_LEVEL_CHART_OF_ACCOUNTS_CODE); 847 newBCHdr.setOrganizationLevelOrganizationCode(BCConstants.INITIAL_ORGANIZATION_LEVEL_ORGANIZATION_CODE); 848 newBCHdr.setOrganizationLevelCode(BCConstants.INITIAL_ORGANIZATION_LEVEL_CODE); 849 newBCHdr.setBudgetTransactionLockUserIdentifier(BCConstants.DEFAULT_BUDGET_HEADER_LOCK_IDS); 850 newBCHdr.setBudgetLockUserIdentifier(BCConstants.DEFAULT_BUDGET_HEADER_LOCK_IDS); 851 newBCHdr.setVersionNumber(DEFAULT_VERSION_NUMBER); 852 FinancialSystemDocumentHeader kualiDocumentHeader = newBCHdr.getDocumentHeader(); 853 newBCHdr.setDocumentNumber(newBCHdr.getDocumentHeader().getDocumentNumber()); 854 kualiDocumentHeader.setOrganizationDocumentNumber(newBCHdr.getUniversityFiscalYear().toString()); 855 kualiDocumentHeader.setFinancialDocumentStatusCode(KFSConstants.INITIAL_KUALI_DOCUMENT_STATUS_CD); 856 kualiDocumentHeader.setFinancialDocumentTotalAmount(KualiDecimal.ZERO); 857 kualiDocumentHeader.setDocumentDescription(String.format("%s %d %s %s", BCConstants.BUDGET_CONSTRUCTION_DOCUMENT_DESCRIPTION, newBCHdr.getUniversityFiscalYear(), newBCHdr.getChartOfAccountsCode(), newBCHdr.getAccountNumber())); 858 kualiDocumentHeader.setExplanation(BCConstants.BUDGET_CONSTRUCTION_DOCUMENT_DESCRIPTION); 859 getPersistenceBrokerTemplate().store(newBCHdr); 860 documentService.prepareWorkflowDocument(newBCHdr); 861 // September 2, 2009: since this document is not routed, calling this method should set it to final 862 documentService.routeDocument(newBCHdr, "created by Genesis", new ArrayList()); 863 } 864 865 866 /* 867 * **************************************************************************** 868 * (4) here are the routines which freeze accounting at the beginning of * 869 * budget construction (so updates can be done in parallel, or updates * 870 * for the budget year only can be done without affecting the current * 871 * chart of accounts). * 872 * These routines only run once, at genesis. * 873 * **************************************************************************** 874 */ 875 876 // public routines 877 public void createChartForNextBudgetCycle() { 878 // first we have to remove what's there 879 // (the documentation says deleteByQuery (1) ignores object references and (2) does 880 // not synchronize the cache. so, we clear the cache before and after.) 881 getPersistenceBrokerTemplate().clearCache(); 882 Criteria criteriaID = QueryByCriteria.CRITERIA_SELECT_ALL; 883 QueryByCriteria killAcctQuery = new QueryByCriteria(BudgetConstructionAccountReports.class); 884 killAcctQuery.setCriteria(criteriaID); 885 getPersistenceBrokerTemplate().deleteByQuery(killAcctQuery); 886 QueryByCriteria killOrgQuery = new QueryByCriteria(BudgetConstructionOrganizationReports.class); 887 killOrgQuery.setCriteria(criteriaID); 888 getPersistenceBrokerTemplate().deleteByQuery(killOrgQuery); 889 getPersistenceBrokerTemplate().clearCache(); 890 // build the organization table 891 buildNewOrganizationReportsTo(); 892 // build the account table 893 buildNewAccountReportsTo(); 894 } 895 896 // private working methods for the BC chart update 897 898 protected void buildNewAccountReportsTo() { 899 900 // All active accounts are loaded into the budget accounting table 901 902 Integer sqlChartOfAccountsCode = 0; 903 Integer sqlAccountNumber = 1; 904 Integer sqlReportsToChartofAccountsCode = 0; 905 Integer sqlOrganizationCode = 2; 906 907 Long accountsAdded = new Long(0); 908 909 Criteria criteriaID = new Criteria(); 910 /* current IU genesis does NOT check for closed accounts--it loads all accounts 911 * it is possible that an account which has been closed still has base budget 912 */ 913 criteriaID = QueryByCriteria.CRITERIA_SELECT_ALL; 914 String[] queryAttr = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ACCOUNT_NUMBER, KFSPropertyConstants.ORGANIZATION_CODE }; 915 ReportQueryByCriteria queryID = new ReportQueryByCriteria(Account.class, queryAttr, criteriaID, true); 916 Iterator Results = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 917 while (Results.hasNext()) { 918 Object[] ReturnList = (Object[]) Results.next(); 919 // just save this stuff, one at a time 920 // it isn't needed for anything else, so we don't need to store it in memory in a map 921 BudgetConstructionAccountReports acctRpts = new BudgetConstructionAccountReports(); 922 acctRpts.setChartOfAccountsCode((String) ReturnList[sqlChartOfAccountsCode]); 923 acctRpts.setAccountNumber((String) ReturnList[sqlAccountNumber]); 924 acctRpts.setReportsToChartOfAccountsCode((String) ReturnList[sqlReportsToChartofAccountsCode]); 925 acctRpts.setReportsToOrganizationCode((String) ReturnList[sqlOrganizationCode]); 926 acctRpts.setVersionNumber(DEFAULT_VERSION_NUMBER); 927 getPersistenceBrokerTemplate().store(acctRpts); 928 accountsAdded = accountsAdded + 1; 929 } 930 LOG.info(String.format("\nAccount reporting lines added to budget construction %d", accountsAdded)); 931 } 932 933 protected void buildNewOrganizationReportsTo() { 934 935 // all active organizations are loaded into the budget construction 936 // organization table 937 938 Integer sqlChartOfAccountsCode = 0; 939 Integer sqlOrganizationCode = 1; 940 Integer sqlReportsToChartOfAccountsCode = 2; 941 Integer sqlReportsToOrganizationCode = 3; 942 Integer sqlResponsibilityCenterCode = 4; 943 944 Long organizationsAdded = new Long(0); 945 946 Criteria criteriaID = new Criteria(); 947 /* 948 * IU genesis takes all organizations, not just active ones 949 * the reason is that a closed account which still has a base budget 950 * might report to one of these organizations 951 */ 952 criteriaID = QueryByCriteria.CRITERIA_SELECT_ALL; 953 String[] queryAttr = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ORGANIZATION_CODE, KFSPropertyConstants.REPORTS_TO_CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.REPORTS_TO_ORGANIZATION_CODE, KFSPropertyConstants.RESPONSIBILITY_CENTER_CODE }; 954 ReportQueryByCriteria queryID = new ReportQueryByCriteria(Organization.class, queryAttr, criteriaID, true); 955 Iterator Results = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 956 while (Results.hasNext()) { 957 Object[] ReturnList = (Object[]) Results.next(); 958 // just save this stuff, one at a time 959 // it isn't needed for anything else 960 BudgetConstructionOrganizationReports orgRpts = new BudgetConstructionOrganizationReports(); 961 orgRpts.setChartOfAccountsCode((String) ReturnList[sqlChartOfAccountsCode]); 962 orgRpts.setOrganizationCode((String) ReturnList[sqlOrganizationCode]); 963 orgRpts.setReportsToChartOfAccountsCode((String) ReturnList[sqlReportsToChartOfAccountsCode]); 964 orgRpts.setReportsToOrganizationCode((String) ReturnList[sqlReportsToOrganizationCode]); 965 orgRpts.setResponsibilityCenterCode((String) ReturnList[sqlResponsibilityCenterCode]); 966 orgRpts.setVersionNumber(DEFAULT_VERSION_NUMBER); 967 getPersistenceBrokerTemplate().store(orgRpts); 968 organizationsAdded = organizationsAdded + 1; 969 } 970 LOG.info(String.format("\nOrganization reporting lines added to budget construction %d", organizationsAdded)); 971 } 972 973 /* 974 * ********************************************************************************* 975 * (5) these are the routines that build the security organization hierarchy 976 * -- they run every time the budget construction update process runs 977 * -- they are designed to pick up any changes made to the BC account and BC 978 * organization tables 979 * -- based on changes, they will adjust the security levels of accounts in the BC 980 * header. for a header at the level of an organization that is no longer valid, 981 * the level will return to the account manager level. for a header at the level 982 * of an organization that has changed its location in the hierarchy, the new 983 * level will be replace the former one in the header 984 * -- this process only affects accounts in the budget construction pending 985 * general ledger, and it is assumed that all updates to the PBGL have been 986 * finished when this process runs. 987 * ********************************************************************************* 988 */ 989 990 private HashMap<String, BudgetConstructionAccountReports> acctRptsToMap = new HashMap<String, BudgetConstructionAccountReports>(1); 991 private HashMap<String, BudgetConstructionOrganizationReports> orgRptsToMap = new HashMap<String, BudgetConstructionOrganizationReports>(1); 992 private HashMap<String, BudgetConstructionAccountOrganizationHierarchy> acctOrgHierMap = new HashMap<String, BudgetConstructionAccountOrganizationHierarchy>(1); 993 994 protected void organizationHierarchyCleanUp() { 995 acctRptsToMap.clear(); 996 orgRptsToMap.clear(); 997 acctOrgHierMap.clear(); 998 } 999 1000 private BudgetConstructionHeader budgetConstructionHeader; 1001 // these are the values at the root of the organization tree 1002 // they report to themselves, and they are at the highest level of every 1003 // organization's reporting chain 1004 private String rootChart; 1005 private String rootOrganization; 1006 1007 private Integer nHeadersBackToZero = 0; 1008 private Integer nHeadersSwitchingLevels = 0; 1009 1010 1011 /* 1012 * rebuild the organization hierarchy based on changes to BC accounting and BC organization tables 1013 */ 1014 1015 public void rebuildOrganizationHierarchy(Integer BaseYear) { 1016 // ******** 1017 // this routine REQUIRES that pending GL is complete 1018 // we only build a hierarchy for accounts that exist in the GL 1019 // ******** 1020 1021 Integer RequestYear = BaseYear + 1; 1022 1023 // 1024 // first we have to clear out what's there for the coming fiscal year 1025 // again, we clear the cache after doing a deleteByQuery 1026 getPersistenceBrokerTemplate().clearCache(); 1027 Criteria criteriaID = new Criteria(); 1028 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 1029 QueryByCriteria killOrgHierQuery = new QueryByCriteria(BudgetConstructionAccountOrganizationHierarchy.class, criteriaID); 1030 killOrgHierQuery.setCriteria(criteriaID); 1031 getPersistenceBrokerTemplate().deleteByQuery(killOrgHierQuery); 1032 getPersistenceBrokerTemplate().clearCache(); 1033 // 1034 // now we fetch the root of the organization tree 1035 String[] rootNode = SpringContext.getBean(OrganizationService.class).getRootOrganizationCode(); 1036 rootChart = rootNode[0]; 1037 rootOrganization = rootNode[1]; 1038 // 1039 // read the entire account reports to table, and build a hash map for the 1040 // join with the PBGL accounts 1041 readAcctReportsTo(); 1042 // read the entire organization reports to table, and build a hash map for 1043 // getting the organization tree 1044 readOrgReportsTo(); 1045 // 1046 // we query the budget construction header and loop through the results 1047 // we build a hierarchy for every account we find 1048 // we reset level of any account which no longer exists in the hierarchy 1049 criteriaID = new Criteria(); 1050 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 1051 acctOrgHierMap = new HashMap<String, BudgetConstructionAccountOrganizationHierarchy>(hashObjectSize(BudgetConstructionAccountOrganizationHierarchy.class, criteriaID) * BCConstants.AVERAGE_REPORTING_TREE_SIZE); 1052 QueryByCriteria queryID = new QueryByCriteria(BudgetConstructionHeader.class, criteriaID); 1053 Iterator Results = getPersistenceBrokerTemplate().getIteratorByQuery(queryID); 1054 while (Results.hasNext()) { 1055 BudgetConstructionHeader extantBCHdr = (BudgetConstructionHeader) Results.next(); 1056 buildAcctOrgHierFromAcctRpts(acctRptsToMap.get(getAcctRptsToKeyFromBCHdr(extantBCHdr)), RequestYear); 1057 updateBudgetConstructionHeaderAsNeeded(extantBCHdr); 1058 } 1059 organizationHierarchyCleanUp(); 1060 } 1061 1062 /* 1063 * verify that all the accounts in the budget construction header table are in the budget construction accounting table as well. 1064 * 1065 * genesis initially inserts all accounts in the chart of accounts table into the budget construction accounting table. 1066 * after genesis, the budget construction accounting and organization tables must be maintained separately, to allow changes for the coming fiscal year to be made without changing the current account/organization structure. 1067 * this means that accounts can come into budget construction via the general ledger or via payroll (CSF) which were created after genesis but for some reason not entered into the budget construction accounting tables. 1068 * there is no good way for a program to decide why this happened and what to do about it. (For instance, it could be that the payroll account is not supposed to exist in the coming year, and the account should not be 1069 * in budget construction. It could also be that some base budget GL was not transferred out of an account which is going away in the new fiscal year. or, it could be that the chart manager missed something and the account 1070 * should be in the budget construction accounting table. A real live person needs to decide what happened and what to do.) so, check for this situation and print a log message if it occurs. 1071 */ 1072 1073 public Map verifyAccountsAreAccessible(Integer requestFiscalYear) 1074 { 1075 HashMap<String,String[]> returnMap = new HashMap<String,String[]>(); 1076 1077 Criteria criteriaId = new Criteria(); 1078 // allow more than one year in BC 1079 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, requestFiscalYear); 1080 // query for the chart, account, and a field unique to the joined BC Account table 1081 String[] selectList = {KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, 1082 KFSPropertyConstants.ACCOUNT_NUMBER, 1083 BCPropertyConstants.BUDGET_CONSTRUCTION_ACCOUNT_REPORTS+"."+KFSPropertyConstants.REPORTS_TO_ORGANIZATION_CODE}; 1084 ReportQueryByCriteria missingBCAccounts = new ReportQueryByCriteria(BudgetConstructionHeader.class,criteriaId); 1085 missingBCAccounts.setAttributes(selectList); 1086 // set up an outer join path to the BC Account table 1087 missingBCAccounts.setPathOuterJoin(BCPropertyConstants.BUDGET_CONSTRUCTION_ACCOUNT_REPORTS); 1088 // 1089 // look for a null value of the organization code to identify accounts not in Budget Construction Accounting 1090 Iterator <Object[]> returnedRows = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(missingBCAccounts); 1091 while (returnedRows.hasNext()) 1092 { 1093 Object[] returnedFields = returnedRows.next(); 1094 if (returnedFields[2] == null) 1095 { 1096 // store the missing row in the map 1097 String chart = (String) returnedFields[0]; 1098 String account = (String) returnedFields[1]; 1099 String[] chartAccount = {chart, account}; 1100 returnMap.put(chart+account,chartAccount); 1101 } 1102 } 1103 1104 return returnMap; 1105 } 1106 1107 // private utility methods 1108 1109 protected void buildAcctOrgHierFromAcctRpts(BudgetConstructionAccountReports acctRpts, Integer RequestYear) { 1110 // it is possible that a header row could have an account which exists in the chart but not in account reports to, the parallel chart for budget construction. these two charts must be maintained in parallel. if that is the case, a verification routine will print a list of problems at the end. here we just skip building the hierarchy. 1111 if (acctRpts == null) 1112 { 1113 return; 1114 } 1115 // part of the key of the budget construction header is a sub account 1116 // so, our algorithm could visit the same account more than once if the account has more than one subaccount. 1117 // if the hierarchy for this account is already built, we skip this routine. 1118 String inKey = getOrgHierarchyKeyFromAcctRpts(acctRpts); 1119 if (acctOrgHierMap.get(inKey) != null) { 1120 return; 1121 } 1122 Integer orgLevel = 1; 1123 // the organization the account directly reports to is at level 1 1124 // (the account starts out at the account fiscal office level--level 0) 1125 BudgetConstructionAccountOrganizationHierarchy acctOrgHier; 1126 acctOrgHier = new BudgetConstructionAccountOrganizationHierarchy(); 1127 acctOrgHier.setUniversityFiscalYear(RequestYear); 1128 acctOrgHier.setChartOfAccountsCode(acctRpts.getChartOfAccountsCode()); 1129 acctOrgHier.setAccountNumber(acctRpts.getAccountNumber()); 1130 acctOrgHier.setOrganizationLevelCode(orgLevel); 1131 acctOrgHier.setVersionNumber(DEFAULT_VERSION_NUMBER); 1132 acctOrgHier.setOrganizationChartOfAccountsCode(acctRpts.getReportsToChartOfAccountsCode()); 1133 acctOrgHier.setOrganizationCode(acctRpts.getReportsToOrganizationCode()); 1134 // save the new row 1135 getPersistenceBrokerTemplate().store(acctOrgHier); 1136 // save the new row in a hash map so we can merge with the budget header 1137 String mapKey = getOrgHierarchyKey(acctOrgHier); 1138 acctOrgHierMap.put(mapKey, acctOrgHier); 1139 // now we have to loop to assign the hierarchy 1140 // (especially before testing, we need to be on the look out for infinite 1141 // loops. assertions are verboten, so we'll just code a high value for 1142 // the level limit, instead of using a potentially infinite while loop. we have no control over the DB, and there could be a circular reporting loop in the DB.) 1143 while (orgLevel < MAXIMUM_ORGANIZATION_TREE_DEPTH) { 1144 // find the current organization in the BC organization reports to table 1145 String orgKey = getOrgRptsToKeyFromAcctOrgHier(acctOrgHier); 1146 if (noNewMapEntryNeeded(orgRptsToMap.get(orgKey))) { 1147 // get out if we have found the root of the reporting tree 1148 break; 1149 } 1150 orgLevel = orgLevel + 1; 1151 BudgetConstructionOrganizationReports orgRpts = orgRptsToMap.get(orgKey); 1152 acctOrgHier = new BudgetConstructionAccountOrganizationHierarchy(); 1153 acctOrgHier.setUniversityFiscalYear(RequestYear); 1154 acctOrgHier.setChartOfAccountsCode(acctRpts.getChartOfAccountsCode()); 1155 acctOrgHier.setAccountNumber(acctRpts.getAccountNumber()); 1156 acctOrgHier.setOrganizationLevelCode(orgLevel); 1157 acctOrgHier.setVersionNumber(DEFAULT_VERSION_NUMBER); 1158 acctOrgHier.setOrganizationChartOfAccountsCode(orgRpts.getReportsToChartOfAccountsCode()); 1159 acctOrgHier.setOrganizationCode(orgRpts.getReportsToOrganizationCode()); 1160 // save the new row 1161 getPersistenceBrokerTemplate().store(acctOrgHier); 1162 // save the new row in a hash map so we can merge with the budget header 1163 mapKey = getOrgHierarchyKey(acctOrgHier); 1164 acctOrgHierMap.put(mapKey, acctOrgHier); 1165 } 1166 if (orgLevel >= MAXIMUM_ORGANIZATION_TREE_DEPTH) { 1167 LOG.warn(String.format("\n%s/%s reports to more than %d organizations", acctRpts.getChartOfAccountsCode(), acctRpts.getAccountNumber(), MAXIMUM_ORGANIZATION_TREE_DEPTH)); 1168 } 1169 } 1170 1171 protected String getAcctRptsToKey(BudgetConstructionAccountReports acctRpts) { 1172 String TestKey = new String(); 1173 TestKey = acctRpts.getChartOfAccountsCode() + acctRpts.getAccountNumber(); 1174 return TestKey; 1175 } 1176 1177 protected String getAcctRptsToKeyFromBCHdr(BudgetConstructionHeader bCHdr) { 1178 String TestKey = new String(); 1179 TestKey = bCHdr.getChartOfAccountsCode() + bCHdr.getAccountNumber(); 1180 return TestKey; 1181 } 1182 1183 protected String getOrgHierarchyKey(BudgetConstructionAccountOrganizationHierarchy orgHier) { 1184 String TestKey = new String(); 1185 TestKey = orgHier.getChartOfAccountsCode() + orgHier.getAccountNumber() + orgHier.getOrganizationChartOfAccountsCode() + orgHier.getOrganizationCode(); 1186 return TestKey; 1187 } 1188 1189 protected String getOrgHierarchyKeyFromAcctRpts(BudgetConstructionAccountReports acctRpts) { 1190 String TestKey = new String(); 1191 TestKey = acctRpts.getChartOfAccountsCode() + acctRpts.getAccountNumber() + acctRpts.getReportsToChartOfAccountsCode() + acctRpts.getReportsToOrganizationCode(); 1192 return TestKey; 1193 } 1194 1195 protected String getOrgHierarchyKeyFromBCHeader(BudgetConstructionHeader bCHdr) { 1196 String TestKey = new String(); 1197 TestKey = bCHdr.getChartOfAccountsCode() + bCHdr.getAccountNumber() + bCHdr.getOrganizationLevelChartOfAccountsCode() + bCHdr.getOrganizationLevelOrganizationCode(); 1198 return TestKey; 1199 } 1200 1201 protected String getOrgRptsToKey(BudgetConstructionOrganizationReports orgRpts) { 1202 String TestKey = new String(); 1203 TestKey = orgRpts.getChartOfAccountsCode() + orgRpts.getOrganizationCode(); 1204 return TestKey; 1205 } 1206 1207 protected String getOrgRptsToKeyFromAcctOrgHier(BudgetConstructionAccountOrganizationHierarchy acctOrgHier) { 1208 String TestKey = new String(); 1209 TestKey = acctOrgHier.getOrganizationChartOfAccountsCode() + acctOrgHier.getOrganizationCode(); 1210 return TestKey; 1211 } 1212 1213 protected boolean noNewMapEntryNeeded(BudgetConstructionOrganizationReports orgRpts) { 1214 // no new entry is needed we are at the root of the organization tree 1215 String thisChart = orgRpts.getChartOfAccountsCode(); 1216 String thisOrg = orgRpts.getOrganizationCode(); 1217 if ((thisChart.compareTo(rootChart) == 0) && (thisOrg.compareTo(rootOrganization) == 0)) { 1218 return true; 1219 } 1220 // no new entry is needed if either the chart or the organization 1221 // which this organization reports to is null 1222 // or if the organization reports to itself 1223 // this check is here in case the chart/org reporting hierarchy is not set up properly in the DB, and some organizations do not 1224 // ultimately report to a single root of the organization tree. 1225 String rptsToChart = orgRpts.getReportsToChartOfAccountsCode(); 1226 if (rptsToChart.length() == 0) { 1227 LOG.warn(String.format("\n(%s, %s) reports to a null chart", thisChart, thisOrg)); 1228 return true; 1229 } 1230 String rptsToOrg = orgRpts.getReportsToOrganizationCode(); 1231 if (rptsToOrg.length() == 0) { 1232 LOG.warn(String.format("\n(%s, %s) reports to a null organization", thisChart, thisOrg)); 1233 return true; 1234 } 1235 if ((thisChart.compareTo(rptsToChart) == 0) && (thisOrg.compareTo(rptsToOrg) == 0)) { 1236 LOG.warn(String.format("\n(%s,%s) reports to itself and is not the root", thisChart, thisOrg)); 1237 return true; 1238 } 1239 return false; 1240 } 1241 1242 protected void readAcctReportsTo() { 1243 // we will use a report query, to bypass the "persistence" bureaucracy and its cache, saving memory and time 1244 // we will use the OJB class as a convenient container object in the hashmap 1245 Integer sqlChartOfAccountsCode = 0; 1246 Integer sqlAccountNumber = 1; 1247 Integer sqlReportsToChartofAccountsCode = 2; 1248 Integer sqlOrganizationCode = 3; 1249 Criteria criteriaID = ReportQueryByCriteria.CRITERIA_SELECT_ALL; 1250 // we always get a new copy of the map 1251 acctRptsToMap = new HashMap<String, BudgetConstructionAccountReports>(hashObjectSize(BudgetConstructionAccountReports.class, criteriaID)); 1252 String[] queryAttr = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ACCOUNT_NUMBER, KFSPropertyConstants.REPORTS_TO_CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.REPORTS_TO_ORGANIZATION_CODE }; 1253 ReportQueryByCriteria queryID = new ReportQueryByCriteria(BudgetConstructionAccountReports.class, queryAttr, criteriaID); 1254 Iterator Results = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 1255 while (Results.hasNext()) { 1256 Object[] ReturnList = (Object[]) Results.next(); 1257 BudgetConstructionAccountReports acctRpts = new BudgetConstructionAccountReports(); 1258 acctRpts.setChartOfAccountsCode((String) ReturnList[sqlChartOfAccountsCode]); 1259 acctRpts.setAccountNumber((String) ReturnList[sqlAccountNumber]); 1260 acctRpts.setReportsToChartOfAccountsCode((String) ReturnList[sqlReportsToChartofAccountsCode]); 1261 acctRpts.setReportsToOrganizationCode((String) ReturnList[sqlOrganizationCode]); 1262 String TestKey = getAcctRptsToKey(acctRpts); 1263 acctRptsToMap.put(TestKey, acctRpts); 1264 } 1265 LOG.info("\nAccount Reports To for Organization Hierarchy:"); 1266 LOG.info(String.format("\nNumber of account-reports-to rows: %d", acctRptsToMap.size())); 1267 } 1268 1269 protected void readOrgReportsTo() { 1270 // we will use a report query, to bypass the "persistence" bureaucracy and its cache, saving memory and time 1271 // we will use the OJB class as a convenient container object in the hashmap 1272 Integer sqlChartOfAccountsCode = 0; 1273 Integer sqlOrganizationCode = 1; 1274 Integer sqlReportsToChartofAccountsCode = 2; 1275 Integer sqlReportsToOrganizationCode = 3; 1276 Criteria criteriaID = ReportQueryByCriteria.CRITERIA_SELECT_ALL; 1277 // build a new map 1278 orgRptsToMap = new HashMap<String, BudgetConstructionOrganizationReports>(hashObjectSize(BudgetConstructionOrganizationReports.class, criteriaID)); 1279 String[] queryAttr = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ORGANIZATION_CODE, KFSPropertyConstants.REPORTS_TO_CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.REPORTS_TO_ORGANIZATION_CODE }; 1280 ReportQueryByCriteria queryID = new ReportQueryByCriteria(BudgetConstructionOrganizationReports.class, queryAttr, criteriaID); 1281 Iterator Results = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 1282 while (Results.hasNext()) { 1283 Object[] ReturnList = (Object[]) Results.next(); 1284 BudgetConstructionOrganizationReports orgRpts = new BudgetConstructionOrganizationReports(); 1285 orgRpts.setChartOfAccountsCode((String) ReturnList[sqlChartOfAccountsCode]); 1286 orgRpts.setOrganizationCode((String) ReturnList[sqlOrganizationCode]); 1287 orgRpts.setReportsToChartOfAccountsCode((String) ReturnList[sqlReportsToChartofAccountsCode]); 1288 orgRpts.setReportsToOrganizationCode((String) ReturnList[sqlReportsToOrganizationCode]); 1289 String TestKey = getOrgRptsToKey(orgRpts); 1290 orgRptsToMap.put(TestKey, orgRpts); 1291 } 1292 LOG.info("\nOrganization Reports To for Organization Hierarchy:"); 1293 LOG.info(String.format("\nNumber of organization-reports-to rows: %d", orgRptsToMap.size())); 1294 } 1295 1296 protected void updateBudgetConstructionHeaderAsNeeded(BudgetConstructionHeader bCHdr) { 1297 // header rows at the lowest (initial) level should be left alone 1298 if (bCHdr.getOrganizationLevelCode().equals(BCConstants.INITIAL_ORGANIZATION_LEVEL_CODE)) { 1299 return; 1300 } 1301 // we will only update if the level of the organization has changed 1302 // or if the organization has disappeared completely 1303 String mapKey = getOrgHierarchyKeyFromBCHeader(bCHdr); 1304 BudgetConstructionAccountOrganizationHierarchy acctOrgHier = acctOrgHierMap.get(mapKey); 1305 if (acctOrgHier == null) { 1306 // the account no longer reports to this organization 1307 // we have to return to the lowest level and the default the 1308 // organization reported to 1309 nHeadersBackToZero = nHeadersBackToZero + 1; 1310 bCHdr.setOrganizationLevelChartOfAccountsCode(BCConstants.INITIAL_ORGANIZATION_LEVEL_CHART_OF_ACCOUNTS_CODE); 1311 bCHdr.setOrganizationLevelOrganizationCode(BCConstants.INITIAL_ORGANIZATION_LEVEL_ORGANIZATION_CODE); 1312 bCHdr.setOrganizationLevelCode(BCConstants.INITIAL_ORGANIZATION_LEVEL_CODE); 1313 getPersistenceBrokerTemplate().store(bCHdr); 1314 return; 1315 } 1316 Integer levelFromHierarchy = acctOrgHier.getOrganizationLevelCode(); 1317 Integer levelFromHeader = bCHdr.getOrganizationLevelCode(); 1318 if (!levelFromHierarchy.equals(levelFromHeader)) { 1319 // the organization reported to has changed its location in the hierarchy 1320 bCHdr.setOrganizationLevelCode(levelFromHierarchy); 1321 getPersistenceBrokerTemplate().store(bCHdr); 1322 nHeadersSwitchingLevels = nHeadersSwitchingLevels + 1; 1323 } 1324 } 1325 1326 1327 /* 1328 * ************************************************************************** 1329 * (6) here are the routines we will use for updating budget construction GL* 1330 * ************************************************************************** 1331 */ 1332 // maps (hash maps) to return the results of the GL call 1333 // --pBGLFromGL contains all the rows returned, stuffed into an object that can be 1334 // saved to the pending budget construction general ledger 1335 // --bCHdrFromGL contains one entry for each potentially new key for the budget 1336 // construction header table. 1337 private HashMap<String, PendingBudgetConstructionGeneralLedger> pBGLFromGL = new HashMap<String, PendingBudgetConstructionGeneralLedger>(1); 1338 private HashMap<String, String> documentNumberFromBCHdr = new HashMap<String, String>(1); 1339 private HashMap<String, Integer> skippedPBGLKeys = new HashMap(); 1340 1341 protected void pBGLCleanUp() { 1342 pBGLFromGL.clear(); 1343 documentNumberFromBCHdr.clear(); 1344 } 1345 1346 // these are the indexes for each of the fields returned in the select list 1347 // of the SQL statement 1348 private Integer sqlChartOfAccountsCode = 0; 1349 private Integer sqlAccountNumber = 1; 1350 private Integer sqlSubAccountNumber = 2; 1351 private Integer sqlObjectCode = 3; 1352 private Integer sqlSubObjectCode = 4; 1353 private Integer sqlBalanceTypeCode = 5; 1354 private Integer sqlObjectTypeCode = 6; 1355 private Integer sqlAccountLineAnnualBalanceAmount = 7; 1356 private Integer sqlBeginningBalanceLineAmount = 8; 1357 1358 private Integer nGLHeadersAdded = new Integer(0); 1359 private Integer nGLRowsAdded = new Integer(0); 1360 private Integer nGLRowsUpdated = new Integer(0); 1361 private Integer nCurrentPBGLRows = new Integer(0); 1362 private Integer nGLBBRowsZeroNet = new Integer(0); 1363 private Integer nGLBBRowsRead = new Integer(0); 1364 private Integer nGLRowsMatchingPBGL = new Integer(0); 1365 private Integer nGLBBKeysRead = new Integer(0); 1366 private Integer nGLBBRowsSkipped = new Integer(0); 1367 1368 // public methods 1369 1370 public void clearHangingBCLocks(Integer BaseYear) { 1371 // this routine cleans out any locks that might remain from people leaving 1372 // the application abnormally (for example, Fire! Fire! Leave your computer and get out now!). it assumes that 1373 // people are shut out of the application during a batch run, and that all 1374 // work prior to the batch run has either been committed, or lost because the connection was broken before a save. 1375 BudgetConstructionHeader lockedDocuments; 1376 // 1377 Integer RequestYear = BaseYear + 1; 1378 Criteria criteriaID = new Criteria(); 1379 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 1380 Criteria lockID = new Criteria(); 1381 Criteria tranLockID = new Criteria(); 1382 if (BCConstants.DEFAULT_BUDGET_HEADER_LOCK_IDS == null) { 1383 // make sure that a NULL test is used in case = NULL is not supported 1384 // by the database 1385 lockID.addNotNull(BCPropertyConstants.BUDGET_LOCK_USER_IDENTIFIER); 1386 tranLockID.addNotNull(BCPropertyConstants.BUDGET_TRANSACTION_LOCK_USER_IDENTIFIER); 1387 } 1388 else { 1389 lockID.addNotEqualTo(BCPropertyConstants.BUDGET_LOCK_USER_IDENTIFIER, BCConstants.DEFAULT_BUDGET_HEADER_LOCK_IDS); 1390 tranLockID.addNotEqualTo(BCPropertyConstants.BUDGET_TRANSACTION_LOCK_USER_IDENTIFIER, BCConstants.DEFAULT_BUDGET_HEADER_LOCK_IDS); 1391 } 1392 ; 1393 lockID.addOrCriteria(tranLockID); 1394 criteriaID.addAndCriteria(lockID); 1395 // 1396 QueryByCriteria queryID = new QueryByCriteria(BudgetConstructionHeader.class, criteriaID); 1397 Iterator Results = getPersistenceBrokerTemplate().getIteratorByQuery(queryID); 1398 // now just loop through and change the locks 1399 while (Results.hasNext()) { 1400 lockedDocuments = (BudgetConstructionHeader) Results.next(); 1401 lockedDocuments.setBudgetLockUserIdentifier(BCConstants.DEFAULT_BUDGET_HEADER_LOCK_IDS); 1402 lockedDocuments.setBudgetTransactionLockUserIdentifier(BCConstants.DEFAULT_BUDGET_HEADER_LOCK_IDS); 1403 getPersistenceBrokerTemplate().store(lockedDocuments); 1404 } 1405 // we need to clear the position and funding locks as well 1406 clearHangingPositionLocks(RequestYear); 1407 QueryByCriteria queryId = new QueryByCriteria(BudgetConstructionFundingLock.class, QueryByCriteria.CRITERIA_SELECT_ALL); 1408 getPersistenceBrokerTemplate().deleteByQuery(queryId); 1409 getPersistenceBrokerTemplate().clearCache(); 1410 } 1411 1412 public void initialLoadToPBGL(Integer BaseYear) { 1413 readBCHeaderForDocNumber(BaseYear); 1414 // exclude rows with a new GL balance of 0 from the initial pending GL 1415 readGLForPBGL(BaseYear,true); 1416 addNewGLRowsToPBGL(BaseYear); 1417 writeFinalDiagnosticCounts(); 1418 pBGLCleanUp(); 1419 } 1420 1421 public void updateToPBGL(Integer BaseYear) { 1422 readBCHeaderForDocNumber(BaseYear); 1423 // allow rows with a net GL balance of 0 to update the pending GL 1424 readGLForPBGL(BaseYear,false); 1425 updateCurrentPBGL(BaseYear); 1426 addNewGLRowsToPBGL(BaseYear); 1427 writeFinalDiagnosticCounts(); 1428 pBGLCleanUp(); 1429 } 1430 1431 // 1432 // clear any hanging position locks 1433 protected void clearHangingPositionLocks(Integer RequestYear) 1434 { 1435 BudgetConstructionPosition lockedPositions; 1436 Criteria criteriaID = new Criteria(); 1437 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 1438 Criteria lockID = new Criteria(); 1439 if (BCConstants.DEFAULT_BUDGET_HEADER_LOCK_IDS == null) { 1440 // make sure that a NULL test is used in case = NULL is not supported 1441 // by the database 1442 lockID.addNotNull(BCPropertyConstants.POSITION_LOCK_USER_IDENTIFIER); 1443 } 1444 else { 1445 lockID.addNotEqualTo(BCPropertyConstants.POSITION_LOCK_USER_IDENTIFIER, BCConstants.DEFAULT_BUDGET_HEADER_LOCK_IDS); 1446 } 1447 ; 1448 criteriaID.addAndCriteria(lockID); 1449 // 1450 QueryByCriteria queryID = new QueryByCriteria(BudgetConstructionPosition.class, criteriaID); 1451 Iterator Results = getPersistenceBrokerTemplate().getIteratorByQuery(queryID); 1452 // now just loop through and change the locks 1453 while (Results.hasNext()) { 1454 lockedPositions = (BudgetConstructionPosition) Results.next(); 1455 lockedPositions.setPositionLockUserIdentifier(BCConstants.DEFAULT_BUDGET_HEADER_LOCK_IDS); 1456 getPersistenceBrokerTemplate().store(lockedPositions); 1457 } 1458 } 1459 1460 // 1461 // two test routines to display the field values in the two business objects 1462 // produced from the GL read. these are primarily here for initial testing 1463 protected void info() { 1464 if (!LOG.isEnabledFor(Level.INFO)) { 1465 return; 1466 } 1467 ; 1468 // print one header row 1469 for (Map.Entry<String, String> bcHeaderRows : documentNumberFromBCHdr.entrySet()) { 1470 String toPrint = bcHeaderRows.getValue(); 1471 LOG.info(String.format("\n\nA sample document number %s\n", toPrint)); 1472 break; 1473 } 1474 // print one PBGL row 1475 for (Map.Entry<String, PendingBudgetConstructionGeneralLedger> pBGLRows : pBGLFromGL.entrySet()) { 1476 PendingBudgetConstructionGeneralLedger toPrint = pBGLRows.getValue(); 1477 LOG.info("\n\nA sample PBGL row\n"); 1478 LOG.info(String.format("\nDocument Number = %s", toPrint.getDocumentNumber())); 1479 LOG.info(String.format("\nUniversity Fiscal Year = %d", toPrint.getUniversityFiscalYear())); 1480 LOG.info(String.format("\nChart: %s", toPrint.getChartOfAccountsCode())); 1481 LOG.info(String.format("\nAccount: %s", toPrint.getAccountNumber())); 1482 LOG.info(String.format("\nSub Account: %s", toPrint.getSubAccountNumber())); 1483 LOG.info(String.format("\nObject Code: %s", toPrint.getFinancialObjectCode())); 1484 LOG.info(String.format("\nSubobject Code: %s", toPrint.getFinancialSubObjectCode())); 1485 LOG.info(String.format("\nBalance Type: %s", toPrint.getFinancialBalanceTypeCode())); 1486 LOG.info(String.format("\nObject Type: %s", toPrint.getFinancialObjectTypeCode())); 1487 LOG.info(String.format("\nBase Amount: %s", toPrint.getFinancialBeginningBalanceLineAmount().toString())); 1488 LOG.info(String.format("\nRequest Amount: %s", toPrint.getAccountLineAnnualBalanceAmount().toString())); 1489 LOG.info(String.format("\nVersion Number: %d", toPrint.getVersionNumber())); 1490 break; 1491 } 1492 } 1493 1494 protected void debug() { 1495 if (!LOG.isEnabledFor(Level.DEBUG)) { 1496 return; 1497 } 1498 ; 1499 // print one header row 1500 for (Map.Entry<String, String> bcHeaderRows : documentNumberFromBCHdr.entrySet()) { 1501 String toPrint = bcHeaderRows.getValue(); 1502 LOG.debug(String.format("\n\nA sample document number %s\n", toPrint)); 1503 break; 1504 } 1505 // print one PBGL row 1506 for (Map.Entry<String, PendingBudgetConstructionGeneralLedger> pBGLRows : pBGLFromGL.entrySet()) { 1507 PendingBudgetConstructionGeneralLedger toPrint = pBGLRows.getValue(); 1508 LOG.debug("\n\nA sample PBGL row\n"); 1509 LOG.debug(String.format("\nDocument Number = %s", toPrint.getDocumentNumber())); 1510 LOG.debug(String.format("\nUniversity Fiscal Year = %d", toPrint.getUniversityFiscalYear())); 1511 LOG.debug(String.format("\nChart: %s", toPrint.getChartOfAccountsCode())); 1512 LOG.debug(String.format("\nAccount: %s", toPrint.getAccountNumber())); 1513 LOG.debug(String.format("\nSub Account: %s", toPrint.getSubAccountNumber())); 1514 LOG.debug(String.format("\nObject Code: %s", toPrint.getFinancialObjectCode())); 1515 LOG.debug(String.format("\nSubobject Code: %s", toPrint.getFinancialSubObjectCode())); 1516 LOG.debug(String.format("\nBalance Type: %s", toPrint.getFinancialBalanceTypeCode())); 1517 LOG.debug(String.format("\nObject Type: %s", toPrint.getFinancialObjectTypeCode())); 1518 LOG.debug(String.format("\nBase Amount: %s", toPrint.getFinancialBeginningBalanceLineAmount().toString())); 1519 LOG.debug(String.format("\nRequest Amount: %s", toPrint.getAccountLineAnnualBalanceAmount().toString())); 1520 LOG.debug(String.format("\nVersion Number: %d", toPrint.getVersionNumber())); 1521 break; 1522 } 1523 } 1524 1525 // 1526 // 1527 // private working methods 1528 1529 // 1530 protected void addNewGLRowsToPBGL(Integer BaseYear) { 1531 // this method adds the GL rows not yet in PBGL to PBGL 1532 for (Map.Entry<String, PendingBudgetConstructionGeneralLedger> newPBGLRows : pBGLFromGL.entrySet()) { 1533 PendingBudgetConstructionGeneralLedger rowToAdd = newPBGLRows.getValue(); 1534 // no rows with zero base are added to the budget construction pending general ledger 1535 if (rowToAdd.getFinancialBeginningBalanceLineAmount().isZero()) 1536 { 1537 nGLBBRowsZeroNet = nGLBBRowsZeroNet + 1; 1538 } 1539 else 1540 { 1541 nGLRowsAdded = nGLRowsAdded + 1; 1542 getPersistenceBrokerTemplate().store(rowToAdd); 1543 } 1544 } 1545 } 1546 1547 // 1548 // these two methods build the GL field string that triggers creation of a new 1549 // pending budget construction general ledger row 1550 protected String buildGLTestKeyFromPBGL(PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger) { 1551 String PBGLTestKey = new String(); 1552 PBGLTestKey = pendingBudgetConstructionGeneralLedger.getChartOfAccountsCode() + pendingBudgetConstructionGeneralLedger.getAccountNumber() + pendingBudgetConstructionGeneralLedger.getSubAccountNumber() + pendingBudgetConstructionGeneralLedger.getFinancialObjectCode() + pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode() + pendingBudgetConstructionGeneralLedger.getFinancialBalanceTypeCode() + pendingBudgetConstructionGeneralLedger.getFinancialObjectTypeCode(); 1553 return PBGLTestKey; 1554 } 1555 1556 protected String buildGLTestKeyFromSQLResults(Object[] sqlResult) { 1557 String GLTestKey = new String(); 1558 GLTestKey = (String) sqlResult[sqlChartOfAccountsCode] + (String) sqlResult[sqlAccountNumber] + (String) sqlResult[sqlSubAccountNumber] + (String) sqlResult[sqlObjectCode] + (String) sqlResult[sqlSubObjectCode] + (String) sqlResult[sqlBalanceTypeCode] + (String) sqlResult[sqlObjectTypeCode]; 1559 return GLTestKey; 1560 } 1561 1562 // 1563 // these two methods build the GL field string that triggers creation of a new 1564 // budget construction header 1565 public String buildHeaderTestKeyFromPBGL(PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger) { 1566 String headerBCTestKey = new String(); 1567 headerBCTestKey = pendingBudgetConstructionGeneralLedger.getChartOfAccountsCode() + pendingBudgetConstructionGeneralLedger.getAccountNumber() + pendingBudgetConstructionGeneralLedger.getSubAccountNumber(); 1568 return headerBCTestKey; 1569 } 1570 1571 protected String buildHeaderTestKeyFromSQLResults(Object[] sqlResult) { 1572 String headerBCTestKey = new String(); 1573 headerBCTestKey = (String) sqlResult[sqlChartOfAccountsCode] + (String) sqlResult[sqlAccountNumber] + (String) sqlResult[sqlSubAccountNumber]; 1574 return headerBCTestKey; 1575 } 1576 1577 protected PendingBudgetConstructionGeneralLedger newPBGLBusinessObject(Integer RequestYear, Object[] sqlResult) { 1578 PendingBudgetConstructionGeneralLedger PBGLObj = new PendingBudgetConstructionGeneralLedger(); 1579 /* 1580 * the document number will be set later if we have to store this in a new document 1581 * a new row in an existing document will take it's document number from the existing document 1582 * otherwise (existing document, existing row), the only field in this that will be different from 1583 * the existing row is the beginning balance amount 1584 */ 1585 PBGLObj.setUniversityFiscalYear(RequestYear); 1586 PBGLObj.setChartOfAccountsCode((String) sqlResult[sqlChartOfAccountsCode]); 1587 PBGLObj.setAccountNumber((String) sqlResult[sqlAccountNumber]); 1588 PBGLObj.setSubAccountNumber((String) sqlResult[sqlSubAccountNumber]); 1589 PBGLObj.setFinancialObjectCode((String) sqlResult[sqlObjectCode]); 1590 PBGLObj.setFinancialSubObjectCode((String) sqlResult[sqlSubObjectCode]); 1591 PBGLObj.setFinancialBalanceTypeCode((String) sqlResult[sqlBalanceTypeCode]); 1592 PBGLObj.setFinancialObjectTypeCode((String) sqlResult[sqlObjectTypeCode]); 1593 KualiDecimal BaseAmount = (KualiDecimal) sqlResult[sqlBeginningBalanceLineAmount]; 1594 BaseAmount = BaseAmount.add((KualiDecimal) sqlResult[sqlAccountLineAnnualBalanceAmount]); 1595 KualiInteger DollarBaseAmount = new KualiInteger(BaseAmount.bigDecimalValue()); 1596 PBGLObj.setFinancialBeginningBalanceLineAmount(DollarBaseAmount); 1597 PBGLObj.setAccountLineAnnualBalanceAmount(KualiInteger.ZERO); 1598 // ObjectID is set in the BusinessObjectBase on insert and update 1599 // but, we must set the version number 1600 PBGLObj.setVersionNumber(DEFAULT_VERSION_NUMBER); 1601 return PBGLObj; 1602 } 1603 1604 protected void readBCHeaderForDocNumber(Integer BaseYear) { 1605 // we have to read all the budget construction header objects so that 1606 // we can use them to assign document numbers 1607 // 1608 Integer RequestYear = BaseYear + 1; 1609 // 1610 Long documentsRead = new Long(0); 1611 Criteria criteriaId = new Criteria(); 1612 criteriaId.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 1613 documentNumberFromBCHdr = new HashMap<String, String>(hashObjectSize(BudgetConstructionHeader.class, criteriaId)); 1614 String[] queryAttr = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ACCOUNT_NUMBER, KFSPropertyConstants.SUB_ACCOUNT_NUMBER, KFSPropertyConstants.DOCUMENT_NUMBER }; 1615 ReportQueryByCriteria queryId = new ReportQueryByCriteria(BudgetConstructionHeader.class, queryAttr, criteriaId); 1616 Iterator Results = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryId); 1617 while (Results.hasNext()) { 1618 Object[] rowReturned = (Object[]) Results.next(); 1619 String hashKey = ((String) rowReturned[0]) + ((String) rowReturned[1]) + ((String) rowReturned[2]); 1620 documentNumberFromBCHdr.put(hashKey, ((String) rowReturned[3])); 1621 documentsRead = documentsRead + 1; 1622 } 1623 LOG.info(String.format("\nBC Headers read = %d", documentsRead)); 1624 } 1625 1626 protected void readGLForPBGL(Integer BaseYear, boolean excludeZeroNetAmounts) { 1627 Integer RequestYear = BaseYear + 1; 1628 // 1629 // set up a report query to fetch all the GL rows we are going to need 1630 Criteria criteriaID = new Criteria(); 1631 // we only pick up a single balance type 1632 // we also use an integer fiscal year 1633 // *** this is a point of change if either of these criteria change *** 1634 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 1635 criteriaID.addEqualTo(KFSPropertyConstants.BALANCE_TYPE_CODE, KFSConstants.BALANCE_TYPE_BASE_BUDGET); 1636 // we'll estimate the size of the PBGL map from the number of 1637 // base budget rows in the GL. this should be close 1638 pBGLFromGL = new HashMap<String, PendingBudgetConstructionGeneralLedger>(hashObjectSize(Balance.class, criteriaID)); 1639 String[] queryAttr = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ACCOUNT_NUMBER, KFSPropertyConstants.SUB_ACCOUNT_NUMBER, KFSPropertyConstants.OBJECT_CODE, KFSPropertyConstants.SUB_OBJECT_CODE, KFSPropertyConstants.BALANCE_TYPE_CODE, KFSPropertyConstants.OBJECT_TYPE_CODE, KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT, KFSPropertyConstants.BEGINNING_BALANCE_LINE_AMOUNT }; 1640 ReportQueryByCriteria queryID = new ReportQueryByCriteria(Balance.class, queryAttr, criteriaID, true); 1641 // 1642 // set up the hashmaps by iterating through the results 1643 1644 LOG.info("\nGL Query started: " + String.format("%tT", dateTimeService.getCurrentDate())); 1645 Iterator Results = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 1646 LOG.info("\nGL Query finished: " + String.format("%tT", dateTimeService.getCurrentDate())); 1647 while (Results.hasNext()) { 1648 Object[] ReturnList = (Object[]) Results.next(); 1649 LOG.debug(String.format("\nfields returned = %d\n", ReturnList.length)); 1650 LOG.debug(String.format("\nvalue in last field = %s\n", ReturnList[sqlBeginningBalanceLineAmount].toString())); 1651 // 1652 // we never load a new pending budget construction row from a general ledger row which has a net zero balance 1653 // but we allow for the fact that the general ledger counterpart of a pending row already loaded could be netted to zero by a budget transfer. in this case, we need to update the pending amount. 1654 KualiDecimal BaseAmount = (KualiDecimal) ReturnList[sqlBeginningBalanceLineAmount]; 1655 BaseAmount = BaseAmount.add((KualiDecimal) ReturnList[sqlAccountLineAnnualBalanceAmount]); 1656 // even when we are updating, it could be that the entire key is not in the pending general ledger because the amounts in all GL rows for that key have always netted to zero. 1657 // in this case, there will be no document number, and we should skip the row. 1658 String HeaderTestKey = buildHeaderTestKeyFromSQLResults(ReturnList); 1659 String documentNumberForKey = documentNumberFromBCHdr.get(HeaderTestKey); 1660 if (documentNumberForKey == null) 1661 { 1662 if (BaseAmount.isZero()) 1663 { 1664 // keys in which all rows have base amounts of zero need not have a key 1665 nGLBBRowsRead = nGLBBRowsRead + 1; 1666 nGLBBRowsZeroNet = nGLBBRowsZeroNet + 1; 1667 } 1668 else 1669 { 1670 // the amounts are *not* zero--the row should *not* be skipped, and we need to log an error 1671 recordSkippedKeys(HeaderTestKey); 1672 } 1673 continue; 1674 } 1675 if ((excludeZeroNetAmounts) && BaseAmount.isZero()) 1676 { 1677 // exclude any rows where the amounts add to 0 1678 // (we don't do it in the WHERE clause to be certain we are ANSI standard) 1679 nGLBBRowsRead = nGLBBRowsRead + 1; 1680 nGLBBRowsZeroNet = nGLBBRowsZeroNet + 1; 1681 continue; 1682 } 1683 // 1684 // we always need to build a new PGBL object 1685 // we have selected the entire key from GL_BALANCE_T 1686 String GLTestKey = buildGLTestKeyFromSQLResults(ReturnList); 1687 pBGLFromGL.put(GLTestKey, newPBGLBusinessObject(RequestYear, ReturnList)); 1688 // we need to add a document number to the PBGL object 1689 pBGLFromGL.get(GLTestKey).setDocumentNumber(documentNumberForKey); 1690 } 1691 LOG.info("\nHash maps built: " + String.format("%tT", dateTimeService.getCurrentDate())); 1692 info(); 1693 nGLBBKeysRead = documentNumberFromBCHdr.size(); 1694 nGLBBRowsRead = pBGLFromGL.size() + nGLBBRowsRead; 1695 } 1696 1697 protected void recordSkippedKeys(String badGLKey) { 1698 nGLBBRowsSkipped = nGLBBRowsSkipped + 1; 1699 if (skippedPBGLKeys.get(badGLKey) == null) { 1700 skippedPBGLKeys.put(badGLKey, new Integer(1)); 1701 } 1702 else { 1703 Integer rowCount = skippedPBGLKeys.get(badGLKey) + 1; 1704 skippedPBGLKeys.put(badGLKey, rowCount); 1705 } 1706 } 1707 1708 protected void updateBaseBudgetAmount(PendingBudgetConstructionGeneralLedger currentPBGLInstance) { 1709 String TestKey = buildGLTestKeyFromPBGL(currentPBGLInstance); 1710 if (!pBGLFromGL.containsKey(TestKey)) { 1711 return; 1712 } 1713 PendingBudgetConstructionGeneralLedger matchFromGL = pBGLFromGL.get(TestKey); 1714 KualiInteger baseFromCurrentGL = matchFromGL.getFinancialBeginningBalanceLineAmount(); 1715 KualiInteger baseFromPBGL = currentPBGLInstance.getFinancialBeginningBalanceLineAmount(); 1716 // remove the candidate GL from the hash list 1717 // it won't match with anything else 1718 // it should NOT be inserted into the PBGL table 1719 pBGLFromGL.remove(TestKey); 1720 if (baseFromCurrentGL.equals(baseFromPBGL)) { 1721 // no need to update--false alarm 1722 nGLRowsMatchingPBGL = nGLRowsMatchingPBGL+1; 1723 return; 1724 } 1725 // update the base amount and store the updated PBGL row 1726 nGLRowsUpdated = nGLRowsUpdated + 1; 1727 currentPBGLInstance.setFinancialBeginningBalanceLineAmount(baseFromCurrentGL); 1728 getPersistenceBrokerTemplate().store(currentPBGLInstance); 1729 } 1730 1731 protected void updateCurrentPBGL(Integer BaseYear) { 1732 Integer RequestYear = BaseYear + 1; 1733 1734 1735 // what we are going to do here is what Oracle calls a hash join 1736 // 1737 // we will merge the current PBGL rows with the GL detail, and 1738 // replace the amount on each current PBGL row which matches from 1739 // the GL row, and remove the GL row 1740 // 1741 // we will compare the GL Key row with the the current PBGL row, 1742 // and if the keys are the same, we will eliminate the GL key row 1743 // 1744 // fetch the current PBGL rows 1745 Criteria criteriaID = new Criteria(); 1746 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 1747 QueryByCriteria queryID = new QueryByCriteria(PendingBudgetConstructionGeneralLedger.class, criteriaID); 1748 Iterator Results = getPersistenceBrokerTemplate().getIteratorByQuery(queryID); 1749 // loop through the results 1750 while (Results.hasNext()) { 1751 nCurrentPBGLRows = nCurrentPBGLRows + 1; 1752 PendingBudgetConstructionGeneralLedger currentPBGLInstance = (PendingBudgetConstructionGeneralLedger) Results.next(); 1753 // update the base amount and store the result if necessary 1754 updateBaseBudgetAmount(currentPBGLInstance); 1755 } 1756 } 1757 1758 protected void writeFinalDiagnosticCounts() { 1759 LOG.info(String.format("\n\nGeneral Ledger Run Statistics\n\n")); 1760 LOG.info(String.format("\nGeneral Ledger BB Keys read: %d", nGLBBKeysRead)); 1761 LOG.info(String.format("\nGeneral Ledger BB Rows read: %d", nGLBBRowsRead)); 1762 LOG.info(String.format("\nExisting Pending General Ledger rows: %d", nCurrentPBGLRows)); 1763 LOG.info(String.format("\nof these...")); 1764 LOG.info(String.format("\nnew PBGL rows written: %d", nGLRowsAdded)); 1765 LOG.info(String.format("\ncurrent PBGL amounts updated: %d", nGLRowsUpdated)); 1766 LOG.info(String.format("\ncurrent PBGL rows already matching a GL row: %d",nGLRowsMatchingPBGL)); 1767 LOG.info(String.format("\nGL rows with zero net amounts (skipped) %d\n", nGLBBRowsZeroNet)); 1768 LOG.info(String.format("\nGL account/subaccount keys skipped: %d", nGLBBRowsSkipped)); 1769 if (!skippedPBGLKeys.isEmpty()) { 1770 for (Map.Entry<String, Integer> skippedRows : skippedPBGLKeys.entrySet()) { 1771 LOG.info(String.format("\nGL key %s with %d rows skipped--no document header", skippedRows.getKey(), skippedRows.getValue())); 1772 1773 } 1774 } 1775 LOG.info(String.format("\n\nend of General Ledger run statics")); 1776 } 1777 1778 /* 1779 * ****************************************************************************** 1780 * (7) there could be an object class in the object code table that was marked 1781 * as inactive during the current fiscal year. there could also be GL rows 1782 * with base budget which refer to this object code. the fiscal year makers 1783 * routine would NOT copy a deleted object code into the new fiscal year. 1784 * to maintain referential integrity, we will copy such an object code (but 1785 * mark it as deleted) into the new fiscal year if it will occur in budget 1786 * construction. 1787 */ 1788 1789 private HashMap<String, String[]> baseYearInactiveObjects = new HashMap<String, String[]>(1); 1790 private HashMap<String, String[]> gLBBObjects = new HashMap<String, String[]>(1); 1791 private Integer nInactiveBBObjectCodes = new Integer(0); 1792 1793 protected void objectClassRICleanUp() { 1794 baseYearInactiveObjects.clear(); 1795 gLBBObjects.clear(); 1796 } 1797 1798 public void ensureObjectClassRIForBudget(Integer BaseYear) { 1799 readBaseYearInactiveObjects(BaseYear); 1800 if (baseYearInactiveObjects.isEmpty()) { 1801 // no problems 1802 LOG.info(String.format("\nInactive Object Codes in BC GL: %d", nInactiveBBObjectCodes)); 1803 return; 1804 } 1805 readAndFilterGLBBObjects(BaseYear); 1806 if (gLBBObjects.isEmpty()) { 1807 // no problems 1808 LOG.info(String.format("\nInactive Object Codes in BC GL: %d", nInactiveBBObjectCodes)); 1809 return; 1810 } 1811 // we have to create an object row for the request year 1812 addRIObjectClassesForBB(BaseYear); 1813 LOG.info(String.format("\nInactive Object Codes in BC GL: %d", nInactiveBBObjectCodes)); 1814 objectClassRICleanUp(); 1815 } 1816 1817 protected void addRIObjectClassesForBB(Integer BaseYear) { 1818 // we will read the object table for the request year first 1819 // if the row is there (someone could have added it, or updated it), 1820 // we will not change it at all. 1821 // this is an extra read, but overall looking just for problems 1822 // will require many fewer reads than comparing all object codes in the 1823 // request year to all object codes in the GL BB base. 1824 Integer RequestYear = BaseYear + 1; 1825 for (Map.Entry<String, String[]> problemObjectCodes : gLBBObjects.entrySet()) { 1826 String problemChart = problemObjectCodes.getValue()[0]; 1827 String problemObject = problemObjectCodes.getValue()[1]; 1828 if (isObjectInRequestYear(BaseYear, problemChart, problemObject)) { 1829 // everything is fine 1830 continue; 1831 } 1832 // now we have to add the object to the request year as an inactive object 1833 Criteria criteriaID = new Criteria(); 1834 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 1835 criteriaID.addColumnEqualTo(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, problemChart); 1836 criteriaID.addEqualTo(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, problemObject); 1837 ReportQueryByCriteria queryID = new ReportQueryByCriteria(ObjectCode.class, criteriaID); 1838 Iterator Results = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 1839 if (!Results.hasNext()) { 1840 // this should never happen 1841 // if it does, it will cause an RI exception in the GL load to BC 1842 // at least this message will give some clue 1843 LOG.warn(String.format("could not find BB object (%s, %s) in %d", problemChart, problemObject, BaseYear)); 1844 continue; 1845 } 1846 ObjectCode baseYearObject = (ObjectCode) TransactionalServiceUtils.retrieveFirstAndExhaustIterator(Results); 1847 baseYearObject.setUniversityFiscalYear(RequestYear); 1848 baseYearObject.setActive(false); 1849 getPersistenceBrokerTemplate().store(baseYearObject); 1850 } 1851 } 1852 1853 protected boolean isObjectInRequestYear(Integer BaseYear, String Chart, String ObjectCode) { 1854 Integer RequestYear = BaseYear + 1; 1855 Criteria criteriaID = new Criteria(); 1856 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 1857 criteriaID.addEqualTo(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, Chart); 1858 criteriaID.addEqualTo(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, ObjectCode); 1859 QueryByCriteria queryID = new QueryByCriteria(ObjectCode.class, criteriaID); 1860 Integer result = getPersistenceBrokerTemplate().getCount(queryID); 1861 return (!result.equals(0)); 1862 } 1863 1864 protected void readBaseYearInactiveObjects(Integer BaseYear) { 1865 Criteria criteriaID = new Criteria(); 1866 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 1867 criteriaID.addEqualTo(KFSPropertyConstants.ACTIVE, false); 1868 baseYearInactiveObjects = new HashMap<String, String[]>(hashObjectSize(ObjectCode.class, criteriaID)); 1869 String[] queryAttr = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.FINANCIAL_OBJECT_CODE }; 1870 ReportQueryByCriteria queryID = new ReportQueryByCriteria(ObjectCode.class, queryAttr, criteriaID); 1871 Iterator result = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 1872 while (result.hasNext()) { 1873 Object[] resultRow = (Object[]) result.next(); 1874 String[] hashMapValue = new String[2]; 1875 hashMapValue[0] = (String) resultRow[0]; 1876 hashMapValue[1] = (String) resultRow[1]; 1877 String hashMapKey = hashMapValue[0] + hashMapValue[1]; 1878 baseYearInactiveObjects.put(hashMapKey, hashMapValue); 1879 } 1880 } 1881 1882 protected void readAndFilterGLBBObjects(Integer BaseYear) { 1883 // this must be done before we read GL for PBGL 1884 // otherwise, we will get an RI violation when we try to add a PBGL 1885 // row with an object inactive in the current year 1886 Criteria criteriaID = new Criteria(); 1887 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 1888 criteriaID.addEqualTo(KFSPropertyConstants.BALANCE_TYPE_CODE, KFSConstants.BALANCE_TYPE_BASE_BUDGET); 1889 gLBBObjects = new HashMap<String, String[]>(hashObjectSize(Balance.class, criteriaID)); 1890 String[] queryAttr = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.OBJECT_CODE }; 1891 ReportQueryByCriteria queryID = new ReportQueryByCriteria(Balance.class, queryAttr, criteriaID, true); 1892 Iterator result = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 1893 while (result.hasNext()) { 1894 Object[] resultRow = (Object[]) result.next(); 1895 String[] hashMapValue = new String[2]; 1896 hashMapValue[0] = (String) resultRow[0]; 1897 hashMapValue[1] = (String) resultRow[1]; 1898 String hashMapKey = hashMapValue[0] + hashMapValue[1]; 1899 if (baseYearInactiveObjects.get(hashMapKey) != null) { 1900 gLBBObjects.put(hashMapKey, hashMapValue); 1901 nInactiveBBObjectCodes = nInactiveBBObjectCodes + 1; 1902 } 1903 } 1904 } 1905 1906 /**************************************************************************************** 1907 * (9) this code builds the budget construction CSF tracker and the budget construction 1908 * appointment funding 1909 **************************************************************************************** 1910 */ 1911 1912 // the set of new BCSF objects to be written 1913 private HashMap<String, BudgetConstructionCalculatedSalaryFoundationTracker> bCSF = new HashMap<String, BudgetConstructionCalculatedSalaryFoundationTracker>(1); 1914 // hashmap to hold the document numbers for each accounting key in the header 1915 private HashMap<String, String> bcHdrDocNumbers = new HashMap<String, String>(1); 1916 // hashset to hold the accounting string for each pending GL entry 1917 private HashSet<String> currentPBGLKeys = new HashSet<String>(1); 1918 // hashMap for finding the object type of "detailed position" object codes 1919 private HashMap<String, String> detailedPositionObjectTypes = new HashMap<String, String>(1); 1920 // keys for deleted or vacant rows present in the override CSF: none of these keys 1921 // will load to BCSF from either the override or actual CSF (even if they 1922 // are active in the actual CSF) 1923 private HashSet<String> csfOverrideKeys = new HashSet<String>(1);; 1924 // EMPLID's in CSF which have more than one active row 1925 // we budget in whole dollars, while payroll deals in pennies 1926 // we will use this for our complicated rounding algorithm, to keep the total budget base salary within a dollar of the payroll salary 1927 private HashMap<String, roundMechanism> keysNeedingRounding = new HashMap<String, roundMechanism>(1); 1928 // we need the position normal work months to write a new appointment funding row: the normal work months is the "months appointment" 1929 private HashMap<String, Integer> positionNormalWorkMonths = new HashMap<String, Integer>(1); 1930 1931 protected void buildAppointmentFundingCleanUp() { 1932 bCSF.clear(); 1933 bcHdrDocNumbers.clear(); 1934 currentPBGLKeys.clear(); 1935 detailedPositionObjectTypes.clear(); 1936 csfOverrideKeys.clear(); 1937 keysNeedingRounding.clear(); 1938 positionNormalWorkMonths.clear(); 1939 } 1940 1941 // 1942 // counters 1943 // 1944 Integer CSFRowsRead = new Integer(0); 1945 Integer CSFRowsVacant = new Integer(0); 1946 Integer CSFVacantsConsolidated = new Integer(0); 1947 Integer CSFOverrideDeletesRead = new Integer(0); 1948 Integer CSFOverrideRead = new Integer(0); 1949 Integer CSFOverrideVacant = new Integer(0); 1950 Integer CSFForBCSF = new Integer(0); 1951 Integer CSFCurrentGLRows = new Integer(0); 1952 Integer CSFCurrentBCAFRows = new Integer(0); 1953 Integer CSFBCSFRowsMatchingGL = new Integer(0); 1954 Integer CSFBCSFRowsMatchingBCAF = new Integer(0); 1955 Integer CSFNewGLRows = new Integer(0); 1956 Integer CSFNewBCAFRows = new Integer(0); 1957 Integer CSFBCAFRowsMarkedDeleted = new Integer(0); 1958 Integer CSFBCAFRowsMissing = new Integer(0); 1959 Integer CSFBadObjectsSkipped = new Integer(0); 1960 1961 public void buildAppointmentFundingAndBCSF(Integer BaseYear) { 1962 /********************************************************************* 1963 * RI requirements: 1964 * this method assumes that ALL budget construction positions for the 1965 * request year are in the position table, 1966 * and 1967 * that budget construction documents for every accounting key in the 1968 * CSF tables have been created 1969 **********************************************************************/ 1970 // budget construction CSF tracker is always rebuilt from scratch from the CSF Tracker 1971 // (it doesn't make sense to carry a base salary line that has 1972 // has disappeared from the CSF Tracker--hence from the payroll system, hence the current base--from 1973 // one run of the batch update process to the next.) 1974 clearBCCSF(BaseYear); 1975 clearBCCSF(BaseYear + 1); 1976 // build the new BC CSF objects in memory 1977 setUpCSFOverrideKeys(BaseYear); 1978 setUpBCSFMap(BaseYear); 1979 setUpKeysNeedingRounding(BaseYear); 1980 readCSFOverride(BaseYear); 1981 readCSF(BaseYear); 1982 CSFForBCSF = bCSF.size(); 1983 adjustCSFRounding(); 1984 // store bCSF rows matching current appointment funding 1985 readExistingAppointmentFunding(BaseYear); 1986 // if all of the bCSF rows have been stored (they all already exist in PBGL, so we don't have to worry about PBLG), we can quit here 1987 if (bCSF.size() == 0) { 1988 CSFDiagnostics(); 1989 buildAppointmentFundingCleanUp(); 1990 return; 1991 } 1992 // what we have left are the bCSF rows that do NOT match appointment funding 1993 // -- createNewBCDocumentsFromGLCSF is called in both the genesis and update 1994 // steps, before anything else is done. therefore, we are assured that 1995 // all the documents exist. 1996 // -- we need to create new GL (if the accounting key is not yet in GL). 1997 // this requires that we have a document number, so we need to read those. 1998 // this happens regardless of whether GL updates are allowed, because we 1999 // will only be adding GL rows with 0 amounts when an account not yet in budget construction comes in from payroll. 2000 // there is only base in CSF, no request, and obviously no base budget in the GL exists for an account that isn't in GL. 2001 // 2002 // >> RI requires that data is stored in the order indicated. 2003 // -- we will also have to create new appointment funding rows (again, with 2004 // no request. 2005 // -- finally, we will have to store the bCSF rows themselves. 2006 setUpbcHdrDocNumbers(BaseYear); 2007 setUpCurrentPBGLKeys(BaseYear); 2008 setUpPositionNormalWorkMonths(BaseYear); 2009 readAndWriteBCSFAndNewAppointmentFundingAndNewPBGL(BaseYear); 2010 CSFDiagnostics(); 2011 buildAppointmentFundingCleanUp(); 2012 } 2013 2014 // overload the vacant BCSF line object builders 2015 protected void addToExistingBCSFVacant(CalculatedSalaryFoundationTrackerOverride csf, String csfKey) { 2016 // 2017 // this method takes care of a rare occurrence. 2018 // - more than one person shares a position in the same line. 2019 // - both people leave on the same day 2020 // - since each vacant CSF line carries the EMPLID of the last 2021 // incumbent, and EMPLID is part of the key, there are two 2022 // separate lines in CSF. 2023 // - the EMPLID is replaced with the vacant ID in BCSF, so there 2024 // will only be one line in BCSF, and we need to aggregate the 2025 // amounts and effort 2026 BudgetConstructionCalculatedSalaryFoundationTracker nowBCSF = bCSF.get(csfKey); 2027 // first round the amount to whole dollars 2028 KualiInteger roundedAmount = new KualiInteger(csf.getCsfAmount(), RoundingMode.valueOf(KualiInteger.ROUND_BEHAVIOR)); 2029 nowBCSF.setCsfAmount(nowBCSF.getCsfAmount().add(roundedAmount)); 2030 // increase the percent time (maximum of 100) 2031 BigDecimal pctTime = nowBCSF.getCsfTimePercent(); 2032 pctTime = pctTime.add(csf.getCsfTimePercent()); 2033 if (pctTime.floatValue() > 100.0) { 2034 pctTime = new BigDecimal(100.00); 2035 } 2036 nowBCSF.setCsfTimePercent(pctTime); 2037 // increase the FTE (full-time equivalent) (maximum of 1.0) 2038 BigDecimal csfFTE = nowBCSF.getCsfFullTimeEmploymentQuantity(); 2039 csfFTE = csfFTE.add(csf.getCsfFullTimeEmploymentQuantity()); 2040 if (csfFTE.floatValue() > 1.0) { 2041 csfFTE = new BigDecimal(1.00); 2042 } 2043 nowBCSF.setCsfFullTimeEmploymentQuantity(csfFTE); 2044 CSFVacantsConsolidated = CSFVacantsConsolidated + 1; 2045 } 2046 2047 protected void addToExistingBCSFVacant(CalculatedSalaryFoundationTracker csf, String csfKey) { 2048 // 2049 // this method takes care of a rare occurrence. 2050 // - more than one person shares a position in the same line. 2051 // - both people leave on the same day 2052 // - since each vacant CSF line carries the EMPLID of the last 2053 // incumbent, and EMPLID is part of the key, there are two 2054 // separate lines in CSF. 2055 // - the EMPLID is replaced with the vacant ID in BCSF, so there 2056 // will only be one line in BCSF, and we need to aggregate the 2057 // amounts and effort 2058 BudgetConstructionCalculatedSalaryFoundationTracker nowBCSF = bCSF.get(csfKey); 2059 // first round the amount to whole dollars 2060 KualiInteger roundedAmount = new KualiInteger(csf.getCsfAmount(), RoundingMode.valueOf(KualiInteger.ROUND_BEHAVIOR)); 2061 nowBCSF.setCsfAmount(nowBCSF.getCsfAmount().add(roundedAmount)); 2062 // increase the percent time (maximum of 100) 2063 BigDecimal pctTime = nowBCSF.getCsfTimePercent(); 2064 pctTime = pctTime.add(csf.getCsfTimePercent()); 2065 if (pctTime.floatValue() > 100.0) { 2066 pctTime = new BigDecimal(100.00); 2067 } 2068 nowBCSF.setCsfTimePercent(pctTime); 2069 // increase the FTE (full-time equivalent) (maximum of 1.0) 2070 BigDecimal csfFTE = nowBCSF.getCsfFullTimeEmploymentQuantity(); 2071 csfFTE = csfFTE.add(csf.getCsfFullTimeEmploymentQuantity()); 2072 if (csfFTE.floatValue() > 1.0) { 2073 csfFTE = new BigDecimal(1.00); 2074 } 2075 nowBCSF.setCsfFullTimeEmploymentQuantity(csfFTE); 2076 CSFVacantsConsolidated = CSFVacantsConsolidated + 1; 2077 } 2078 2079 // make the rounding adjustments 2080 protected void adjustCSFRounding() { 2081 for (Map.Entry<String, roundMechanism> roundMap : keysNeedingRounding.entrySet()) { 2082 roundMechanism rx = roundMap.getValue(); 2083 rx.fixRoundErrors(); 2084 } 2085 // we can reclaim the storage 2086 keysNeedingRounding.clear(); 2087 } 2088 2089 // overload the BCSF object builders 2090 protected void buildAndStoreBCSFfromCSF(CalculatedSalaryFoundationTrackerOverride csf, String csfKey) { 2091 boolean vacantLine = isVacantLine(csf); 2092 BudgetConstructionCalculatedSalaryFoundationTracker csfBC = new BudgetConstructionCalculatedSalaryFoundationTracker(); 2093 // budget construction CSF contains the coming fiscal year 2094 csfBC.setUniversityFiscalYear(csf.getUniversityFiscalYear() + 1); 2095 csfBC.setChartOfAccountsCode(csf.getChartOfAccountsCode()); 2096 csfBC.setAccountNumber(csf.getAccountNumber()); 2097 csfBC.setSubAccountNumber(csf.getSubAccountNumber()); 2098 csfBC.setFinancialObjectCode(csf.getFinancialObjectCode()); 2099 csfBC.setFinancialSubObjectCode(csf.getFinancialSubObjectCode()); 2100 csfBC.setPositionNumber(csf.getPositionNumber()); 2101 // budget construction CSF always contains the vacant EMPLID, not 2102 // the EMPLID of the last incumbent 2103 csfBC.setEmplid((vacantLine ? BCConstants.VACANT_EMPLID : csf.getEmplid())); 2104 csfBC.setCsfFullTimeEmploymentQuantity(csf.getCsfFullTimeEmploymentQuantity()); 2105 csfBC.setCsfTimePercent(csf.getCsfTimePercent()); 2106 csfBC.setCsfFundingStatusCode(csf.getCsfFundingStatusCode()); 2107 // we only worry about rounding errors when the line is not vacant 2108 // since all vacant lines in CSF have the same (vacant) EMPLID, we 2109 // would have to round by position. 2110 if (!vacantLine) { 2111 // changed BO type from KualiDecimal to KualiInteger (June 1, 2007) 2112 // csfBC.setCsfAmount(csf.getCsfAmount()); 2113 bCSF.put(csfKey, csfBC); 2114 // now we have to round and save the rounding error 2115 roundMechanism rX = keysNeedingRounding.get(csf.getEmplid()); 2116 rX.addNewBCSF(csfBC, csf.getCsfAmount()); 2117 } 2118 else { 2119 // for vacant lines, we have to round to whole dollars 2120 csfBC.setCsfAmount(new KualiInteger(csf.getCsfAmount(), RoundingMode.valueOf(KualiInteger.ROUND_BEHAVIOR))); 2121 bCSF.put(csfKey, csfBC); 2122 } 2123 } 2124 2125 protected void buildAndStoreBCSFfromCSF(CalculatedSalaryFoundationTracker csf, String csfKey) { 2126 boolean vacantLine = isVacantLine(csf); 2127 BudgetConstructionCalculatedSalaryFoundationTracker csfBC = new BudgetConstructionCalculatedSalaryFoundationTracker(); 2128 // budget construction CSF contains the coming fiscal year 2129 csfBC.setUniversityFiscalYear(csf.getUniversityFiscalYear() + 1); 2130 csfBC.setChartOfAccountsCode(csf.getChartOfAccountsCode()); 2131 csfBC.setAccountNumber(csf.getAccountNumber()); 2132 csfBC.setSubAccountNumber(csf.getSubAccountNumber()); 2133 csfBC.setFinancialObjectCode(csf.getFinancialObjectCode()); 2134 csfBC.setFinancialSubObjectCode(csf.getFinancialSubObjectCode()); 2135 csfBC.setPositionNumber(csf.getPositionNumber()); 2136 // budget construction CSF always contains the vacant EMPLID, not 2137 // the EMPLID of the last incumbent 2138 csfBC.setEmplid((vacantLine ? BCConstants.VACANT_EMPLID : csf.getEmplid())); 2139 csfBC.setCsfFullTimeEmploymentQuantity(csf.getCsfFullTimeEmploymentQuantity()); 2140 csfBC.setCsfTimePercent(csf.getCsfTimePercent()); 2141 csfBC.setCsfFundingStatusCode(csf.getCsfFundingStatusCode()); 2142 // we only worry about rounding errors when the line is not vacant 2143 // since all vacant lines in CSF have the same (vacant) EMPLID, we 2144 // would have to round by position, and positions can be shared. 2145 if (!vacantLine) { 2146 bCSF.put(csfKey, csfBC); 2147 // now we have to round and save the rounding error 2148 roundMechanism rX = keysNeedingRounding.get(csf.getEmplid()); 2149 rX.addNewBCSF(csfBC, csf.getCsfAmount()); 2150 } 2151 else { 2152 // for vacant lines, we have to round to whole dollars 2153 csfBC.setCsfAmount(new KualiInteger(csf.getCsfAmount(), RoundingMode.valueOf(KualiInteger.ROUND_BEHAVIOR))); 2154 bCSF.put(csfKey, csfBC); 2155 } 2156 } 2157 2158 protected void buildAppointemntFundingFromBCSF(BudgetConstructionCalculatedSalaryFoundationTracker bcsf) { 2159 // current referential integrity insists that the position exists 2160 // if implementers take it out, we hedge our bets below 2161 String positionNumber = bcsf.getPositionNumber(); 2162 Integer normalWorkMonths = (positionNormalWorkMonths.containsKey(positionNumber) ? positionNormalWorkMonths.get(positionNumber) : 12); 2163 // rqstAmount and notOnLeave are used elsewhere and defined globally 2164 KualiInteger defaultAmount = KualiInteger.ZERO; 2165 BigDecimal defaultFractions = new BigDecimal(0); 2166 // 2167 PendingBudgetConstructionAppointmentFunding bcaf = new PendingBudgetConstructionAppointmentFunding(); 2168 bcaf.setUniversityFiscalYear(bcsf.getUniversityFiscalYear()); 2169 bcaf.setChartOfAccountsCode(bcsf.getChartOfAccountsCode()); 2170 bcaf.setAccountNumber(bcsf.getAccountNumber()); 2171 bcaf.setSubAccountNumber(bcsf.getSubAccountNumber()); 2172 bcaf.setFinancialObjectCode(bcsf.getFinancialObjectCode()); 2173 bcaf.setFinancialSubObjectCode(bcsf.getFinancialSubObjectCode()); 2174 bcaf.setEmplid(bcsf.getEmplid()); 2175 bcaf.setPositionNumber(positionNumber); 2176 bcaf.setAppointmentRequestedFteQuantity(bcsf.getCsfFullTimeEmploymentQuantity()); 2177 bcaf.setAppointmentRequestedTimePercent(bcsf.getCsfTimePercent()); 2178 // set the defaults 2179 bcaf.setAppointmentFundingDurationCode(notOnLeave); 2180 bcaf.setAppointmentRequestedCsfAmount(defaultAmount); 2181 bcaf.setAppointmentRequestedCsfFteQuantity(defaultFractions); 2182 bcaf.setAppointmentRequestedCsfTimePercent(defaultFractions); 2183 bcaf.setAppointmentTotalIntendedAmount(defaultAmount); 2184 bcaf.setAppointmentTotalIntendedFteQuantity(defaultFractions); 2185 bcaf.setAppointmentRequestedAmount(rqstAmount); 2186 bcaf.setAppointmentRequestedPayRate(defaultFractions); 2187 bcaf.setAppointmentFundingMonth(normalWorkMonths); 2188 // for a new row, these are always false 2189 bcaf.setAppointmentFundingDeleteIndicator(false); 2190 bcaf.setPositionObjectChangeIndicator(false); 2191 bcaf.setPositionSalaryChangeIndicator(false); 2192 // now store the result 2193 getPersistenceBrokerTemplate().store(bcaf); 2194 // store the new BCSF row as well 2195 getPersistenceBrokerTemplate().store(bcsf); 2196 } 2197 2198 protected String buildAppointmentFundingKey(PendingBudgetConstructionAppointmentFunding bcaf) { 2199 return (bcaf.getEmplid()) + bcaf.getPositionNumber() + bcaf.getAccountNumber() + bcaf.getChartOfAccountsCode() + bcaf.getSubAccountNumber() + bcaf.getFinancialObjectCode() + bcaf.getFinancialSubObjectCode(); 2200 } 2201 2202 // overload the CSF key builders 2203 protected String buildCSFKey(CalculatedSalaryFoundationTrackerOverride csf) { 2204 return (csf.getEmplid()) + csf.getPositionNumber() + csf.getAccountNumber() + csf.getChartOfAccountsCode() + csf.getSubAccountNumber() + csf.getFinancialObjectCode() + csf.getFinancialSubObjectCode(); 2205 } 2206 2207 protected String buildCSFKey(CalculatedSalaryFoundationTracker csf) { 2208 return (csf.getEmplid()) + csf.getPositionNumber() + csf.getAccountNumber() + csf.getChartOfAccountsCode() + csf.getSubAccountNumber() + csf.getFinancialObjectCode() + csf.getFinancialSubObjectCode(); 2209 } 2210 2211 protected String buildDocKeyFromBCSF(BudgetConstructionCalculatedSalaryFoundationTracker bcsf) { 2212 // see setUpbcHdrDocNumbers for the correct key elements 2213 // the order here must match the order there 2214 return bcsf.getChartOfAccountsCode() + bcsf.getAccountNumber() + bcsf.getSubAccountNumber(); 2215 } 2216 2217 protected boolean buildPBGLFromBCSFAndStore(BudgetConstructionCalculatedSalaryFoundationTracker bcsf) { 2218 // first we need to see if a new PBGL row is needed 2219 String testKey = buildPBGLKey(bcsf); 2220 if (currentPBGLKeys.contains(testKey)) { 2221 return true; 2222 } 2223 // Budget construction cannot show detailed salary lines unless the object code supports "detailed positions". But, the CSF is a plug-in, so Kuali cannot assume that the CSF enforces this rule. 2224 // Here, we test whether the object class is valid. if it is not, we write a message and skip the row. 2225 // we do this before the GL check, so we can be sure we log all the rows that have problems instead of requiring multiple runs to find them all. 2226 String objectType = detailedPositionObjectTypes.get(bcsf.getChartOfAccountsCode() + bcsf.getFinancialObjectCode()); 2227 if (objectType == null) { 2228 LOG.warn(String.format("\nthis row has an object class which does not support" + " detailed positions (skipped):\n" + "position: %s, EMPLID: %s, accounting string =" + "(%s,%s,%s,%s,%s", bcsf.getPositionNumber(), bcsf.getEmplid(), bcsf.getChartOfAccountsCode(), bcsf.getAccountNumber(), bcsf.getSubAccountNumber(), bcsf.getFinancialObjectCode(), bcsf.getFinancialSubObjectCode())); 2229 CSFBadObjectsSkipped = CSFBadObjectsSkipped + 1; 2230 return false; 2231 } 2232 // we need a new row. 2233 // store the key so we won't try to add another row from a different person's bcsf which has the same key. 2234 currentPBGLKeys.add(testKey); 2235 String docKey = buildDocKeyFromBCSF(bcsf); 2236 // we never have to build a new document header because createNewBCDocumentsFromGLCSF is always called earlier in the step containing this routine. 2237 // fill in the fields. 2238 PendingBudgetConstructionGeneralLedger pbGL = new PendingBudgetConstructionGeneralLedger(); 2239 pbGL.setDocumentNumber(bcHdrDocNumbers.get(docKey)); 2240 pbGL.setUniversityFiscalYear(bcsf.getUniversityFiscalYear()); 2241 pbGL.setChartOfAccountsCode(bcsf.getChartOfAccountsCode()); 2242 pbGL.setAccountNumber(bcsf.getAccountNumber()); 2243 pbGL.setSubAccountNumber(bcsf.getSubAccountNumber()); 2244 pbGL.setFinancialObjectCode(bcsf.getFinancialObjectCode()); 2245 pbGL.setFinancialSubObjectCode(bcsf.getFinancialSubObjectCode()); 2246 pbGL.setFinancialBalanceTypeCode(KFSConstants.BALANCE_TYPE_BASE_BUDGET); 2247 pbGL.setFinancialObjectTypeCode(objectType); 2248 pbGL.setAccountLineAnnualBalanceAmount(KualiInteger.ZERO); 2249 pbGL.setFinancialBeginningBalanceLineAmount(KualiInteger.ZERO); 2250 // store the new PBGL row 2251 getPersistenceBrokerTemplate().store(pbGL); 2252 CSFNewGLRows = CSFNewGLRows + 1; 2253 return true; 2254 } 2255 2256 // these two rows are overloaded so we have a standardized key 2257 protected String buildPBGLKey(BudgetConstructionCalculatedSalaryFoundationTracker bcsf) { 2258 return bcsf.getAccountNumber() + bcsf.getFinancialObjectCode() + bcsf.getChartOfAccountsCode() + bcsf.getSubAccountNumber() + bcsf.getFinancialSubObjectCode(); 2259 } 2260 2261 protected String buildPBGLKey(PendingBudgetConstructionGeneralLedger pbgl) { 2262 return pbgl.getAccountNumber() + pbgl.getFinancialObjectCode() + pbgl.getChartOfAccountsCode() + pbgl.getSubAccountNumber() + pbgl.getFinancialSubObjectCode(); 2263 } 2264 2265 protected String buildVacantCSFKey(CalculatedSalaryFoundationTrackerOverride csf) { 2266 boolean vacantLine = isVacantLine(csf); 2267 return (vacantLine ? BCConstants.VACANT_EMPLID : csf.getEmplid()) + csf.getPositionNumber() + csf.getAccountNumber() + csf.getChartOfAccountsCode() + csf.getSubAccountNumber() + csf.getFinancialObjectCode() + csf.getFinancialSubObjectCode(); 2268 } 2269 2270 protected String buildVacantCSFKey(CalculatedSalaryFoundationTracker csf) { 2271 boolean vacantLine = isVacantLine(csf); 2272 return (vacantLine ? BCConstants.VACANT_EMPLID : csf.getEmplid()) + csf.getPositionNumber() + csf.getAccountNumber() + csf.getChartOfAccountsCode() + csf.getSubAccountNumber() + csf.getFinancialObjectCode() + csf.getFinancialSubObjectCode(); 2273 } 2274 2275 // 2276 // clean out the existing BCSF data for the key in question. 2277 protected void clearBCCSF(Integer FiscalYear) { 2278 Criteria criteriaID = new Criteria(); 2279 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, FiscalYear); 2280 QueryByCriteria queryID = new QueryByCriteria(BudgetConstructionCalculatedSalaryFoundationTracker.class, criteriaID); 2281 getPersistenceBrokerTemplate().deleteByQuery(queryID); 2282 // as always, we should clear the cache after a bulk delete, even though in this case we haven't yet fetched much 2283 getPersistenceBrokerTemplate().clearCache(); 2284 } 2285 2286 2287 protected void CSFDiagnostics() { 2288 LOG.info(String.format("\n\nResults of building BC CSF")); 2289 LOG.info(String.format("\nCSF override active rows %d", CSFOverrideRead)); 2290 LOG.info(String.format("\nCSF override deletes %d", CSFOverrideDeletesRead)); 2291 LOG.info(String.format("\nCSF rows read %d", CSFRowsRead)); 2292 LOG.info(String.format("\n\nCSF overrides vacant %d", CSFOverrideVacant)); 2293 LOG.info(String.format("\nCSF vacant %d", CSFRowsVacant)); 2294 LOG.info(String.format("\nCSF vacants consolidated %d", CSFVacantsConsolidated)); 2295 LOG.info(String.format("\n\nBudgetConstruction CSF rows %d", CSFForBCSF)); 2296 LOG.info(String.format("\n\nCurrent PBGL rows with position object classes %d", CSFCurrentGLRows)); 2297 LOG.info(String.format("\nNew PBGL rows created from CSF %d", CSFNewGLRows)); 2298 LOG.info(String.format("\nCSF rows skipped: bad obj code %d",CSFBadObjectsSkipped)); 2299 LOG.info(String.format("\n\nCurrent appt funding rows %d", CSFCurrentBCAFRows)); 2300 LOG.info(String.format("\nNew appt funding rows from CSF %d",CSFNewBCAFRows)); 2301 LOG.info(String.format("\nAppt funding rows not in BCSF %d", CSFBCAFRowsMissing)); 2302 LOG.info(String.format("\nAppt funding rows marked deleted %d", CSFBCAFRowsMarkedDeleted)); 2303 LOG.info(String.format("\n\nend of BC CSF build statistics")); 2304 } 2305 2306 protected ArrayList<String> findPositionRequiredObjectCodes(Integer BaseYear) { 2307 // we want to build an SQL IN criteria to filter a return set. 2308 // we will find distinct objects only, regardless of chart, so OJB can build the SQL instead of our having to concatenate the chart and object code fields. 2309 // this will not be a concern--it will make the return set bigger,but include every case we want. 2310 // the result will be used to build a list to check for missing PBGL rows, so having more PBGL rows than we need will not cause us to miss any. 2311 Integer RequestYear = BaseYear + 1; 2312 ArrayList<String> objectCodesWithIndividualPositions = new ArrayList<String>(10); 2313 2314 Map<String, Object> laborObjectCodeMap = new HashMap<String, Object>(); 2315 laborObjectCodeMap.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 2316 laborObjectCodeMap.put(KFSPropertyConstants.DETAIL_POSITION_REQUIRED_INDICATOR, true); 2317 laborObjectCodeMap.put(KFSPropertyConstants.ACTIVE,true); 2318 List<LaborLedgerObject> laborLedgerObjects = kualiModuleService.getResponsibleModuleService(LaborLedgerObject.class).getExternalizableBusinessObjectsList(LaborLedgerObject.class, laborObjectCodeMap); 2319 2320 for(LaborLedgerObject laborObject: laborLedgerObjects) { 2321 objectCodesWithIndividualPositions.add(laborObject.getFinancialObjectCode()); 2322 } 2323 2324 return objectCodesWithIndividualPositions; 2325 } 2326 2327 // we will overload both of these checks as well 2328 protected boolean isVacantLine(CalculatedSalaryFoundationTracker csf) { 2329 return ((csf.getCsfFundingStatusCode().equals(BCConstants.csfFundingStatusFlag.VACANT.getFlagValue())) || (csf.getCsfFundingStatusCode().equals(BCConstants.csfFundingStatusFlag.UNFUNDED.getFlagValue()))); 2330 } 2331 2332 protected boolean isVacantLine(CalculatedSalaryFoundationTrackerOverride csf) { 2333 return ((csf.getCsfFundingStatusCode().equals(BCConstants.csfFundingStatusFlag.VACANT.getFlagValue())) || (csf.getCsfFundingStatusCode().equals(BCConstants.csfFundingStatusFlag.UNFUNDED.getFlagValue()))); 2334 } 2335 2336 // here are the routines to build BCSF 2337 2338 protected void readAndWriteBCSFAndNewAppointmentFundingAndNewPBGL(Integer BaseYear) { 2339 // read through the remaining BCSF objects (those that do not presently exist in appointment funding (BCAF). 2340 // we will check whether they exist in Pending GL (PBGL), and, if not, write a new GL line. 2341 // Then, we will write a PBGL row and a BCSF row (in that order, because of referential integrity. 2342 // The PBGL and BCAF rows will have 0 amounts. 2343 // People will have to fill in the budgets (or mark the funding deleted) to cover people in the payroll in budgeted positions, but not funding in the base budget in GL. 2344 CSFNewBCAFRows = bCSF.size(); 2345 for (Map.Entry<String, BudgetConstructionCalculatedSalaryFoundationTracker> orphanBCSF : bCSF.entrySet()) { 2346 BudgetConstructionCalculatedSalaryFoundationTracker bcsf = orphanBCSF.getValue(); 2347 if (!buildPBGLFromBCSFAndStore(bcsf)) { 2348 continue; 2349 } 2350 buildAppointemntFundingFromBCSF(bcsf); 2351 } 2352 } 2353 2354 protected void readCSF(Integer BaseYear) { 2355 Criteria criteriaID = new Criteria(); 2356 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 2357 criteriaID.addEqualTo(KFSPropertyConstants.CSF_DELETE_CODE, BCConstants.ACTIVE_CSF_DELETE_CODE); 2358 QueryByCriteria queryID = new QueryByCriteria(CalculatedSalaryFoundationTracker.class, criteriaID); 2359 Iterator csfResultSet = getPersistenceBrokerTemplate().getIteratorByQuery(queryID); 2360 while (csfResultSet.hasNext()) { 2361 CalculatedSalaryFoundationTracker csfRow = (CalculatedSalaryFoundationTracker) csfResultSet.next(); 2362 CSFRowsRead = CSFRowsRead + 1; 2363 CSFRowsVacant = CSFRowsVacant + (isVacantLine(csfRow) ? 1 : 0); 2364 // has this been overridden? if so, don't store it 2365 String testKey = buildCSFKey(csfRow); 2366 if (csfOverrideKeys.contains(testKey)) { 2367 continue; 2368 } 2369 // is the line vacant 2370 testKey = buildVacantCSFKey(csfRow); 2371 if (isVacantLine(csfRow) && (bCSF.containsKey(testKey))) { 2372 //the line is vacant and it is already in CSF 2373 addToExistingBCSFVacant(csfRow, testKey); 2374 } 2375 else { 2376 buildAndStoreBCSFfromCSF(csfRow, testKey); 2377 } 2378 } 2379 // we no longer need the list of csf override keys--recycle 2380 csfOverrideKeys.clear(); 2381 } 2382 2383 protected void readCSFOverride(Integer BaseYear) { 2384 Criteria criteriaID = new Criteria(); 2385 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 2386 criteriaID.addEqualTo(KFSPropertyConstants.CSF_DELETE_CODE, BCConstants.ACTIVE_CSF_DELETE_CODE); 2387 criteriaID.addEqualTo(KFSPropertyConstants.ACTIVE, true); 2388 QueryByCriteria queryID = new QueryByCriteria(CalculatedSalaryFoundationTrackerOverride.class, criteriaID); 2389 Iterator csfResultSet = getPersistenceBrokerTemplate().getIteratorByQuery(queryID); 2390 while (csfResultSet.hasNext()) { 2391 CalculatedSalaryFoundationTrackerOverride csfRow = (CalculatedSalaryFoundationTrackerOverride) csfResultSet.next(); 2392 CSFOverrideRead = CSFOverrideRead + 1; 2393 CSFOverrideVacant = CSFOverrideVacant + (isVacantLine(csfRow) ? 1 : 0); 2394 // is the line vacant 2395 String testKey = buildVacantCSFKey(csfRow); 2396 if (isVacantLine(csfRow) && (bCSF.containsKey(testKey))) { 2397 //the line is vacant and it is already in CSF 2398 addToExistingBCSFVacant(csfRow, testKey); 2399 } 2400 else { 2401 buildAndStoreBCSFfromCSF(csfRow, testKey); 2402 } 2403 } 2404 } 2405 2406 protected void readExistingAppointmentFunding(Integer BaseYear) { 2407 // we will read all existing appointment funding (BCAF). 2408 // -- if a BCAF object matches with a BCSF row, we will store and remove the BCSF row, and ignore the AF object 2409 // -- if a BCAF object does NOT match with a BCSF row, we will check to see if it has been altered by a user. if not, we will mark it deleted and store it as such. 2410 // 2411 Integer RequestYear = BaseYear + 1; 2412 Criteria criteriaID = new Criteria(); 2413 // we add this criterion so that it is possible to have more than one year at a time in budget construction if an institution wants to do that. 2414 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 2415 QueryByCriteria queryID = new QueryByCriteria(PendingBudgetConstructionAppointmentFunding.class, criteriaID); 2416 Iterator bcafResults = getPersistenceBrokerTemplate().getIteratorByQuery(queryID); 2417 while (bcafResults.hasNext()) { 2418 CSFCurrentBCAFRows = CSFCurrentBCAFRows + 1; 2419 PendingBudgetConstructionAppointmentFunding bcaf = (PendingBudgetConstructionAppointmentFunding) bcafResults.next(); 2420 String testKey = buildAppointmentFundingKey(bcaf); 2421 if (bCSF.containsKey(testKey)) { 2422 // the new BCSF row is already in appointment funding. 2423 // we store the BCSF row, delete it from the hash map, and go on. 2424 BudgetConstructionCalculatedSalaryFoundationTracker bCSFRow = bCSF.get(testKey); 2425 getPersistenceBrokerTemplate().store(bCSFRow); 2426 bCSF.remove(testKey); 2427 } 2428 else { 2429 // the current funding row is NOT in the new base set. 2430 // we will mark it deleted if it came in from CSF and has not been altered by a user. 2431 // we never remove any existing rows from BCAF in batch. 2432 untouchedAppointmentFunding(bcaf); 2433 } 2434 } 2435 } 2436 2437 // set up the hash objects 2438 protected void setUpBCSFMap(Integer BaseYear) { 2439 // we'll just overestimate, making the size equal to active override rows and active CSF rows, even though the former might replace some of the latter 2440 Integer bCSFSize = new Integer(0); 2441 Criteria criteriaID = new Criteria(); 2442 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 2443 criteriaID.addEqualTo(KFSPropertyConstants.CSF_DELETE_CODE, BCConstants.ACTIVE_CSF_DELETE_CODE); 2444 criteriaID.addEqualTo(KFSPropertyConstants.ACTIVE,true); 2445 Criteria criteriaIDCSF = new Criteria(); 2446 criteriaIDCSF.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 2447 criteriaIDCSF.addEqualTo(KFSPropertyConstants.CSF_DELETE_CODE, BCConstants.ACTIVE_CSF_DELETE_CODE); 2448 bCSFSize = hashObjectSize(CalculatedSalaryFoundationTrackerOverride.class, criteriaID) + hashObjectSize(CalculatedSalaryFoundationTracker.class, criteriaIDCSF); 2449 bCSF = new HashMap<String, BudgetConstructionCalculatedSalaryFoundationTracker>(bCSFSize); 2450 } 2451 2452 protected void setUpbcHdrDocNumbers(Integer BaseYear) { 2453 Integer RequestYear = BaseYear + 1; 2454 Criteria criteriaID = new Criteria(); 2455 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 2456 bcHdrDocNumbers = new HashMap<String, String>(hashObjectSize(BudgetConstructionHeader.class, criteriaID)); 2457 // now we have to get the actual data 2458 String[] headerList = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.ACCOUNT_NUMBER, KFSPropertyConstants.SUB_ACCOUNT_NUMBER, KFSPropertyConstants.DOCUMENT_NUMBER }; 2459 ReportQueryByCriteria queryID = new ReportQueryByCriteria(BudgetConstructionHeader.class, headerList, criteriaID); 2460 Iterator headerRows = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 2461 while (headerRows.hasNext()) { 2462 Object[] headerRow = (Object[]) headerRows.next(); 2463 String testKey = ((String) headerRow[0]) + ((String) headerRow[1]) + ((String) headerRow[2]); 2464 bcHdrDocNumbers.put(testKey, ((String) headerRow[3])); 2465 } 2466 } 2467 2468 protected void setUpCSFOverrideKeys(Integer BaseYear) { 2469 // these are rows in CSF Override--they should take precedence over what is in CSF 2470 // the idea is this: 2471 // (1) we build BCSF from CSF Override first. so, when we read CSF, we will not create a new BCSF entry if the override already has created one. 2472 // (2) the override will create an entry with the same key as CSF unless (a) the override has a deleted row or (b) the override has a vacant row so that the EMPLID is changed to the vacant EMPLID in BCSF. 2473 // So, we create a list of override keys possibly missing in BCSF which can be used to eliminate CSF candidates for BCSF. 2474 Criteria criteriaID = new Criteria(); 2475 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 2476 criteriaID.addEqualTo(KFSPropertyConstants.ACTIVE, true); 2477 Criteria deleteCriteria = new Criteria(); 2478 deleteCriteria.addNotEqualTo(KFSPropertyConstants.CSF_DELETE_CODE, BCConstants.ACTIVE_CSF_DELETE_CODE); 2479 Criteria vacantCriteria = new Criteria(); 2480 vacantCriteria.addEqualTo(KFSPropertyConstants.CSF_FUNDING_STATUS_CODE, BCConstants.csfFundingStatusFlag.VACANT.getFlagValue()); 2481 deleteCriteria.addOrCriteria(vacantCriteria); 2482 criteriaID.addAndCriteria(deleteCriteria); 2483 csfOverrideKeys = new HashSet<String>(hashObjectSize(CalculatedSalaryFoundationTrackerOverride.class, criteriaID)); 2484 // now we want to build the hash set 2485 QueryByCriteria qry = new QueryByCriteria(CalculatedSalaryFoundationTrackerOverride.class, criteriaID); 2486 Iterator<CalculatedSalaryFoundationTrackerOverride> csfOvrd = getPersistenceBrokerTemplate().getIteratorByQuery(qry); 2487 while (csfOvrd.hasNext()) { 2488 CalculatedSalaryFoundationTrackerOverride csfOvrdRow = csfOvrd.next(); 2489 csfOverrideKeys.add(buildCSFKey(csfOvrdRow)); 2490 CSFOverrideDeletesRead = CSFOverrideDeletesRead + ((csfOvrdRow.getCsfDeleteCode().equals(BCConstants.ACTIVE_CSF_DELETE_CODE)? 0 : 1)); 2491 } 2492 } 2493 2494 protected void setUpCurrentPBGLKeys(Integer BaseYear) { 2495 // this will actually set up two maps. 2496 // both will be used in the same routine to build the PBGL for BCSF. 2497 // keys not in the base budget (someone is being paid from an account, but no one has yet bothered to move base budget funding into the account to cover the expense). 2498 Integer RequestYear = BaseYear + 1; 2499 Criteria criteriaID = new Criteria(); 2500 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 2501 criteriaID.addIn(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, this.findPositionRequiredObjectCodes(BaseYear)); 2502 currentPBGLKeys = new HashSet<String>(hashObjectSize(PendingBudgetConstructionGeneralLedger.class, criteriaID)); 2503 // now do the same for the detailed position object code--> object type map (object codes that are allowed to fund individual HR positions) 2504 detailedPositionObjectTypes = new HashMap<String, String>(hashObjectSize(ObjectCode.class, criteriaID)); 2505 // the PBGL has already been built. 2506 // we will get business objects so we can use an overloaded method that will be easy to change in order to extract the key. 2507 // the objects are of no further use, and will disappear when we clear the cache 2508 int counter = 0; 2509 QueryByCriteria pbGLQuery = new QueryByCriteria(PendingBudgetConstructionGeneralLedger.class, criteriaID); 2510 Iterator pbGLObjects = getPersistenceBrokerTemplate().getIteratorByQuery(pbGLQuery); 2511 while (pbGLObjects.hasNext()) { 2512 PendingBudgetConstructionGeneralLedger pbGLRow = (PendingBudgetConstructionGeneralLedger) pbGLObjects.next(); 2513 String testKey = this.buildPBGLKey(pbGLRow); 2514 currentPBGLKeys.add(testKey); 2515 counter = counter + 1; 2516 } 2517 CSFCurrentGLRows = new Integer(counter); 2518 // 2519 // now we have to set up the query to read the object types 2520 String[] objectTypeSelectList = { KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, KFSPropertyConstants.FINANCIAL_OBJECT_TYPE_CODE }; 2521 ReportQueryByCriteria queryID = new ReportQueryByCriteria(ObjectCode.class, objectTypeSelectList, criteriaID); 2522 Iterator objectTypeRowReturned = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 2523 while (objectTypeRowReturned.hasNext()) { 2524 Object[] objectRow = (Object[]) objectTypeRowReturned.next(); 2525 String keyString = ((String) objectRow[0]) + ((String) objectRow[1]); 2526 String valueString = (String) objectRow[2]; 2527 detailedPositionObjectTypes.put(keyString, valueString); 2528 } 2529 } 2530 2531 protected void setUpKeysNeedingRounding(Integer BaseYear) { 2532 Integer emplidCSFOvrdCount = new Integer(0); 2533 Integer emplidCSFCount = new Integer(0); 2534 Criteria criteriaID = new Criteria(); 2535 criteriaID.addEqualTo(KFSPropertyConstants.CSF_DELETE_CODE, BCConstants.ACTIVE_CSF_DELETE_CODE); 2536 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 2537 criteriaID.addEqualTo(KFSPropertyConstants.ACTIVE, true); 2538 Criteria criteriaIDCSF = new Criteria(); 2539 criteriaIDCSF.addEqualTo(KFSPropertyConstants.CSF_DELETE_CODE, BCConstants.ACTIVE_CSF_DELETE_CODE); 2540 criteriaIDCSF.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, BaseYear); 2541 keysNeedingRounding = new HashMap<String, roundMechanism>(hashObjectSize(CalculatedSalaryFoundationTrackerOverride.class, criteriaID, KFSPropertyConstants.EMPLID) + hashObjectSize(CalculatedSalaryFoundationTracker.class, criteriaIDCSF, KFSPropertyConstants.EMPLID)); 2542 // now fill the hashmap: there will be one rounding bucket for each EMPLID 2543 String[] columnList = { KFSPropertyConstants.EMPLID }; 2544 // first use CSF Override 2545 ReportQueryByCriteria queryID = new ReportQueryByCriteria(CalculatedSalaryFoundationTrackerOverride.class, columnList, criteriaID, true); 2546 Iterator emplidOvrd = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 2547 while (emplidOvrd.hasNext()) { 2548 String newKey = (String) ((Object[]) emplidOvrd.next())[0]; 2549 keysNeedingRounding.put(newKey, new roundMechanism()); 2550 } 2551 LOG.info(String.format("\nEMPLID's from CSF override: %d", keysNeedingRounding.size())); 2552 // now add the EMPLID's from CSF itself (the criterion differs because of the active code on CSF override--CSF override *must* precede CSF here) 2553 queryID = new ReportQueryByCriteria(CalculatedSalaryFoundationTracker.class, columnList, criteriaIDCSF, true); 2554 Iterator emplidIter = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 2555 while (emplidIter.hasNext()) { 2556 String newKey = (String) ((Object[]) emplidIter.next())[0]; 2557 // insert what is not already there from CSF override 2558 if (!keysNeedingRounding.containsKey(newKey)) { 2559 keysNeedingRounding.put(newKey, new roundMechanism()); 2560 } 2561 } 2562 LOG.info(String.format("\nEMPLID total for BCSF: %d", keysNeedingRounding.size())); 2563 } 2564 2565 // read the position table so we can attach normal work months to new bcaf rows 2566 protected void setUpPositionNormalWorkMonths(Integer BaseYear) { 2567 Integer RequestYear = BaseYear + 1; 2568 Criteria criteriaID = new Criteria(); 2569 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, RequestYear); 2570 positionNormalWorkMonths = new HashMap<String, Integer>(hashObjectSize(BudgetConstructionPosition.class, criteriaID)); 2571 String[] fieldList = { KFSPropertyConstants.POSITION_NUMBER, KFSPropertyConstants.IU_NORMAL_WORK_MONTHS }; 2572 ReportQueryByCriteria queryID = new ReportQueryByCriteria(BudgetConstructionPosition.class, fieldList, criteriaID); 2573 Iterator positionRows = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID); 2574 while (positionRows.hasNext()) { 2575 // apparently, numbers come back from report queries in DB-specific ways. use Number to be safe (as OJB itself does) since the results do not go through the business object 2576 Object[] positionRow = (Object[]) positionRows.next(); 2577 positionNormalWorkMonths.put((String) positionRow[0], (Integer) ((Number) positionRow[1]).intValue()); 2578 } 2579 } 2580 2581 // these are the four values used to decide whether the current appointment funding row, missing from BCSF, has been entered by a user or is due to a CSF row that has since gone away 2582 String notOnLeave = new String(BCConstants.AppointmentFundingDurationCodes.NONE.durationCode); 2583 KualiInteger rqstAmount = new KualiInteger(0); 2584 BigDecimal pctTime = new BigDecimal(0); 2585 BigDecimal FTE = new BigDecimal(0); 2586 2587 protected void untouchedAppointmentFunding(PendingBudgetConstructionAppointmentFunding bcaf) { 2588 // this checks to see whether the missing row could have come in from CSF earlier, but the CSF row which created it is not inactive. 2589 // if they it not come in from CSF, then it follows that someone entered it and we should not touch it. 2590 CSFBCAFRowsMissing = CSFBCAFRowsMissing + 1; 2591 if ((bcaf.getAppointmentRequestedAmount().compareTo(rqstAmount) != 0) || (bcaf.getAppointmentFundingDurationCode().compareTo(notOnLeave) != 0) || (bcaf.isAppointmentFundingDeleteIndicator())) { 2592 return; 2593 } 2594 // 2595 // this should happen so rarely that we trade time for space, and do 2596 // an individual OBJ SQL call to see whether the missing row did in fact 2597 // come in from CSF. anecdotal evidence indicates there are about 25 or so 2598 // a day. if this gets to be a major run-time problem, the fix would be to 2599 // create another hashMap<String,BigDecimal[]), where the key would be 2600 // the accounting key, position, and EMPLID (and if the line were vacant, 2601 // another key differing only by the replacement of EMPLID by the VACANT_EMPLID 2602 // value). The BigDecimal[] would be csfTimePercent and 2603 // csfFullTimeEmploymentQuantity. 2604 // 2605 Criteria criteriaID = new Criteria(); 2606 criteriaID.addEqualTo(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, bcaf.getUniversityFiscalYear() - 1); 2607 criteriaID.addEqualTo(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, bcaf.getChartOfAccountsCode()); 2608 criteriaID.addEqualTo(KFSPropertyConstants.ACCOUNT_NUMBER, bcaf.getAccountNumber()); 2609 criteriaID.addEqualTo(KFSPropertyConstants.SUB_ACCOUNT_NUMBER, bcaf.getSubAccountNumber()); 2610 criteriaID.addEqualTo(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, bcaf.getFinancialObjectCode()); 2611 criteriaID.addEqualTo(KFSPropertyConstants.FINANCIAL_SUB_OBJECT_CODE, bcaf.getFinancialSubObjectCode()); 2612 criteriaID.addEqualTo(KFSPropertyConstants.POSITION_NUMBER, bcaf.getPositionNumber()); 2613 // if the budget construction appointment funding (BCAF) row has a vacant ID, we look for vacant flags in CSF. 2614 if (bcaf.getEmplid().equals(BCConstants.VACANT_EMPLID)) 2615 { 2616 // the EMPLID in CSF will not match with BCAF: we want to see if a row matching on the other criteria is vacant 2617 // funding status is "vacant" or "unfunded" 2618 Criteria flagCriteria = new Criteria(); 2619 flagCriteria.addEqualTo(KFSPropertyConstants.CSF_FUNDING_STATUS_CODE, BCConstants.csfFundingStatusFlag.VACANT.getFlagValue()); 2620 Criteria vacantCriteria = new Criteria(); 2621 vacantCriteria.addEqualTo(KFSPropertyConstants.CSF_FUNDING_STATUS_CODE, BCConstants.csfFundingStatusFlag.UNFUNDED.getFlagValue()); 2622 flagCriteria.addOrCriteria(vacantCriteria); 2623 criteriaID.addAndCriteria(flagCriteria); 2624 } 2625 else 2626 { 2627 // since the BCAF row is not vacant, we require that it match CSF on EMPLID 2628 criteriaID.addEqualTo(KFSPropertyConstants.EMPLID, bcaf.getEmplid()); 2629 } 2630 // we are going to compare two numbers (an FTE and a Percent Time), which both originated from CSF. We have to go through the Kuali filters to make sure the values are strictly comparable. We can't do a report query. 2631 QueryByCriteria queryID = new QueryByCriteria(CalculatedSalaryFoundationTracker.class, criteriaID); 2632 Iterator<CalculatedSalaryFoundationTracker> resultSet = getPersistenceBrokerTemplate().getIteratorByQuery(queryID); 2633 if (!resultSet.hasNext()) { 2634 // the line did not come from CSF, so it must have been added by a user. 2635 // therefore, we should *not* mark it deleted 2636 return; 2637 } 2638 // we have to check whether the CFTE and the percent time are the same. 2639 // rounding is required because these can be stored as large numbers in the DB. 2640 CalculatedSalaryFoundationTracker resultCSF = resultSet.next(); 2641 if (untouchedFTEPercentTimeCheck(bcaf, resultCSF)) 2642 { 2643 // we need to mark this bcaf line deleted 2644 bcaf.setAppointmentRequestedFteQuantity(FTE); 2645 bcaf.setAppointmentRequestedTimePercent(pctTime); 2646 bcaf.setAppointmentFundingDeleteIndicator(true); 2647 getPersistenceBrokerTemplate().store(bcaf); 2648 CSFBCAFRowsMarkedDeleted = CSFBCAFRowsMarkedDeleted + 1; 2649 } 2650 // we also need to exhaust the iterator, so OJB will close the cursor (Oracle) 2651 TransactionalServiceUtils.exhaustIterator(resultSet); 2652 } 2653 2654 protected static final MathContext compareContext = new MathContext(2,RoundingMode.HALF_UP); 2655 2656 protected boolean untouchedFTEPercentTimeCheck(PendingBudgetConstructionAppointmentFunding bcaf, CalculatedSalaryFoundationTracker resultCSF) 2657 { 2658 // we need to check whether the current appointment funding row not in BCSF should be marked deleted. 2659 // it should be if it has not been "touched" by a user. 2660 // the criteria for "not touched by a user" are (a) it got into appointment funding via CSF, so it matched with a CSF row, (b) the request amount is 0, and (c) the request FTE and percent time match those of the CSF row. 2661 // (b) was checked earlier, since it can be checked without a CSF look-up and if the amount is not 0 we can avoid an expensive DB call. 2662 // (a) is a given, because the resultCSF comes from the matching CSF row. So, all that is left is (c). we do this in a separate method because it is convoluted and doing so isolates the code. 2663 // (c) must be done with rounded values, to avoid letting differences in high-order decimal places in DB storage make equivalent values technically unequal. 2664 // 2665 // we are making the assumption here, based on the business object, that these values are BigDecimal 2666 // for BigDecimal, compareTo ignores the scale if it differs between the two comparands and only looks at the values (2 = 2.00), while equals will return false for the same value but different scales 2667 BigDecimal CSFFTE = resultCSF.getCsfFullTimeEmploymentQuantity().round(compareContext); 2668 BigDecimal BCAFFTE = bcaf.getAppointmentRequestedFteQuantity().round(compareContext); 2669 boolean FTEOK = (CSFFTE.compareTo(BCAFFTE) == 0); 2670 BigDecimal CSFPctTime = resultCSF.getCsfTimePercent().round(compareContext); 2671 BigDecimal BCAFPctTime = bcaf.getAppointmentRequestedTimePercent().round(compareContext); 2672 boolean PctTimeOK = (CSFPctTime.compareTo(BCAFPctTime) == 0); 2673 String bcafPosition = bcaf.getPositionNumber(); 2674 return (FTEOK && PctTimeOK); 2675 } 2676 2677 // this is an inner class which will store the data we need to perform the rounding, and supply the methods as well 2678 KualiDecimal shavePennies = new KualiDecimal(100); 2679 2680 protected class roundMechanism { 2681 // the idea here is that people split over many lines could lose or gain several dollars if we rounded each salary line individually. so, we do the following. 2682 // (1) assume that all the amounts are positive 2683 // (2) truncate the actual amount to the next lowest integer (round floor) 2684 // (3) accumulate the difference in a running total to two decimal places 2685 // (4) when all the lines for a person have been encountered, we round the 2686 // difference to the next whole integer. 2687 // (5) add the difference in dollar increments to each of the lines until the 2688 // difference amount is exhausted 2689 // In other words, we only use "bankers rounding" at the end. We truncate by converting to an int, which calls BigDecimal.intvalue. 2690 private KualiDecimal diffAmount = new KualiDecimal(0); 2691 private ArrayList<BudgetConstructionCalculatedSalaryFoundationTracker> candidateBCSFRows = new ArrayList<BudgetConstructionCalculatedSalaryFoundationTracker>(10); 2692 2693 public void addNewBCSF(BudgetConstructionCalculatedSalaryFoundationTracker bCSF, KualiDecimal amountFromCSFToSet) { 2694 // lop off the pennies--go down to the nearest whole dollar 2695 KualiInteger wholeDollarsCSFAmount = new KualiInteger(amountFromCSFToSet, RoundingMode.FLOOR); 2696 // store the whole dollar amount in the budget construction CSF object 2697 bCSF.setCsfAmount(wholeDollarsCSFAmount); 2698 // find the pennies that were shaved off 2699 KualiDecimal penniesFromCSFAmount = amountFromCSFToSet; 2700 // BigDecimal values are immutable. So, we have to reset the pointer after the subtract 2701 penniesFromCSFAmount = penniesFromCSFAmount.subtract(wholeDollarsCSFAmount.kualiDecimalValue()); 2702 // just round negative amounts and return. 2703 // this is only a safety measure. negative salaries are illegal in budget construction. 2704 if (wholeDollarsCSFAmount.isNegative()) { 2705 return; 2706 } 2707 // save the difference. (KualiDecimal values are immutable, so we need to redirect the diffAmount pointer to a new one.) 2708 diffAmount = diffAmount.add(penniesFromCSFAmount); 2709 // store the budget construction CSF row with the truncated amount for possible adjustment later 2710 candidateBCSFRows.add(bCSF); 2711 } 2712 2713 public void fixRoundErrors() { 2714 // this routine adjusts the BCSF values so that the total for each EMPLID round to the nearest whole dollar amount 2715 if (!diffAmount.isGreaterThan(KualiDecimal.ZERO)) { 2716 return; 2717 } 2718 KualiDecimal adjustAmount = new KualiDecimal(1); 2719 // no rounding is necessary if the difference is less than a half a buck. 2720 // this will also prevent our accessing an empty array list. 2721 // we should adjust things with only one row if the pennies were >= .5, though. 2722 // 2723 // now we use "banker's rounding" on the adjustment amount 2724 if (diffAmount.multiply(shavePennies).mod(shavePennies).isGreaterEqual(new KualiDecimal(50))) { 2725 diffAmount = diffAmount.add(adjustAmount); 2726 } 2727 if (diffAmount.isLessThan(adjustAmount)) { 2728 return; 2729 } 2730 for (BudgetConstructionCalculatedSalaryFoundationTracker rCSF : candidateBCSFRows) { 2731 KualiInteger fixBCSFAmount = rCSF.getCsfAmount(); 2732 rCSF.setCsfAmount(fixBCSFAmount.add(new KualiInteger(adjustAmount.intValue()))); 2733 diffAmount = diffAmount.subtract(adjustAmount); 2734 if (diffAmount.isLessThan(adjustAmount)) { 2735 break; 2736 } 2737 } 2738 } 2739 } 2740 2741 // 2742 // here are the routines Spring uses to "wire the beans" 2743 // 2744 public void setDocumentService(DocumentService documentService) { 2745 this.documentService = documentService; 2746 } 2747 2748 public void setDateTimeService(DateTimeService dateTimeService) { 2749 this.dateTimeService = dateTimeService; 2750 } 2751 2752 public void setBudgetConstructionHumanResourcesPayrollInterfaceDao(BudgetConstructionHumanResourcesPayrollInterfaceDao budgetConstructionHumanResourcesPayrollInterfaceDao) { 2753 // at IU, some of the tables in Genesis take data from base tables (not budget tables) in the Human Resources/payroll system. 2754 // (specifically, the months appointment in appointment funding is set from the position table, and some budget construction CSF tracker rows are set to vacant based on attributes of the appointment--the incumbent's position does not match the appointment's "grandfathered" attribute.) 2755 // these cases are particular to IU, so are not included in Kuali. but, the idea that budget tables built with this Dao may need to interact with HR/payroll is accommodated with the injection of this service. 2756 this.budgetConstructionHumanResourcesPayrollInterfaceDao = budgetConstructionHumanResourcesPayrollInterfaceDao; 2757 } 2758 2759 public void setWorkflowDocumentService(WorkflowDocumentService workflowDocumentService) { 2760 this.workflowDocumentService = workflowDocumentService; 2761 } 2762 2763 /** 2764 * Sets the kualiModuleService attribute value. 2765 * @param kualiModuleService The kualiModuleService to set. 2766 */ 2767 public void setKualiModuleService(KualiModuleService kualiModuleService) { 2768 this.kualiModuleService = kualiModuleService; 2769 } 2770 2771 2772 }