1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.kuali.hr.time.overtime.weekly.rule.service;
17
18 import java.math.BigDecimal;
19 import java.sql.Date;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.Comparator;
24 import java.util.List;
25 import java.util.ListIterator;
26 import java.util.Set;
27
28 import org.apache.commons.collections.CollectionUtils;
29 import org.apache.commons.lang.ObjectUtils;
30 import org.apache.commons.lang.StringUtils;
31 import org.joda.time.DateTimeZone;
32 import org.kuali.hr.lm.leaveblock.LeaveBlock;
33 import org.kuali.hr.time.assignment.Assignment;
34 import org.kuali.hr.time.calendar.CalendarEntries;
35 import org.kuali.hr.time.earncode.EarnCode;
36 import org.kuali.hr.time.flsa.FlsaDay;
37 import org.kuali.hr.time.flsa.FlsaWeek;
38 import org.kuali.hr.time.overtime.weekly.rule.WeeklyOvertimeRule;
39 import org.kuali.hr.time.overtime.weekly.rule.dao.WeeklyOvertimeRuleDao;
40 import org.kuali.hr.time.service.base.TkServiceLocator;
41 import org.kuali.hr.time.timeblock.TimeBlock;
42 import org.kuali.hr.time.timeblock.TimeHourDetail;
43 import org.kuali.hr.time.timesheet.TimesheetDocument;
44 import org.kuali.hr.time.util.TKUtils;
45 import org.kuali.hr.time.util.TkConstants;
46 import org.kuali.hr.time.util.TkTimeBlockAggregate;
47 import org.kuali.hr.time.workarea.WorkArea;
48 import org.kuali.hr.time.workflow.TimesheetDocumentHeader;
49 import org.kuali.rice.krad.service.KRADServiceLocator;
50
51 public class WeeklyOvertimeRuleServiceImpl implements WeeklyOvertimeRuleService {
52
53 private WeeklyOvertimeRuleDao weeklyOvertimeRuleDao;
54
55 @Override
56 public void processWeeklyOvertimeRule(TimesheetDocument timesheetDocument, TkTimeBlockAggregate aggregate) {
57 Date asOfDate = TKUtils.getTimelessDate(timesheetDocument.getDocumentHeader().getEndDate());
58 String principalId = timesheetDocument.getDocumentHeader().getPrincipalId();
59 java.util.Date beginDate = timesheetDocument.getDocumentHeader().getBeginDate();
60 java.util.Date endDate = timesheetDocument.getDocumentHeader().getEndDate();
61 List<WeeklyOvertimeRule> weeklyOvertimeRules = getWeeklyOvertimeRules(asOfDate);
62
63 List<List<FlsaWeek>> flsaWeeks = getFlsaWeeks(principalId, beginDate, endDate, aggregate);
64
65 for (WeeklyOvertimeRule weeklyOvertimeRule : weeklyOvertimeRules) {
66 Set<String> maxHoursEarnCodes = TkServiceLocator.getEarnCodeGroupService().getEarnCodeListForEarnCodeGroup(weeklyOvertimeRule.getMaxHoursEarnGroup(), asOfDate);
67 Set<String> convertFromEarnCodes = TkServiceLocator.getEarnCodeGroupService().getEarnCodeListForEarnCodeGroup(weeklyOvertimeRule.getConvertFromEarnGroup(), asOfDate);
68
69 for (List<FlsaWeek> flsaWeekParts : flsaWeeks) {
70 BigDecimal existingMaxHours = getMaxHours(flsaWeekParts, maxHoursEarnCodes);
71 BigDecimal newOvertimeHours = existingMaxHours.subtract(weeklyOvertimeRule.getMaxHours(), TkConstants.MATH_CONTEXT);
72
73 if (newOvertimeHours.compareTo(BigDecimal.ZERO) != 0) {
74 applyOvertimeToFlsaWeeks(flsaWeekParts, weeklyOvertimeRule, asOfDate, convertFromEarnCodes, newOvertimeHours);
75 }
76 }
77 }
78
79 savePreviousNextCalendarTimeBlocks(flsaWeeks);
80 }
81
82
83
84
85
86
87
88
89
90
91
92
93 protected List<List<FlsaWeek>> getFlsaWeeks(String principalId, java.util.Date beginDate, java.util.Date endDate, TkTimeBlockAggregate aggregate) {
94 List<List<FlsaWeek>> flsaWeeks = new ArrayList<List<FlsaWeek>>();
95
96 DateTimeZone zone = TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
97 List<FlsaWeek> currentWeeks = aggregate.getFlsaWeeks(zone);
98
99 for (ListIterator<FlsaWeek> weekIterator = currentWeeks.listIterator(); weekIterator.hasNext(); ) {
100 List<FlsaWeek> flsaWeek = new ArrayList<FlsaWeek>();
101
102 int index = weekIterator.nextIndex();
103 FlsaWeek currentWeek = weekIterator.next();
104
105 if (index == 0 && !currentWeek.isFirstWeekFull()) {
106 TimesheetDocumentHeader timesheetDocumentHeader = TkServiceLocator.getTimesheetDocumentHeaderService().getPreviousDocumentHeader(principalId, beginDate);
107 if (timesheetDocumentHeader != null) {
108 TimesheetDocument timesheetDocument = TkServiceLocator.getTimesheetService().getTimesheetDocument(timesheetDocumentHeader.getDocumentId());
109 List<String> assignmentKeys = new ArrayList<String>();
110 for(Assignment assignment : timesheetDocument.getAssignments()) {
111 assignmentKeys.add(assignment.getAssignmentKey());
112 }
113
114 List<TimeBlock> timeBlocks = TkServiceLocator.getTimeBlockService().getTimeBlocks(timesheetDocumentHeader.getDocumentId());
115 List<LeaveBlock> leaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(principalId, timesheetDocumentHeader.getBeginDate(), timesheetDocumentHeader.getEndDate(), assignmentKeys);
116 if (CollectionUtils.isNotEmpty(timeBlocks)) {
117 CalendarEntries calendarEntry = TkServiceLocator.getCalendarService().getCalendarDatesByPayEndDate(principalId, timesheetDocumentHeader.getEndDate(), TkConstants.PAY_CALENDAR_TYPE);
118 TkTimeBlockAggregate previousAggregate = new TkTimeBlockAggregate(timeBlocks, leaveBlocks, calendarEntry, calendarEntry.getCalendarObj(), true);
119 List<FlsaWeek> previousWeek = previousAggregate.getFlsaWeeks(zone);
120 if (CollectionUtils.isNotEmpty(previousWeek)) {
121 flsaWeek.add(previousWeek.get(previousWeek.size() - 1));
122 }
123 }
124 }
125 }
126
127 flsaWeek.add(currentWeek);
128
129 if (index == currentWeeks.size() - 1 && !currentWeek.isLastWeekFull()) {
130 TimesheetDocumentHeader timesheetDocumentHeader = TkServiceLocator.getTimesheetDocumentHeaderService().getNextDocumentHeader(principalId, endDate);
131 if (timesheetDocumentHeader != null) {
132 TimesheetDocument timesheetDocument = TkServiceLocator.getTimesheetService().getTimesheetDocument(timesheetDocumentHeader.getDocumentId());
133 List<String> assignmentKeys = new ArrayList<String>();
134 for(Assignment assignment : timesheetDocument.getAssignments()) {
135 assignmentKeys.add(assignment.getAssignmentKey());
136 }
137
138 List<TimeBlock> timeBlocks = TkServiceLocator.getTimeBlockService().getTimeBlocks(timesheetDocumentHeader.getDocumentId());
139 List<LeaveBlock> leaveBlocks = TkServiceLocator.getLeaveBlockService().getLeaveBlocksForTimeCalendar(principalId, timesheetDocumentHeader.getBeginDate(), timesheetDocumentHeader.getEndDate(), assignmentKeys);
140 if (CollectionUtils.isNotEmpty(timeBlocks)) {
141 CalendarEntries calendarEntry = TkServiceLocator.getCalendarService().getCalendarDatesByPayEndDate(principalId, timesheetDocumentHeader.getEndDate(), TkConstants.PAY_CALENDAR_TYPE);
142 TkTimeBlockAggregate nextAggregate = new TkTimeBlockAggregate(timeBlocks, leaveBlocks, calendarEntry, calendarEntry.getCalendarObj(), true);
143 List<FlsaWeek> nextWeek = nextAggregate.getFlsaWeeks(zone);
144 if (CollectionUtils.isNotEmpty(nextWeek)) {
145 flsaWeek.add(nextWeek.get(0));
146 }
147 }
148 }
149 }
150
151 flsaWeeks.add(flsaWeek);
152 }
153
154 return flsaWeeks;
155 }
156
157
158
159
160
161
162
163
164
165 protected BigDecimal getMaxHours(List<FlsaWeek> flsaWeeks, Set<String> maxHoursEarnCodes) {
166 BigDecimal maxHours = BigDecimal.ZERO;
167
168 for (FlsaWeek flsaWeek : flsaWeeks) {
169 for (FlsaDay flsaDay : flsaWeek.getFlsaDays()) {
170 for (TimeBlock timeBlock : flsaDay.getAppliedTimeBlocks()) {
171 for (TimeHourDetail timeHourDetail : timeBlock.getTimeHourDetails()) {
172 if (maxHoursEarnCodes.contains(timeHourDetail.getEarnCode())) {
173 maxHours = maxHours.add(timeHourDetail.getHours(), TkConstants.MATH_CONTEXT);
174 }
175 }
176 }
177
178 for (LeaveBlock leaveBlock : flsaDay.getAppliedLeaveBlocks()) {
179 if (maxHoursEarnCodes.contains(leaveBlock.getEarnCode())) {
180 maxHours = maxHours.add(leaveBlock.getLeaveAmount().negate());
181 }
182 }
183 }
184 }
185
186 return maxHours;
187 }
188
189
190
191
192
193
194
195
196
197
198
199 protected String getOvertimeEarnCode(WeeklyOvertimeRule weeklyOvertimeRule, TimeBlock timeBlock, Date asOfDate) {
200 String overtimeEarnCode = weeklyOvertimeRule.getConvertToEarnCode();
201
202 WorkArea workArea = TkServiceLocator.getWorkAreaService().getWorkArea(timeBlock.getWorkArea(), asOfDate);
203 if (StringUtils.isNotBlank(workArea.getDefaultOvertimeEarnCode())){
204 overtimeEarnCode = workArea.getDefaultOvertimeEarnCode();
205 }
206
207 if (StringUtils.isNotEmpty(timeBlock.getOvertimePref())) {
208 overtimeEarnCode = timeBlock.getOvertimePref();
209 }
210
211 return overtimeEarnCode;
212 }
213
214
215
216
217
218
219
220
221
222
223 protected void applyOvertimeToFlsaWeeks(List<FlsaWeek> flsaWeeks, WeeklyOvertimeRule weeklyOvertimeRule, Date asOfDate, Set<String> convertFromEarnCodes, BigDecimal overtimeHours) {
224 List<FlsaDay> flsaDays = getFlsaDays(flsaWeeks);
225
226 if (overtimeHours.compareTo(BigDecimal.ZERO) > 0) {
227 applyPositiveOvertimeToFlsaWeek(flsaDays, weeklyOvertimeRule, asOfDate, convertFromEarnCodes, overtimeHours);
228 } else if (overtimeHours.compareTo(BigDecimal.ZERO) < 0) {
229 applyNegativeOvertimeToFlsaWeek(flsaDays, weeklyOvertimeRule, asOfDate, convertFromEarnCodes, overtimeHours);
230 }
231
232 removeEmptyOvertime(flsaDays, weeklyOvertimeRule, asOfDate);
233 }
234
235
236
237
238
239
240
241
242 protected List<FlsaDay> getFlsaDays(List<FlsaWeek> flsaWeeks) {
243 List<FlsaDay> flsaDays = new ArrayList<FlsaDay>();
244
245 for (FlsaWeek flsaWeek : flsaWeeks) {
246 flsaDays.addAll(flsaWeek.getFlsaDays());
247 }
248
249 return flsaDays;
250 }
251
252
253
254
255
256
257
258
259
260
261 protected void applyPositiveOvertimeToFlsaWeek(List<FlsaDay> flsaDays, WeeklyOvertimeRule weeklyOvertimeRule, Date asOfDate, Set<String> convertFromEarnCodes, BigDecimal overtimeHours) {
262 for (ListIterator<FlsaDay> dayIterator = flsaDays.listIterator(flsaDays.size()); dayIterator.hasPrevious(); ) {
263 FlsaDay flsaDay = dayIterator.previous();
264
265 List<TimeBlock> timeBlocks = flsaDay.getAppliedTimeBlocks();
266 Collections.sort(timeBlocks, new Comparator<TimeBlock>() {
267 public int compare(TimeBlock timeBlock1, TimeBlock timeBlock2) {
268 return ObjectUtils.compare(timeBlock1.getBeginTimestamp(), timeBlock2.getBeginTimestamp());
269 }
270 });
271
272 for (ListIterator<TimeBlock> timeBlockIterator = timeBlocks.listIterator(timeBlocks.size()); timeBlockIterator.hasPrevious(); ) {
273 TimeBlock timeBlock = timeBlockIterator.previous();
274 String overtimeEarnCode = getOvertimeEarnCode(weeklyOvertimeRule, timeBlock, asOfDate);
275 overtimeHours = applyPositiveOvertimeOnTimeBlock(timeBlock, overtimeEarnCode, convertFromEarnCodes, overtimeHours);
276 }
277
278 flsaDay.remapTimeHourDetails();
279 }
280 }
281
282
283
284
285
286
287
288
289
290
291 protected void applyNegativeOvertimeToFlsaWeek(List<FlsaDay> flsaDays, WeeklyOvertimeRule weeklyOvertimeRule, Date asOfDate, Set<String> convertFromEarnCodes, BigDecimal overtimeHours) {
292 for (ListIterator<FlsaDay> dayIterator = flsaDays.listIterator(); dayIterator.hasNext(); ) {
293 FlsaDay flsaDay = dayIterator.next();
294
295 List<TimeBlock> timeBlocks = flsaDay.getAppliedTimeBlocks();
296 Collections.sort(timeBlocks, new Comparator<TimeBlock>() {
297 public int compare(TimeBlock timeBlock1, TimeBlock timeBlock2) {
298 return ObjectUtils.compare(timeBlock1.getBeginTimestamp(), timeBlock2.getBeginTimestamp());
299 }
300 });
301
302 for (ListIterator<TimeBlock> timeBlockIterator = timeBlocks.listIterator(); timeBlockIterator.hasNext(); ) {
303 TimeBlock timeBlock = timeBlockIterator.next();
304 String overtimeEarnCode = getOvertimeEarnCode(weeklyOvertimeRule, timeBlock, asOfDate);
305 overtimeHours = applyNegativeOvertimeOnTimeBlock(timeBlock, overtimeEarnCode, convertFromEarnCodes, overtimeHours);
306 }
307
308 flsaDay.remapTimeHourDetails();
309 }
310 }
311
312 protected void removeEmptyOvertime(List<FlsaDay> flsaDays, WeeklyOvertimeRule weeklyOvertimeRule, Date asOfDate) {
313 for (ListIterator<FlsaDay> dayIterator = flsaDays.listIterator(); dayIterator.hasNext(); ) {
314 FlsaDay flsaDay = dayIterator.next();
315
316 List<TimeBlock> timeBlocks = flsaDay.getAppliedTimeBlocks();
317 for (ListIterator<TimeBlock> timeBlockIterator = timeBlocks.listIterator(); timeBlockIterator.hasNext(); ) {
318 TimeBlock timeBlock = timeBlockIterator.next();
319 String overtimeEarnCode = getOvertimeEarnCode(weeklyOvertimeRule, timeBlock, asOfDate);
320
321 List<TimeHourDetail> timeHourDetails = timeBlock.getTimeHourDetails();
322 List<TimeHourDetail> oldTimeHourDetails = new ArrayList<TimeHourDetail>();
323
324 TimeHourDetail overtimeTimeHourDetail = getTimeHourDetailByEarnCode(timeHourDetails, Collections.singletonList(overtimeEarnCode));
325 if (overtimeTimeHourDetail != null) {
326 if (overtimeTimeHourDetail.getHours().compareTo(BigDecimal.ZERO) == 0) {
327 oldTimeHourDetails.add(overtimeTimeHourDetail);
328 }
329 }
330
331 for (TimeHourDetail oldTimeHourDetail : oldTimeHourDetails) {
332 timeBlock.removeTimeHourDetail(oldTimeHourDetail);
333 }
334 }
335 }
336 }
337
338
339
340
341
342
343
344
345
346
347
348 protected BigDecimal applyPositiveOvertimeOnTimeBlock(TimeBlock timeBlock, String overtimeEarnCode, Set<String> convertFromEarnCodes, BigDecimal overtimeHours) {
349 BigDecimal applied = BigDecimal.ZERO;
350 List<TimeHourDetail> timeHourDetails = timeBlock.getTimeHourDetails();
351 List<TimeHourDetail> newTimeHourDetails = new ArrayList<TimeHourDetail>();
352
353 for (TimeHourDetail timeHourDetail : timeHourDetails) {
354 if (convertFromEarnCodes.contains(timeHourDetail.getEarnCode())) {
355 if (timeHourDetail.getHours().compareTo(overtimeHours) >= 0) {
356 applied = overtimeHours;
357 } else {
358 applied = timeHourDetail.getHours();
359 }
360
361 EarnCode earnCodeObj = TkServiceLocator.getEarnCodeService().getEarnCode(overtimeEarnCode, timeBlock.getEndDate());
362 BigDecimal hours = earnCodeObj.getInflateFactor().multiply(applied, TkConstants.MATH_CONTEXT).setScale(TkConstants.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_UP);
363
364 TimeHourDetail overtimeTimeHourDetail = getTimeHourDetailByEarnCode(timeHourDetails, Collections.singletonList(overtimeEarnCode));
365 if (overtimeTimeHourDetail != null) {
366 overtimeTimeHourDetail.setHours(overtimeTimeHourDetail.getHours().add(hours, TkConstants.MATH_CONTEXT));
367 } else {
368 TimeHourDetail newTimeHourDetail = new TimeHourDetail();
369 newTimeHourDetail.setTkTimeBlockId(timeBlock.getTkTimeBlockId());
370 newTimeHourDetail.setEarnCode(overtimeEarnCode);
371 newTimeHourDetail.setHours(hours);
372
373 newTimeHourDetails.add(newTimeHourDetail);
374 }
375
376 timeHourDetail.setHours(timeHourDetail.getHours().subtract(applied, TkConstants.MATH_CONTEXT).setScale(TkConstants.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_UP));
377 }
378 }
379
380 for (TimeHourDetail newTimeHourDetail : newTimeHourDetails) {
381 timeBlock.addTimeHourDetail(newTimeHourDetail);
382 }
383
384 return overtimeHours.subtract(applied);
385 }
386
387
388
389
390
391
392
393
394
395
396
397 protected BigDecimal applyNegativeOvertimeOnTimeBlock(TimeBlock timeBlock, String overtimeEarnCode, Set<String> convertFromEarnCodes, BigDecimal overtimeHours) {
398 BigDecimal applied = BigDecimal.ZERO;
399 List<TimeHourDetail> timeHourDetails = timeBlock.getTimeHourDetails();
400
401 for (TimeHourDetail timeHourDetail : timeHourDetails) {
402 if (convertFromEarnCodes.contains(timeHourDetail.getEarnCode())) {
403 TimeHourDetail overtimeTimeHourDetail = getTimeHourDetailByEarnCode(timeHourDetails, Collections.singletonList(overtimeEarnCode));
404
405 if (overtimeTimeHourDetail != null) {
406 applied = overtimeTimeHourDetail.getHours().add(overtimeHours, TkConstants.MATH_CONTEXT);
407 if (applied.compareTo(BigDecimal.ZERO) >= 0) {
408 applied = overtimeHours;
409 } else {
410 applied = overtimeTimeHourDetail.getHours().negate();
411 }
412
413 EarnCode earnCodeObj = TkServiceLocator.getEarnCodeService().getEarnCode(overtimeEarnCode, timeBlock.getEndDate());
414 BigDecimal hours = earnCodeObj.getInflateFactor().multiply(applied, TkConstants.MATH_CONTEXT).setScale(TkConstants.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_DOWN);
415
416 overtimeTimeHourDetail.setHours(overtimeTimeHourDetail.getHours().add(hours, TkConstants.MATH_CONTEXT));
417
418 timeHourDetail.setHours(timeHourDetail.getHours().subtract(applied, TkConstants.MATH_CONTEXT).setScale(TkConstants.BIG_DECIMAL_SCALE, BigDecimal.ROUND_HALF_UP));
419 }
420 }
421 }
422
423 return overtimeHours.subtract(applied);
424 }
425
426 protected TimeHourDetail getTimeHourDetailByEarnCode(List<TimeHourDetail> timeHourDetails, Collection<String> earnCodes) {
427 TimeHourDetail result = null;
428
429 for (TimeHourDetail timeHourDetail : timeHourDetails) {
430 if (earnCodes.contains(timeHourDetail.getEarnCode())) {
431 result = timeHourDetail;
432 break;
433 }
434 }
435
436 return result;
437 }
438
439
440
441
442
443
444 protected void savePreviousNextCalendarTimeBlocks(List<List<FlsaWeek>> flsaWeeks) {
445 for (ListIterator<List<FlsaWeek>> weeksIterator = flsaWeeks.listIterator(); weeksIterator.hasNext(); ) {
446 int index = weeksIterator.nextIndex();
447 List<FlsaWeek> currentWeekParts = weeksIterator.next();
448
449 if (index == 0 && currentWeekParts.size() > 1) {
450 FlsaWeek previousFlsaWeek = currentWeekParts.get(0);
451 for (FlsaDay flsaDay : previousFlsaWeek.getFlsaDays()) {
452 KRADServiceLocator.getBusinessObjectService().save(flsaDay.getAppliedTimeBlocks());
453 }
454 }
455
456 if (index == flsaWeeks.size() - 1 && currentWeekParts.size() > 1) {
457 FlsaWeek nextFlsaWeek = currentWeekParts.get(currentWeekParts.size() - 1);
458 for (FlsaDay flsaDay : nextFlsaWeek.getFlsaDays()) {
459 KRADServiceLocator.getBusinessObjectService().save(flsaDay.getAppliedTimeBlocks());
460 }
461 }
462 }
463 }
464
465 @Override
466 public List<WeeklyOvertimeRule> getWeeklyOvertimeRules(Date asOfDate) {
467 return weeklyOvertimeRuleDao.findWeeklyOvertimeRules(asOfDate);
468 }
469
470 @Override
471 public void saveOrUpdate(WeeklyOvertimeRule weeklyOvertimeRule) {
472 weeklyOvertimeRuleDao.saveOrUpdate(weeklyOvertimeRule);
473 }
474
475 @Override
476 public void saveOrUpdate(List<WeeklyOvertimeRule> weeklyOvertimeRules) {
477 weeklyOvertimeRuleDao.saveOrUpdate(weeklyOvertimeRules);
478 }
479
480 public void setWeeklyOvertimeRuleDao(WeeklyOvertimeRuleDao weeklyOvertimeRuleDao) {
481 this.weeklyOvertimeRuleDao = weeklyOvertimeRuleDao;
482 }
483
484 @Override
485 public WeeklyOvertimeRule getWeeklyOvertimeRule(String tkWeeklyOvertimeRuleId) {
486 return weeklyOvertimeRuleDao.getWeeklyOvertimeRule(tkWeeklyOvertimeRuleId);
487 }
488
489 }