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    }