View Javadoc

1   /**
2    * Copyright 2004-2012 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.hr.time.flsa;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.joda.time.DateTime;
20  import org.joda.time.DateTimeZone;
21  import org.joda.time.Interval;
22  import org.joda.time.LocalDateTime;
23  import org.kuali.hr.time.timeblock.TimeBlock;
24  import org.kuali.hr.time.timeblock.TimeHourDetail;
25  import org.kuali.hr.time.util.TKUtils;
26  import org.kuali.hr.time.util.TkConstants;
27  
28  import java.math.BigDecimal;
29  import java.util.ArrayList;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  
34  public class FlsaDay {
35  	private Map<String,BigDecimal> earnCodeToHours = new HashMap<String,BigDecimal>();
36  	private Map<String,List<TimeBlock>> earnCodeToTimeBlocks = new HashMap<String,List<TimeBlock>>();
37  	private List<TimeBlock> appliedTimeBlocks = new ArrayList<TimeBlock>();
38  
39  	Interval flsaDateInterval;
40  	LocalDateTime flsaDate;
41      DateTimeZone timeZone;
42  
43      /**
44       *
45       * @param flsaDate A LocalDateTime because we want to be conscious of the
46       * relative nature of this flsa/window
47       * @param timeBlocks
48       * @param timeZone The timezone we are constructing, relative.
49       */
50  	public FlsaDay(LocalDateTime flsaDate, List<TimeBlock> timeBlocks, DateTimeZone timeZone) {
51  		this.flsaDate = flsaDate;
52          this.timeZone = timeZone;
53  		flsaDateInterval = new Interval(flsaDate.toDateTime(timeZone), flsaDate.toDateTime(timeZone).plusHours(24));
54  		this.setTimeBlocks(timeBlocks);
55  	}
56  
57  	/**
58  	 * Handles the breaking apart of existing time blocks around FLSA boundaries.
59  	 *
60  	 * This method will compare the FLSA interval against the timeblock interval
61  	 * to determine how many hours overlap.  It will then examine the time hour
62  	 * details
63  	 *
64  	 * @param timeBlocks a sorted list of time blocks.
65  	 */
66  	public void setTimeBlocks(List<TimeBlock> timeBlocks) {
67  		for (TimeBlock block : timeBlocks)
68  			if (!applyBlock(block, this.appliedTimeBlocks))
69  				break;
70  	}
71  
72  	/**
73  	 * This method will compute the mappings present for this object:
74  	 *
75  	 * earnCodeToTimeBlocks
76  	 * earnCodeToHours
77  	 *
78  	 */
79  	public void remapTimeHourDetails() {
80  		List<TimeBlock> reApplied = new ArrayList<TimeBlock>(appliedTimeBlocks.size());
81  		earnCodeToHours.clear();
82  		earnCodeToTimeBlocks.clear();
83  		for (TimeBlock block : appliedTimeBlocks) {
84  			applyBlock(block, reApplied);
85  		}
86  	}
87  
88  	/**
89       * This method determines if the provided TimeBlock is applicable to this
90       * FLSA day, and if so will add it to the applyList. It could be the case
91       * that a TimeBlock is on the boundary of the FLSA day so that only a
92       * partial amount of the hours for that TimeBlock will count towards this
93       * day.
94       *
95       * |---------+------------------+---------|
96       * | Day 1   | Day 1/2 Boundary | Day 2   |
97       * |---------+------------------+---------|
98       * | Block 1 |             | Block 2      |
99       * |---------+------------------+---------|
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 }