function getShiftData($user_date_id = NULL, $user_id = NULL, $epoch = NULL, $filter = NULL, $tmp_punch_control_obj = NULL) { global $profiler; $profiler->startTimer('PayPeriodScheduleFactory::getShiftData()'); if (is_numeric($user_date_id) and $user_date_id > 0) { $user_id = $epoch = NULL; } if ($user_date_id == '' and $user_id == '' and $epoch == '') { return FALSE; } //Debug::text('User Date ID: '. $user_date_id .' User ID: '. $user_id .' TimeStamp: '. TTDate::getDate('DATE+TIME', $epoch), __FILE__, __LINE__, __METHOD__, 10); $new_shift_trigger_time = $this->getNewDayTriggerTime(); $plf = new PunchListFactory(); if ($user_date_id != '') { $plf->getByUserDateId($user_date_id); } else { //Get punches by time stamp. $punch_control_id = 0; if (is_object($tmp_punch_control_obj)) { $punch_control_id = $tmp_punch_control_obj->getId(); } $plf->getShiftPunchesByUserIDAndEpoch($user_id, $epoch, $punch_control_id, $this->getMaximumShiftTime()); unset($punch_control_id); } Debug::text('Punch Rows: ' . $plf->getRecordCount() . ' UserID: ' . $user_id . ' Date: ' . TTDate::getDate('DATE+TIME', $epoch) . '(' . $epoch . ') MaximumShiftTime: ' . $this->getMaximumShiftTime(), __FILE__, __LINE__, __METHOD__, 10); //Debug::Arr($punches, ' Punches: ', __FILE__, __LINE__, __METHOD__, 10); if ($plf->getRecordCount() > 0) { $shift = 0; $i = 0; $nearest_shift_id = 0; $nearest_punch_difference = FALSE; $prev_punch_obj = FALSE; foreach ($plf as $p_obj) { //Debug::text('Shift: '. $shift .' Punch ID: '. $p_obj->getID() .' Punch Control ID: '. $p_obj->getPunchControlID() .' TimeStamp: '. TTDate::getDate('DATE+TIME', $p_obj->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10); //If we're editing a punch, we need to use the object passed to this function instead of the one //from the database. if ($epoch == NULL) { //If user_date_id is passed without epoch, set epoch to the first punch we find. $epoch = $p_obj->getTimeStamp(); } if (isset($prev_punch_arr) and $p_obj->getTimeStamp() > $prev_punch_arr['time_stamp']) { $shift_data[$shift]['previous_punch_key'] = $i - 1; if ($shift_data[$shift]['previous_punch_key'] < 0) { $shift_data[$shift]['previous_punch_key'] = NULL; } } //Determine if a non-saved PunchControl object was passed, and if so, match the IDs to use that instead. if (is_object($tmp_punch_control_obj) and $p_obj->getPunchControlID() == $tmp_punch_control_obj->getId()) { Debug::text('Passed non-saved punch control object that matches, using that instead... Using ID: ' . (int) $tmp_punch_control_obj->getId(), __FILE__, __LINE__, __METHOD__, 10); $punch_control_obj = $tmp_punch_control_obj; } else { $punch_control_obj = $p_obj->getPunchControlObject(); } //Can't use PunchControl object total_time because the record may not be saved yet when editing //an already existing punch. //When editing, simply pass the existing PunchControl object to this function so we can //use it instead of the one in the database perhaps? $total_time = $punch_control_obj->getTotalTime(); /* //We can't skip records with total_time == 0, because then when deleting one of the two //punches in a pair, the remaining punch is ignored and causing punches to jump around between days in some cases. if ( $total_time == 0 ) { Debug::text('Total time is 0, skipping this punch control object...', __FILE__, __LINE__, __METHOD__, 10); //continue; } */ if ($i > 0 and isset($shift_data[$shift]['last_out']) and ($p_obj->getStatus() == 10 or $p_obj->getStatus() == $prev_punch_arr['status_id'])) { Debug::text('Checking for new shift...', __FILE__, __LINE__, __METHOD__, 10); if ($p_obj->getTimeStamp() - $shift_data[$shift]['last_out']['time_stamp'] > $new_shift_trigger_time) { $shift++; } } if (!isset($shift_data[$shift]['total_time'])) { $shift_data[$shift]['total_time'] = 0; } $punch_day_epoch = TTDate::getBeginDayEpoch($p_obj->getTimeStamp()); if (!isset($shift_data[$shift]['total_time_per_day'][$punch_day_epoch])) { $shift_data[$shift]['total_time_per_day'][$punch_day_epoch] = 0; } //Determine which shift is closest to the given epoch. $punch_difference_from_epoch = abs($epoch - $p_obj->getTimeStamp()); if ($nearest_punch_difference === FALSE or $punch_difference_from_epoch < $nearest_punch_difference) { Debug::text('Nearest Shift Determined to be: ' . $shift . ' Nearest Punch Diff: ' . (int) $nearest_punch_difference . ' Punch Diff: ' . $punch_difference_from_epoch, __FILE__, __LINE__, __METHOD__, 10); $nearest_shift_id = $shift; $nearest_punch_difference = $punch_difference_from_epoch; } $punch_arr = array('id' => $p_obj->getId(), 'punch_control_id' => $p_obj->getPunchControlId(), 'user_date_id' => $punch_control_obj->getUserDateID(), 'time_stamp' => $p_obj->getTimeStamp(), 'status_id' => $p_obj->getStatus(), 'type_id' => $p_obj->getType()); $shift_data[$shift]['punches'][] = $punch_arr; $shift_data[$shift]['punch_control_ids'][] = $p_obj->getPunchControlId(); if ($punch_control_obj->getUserDateID() != FALSE) { $shift_data[$shift]['user_date_ids'][] = $punch_control_obj->getUserDateID(); } $shift_data[$shift]['span_midnight'] = FALSE; if (!isset($shift_data[$shift]['first_in']) and $p_obj->getStatus() == 10) { //Debug::text('First In -- Punch ID: '. $p_obj->getID() .' Punch Control ID: '. $p_obj->getPunchControlID() .' TimeStamp: '. TTDate::getDate('DATE+TIME', $p_obj->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10); $shift_data[$shift]['first_in'] = $punch_arr; } elseif ($p_obj->getStatus() == 20) { //Debug::text('Last Out -- Punch ID: '. $p_obj->getID() .' Punch Control ID: '. $p_obj->getPunchControlID() .' TimeStamp: '. TTDate::getDate('DATE+TIME', $p_obj->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10); $shift_data[$shift]['last_out'] = $punch_arr; //Debug::text('Total Time: '. $total_time, __FILE__, __LINE__, __METHOD__, 10); $shift_data[$shift]['total_time'] += $total_time; //Check to see if the previous punch was on a different day then the current punch. if (isset($prev_punch_arr) and is_array($prev_punch_arr) and ($p_obj->getStatus() == 20 and $prev_punch_arr['status_id'] != 20) and TTDate::doesRangeSpanMidnight($prev_punch_arr['time_stamp'], $p_obj->getTimeStamp()) == TRUE) { Debug::text('Punch pair DOES span midnight', __FILE__, __LINE__, __METHOD__, 10); $shift_data[$shift]['span_midnight'] = TRUE; $total_time_for_each_day_arr = TTDate::calculateTimeOnEachDayBetweenRange($prev_punch_arr['time_stamp'], $p_obj->getTimeStamp()); if (is_array($total_time_for_each_day_arr)) { foreach ($total_time_for_each_day_arr as $begin_day_epoch => $day_total_time) { if (!isset($shift_data[$shift]['total_time_per_day'][$begin_day_epoch])) { $shift_data[$shift]['total_time_per_day'][$begin_day_epoch] = 0; } $shift_data[$shift]['total_time_per_day'][$begin_day_epoch] += $day_total_time; } } unset($total_time_for_each_day_arr, $begin_day_epoch, $day_total_time, $prev_day_total_time); } else { $shift_data[$shift]['total_time_per_day'][$punch_day_epoch] += $total_time; } } $prev_punch_arr = $punch_arr; $i++; } //Debug::Arr($shift_data, 'aShift Data:', __FILE__, __LINE__, __METHOD__, 10); if (isset($shift_data)) { //Loop through each shift to determine the day with the most time. foreach ($shift_data as $tmp_shift_key => $tmp_shift_data) { krsort($shift_data[$tmp_shift_key]['total_time_per_day']); //Sort by day first arsort($shift_data[$tmp_shift_key]['total_time_per_day']); //Sort by total time per day. reset($shift_data[$tmp_shift_key]['total_time_per_day']); $shift_data[$tmp_shift_key]['day_with_most_time'] = key($shift_data[$tmp_shift_key]['total_time_per_day']); $shift_data[$tmp_shift_key]['punch_control_ids'] = array_unique($shift_data[$tmp_shift_key]['punch_control_ids']); if (isset($shift_data[$tmp_shift_key]['user_date_ids'])) { $shift_data[$tmp_shift_key]['user_date_ids'] = array_unique($shift_data[$tmp_shift_key]['user_date_ids']); } } unset($tmp_shift_key, $tmp_shift_data); if ($filter == 'first_shift') { //Only return first shift. $shift_data = $shift_data[0]; } elseif ($filter == 'last_shift') { //Only return last shift. $shift_data = $shift_data[$shift]; } elseif ($filter == 'nearest_shift') { $shift_data = $shift_data[$nearest_shift_id]; //Check to make sure the nearest shift is within the new shift trigger time of EPOCH. if (isset($shift_data['first_in']['time_stamp'])) { $first_in = $shift_data['first_in']['time_stamp']; } elseif (isset($shift_data['last_out']['time_stamp'])) { $first_in = $shift_data['last_out']['time_stamp']; } if (isset($shift_data['last_out']['time_stamp'])) { $last_out = $shift_data['last_out']['time_stamp']; } elseif (isset($shift_data['first_in']['time_stamp'])) { $last_out = $shift_data['first_in']['time_stamp']; } if (TTDate::isTimeOverLap($epoch, $epoch, $first_in - $new_shift_trigger_time, $last_out + $new_shift_trigger_time) == FALSE) { Debug::Text('Nearest shift is outside the new shift trigger time... Epoch: ' . $epoch . ' First In: ' . $first_in . ' Last Out: ' . $last_out . ' New Shift Trigger: ' . $new_shift_trigger_time, __FILE__, __LINE__, __METHOD__, 10); return FALSE; } unset($first_in, $last_out); } $profiler->stopTimer('PayPeriodScheduleFactory::getShiftData()'); //Debug::Arr($shift_data, 'bShift Data:', __FILE__, __LINE__, __METHOD__, 10); return $shift_data; } } $profiler->stopTimer('PayPeriodScheduleFactory::getShiftData()'); return FALSE; }
function preSave() { if ($this->isNew()) { //Debug::text(' Setting Original TimeStamp: '. $this->getTimeStamp(), __FILE__, __LINE__, __METHOD__,10); $this->setOriginalTimeStamp($this->getTimeStamp()); } if ($this->getDeleted() == FALSE) { if ($this->getTransfer() == TRUE and $this->getEnableAutoTransfer() == TRUE) { Debug::text(' Transfer is Enabled, automatic punch out of last punch pair: ', __FILE__, __LINE__, __METHOD__, 10); //Check to make sure there is an open punch pair. $plf = new PunchListFactory(); $plf->getPreviousPunchByUserIdAndEpoch($this->getUser(), $this->getTimeStamp()); if ($plf->getRecordCount() > 0) { $p_obj = $plf->getCurrent(); Debug::text(' Found Last Punch: ', __FILE__, __LINE__, __METHOD__, 10); if ($p_obj->getStatus() == 10) { Debug::text(' Last Punch was in. Auto Punch Out now: ', __FILE__, __LINE__, __METHOD__, 10); //Make sure the current punch status is IN $this->setStatus(10); //In $this->setType(10); //Normal (can't transfer in/out of lunches?) $pf = new PunchFactory(); $pf->setUser($this->getUser()); $pf->setEnableAutoTransfer(FALSE); $pf->setPunchControlID($p_obj->getPunchControlID()); $pf->setTransfer(TRUE); $pf->setType($p_obj->getNextType()); $pf->setStatus(20); //Out $pf->setTimeStamp($this->getTimeStamp(), FALSE); //Disable rounding. $pf->setActualTimeStamp($this->getTimeStamp()); $pf->setOriginalTimeStamp($this->getTimeStamp()); if ($pf->isValid()) { if ($pf->Save(FALSE) == TRUE) { $p_obj->getPunchControlObject()->setEnableCalcTotalTime(TRUE); $p_obj->getPunchControlObject()->setEnableCalcSystemTotalTime(TRUE); $p_obj->getPunchControlObject()->setEnableCalcUserDateTotal(TRUE); $p_obj->getPunchControlObject()->setEnableCalcException(TRUE); $p_obj->getPunchControlObject()->setEnablePreMatureException(TRUE); if ($p_obj->getPunchControlObject()->isValid()) { $p_obj->getPunchControlObject()->Save(); } else { Debug::text(' aError saving auto out punch...', __FILE__, __LINE__, __METHOD__, 10); } } else { Debug::text(' bError saving auto out punch...', __FILE__, __LINE__, __METHOD__, 10); } } else { Debug::text(' cError saving auto out punch...', __FILE__, __LINE__, __METHOD__, 10); } } else { Debug::text(' Last Punch was out. No Auto Punch ', __FILE__, __LINE__, __METHOD__, 10); } } unset($plf, $p_obj, $pf); } //Split punch at midnight. //This has to be an Out punch, and the previous punch has to be an in punch in order for the split to occur. //Check to make sure there is an open punch pair. //Make sure this punch isn't right at midnight either, as no point in splitting a punch at that time. //FIXME: What happens if a supervisor edits a 11:30PM punch and makes it 5:00AM the next day? // We can't split punches when editing, because we have to split punch_control_ids prior to saving etc... if ($this->isNew() == TRUE and $this->getStatus() == 20 and $this->getEnableSplitAtMidnight() == TRUE and $this->getTimeStamp() != TTDate::getBeginDayEpoch($this->getTimeStamp()) and (is_object($this->getPunchControlObject()) and is_object($this->getPunchControlObject()->getPayPeriodScheduleObject()) and $this->getPunchControlObject()->getPayPeriodScheduleObject()->getShiftAssignedDay() == 40)) { $plf = new PunchListFactory(); $plf->getPreviousPunchByUserIdAndEpoch($this->getUser(), $this->getTimeStamp()); if ($plf->getRecordCount() > 0) { $p_obj = $plf->getCurrent(); Debug::text(' Found Last Punch: ', __FILE__, __LINE__, __METHOD__, 10); if ($p_obj->getStatus() == 10 and TTDate::doesRangeSpanMidnight($this->getTimeStamp(), $p_obj->getTimeStamp())) { Debug::text(' Last Punch was in and this is an out punch that spans midnight. Split Punch at midnight now: ', __FILE__, __LINE__, __METHOD__, 10); //FIXME: This will fail if a shift spans multiple days! //Make sure the current punch status is OUT //But we can split LUNCH/Break punches, because someone could punch in at 8PM, then out for lunch at 1:00AM, this would need to be split. $this->setStatus(20); //Out //Reduce the out punch by 60 seconds, and increase the current punch by 60seconds so no time is lost. $this->setTimeStamp($this->getTimeStamp() + 60); //FIXME: May need to use ActualTimeStamp here so we aren't double rounding. //Get new punch control ID for the midnight punch and this one. $new_punch_control_id = $this->getPunchControlObject()->getNextInsertId(); $this->setPunchControlID($new_punch_control_id); Debug::text(' Split Punch: Punching out just before midnight yesterday...', __FILE__, __LINE__, __METHOD__, 10); // //Punch out just before midnight // $pf = new PunchFactory(); $pf->setUser($this->getUser()); $pf->setEnableSplitAtMidnight(FALSE); $pf->setTransfer(FALSE); $pf->setEnableAutoTransfer(FALSE); $pf->setType(10); //Normal $pf->setStatus(20); //Out $before_midnight_timestamp = TTDate::getBeginDayEpoch($this->getTimeStamp()) - 60; $pf->setTimeStamp($before_midnight_timestamp, FALSE); //Disable rounding. $pf->setActualTimeStamp($before_midnight_timestamp); $pf->setOriginalTimeStamp($before_midnight_timestamp); $pf->setPunchControlID($p_obj->getPunchControlID()); if ($pf->isValid()) { if ($pf->Save(FALSE) == TRUE) { $p_obj->getPunchControlObject()->setEnableCalcTotalTime(TRUE); $p_obj->getPunchControlObject()->setEnableCalcSystemTotalTime(TRUE); $p_obj->getPunchControlObject()->setEnableCalcUserDateTotal(TRUE); $p_obj->getPunchControlObject()->setEnableCalcException(TRUE); $p_obj->getPunchControlObject()->setEnablePreMatureException(TRUE); $p_obj->getPunchControlObject()->Save(); } } unset($pf, $p_obj, $before_midnight_timestamp); Debug::text(' Split Punch: Punching int at midnight today...', __FILE__, __LINE__, __METHOD__, 10); // //Punch in again right at midnight. // $pf = new PunchFactory(); $pf->setUser($this->getUser()); $pf->setEnableSplitAtMidnight(FALSE); $pf->setTransfer(FALSE); $pf->setEnableAutoTransfer(FALSE); $pf->setType(10); //Normal $pf->setStatus(10); //In $at_midnight_timestamp = TTDate::getBeginDayEpoch($this->getTimeStamp()); $pf->setTimeStamp($at_midnight_timestamp, FALSE); //Disable rounding. $pf->setActualTimeStamp($at_midnight_timestamp); $pf->setOriginalTimeStamp($at_midnight_timestamp); $pf->setPunchControlID($new_punch_control_id); if ($pf->isValid()) { if ($pf->Save(FALSE) == TRUE) { $pcf = new PunchControlFactory(); $pcf->setId($pf->getPunchControlID()); $pcf->setPunchObject($pf); $pcf->setBranch($this->getPunchControlObject()->getBranch()); $pcf->setDepartment($this->getPunchControlObject()->getDepartment()); $pcf->setJob($this->getPunchControlObject()->getJob()); $pcf->setJobItem($this->getPunchControlObject()->getJobItem()); $pcf->setOtherID1($this->getPunchControlObject()->getOtherID1()); $pcf->setOtherID2($this->getPunchControlObject()->getOtherID2()); $pcf->setOtherID3($this->getPunchControlObject()->getOtherID3()); $pcf->setOtherID4($this->getPunchControlObject()->getOtherID4()); $pcf->setOtherID5($this->getPunchControlObject()->getOtherID5()); $pcf->setEnableStrictJobValidation(TRUE); $pcf->setEnableCalcUserDateID(TRUE); $pcf->setEnableCalcTotalTime(TRUE); $pcf->setEnableCalcSystemTotalTime(TRUE); $pcf->setEnableCalcWeeklySystemTotalTime(TRUE); $pcf->setEnableCalcUserDateTotal(TRUE); $pcf->setEnableCalcException(TRUE); if ($pcf->isValid() == TRUE) { $pcf->Save(TRUE, TRUE); //Force isNEW() lookup. } } } unset($pf, $at_midnight_timestamp, $new_punch_control_id); } else { Debug::text(' Last Punch was out. No Auto Punch ', __FILE__, __LINE__, __METHOD__, 10); } } } } return TRUE; }
function getShiftData($user_date_id = NULL, $user_id = NULL, $epoch = NULL, $filter = NULL, $tmp_punch_control_obj = NULL, $maximum_shift_time = NULL, $new_shift_trigger_time = NULL, $plf = NULL) { global $profiler; $profiler->startTimer('PayPeriodScheduleFactory::getShiftData()'); if (is_numeric($user_date_id) and $user_date_id > 0) { $user_id = $epoch = NULL; } if ($user_date_id == '' and $user_id == '' and $epoch == '') { return FALSE; } if ($maximum_shift_time === NULL) { $maximum_shift_time = $this->getMaximumShiftTime(); } //Debug::text('User Date ID: '. $user_date_id .' User ID: '. $user_id .' TimeStamp: '. TTDate::getDate('DATE+TIME', $epoch), __FILE__, __LINE__, __METHOD__, 10); if ($new_shift_trigger_time === NULL) { $new_shift_trigger_time = $this->getNewDayTriggerTime(); } if (!is_object($plf)) { $plf = TTnew('PunchListFactory'); if ($user_date_id != '') { $plf->getByUserDateId($user_date_id); } else { //Get punches by time stamp. $punch_control_id = 0; if (is_object($tmp_punch_control_obj)) { $punch_control_id = $tmp_punch_control_obj->getId(); } //We need to double the maximum shift time when searching for punches. //Assuming a maximum punch time of 14hrs: // In: 10:00AM Out: 2:00PM // In: 6:00PM Out: 6:00AM (next day) // The above scenario when adding the last 6:00AM punch on the next day will only look back 14hrs and not find the first // punch pair, therefore allowing more than 14hrs on the same day. // So we need to extend the maximum shift time just when searching for punches and let getShiftData() sort out the proper maximum shift time itself. $plf->getShiftPunchesByUserIDAndEpoch($user_id, $epoch, $punch_control_id, $maximum_shift_time * 2); unset($punch_control_id); } } Debug::text('Punch Rows: ' . $plf->getRecordCount() . ' UserID: ' . $user_id . ' Date: ' . TTDate::getDate('DATE+TIME', $epoch) . '(' . $epoch . ') MaximumShiftTime: ' . $maximum_shift_time . ' Filter: ' . $filter, __FILE__, __LINE__, __METHOD__, 10); if ($plf->getRecordCount() > 0) { $shift = 0; $i = 0; $nearest_shift_id = 0; $nearest_punch_difference = FALSE; $prev_punch_obj = FALSE; foreach ($plf as $p_obj) { //Debug::text('Shift: '. $shift .' Punch ID: '. $p_obj->getID() .' Punch Control ID: '. $p_obj->getPunchControlID() .' TimeStamp: '. TTDate::getDate('DATE+TIME', $p_obj->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10); //If we're editing a punch, we need to use the object passed to this function instead of the one //from the database. if ($epoch == NULL) { //If user_date_id is passed without epoch, set epoch to the first punch we find. $epoch = $p_obj->getTimeStamp(); } if (isset($prev_punch_arr) and $p_obj->getTimeStamp() > $prev_punch_arr['time_stamp']) { $shift_data[$shift]['previous_punch_key'] = $i - 1; if ($shift_data[$shift]['previous_punch_key'] < 0) { $shift_data[$shift]['previous_punch_key'] = NULL; } } //Determine if a non-saved PunchControl object was passed, and if so, match the IDs to use that instead. if (is_object($tmp_punch_control_obj) and $p_obj->getPunchControlID() == $tmp_punch_control_obj->getId()) { Debug::text('Passed non-saved punch control object that matches, using that instead... Using ID: ' . (int) $tmp_punch_control_obj->getId(), __FILE__, __LINE__, __METHOD__, 10); $punch_control_obj = $tmp_punch_control_obj; } else { $punch_control_obj = $p_obj->getPunchControlObject(); } //Can't use PunchControl object total_time because the record may not be saved yet when editing //an already existing punch. //When editing, simply pass the existing PunchControl object to this function so we can //use it instead of the one in the database perhaps? $total_time = $punch_control_obj->getTotalTime(); //We can't skip records with total_time == 0, because then when deleting one of the two //punches in a pair, the remaining punch is ignored and causing punches to jump around between days in some cases. if ($i > 0 and isset($shift_data[$shift]['last_out']) and ($p_obj->getStatus() == 10 or $p_obj->getStatus() == $prev_punch_arr['status_id'])) { Debug::text('Checking for new shift... This Control ID: ' . $p_obj->getPunchControlID() . ' Last Out Control ID: ' . $shift_data[$shift]['last_out']['punch_control_id'] . ' Last Out Time: ' . TTDate::getDate('DATE+TIME', $shift_data[$shift]['last_out']['time_stamp']), __FILE__, __LINE__, __METHOD__, 10); //Assume that if two punches are assigned to the same punch_control_id are the same shift, even if the time between //them exceeds the new_shift_trigger_time. This helps fix the bug where you could add a In punch then add a Out //punch BEFORE the In punch as long as it was more than the Maximum Shift Time before the In Punch. //ie: Add: In Punch 10-Dec-09 @ 8:00AM, Add: Out Punch 09-Dec-09 @ 5:00PM. //Basically it just helps the validation checks to determine the error. // //It used to be that if shifts are split at midnight, new_shift_trigger_time must be 0, so the "split" punch can occur at midnight. //However we have since added a check to see if punches span midnight and trigger a new shift based on that, regardless of the new shift trigger time. //As the new_shift_trigger_time of 0 also affected lunch/break automatic detection by Punch Time, since an Out punch and a In punch of any time //would trigger a new shift, and it wouldn't be detected as lunch/break. // //What happens when the employee takes lunch/break over midnight? Lunch out at 11:30PM Lunch IN at 12:30AM // We need to split those into two lunches, or two breaks? But then that can affect those policies if they are only allowed one break. // Or do we not split the shift at all when this occurs? Currently we don't split at all. if ($p_obj->getPunchControlID() != $shift_data[$shift]['last_out']['punch_control_id'] and ($p_obj->getTimeStamp() - $shift_data[$shift]['last_out']['time_stamp'] >= $new_shift_trigger_time or $this->getShiftAssignedDay() == 40 and $p_obj->getType() == 10 and $shift_data[$shift]['last_out']['type_id'] == 10 and TTDate::doesRangeSpanMidnight($shift_data[$shift]['last_out']['time_stamp'], $p_obj->getTimeStamp(), TRUE) == TRUE)) { $shift++; } } elseif ($i > 0 and isset($prev_punch_arr['time_stamp']) and $prev_punch_arr['punch_control_id'] != $p_obj->getPunchControlId() and abs($prev_punch_arr['time_stamp'] - $p_obj->getTimeStamp()) > $maximum_shift_time) { //Debug::text(' New shift because two punch_control records exist and punch timestamp exceed maximum shift time.', __FILE__, __LINE__, __METHOD__, 10); $shift++; } if (!isset($shift_data[$shift]['total_time'])) { $shift_data[$shift]['total_time'] = 0; } $punch_day_epoch = TTDate::getBeginDayEpoch($p_obj->getTimeStamp()); if (!isset($shift_data[$shift]['total_time_per_day'][$punch_day_epoch])) { $shift_data[$shift]['total_time_per_day'][$punch_day_epoch] = 0; } //Determine which shift is closest to the given epoch. $punch_difference_from_epoch = abs($epoch - $p_obj->getTimeStamp()); if ($nearest_punch_difference === FALSE or $punch_difference_from_epoch <= $nearest_punch_difference) { Debug::text('Nearest Shift Determined to be: ' . $shift . ' Nearest Punch Diff: ' . (int) $nearest_punch_difference . ' Punch Diff: ' . $punch_difference_from_epoch, __FILE__, __LINE__, __METHOD__, 10); //If two punches have the same timestamp, use the shift that matches the passed punch control object, which is usually the one we are currently editing... //This is for splitting shifts at exactly midnight. if ($punch_difference_from_epoch != $nearest_punch_difference or $punch_difference_from_epoch == $nearest_punch_difference and (is_object($tmp_punch_control_obj) and $tmp_punch_control_obj->getId() == $p_obj->getPunchControlID())) { //Debug::text('Found two punches with the same timestamp... Tmp Punch Control: '.$tmp_punch_control_obj->getId() .' Punch Control: '. $p_obj->getPunchControlID() , __FILE__, __LINE__, __METHOD__, 10); $nearest_shift_id = $shift; $nearest_punch_difference = $punch_difference_from_epoch; } } $punch_arr = array('id' => $p_obj->getId(), 'punch_control_id' => $p_obj->getPunchControlId(), 'user_date_id' => $punch_control_obj->getUserDateID(), 'time_stamp' => $p_obj->getTimeStamp(), 'status_id' => $p_obj->getStatus(), 'type_id' => $p_obj->getType()); $shift_data[$shift]['punches'][] = $punch_arr; $shift_data[$shift]['punch_control_ids'][] = $p_obj->getPunchControlId(); if ($punch_control_obj->getUserDateID() != FALSE) { $shift_data[$shift]['user_date_ids'][] = $punch_control_obj->getUserDateID(); } if (!isset($shift_data[$shift]['span_midnight'])) { $shift_data[$shift]['span_midnight'] = FALSE; } if (!isset($shift_data[$shift]['first_in']) and $p_obj->getStatus() == 10) { //Debug::text('First In -- Punch ID: '. $p_obj->getID() .' Punch Control ID: '. $p_obj->getPunchControlID() .' TimeStamp: '. TTDate::getDate('DATE+TIME', $p_obj->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10); $shift_data[$shift]['first_in'] = $punch_arr; } elseif ($p_obj->getStatus() == 20) { //Debug::text('Last Out -- Punch ID: '. $p_obj->getID() .' Punch Control ID: '. $p_obj->getPunchControlID() .' TimeStamp: '. TTDate::getDate('DATE+TIME', $p_obj->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10); $shift_data[$shift]['last_out'] = $punch_arr; //Debug::text('Total Time: '. $total_time, __FILE__, __LINE__, __METHOD__, 10); $shift_data[$shift]['total_time'] += $total_time; //Check to see if the previous punch was on a different day then the current punch. if (isset($prev_punch_arr) and is_array($prev_punch_arr) and ($p_obj->getStatus() == 20 and $prev_punch_arr['status_id'] != 20) and TTDate::doesRangeSpanMidnight($prev_punch_arr['time_stamp'], $p_obj->getTimeStamp()) == TRUE) { Debug::text('Punch PAIR DOES span midnight', __FILE__, __LINE__, __METHOD__, 10); $shift_data[$shift]['span_midnight'] = TRUE; $total_time_for_each_day_arr = TTDate::calculateTimeOnEachDayBetweenRange($prev_punch_arr['time_stamp'], $p_obj->getTimeStamp()); if (is_array($total_time_for_each_day_arr)) { foreach ($total_time_for_each_day_arr as $begin_day_epoch => $day_total_time) { if (!isset($shift_data[$shift]['total_time_per_day'][$begin_day_epoch])) { $shift_data[$shift]['total_time_per_day'][$begin_day_epoch] = 0; } $shift_data[$shift]['total_time_per_day'][$begin_day_epoch] += $day_total_time; } } unset($total_time_for_each_day_arr, $begin_day_epoch, $day_total_time, $prev_day_total_time); } else { $shift_data[$shift]['total_time_per_day'][$punch_day_epoch] += $total_time; } } $prev_punch_arr = $punch_arr; $i++; } //Debug::Arr($shift_data, 'aShift Data:', __FILE__, __LINE__, __METHOD__, 10); if (isset($shift_data)) { //Loop through each shift to determine the day with the most time. foreach ($shift_data as $tmp_shift_key => $tmp_shift_data) { krsort($shift_data[$tmp_shift_key]['total_time_per_day']); //Sort by day first arsort($shift_data[$tmp_shift_key]['total_time_per_day']); //Sort by total time per day. reset($shift_data[$tmp_shift_key]['total_time_per_day']); $shift_data[$tmp_shift_key]['day_with_most_time'] = key($shift_data[$tmp_shift_key]['total_time_per_day']); $shift_data[$tmp_shift_key]['punch_control_ids'] = array_unique($shift_data[$tmp_shift_key]['punch_control_ids']); if (isset($shift_data[$tmp_shift_key]['user_date_ids'])) { $shift_data[$tmp_shift_key]['user_date_ids'] = array_unique($shift_data[$tmp_shift_key]['user_date_ids']); } } unset($tmp_shift_key, $tmp_shift_data); if ($filter == 'first_shift') { //Only return first shift. $shift_data = $shift_data[0]; } elseif ($filter == 'last_shift') { //Only return last shift. $shift_data = $shift_data[$shift]; } elseif ($filter == 'nearest_shift') { $shift_data = $shift_data[$nearest_shift_id]; //Check to make sure the nearest shift is within the new shift trigger time of EPOCH. if (isset($shift_data['first_in']['time_stamp'])) { $first_in = $shift_data['first_in']['time_stamp']; } elseif (isset($shift_data['last_out']['time_stamp'])) { $first_in = $shift_data['last_out']['time_stamp']; } if (isset($shift_data['last_out']['time_stamp'])) { $last_out = $shift_data['last_out']['time_stamp']; } elseif (isset($shift_data['first_in']['time_stamp'])) { $last_out = $shift_data['first_in']['time_stamp']; } //The check below must occur so if the user attempts to add an In punch that occurs AFTER the Out punch, this function //still returns the shift data, so the validation checks can occur in PunchControl factory. if ($first_in > $last_out) { //It appears that the first in punch has occurred after the OUT punch, so swap first_in and last_out, so we don't return FALSE in this case. list($first_in, $last_out) = array($last_out, $first_in); } if (TTDate::isTimeOverLap($epoch, $epoch, $first_in - $new_shift_trigger_time, $last_out + $new_shift_trigger_time) == FALSE) { Debug::Text('Nearest shift is outside the new shift trigger time... Epoch: ' . $epoch . ' First In: ' . $first_in . ' Last Out: ' . $last_out . ' New Shift Trigger: ' . $new_shift_trigger_time, __FILE__, __LINE__, __METHOD__, 10); return FALSE; } unset($first_in, $last_out); } $profiler->stopTimer('PayPeriodScheduleFactory::getShiftData()'); //Debug::Arr($shift_data, 'bShift Data:', __FILE__, __LINE__, __METHOD__, 10); return $shift_data; } } $profiler->stopTimer('PayPeriodScheduleFactory::getShiftData()'); return FALSE; }
public static function calculateTimeOnEachDayBetweenRange($start_epoch, $end_epoch) { if (TTDate::doesRangeSpanMidnight($start_epoch, $end_epoch) == TRUE) { $total_before_first_midnight = TTDate::getEndDayEpoch($start_epoch) + 1 - $start_epoch; if ($total_before_first_midnight > 0) { $retval[TTDate::getBeginDayEpoch($start_epoch)] = $total_before_first_midnight; } $loop_start = TTDate::getEndDayEpoch($start_epoch) + 1; $loop_end = TTDate::getBeginDayEpoch($end_epoch); for ($x = $loop_start; $x < $loop_end; $x += 86400) { $retval[TTDate::getBeginDayEpoch($x)] = 86400; } $total_after_last_midnight = $end_epoch - TTDate::getBeginDayEpoch($end_epoch); if ($total_after_last_midnight > 0) { $retval[TTDate::getBeginDayEpoch($end_epoch)] = $total_after_last_midnight; } } else { $retval = array(TTDate::getBeginDayEpoch($start_epoch) => $end_epoch - $start_epoch); } return $retval; }
static function calcExceptions($user_date_id, $enable_premature_exceptions = FALSE, $enable_future_exceptions = TRUE) { global $profiler; $profiler->startTimer("ExceptionPolicy::calcExceptions()"); if ($user_date_id == '') { return FALSE; } Debug::text(' User Date ID: ' . $user_date_id . ' PreMature: ' . (int) $enable_premature_exceptions, __FILE__, __LINE__, __METHOD__, 10); $current_epoch = TTDate::getTime(); //Get user date info $udlf = TTnew('UserDateListFactory'); $udlf->getById($user_date_id); if ($udlf->getRecordCount() > 0) { $user_date_obj = $udlf->getCurrent(); if ($enable_future_exceptions == FALSE and $user_date_obj->getDateStamp() > TTDate::getEndDayEpoch($current_epoch)) { return FALSE; } } else { return FALSE; } //16hrs... If punches are older then this time, its no longer premature. //This should actually be the PayPeriod Schedule maximum shift time. if (is_object($user_date_obj->getPayPeriodObject()) and is_object($user_date_obj->getPayPeriodObject()->getPayPeriodScheduleObject())) { self::$premature_delay = $user_date_obj->getPayPeriodObject()->getPayPeriodScheduleObject()->getMaximumShiftTime(); Debug::text(' Setting preMature Exception delay to maximum shift time: ' . self::$premature_delay, __FILE__, __LINE__, __METHOD__, 10); } else { self::$premature_delay = 57600; } //Get list of existing exceptions, so we can determine if we need to delete any. We can't delete them all blindly and re-create them //as this will send duplicate email notifications for every single punch. $existing_exceptions = array(); $elf = TTnew('ExceptionListFactory'); $elf->getByUserDateID($user_date_id); if ($elf->getRecordCount() > 0) { foreach ($elf as $e_obj) { $existing_exceptions[] = array('id' => $e_obj->getId(), 'user_date_id' => $e_obj->getUserDateID(), 'exception_policy_id' => $e_obj->getExceptionPolicyID(), 'type_id' => $e_obj->getType(), 'punch_id' => $e_obj->getPunchID(), 'punch_control_id' => $e_obj->getPunchControlID()); } } unset($elf, $e_obj); //Get all Punches on this date for this user. $plf = TTnew('PunchListFactory'); $plf->getByUserDateId($user_date_id); if ($plf->getRecordCount() > 0) { Debug::text(' Found Punches: ' . $plf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10); } $slf = TTnew('ScheduleListFactory'); $slf->getByUserDateIdAndStatusId($user_date_id, 10); if ($slf->getRecordCount() > 0) { Debug::text(' Found Schedule: ' . $slf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10); } $schedule_id_cache = NULL; //Cache schedule IDs so we don't need to do a lookup for every exception. $current_exceptions = array(); //Array holding current exception data. //Get all active exceptions. $eplf = TTnew('ExceptionPolicyListFactory'); $eplf->getByPolicyGroupUserIdAndActive($user_date_obj->getUser(), TRUE); if ($eplf->getRecordCount() > 0) { Debug::text(' Found Active Exceptions: ' . $eplf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10); foreach ($eplf as $ep_obj) { //Debug::text(' Found Exception Type: '. $ep_obj->getType() .' ID: '. $ep_obj->getID() .' Control ID: '. $ep_obj->getExceptionPolicyControl(), __FILE__, __LINE__, __METHOD__,10); if ($enable_premature_exceptions == TRUE and self::isPreMature($ep_obj->getType()) == TRUE) { //Debug::text(' Premature Exception: '. $ep_obj->getType() , __FILE__, __LINE__, __METHOD__,10); $type_id = 5; //Pre-Mature } else { //Debug::text(' NOT Premature Exception: '. $ep_obj->getType() , __FILE__, __LINE__, __METHOD__,10); $type_id = 50; //Active } switch (strtolower($ep_obj->getType())) { case 's1': //Unscheduled Absence... Anytime they are scheduled and have not punched in. //Ignore these exceptions if the schedule is after today (not including today), //so if a supervisors schedules an employee two days in advance they don't get a unscheduled //absence appearing right away. //Since we now trigger In Late/Out Late exceptions immediately after schedule time, only trigger this exception after //the schedule end time has passed. //**We also need to handle shifts that start at 11:00PM on one day, end at 8:00AM the next day, and they are assigned to the day where //the most time is worked (ie: the next day). //Handle split shifts too... //- This has a side affect that if the schedule policy start/stop time is set to 0, it will trigger both a UnScheduled Absence // and a Not Scheduled exception for the same schedule/punch. //Loop through all schedules, then find punches to match. if ($slf->getRecordCount() > 0) { foreach ($slf as $s_obj) { if ($s_obj->getStatus() == 10 and $current_epoch >= $s_obj->getEndTime()) { $add_exception = TRUE; //Debug::text(' Found Schedule: Start Time: '. TTDate::getDate('DATE+TIME', $s_obj->getStartTime() ), __FILE__, __LINE__, __METHOD__,10); //Find punches that fall within this schedule time including start/stop window. if (TTDate::doesRangeSpanMidnight($s_obj->getStartTime(), $s_obj->getEndTime()) and is_object($user_date_obj) and is_object($user_date_obj->getPayPeriodObject()) and is_object($user_date_obj->getPayPeriodObject()->getPayPeriodScheduleObject())) { //Get punches from both days. $plf_tmp = TTnew('PunchListFactory'); $plf_tmp->getShiftPunchesByUserIDAndEpoch($user_date_obj->getUser(), $s_obj->getStartTime(), 0, $user_date_obj->getPayPeriodObject()->getPayPeriodScheduleObject()->getMaximumShiftTime()); Debug::text(' Schedule spans midnight... Found rows from expanded search: ' . $plf_tmp->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10); if ($plf_tmp->getRecordCount() > 0) { foreach ($plf_tmp as $p_obj_tmp) { if ($s_obj->inSchedule($p_obj_tmp->getTimeStamp())) { Debug::text(' aFound punch for schedule...', __FILE__, __LINE__, __METHOD__, 10); $add_exception = FALSE; break; } } } unset($plf_tmp, $p_obj_tmp); } else { //Get punches from just this day. foreach ($plf as $p_obj) { if ($s_obj->inSchedule($p_obj->getTimeStamp())) { //Debug::text(' bFound punch for schedule...', __FILE__, __LINE__, __METHOD__,10); $add_exception = FALSE; break; } } } if ($add_exception == TRUE) { //Debug::text(' Adding S1 exception...', __FILE__, __LINE__, __METHOD__,10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => FALSE, 'schedule_obj' => $s_obj); } } } } unset($s_obj, $add_exception); break; case 's2': //Not Scheduled //**We also need to handle shifts that start at 11:00PM on one day, end at 8:00AM the next day, and they are assigned to the day where //the most time is worked (ie: the next day). //Handle split shifts too... if ($plf->getRecordCount() > 1) { //Make sure at least two punche exist. //Loop through each punch, find out if they are scheduled, and if they are in early $prev_punch_time_stamp = FALSE; foreach ($plf as $p_obj) { //Ignore punches that have the exact same timestamp, as they are likely transfer punches. if ($prev_punch_time_stamp != $p_obj->getTimeStamp() and $p_obj->getType() == 10 and $p_obj->getStatus() == 10) { //Normal In if (!isset($scheduled_id_cache[$p_obj->getID()])) { $scheduled_id_cache[$p_obj->getID()] = $p_obj->findScheduleID(NULL, $user_date_obj->getUser()); } //Check if no schedule exists, or an absent schedule exists. If they work when not scheduled (no schedule) or schedule absent, both should trigger this. if ($p_obj->setScheduleID($scheduled_id_cache[$p_obj->getID()]) == FALSE or is_object($p_obj->getScheduleObject()) and $p_obj->getScheduleObject()->getStatus() == 20) { //Debug::text(' Worked when wasnt scheduled', __FILE__, __LINE__, __METHOD__,10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $p_obj->getID(), 'punch_control_id' => FALSE); } else { Debug::text(' Schedule Found', __FILE__, __LINE__, __METHOD__, 10); } } $prev_punch_time_stamp = $p_obj->getTimeStamp(); } } unset($scheduled_id_cache, $prev_punch_time_stamp, $p_obj); break; case 's3': //In Early if ($plf->getRecordCount() > 0) { //Loop through each punch, find out if they are scheduled, and if they are in early $prev_punch_time_stamp = FALSE; foreach ($plf as $p_obj) { //Ignore punches that have the exact same timestamp, as they are likely transfer punches. if ($prev_punch_time_stamp != $p_obj->getTimeStamp() and $p_obj->getType() == 10 and $p_obj->getStatus() == 10) { //Normal In if (!isset($scheduled_id_cache[$p_obj->getID()])) { $scheduled_id_cache[$p_obj->getID()] = $p_obj->findScheduleID(NULL, $user_date_obj->getUser()); } if ($p_obj->setScheduleID($scheduled_id_cache[$p_obj->getID()]) == TRUE) { if ($p_obj->getTimeStamp() < $p_obj->getScheduleObject()->getStartTime()) { if (TTDate::inWindow($p_obj->getTimeStamp(), $p_obj->getScheduleObject()->getStartTime(), $ep_obj->getGrace()) == TRUE) { Debug::text(' Within Grace time, IGNORE EXCEPTION: ', __FILE__, __LINE__, __METHOD__, 10); } elseif (TTDate::inWindow($p_obj->getTimeStamp(), $p_obj->getScheduleObject()->getStartTime(), $ep_obj->getWatchWindow()) == TRUE) { Debug::text(' NOT Within Grace time, SET EXCEPTION: ', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $p_obj->getID(), 'punch_control_id' => FALSE, 'punch_obj' => $p_obj, 'schedule_obj' => $p_obj->getScheduleObject()); } } } else { Debug::text(' NO Schedule Found', __FILE__, __LINE__, __METHOD__, 10); } } $prev_punch_time_stamp = $p_obj->getTimeStamp(); } } break; case 's4': //In Late if ($plf->getRecordCount() > 0) { $prev_punch_time_stamp = FALSE; foreach ($plf as $p_obj) { Debug::text(' In Late. Punch: ' . TTDate::getDate('DATE+TIME', $p_obj->getTimeStamp()), __FILE__, __LINE__, __METHOD__, 10); //Ignore punches that have the exact same timestamp and/or punches with the transfer flag, as they are likely transfer punches. if ($prev_punch_time_stamp != $p_obj->getTimeStamp() and $p_obj->getTransfer() == FALSE and $p_obj->getType() == 10 and $p_obj->getStatus() == 10) { //Normal In if (!isset($scheduled_id_cache[$p_obj->getID()])) { $scheduled_id_cache[$p_obj->getID()] = $p_obj->findScheduleID(NULL, $user_date_obj->getUser()); } if ($p_obj->setScheduleID($scheduled_id_cache[$p_obj->getID()]) == TRUE) { if ($p_obj->getTimeStamp() > $p_obj->getScheduleObject()->getStartTime()) { if (TTDate::inWindow($p_obj->getTimeStamp(), $p_obj->getScheduleObject()->getStartTime(), $ep_obj->getGrace()) == TRUE) { Debug::text(' Within Grace time, IGNORE EXCEPTION: ', __FILE__, __LINE__, __METHOD__, 10); } elseif (TTDate::inWindow($p_obj->getTimeStamp(), $p_obj->getScheduleObject()->getStartTime(), $ep_obj->getWatchWindow()) == TRUE) { Debug::text(' NOT Within Grace time, SET EXCEPTION: ', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $p_obj->getID(), 'punch_control_id' => FALSE, 'punch_obj' => $p_obj, 'schedule_obj' => $p_obj->getScheduleObject()); } } } else { Debug::text(' NO Schedule Found', __FILE__, __LINE__, __METHOD__, 10); } } $prev_punch_time_stamp = $p_obj->getTimeStamp(); } } unset($scheduled_id_cache); //Late Starting their shift, with no punch yet, trigger exception if: // - Schedule is found // - Current time is after schedule start time and before schedule end time. // - Current time is after exception grace time //Make sure we take into account split shifts. Debug::text(' Checking Late Starting Shift exception... Current time: ' . TTDate::getDate('DATE+TIME', $current_epoch), __FILE__, __LINE__, __METHOD__, 10); if ($slf->getRecordCount() > 0) { foreach ($slf as $s_obj) { if ($s_obj->getStatus() == 10 and ($current_epoch >= $s_obj->getStartTime() and $current_epoch <= $s_obj->getEndTime())) { if (TTDate::inWindow($current_epoch, $s_obj->getStartTime(), $ep_obj->getGrace()) == TRUE) { Debug::text(' Within Grace time, IGNORE EXCEPTION: ', __FILE__, __LINE__, __METHOD__, 10); } else { //See if we can find a punch within the schedule time, if so assume we already created the exception above. //Make sure we take into account the schedule policy start/stop window. //However in the case where a single schedule shift and just one punch exists, if an employee comes in really //early (1AM) before the schedule start/stop window it will trigger an In Late exception. //This could still be correct though if they only come in for an hour, then come in late for their shift later. //Schedule start/stop time needs to be correct. //Also need to take into account shifts that span midnight, ie: 10:30PM to 6:00AM, as its important the schedules/punches match up properly. $add_exception = TRUE; Debug::text(' Found Schedule: Start Time: ' . TTDate::getDate('DATE+TIME', $s_obj->getStartTime()), __FILE__, __LINE__, __METHOD__, 10); //Find punches that fall within this schedule time including start/stop window. if (TTDate::doesRangeSpanMidnight($s_obj->getStartTime(), $s_obj->getEndTime()) and is_object($user_date_obj) and is_object($user_date_obj->getPayPeriodObject()) and is_object($user_date_obj->getPayPeriodObject()->getPayPeriodScheduleObject())) { //Get punches from both days. $plf_tmp = TTnew('PunchListFactory'); $plf_tmp->getShiftPunchesByUserIDAndEpoch($user_date_obj->getUser(), $s_obj->getStartTime(), 0, $user_date_obj->getPayPeriodObject()->getPayPeriodScheduleObject()->getMaximumShiftTime()); Debug::text(' Schedule spans midnight... Found rows from expanded search: ' . $plf_tmp->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10); if ($plf_tmp->getRecordCount() > 0) { foreach ($plf_tmp as $p_obj_tmp) { if ($s_obj->inSchedule($p_obj_tmp->getTimeStamp())) { Debug::text(' Found punch for this schedule, skipping schedule...', __FILE__, __LINE__, __METHOD__, 10); $add_exception = FALSE; continue 2; //Skip to next schedule without creating exception. } } } unset($plf_tmp, $p_obj_tmp); } else { //Get punches from just this day. foreach ($plf as $p_obj) { if ($s_obj->inSchedule($p_obj->getTimeStamp())) { Debug::text(' bFound punch for schedule...', __FILE__, __LINE__, __METHOD__, 10); $add_exception = FALSE; break; } } } if ($add_exception == TRUE) { Debug::text(' NOT Within Grace time, SET EXCEPTION: ', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => FALSE, 'schedule_obj' => $s_obj); } } } } } else { Debug::text(' NO Schedule Found', __FILE__, __LINE__, __METHOD__, 10); } break; case 's5': //Out Early if ($plf->getRecordCount() > 0) { //Loop through each punch, find out if they are scheduled, and if they are in early $prev_punch_time_stamp = FALSE; $total_punches = $plf->getRecordCount(); $x = 1; foreach ($plf as $p_obj) { //Ignore punches that have the exact same timestamp and/or punches with the transfer flag, as they are likely transfer punches. //For Out Early, we have to wait until we are at the last punch, or there is a subsequent punch // to see if it matches the exact same time (transfer) //Therefore we need a two step confirmation before this exception can be triggered. Current punch, then next punch if it exists. if ($p_obj->getTransfer() == FALSE and $p_obj->getType() == 10 and $p_obj->getStatus() == 20) { //Normal Out if (!isset($scheduled_id_cache[$p_obj->getID()])) { $scheduled_id_cache[$p_obj->getID()] = $p_obj->findScheduleID(NULL, $user_date_obj->getUser()); } if ($p_obj->setScheduleID($scheduled_id_cache[$p_obj->getID()]) == TRUE) { if ($p_obj->getTimeStamp() < $p_obj->getScheduleObject()->getEndTime()) { if (TTDate::inWindow($p_obj->getTimeStamp(), $p_obj->getScheduleObject()->getEndTime(), $ep_obj->getGrace()) == TRUE) { Debug::text(' Within Grace time, IGNORE EXCEPTION: ', __FILE__, __LINE__, __METHOD__, 10); } elseif (TTDate::inWindow($p_obj->getTimeStamp(), $p_obj->getScheduleObject()->getEndTime(), $ep_obj->getWatchWindow()) == TRUE) { Debug::text(' NOT Within Grace time, SET EXCEPTION: ', __FILE__, __LINE__, __METHOD__, 10); $tmp_exception = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $p_obj->getID(), 'punch_control_id' => FALSE, 'punch_obj' => $p_obj, 'schedule_obj' => $p_obj->getScheduleObject()); if ($x == $total_punches) { //Trigger exception if we're the last punch. $current_exceptions[] = $tmp_exception; } else { //Save exception to be triggered if the next punch doesn't match the same time. } } } } else { Debug::text(' NO Schedule Found', __FILE__, __LINE__, __METHOD__, 10); } } elseif ($p_obj->getType() == 10 and $p_obj->getStatus() == 10) { //Normal In //This comes after an OUT punch, so we need to check if there are two punches //in a row with the same timestamp, if so ignore the exception. if (isset($tmp_exception) and $p_obj->getTimeStamp() == $prev_punch_time_stamp) { unset($tmp_exception); } elseif (isset($tmp_exception)) { $current_exceptions[] = $tmp_exception; //Set exception. } } $prev_punch_time_stamp = $p_obj->getTimeStamp(); $x++; } } unset($tmp_exception, $x, $prev_punch_time_stamp); break; case 's6': //Out Late if ($plf->getRecordCount() > 0) { $prev_punch_time_stamp = FALSE; foreach ($plf as $p_obj) { $punch_pairs[$p_obj->getPunchControlID()][] = array('status_id' => $p_obj->getStatus(), 'punch_control_id' => $p_obj->getPunchControlID(), 'time_stamp' => $p_obj->getTimeStamp()); if ($prev_punch_time_stamp != $p_obj->getTimeStamp() and $p_obj->getType() == 10 and $p_obj->getStatus() == 20) { //Normal Out if (!isset($scheduled_id_cache[$p_obj->getID()])) { $scheduled_id_cache[$p_obj->getID()] = $p_obj->findScheduleID(NULL, $user_date_obj->getUser()); } if ($p_obj->setScheduleID($scheduled_id_cache[$p_obj->getID()]) == TRUE) { if ($p_obj->getTimeStamp() > $p_obj->getScheduleObject()->getEndTime()) { if (TTDate::inWindow($p_obj->getTimeStamp(), $p_obj->getScheduleObject()->getEndTime(), $ep_obj->getGrace()) == TRUE) { Debug::text(' Within Grace time, IGNORE EXCEPTION: ', __FILE__, __LINE__, __METHOD__, 10); } elseif (TTDate::inWindow($p_obj->getTimeStamp(), $p_obj->getScheduleObject()->getEndTime(), $ep_obj->getWatchWindow()) == TRUE) { Debug::text(' NOT Within Grace time, SET EXCEPTION: ', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $p_obj->getID(), 'punch_control_id' => FALSE, 'punch_obj' => $p_obj, 'schedule_obj' => $p_obj->getScheduleObject()); } } } else { Debug::text(' NO Schedule Found', __FILE__, __LINE__, __METHOD__, 10); } } $prev_punch_time_stamp = $p_obj->getTimeStamp(); } //Trigger exception if no out punch and we have passed schedule out time. // - Schedule is found // - Make sure the user is missing an OUT punch. // - Current time is after schedule end time // - Current time is after exception grace time // - Current time is before schedule end time + maximum shift time. if (isset($punch_pairs) and $slf->getRecordCount() > 0) { foreach ($punch_pairs as $punch_control_id => $punch_pair) { if (count($punch_pair) != 2) { Debug::text('aFound Missing Punch: ', __FILE__, __LINE__, __METHOD__, 10); if ($punch_pair[0]['status_id'] == 10) { //Missing Out Punch Debug::text('bFound Missing Out Punch: ', __FILE__, __LINE__, __METHOD__, 10); foreach ($slf as $s_obj) { Debug::text('Punch: ' . TTDate::getDate('DATE+TIME', $punch_pair[0]['time_stamp']) . ' Schedule Start Time: ' . TTDate::getDate('DATE+TIME', $s_obj->getStartTime()) . ' End Time: ' . TTDate::getDate('DATE+TIME', $s_obj->getEndTime()), __FILE__, __LINE__, __METHOD__, 10); //Because this is just an IN punch, make sure the IN punch is before the schedule end time //So we can eliminate split shift schedules. if ($punch_pair[0]['time_stamp'] <= $s_obj->getEndTime() and $current_epoch >= $s_obj->getEndTime() and $current_epoch <= $s_obj->getEndTime() + self::$premature_delay) { if (TTDate::inWindow($current_epoch, $s_obj->getEndTime(), $ep_obj->getGrace()) == TRUE) { Debug::text(' Within Grace time, IGNORE EXCEPTION: ', __FILE__, __LINE__, __METHOD__, 10); } else { Debug::text(' NOT Within Grace time, SET EXCEPTION: ', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => $punch_pair[0]['punch_control_id'], 'schedule_obj' => $s_obj); } } } } } else { Debug::text('No Missing Punches...', __FILE__, __LINE__, __METHOD__, 10); } } } unset($punch_pairs, $punch_pair); } break; case 'm1': //Missing In Punch if ($plf->getRecordCount() > 0) { foreach ($plf as $p_obj) { //Debug::text(' Punch: Status: '. $p_obj->getStatus() .' Punch Control ID: '. $p_obj->getPunchControlID() .' Punch ID: '. $p_obj->getId() .' TimeStamp: '. $p_obj->getTimeStamp(), __FILE__, __LINE__, __METHOD__,10); if ($type_id == 5 and $p_obj->getTimeStamp() < $current_epoch - self::$premature_delay) { $type_id = 50; } $punch_pairs[$p_obj->getPunchControlID()][] = array('status_id' => $p_obj->getStatus(), 'punch_control_id' => $p_obj->getPunchControlID(), 'punch_id' => $p_obj->getId()); } if (isset($punch_pairs)) { foreach ($punch_pairs as $punch_control_id => $punch_pair) { //Debug::Arr($punch_pair, 'Punch Pair for Control ID:'. $punch_control_id, __FILE__, __LINE__, __METHOD__,10); if (count($punch_pair) != 2) { Debug::text('a1Found Missing Punch: ', __FILE__, __LINE__, __METHOD__, 10); if ($punch_pair[0]['status_id'] == 20) { //Missing In Punch Debug::text('b1Found Missing In Punch: ', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => $punch_pair[0]['punch_control_id']); } } else { Debug::text('No Missing Punches...', __FILE__, __LINE__, __METHOD__, 10); } } } unset($punch_pairs, $punch_pair); } break; case 'm2': //Missing Out Punch if ($plf->getRecordCount() > 0) { foreach ($plf as $p_obj) { Debug::text(' Punch: Status: ' . $p_obj->getStatus() . ' Punch Control ID: ' . $p_obj->getPunchControlID() . ' Punch ID: ' . $p_obj->getId() . ' TimeStamp: ' . $p_obj->getTimeStamp(), __FILE__, __LINE__, __METHOD__, 10); //This causes the exception to trigger if the first punch pair is more than the Maximum Shift time away from the current punch, //ie: In: 1:00AM, Out: 2:00AM, In 3:00PM (Maximum Shift Time less than 12hrs). The missing punch exception will be triggered immediately upon the 3:00PM punch. //if ( $type_id == 5 AND $p_obj->getTimeStamp() < ($current_epoch-self::$premature_delay) ) { // $type_id = 50; //} $punch_pairs[$p_obj->getPunchControlID()][] = array('status_id' => $p_obj->getStatus(), 'punch_control_id' => $p_obj->getPunchControlID(), 'time_stamp' => $p_obj->getTimeStamp()); } if (isset($punch_pairs)) { foreach ($punch_pairs as $punch_control_id => $punch_pair) { if (count($punch_pair) != 2) { Debug::text('a2Found Missing Punch: ', __FILE__, __LINE__, __METHOD__, 10); if ($punch_pair[0]['status_id'] == 10) { //Missing Out Punch Debug::text('b2Found Missing Out Punch: ', __FILE__, __LINE__, __METHOD__, 10); //Make sure we are at least MaximumShift Time from the matching In punch before trigging this exception. //Even when an supervisor is entering punches for today, make missing out punch pre-mature if the maximum shift time isn't exceeded. //This will prevent timesheet recalculations from having missing punches for everyone today. //if ( $type_id == 5 AND $punch_pair[0]['time_stamp'] < ($current_epoch-self::$premature_delay) ) { if ($punch_pair[0]['time_stamp'] < $current_epoch - self::$premature_delay) { $type_id = 50; } else { $type_id = 5; } $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => $punch_pair[0]['punch_control_id']); } } else { Debug::text('No Missing Punches...', __FILE__, __LINE__, __METHOD__, 10); } } } unset($punch_pairs, $punch_pair); } break; case 'm3': //Missing Lunch In/Out punch if ($plf->getRecordCount() > 0) { //We need to account for cases where they may punch IN from lunch first, then Out. //As well as just a Lunch In punch and nothing else. foreach ($plf as $p_obj) { if ($type_id == 5 and $p_obj->getTimeStamp() < $current_epoch - self::$premature_delay) { $type_id = 50; } $punches[] = $p_obj; } if (isset($punches) and is_array($punches)) { foreach ($punches as $key => $p_obj) { if ($p_obj->getType() == 20) { //Lunch Debug::text(' Punch: Status: ' . $p_obj->getStatus() . ' Punch Control ID: ' . $p_obj->getPunchControlID() . ' TimeStamp: ' . $p_obj->getTimeStamp(), __FILE__, __LINE__, __METHOD__, 10); if ($p_obj->getStatus() == 10) { //Make sure previous punch is Lunch/Out if (!isset($punches[$key - 1]) or isset($punches[$key - 1]) and is_object($punches[$key - 1]) and ($punches[$key - 1]->getType() != 20 or $punches[$key - 1]->getStatus() != 20)) { //Invalid punch $invalid_punches[] = array('punch_id' => $p_obj->getId()); } } else { //Make sure next punch is Lunch/In if (!isset($punches[$key + 1]) or isset($punches[$key + 1]) and is_object($punches[$key + 1]) and ($punches[$key + 1]->getType() != 20 or $punches[$key + 1]->getStatus() != 10)) { //Invalid punch $invalid_punches[] = array('punch_id' => $p_obj->getId()); } } } } unset($punches, $key, $p_obj); if (isset($invalid_punches) and count($invalid_punches) > 0) { foreach ($invalid_punches as $invalid_punch_arr) { Debug::text('Found Missing Lunch In/Out Punch: ', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $invalid_punch_arr['punch_id'], 'punch_control_id' => FALSE); } unset($invalid_punch_arr); } else { Debug::text('Lunch Punches match up.', __FILE__, __LINE__, __METHOD__, 10); } unset($invalid_punches); } } break; case 'm4': //Missing Break In/Out punch if ($plf->getRecordCount() > 0) { //We need to account for cases where they may punch IN from break first, then Out. //As well as just a break In punch and nothing else. foreach ($plf as $p_obj) { if ($type_id == 5 and $p_obj->getTimeStamp() < $current_epoch - self::$premature_delay) { $type_id = 50; } $punches[] = $p_obj; } if (isset($punches) and is_array($punches)) { foreach ($punches as $key => $p_obj) { if ($p_obj->getType() == 30) { //Break Debug::text(' Punch: Status: ' . $p_obj->getStatus() . ' Type: ' . $p_obj->getType() . ' Punch Control ID: ' . $p_obj->getPunchControlID() . ' TimeStamp: ' . $p_obj->getTimeStamp(), __FILE__, __LINE__, __METHOD__, 10); if ($p_obj->getStatus() == 10) { //Make sure previous punch is Break/Out if (!isset($punches[$key - 1]) or isset($punches[$key - 1]) and is_object($punches[$key - 1]) and ($punches[$key - 1]->getType() != 30 or $punches[$key - 1]->getStatus() != 20)) { //Invalid punch $invalid_punches[] = array('punch_id' => $p_obj->getId()); } } else { //Make sure next punch is Break/In if (!isset($punches[$key + 1]) or isset($punches[$key + 1]) and is_object($punches[$key + 1]) and ($punches[$key + 1]->getType() != 30 or $punches[$key + 1]->getStatus() != 10)) { //Invalid punch $invalid_punches[] = array('punch_id' => $p_obj->getId()); } } } } unset($punches, $key, $p_obj); if (isset($invalid_punches) and count($invalid_punches) > 0) { foreach ($invalid_punches as $invalid_punch_arr) { Debug::text('Found Missing Break In/Out Punch: ', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $invalid_punch_arr['punch_id'], 'punch_control_id' => FALSE); } unset($invalid_punch_arr); } else { Debug::text('Lunch Punches match up.', __FILE__, __LINE__, __METHOD__, 10); } unset($invalid_punches); } } break; case 'c1': //Missed Check-in //Use grace period and make sure the employee punches within that period of time (usually a transfer punch, but break/lunch should work too) if ($plf->getRecordCount() > 0 and $ep_obj->getGrace() > 0) { $prev_punch_time_stamp = FALSE; $prev_punch_obj = FALSE; $x = 1; foreach ($plf as $p_obj) { Debug::text(' Missed Check-In Punch: ' . TTDate::getDate('DATE+TIME', $p_obj->getTimeStamp()) . ' Delay: ' . self::$premature_delay . ' Current Epoch: ' . $current_epoch, __FILE__, __LINE__, __METHOD__, 10); //Handle punch pairs below. Only trigger on OUT punches. if (is_object($prev_punch_obj) and $prev_punch_obj->getStatus() == 10 and $p_obj->getStatus() == 20 and $p_obj->getTimeStamp() - $prev_punch_time_stamp > $ep_obj->getGrace()) { //Only check OUT punches when paired. Debug::text(' Triggering excepetion as employee missed check-in within: ' . ($p_obj->getTimeStamp() - $prev_punch_time_stamp), __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $p_obj->getID(), 'punch_control_id' => FALSE, 'punch_obj' => $p_obj, 'schedule_obj' => $p_obj->getScheduleObject()); } elseif ($prev_punch_time_stamp !== FALSE) { Debug::text(' Employee Checked-In within: ' . ($p_obj->getTimeStamp() - $prev_punch_time_stamp), __FILE__, __LINE__, __METHOD__, 10); } //Handle cases where there is a IN punch but no OUT punch yet. //However ignore cases where there is a OUT punch but no IN punch. if ($x == $plf->getRecordCount() and $p_obj->getStatus() == 10 and $current_epoch - $p_obj->getTimeStamp() > $ep_obj->getGrace() and $p_obj->getTimeStamp() > $current_epoch - self::$premature_delay) { Debug::text(' Triggering excepetion as employee hasnt checked in yet, within: ' . ($current_epoch - $prev_punch_time_stamp), __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => $p_obj->getPunchControlID(), 'punch_obj' => $p_obj, 'schedule_obj' => $p_obj->getScheduleObject()); } $prev_punch_time_stamp = $p_obj->getTimeStamp(); $prev_punch_obj = $p_obj; $x++; } } unset($prev_punch_obj, $prev_punch_time_stamp, $x); break; case 'd1': //No Branch or Department $add_exception = FALSE; foreach ($plf as $p_obj) { //In punches only if ($p_obj->getStatus() == 10 and is_object($p_obj->getPunchControlObject())) { //If no Tasks are setup, ignore checking them. if ($p_obj->getPunchControlObject()->getBranch() == '' or $p_obj->getPunchControlObject()->getBranch() == 0 or $p_obj->getPunchControlObject()->getBranch() == FALSE) { $add_exception = TRUE; } if ($p_obj->getPunchControlObject()->getDepartment() == '' or $p_obj->getPunchControlObject()->getDepartment() == 0 or $p_obj->getPunchControlObject()->getDepartment() == FALSE) { //Make sure at least one task exists before triggering exception. $dlf = TTNew('DepartmentListFactory'); $dlf->getByCompanyID($user_date_obj->getUserObject()->getCompany(), 1); //Limit to just 1 record. if ($dlf->getRecordCount() > 0) { $add_exception = TRUE; } } if ($add_exception === TRUE) { $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $p_obj->getId(), 'punch_control_id' => $p_obj->getPunchControlId()); } } } break; case 's7': //Over Scheduled Hours if ($plf->getRecordCount() > 0) { //FIXME: Assign this exception to the last punch of the day, so it can be related back to a punch branch/department? //This ONLY takes in to account WORKED hours, not paid absence hours. //FIXME: Do we want to trigger this before their last out punch? $schedule_total_time = 0; if ($slf->getRecordCount() > 0) { //Check for schedule policy foreach ($slf as $s_obj) { Debug::text(' Schedule Total Time: ' . $s_obj->getTotalTime(), __FILE__, __LINE__, __METHOD__, 10); $schedule_total_time += $s_obj->getTotalTime(); } $daily_total_time = 0; if ($schedule_total_time > 0) { //Get daily total time. $udtlf = TTnew('UserDateTotalListFactory'); //Take into account auto-deduct/add meal policies, but not paid absences. //$udtlf->getByUserDateIdAndStatusAndType( $user_date_id, 10, 10 ); $udtlf->getByUserDateId($user_date_id); if ($udtlf->getRecordCount() > 0) { foreach ($udtlf as $udt_obj) { if ($udt_obj->getTimeCategory() == 'worked_time') { $daily_total_time += $udt_obj->getTotalTime(); } } } Debug::text(' Daily Total Time: ' . $daily_total_time . ' Schedule Total Time: ' . $schedule_total_time, __FILE__, __LINE__, __METHOD__, 10); if ($daily_total_time > 0 and $daily_total_time > $schedule_total_time + $ep_obj->getGrace()) { Debug::text(' Worked Over Scheduled Hours', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => FALSE); } else { Debug::text(' DID NOT Work Over Scheduled Hours', __FILE__, __LINE__, __METHOD__, 10); } } } else { Debug::text(' Not Scheduled', __FILE__, __LINE__, __METHOD__, 10); } } break; case 's8': //Under Scheduled Hours if ($plf->getRecordCount() > 0) { //FIXME: Assign this exception to the last punch of the day, so it can be related back to a punch branch/department? //This ONLY takes in to account WORKED hours, not paid absence hours. $schedule_total_time = 0; if ($slf->getRecordCount() > 0) { //Check for schedule policy foreach ($slf as $s_obj) { Debug::text(' Schedule Total Time: ' . $s_obj->getTotalTime(), __FILE__, __LINE__, __METHOD__, 10); $schedule_total_time += $s_obj->getTotalTime(); } $daily_total_time = 0; if ($schedule_total_time > 0) { //Get daily total time. $udtlf = TTnew('UserDateTotalListFactory'); //Take into account auto-deduct/add meal policies //$udtlf->getByUserDateIdAndStatusAndType( $user_date_id, 10, 10 ); $udtlf->getByUserDateId($user_date_id); if ($udtlf->getRecordCount() > 0) { foreach ($udtlf as $udt_obj) { if ($udt_obj->getTimeCategory() == 'worked_time') { $daily_total_time += $udt_obj->getTotalTime(); } } } Debug::text(' Daily Total Time: ' . $daily_total_time . ' Schedule Total Time: ' . $schedule_total_time, __FILE__, __LINE__, __METHOD__, 10); if ($daily_total_time < $schedule_total_time - $ep_obj->getGrace()) { Debug::text(' Worked Under Scheduled Hours', __FILE__, __LINE__, __METHOD__, 10); if ($type_id == 5 and $user_date_obj->getDateStamp() < TTDate::getBeginDayEpoch($current_epoch - self::$premature_delay)) { $type_id = 50; } $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => FALSE); } else { Debug::text(' DID NOT Work Under Scheduled Hours', __FILE__, __LINE__, __METHOD__, 10); } } } else { Debug::text(' Not Scheduled', __FILE__, __LINE__, __METHOD__, 10); } } break; case 'o1': //Over Daily Time. if ($plf->getRecordCount() > 0) { //FIXME: Assign this exception to the last punch of the day, so it can be related back to a punch branch/department? //This ONLY takes in to account WORKED hours, not paid absence hours. //FIXME: Do we want to trigger this before their last out punch? $daily_total_time = 0; //Get daily total time. $udtlf = TTnew('UserDateTotalListFactory'); //Take into account auto-deduct/add meal policies //$udtlf->getByUserDateIdAndStatusAndType( $user_date_id, 10, 10 ); $udtlf->getByUserDateId($user_date_id); if ($udtlf->getRecordCount() > 0) { foreach ($udtlf as $udt_obj) { if ($udt_obj->getTimeCategory() == 'worked_time') { $daily_total_time += $udt_obj->getTotalTime(); } } } Debug::text(' Daily Total Time: ' . $daily_total_time . ' Watch Window: ' . $ep_obj->getWatchWindow() . ' User Date ID: ' . $user_date_id, __FILE__, __LINE__, __METHOD__, 10); if ($daily_total_time > 0 and $daily_total_time > $ep_obj->getWatchWindow()) { Debug::text(' Worked Over Daily Hours', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => FALSE); } else { Debug::text(' DID NOT Work Over Scheduled Hours', __FILE__, __LINE__, __METHOD__, 10); } } break; case 'o2': //Over Weekly Time. //Over Weekly Time. case 's9': //Over Weekly Scheduled Time. if ($plf->getRecordCount() > 0) { //FIXME: Assign this exception to the last punch of the day, so it can be related back to a punch branch/department? //Get Pay Period Schedule info //FIXME: Do we want to trigger this before their last out punch? if (is_object($user_date_obj->getPayPeriodObject()) and is_object($user_date_obj->getPayPeriodObject()->getPayPeriodScheduleObject())) { $start_week_day_id = $user_date_obj->getPayPeriodObject()->getPayPeriodScheduleObject()->getStartWeekDay(); } else { $start_week_day_id = 0; } Debug::text('Start Week Day ID: ' . $start_week_day_id, __FILE__, __LINE__, __METHOD__, 10); $weekly_scheduled_total_time = 0; //Currently we only consider committed scheduled shifts. We may need to change this to take into account //recurring scheduled shifts that haven't been committed yet as well. //In either case though we should take into account the entires week worth of scheduled time even if we are only partially through //the week, that way we won't be triggering s9 exceptions on a Wed and a Fri or something, it will only occur on the last days of the week. if (strtolower($ep_obj->getType()) == 's9') { $tmp_slf = TTnew('ScheduleListFactory'); $tmp_slf->getByUserIdAndStartDateAndEndDate($user_date_obj->getUser(), TTDate::getBeginWeekEpoch($user_date_obj->getDateStamp(), $start_week_day_id), TTDate::getEndWeekEpoch($user_date_obj->getDateStamp(), $start_week_day_id)); if ($tmp_slf->getRecordCount() > 0) { foreach ($tmp_slf as $s_obj) { if ($s_obj->getStatus() == 10) { //Only working shifts. $weekly_scheduled_total_time += $s_obj->getTotalTime(); } } } unset($tmp_slf, $s_obj); } //This ONLY takes in to account WORKED hours, not paid absence hours. $weekly_total_time = 0; //Get daily total time. $udtlf = TTnew('UserDateTotalListFactory'); $weekly_total_time = $udtlf->getWorkedTimeSumByUserIDAndStartDateAndEndDate($user_date_obj->getUser(), TTDate::getBeginWeekEpoch($user_date_obj->getDateStamp(), $start_week_day_id), $user_date_obj->getDateStamp()); Debug::text(' Weekly Total Time: ' . $weekly_total_time . ' Weekly Scheduled Total Time: ' . $weekly_scheduled_total_time . ' Watch Window: ' . $ep_obj->getWatchWindow() . ' Grace: ' . $ep_obj->getGrace() . ' User Date ID: ' . $user_date_id, __FILE__, __LINE__, __METHOD__, 10); //Don't trigger either of these exceptions unless both the worked and scheduled time is greater than 0. If they aren't scheduled at all //it should trigger a Unscheduled Absence exception instead of a over weekly scheduled time exception. if (strtolower($ep_obj->getType()) == 'o2' and $weekly_total_time > 0 and $weekly_total_time > $ep_obj->getWatchWindow() or strtolower($ep_obj->getType()) == 's9' and $weekly_scheduled_total_time > 0 and $weekly_total_time > 0 and $weekly_total_time > $weekly_scheduled_total_time + $ep_obj->getGrace()) { Debug::text(' Worked Over Weekly Hours', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => FALSE); } else { Debug::text(' DID NOT Work Over Scheduled Hours', __FILE__, __LINE__, __METHOD__, 10); } } break; case 'l1': //Long Lunch //Long Lunch case 'l2': //Short Lunch if ($plf->getRecordCount() > 0) { //Get all lunch punches. $pair = 0; $x = 0; $out_for_lunch = FALSE; foreach ($plf as $p_obj) { if ($p_obj->getStatus() == 20 and $p_obj->getType() == 20) { $lunch_out_timestamp = $p_obj->getTimeStamp(); $lunch_punch_arr[$pair]['punch_id'] = $p_obj->getId(); $out_for_lunch = TRUE; } elseif ($out_for_lunch == TRUE and $p_obj->getStatus() == 10 and $p_obj->getType() == 20) { $lunch_punch_arr[$pair][20] = $lunch_out_timestamp; $lunch_punch_arr[$pair][10] = $p_obj->getTimeStamp(); $out_for_lunch = FALSE; $pair++; unset($lunch_out_timestamp); } else { $out_for_lunch = FALSE; } } if (isset($lunch_punch_arr)) { //Debug::Arr($lunch_punch_arr, 'Lunch Punch Array: ', __FILE__, __LINE__, __METHOD__,10); foreach ($lunch_punch_arr as $pair => $time_stamp_arr) { if (isset($time_stamp_arr[10]) and isset($time_stamp_arr[20])) { $lunch_total_time = bcsub($time_stamp_arr[10], $time_stamp_arr[20]); Debug::text(' Lunch Total Time: ' . $lunch_total_time, __FILE__, __LINE__, __METHOD__, 10); if (!isset($scheduled_id_cache[$p_obj->getID()])) { $scheduled_id_cache[$p_obj->getID()] = $p_obj->findScheduleID(NULL, $user_date_obj->getUser()); } //Check to see if they have a schedule policy if ($p_obj->setScheduleID($scheduled_id_cache[$p_obj->getID()]) == TRUE and is_object($p_obj->getScheduleObject()) == TRUE and is_object($p_obj->getScheduleObject()->getSchedulePolicyObject()) == TRUE) { $mp_obj = $p_obj->getScheduleObject()->getSchedulePolicyObject()->getMealPolicyObject(); } else { $mplf = TTnew('MealPolicyListFactory'); $mplf->getByPolicyGroupUserId($user_date_obj->getUserObject()->getId()); if ($mplf->getRecordCount() > 0) { Debug::text('Found Meal Policy to apply.', __FILE__, __LINE__, __METHOD__, 10); $mp_obj = $mplf->getCurrent(); } } if (isset($mp_obj) and is_object($mp_obj)) { $meal_policy_lunch_time = $mp_obj->getAmount(); Debug::text('Meal Policy Time: ' . $meal_policy_lunch_time, __FILE__, __LINE__, __METHOD__, 10); $add_exception = FALSE; if (strtolower($ep_obj->getType()) == 'l1' and $meal_policy_lunch_time > 0 and $lunch_total_time > 0 and $lunch_total_time > $meal_policy_lunch_time + $ep_obj->getGrace()) { $add_exception = TRUE; } elseif (strtolower($ep_obj->getType()) == 'l2' and $meal_policy_lunch_time > 0 and $lunch_total_time > 0 and $lunch_total_time < $meal_policy_lunch_time - $ep_obj->getGrace()) { $add_exception = TRUE; } if ($add_exception == TRUE) { Debug::text('Adding Exception!', __FILE__, __LINE__, __METHOD__, 10); if (isset($time_stamp_arr['punch_id'])) { $punch_id = $time_stamp_arr['punch_id']; } else { $punch_id = FALSE; } $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $punch_id, 'punch_control_id' => FALSE); unset($punch_id); } else { Debug::text('Not Adding Exception!', __FILE__, __LINE__, __METHOD__, 10); } } } else { Debug::text(' Lunch Punches not paired... Skipping!', __FILE__, __LINE__, __METHOD__, 10); } } } else { Debug::text(' No Lunch Punches found, or none are paired.', __FILE__, __LINE__, __METHOD__, 10); } } break; case 'l3': //No Lunch if ($plf->getRecordCount() > 0) { //If they are scheduled or not, we can check for a meal policy and base our //decision off that. We don't want a No Lunch exception on a 3hr short shift though. //Also ignore this exception if the lunch is auto-deduct. //**Try to assign this exception to a specific punch control id, so we can do searches based on punch branch. //Find meal policy //Use scheduled meal policy first. $meal_policy_obj = NULL; if ($slf->getRecordCount() > 0) { Debug::text('Schedule Found...', __FILE__, __LINE__, __METHOD__, 10); foreach ($slf as $s_obj) { if ($s_obj->getSchedulePolicyObject() !== FALSE and $s_obj->getSchedulePolicyObject()->getMealPolicyObject() !== FALSE and $s_obj->getSchedulePolicyObject()->getMealPolicyObject()->getType() != 10) { Debug::text('Found Schedule Meal Policy... Trigger Time: ' . $s_obj->getSchedulePolicyObject()->getMealPolicyObject()->getTriggerTime(), __FILE__, __LINE__, __METHOD__, 10); $meal_policy_obj = $s_obj->getSchedulePolicyObject()->getMealPolicyObject(); } else { Debug::text('Schedule Meal Policy does not exist, or is auto-deduct?', __FILE__, __LINE__, __METHOD__, 10); } } } else { Debug::text('No Schedule Found...', __FILE__, __LINE__, __METHOD__, 10); //Check if they have a meal policy, with no schedule. $mplf = TTnew('MealPolicyListFactory'); $mplf->getByPolicyGroupUserId($user_date_obj->getUser()); if ($mplf->getRecordCount() > 0) { foreach ($mplf as $mp_obj) { if ($mp_obj->getType() != 10) { Debug::text('Found UnScheduled meal Policy... Trigger Time: ' . $mp_obj->getTriggerTime(), __FILE__, __LINE__, __METHOD__, 10); $meal_policy_obj = $mp_obj; } } unset($mplf, $mp_obj); } else { //There is no meal policy or schedule policy with a meal policy assigned to it //With out this we could still apply No meal exceptions, but they will happen even on //a 2minute shift. Debug::text('No Lunch policy, applying No meal exception.', __FILE__, __LINE__, __METHOD__, 10); $meal_policy_obj = TRUE; } } if (is_object($meal_policy_obj) or $meal_policy_obj === TRUE) { $punch_control_id = FALSE; $daily_total_time = 0; $udtlf = TTnew('UserDateTotalListFactory'); $udtlf->getByUserDateIdAndStatus($user_date_id, 20); if ($udtlf->getRecordCount() > 0) { foreach ($udtlf as $udt_obj) { $daily_total_time += $udt_obj->getTotalTime(); $punch_control_total_time[$udt_obj->getPunchControlID()] = $udt_obj->getTotalTime(); } } Debug::text('Day Total Time: ' . $daily_total_time, __FILE__, __LINE__, __METHOD__, 10); //Debug::Arr($punch_control_total_time, 'Punch Control Total Time: ', __FILE__, __LINE__, __METHOD__,10); if ($daily_total_time > 0 and ($meal_policy_obj === TRUE or $daily_total_time > $meal_policy_obj->getTriggerTime())) { //Check for meal punch. $meal_punch = FALSE; $tmp_punch_total_time = 0; $tmp_punch_control_ids = array(); foreach ($plf as $p_obj) { if ($p_obj->getType() == 20) { //20 = Lunch Debug::text('Found meal Punch: ' . $p_obj->getTimeStamp(), __FILE__, __LINE__, __METHOD__, 10); $meal_punch = TRUE; break; } if (isset($punch_control_total_time[$p_obj->getPunchControlID()]) and !isset($tmp_punch_control_ids[$p_obj->getPunchControlID()])) { $tmp_punch_total_time += $punch_control_total_time[$p_obj->getPunchControlID()]; if ($punch_control_id === FALSE and ($meal_policy_obj === TRUE or $tmp_punch_total_time > $meal_policy_obj->getTriggerTime())) { Debug::text('Found punch control for exception: ' . $p_obj->getPunchControlID() . ' Total Time: ' . $tmp_punch_total_time, __FILE__, __LINE__, __METHOD__, 10); $punch_control_id = $p_obj->getPunchControlID(); //Don't meal the loop here, as we have to continue on and check for other meals. } } $tmp_punch_control_ids[$p_obj->getPunchControlID()] = TRUE; } unset($tmp_punch_total_time, $tmp_punch_control_ids); if ($meal_punch == FALSE) { Debug::text('Triggering No Lunch exception!', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => $punch_control_id); } } } } break; case 'b1': //Long Break //Long Break case 'b2': //Short Break if ($plf->getRecordCount() > 0) { //Get all break punches. $pair = 0; $x = 0; $out_for_break = FALSE; foreach ($plf as $p_obj) { if ($p_obj->getStatus() == 20 and $p_obj->getType() == 30) { $break_out_timestamp = $p_obj->getTimeStamp(); $break_punch_arr[$pair]['punch_id'] = $p_obj->getId(); $out_for_break = TRUE; } elseif ($out_for_break == TRUE and $p_obj->getStatus() == 10 and $p_obj->getType() == 30) { $break_punch_arr[$pair][20] = $break_out_timestamp; $break_punch_arr[$pair][10] = $p_obj->getTimeStamp(); $out_for_break = FALSE; $pair++; unset($break_out_timestamp); } else { $out_for_break = FALSE; } } unset($pair); if (isset($break_punch_arr)) { //Debug::Arr($break_punch_arr, 'Break Punch Array: ', __FILE__, __LINE__, __METHOD__,10); foreach ($break_punch_arr as $pair => $time_stamp_arr) { if (isset($time_stamp_arr[10]) and isset($time_stamp_arr[20])) { $break_total_time = bcsub($time_stamp_arr[10], $time_stamp_arr[20]); Debug::text(' Break Total Time: ' . $break_total_time, __FILE__, __LINE__, __METHOD__, 10); if (!isset($scheduled_id_cache[$p_obj->getID()])) { $scheduled_id_cache[$p_obj->getID()] = $p_obj->findScheduleID(NULL, $user_date_obj->getUser()); } //Check to see if they have a schedule policy $bplf = TTnew('BreakPolicyListFactory'); if ($p_obj->setScheduleID($scheduled_id_cache[$p_obj->getID()]) == TRUE and is_object($p_obj->getScheduleObject()) == TRUE and is_object($p_obj->getScheduleObject()->getSchedulePolicyObject()) == TRUE) { $break_policy_ids = $p_obj->getScheduleObject()->getSchedulePolicyObject()->getBreakPolicy(); $bplf->getByIdAndCompanyId($break_policy_ids, $user_date_obj->getUserObject()->getCompany()); } else { $bplf->getByPolicyGroupUserId($user_date_obj->getUser()); } unset($break_policy_ids); if ($bplf->getRecordCount() > 0) { Debug::text('Found Break Policy(ies) to apply: ' . $bplf->getRecordCount() . ' Pair: ' . $pair, __FILE__, __LINE__, __METHOD__, 10); foreach ($bplf as $bp_obj) { $bp_objs[] = $bp_obj; } unset($bplf, $bp_obj); if (isset($bp_objs[$pair]) and is_object($bp_objs[$pair])) { $bp_obj = $bp_objs[$pair]; $break_policy_break_time = $bp_obj->getAmount(); Debug::text('Break Policy Time: ' . $break_policy_break_time . ' ID: ' . $bp_obj->getID(), __FILE__, __LINE__, __METHOD__, 10); $add_exception = FALSE; if (strtolower($ep_obj->getType()) == 'b1' and $break_policy_break_time > 0 and $break_total_time > 0 and $break_total_time > $break_policy_break_time + $ep_obj->getGrace()) { $add_exception = TRUE; } elseif (strtolower($ep_obj->getType()) == 'b2' and $break_policy_break_time > 0 and $break_total_time > 0 and $break_total_time < $break_policy_break_time - $ep_obj->getGrace()) { $add_exception = TRUE; } if ($add_exception == TRUE) { Debug::text('Adding Exception! ' . $ep_obj->getType(), __FILE__, __LINE__, __METHOD__, 10); if (isset($time_stamp_arr['punch_id'])) { $punch_id = $time_stamp_arr['punch_id']; } else { $punch_id = FALSE; } $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $punch_id, 'punch_control_id' => FALSE); unset($punch_id); } else { Debug::text('Not Adding Exception!', __FILE__, __LINE__, __METHOD__, 10); } unset($bp_obj); } unset($bp_objs); } } else { Debug::text(' Break Punches not paired... Skipping!', __FILE__, __LINE__, __METHOD__, 10); } } } else { Debug::text(' No Break Punches found, or none are paired.', __FILE__, __LINE__, __METHOD__, 10); } } break; case 'b3': //Too Many Breaks //Too Many Breaks case 'b4': //Too Few Breaks if ($plf->getRecordCount() > 0) { //Get all break punches. $pair = 0; $x = 0; $out_for_break = FALSE; foreach ($plf as $p_obj) { if ($p_obj->getStatus() == 20 and $p_obj->getType() == 30) { $break_out_timestamp = $p_obj->getTimeStamp(); $break_punch_arr[$pair]['punch_id'] = $p_obj->getId(); $out_for_break = TRUE; } elseif ($out_for_break == TRUE and $p_obj->getStatus() == 10 and $p_obj->getType() == 30) { $break_punch_arr[$pair][20] = $break_out_timestamp; $break_punch_arr[$pair][10] = $p_obj->getTimeStamp(); $out_for_break = FALSE; $pair++; unset($break_out_timestamp); } else { $out_for_break = FALSE; } } unset($pair); //Get daily total time. $daily_total_time = 0; $udtlf = TTnew('UserDateTotalListFactory'); //$udtlf->getByUserDateIdAndStatusAndType( $user_date_id, 10, 10 ); $udtlf->getByUserDateId($user_date_id); if ($udtlf->getRecordCount() > 0) { foreach ($udtlf as $udt_obj) { if ($udt_obj->getTimeCategory() == 'worked_time') { $daily_total_time += $udt_obj->getTotalTime(); } } } Debug::text(' Daily Total Time: ' . $daily_total_time . ' User Date ID: ' . $user_date_id, __FILE__, __LINE__, __METHOD__, 10); //Make sure we take into account how long they have currently worked, so we don't //say too few breaks for 3hr shift that they employee took one break on. //Trigger this exception if the employee doesn't take a break at all? if (isset($break_punch_arr)) { $total_breaks = count($break_punch_arr); //Debug::Arr($break_punch_arr, 'Break Punch Array: ', __FILE__, __LINE__, __METHOD__,10); foreach ($break_punch_arr as $pair => $time_stamp_arr) { if (isset($time_stamp_arr[10]) and isset($time_stamp_arr[20])) { $break_total_time = bcsub($time_stamp_arr[10], $time_stamp_arr[20]); Debug::text(' Break Total Time: ' . $break_total_time, __FILE__, __LINE__, __METHOD__, 10); if (!isset($scheduled_id_cache[$p_obj->getID()])) { $scheduled_id_cache[$p_obj->getID()] = $p_obj->findScheduleID(NULL, $user_date_obj->getUser()); } //Check to see if they have a schedule policy $bplf = TTnew('BreakPolicyListFactory'); if ($p_obj->setScheduleID($scheduled_id_cache[$p_obj->getID()]) == TRUE and is_object($p_obj->getScheduleObject()) == TRUE and is_object($p_obj->getScheduleObject()->getSchedulePolicyObject()) == TRUE) { $break_policy_ids = $p_obj->getScheduleObject()->getSchedulePolicyObject()->getBreakPolicy(); $bplf->getByIdAndCompanyId($break_policy_ids, $user_date_obj->getUserObject()->getCompany()); } else { //$bplf->getByPolicyGroupUserId( $user_date_obj->getUser() ); $bplf->getByPolicyGroupUserIdAndDayTotalTime($user_date_obj->getUser(), $daily_total_time); } unset($break_policy_ids); $allowed_breaks = $bplf->getRecordCount(); $add_exception = FALSE; if (strtolower($ep_obj->getType()) == 'b3' and $total_breaks > $allowed_breaks) { Debug::text(' Too many breaks taken...', __FILE__, __LINE__, __METHOD__, 10); $add_exception = TRUE; } elseif (strtolower($ep_obj->getType()) == 'b4' and $total_breaks < $allowed_breaks) { Debug::text(' Too few breaks taken...', __FILE__, __LINE__, __METHOD__, 10); $add_exception = TRUE; } else { Debug::text(' Proper number of breaks taken...', __FILE__, __LINE__, __METHOD__, 10); } if ($add_exception == TRUE and (strtolower($ep_obj->getType()) == 'b4' or strtolower($ep_obj->getType()) == 'b3' and $pair > $allowed_breaks - 1)) { Debug::text('Adding Exception! ' . $ep_obj->getType(), __FILE__, __LINE__, __METHOD__, 10); if (isset($time_stamp_arr['punch_id']) and strtolower($ep_obj->getType()) == 'b3') { $punch_id = $time_stamp_arr['punch_id']; } else { $punch_id = FALSE; } $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $punch_id, 'punch_control_id' => FALSE); unset($punch_id); } else { Debug::text('Not Adding Exception!', __FILE__, __LINE__, __METHOD__, 10); } } } } } break; case 'b5': //No Break if ($plf->getRecordCount() > 0) { //If they are scheduled or not, we can check for a break policy and base our //decision off that. We don't want a No Break exception on a 3hr short shift though. //Also ignore this exception if the break is auto-deduct. //**Try to assign this exception to a specific punch control id, so we can do searches based on punch branch. //Find break policy //Use scheduled break policy first. $break_policy_obj = NULL; if ($slf->getRecordCount() > 0) { Debug::text('Schedule Found...', __FILE__, __LINE__, __METHOD__, 10); foreach ($slf as $s_obj) { if ($s_obj->getSchedulePolicyObject() !== FALSE) { $break_policy_ids = $s_obj->getSchedulePolicyObject()->getBreakPolicy(); if (is_array($break_policy_ids)) { $bplf = TTNew('BreakPolicyListFactory'); $bplf->getByIdAndCompanyId($break_policy_ids, $user_date_obj->getUserObject()->getCompany()); if ($bplf->getRecordCount() > 0) { foreach ($bplf as $bp_obj) { if ($bp_obj->getType() != 10) { $break_policy_obj = $bp_obj; break; } } } } } unset($s_obj, $break_policy_ids, $bplf, $bp_obj); } } else { Debug::text('No Schedule Found...', __FILE__, __LINE__, __METHOD__, 10); //Check if they have a break policy, with no schedule. $bplf = TTnew('BreakPolicyListFactory'); $bplf->getByPolicyGroupUserId($user_date_obj->getUser()); if ($bplf->getRecordCount() > 0) { Debug::text('Found UnScheduled Break Policy...', __FILE__, __LINE__, __METHOD__, 10); foreach ($bplf as $bp_obj) { if ($bp_obj->getType() != 10) { $break_policy_obj = $bp_obj; break; } } unset($bplf, $bp_obj); } else { //There is no break policy or schedule policy with a break policy assigned to it //With out this we could still apply No Break exceptions, but they will happen even on //a 2minute shift. Debug::text('No break policy, applying No break exception.', __FILE__, __LINE__, __METHOD__, 10); $break_policy_obj = TRUE; } } if (is_object($break_policy_obj) or $break_policy_obj === TRUE) { $punch_control_id = FALSE; $daily_total_time = 0; $udtlf = TTnew('UserDateTotalListFactory'); $udtlf->getByUserDateIdAndStatus($user_date_id, 20); if ($udtlf->getRecordCount() > 0) { foreach ($udtlf as $udt_obj) { $daily_total_time += $udt_obj->getTotalTime(); $punch_control_total_time[$udt_obj->getPunchControlID()] = $udt_obj->getTotalTime(); } } Debug::text('Day Total Time: ' . $daily_total_time, __FILE__, __LINE__, __METHOD__, 10); //Debug::Arr($punch_control_total_time, 'Punch Control Total Time: ', __FILE__, __LINE__, __METHOD__,10); if ($daily_total_time > 0 and ($break_policy_obj === TRUE or $daily_total_time > $break_policy_obj->getTriggerTime())) { //Check for break punch. $break_punch = FALSE; $tmp_punch_total_time = 0; $tmp_punch_control_ids = array(); foreach ($plf as $p_obj) { if ($p_obj->getType() == 30) { //30 = Break Debug::text('Found break Punch: ' . $p_obj->getTimeStamp(), __FILE__, __LINE__, __METHOD__, 10); $break_punch = TRUE; break; } if (isset($punch_control_total_time[$p_obj->getPunchControlID()]) and !isset($tmp_punch_control_ids[$p_obj->getPunchControlID()])) { $tmp_punch_total_time += $punch_control_total_time[$p_obj->getPunchControlID()]; if ($punch_control_id === FALSE and ($break_policy_obj === TRUE or $tmp_punch_total_time > $break_policy_obj->getTriggerTime())) { Debug::text('Found punch control for exception: ' . $p_obj->getPunchControlID(), __FILE__, __LINE__, __METHOD__, 10); $punch_control_id = $p_obj->getPunchControlID(); //Don't break the loop here, as we have to continue on and check for other breaks. } } $tmp_punch_control_ids[$p_obj->getPunchControlID()] = TRUE; } unset($tmp_punch_total_time, $tmp_punch_control_ids); if ($break_punch == FALSE) { Debug::text('Triggering No Break exception!', __FILE__, __LINE__, __METHOD__, 10); $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => $punch_control_id); } } } } break; case 'v1': //TimeSheet Not Verified //Get pay period schedule data, determine if timesheet verification is even enabled. if (is_object($user_date_obj->getPayPeriodObject()) and is_object($user_date_obj->getPayPeriodObject()->getPayPeriodScheduleObject()) and $user_date_obj->getPayPeriodObject()->getPayPeriodScheduleObject()->getTimeSheetVerifyType() > 10) { Debug::text('Verification enabled... Window Start: ' . TTDate::getDate('DATE+TIME', $user_date_obj->getPayPeriodObject()->getTimeSheetVerifyWindowStartDate()) . ' Grace Time: ' . $ep_obj->getGrace(), __FILE__, __LINE__, __METHOD__, 10); //*Only* trigger this exception on the last day of the pay period, because when the pay period is verified it has to force the last day to be recalculated. //Ignore timesheets without any time, (worked and absence). Or we could use the Watch Window to specify the minimum time required on //a timesheet to trigger this instead? //Make sure we are after the timesheet window start date + the grace period. if ($user_date_obj->getPayPeriodObject()->getStatus() != 50 and $current_epoch >= $user_date_obj->getPayPeriodObject()->getTimeSheetVerifyWindowStartDate() + $ep_obj->getGrace() and TTDate::getBeginDayEpoch($user_date_obj->getDateStamp()) == TTDate::getBeginDayEpoch($user_date_obj->getPayPeriodObject()->getEndDate())) { //Get pay period total time, include worked and paid absence time. $udtlf = TTnew('UserDateTotalListFactory'); $total_time = $udtlf->getTimeSumByUserIDAndPayPeriodId($user_date_obj->getUser(), $user_date_obj->getPayPeriodObject()->getID()); if ($total_time > 0) { //Check to see if pay period has been verified or not yet. $pptsvlf = TTnew('PayPeriodTimeSheetVerifyListFactory'); $pptsvlf->getByPayPeriodIdAndUserId($user_date_obj->getPayPeriodObject()->getId(), $user_date_obj->getUser()); $pay_period_verified = FALSE; if ($pptsvlf->getRecordCount() > 0) { $pay_period_verified = $pptsvlf->getCurrent()->getAuthorized(); } if ($pay_period_verified == FALSE) { //Always allow for emailing this exception because it can be triggered after a punch is modified and //any supervisor would need to be notified to verify the timesheet again. $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => FALSE, 'enable_email_notification' => TRUE); } else { Debug::text('TimeSheet has already been authorized!', __FILE__, __LINE__, __METHOD__, 10); } } else { Debug::text('Timesheet does not have any worked or paid absence time...', __FILE__, __LINE__, __METHOD__, 10); } unset($udtlf, $total_time); } else { Debug::text('Not within timesheet verification window, or not after grace time.', __FILE__, __LINE__, __METHOD__, 10); } } else { Debug::text('No Pay Period Schedule or TimeSheet Verificiation disabled...', __FILE__, __LINE__, __METHOD__, 10); } break; case 'j1': //Not Allowed on Job if (getTTProductEdition() >= TT_PRODUCT_CORPORATE and $plf->getRecordCount() > 0) { foreach ($plf as $p_obj) { if ($p_obj->getStatus() == 10) { //In punches if (is_object($p_obj->getPunchControlObject()) and $p_obj->getPunchControlObject()->getJob() > 0) { //Found job punch, check job settings. $jlf = TTnew('JobListFactory'); $jlf->getById($p_obj->getPunchControlObject()->getJob()); if ($jlf->getRecordCount() > 0) { $j_obj = $jlf->getCurrent(); if ($j_obj->isAllowedUser($user_date_obj->getUser()) == FALSE) { $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => $p_obj->getPunchControlId()); } else { Debug::text(' User allowed on Job!', __FILE__, __LINE__, __METHOD__, 10); } } else { Debug::text(' Job not found!', __FILE__, __LINE__, __METHOD__, 10); } } else { //Debug::text(' Not a Job Punch...', __FILE__, __LINE__, __METHOD__,10); } } } unset($j_obj); } break; case 'j2': //Not Allowed on Task if (getTTProductEdition() >= TT_PRODUCT_CORPORATE and $plf->getRecordCount() > 0) { foreach ($plf as $p_obj) { if ($p_obj->getStatus() == 10) { //In punches if (is_object($p_obj->getPunchControlObject()) and $p_obj->getPunchControlObject()->getJob() > 0 and $p_obj->getPunchControlObject()->getJobItem() > 0) { //Found job punch, check job settings. $jlf = TTnew('JobListFactory'); $jlf->getById($p_obj->getPunchControlObject()->getJob()); if ($jlf->getRecordCount() > 0) { $j_obj = $jlf->getCurrent(); if ($j_obj->isAllowedItem($p_obj->getPunchControlObject()->getJobItem()) == FALSE) { $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => $p_obj->getPunchControlId()); } else { Debug::text(' Job item allowed on job!', __FILE__, __LINE__, __METHOD__, 10); } } else { Debug::text(' Job not found!', __FILE__, __LINE__, __METHOD__, 10); } } else { //Debug::text(' Not a Job Punch...', __FILE__, __LINE__, __METHOD__,10); } } } unset($j_obj); } break; case 'j3': //Job already completed if (getTTProductEdition() >= TT_PRODUCT_CORPORATE and $plf->getRecordCount() > 0) { foreach ($plf as $p_obj) { if ($p_obj->getStatus() == 10) { //In punches if (is_object($p_obj->getPunchControlObject()) and $p_obj->getPunchControlObject()->getJob() > 0) { //Found job punch, check job settings. $jlf = TTnew('JobListFactory'); $jlf->getById($p_obj->getPunchControlObject()->getJob()); if ($jlf->getRecordCount() > 0) { $j_obj = $jlf->getCurrent(); //Status is completed and the User Date Stamp is greater then the job end date. //If no end date is set, ignore this. if ($j_obj->getStatus() == 30 and $j_obj->getEndDate() != FALSE and $user_date_obj->getDateStamp() > $j_obj->getEndDate()) { $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => FALSE, 'punch_control_id' => $p_obj->getPunchControlId()); } else { Debug::text(' Job Not Completed!', __FILE__, __LINE__, __METHOD__, 10); } } else { Debug::text(' Job not found!', __FILE__, __LINE__, __METHOD__, 10); } } else { Debug::text(' Not a Job Punch...', __FILE__, __LINE__, __METHOD__, 10); } } } unset($j_obj); } break; case 'j4': //No Job or Task if (getTTProductEdition() >= TT_PRODUCT_CORPORATE and $plf->getRecordCount() > 0) { foreach ($plf as $p_obj) { $add_exception = FALSE; //In punches only if ($p_obj->getStatus() == 10 and is_object($p_obj->getPunchControlObject())) { //If no Tasks are setup, ignore checking them. if ($p_obj->getPunchControlObject()->getJob() == '' or $p_obj->getPunchControlObject()->getJob() == 0 or $p_obj->getPunchControlObject()->getJob() == FALSE) { $add_exception = TRUE; } if ($p_obj->getPunchControlObject()->getJobItem() == '' or $p_obj->getPunchControlObject()->getJobItem() == 0 or $p_obj->getPunchControlObject()->getJobItem() == FALSE) { //Make sure at least one task exists before triggering exception. $jilf = TTNew('JobItemListFactory'); $jilf->getByCompanyID($user_date_obj->getUserObject()->getCompany(), 1); //Limit to just 1 record. if ($jilf->getRecordCount() > 0) { $add_exception = TRUE; } } if ($add_exception === TRUE) { $current_exceptions[] = array('user_date_id' => $user_date_id, 'exception_policy_id' => $ep_obj->getId(), 'type_id' => $type_id, 'punch_id' => $p_obj->getId(), 'punch_control_id' => $p_obj->getPunchControlId()); } } } } break; default: Debug::text('BAD, should never get here: ', __FILE__, __LINE__, __METHOD__, 10); break; } } } unset($ep_obj); $exceptions = self::diffExistingAndCurrentExceptions($existing_exceptions, $current_exceptions); if (is_array($exceptions)) { if (isset($exceptions['create_exceptions']) and is_array($exceptions['create_exceptions']) and count($exceptions['create_exceptions']) > 0) { Debug::text('Creating new exceptions... Total: ' . count($exceptions['create_exceptions']), __FILE__, __LINE__, __METHOD__, 10); foreach ($exceptions['create_exceptions'] as $tmp_exception) { $ef = TTnew('ExceptionFactory'); $ef->setUserDateID($tmp_exception['user_date_id']); $ef->setExceptionPolicyID($tmp_exception['exception_policy_id']); $ef->setType($tmp_exception['type_id']); if (isset($tmp_exception['punch_control_id']) and $tmp_exception['punch_control_id'] != '') { $ef->setPunchControlId($tmp_exception['punch_control_id']); } if (isset($tmp_exception['punch_id']) and $tmp_exception['punch_id'] != '') { $ef->setPunchId($tmp_exception['punch_id']); } $ef->setEnableDemerits(TRUE); if ($ef->isValid()) { $ef->Save(FALSE); //Save exception prior to emailing it, otherwise we can't save audit logs. if ($enable_premature_exceptions == TRUE or isset($tmp_exception['enable_email_notification']) and $tmp_exception['enable_email_notification'] == TRUE) { $eplf = TTnew('ExceptionPolicyListFactory'); $eplf->getById($tmp_exception['exception_policy_id']); if ($eplf->getRecordCount() == 1) { $ep_obj = $eplf->getCurrent(); $ef->emailException($user_date_obj->getUserObject(), $user_date_obj, isset($tmp_exception['punch_obj']) ? $tmp_exception['punch_obj'] : NULL, isset($tmp_exception['schedule_obj']) ? $tmp_exception['schedule_obj'] : NULL, $ep_obj); } } else { Debug::text('Not emailing new exception: User Date ID: ' . $tmp_exception['user_date_id'] . ' Type ID: ' . $tmp_exception['type_id'] . ' Enable PreMature: ' . (int) $enable_premature_exceptions, __FILE__, __LINE__, __METHOD__, 10); } } unset($ef); } } if (isset($exceptions['delete_exceptions']) and is_array($exceptions['delete_exceptions']) and count($exceptions['delete_exceptions']) > 0) { Debug::Text('Deleting no longer valid exceptions... Total: ' . count($exceptions['delete_exceptions']), __FILE__, __LINE__, __METHOD__, 10); $ef = TTnew('ExceptionFactory'); $ef->bulkDelete($exceptions['delete_exceptions']); } } $profiler->stopTimer("ExceptionPolicy::calcExceptions()"); return TRUE; }
function preSave() { if ($this->isNew()) { Debug::text(' Setting Original TimeStamp: ' . $this->getTimeStamp(), __FILE__, __LINE__, __METHOD__, 10); $this->setOriginalTimeStamp($this->getTimeStamp()); } if ($this->getDeleted() == FALSE) { if ($this->isNew() and $this->getTransfer() == TRUE and $this->getEnableAutoTransfer() == TRUE) { Debug::text(' Transfer is Enabled, automatic punch out of last punch pair...', __FILE__, __LINE__, __METHOD__, 10); //Use actual time stamp, not rounded timestamp. This should only be called on new punches as well, otherwise Actual Time Stamp could be incorrect. $p_obj = $this->getPreviousPunchObject($this->getActualTimeStamp()); if (is_object($p_obj)) { Debug::text(' Found Last Punch: ', __FILE__, __LINE__, __METHOD__, 10); if ($p_obj->getStatus() == 10) { Debug::text(' Last Punch was in. Auto Punch Out now: ', __FILE__, __LINE__, __METHOD__, 10); //Make sure the current punch status is IN $this->setStatus(10); //In $this->setType(10); //Normal (can't transfer in/out of lunches?) $pf = TTnew('PunchFactory'); $pf->setUser($this->getUser()); $pf->setEnableAutoTransfer(FALSE); $pf->setPunchControlID($p_obj->getPunchControlID()); $pf->setTransfer(TRUE); $pf->setType($p_obj->getNextType()); $pf->setStatus(20); //Out $pf->setTimeStamp($this->getTimeStamp(), FALSE); //Disable rounding. $pf->setActualTimeStamp($this->getTimeStamp()); //$pf->setOriginalTimeStamp( $this->getTimeStamp() ); //set in preSave() $pf->setLongitude($this->getLongitude()); $pf->setLatitude($this->getLatitude()); if ($pf->isValid()) { if ($pf->Save(FALSE) == TRUE) { $p_obj->getPunchControlObject()->setEnableCalcTotalTime(TRUE); $p_obj->getPunchControlObject()->setEnableCalcSystemTotalTime(TRUE); $p_obj->getPunchControlObject()->setEnableCalcUserDateTotal(TRUE); $p_obj->getPunchControlObject()->setEnableCalcException(TRUE); $p_obj->getPunchControlObject()->setEnablePreMatureException(TRUE); if ($p_obj->getPunchControlObject()->isValid()) { $p_obj->getPunchControlObject()->Save(); } else { Debug::text(' aError saving auto out punch...', __FILE__, __LINE__, __METHOD__, 10); } } else { Debug::text(' bError saving auto out punch...', __FILE__, __LINE__, __METHOD__, 10); } } else { Debug::text(' cError saving auto out punch...', __FILE__, __LINE__, __METHOD__, 10); } } else { Debug::text(' Last Punch was out. No Auto Punch out needed, removing transfer flag from this punch...', __FILE__, __LINE__, __METHOD__, 10); $this->setTransfer(FALSE); } } unset($p_obj, $pf); } //Split punch at midnight. //This has to be an Out punch, and the previous punch has to be an in punch in order for the split to occur. //Check to make sure there is an open punch pair. //Make sure this punch isn't right at midnight either, as no point in splitting a punch at that time. //FIXME: What happens if a supervisor edits a 11:30PM punch and makes it 5:00AM the next day? // We can't split punches when editing existing punches, because we have to split punch_control_ids prior to saving etc... // But we can split when supervisors are adding new punches. //Debug::text('Split at Midnight Enabled: '. $this->getEnableSplitAtMidnight() .' IsNew: '. $this->isNew() .' Status: '. $this->getStatus() .' TimeStamp: '. $this->getTimeStamp() .' Punch Control ID: '. $this->getPunchControlID(), __FILE__, __LINE__, __METHOD__,10); if ($this->isNew() == TRUE and $this->getStatus() == 20 and $this->getEnableSplitAtMidnight() == TRUE and $this->getTimeStamp() != TTDate::getBeginDayEpoch($this->getTimeStamp()) and (is_object($this->getPunchControlObject()) and is_object($this->getPunchControlObject()->getPayPeriodScheduleObject()) and $this->getPunchControlObject()->getPayPeriodScheduleObject()->getShiftAssignedDay() == 40)) { $plf = TTnew('PunchListFactory'); $plf->getPreviousPunchByUserIdAndEpoch($this->getUser(), $this->getTimeStamp()); if ($plf->getRecordCount() > 0) { $p_obj = $plf->getCurrent(); Debug::text(' Found Last Punch... ID: ' . $p_obj->getId() . ' Timestamp: ' . $p_obj->getTimeStamp(), __FILE__, __LINE__, __METHOD__, 10); if ($p_obj->getStatus() == 10 and TTDate::doesRangeSpanMidnight($this->getTimeStamp(), $p_obj->getTimeStamp())) { Debug::text(' Last Punch was in and this is an out punch that spans midnight. Split Punch at midnight now: ', __FILE__, __LINE__, __METHOD__, 10); //FIXME: This will fail if a shift spans multiple days! //Make sure the current punch status is OUT //But we can split LUNCH/Break punches, because someone could punch in at 8PM, then out for lunch at 1:00AM, this would need to be split. $this->setStatus(20); //Out //Reduce the out punch by 60 seconds, and increase the current punch by 60seconds so no time is lost. $this->setTimeStamp($this->getTimeStamp()); //FIXME: May need to use ActualTimeStamp here so we aren't double rounding. //Get new punch control ID for the midnight punch and this one. $new_punch_control_id = $this->getPunchControlObject()->getNextInsertId(); //Since we need to change the PunchControlID, copy the current punch_control object to work around getGenericObject() checking the //IDs and not returning the object anymore. $tmp_punch_control_obj = $this->getPunchControlObject(); $this->setPunchControlID($new_punch_control_id); Debug::text(' Split Punch: Punching out just before midnight yesterday...', __FILE__, __LINE__, __METHOD__, 10); // //Punch out just before midnight //The issue with this is that if rounding is enabled this will ignore it, and the shift for this day may total: 3.98hrs. //when they want it to total 4.00hrs. Why don't we split shifts at exactly midnight with no gap at all? //Split shifts right at midnight causes additional issues when editing those punches, TimeTrex will want to combine them on the same day again. $pf = TTnew('PunchFactory'); $pf->setUser($this->getUser()); $pf->setEnableSplitAtMidnight(FALSE); $pf->setTransfer(FALSE); $pf->setEnableAutoTransfer(FALSE); $pf->setType(10); //Normal $pf->setStatus(20); //Out //We used to have to make this punch 60seconds before midnight, but getShiftData() was modified to support punch at exactly midnight. $before_midnight_timestamp = TTDate::getBeginDayEpoch($this->getTimeStamp()); $pf->setTimeStamp($before_midnight_timestamp, FALSE); //Disable rounding. $pf->setActualTimeStamp($before_midnight_timestamp); //$pf->setOriginalTimeStamp( $before_midnight_timestamp ); //set in preSave() $pf->setPunchControlID($p_obj->getPunchControlID()); if ($pf->isValid()) { if ($pf->Save(FALSE) == TRUE) { $p_obj->getPunchControlObject()->setEnableCalcTotalTime(TRUE); $p_obj->getPunchControlObject()->setEnableCalcSystemTotalTime(TRUE); $p_obj->getPunchControlObject()->setEnableCalcUserDateTotal(TRUE); $p_obj->getPunchControlObject()->setEnableCalcException(TRUE); $p_obj->getPunchControlObject()->setEnablePreMatureException(TRUE); if ($p_obj->getPunchControlObject()->isValid()) { $p_obj->getPunchControlObject()->Save(); } } } unset($pf, $p_obj, $before_midnight_timestamp); Debug::text(' Split Punch: Punching int at midnight today...', __FILE__, __LINE__, __METHOD__, 10); // //Punch in again right at midnight. // $pf = TTnew('PunchFactory'); $pf->setUser($this->getUser()); $pf->setEnableSplitAtMidnight(FALSE); $pf->setTransfer(FALSE); $pf->setEnableAutoTransfer(FALSE); $pf->setType(10); //Normal $pf->setStatus(10); //In $at_midnight_timestamp = TTDate::getBeginDayEpoch($this->getTimeStamp()); $pf->setTimeStamp($at_midnight_timestamp, FALSE); //Disable rounding. $pf->setActualTimeStamp($at_midnight_timestamp); //$pf->setOriginalTimeStamp( $at_midnight_timestamp ); //set in preSave() $pf->setPunchControlID($new_punch_control_id); if ($pf->isValid()) { if ($pf->Save(FALSE) == TRUE) { $pcf = TTnew('PunchControlFactory'); $pcf->setId($pf->getPunchControlID()); $pcf->setPunchObject($pf); $pcf->setBranch($tmp_punch_control_obj->getBranch()); $pcf->setDepartment($tmp_punch_control_obj->getDepartment()); $pcf->setJob($tmp_punch_control_obj->getJob()); $pcf->setJobItem($tmp_punch_control_obj->getJobItem()); $pcf->setOtherID1($tmp_punch_control_obj->getOtherID1()); $pcf->setOtherID2($tmp_punch_control_obj->getOtherID2()); $pcf->setOtherID3($tmp_punch_control_obj->getOtherID3()); $pcf->setOtherID4($tmp_punch_control_obj->getOtherID4()); $pcf->setOtherID5($tmp_punch_control_obj->getOtherID5()); $pcf->setEnableStrictJobValidation(TRUE); $pcf->setEnableCalcUserDateID(TRUE); $pcf->setEnableCalcTotalTime(TRUE); $pcf->setEnableCalcSystemTotalTime(TRUE); $pcf->setEnableCalcWeeklySystemTotalTime(TRUE); $pcf->setEnableCalcUserDateTotal(TRUE); $pcf->setEnableCalcException(TRUE); if ($pcf->isValid() == TRUE) { $pcf->Save(TRUE, TRUE); //Force isNEW() lookup. } } } unset($pf, $at_midnight_timestamp, $new_punch_control_id, $tmp_punch_control_obj); } else { Debug::text(' Last Punch was out. No Auto Punch ', __FILE__, __LINE__, __METHOD__, 10); } } } } return TRUE; }