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.flsa; 017 018 import java.math.BigDecimal; 019 import java.util.ArrayList; 020 import java.util.HashMap; 021 import java.util.List; 022 import java.util.Map; 023 024 import org.apache.commons.lang.StringUtils; 025 import org.joda.time.DateTime; 026 import org.joda.time.DateTimeZone; 027 import org.joda.time.Interval; 028 import org.joda.time.LocalDateTime; 029 import org.kuali.hr.lm.leaveblock.LeaveBlock; 030 import org.kuali.hr.time.timeblock.TimeBlock; 031 import org.kuali.hr.time.timeblock.TimeHourDetail; 032 import org.kuali.hr.time.util.TKUtils; 033 import org.kuali.hr.time.util.TkConstants; 034 035 public class FlsaDay { 036 037 private Map<String,List<TimeBlock>> earnCodeToTimeBlocks = new HashMap<String,List<TimeBlock>>(); 038 private List<TimeBlock> appliedTimeBlocks = new ArrayList<TimeBlock>(); 039 040 private Map<String,List<LeaveBlock>> earnCodeToLeaveBlocks = new HashMap<String,List<LeaveBlock>>(); 041 private List<LeaveBlock> appliedLeaveBlocks = new ArrayList<LeaveBlock>(); 042 043 Interval flsaDateInterval; 044 LocalDateTime flsaDate; 045 DateTimeZone timeZone; 046 047 /** 048 * 049 * @param flsaDate A LocalDateTime because we want to be conscious of the 050 * relative nature of this flsa/window 051 * @param timeBlocks 052 * @param timeZone The timezone we are constructing, relative. 053 */ 054 public FlsaDay(LocalDateTime flsaDate, List<TimeBlock> timeBlocks, List<LeaveBlock> leaveBlocks, DateTimeZone timeZone) { 055 this.flsaDate = flsaDate; 056 this.timeZone = timeZone; 057 flsaDateInterval = new Interval(flsaDate.toDateTime(timeZone), flsaDate.toDateTime(timeZone).plusHours(24)); 058 this.setTimeBlocks(timeBlocks); 059 this.setLeaveBlocks(leaveBlocks); 060 } 061 062 /** 063 * Handles the breaking apart of existing time blocks around FLSA boundaries. 064 * 065 * This method will compare the FLSA interval against the timeblock interval 066 * to determine how many hours overlap. It will then examine the time hour 067 * details 068 * 069 * @param timeBlocks a sorted list of time blocks. 070 */ 071 public void setTimeBlocks(List<TimeBlock> timeBlocks) { 072 for (TimeBlock block : timeBlocks) { 073 applyBlock(block, this.appliedTimeBlocks); 074 //if (!applyBlock(block, this.appliedTimeBlocks)) { 075 // break; 076 //} 077 } 078 } 079 080 /** 081 * Handles the breaking apart of existing leave blocks around FLSA boundaries. 082 * 083 * This method will compare the FLSA interval against the leaveblock interval 084 * to determine how many hours overlap. It will then examine the leave hour 085 * details 086 * 087 * @param leaveBlocks a sorted list of leave blocks. 088 */ 089 public void setLeaveBlocks(List<LeaveBlock> leaveBlocks) { 090 for (LeaveBlock block : leaveBlocks) { 091 applyBlock(block, this.appliedLeaveBlocks); 092 /*if (!applyBlock(block, this.appliedLeaveBlocks)) { 093 break; 094 }*/ 095 } 096 } 097 098 /** 099 * This method will compute the mappings present for this object: 100 * 101 * earnCodeToTimeBlocks 102 * earnCodeToHours 103 * 104 */ 105 public void remapTimeHourDetails() { 106 List<TimeBlock> reApplied = new ArrayList<TimeBlock>(appliedTimeBlocks.size()); 107 earnCodeToTimeBlocks.clear(); 108 for (TimeBlock block : appliedTimeBlocks) { 109 applyBlock(block, reApplied); 110 } 111 } 112 113 /** 114 * This method will compute the mappings present for this object: 115 * 116 * earnCodeToTimeBlocks 117 * earnCodeToHours 118 * 119 */ 120 public void remapLeaveHourDetails() { 121 List<LeaveBlock> reApplied = new ArrayList<LeaveBlock>(appliedLeaveBlocks.size()); 122 earnCodeToLeaveBlocks.clear(); 123 for (LeaveBlock block : appliedLeaveBlocks) { 124 applyBlock(block, reApplied); 125 } 126 } 127 128 /** 129 * This method determines if the provided TimeBlock is applicable to this 130 * FLSA day, and if so will add it to the applyList. It could be the case 131 * that a TimeBlock is on the boundary of the FLSA day so that only a 132 * partial amount of the hours for that TimeBlock will count towards this 133 * day. 134 * 135 * |---------+------------------+---------| 136 * | Day 1 | Day 1/2 Boundary | Day 2 | 137 * |---------+------------------+---------| 138 * | Block 1 | | Block 2 | 139 * |---------+------------------+---------| 140 * 141 * The not so obvious ascii diagram above is intended to illustrate the case 142 * where on day one you have 1 fully overlapping time block (block1) and one 143 * partially overlapping time block (block2). Block 2 belongs to both FLSA 144 * Day 1 and Day 2. 145 * 146 * @param block A time block that we want to check and apply to this day. 147 * @param applyList A list of time blocks we want to add applicable time blocks to. 148 * 149 * @return True if the block is applicable, false otherwise. The return 150 * value can be used as a quick exit for the setTimeBlocks() method. 151 * 152 * TODO : Bucketing of partial FLSA days is still suspect, however real life examples of this are likely non-existent to rare. 153 * 154 * Danger may still lurk in day-boundary overlapping time blocks that have multiple Time Hour Detail entries. 155 */ 156 private boolean applyBlock(TimeBlock block, List<TimeBlock> applyList) { 157 DateTime beginDateTime = new DateTime(block.getBeginTimestamp(), this.timeZone); 158 DateTime endDateTime = new DateTime(block.getEndTimestamp(), this.timeZone); 159 160 if (beginDateTime.isAfter(flsaDateInterval.getEnd())) { 161 return false; 162 } 163 164 Interval timeBlockInterval = null; 165 //Requested to have zero hour time blocks be able to be added to the GUI 166 boolean zeroHoursTimeBlock = false; 167 if(endDateTime.getMillis() > beginDateTime.getMillis()){ 168 timeBlockInterval = new Interval(beginDateTime,endDateTime); 169 } 170 171 if(flsaDateInterval.contains(beginDateTime)){ 172 zeroHoursTimeBlock = true; 173 } 174 175 Interval overlapInterval = flsaDateInterval.overlap(timeBlockInterval); 176 long overlap = (overlapInterval == null) ? 0L : overlapInterval.toDurationMillis(); 177 BigDecimal overlapHours = TKUtils.convertMillisToHours(overlap); 178 if((overlapHours.compareTo(BigDecimal.ZERO) == 0) && flsaDateInterval.contains(beginDateTime) && flsaDateInterval.contains(endDateTime)){ 179 if(block.getHours().compareTo(BigDecimal.ZERO) > 0){ 180 overlapHours = block.getHours(); 181 } 182 } 183 184 // Local lookup for this time-block to ensure we are not over applicable hours. 185 // You will notice below we are earn codes globally per day, and also locally per timeblock. 186 // The local per-time block mapping is used only to verify that we have not gone over allocated overlap time 187 // for the individual time block. 188 Map<String,BigDecimal> localEarnCodeToHours = new HashMap<String,BigDecimal>(); 189 190 if (zeroHoursTimeBlock || overlapHours.compareTo(BigDecimal.ZERO) > 0 || (flsaDateInterval.contains(beginDateTime) && StringUtils.equals(block.getEarnCodeType(),TkConstants.EARN_CODE_AMOUNT))) { 191 192 List<TimeHourDetail> details = block.getTimeHourDetails(); 193 for (TimeHourDetail thd : details) { 194 BigDecimal localEcHours = localEarnCodeToHours.containsKey(thd.getEarnCode()) ? localEarnCodeToHours.get(thd.getEarnCode()) : BigDecimal.ZERO; 195 //NOTE adding this in the last few hours before release.. remove if side effects are noticed 196 if (overlapHours.compareTo(localEcHours) >= 0 || thd.getAmount().compareTo(BigDecimal.ZERO) == 0) { 197 localEcHours = localEcHours.add(thd.getHours(), TkConstants.MATH_CONTEXT); 198 localEarnCodeToHours.put(thd.getEarnCode(), localEcHours); 199 } 200 } 201 202 List<TimeBlock> blocks = earnCodeToTimeBlocks.get(block.getEarnCode()); 203 if (blocks == null) { 204 blocks = new ArrayList<TimeBlock>(); 205 earnCodeToTimeBlocks.put(block.getEarnCode(), blocks); 206 } 207 blocks.add(block); 208 applyList.add(block); 209 } 210 211 return true; 212 } 213 214 /** 215 * This method determines if the provided LeaveBlock is applicable to this 216 * FLSA day, and if so will add it to the applyList. It could be the case 217 * that a LeaveBlock is on the boundary of the FLSA day so that only a 218 * partial amount of the hours for that LeaveBlock will count towards this 219 * day. 220 * 221 * |---------+------------------+---------| 222 * | Day 1 | Day 1/2 Boundary | Day 2 | 223 * |---------+------------------+---------| 224 * | Block 1 | | Block 2 | 225 * |---------+------------------+---------| 226 * 227 * The not so obvious ascii diagram above is intended to illustrate the case 228 * where on day one you have 1 fully overlapping leave block (block1) and one 229 * partially overlapping leave block (block2). Block 2 belongs to both FLSA 230 * Day 1 and Day 2. 231 * 232 * @param block A leave block that we want to check and apply to this day. 233 * @param applyList A list of leave blocks we want to add applicable leave blocks to. 234 * 235 * @return True if the block is applicable, false otherwise. The return 236 * value can be used as a quick exit for the setLeaveBlocks() method. 237 * 238 * TODO : Bucketing of partial FLSA days is still suspect, however real life examples of this are likely non-existent to rare. 239 */ 240 private boolean applyBlock(LeaveBlock block, List<LeaveBlock> applyList) { 241 DateTime beginDateTime = new DateTime(block.getLeaveDate(), this.timeZone); 242 DateTime endDateTime = new DateTime(block.getLeaveDate(), this.timeZone); 243 244 if (beginDateTime.isAfter(flsaDateInterval.getEnd())) 245 return false; 246 247 Interval leaveBlockInterval = null; 248 if(endDateTime.getMillis() > beginDateTime.getMillis()){ 249 leaveBlockInterval = new Interval(beginDateTime,endDateTime); 250 } 251 252 Interval overlapInterval = flsaDateInterval.overlap(leaveBlockInterval); 253 long overlap = (overlapInterval == null) ? 0L : overlapInterval.toDurationMillis(); 254 BigDecimal overlapHours = TKUtils.convertMillisToHours(overlap); 255 if((overlapHours.compareTo(BigDecimal.ZERO) == 0) && flsaDateInterval.contains(beginDateTime) && flsaDateInterval.contains(endDateTime)){ 256 if(block.getLeaveAmount().negate().compareTo(BigDecimal.ZERO) > 0){ 257 overlapHours = block.getLeaveAmount().negate(); 258 } 259 } 260 261 if (overlapHours.compareTo(BigDecimal.ZERO) > 0) { 262 List<LeaveBlock> blocks = earnCodeToLeaveBlocks.get(block.getEarnCode()); 263 if (blocks == null) { 264 blocks = new ArrayList<LeaveBlock>(); 265 earnCodeToLeaveBlocks.put(block.getEarnCode(), blocks); 266 } 267 blocks.add(block); 268 applyList.add(block); 269 } 270 271 return true; 272 } 273 274 public Map<String, List<TimeBlock>> getEarnCodeToTimeBlocks() { 275 return earnCodeToTimeBlocks; 276 } 277 278 public void setEarnCodeToTimeBlocks(Map<String, List<TimeBlock>> earnCodeToTimeBlocks) { 279 this.earnCodeToTimeBlocks = earnCodeToTimeBlocks; 280 } 281 282 public List<TimeBlock> getAppliedTimeBlocks() { 283 return appliedTimeBlocks; 284 } 285 286 public void setAppliedTimeBlocks(List<TimeBlock> appliedTimeBlocks) { 287 this.appliedTimeBlocks = appliedTimeBlocks; 288 } 289 290 public Map<String, List<LeaveBlock>> getEarnCodeToLeaveBlocks() { 291 return earnCodeToLeaveBlocks; 292 } 293 294 public void setEarnCodeToLeaveBlocks(Map<String, List<LeaveBlock>> earnCodeToLeaveBlocks) { 295 this.earnCodeToLeaveBlocks = earnCodeToLeaveBlocks; 296 } 297 298 public List<LeaveBlock> getAppliedLeaveBlocks() { 299 return appliedLeaveBlocks; 300 } 301 302 public void setAppliedLeaveBlocks(List<LeaveBlock> appliedLeaveBlocks) { 303 this.appliedLeaveBlocks = appliedLeaveBlocks; 304 } 305 306 }