View Javadoc

1   /**
2    * Copyright 2004-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.kpme.tklm.time.flsa;
17  
18  import java.math.BigDecimal;
19  import java.util.ArrayList;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  
24  import java.math.BigDecimal;
25  import java.util.ArrayList;
26  import java.util.HashMap;
27  import java.util.List;
28  import java.util.Map;
29  
30  import org.apache.commons.lang.StringUtils;
31  import org.joda.time.DateTime;
32  import org.joda.time.DateTimeZone;
33  import org.joda.time.Interval;
34  import org.joda.time.LocalDateTime;
35  import org.kuali.kpme.core.util.HrConstants;
36  import org.kuali.kpme.core.util.TKUtils;
37  import org.kuali.kpme.tklm.api.time.flsa.FlsaDayContract;
38  import org.kuali.kpme.tklm.leave.block.LeaveBlock;
39  import org.kuali.kpme.tklm.time.timeblock.TimeBlock;
40  import org.kuali.kpme.tklm.time.timehourdetail.TimeHourDetail;
41  
42  public class FlsaDay implements FlsaDayContract {
43  
44  	private Map<String,List<TimeBlock>> earnCodeToTimeBlocks = new HashMap<String,List<TimeBlock>>();
45  	private List<TimeBlock> appliedTimeBlocks = new ArrayList<TimeBlock>();
46  	
47  	private Map<String,List<LeaveBlock>> earnCodeToLeaveBlocks = new HashMap<String,List<LeaveBlock>>();
48  	private List<LeaveBlock> appliedLeaveBlocks = new ArrayList<LeaveBlock>();
49  
50  	Interval flsaDateInterval;
51  	LocalDateTime flsaDate;
52      DateTimeZone timeZone;
53  
54      /**
55       *
56       * @param flsaDate A LocalDateTime because we want to be conscious of the
57       * relative nature of this flsa/window
58       * @param timeBlocks
59       * @param timeZone The timezone we are constructing, relative.
60       */
61  	public FlsaDay(LocalDateTime flsaDate, List<TimeBlock> timeBlocks, List<LeaveBlock> leaveBlocks, DateTimeZone timeZone) {
62  		this.flsaDate = flsaDate;
63          this.timeZone = timeZone;
64  		flsaDateInterval = new Interval(flsaDate.toDateTime(timeZone), flsaDate.toDateTime(timeZone).plusDays(1));
65  		this.setTimeBlocks(timeBlocks);
66  		this.setLeaveBlocks(leaveBlocks);
67  	}
68  
69  	/**
70  	 * Handles the breaking apart of existing time blocks around FLSA boundaries.
71  	 *
72  	 * This method will compare the FLSA interval against the timeblock interval
73  	 * to determine how many hours overlap.  It will then examine the time hour
74  	 * details
75  	 *
76  	 * @param timeBlocks a sorted list of time blocks.
77  	 */
78  	public void setTimeBlocks(List<TimeBlock> timeBlocks) {
79  		for (TimeBlock block : timeBlocks) {
80              applyBlock(block, this.appliedTimeBlocks);
81          }
82  	}
83  	
84  	/**
85  	 * Handles the breaking apart of existing leave blocks around FLSA boundaries.
86  	 *
87  	 * This method will compare the FLSA interval against the leaveblock interval
88  	 * to determine how many hours overlap.  It will then examine the leave hour
89  	 * details
90  	 *
91  	 * @param leaveBlocks a sorted list of time blocks.
92  	 */
93  	public void setLeaveBlocks(List<LeaveBlock> leaveBlocks) {
94  		for (LeaveBlock block : leaveBlocks) {
95              applyBlock(block, this.appliedLeaveBlocks);
96          }
97  	}
98  
99  	/**
100 	 * This method will compute the mappings present for this object:
101 	 *
102 	 * earnCodeToTimeBlocks
103 	 * earnCodeToHours
104 	 *
105 	 */
106 	public void remapTimeHourDetails() {
107 		List<TimeBlock> reApplied = new ArrayList<TimeBlock>(appliedTimeBlocks.size());
108 		earnCodeToTimeBlocks.clear();
109 		for (TimeBlock block : appliedTimeBlocks) {
110 			applyBlock(block, reApplied);
111 		}
112 	}
113 	
114 	/**
115 	 * This method will compute the mappings present for this object:
116 	 *
117 	 * earnCodeToTimeBlocks
118 	 * earnCodeToHours
119 	 *
120 	 */
121 	public void remapLeaveHourDetails() {
122 		List<LeaveBlock> reApplied = new ArrayList<LeaveBlock>(appliedLeaveBlocks.size());
123 		earnCodeToLeaveBlocks.clear();
124 		for (LeaveBlock block : appliedLeaveBlocks) {
125 			applyBlock(block, reApplied);
126 		}
127 	}
128 
129 	/**
130      * This method determines if the provided TimeBlock is applicable to this
131      * FLSA day, and if so will add it to the applyList. It could be the case
132      * that a TimeBlock is on the boundary of the FLSA day so that only a
133      * partial amount of the hours for that TimeBlock will count towards this
134      * day.
135      *
136      * |---------+------------------+---------|
137      * | Day 1   | Day 1/2 Boundary | Day 2   |
138      * |---------+------------------+---------|
139      * | Block 1 |             | Block 2      |
140      * |---------+------------------+---------|
141      *
142      * The not so obvious ascii diagram above is intended to illustrate the case
143      * where on day one you have 1 fully overlapping time block (block1) and one
144      * partially overlapping time block (block2). Block 2 belongs to both FLSA
145      * Day 1 and Day 2.
146      *
147 	 * @param block A time block that we want to check and apply to this day.
148 	 * @param applyList A list of time blocks we want to add applicable time blocks to.
149 	 *
150 	 * @return True if the block is applicable, false otherwise.  The return
151 	 * value can be used as a quick exit for the setTimeBlocks() method.
152 	 *
153 	 * TODO : Bucketing of partial FLSA days is still suspect, however real life examples of this are likely non-existent to rare.
154      *
155      * Danger may still lurk in day-boundary overlapping time blocks that have multiple Time Hour Detail entries.
156 	 */
157 	private boolean applyBlock(TimeBlock block, List<TimeBlock> applyList) {
158 		DateTime beginDateTime = block.getBeginDateTime().withZone(timeZone);
159 		DateTime endDateTime = block.getEndDateTime().withZone(timeZone);
160 
161 		if (beginDateTime.isAfter(flsaDateInterval.getEnd()))
162 			return false;
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(),HrConstants.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(), HrConstants.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 
248 		Interval leaveBlockInterval = null;
249 		if(endDateTime.getMillis() > beginDateTime.getMillis()){
250 			leaveBlockInterval = new Interval(beginDateTime,endDateTime);
251 		}
252 
253 		Interval overlapInterval = flsaDateInterval.overlap(leaveBlockInterval);
254 		long overlap = (overlapInterval == null) ? 0L : overlapInterval.toDurationMillis();
255 		BigDecimal overlapHours = TKUtils.convertMillisToHours(overlap);
256 		if((overlapHours.compareTo(BigDecimal.ZERO) == 0) && flsaDateInterval.contains(beginDateTime) && flsaDateInterval.contains(endDateTime)){
257 			if(block.getLeaveAmount().negate().compareTo(BigDecimal.ZERO) > 0){
258 				overlapHours = block.getLeaveAmount().negate();
259 			}
260 		}
261 		
262 		if (overlapHours.compareTo(BigDecimal.ZERO) > 0)  {
263 			List<LeaveBlock> blocks = earnCodeToLeaveBlocks.get(block.getEarnCode());
264 			if (blocks == null) {
265 				blocks = new ArrayList<LeaveBlock>();
266 				earnCodeToLeaveBlocks.put(block.getEarnCode(), blocks);
267 			}
268 			blocks.add(block);
269 			applyList.add(block);
270 		}
271 
272 		return true;
273 	}
274 
275 	public Map<String, List<TimeBlock>> getEarnCodeToTimeBlocks() {
276 		return earnCodeToTimeBlocks;
277 	}
278 
279 	public void setEarnCodeToTimeBlocks(Map<String, List<TimeBlock>> earnCodeToTimeBlocks) {
280 		this.earnCodeToTimeBlocks = earnCodeToTimeBlocks;
281 	}
282 
283 	public List<TimeBlock> getAppliedTimeBlocks() {
284 		return appliedTimeBlocks;
285 	}
286 
287 	public void setAppliedTimeBlocks(List<TimeBlock> appliedTimeBlocks) {
288 		this.appliedTimeBlocks = appliedTimeBlocks;
289 	}
290 
291 	public Map<String, List<LeaveBlock>> getEarnCodeToLeaveBlocks() {
292 		return earnCodeToLeaveBlocks;
293 	}
294 
295 	public void setEarnCodeToLeaveBlocks(Map<String, List<LeaveBlock>> earnCodeToLeaveBlocks) {
296 		this.earnCodeToLeaveBlocks = earnCodeToLeaveBlocks;
297 	}
298 
299 	public List<LeaveBlock> getAppliedLeaveBlocks() {
300 		return appliedLeaveBlocks;
301 	}
302 
303 	public void setAppliedLeaveBlocks(List<LeaveBlock> appliedLeaveBlocks) {
304 		this.appliedLeaveBlocks = appliedLeaveBlocks;
305 	}
306 
307 	public LocalDateTime getFlsaDate() {
308 		return flsaDate;
309 	}
310 
311 	public void setFlsaDate(LocalDateTime flsaDate) {
312 		this.flsaDate = flsaDate;
313 	}
314 
315 }