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 }