001    /**
002     * Copyright 2004-2013 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.hr.time.earncode.service;
017    
018    import java.math.BigDecimal;
019    import java.sql.Date;
020    import java.util.ArrayList;
021    import java.util.Comparator;
022    import java.util.LinkedHashMap;
023    import java.util.LinkedList;
024    import java.util.List;
025    import java.util.Map;
026    import java.util.Set;
027    
028    import org.apache.commons.lang.StringUtils;
029    import org.kuali.hr.job.Job;
030    import org.kuali.hr.lm.LMConstants;
031    import org.kuali.hr.lm.accrual.AccrualCategory;
032    import org.kuali.hr.lm.earncodesec.EarnCodeSecurity;
033    import org.kuali.hr.lm.earncodesec.EarnCodeType;
034    import org.kuali.hr.time.assignment.Assignment;
035    import org.kuali.hr.time.collection.rule.TimeCollectionRule;
036    import org.kuali.hr.time.earncode.EarnCode;
037    import org.kuali.hr.time.earncode.dao.EarnCodeDao;
038    import org.kuali.hr.time.principal.PrincipalHRAttributes;
039    import org.kuali.hr.time.roles.TkUserRoles;
040    import org.kuali.hr.time.service.base.TkServiceLocator;
041    import org.kuali.hr.time.util.TKContext;
042    import org.kuali.hr.time.util.TKUser;
043    import org.kuali.hr.time.util.TKUtils;
044    import org.kuali.hr.time.util.TkConstants;
045    import org.kuali.hr.time.workarea.WorkArea;
046    import org.kuali.rice.krad.util.GlobalVariables;
047    
048    import com.google.common.collect.Ordering;
049    
050    public class EarnCodeServiceImpl implements EarnCodeService {
051    
052            private EarnCodeDao earnCodeDao;
053    
054            public void setEarnCodeDao(EarnCodeDao earnCodeDao) {
055                    this.earnCodeDao = earnCodeDao;
056            }
057    
058        public List<EarnCode> getEarnCodesForLeaveAndTime(Assignment a, Date asOfDate, boolean isLeavePlanningCalendar) {
059            //  This method combining both leave calendar and timesheet calendar earn codes may never be used, but it is available.
060            //  It was specified in kpme-1745, "Implement getEarnCodesForLeaveAndTime and call both of the above methods and return in one collection."
061            List<EarnCode> earnCodes = getEarnCodesForTime(a, asOfDate);
062            List<EarnCode> leaveEarnCodes = getEarnCodesForLeave(a, asOfDate, isLeavePlanningCalendar);
063            //  the following list processing does work as hoped, comparing the objects' data, rather than their references to memory structures.
064            earnCodes.removeAll(leaveEarnCodes); //ensures no overlap during the addAll
065            earnCodes.addAll(leaveEarnCodes);
066    
067            return earnCodes;
068        }
069    
070        public List<EarnCode> getEarnCodesForTime(Assignment a, Date asOfDate, boolean includeRegularEarnCode) {
071            //getEarnCodesForTime and getEarnCodesForLeave have some overlapping logic, but they were separated so that they could follow their own distinct logic, so consolidation of logic is not desirable.
072    
073            if (a == null) throw new RuntimeException("No assignment parameter.");
074            Job job = a.getJob();
075            if (job == null || job.getPayTypeObj() == null) throw new RuntimeException("Null job or null job pay type on assignment.");
076    
077            List<EarnCode> earnCodes = new LinkedList<EarnCode>();
078            String earnTypeCode = EarnCodeType.TIME.getCode();
079    
080            TimeCollectionRule tcr = a.getTimeCollectionRule();
081    
082            boolean isClockUser = tcr == null || tcr.isClockUserFl();
083            boolean isUsersTimesheet = StringUtils.equals(TKContext.getPrincipalId(),a.getPrincipalId());
084    
085            // Reg earn codes will typically not be defined in the earn code security table
086            EarnCode regularEarnCode = getEarnCode(job.getPayTypeObj().getRegEarnCode(), asOfDate);
087            if (regularEarnCode == null) {
088                throw new RuntimeException("No regular earn code defined for job pay type.");
089            } else {
090                //  if you are a clock user and this is your timesheet and you are processing the reg earn code, do not add this earn code. Use the clock in/out mechanism.
091                if (!isClockUser || !isUsersTimesheet || includeRegularEarnCode ) {
092                    earnCodes.add(regularEarnCode);
093                }
094            }
095    
096            List<String> listAccrualCategories = new LinkedList<String>();
097            String accrualCategory;
098    
099            //  first make a list of the accrual categories available to the user's Leave Plan (yes, leave plan), for later comparison.
100            PrincipalHRAttributes principalHRAttributes = TkServiceLocator.getPrincipalHRAttributeService().getPrincipalCalendar(job.getPrincipalId(), asOfDate);
101            boolean fmlaEligible = principalHRAttributes.isFmlaEligible();
102            boolean workersCompEligible = principalHRAttributes.isWorkersCompEligible();
103    
104            String leavePlan = principalHRAttributes.getLeavePlan();
105            if (leavePlan != null) {
106                for (AccrualCategory accrualCategories : TkServiceLocator.getAccrualCategoryService().getActiveAccrualCategoriesForLeavePlan(leavePlan, asOfDate)) {
107                    accrualCategory = accrualCategories.getAccrualCategory();
108                    if(accrualCategory != null) {
109                        listAccrualCategories.add(accrualCategory);
110                    }
111                }
112            }
113    
114            //  get all earn codes by user security, then we'll filter on accrual category first as we process them.
115            List<EarnCodeSecurity> decs = TkServiceLocator.getEarnCodeSecurityService().getEarnCodeSecurities(job.getDept(), job.getHrSalGroup(), job.getLocation(), asOfDate);
116            for (EarnCodeSecurity dec : decs) {
117    
118                boolean addEarnCode = addEarnCodeBasedOnEmployeeApproverSettings(dec, a, asOfDate);
119                if (addEarnCode) {
120    
121                    //  allow types Time AND Both
122                    if (earnTypeCode.equals(dec.getEarnCodeType()) || EarnCodeType.BOTH.getCode().equals(dec.getEarnCodeType())) {
123                        EarnCode ec = getEarnCode(dec.getEarnCode(), asOfDate);
124    
125                        //  make sure we got something back from the earn code dao
126                        if (ec != null) {
127    
128                            //  if the user's fmla flag is Yes, that means we are not restricting codes based on this flag, so any code is shown.
129                            //    if the fmla flag on a code is yes they can see it.    (allow)
130                            //    if the fmla flag on a code is no they should see it.  (allow)
131                            //  if the user's fmla flag is No,
132                            //    they can see any codes which are fmla=no.             (allow)
133                            //    they can not see codes with fmla=yes.                 (exclude earn code)
134                            //  the fmla earn codes=no do not require any exclusion
135                            //  the only action required is if the fmla user flag=no: exclude those codes with fmla=yes.
136    
137                            if ( (fmlaEligible || ec.getFmla().equals("N")) ) {
138                                if (ec.getAccrualCategory() == null
139                                            || (listAccrualCategories.contains(ec.getAccrualCategory()) 
140                                                            && LMConstants.ACCRUAL_BALANCE_ACTION.USAGE.equals(ec.getAccrualBalanceAction()))) {
141                                    // go on, we are allowing these three combinations: YY, YN, NN
142    
143                                    //  apply the same logic as FMLA to the Worker Compensation flags.
144                                    if ( (workersCompEligible || ec.getWorkmansComp().equals("N")) ) {
145                                        // go on, we are allowing these three combinations: YY, YN, NN.
146    
147                                        //  determine if the holiday earn code should be displayed.
148                                        if ( showEarnCodeIfHoliday(ec, dec) ) {
149                                            //  non-Holiday earn code will go on, Holiday earn code must meet some requirements in the method.
150                                            if ( !StringUtils.equals(regularEarnCode.toString(), dec.getEarnCode()) ) {
151                                                //  add earn code if it is not the reg earn code.
152                                                earnCodes.add(ec);
153                                            }
154                                        }
155                                    }
156                                }
157                            }
158                        }
159                    }
160                }
161            }
162    
163            return earnCodes;
164        }
165    
166        public List<EarnCode> getEarnCodesForTime(Assignment a, Date asOfDate) {
167            return getEarnCodesForTime(a, asOfDate, false);
168        }
169    
170        public List<EarnCode> getEarnCodesForLeave(Assignment a, Date asOfDate, boolean isLeavePlanningCalendar) {
171            //getEarnCodesForTime and getEarnCodesForLeave have some overlapping logic, but they were separated so that they could follow their own distinct logic, so consolidation of logic is not desirable.
172    
173            if (a == null) throw new RuntimeException("No assignment parameter.");
174            Job job = a.getJob();
175            if (job == null || job.getPayTypeObj() == null) throw new RuntimeException("Null job or null job pay type on assignment.");
176    
177            List<EarnCode> earnCodes = new LinkedList<EarnCode>();
178            String earnTypeCode = EarnCodeType.LEAVE.getCode();
179            // skip getting the regular earn code for Leave Calendar
180    
181            List<String> listAccrualCategories = new LinkedList<String>();
182            String accrualCategory;
183    
184            //  first make a list of the accrual categories available to the user's leave plan, for later comparison.
185            PrincipalHRAttributes principalHRAttributes = TkServiceLocator.getPrincipalHRAttributeService().getPrincipalCalendar(job.getPrincipalId(), asOfDate);
186            boolean fmlaEligible = principalHRAttributes.isFmlaEligible();
187            boolean workersCompEligible = principalHRAttributes.isWorkersCompEligible();
188    
189            String leavePlan = principalHRAttributes.getLeavePlan();
190            if (leavePlan != null) {
191                for (AccrualCategory accrualCategories : TkServiceLocator.getAccrualCategoryService().getActiveAccrualCategoriesForLeavePlan(leavePlan, asOfDate)) {
192                    accrualCategory = accrualCategories.getAccrualCategory();
193                    if(accrualCategory != null) {
194                        listAccrualCategories.add(accrualCategory);
195                    }
196                }
197            }
198    
199            //  get all earn codes by user security, then we'll filter on accrual category first as we process them.
200            List<EarnCodeSecurity> decs = TkServiceLocator.getEarnCodeSecurityService().getEarnCodeSecurities(job.getDept(), job.getHrSalGroup(), job.getLocation(), asOfDate);
201            for (EarnCodeSecurity dec : decs) {
202    
203                boolean addEarnCode = addEarnCodeBasedOnEmployeeApproverSettings(dec, a, asOfDate);
204                if (addEarnCode) {
205    
206                    //  allow types Leave AND Both
207                    if (earnTypeCode.equals(dec.getEarnCodeType()) || EarnCodeType.BOTH.getCode().equals(dec.getEarnCodeType())) {
208                        EarnCode ec = getEarnCode(dec.getEarnCode(), asOfDate);
209    
210                        //  make sure we got something back from the earn code dao
211                        if (ec != null) {
212    
213                            //  if the user's fmla flag is Yes, that means we are not restricting codes based on this flag, so any code is shown.
214                            //    if the fmla flag on a code is yes they can see it.    (allow)
215                            //    if the fmla flag on a code is no they should see it.  (allow)
216                            //  if the user's fmla flag is No,
217                            //    they can see any codes which are fmla=no.             (allow)
218                            //    they can not see codes with fmla=yes.                 (exclude earn code)
219                            //  the fmla earn codes=no do not require any exclusion
220                            //  the only action required is if the fmla user flag=no: exclude those codes with fmla=yes.
221    
222                            if ( (fmlaEligible || ec.getFmla().equals("N")) ) {
223                                    if (listAccrualCategories.contains(ec.getAccrualCategory()) || ec.getAccrualCategory() == null) {
224                                    if (StringUtils.equals(ec.getAccrualBalanceAction(), LMConstants.ACCRUAL_BALANCE_ACTION.USAGE)) {
225                                            // go on, we are allowing these three combinations: YY, YN, NN.
226                                            
227                                                //  Apply the same logic as FMLA to the Worker Compensation flags.
228                                                if ( (workersCompEligible || ec.getWorkmansComp().equals("N")) ) {
229                                                    // go on, we are allowing these three combinations: YY, YN, NN.
230                                                    
231                                            //  now process the scheduled leave flag, but only for the Planning Calendar, not for the Reporting Calendar.
232                                            //  determine if the planning calendar is in effect.
233                                            if (isLeavePlanningCalendar) {
234    
235                                                //  if the allow_schd_leave flag=yes, add the earn code
236                                                if (ec.getAllowScheduledLeave().equals("Y")) {
237                                                        earnCodes.add(ec);
238                                                }
239    
240                                            } else {
241                                                //  this is a reporting calendar, so ignore scheduled leave flag, and add this earn code.
242                                                earnCodes.add(ec);
243                                            }
244                                        }
245                                    }
246                                }
247                            }
248                        }
249                    }
250                }
251            }   //  end of decs loop
252    
253            return earnCodes;
254        }
255    
256        private boolean addEarnCodeBasedOnEmployeeApproverSettings(EarnCodeSecurity security, Assignment a, Date asOfDate) {
257            boolean addEarnCode = false;
258            if (security.isEmployee() &&
259                    (StringUtils.equals(TKUser.getCurrentTargetPerson().getEmployeeId(), GlobalVariables.getUserSession().getPerson().getEmployeeId()))) {
260                addEarnCode = true;
261            }
262            // Check approver flag
263            if (!addEarnCode && security.isApprover()) {
264                Set<Long> workAreas = TkUserRoles.getUserRoles(GlobalVariables.getUserSession().getPrincipalId()).getApproverWorkAreas();
265                for (Long wa : workAreas) {
266                    WorkArea workArea = TkServiceLocator.getWorkAreaService().getWorkArea(wa, asOfDate);
267                    if (workArea!= null && a.getWorkArea().compareTo(workArea.getWorkArea())==0) {
268                        addEarnCode = true;
269                        break;
270                    }
271                }
272            }
273            return addEarnCode;
274        }
275    
276        private boolean showEarnCodeIfHoliday(EarnCode earnCode, EarnCodeSecurity security) {
277            if (earnCode.getEarnCode().equals(TkConstants.HOLIDAY_EARN_CODE)) {
278                if (security.isApprover() || TkUserRoles.getUserRoles(GlobalVariables.getUserSession().getPrincipalId()).isSystemAdmin()) {
279                    return true;
280                } else {
281                    return false;
282                }
283            } else {
284                return true;
285            }
286        }
287    
288        @Override
289        public List<EarnCode> getEarnCodesForPrincipal(String principalId, Date asOfDate, boolean isLeavePlanningCalendar) {
290            List<EarnCode> earnCodes = new LinkedList<EarnCode>();
291            List<Assignment> assignments = TkServiceLocator.getAssignmentService().getAssignments(principalId, asOfDate);
292            for (Assignment assignment : assignments) {
293                List<EarnCode> assignmentEarnCodes = getEarnCodesForLeave(assignment, asOfDate, isLeavePlanningCalendar);
294                //  the following list processing does work as hoped, comparing the objects' data, rather than their references to memory structures.
295                earnCodes.removeAll(assignmentEarnCodes); //ensures no overlap during the addAll
296                earnCodes.addAll(assignmentEarnCodes);
297            }
298    
299            return earnCodes;
300        }
301    
302        public EarnCode getEarnCode(String earnCode, Date asOfDate) {
303                    return earnCodeDao.getEarnCode(earnCode, asOfDate);
304            }
305    
306        @Override
307        public String getEarnCodeType(String earnCode, Date asOfDate) {
308            EarnCode earnCodeObj = getEarnCode(earnCode, asOfDate);
309            return earnCodeObj.getEarnCodeType();
310        }
311    
312            @Override
313            public EarnCode getEarnCodeById(String earnCodeId) {
314                    return earnCodeDao.getEarnCodeById(earnCodeId);
315            }
316    
317            public List<EarnCode> getOvertimeEarnCodes(Date asOfDate){
318                    return earnCodeDao.getOvertimeEarnCodes(asOfDate);
319            }
320    
321            public List<String> getOvertimeEarnCodesStrs(Date asOfDate){
322                    List<String> ovtEarnCodeStrs = new ArrayList<String>();
323                    List<EarnCode> ovtEarnCodes = getOvertimeEarnCodes(asOfDate);
324                    if(ovtEarnCodes != null){
325                            for(EarnCode ovtEc : ovtEarnCodes){
326                                    ovtEarnCodeStrs.add(ovtEc.getEarnCode());
327                            }
328                    }
329                    return ovtEarnCodeStrs;
330            }
331    
332            @Override
333            public int getEarnCodeCount(String earnCode) {
334                    return earnCodeDao.getEarnCodeCount(earnCode);
335            }
336    
337            @Override
338            public int getNewerEarnCodeCount(String earnCode, Date effdt) {
339                    return earnCodeDao.getNewerEarnCodeCount(earnCode, effdt);
340            }
341    
342            @Override
343            public BigDecimal roundHrsWithEarnCode(BigDecimal hours, EarnCode earnCode) {
344                    String roundOption = LMConstants.ROUND_OPTION_MAP.get(earnCode.getRoundingOption());
345                    BigDecimal fractScale = new BigDecimal(earnCode.getFractionalTimeAllowed());
346                    if(roundOption == null) {
347                            throw new RuntimeException("Rounding option of Earn Code " + earnCode.getEarnCode() + " is not recognized.");
348                    }
349                    BigDecimal roundedHours = hours;
350                    if(roundOption.equals("Traditional")) {
351                            roundedHours = hours.setScale(fractScale.scale(), BigDecimal.ROUND_HALF_EVEN);
352                    } else if(roundOption.equals("Truncate")) {
353                            roundedHours = hours.setScale(fractScale.scale(), BigDecimal.ROUND_DOWN);
354                    }
355                    return roundedHours;
356            }
357    
358            @Override
359            public Map<String, String> getEarnCodesForDisplay(String principalId, boolean isLeavePlanningCalendar) {
360                    return getEarnCodesForDisplayWithEffectiveDate(principalId, TKUtils.getCurrentDate(), isLeavePlanningCalendar);
361            }
362    
363        public List<EarnCode> getEarnCodes(String earnCode, String ovtEarnCode, String descr, String leavePlan, String accrualCategory, Date fromEffdt, Date toEffdt, String active, String showHist) {
364            return earnCodeDao.getEarnCodes(earnCode, ovtEarnCode, descr, leavePlan, accrualCategory, fromEffdt, toEffdt, active, showHist);
365        }
366    
367        @Override
368        public Map<String, String> getEarnCodesForDisplayWithEffectiveDate(String principalId, Date asOfDate, boolean isLeavePlanningCalendar) {
369            List<EarnCode> earnCodes = this.getEarnCodesForPrincipal(principalId, asOfDate, isLeavePlanningCalendar);
370    
371            Date currentDate = TKUtils.getCurrentDate();
372            boolean futureDate = asOfDate.after(currentDate);
373            List<EarnCode> copyList = new ArrayList<EarnCode>();
374            copyList.addAll(earnCodes);
375            for (EarnCode earnCode : copyList) {
376                if ( futureDate
377                        && !earnCode.getAllowScheduledLeave().equalsIgnoreCase("Y")) {
378                    earnCodes.remove(earnCode);
379                }
380            }
381            Comparator<EarnCode> earnCodeComparator = new Comparator<EarnCode>() {
382                @Override
383                public int compare(EarnCode ec1, EarnCode ec2) {
384                    return ec1.getEarnCode().compareToIgnoreCase(ec2.getEarnCode());
385                }
386            };
387            // Order by leaveCode ascending
388            Ordering<EarnCode> ordering = Ordering.from(earnCodeComparator);
389    
390            Map<String, String> earnCodesForDisplay = new LinkedHashMap<String, String>();
391            for (EarnCode earnCode : ordering.sortedCopy(earnCodes)) {
392                earnCodesForDisplay.put(earnCode.getEarnCodeKeyForDisplay(), earnCode.getEarnCodeValueForDisplay());
393            }
394            return earnCodesForDisplay;
395        }
396    
397    }