001 /**
002 * Copyright 2004-2012 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 org.apache.commons.lang.StringUtils;
019 import org.joda.time.DateTime;
020 import org.joda.time.DateTimeZone;
021 import org.joda.time.Interval;
022 import org.joda.time.LocalDateTime;
023 import org.kuali.hr.time.timeblock.TimeBlock;
024 import org.kuali.hr.time.timeblock.TimeHourDetail;
025 import org.kuali.hr.time.util.TKUtils;
026 import org.kuali.hr.time.util.TkConstants;
027
028 import java.math.BigDecimal;
029 import java.util.ArrayList;
030 import java.util.HashMap;
031 import java.util.List;
032 import java.util.Map;
033
034 public class FlsaDay {
035 private Map<String,BigDecimal> earnCodeToHours = new HashMap<String,BigDecimal>();
036 private Map<String,List<TimeBlock>> earnCodeToTimeBlocks = new HashMap<String,List<TimeBlock>>();
037 private List<TimeBlock> appliedTimeBlocks = new ArrayList<TimeBlock>();
038
039 Interval flsaDateInterval;
040 LocalDateTime flsaDate;
041 DateTimeZone timeZone;
042
043 /**
044 *
045 * @param flsaDate A LocalDateTime because we want to be conscious of the
046 * relative nature of this flsa/window
047 * @param timeBlocks
048 * @param timeZone The timezone we are constructing, relative.
049 */
050 public FlsaDay(LocalDateTime flsaDate, List<TimeBlock> timeBlocks, DateTimeZone timeZone) {
051 this.flsaDate = flsaDate;
052 this.timeZone = timeZone;
053 flsaDateInterval = new Interval(flsaDate.toDateTime(timeZone), flsaDate.toDateTime(timeZone).plusHours(24));
054 this.setTimeBlocks(timeBlocks);
055 }
056
057 /**
058 * Handles the breaking apart of existing time blocks around FLSA boundaries.
059 *
060 * This method will compare the FLSA interval against the timeblock interval
061 * to determine how many hours overlap. It will then examine the time hour
062 * details
063 *
064 * @param timeBlocks a sorted list of time blocks.
065 */
066 public void setTimeBlocks(List<TimeBlock> timeBlocks) {
067 for (TimeBlock block : timeBlocks)
068 if (!applyBlock(block, this.appliedTimeBlocks))
069 break;
070 }
071
072 /**
073 * This method will compute the mappings present for this object:
074 *
075 * earnCodeToTimeBlocks
076 * earnCodeToHours
077 *
078 */
079 public void remapTimeHourDetails() {
080 List<TimeBlock> reApplied = new ArrayList<TimeBlock>(appliedTimeBlocks.size());
081 earnCodeToHours.clear();
082 earnCodeToTimeBlocks.clear();
083 for (TimeBlock block : appliedTimeBlocks) {
084 applyBlock(block, reApplied);
085 }
086 }
087
088 /**
089 * This method determines if the provided TimeBlock is applicable to this
090 * FLSA day, and if so will add it to the applyList. It could be the case
091 * that a TimeBlock is on the boundary of the FLSA day so that only a
092 * partial amount of the hours for that TimeBlock will count towards this
093 * day.
094 *
095 * |---------+------------------+---------|
096 * | Day 1 | Day 1/2 Boundary | Day 2 |
097 * |---------+------------------+---------|
098 * | Block 1 | | Block 2 |
099 * |---------+------------------+---------|
100 *
101 * The not so obvious ascii diagram above is intended to illustrate the case
102 * where on day one you have 1 fully overlapping time block (block1) and one
103 * partially overlapping time block (block2). Block 2 belongs to both FLSA
104 * Day 1 and Day 2.
105 *
106 * @param block A time block that we want to check and apply to this day.
107 * @param applyList A list of time blocks we want to add applicable time blocks to.
108 *
109 * @return True if the block is applicable, false otherwise. The return
110 * value can be used as a quick exit for the setTimeBlocks() method.
111 *
112 * TODO : Bucketing of partial FLSA days is still suspect, however real life examples of this are likely non-existent to rare.
113 *
114 * Danger may still lurk in day-boundary overlapping time blocks that have multiple Time Hour Detail entries.
115 */
116 private boolean applyBlock(TimeBlock block, List<TimeBlock> applyList) {
117 DateTime beginDateTime = new DateTime(block.getBeginTimestamp(), this.timeZone);
118 DateTime endDateTime = new DateTime(block.getEndTimestamp(), this.timeZone);
119
120 if (beginDateTime.isAfter(flsaDateInterval.getEnd()))
121 return false;
122
123 Interval timeBlockInterval = null;
124 //Requested to have zero hour time blocks be able to be added to the GUI
125 boolean zeroHoursTimeBlock = false;
126 if(endDateTime.getMillis() > beginDateTime.getMillis()){
127 timeBlockInterval = new Interval(beginDateTime,endDateTime);
128 }
129
130 if(flsaDateInterval.contains(beginDateTime)){
131 zeroHoursTimeBlock = true;
132 }
133
134 Interval overlapInterval = flsaDateInterval.overlap(timeBlockInterval);
135 long overlap = (overlapInterval == null) ? 0L : overlapInterval.toDurationMillis();
136 BigDecimal overlapHours = TKUtils.convertMillisToHours(overlap);
137 if((overlapHours.compareTo(BigDecimal.ZERO) == 0) && flsaDateInterval.contains(beginDateTime) && flsaDateInterval.contains(endDateTime)){
138 if(block.getHours().compareTo(BigDecimal.ZERO) > 0){
139 overlapHours = block.getHours();
140 }
141 }
142
143 // Local lookup for this time-block to ensure we are not over applicable hours.
144 // You will notice below we are earn codes globally per day, and also locally per timeblock.
145 // The local per-time block mapping is used only to verify that we have not gone over allocated overlap time
146 // for the individual time block.
147 Map<String,BigDecimal> localEarnCodeToHours = new HashMap<String,BigDecimal>();
148
149 if (zeroHoursTimeBlock || overlapHours.compareTo(BigDecimal.ZERO) > 0 || (flsaDateInterval.contains(beginDateTime) && StringUtils.equals(block.getEarnCodeType(),TkConstants.EARN_CODE_AMOUNT))) {
150
151 List<TimeHourDetail> details = block.getTimeHourDetails();
152 for (TimeHourDetail thd : details) {
153 BigDecimal ecHours = earnCodeToHours.containsKey(thd.getEarnCode()) ? earnCodeToHours.get(thd.getEarnCode()) : BigDecimal.ZERO;
154 BigDecimal localEcHours = localEarnCodeToHours.containsKey(thd.getEarnCode()) ? localEarnCodeToHours.get(thd.getEarnCode()) : BigDecimal.ZERO;
155 //NOTE adding this in the last few hours before release.. remove if side effects are noticed
156 if (overlapHours.compareTo(localEcHours) >= 0 || thd.getAmount().compareTo(BigDecimal.ZERO) == 0) {
157 ecHours = ecHours.add(thd.getHours(), TkConstants.MATH_CONTEXT);
158 localEcHours = localEcHours.add(thd.getHours(), TkConstants.MATH_CONTEXT);
159 earnCodeToHours.put(thd.getEarnCode(), ecHours);
160 localEarnCodeToHours.put(thd.getEarnCode(), localEcHours);
161 }
162 }
163
164 List<TimeBlock> blocks = earnCodeToTimeBlocks.get(block.getEarnCode());
165 if (blocks == null) {
166 blocks = new ArrayList<TimeBlock>();
167 earnCodeToTimeBlocks.put(block.getEarnCode(), blocks);
168 }
169 blocks.add(block);
170 applyList.add(block);
171 }
172
173 return true;
174 }
175
176 public Map<String, BigDecimal> getEarnCodeToHours() {
177 return earnCodeToHours;
178 }
179
180 public Map<String, List<TimeBlock>> getEarnCodeToTimeBlocks() {
181 return earnCodeToTimeBlocks;
182 }
183
184 public void setEarnCodeToTimeBlocks(Map<String, List<TimeBlock>> earnCodeToTimeBlocks) {
185 this.earnCodeToTimeBlocks = earnCodeToTimeBlocks;
186 }
187
188 public List<TimeBlock> getAppliedTimeBlocks() {
189 return appliedTimeBlocks;
190 }
191
192 public void setAppliedTimeBlocks(List<TimeBlock> appliedTimeBlocks) {
193 this.appliedTimeBlocks = appliedTimeBlocks;
194 }
195
196
197 }