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.document.service.impl;
017    
018    import java.io.BufferedReader;
019    import java.io.ByteArrayOutputStream;
020    import java.io.InputStream;
021    import java.io.InputStreamReader;
022    import java.math.BigDecimal;
023    import java.text.MessageFormat;
024    import java.util.ArrayList;
025    import java.util.Arrays;
026    import java.util.HashMap;
027    import java.util.List;
028    import java.util.Map;
029    
030    import org.kuali.kfs.module.bc.BCConstants;
031    import org.kuali.kfs.module.bc.BCKeyConstants;
032    import org.kuali.kfs.module.bc.BCPropertyConstants;
033    import org.kuali.kfs.module.bc.businessobject.BudgetConstructionHeader;
034    import org.kuali.kfs.module.bc.businessobject.BudgetConstructionLockStatus;
035    import org.kuali.kfs.module.bc.businessobject.BudgetConstructionPayRateHolding;
036    import org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding;
037    import org.kuali.kfs.module.bc.document.dataaccess.PayrateImportDao;
038    import org.kuali.kfs.module.bc.document.service.BudgetDocumentService;
039    import org.kuali.kfs.module.bc.document.service.LockService;
040    import org.kuali.kfs.module.bc.document.service.PayrateImportService;
041    import org.kuali.kfs.module.bc.exception.BudgetConstructionLockUnavailableException;
042    import org.kuali.kfs.module.bc.util.BudgetParameterFinder;
043    import org.kuali.kfs.module.bc.util.ExternalizedMessageWrapper;
044    import org.kuali.kfs.sys.KFSConstants;
045    import org.kuali.kfs.sys.KFSPropertyConstants;
046    import org.kuali.kfs.sys.ObjectUtil;
047    import org.kuali.kfs.sys.context.SpringContext;
048    import org.kuali.kfs.sys.service.NonTransactional;
049    import org.kuali.kfs.sys.service.OptionsService;
050    import org.kuali.rice.kim.bo.Person;
051    import org.kuali.rice.kns.service.BusinessObjectService;
052    import org.kuali.rice.kns.service.KualiConfigurationService;
053    import org.kuali.rice.kns.util.KualiInteger;
054    import org.springframework.transaction.PlatformTransactionManager;
055    import org.springframework.transaction.TransactionStatus;
056    import org.springframework.transaction.annotation.Transactional;
057    import org.springframework.transaction.support.DefaultTransactionDefinition;
058    
059    import com.lowagie.text.Document;
060    import com.lowagie.text.DocumentException;
061    import com.lowagie.text.Paragraph;
062    import com.lowagie.text.pdf.PdfWriter;
063    
064    public class PayrateImportServiceImpl implements PayrateImportService {
065        
066        private BusinessObjectService businessObjectService;
067        private LockService lockService;
068        private int importCount;
069        private int updateCount;
070        private OptionsService optionsService;
071        private PayrateImportDao payrateImportDao;
072        private BudgetDocumentService budgetDocumentService;
073        
074        /**
075         * 
076         * @see org.kuali.kfs.module.bc.service.PayrateImportService#importFile(java.io.InputStream)
077         */
078        @Transactional
079        public boolean importFile(InputStream fileImportStream, List<ExternalizedMessageWrapper> messageList, String principalId) {
080            Map payRateHoldingPersonUniversalIdentifierKey = new HashMap();
081            payRateHoldingPersonUniversalIdentifierKey.put(KFSPropertyConstants.PERSON_UNIVERSAL_IDENTIFIER, principalId);
082            
083            this.businessObjectService.deleteMatching(BudgetConstructionPayRateHolding.class, payRateHoldingPersonUniversalIdentifierKey);
084            
085            BufferedReader fileReader = new BufferedReader(new InputStreamReader(fileImportStream));
086            this.importCount = 0;
087            
088            try {
089                while(fileReader.ready()) {
090                    BudgetConstructionPayRateHolding budgetConstructionPayRateHolding = new BudgetConstructionPayRateHolding();
091                    String line = fileReader.readLine();
092                    ObjectUtil.convertLineToBusinessObject(budgetConstructionPayRateHolding, line, DefaultImportFileFormat.fieldLengths, Arrays.asList(DefaultImportFileFormat.fieldNames));
093                    budgetConstructionPayRateHolding.setPrincipalId(principalId);
094                    budgetConstructionPayRateHolding.setAppointmentRequestedPayRate(budgetConstructionPayRateHolding.getAppointmentRequestedPayRate().movePointLeft(2));
095                    businessObjectService.save(budgetConstructionPayRateHolding);
096                    this.importCount++;
097                }
098            }
099            catch (Exception e) {
100                this.businessObjectService.deleteMatching(BudgetConstructionPayRateHolding.class, payRateHoldingPersonUniversalIdentifierKey);
101                messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.ERROR_PAYRATE_IMPORT_ABORTED));
102                
103                return false;
104            }
105            
106            if (importCount == 0 ) messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.MSG_PAYRATE_IMPORT_NO_IMPORT_RECORDS));
107            else  messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.MSG_PAYRATE_IMPORT_COUNT, String.valueOf(importCount)));
108            
109            return true;
110        }
111        
112        /**
113         * 
114         * @see org.kuali.kfs.module.bc.service.PayrateImportService#update()
115         */
116        @Transactional
117        public void update(Integer budgetYear, Person user, List<ExternalizedMessageWrapper> messageList, String principalId) {
118            List<PendingBudgetConstructionAppointmentFunding> lockedFundingRecords = new ArrayList<PendingBudgetConstructionAppointmentFunding>();
119            boolean updateContainsErrors = false;
120            this.updateCount = 0;
121            
122            Map payRateHoldingPersonUniversalIdentifierKey = new HashMap();
123            payRateHoldingPersonUniversalIdentifierKey.put(KFSPropertyConstants.PERSON_UNIVERSAL_IDENTIFIER, principalId);
124            List<BudgetConstructionPayRateHolding> records = (List<BudgetConstructionPayRateHolding>) this.businessObjectService.findMatching(BudgetConstructionPayRateHolding.class, payRateHoldingPersonUniversalIdentifierKey);
125            
126            if ( !getPayrateLock(lockedFundingRecords, messageList, budgetYear, user, records) ) {
127                messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.ERROR_PAYRATE_UPDATE_ABORTED, String.valueOf(this.updateCount)));
128                doRollback();
129                return;
130            }
131    
132            List<String> biweeklyPayObjectCodes = BudgetParameterFinder.getBiweeklyPayObjectCodes();
133            for (BudgetConstructionPayRateHolding holdingRecord : records) {
134                if (holdingRecord.getAppointmentRequestedPayRate().equals( -1.0)) {
135                    messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.ERROR_PAYRATE_IMPORT_NO_PAYROLL_MATCH, holdingRecord.getEmplid(), holdingRecord.getPositionNumber()));
136                    updateContainsErrors = true;
137                    continue;
138                } 
139                
140                List<PendingBudgetConstructionAppointmentFunding> fundingRecords = this.payrateImportDao.getFundingRecords(holdingRecord, budgetYear, biweeklyPayObjectCodes);
141                if (fundingRecords.isEmpty()) {
142                    messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.ERROR_PAYRATE_NO_ACTIVE_FUNDING_RECORDS, holdingRecord.getEmplid(), holdingRecord.getPositionNumber()));
143                    updateContainsErrors = true;
144                    continue;
145                }
146                
147                for (PendingBudgetConstructionAppointmentFunding fundingRecord : fundingRecords) {
148                    
149                    if ( !fundingRecord.getAppointmentRequestedPayRate().equals(holdingRecord.getAppointmentRequestedPayRate()) ) {
150                        if (fundingRecord.getAppointmentRequestedFteQuantity().equals(0) || fundingRecord.getAppointmentRequestedFteQuantity() == null) {
151                            messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.ERROR_PAYRATE_NO_UPDATE_FTE_ZERO_OR_BLANK, holdingRecord.getEmplid(), holdingRecord.getPositionNumber(), fundingRecord.getChartOfAccountsCode(), fundingRecord.getAccountNumber(), fundingRecord.getSubAccountNumber()));
152                            updateContainsErrors = true;
153                            continue;
154                        } 
155                        
156                        BigDecimal temp1 = fundingRecord.getAppointmentRequestedTimePercent().divide(new BigDecimal(100));
157                        BigDecimal temp2 = new BigDecimal((fundingRecord.getAppointmentFundingMonth()/fundingRecord.getBudgetConstructionPosition().getIuPayMonths()) * BudgetParameterFinder.getAnnualWorkingHours());
158                        
159                        KualiInteger annualAmount = new KualiInteger(holdingRecord.getAppointmentRequestedPayRate().multiply(temp1.multiply(temp2)));
160                        KualiInteger updateAmount = annualAmount.subtract(fundingRecord.getAppointmentRequestedAmount());
161                        
162                        BudgetConstructionHeader header = budgetDocumentService.getBudgetConstructionHeader(fundingRecord);
163                        if (header == null ) {
164                            messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.ERROR_PAYRATE_NO_BUDGET_DOCUMENT, budgetYear.toString(), fundingRecord.getChartOfAccountsCode(), fundingRecord.getAccountNumber(), fundingRecord.getSubAccountNumber()));
165                            messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.ERROR_PAYRATE_OBJECT_LEVEL_ERROR, fundingRecord.getEmplid(), fundingRecord.getPositionNumber(), fundingRecord.getChartOfAccountsCode(), fundingRecord.getAccountNumber(), fundingRecord.getSubAccountNumber()));
166                            updateContainsErrors = true;
167                            continue;
168                        }
169                        
170                        // update or create pending budget GL record and  plug line
171                        budgetDocumentService.updatePendingBudgetGeneralLedger(fundingRecord, updateAmount);
172                        if (updateAmount.isNonZero()) {
173                            budgetDocumentService.updatePendingBudgetGeneralLedgerPlug(fundingRecord, updateAmount.negated());
174                        }
175                        
176                        fundingRecord.setAppointmentRequestedPayRate(holdingRecord.getAppointmentRequestedPayRate());
177                        fundingRecord.setAppointmentRequestedAmount(annualAmount);
178                        this.businessObjectService.save(fundingRecord);
179                    }
180                }
181                this.updateCount ++;
182            }
183            
184            messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.MSG_PAYRATE_IMPORT_UPDATE_COMPLETE, String.valueOf(updateCount)));
185            
186            for (PendingBudgetConstructionAppointmentFunding recordToUnlock : lockedFundingRecords) {
187                this.lockService.unlockAccount(budgetDocumentService.getBudgetConstructionHeader(recordToUnlock));
188            }
189        }
190        
191        /**
192         * 
193         * @see org.kuali.kfs.module.bc.service.PayrateImportService#generatePdf(java.lang.StringBuilder, java.io.ByteArrayOutputStream)
194         */
195        @NonTransactional
196        public void generatePdf(List<ExternalizedMessageWrapper> logMessages, ByteArrayOutputStream baos) throws DocumentException {
197            Document document = new Document();
198            PdfWriter.getInstance(document, baos);
199            document.open();
200            for (ExternalizedMessageWrapper messageWrapper : logMessages) {
201                String message;
202                if (messageWrapper.getParams().length == 0 ) message = SpringContext.getBean(KualiConfigurationService.class).getPropertyString(messageWrapper.getMessageKey());
203                else {
204                    String temp = SpringContext.getBean(KualiConfigurationService.class).getPropertyString(messageWrapper.getMessageKey());
205                    message = MessageFormat.format(temp, (Object[])messageWrapper.getParams());
206                }
207                document.add(new Paragraph(message));
208            }
209    
210            document.close();
211        }
212        
213        /**
214         * Sets the business object service
215         * 
216         * @param businessObjectService
217         */
218        @NonTransactional
219        public void setBusinessObjectService(BusinessObjectService businessObjectService) {
220            this.businessObjectService = businessObjectService;
221        }
222        
223        /**
224         * sets lock service
225         * 
226         * @param lockService
227         */
228        @NonTransactional
229        public void setLockService(LockService lockService) {
230            this.lockService = lockService;
231        }
232        
233        /**
234         * sets option service
235         * 
236         * @param optionsService
237         */
238        @NonTransactional
239        public void setOptionsService(OptionsService optionsService) {
240            this.optionsService = optionsService;
241        }
242        
243        /**
244         * sets payrate import dao
245         * 
246         * @param payrateImportDao
247         */
248        @NonTransactional
249        public void setPayrateImportDao(PayrateImportDao payrateImportDao) {
250            this.payrateImportDao = payrateImportDao;
251        }
252    
253        /**
254         * Sets the budgetDocumentService attribute value.
255         * @param budgetDocumentService The budgetDocumentService to set.
256         */
257        @NonTransactional
258        public void setBudgetDocumentService(BudgetDocumentService budgetDocumentService) {
259            this.budgetDocumentService = budgetDocumentService;
260        }
261            
262        /**
263         * Creates the locking key to use in retrieving account locks
264         * 
265         * @param record
266         * @return
267         */
268        protected String getLockingKeyString(PendingBudgetConstructionAppointmentFunding record) {
269            return record.getUniversityFiscalYear() + "-" + record.getChartOfAccountsCode() + "-" + record.getAccountNumber() + "-" + record.getSubAccountNumber();
270        }
271        
272        /**
273         * Retrieves Account locks for payrate import records
274         * 
275         * @param lockMap
276         * @param messageList
277         * @param budgetYear
278         * @param user
279         * @param records
280         * @return
281         */
282        @Transactional
283        protected boolean getPayrateLock(List<PendingBudgetConstructionAppointmentFunding> lockedRecords, List<ExternalizedMessageWrapper> messageList, Integer budgetYear, Person user, List<BudgetConstructionPayRateHolding> records) {
284            List<String> biweeklyPayObjectCodes = BudgetParameterFinder.getBiweeklyPayObjectCodes();
285            
286            for (BudgetConstructionPayRateHolding record: records) {
287                List<PendingBudgetConstructionAppointmentFunding> fundingRecords = this.payrateImportDao.getFundingRecords(record, budgetYear, biweeklyPayObjectCodes);
288                try {
289                    lockedRecords.addAll(this.lockService.lockPendingBudgetConstructionAppointmentFundingRecords(fundingRecords, user));
290                } catch(BudgetConstructionLockUnavailableException e) {
291                    BudgetConstructionLockStatus lockStatus = e.getLockStatus();
292                    if ( lockStatus.getLockStatus().equals(BCConstants.LockStatus.BY_OTHER) ) {
293                        messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.ERROR_PAYRATE_ACCOUNT_LOCK_EXISTS));
294                        
295                        return false;
296                    } else if ( lockStatus.getLockStatus().equals(BCConstants.LockStatus.FLOCK_FOUND) ) {
297                        messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.ERROR_PAYRATE_FUNDING_LOCK_EXISTS));
298                        
299                        return false;
300                    } else if ( !lockStatus.getLockStatus().equals(BCConstants.LockStatus.SUCCESS) ) {
301                        messageList.add(new ExternalizedMessageWrapper(BCKeyConstants.ERROR_PAYRATE_BATCH_ACCOUNT_LOCK_FAILED));
302                        return false;
303                    }
304              }
305            
306            }
307            
308            return true;
309        }
310        
311        /**
312         * File format for payrate import file
313         * 
314         */
315        protected static class DefaultImportFileFormat {
316            private static final int[] fieldLengths = new int[] {11, 8, 50, 5, 4, 3, 3, 10, 8};
317            private static final String[] fieldNames = new String[] {KFSPropertyConstants.EMPLID, KFSPropertyConstants.POSITION_NUMBER, KFSPropertyConstants.PERSON_NAME, BCPropertyConstants.SET_SALARY_ID, BCPropertyConstants.SALARY_ADMINISTRATION_PLAN, BCPropertyConstants.GRADE, "unionCode", BCPropertyConstants.APPOINTMENT_REQUESTED_PAY_RATE, BCPropertyConstants.CSF_FREEZE_DATE};
318        }
319        
320        /**
321         * If retrieving budget locks fails, this method rolls back previous changes
322         * 
323         */
324        protected void doRollback() {
325            PlatformTransactionManager transactionManager = SpringContext.getBean(PlatformTransactionManager.class);
326            DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
327            TransactionStatus transactionStatus = transactionManager.getTransaction(defaultTransactionDefinition);
328            transactionManager.rollback( transactionStatus );
329    
330        }
331    
332    }
333