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.web.struts;
017    
018    import static org.kuali.kfs.module.bc.BCConstants.AppointmentFundingDurationCodes.NONE;
019    
020    import java.math.BigDecimal;
021    import java.util.List;
022    import java.util.Map;
023    
024    import javax.servlet.http.HttpServletRequest;
025    import javax.servlet.http.HttpServletResponse;
026    
027    import org.apache.struts.action.ActionForm;
028    import org.apache.struts.action.ActionForward;
029    import org.apache.struts.action.ActionMapping;
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.BCConstants.LockStatus;
034    import org.kuali.kfs.module.bc.businessobject.BudgetConstructionLockStatus;
035    import org.kuali.kfs.module.bc.businessobject.BudgetConstructionPosition;
036    import org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding;
037    import org.kuali.kfs.module.bc.document.service.LockService;
038    import org.kuali.kfs.module.bc.document.service.SalarySettingService;
039    import org.kuali.kfs.module.bc.service.BudgetConstructionPositionService;
040    import org.kuali.kfs.sys.KFSConstants;
041    import org.kuali.kfs.sys.KFSPropertyConstants;
042    import org.kuali.kfs.sys.context.SpringContext;
043    import org.kuali.rice.kns.service.BusinessObjectService;
044    import org.kuali.rice.kns.util.GlobalVariables;
045    import org.kuali.rice.kns.util.MessageList;
046    import org.kuali.rice.kns.util.MessageMap;
047    
048    /**
049     * the struts action for the salary setting for position
050     */
051    public class PositionSalarySettingAction extends DetailSalarySettingAction {
052        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PositionSalarySettingAction.class);
053    
054        private SalarySettingService salarySettingService = SpringContext.getBean(SalarySettingService.class);
055        private BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class);
056        private BudgetConstructionPositionService budgetConstructionPositionService = SpringContext.getBean(BudgetConstructionPositionService.class);
057    
058        /**
059         * @see org.kuali.kfs.module.bc.document.web.struts.SalarySettingBaseAction#loadExpansionScreen(org.apache.struts.action.ActionMapping,
060         *      org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
061         */
062        @Override
063        public ActionForward loadExpansionScreen(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
064            PositionSalarySettingForm positionSalarySettingForm = (PositionSalarySettingForm) form;
065            MessageMap errorMap;
066            if (positionSalarySettingForm.isBudgetByAccountMode()) {
067                errorMap = positionSalarySettingForm.getCallBackErrors();
068            }
069            else {
070                errorMap = GlobalVariables.getMessageMap();
071            }
072    
073            // update the position record if required
074            ActionForward resyncAction = this.resyncPositionBeforeSalarySetting(mapping, form, request, response);
075            if (resyncAction != null) {
076                return resyncAction;
077            }
078    
079            Map<String, Object> fieldValues = positionSalarySettingForm.getKeyMapOfSalarySettingItem();
080            BudgetConstructionPosition budgetConstructionPosition = (BudgetConstructionPosition) businessObjectService.findByPrimaryKey(BudgetConstructionPosition.class, fieldValues);
081            if (budgetConstructionPosition == null) {
082                String positionNumber = (String) fieldValues.get(KFSPropertyConstants.POSITION_NUMBER);
083                String fiscalYear = (String) fieldValues.get(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR);
084                errorMap.putError(KFSConstants.GLOBAL_MESSAGES, BCKeyConstants.ERROR_POSITION_NOT_FOUND, positionNumber, fiscalYear);
085    
086                if (positionSalarySettingForm.isBudgetByAccountMode()) {
087                    return this.returnToCaller(mapping, form, request, response);
088                }
089                else {
090                    this.cleanupAnySessionForm(mapping, request);
091                    return mapping.findForward(BCConstants.MAPPING_ORGANIZATION_SALARY_SETTING_RETURNING);
092                }
093            }
094    
095            // Lock the position even if there are no current funding lines attached
096            Integer universityFiscalYear = budgetConstructionPosition.getUniversityFiscalYear();
097            String positionNumber = budgetConstructionPosition.getPositionNumber();
098            String principalId = GlobalVariables.getUserSession().getPerson().getPrincipalId();
099    
100            // attempt to lock position
101            BudgetConstructionLockStatus bcLockStatus = SpringContext.getBean(LockService.class).lockPosition(positionNumber, universityFiscalYear, principalId);
102            if (!bcLockStatus.getLockStatus().equals(BCConstants.LockStatus.SUCCESS)) {
103                errorMap.putError(KFSConstants.GLOBAL_ERRORS, BCKeyConstants.ERROR_FAIL_TO_LOCK_POSITION, budgetConstructionPosition.toString());
104                if (positionSalarySettingForm.isBudgetByAccountMode()) {
105                    return this.returnToCaller(mapping, form, request, response);
106                }
107                else {
108                    this.cleanupAnySessionForm(mapping, request);
109                    return mapping.findForward(BCConstants.MAPPING_ORGANIZATION_SALARY_SETTING_RETURNING);
110                }
111            }
112    
113    
114            positionSalarySettingForm.setBudgetConstructionPosition(budgetConstructionPosition);
115            if (positionSalarySettingForm.isSingleAccountMode()) {
116                positionSalarySettingForm.pickAppointmentFundingsForSingleAccount();
117            }
118    
119            // acquire position and funding locks for the associated funding lines
120            if (!positionSalarySettingForm.isViewOnlyEntry()) {
121                positionSalarySettingForm.postProcessBCAFLines();
122                positionSalarySettingForm.setNewBCAFLine(positionSalarySettingForm.createNewAppointmentFundingLine());
123    
124                boolean accessModeUpdated = positionSalarySettingForm.updateAccessMode(errorMap);
125                if (!accessModeUpdated) {
126                    this.unlockPositionOnly(positionSalarySettingForm);
127                    if (positionSalarySettingForm.isBudgetByAccountMode()) {
128                        return this.returnToCaller(mapping, form, request, response);
129                    }
130                    else {
131                        this.cleanupAnySessionForm(mapping, request);
132                        return mapping.findForward(BCConstants.MAPPING_ORGANIZATION_SALARY_SETTING_RETURNING);
133                    }
134                }
135    
136                boolean gotLocks = positionSalarySettingForm.acquirePositionAndFundingLocks(errorMap);
137                if (!gotLocks) {
138                    this.unlockPositionOnly(positionSalarySettingForm);
139                    if (positionSalarySettingForm.isBudgetByAccountMode()) {
140                        return this.returnToCaller(mapping, form, request, response);
141                    }
142                    else {
143                        this.cleanupAnySessionForm(mapping, request);
144                        return mapping.findForward(BCConstants.MAPPING_ORGANIZATION_SALARY_SETTING_RETURNING);
145                    }
146                }
147            }
148    
149            return mapping.findForward(KFSConstants.MAPPING_BASIC);
150        }
151    
152        /**
153         * enable to send warning after saving
154         * 
155         * @see org.kuali.kfs.module.bc.document.web.struts.DetailSalarySettingAction#save(org.apache.struts.action.ActionMapping,
156         *      org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
157         */
158        @Override
159        public ActionForward save(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
160            ActionForward saveAction = super.save(mapping, form, request, response);
161    
162            PositionSalarySettingForm positionSalarySettingForm = (PositionSalarySettingForm) form;
163            this.sendWarnings(positionSalarySettingForm, GlobalVariables.getMessageList());
164    
165            return saveAction;
166        }
167    
168        /**
169         * send warning messsages back to the caller
170         * 
171         * @param positionSalarySettingForm the given position salary setting form
172         * @param warnings the warning list that can hold the warning messages if any
173         */
174        public void sendWarnings(PositionSalarySettingForm positionSalarySettingForm, MessageList warnings) {
175            List<PendingBudgetConstructionAppointmentFunding> activeAppointmentFundings = positionSalarySettingForm.getActiveFundingLines();
176            if (activeAppointmentFundings == null || activeAppointmentFundings.isEmpty()) {
177                return;
178            }
179    
180            boolean hasFundingLineInvolveLeave = this.hasFundingLineInvolvedLeave(activeAppointmentFundings);
181            BudgetConstructionPosition budgetConstructionPosition = positionSalarySettingForm.getBudgetConstructionPosition();
182    
183            BigDecimal positionFte = budgetConstructionPosition.getPositionFullTimeEquivalency().setScale(2, BigDecimal.ROUND_HALF_UP);
184            BigDecimal requestedFteQuantityTotal = positionSalarySettingForm.getAppointmentRequestedFteQuantityTotal().setScale(2, BigDecimal.ROUND_HALF_UP);
185            if (!hasFundingLineInvolveLeave && requestedFteQuantityTotal.compareTo(positionFte) != 0) {
186                warnings.add(BCKeyConstants.WARNING_FTE_NOT_EQUAL);
187            }
188    
189            BigDecimal positionStandardHours = budgetConstructionPosition.getPositionStandardHoursDefault().setScale(2, BigDecimal.ROUND_HALF_UP);
190            BigDecimal requestedStandardHoursTotal = positionSalarySettingForm.getAppointmentRequestedStandardHoursTotal().setScale(2, BigDecimal.ROUND_HALF_UP);
191            if (!hasFundingLineInvolveLeave && requestedStandardHoursTotal.compareTo(positionStandardHours) != 0) {
192                warnings.add(BCKeyConstants.WARNING_WORKING_HOUR_NOT_EQUAL);
193            }
194    
195            if (positionSalarySettingForm.isPendingPositionSalaryChange()) {
196                warnings.add(BCKeyConstants.WARNING_RECALCULATE_NEEDED);
197            }
198        }
199    
200        /**
201         * Recalculates all rows where the position change flags are set and the row is edit-able and active. Sets funding months, FTE,
202         * CSF FTE, and normalizes biweekly request amounts, where appropriate This action is called from the global calculate button.
203         * 
204         * @param mapping
205         * @param form
206         * @param request
207         * @param response
208         * @return
209         * @throws Exception
210         */
211        public ActionForward recalculateAllSalarySettingLines(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
212            PositionSalarySettingForm positionSalarySettingForm = (PositionSalarySettingForm) form;
213    
214            List<PendingBudgetConstructionAppointmentFunding> appointmentFundings = positionSalarySettingForm.getActiveFundingLines();
215            for (PendingBudgetConstructionAppointmentFunding appointmentFunding : appointmentFundings) {
216                if (!appointmentFunding.isDisplayOnlyMode()) {
217                    this.recalculateSalarySettingLine(appointmentFunding);
218                }
219            }
220            return mapping.findForward(KFSConstants.MAPPING_BASIC);
221        }
222    
223        /**
224         * Recalculates a single row where the position change flags are set and the row is edit-able and active. Sets funding months,
225         * FTE, CSF FTE, and normalizes biweekly request amounts, where appropriate This action is called from the row action calculate
226         * button.
227         * 
228         * @param mapping
229         * @param form
230         * @param request
231         * @param response
232         * @return
233         * @throws Exception
234         */
235        public ActionForward recalculateSalarySettingLine(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
236            PositionSalarySettingForm positionSalarySettingForm = (PositionSalarySettingForm) form;
237            PendingBudgetConstructionAppointmentFunding appointmentFunding = this.getSelectedFundingLine(request, positionSalarySettingForm);
238    
239            this.recalculateSalarySettingLine(appointmentFunding);
240    
241            return mapping.findForward(KFSConstants.MAPPING_BASIC);
242        }
243    
244        /**
245         * Recalculates a PendingBudgetConstructionAppointmentFunding. Sets funding months, FTE, CSF FTE, and normalizes biweekly
246         * request amounts, where appropriate
247         * 
248         * @param appointmentFunding
249         */
250        protected void recalculateSalarySettingLine(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
251    
252            // do the recalc on the passed line and reset the indicator
253            // there are no rule checks involved in this operation
254            if (appointmentFunding.isPositionSalaryChangeIndicator()) {
255                appointmentFunding.setPositionSalaryChangeIndicator(Boolean.FALSE);
256    
257                // check if months needs reset
258                if (appointmentFunding.getAppointmentFundingDurationCode().equals(NONE.durationCode)) {
259                    if (!appointmentFunding.getAppointmentFundingMonth().equals(appointmentFunding.getBudgetConstructionPosition().getIuNormalWorkMonths())) {
260                        appointmentFunding.setAppointmentFundingMonth(appointmentFunding.getBudgetConstructionPosition().getIuNormalWorkMonths());
261                    }
262                }
263    
264                // recalc request fte and if hourly, normalize request amount and hourly rate
265                salarySettingService.recalculateDerivedInformation(appointmentFunding);
266    
267            }
268    
269    
270            // doing this just to do cleanup of the default object change flag
271            // the rules will force the user to mark the line delete
272            // if the default object doesn't match with the line object when saving
273            if (appointmentFunding.isPositionObjectChangeIndicator()) {
274                appointmentFunding.setPositionObjectChangeIndicator(Boolean.FALSE);
275            }
276    
277        }
278    
279        /**
280         * @see org.kuali.kfs.module.bc.document.web.struts.SalarySettingBaseAction#getFundingAwareObjectName()
281         */
282        @Override
283        protected String getFundingAwareObjectName() {
284            return BCPropertyConstants.BUDGET_CONSTRUCTION_POSITION;
285        }
286    
287        /**
288         * resyn position brefore performing salary setting
289         */
290        private ActionForward resyncPositionBeforeSalarySetting(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
291            PositionSalarySettingForm positionSalarySettingForm = (PositionSalarySettingForm) form;
292    
293            if (!positionSalarySettingForm.isRefreshPositionBeforeSalarySetting()) {
294                return null;
295            }
296    
297            Integer universityFiscalYear = positionSalarySettingForm.getUniversityFiscalYear();
298            String positionNumber = positionSalarySettingForm.getPositionNumber();
299            String principalId = GlobalVariables.getUserSession().getPerson().getPrincipalId();
300    
301            // attempt to lock position and associated funding
302            BudgetConstructionLockStatus bcLockStatus = SpringContext.getBean(LockService.class).lockPositionAndActiveFunding(universityFiscalYear, positionNumber, principalId);
303            if (!bcLockStatus.getLockStatus().equals(BCConstants.LockStatus.SUCCESS)) {
304                GlobalVariables.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, BCKeyConstants.ERROR_POSITION_LOCK_NOT_OBTAINED, new String[] { universityFiscalYear.toString(), positionNumber });
305                return this.returnToCaller(mapping, form, request, response);
306            }
307    
308            try {
309                budgetConstructionPositionService.refreshPositionFromExternal(universityFiscalYear, positionNumber);
310            }
311            finally {
312                // release locks
313                LockStatus lockStatus = SpringContext.getBean(LockService.class).unlockPositionAndActiveFunding(universityFiscalYear, positionNumber, principalId);
314                if (!lockStatus.equals(BCConstants.LockStatus.SUCCESS)) {
315                    LOG.error(String.format("unable to unlock position and active funding records: %s, %s, %s", universityFiscalYear, positionNumber, principalId));
316                    throw new RuntimeException(String.format("unable to unlock position and active funding records: %s, %s, %s", universityFiscalYear, positionNumber, principalId));
317                }
318            }
319    
320            return null;
321        }
322    }