'ID', //'user_date_id' => 'UserDateID', 'user_id' => 'User', 'date_stamp' => 'DateStamp', 'pay_period_id' => 'PayPeriod', 'branch_id' => 'Branch', 'department_id' => 'Department', 'job_id' => 'Job', 'job_item_id' => 'JobItem', 'punch_tag_id' => 'PunchTag', 'quantity' => 'Quantity', 'bad_quantity' => 'BadQuantity', 'total_time' => 'TotalTime', 'actual_total_time' => 'ActualTotalTime', //'meal_policy_id' => 'MealPolicyID', 'note' => 'Note', 'deleted' => 'Deleted', ]; return $variable_function_map; } /** * @param bool $enable * @return bool */ function setEnableRequiredFieldCheck( $enable ) { $this->enable_required_field_check = $enable; return true; } /** * @return bool */ function getEnableRequiredFieldCheck() { return $this->enable_required_field_check; } /** * @return bool|UserFactory */ function getUserObject() { return $this->getGenericObject( 'UserListFactory', $this->getUser(), 'user_obj' ); } /** * @return bool */ function getPayPeriodObject() { return $this->getGenericObject( 'PayPeriodListFactory', $this->getPayPeriod(), 'pay_period_obj' ); } /** * @return null|object */ function getPLFByPunchControlID() { if ( $this->plf == null && $this->getID() != false ) { $this->plf = TTnew( 'PunchListFactory' ); $this->plf->getByPunchControlID( $this->getID() ); } return $this->plf; } /** * @return bool|null */ function getPayPeriodScheduleObject() { if ( is_object( $this->pay_period_schedule_obj ) ) { return $this->pay_period_schedule_obj; } else { if ( TTUUID::isUUID( $this->getUser() ) && $this->getUser() != TTUUID::getZeroID() && $this->getUser() != TTUUID::getNotExistID() ) { $ppslf = TTnew( 'PayPeriodScheduleListFactory' ); /** @var PayPeriodScheduleListFactory $ppslf */ $ppslf->getByUserId( $this->getUser() ); if ( $ppslf->getRecordCount() == 1 ) { $this->pay_period_schedule_obj = $ppslf->getCurrent(); return $this->pay_period_schedule_obj; } else { //Build a temporary default PP schedule so we can still return some shift data at least. // Otherwise many punch validations check won't be run like just determing if a duplicate punch exists. // This fixes a bug where a user is not assigned to a PP schedule and they could enter duplicate punches (ie: 2x Normal In @ 8A) $pps_obj = TTnew('PayPeriodScheduleFactory'); $pps_obj->setMaximumShiftTime( ( 16 * 3600 ) ); $pps_obj->setNewDayTriggerTime( ( 4 * 3600 ) ); $pps_obj->getShiftAssignedDay( 10 ); $this->pay_period_schedule_obj = $pps_obj; return $this->pay_period_schedule_obj; } } return false; } } /** * @return null */ function getShiftData() { if ( $this->shift_data == null && is_object( $this->getPunchObject() ) && TTUUID::isUUID( $this->getUser() ) && $this->getUser() != TTUUID::getZeroID() && $this->getUser() != TTUUID::getNotExistID() ) { if ( is_object( $this->getPayPeriodScheduleObject() ) ) { $this->shift_data = $this->getPayPeriodScheduleObject()->getShiftData( null, $this->getUser(), $this->getPunchObject()->getTimeStamp(), 'nearest_shift', $this ); } else { Debug::Text( 'No pay period schedule found for user ID: ' . $this->getUser(), __FILE__, __LINE__, __METHOD__, 10 ); } } return $this->shift_data; } /** * @return bool */ function getJobObject() { return $this->getGenericObject( 'JobListFactory', $this->getJob(), 'job_obj' ); } /** * @return bool */ function getJobItemObject() { return $this->getGenericObject( 'JobItemListFactory', $this->getJobItem(), 'job_item_obj' ); } /** * @return bool|null */ function getPunchObject() { if ( is_object( $this->punch_obj ) ) { return $this->punch_obj; } return false; } /** * @param object $obj * @return bool */ function setPunchObject( $obj ) { if ( is_object( $obj ) ) { $this->punch_obj = $obj; //Set the user/datestamp based on the punch. if ( $obj->getUser() != false && $obj->getUser() != $this->getUser() ) { $this->setUser( $obj->getUser() ); } if ( $obj->getTimeStamp() != false && ( $this->getDateStamp() == false || TTDate::getMiddleDayEpoch( $obj->getTimeStamp() ) != TTDate::getMiddleDayEpoch( $this->getDateStamp() ) ) ) { $this->setDateStamp( $obj->getTimeStamp() ); } return true; } return false; } /** * @return bool|mixed */ function getUser() { return $this->getGenericDataValue( 'user_id' ); } /** * @param $value * @return bool */ function setUser( $value ) { $value = TTUUID::castUUID( $value ); //Need to be able to support user_id=0 for open shifts. But this can cause problems with importing punches with user_id=0. return $this->setGenericDataValue( 'user_id', $value ); } /** * @return bool|mixed */ function getPayPeriod() { return $this->getGenericDataValue( 'pay_period_id' ); } /** * @param null $value * @return bool */ function setPayPeriod( $value = null ) { if ( $value == null ) { $value = PayPeriodListFactory::findPayPeriod( $this->getUser(), $this->getDateStamp() ); } $value = TTUUID::castUUID( $value ); //Allow NULL pay period, incase its an absence or something in the future. //Cron will fill in the pay period later. return $this->setGenericDataValue( 'pay_period_id', $value ); } /** * @param bool $raw * @return bool|false|int */ function getDateStamp( $raw = false ) { $value = $this->getGenericDataValue( 'date_stamp' ); if ( $value !== false ) { if ( $raw === true ) { return $value; } else { return TTDate::getMiddleDayEpoch( TTDate::strtotime( $value ) ); } } return false; } /** * @param $value * @return bool */ function setDateStamp( $value ) { $value = (int)$value; if ( $value > 0 ) { $value = TTDate::getMiddleDayEpoch( $value ); if ( $this->getDateStamp() !== $value && $this->getOldDateStamp() != $this->getDateStamp() && (int)$this->getDateStamp() != 0 ) { //Only set OldDateStamp if its not empty, that way it won't override an already set OldDateStamp that is valid. Debug::Text( ' Setting Old DateStamp... Current Old DateStamp: ' . (int)$this->getOldDateStamp() . ' Current DateStamp: ' . (int)$this->getDateStamp(), __FILE__, __LINE__, __METHOD__, 10 ); $this->setOldDateStamp( $this->getDateStamp() ); } } $retval = $this->setGenericDataValue( 'date_stamp', $value ); //If the pay period hasn't been set, or the date stamp changed (therefore OldDateStamp() is not empty), try to find the correct pay period. if ( $value > 0 && ( $this->getPayPeriod() == false || !empty( $this->getOldDateStamp() ) ) ) { $this->setPayPeriod(); //Force pay period to be set as soon as the date is. } return $retval; } /** * This must be called after PunchObject() has been set and before isValid() is called. * @return bool */ function findUserDate() { /* Issues to consider: ** Timezones, if one employee is in PST and the payroll administrator/pay period is in EST, if the employee ** punches in at 11:00PM PST, its actually 2AM EST on the next day, so which day does the time get assigned to? ** Use the employees preferred timezone to determine the proper date, otherwise if we use the PP schedule timezone it may ** be a little confusing to employees because they may punch in on one day and have the time appears under different day. 1. Employee punches out at 11:00PM, then is called in early at 4AM to start a new shift. Don't want to pair these punches. 2. Employee starts 11:00PM shift late at 1:00AM the next day. Works until 7AM, then comes in again at 11:00PM the same day and works until 4AM, then 4:30AM to 7:00AM. The 4AM-7AM punches need to be paired on the same day. 3. Ambulance EMT works 36hours straight in a single punch. *Perhaps we should handle lunch punches and normal punches differently? Lunch punches have a different "continuous time setting then normal punches. *Change daily continuous time to: * Group (Normal) Punches: X hours before midnight to punches X hours after midnight * Group (Lunch/Break) Punches: X hours before midnight to punches X hours after midnight * Normal punches X hours after midnight group to punches X hours before midnight. * Lunch/Break punches X hours after midnight group to punches X hours before midnight. OR, what if we change continuous time to be just the gap between punches that cause a new day to start? Combine this with daily cont. time so we know what the window is for punches to begin the gap search. Or we can always just search for a previous punch Xhrs before the current punch. - Do we look back to a In punch, or look back to an Out punch though? I think an Out Punch. What happens if they forgot to punch out though? Logic: If this is an Out punch: Find previous punch back to maximum shift time to find an In punch to pair it with. Else, if this is an In punch: Find previous punch back to maximum shift time to find an Out punch to combine it with. If out punch is found inside of new_shift trigger time, we place this punch on the previous day. Else: we place this punch on todays date. * Minimum time between punches to cause a new shift to start: Xhrs (default: 4hrs) new_day_trigger_time Call it: Minimum time-off that triggers new shift: Minimum Time-Off Between Shifts: * Maximum shift time: Xhrs (for ambulance service) default to 16 or 24hrs? This is essentially how far back we look for In punch to pair out punches with. maximum_shift_length - Add checks to ensure that no punch pair exceeds the maximum_shift_length */ //Don't allow user_id=0, that is only used for open scheduled shifts, and sometimes this can sneak through during import. if ( TTUUID::castUUID( $this->getUser() ) == TTUUID::getZeroID() ) { Debug::Text( 'ERROR: User ID is 0!: ' . $this->getUser(), __FILE__, __LINE__, __METHOD__, 10 ); return false; } /* This needs to be able to run before Validate is called, so we can validate the pay period schedule. */ if ( $this->getDateStamp() == false ) { $this->setDateStamp( $this->getPunchObject()->getTimeStamp() ); } Debug::Text( ' Finding DateStamp: ' . TTDate::getDate( 'DATE+TIME', $this->getPunchObject()->getTimeStamp() ) . ' Punch Control: ' . $this->getID() . ' User: ' . $this->getUser(), __FILE__, __LINE__, __METHOD__, 10 ); $shift_data = $this->getShiftData(); if ( is_array( $shift_data ) ) { switch ( $this->getPayPeriodScheduleObject()->getShiftAssignedDay() ) { default: case 10: //Day they start on case 40: //Split at midnight if ( !isset( $shift_data['first_in']['time_stamp'] ) ) { $shift_data['first_in']['time_stamp'] = $shift_data['last_out']['time_stamp']; } //Can't use the First In user_date_id because it may need to be changed when editing a punch. //Debug::Text('Assign Shifts to the day they START on... Date: '. TTDate::getDate('DATE', $shift_data['first_in']['time_stamp']), __FILE__, __LINE__, __METHOD__, 10); $user_date_epoch = $shift_data['first_in']['time_stamp']; break; case 20: //Day they end on if ( !isset( $shift_data['last_out']['time_stamp'] ) ) { $shift_data['last_out']['time_stamp'] = $shift_data['first_in']['time_stamp']; } Debug::Text( 'Assign Shifts to the day they END on... Date: ' . TTDate::getDate( 'DATE', $shift_data['last_out']['time_stamp'] ), __FILE__, __LINE__, __METHOD__, 10 ); $user_date_epoch = $shift_data['last_out']['time_stamp']; break; case 30: //Day with most time worked Debug::Text( 'Assign Shifts to the day they WORK MOST on... Date: ' . TTDate::getDate( 'DATE', $shift_data['day_with_most_time'] ), __FILE__, __LINE__, __METHOD__, 10 ); $user_date_epoch = $shift_data['day_with_most_time']; break; } } else { Debug::Text( 'Not using shift data...', __FILE__, __LINE__, __METHOD__, 10 ); if ( $this->getPunchObject()->getDeleted() == true ) { //Check to see if there is another punch in the punch pair, and use that timestamp to assign days instead. Debug::Text( 'Punch is being deleted, use timestamp from other punch in pair if it exists...', __FILE__, __LINE__, __METHOD__, 10 ); $plf = TTNew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->getByPunchControlId( $this->getId() ); if ( $plf->getRecordCount() > 0 ) { foreach ( $plf as $p_obj ) { if ( $p_obj->getId() != $this->getPunchObject()->getId() ) { $user_date_epoch = $p_obj->getTimeStamp(); Debug::Text( 'Using timestamp from Punch: ' . $this->getPunchObject()->getId(), __FILE__, __LINE__, __METHOD__, 10 ); break; } } } else { Debug::Text( 'No punches left in punch pair...', __FILE__, __LINE__, __METHOD__, 10 ); return true; } unset( $plf, $p_obj ); } else { $user_date_epoch = $this->getPunchObject()->getTimeStamp(); } } if ( isset( $user_date_epoch ) && $user_date_epoch > 0 ) { $user_date_epoch = TTDate::getMiddleDayEpoch( $user_date_epoch ); Debug::Text( 'Found DateStamp: ' . $user_date_epoch . ' Based On: ' . TTDate::getDate( 'DATE+TIME', $user_date_epoch ), __FILE__, __LINE__, __METHOD__, 10 ); if ( $this->getDateStamp() != $user_date_epoch ) { //Don't set datestamp if its the same, as this triggers setPayPeriod() to be called again. return $this->setDateStamp( $user_date_epoch ); } return true; } Debug::Text( 'No shift data to use to find DateStamp, using timestamp only: ' . TTDate::getDate( 'DATE+TIME', $this->getPunchObject()->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10 ); return true; } /** * @return bool */ function getOldDateStamp() { return $this->getGenericTempDataValue( 'old_date_stamp' ); } /** * @param $value * @return bool */ function setOldDateStamp( $value ) { Debug::Text( ' Setting Old DateStamp: ' . TTDate::getDate( 'DATE', $value ), __FILE__, __LINE__, __METHOD__, 10 ); return $this->setGenericTempDataValue( 'old_date_stamp', TTDate::getMiddleDayEpoch( $value ) ); } /** * @return bool|mixed */ function getBranch() { return $this->getGenericDataValue( 'branch_id' ); } /** * @param string $value UUID * @return bool */ function setBranch( $value ) { $value = TTUUID::castUUID( $value ); //If TTUUID::getNotExistID( 2 ) is passed in, handle it in preValidate() at which point we should have more information to get the previous punch. if ( $this->getUser() != '' && is_object( $this->getUserObject() ) && $value == TTUUID::getNotExistID() ) { //Find default $value = $this->getUserObject()->getDefaultBranch(); Debug::Text( 'Using Default Branch: ' . $value, __FILE__, __LINE__, __METHOD__, 10 ); } return $this->setGenericDataValue( 'branch_id', $value ); } /** * @return bool|mixed */ function getDepartment() { return $this->getGenericDataValue( 'department_id' ); } /** * @param string $value UUID * @return bool */ function setDepartment( $value ) { $value = TTUUID::castUUID( $value ); //If TTUUID::getNotExistID( 2 ) is passed in, handle it in preValidate() at which point we should have more information to get the previous punch. if ( $this->getUser() != '' && is_object( $this->getUserObject() ) && $value == TTUUID::getNotExistID() ) { //Find default $value = $this->getUserObject()->getDefaultDepartment(); Debug::Text( 'Using Default Department: ' . $value, __FILE__, __LINE__, __METHOD__, 10 ); } return $this->setGenericDataValue( 'department_id', $value ); } /** * @return bool|mixed */ function getJob() { return $this->getGenericDataValue( 'job_id' ); } /** * @param string $value UUID * @return bool */ function setJob( $value ) { $value = TTUUID::castUUID( $value ); if ( getTTProductEdition() <= TT_PRODUCT_PROFESSIONAL ) { $value = TTUUID::getZeroID(); } //If TTUUID::getNotExistID( 2 ) is passed in, handle it in preValidate() at which point we should have more information to get the previous punch. if ( $this->getUser() != '' && is_object( $this->getUserObject() ) && $value == TTUUID::getNotExistID() ) { //Find default $value = $this->getUserObject()->getDefaultJob(); Debug::Text( 'Using Default Job: ' . $value, __FILE__, __LINE__, __METHOD__, 10 ); } return $this->setGenericDataValue( 'job_id', $value ); } /** * @return bool|mixed */ function getJobItem() { return $this->getGenericDataValue( 'job_item_id' ); } /** * @param string $value UUID * @return bool */ function setJobItem( $value ) { $value = TTUUID::castUUID( $value ); if ( getTTProductEdition() <= TT_PRODUCT_PROFESSIONAL ) { $value = TTUUID::getZeroID(); } //If TTUUID::getNotExistID( 2 ) is passed in, handle it in preValidate() at which point we should have more information to get the previous punch. if ( $this->getUser() != '' && is_object( $this->getUserObject() ) && $value == TTUUID::getNotExistID() ) { //Find default $value = $this->getUserObject()->getDefaultJobItem(); Debug::Text( 'Using Default Job Item: ' . $value, __FILE__, __LINE__, __METHOD__, 10 ); } return $this->setGenericDataValue( 'job_item_id', $value ); } /** * @return array */ function getPunchTag() { //Always return an array. $this->decodeJSONColumn( 'punch_tag_id' ); $value = $this->getGenericDataValue( 'punch_tag_id' ); if ( $value == false ) { return []; } return $value; } /** * @param string | array $value UUID * @return bool */ function setPunchTag( $value ) { if ( getTTProductEdition() <= TT_PRODUCT_PROFESSIONAL ) { $value = null; } if ( $this->getUser() != '' && is_object( $this->getUserObject() ) && ( $value == TTUUID::getNotExistID() || ( is_array( $value ) && in_array( TTUUID::getNotExistID(), $value, true ) ) ) ) { //Find default $value = $this->getUserObject()->getDefaultPunchTag(); Debug::Text( 'Using Default Punch Tag: ' . implode( ',', (array)$value ), __FILE__, __LINE__, __METHOD__, 10 ); } if ( $value == TTUUID::getZeroID() || empty( $value ) || ( is_array( $value ) && count( $value ) == 1 && isset( $value[0] ) && $value[0] == TTUUID::getZeroID() ) ) { $value = null; } if ( !is_array( $value ) && TTUUID::isUUID( $value ) ) { $value = [ $value ]; } return $this->setGenericDataValue( 'punch_tag_id', $value ); } /** * @return bool|float */ function getQuantity() { return $this->getGenericDataValue( 'quantity' ); } /** * @param $value * @return bool */ function setQuantity( $value ) { if ( $value == false || $value == 0 || $value == '' ) { $value = 0; } return $this->setGenericDataValue( 'quantity', $value ); } /** * @return bool|float */ function getBadQuantity() { return $this->getGenericDataValue( 'bad_quantity' ); } /** * @param $value * @return bool */ function setBadQuantity( $value ) { if ( $value == false || $value == 0 || $value == '' ) { $value = 0; } return $this->setGenericDataValue( 'bad_quantity', $value ); } /** * @return bool|int */ function getTotalTime() { return $this->getGenericDataValue( 'total_time' ); } /** * @param $value * @return bool */ function setTotalTime( $value ) { $value = (int)$value; return $this->setGenericDataValue( 'total_time', $value ); } /** * @return bool|int */ function getActualTotalTime() { return $this->getGenericDataValue( 'actual_total_time' ); } /** * @param $value * @return bool */ function setActualTotalTime( $value ) { $value = (int)$value; if ( $value < 0 ) { $value = 0; } return $this->setGenericDataValue( 'actual_total_time', $value ); } /* function getMealPolicyID() { return $this->getGenericDataValue( 'meal_policy_id' ); } function setMealPolicyID($id) { $id = trim($id); if ( $id == '' OR empty($id) ) { $id = NULL; } $mplf = TTnew( 'MealPolicyListFactory' ); if ( $id == NULL OR $this->Validator->isResultSetWithRows( 'meal_policy', $mplf->getByID($id), TTi18n::gettext('Meal Policy is invalid') ) ) { $this->setGenericDataValue( 'meal_policy_id', $id ); return TRUE; } return FALSE; } */ /** * @return bool|mixed */ function getNote() { return $this->getGenericDataValue( 'note' ); } /** * @param $value * @return bool */ function setNote( $value ) { $value = trim( $value ); return $this->setGenericDataValue( 'note', $value ); } /** * @param bool $force * @return bool * @noinspection PhpUndefinedVariableInspection */ function calcTotalTime( $force = true ) { if ( $force == true || $this->is_total_time_calculated == false ) { $this->is_total_time_calculated = true; $plf = TTnew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->getByPunchControlId( $this->getId() ); //Make sure punches are in In/Out pairs before we bother calculating. if ( $plf->getRecordCount() > 0 && ( $plf->getRecordCount() % 2 ) == 0 ) { Debug::text( ' Found Punches to calculate.', __FILE__, __LINE__, __METHOD__, 10 ); $in_pair = false; foreach ( $plf as $punch_obj ) { //Check for proper in/out pairs //First row should be an Out status (reverse ordering) Debug::text( ' Punch: Status: ' . $punch_obj->getStatus() . ' TimeStamp: ' . $punch_obj->getTimeStamp(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $punch_obj->getStatus() == 20 ) { //Debug::text(' Found Out Status, starting pair: ', __FILE__, __LINE__, __METHOD__, 10); $this->out_punch_obj = $punch_obj; $out_stamp = $punch_obj->getTimeStamp(); $out_actual_stamp = $punch_obj->getActualTimeStamp(); $in_pair = true; } else if ( $in_pair == true ) { $this->in_punch_obj = $punch_obj; $punch_obj->setScheduleID( $punch_obj->findScheduleID( null, $this->getUser() ) ); //Find Schedule Object for this Punch $in_stamp = $punch_obj->getTimeStamp(); $in_actual_stamp = $punch_obj->getActualTimeStamp(); //Got a pair... Totaling. //Debug::text(' Found a pair... Totaling: ', __FILE__, __LINE__, __METHOD__, 10); if ( $out_stamp != '' && $in_stamp != '' ) { //Due to DST, always pay the employee based on the time they actually worked, //which is handled automatically by simple epoch math. //Therefore in fall they get paid one hour more, and spring one hour less. $total_time = ( $out_stamp - $in_stamp );// + TTDate::getDSTOffset( $in_stamp, $out_stamp ); } if ( $out_actual_stamp != '' && $in_actual_stamp != '' ) { $actual_total_time = ( $out_actual_stamp - $in_actual_stamp ); } } } if ( isset( $total_time ) ) { Debug::text( ' Setting TotalTime: ' . $total_time, __FILE__, __LINE__, __METHOD__, 10 ); $this->setTotalTime( $total_time ); $this->setActualTotalTime( $actual_total_time ); return true; } } else { Debug::text( ' No Punches to calculate, or punches arent in pairs. Set total to 0', __FILE__, __LINE__, __METHOD__, 10 ); $this->setTotalTime( 0 ); $this->setActualTotalTime( 0 ); return true; } } return false; } /** * @return bool */ function changePreviousPunchType() { Debug::text( ' Previous Punch to Lunch/Break...', __FILE__, __LINE__, __METHOD__, 10 ); if ( is_object( $this->getPunchObject() ) ) { if ( $this->getPunchObject()->getType() == 20 && $this->getPunchObject()->getStatus() == 10 ) { Debug::text( ' bbPrevious Punch to Lunch...', __FILE__, __LINE__, __METHOD__, 10 ); //We used to use getShiftData() then pull out the previous punch from that, however that can cause problems //based on the Minimum Time-Off Between Shifts. Either way though that can't be less than the lunch/break autodetection time. $previous_punch_obj = $this->getPunchObject()->getPreviousPunchObject( $this->getPunchObject()->getActualTimeStamp() ); if ( is_object( $previous_punch_obj ) && $previous_punch_obj->getType() != 20 ) { Debug::text( ' Previous Punch ID: ' . $previous_punch_obj->getId(), __FILE__, __LINE__, __METHOD__, 10 ); $this->getPunchObject()->setScheduleID( $this->getPunchObject()->findScheduleID() ); if ( $this->getPunchObject()->inMealPolicyWindow( $this->getPunchObject()->getTimeStamp(), $previous_punch_obj->getTimeStamp(), $previous_punch_obj->getStatus() ) == true ) { Debug::text( ' Previous Punch needs to change to Lunch...', __FILE__, __LINE__, __METHOD__, 10 ); $plf = TTnew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->getById( $previous_punch_obj->getId() ); if ( $plf->getRecordCount() == 1 ) { Debug::text( ' Modifying previous punch...', __FILE__, __LINE__, __METHOD__, 10 ); $pf = $plf->getCurrent(); $pf->setUser( $this->getUser() ); $pf->setType( 20 ); //Lunch //If we start re-rounding this punch we have to recalculate the total for the previous punch_control too. $pf->setTimeStamp( $pf->getTimeStamp() ); //Re-round timestamp now that its a lunch punch. if ( $pf->isValid() == true ) { if ( $pf->Save( false ) == true ) { $pcf = $pf->getPunchControlObject(); $pcf->setPunchObject( $pf ); $pcf->setEnableCalcUserDateID( true ); $pcf->setEnableCalcTotalTime( true ); $pcf->setEnableCalcSystemTotalTime( true ); $pcf->setEnableCalcWeeklySystemTotalTime( true ); $pcf->setEnableCalcUserDateTotal( true ); if ( $pcf->isValid() == true ) { Debug::Text( ' Punch Control is valid, saving...', __FILE__, __LINE__, __METHOD__, 10 ); if ( $pcf->Save( true, true ) == true ) { //Force isNew() lookup.\ Debug::text( ' Returning TRUE!', __FILE__, __LINE__, __METHOD__, 10 ); return true; } } } } else { Debug::Text( ' Punch is NOT valid, unable to save! (a)', __FILE__, __LINE__, __METHOD__, 10 ); } } } } } else if ( $this->getPunchObject()->getType() == 30 && $this->getPunchObject()->getStatus() == 10 ) { Debug::text( ' bbPrevious Punch to Break...', __FILE__, __LINE__, __METHOD__, 10 ); //We used to use getShiftData() then pull out the previous punch from that, however that can cause problems //based on the Minimum Time-Off Between Shifts. Either way though that can't be less than the lunch/break autodetection time. $previous_punch_obj = $this->getPunchObject()->getPreviousPunchObject( $this->getPunchObject()->getActualTimeStamp() ); if ( is_object( $previous_punch_obj ) && $previous_punch_obj->getType() != 30 ) { Debug::text( ' Previous Punch ID: ' . $previous_punch_obj->getId(), __FILE__, __LINE__, __METHOD__, 10 ); $this->getPunchObject()->setScheduleID( $this->getPunchObject()->findScheduleID() ); if ( $this->getPunchObject()->inBreakPolicyWindow( $this->getPunchObject()->getTimeStamp(), $previous_punch_obj->getTimeStamp(), $previous_punch_obj->getStatus() ) == true ) { Debug::text( ' Previous Punch needs to change to Break...', __FILE__, __LINE__, __METHOD__, 10 ); $plf = TTnew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->getById( $previous_punch_obj->getId() ); if ( $plf->getRecordCount() == 1 ) { Debug::text( ' Modifying previous punch...', __FILE__, __LINE__, __METHOD__, 10 ); $pf = $plf->getCurrent(); $pf->setUser( $this->getUser() ); $pf->setType( 30 ); //Break //If we start re-rounding this punch we have to recalculate the total for the previous punch_control too. $pf->setTimeStamp( $pf->getTimeStamp() ); //Re-round timestamp now that its a break punch. if ( $pf->isValid() == true ) { Debug::Text( ' Punch is valid, saving...', __FILE__, __LINE__, __METHOD__, 10 ); if ( $pf->Save( false ) == true ) { $pcf = $pf->getPunchControlObject(); $pcf->setPunchObject( $pf ); $pcf->setEnableCalcUserDateID( true ); $pcf->setEnableCalcTotalTime( true ); $pcf->setEnableCalcSystemTotalTime( true ); $pcf->setEnableCalcWeeklySystemTotalTime( true ); $pcf->setEnableCalcUserDateTotal( true ); if ( $pcf->isValid() == true ) { Debug::Text( ' Punch Control is valid, saving...', __FILE__, __LINE__, __METHOD__, 10 ); if ( $pcf->Save( true, true ) == true ) { //Force isNew() lookup. Debug::text( ' Returning TRUE!', __FILE__, __LINE__, __METHOD__, 10 ); return true; } } } } else { Debug::Text( ' Punch is NOT valid, unable to save! (b)', __FILE__, __LINE__, __METHOD__, 10 ); } } } } } } Debug::text( ' Returning FALSE!', __FILE__, __LINE__, __METHOD__, 10 ); return false; } /** * @return bool */ function getEnableCalcSystemTotalTime() { if ( isset( $this->calc_system_total_time ) ) { return $this->calc_system_total_time; } return false; } /** * @param $bool * @return bool */ function setEnableCalcSystemTotalTime( $bool ) { $this->calc_system_total_time = $bool; return true; } /** * @return bool */ function getEnableCalcWeeklySystemTotalTime() { if ( isset( $this->calc_weekly_system_total_time ) ) { return $this->calc_weekly_system_total_time; } return false; } /** * @param $bool * @return bool */ function setEnableCalcWeeklySystemTotalTime( $bool ) { $this->calc_weekly_system_total_time = $bool; return true; } /** * @return bool */ function getEnableCalcException() { if ( isset( $this->calc_exception ) ) { return $this->calc_exception; } return false; } /** * @param $bool * @return bool */ function setEnableCalcException( $bool ) { $this->calc_exception = $bool; return true; } /** * @return bool */ function getEnablePreMatureException() { if ( isset( $this->premature_exception ) ) { return $this->premature_exception; } return false; } /** * @param $bool * @return bool */ function setEnablePreMatureException( $bool ) { $this->premature_exception = $bool; return true; } /** * @return bool */ function getEnableCalcUserDateTotal() { if ( isset( $this->calc_user_date_total ) ) { return $this->calc_user_date_total; } return false; } /** * @param $bool * @return bool */ function setEnableCalcUserDateTotal( $bool ) { $this->calc_user_date_total = $bool; return true; } /** * @return bool */ function getEnableCalcUserDateID() { if ( isset( $this->calc_user_date_id ) ) { return $this->calc_user_date_id; } return false; } /** * @param $bool * @return bool */ function setEnableCalcUserDateID( $bool ) { $this->calc_user_date_id = $bool; return true; } /** * @return bool */ function getEnableCalcTotalTime() { if ( isset( $this->calc_total_time ) ) { return $this->calc_total_time; } return false; } /** * @param $bool * @return bool */ function setEnableCalcTotalTime( $bool ) { $this->calc_total_time = $bool; return true; } /** * @return bool */ function getEnableStrictJobValidation() { if ( isset( $this->strict_job_validiation ) ) { return $this->strict_job_validiation; } return false; } /** * @param $bool * @return bool */ function setEnableStrictJobValidation( $bool ) { $this->setIsValid( false ); //Force revalidation when data is changed. $this->strict_job_validiation = $bool; return true; } /** * @param bool $ignore_warning * @return bool */ function Validate( $ignore_warning = true ) { Debug::text( 'Validating...', __FILE__, __LINE__, __METHOD__, 10 ); // // BELOW: Validation code moved from set*() functions. // // User $ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */ $this->Validator->isResultSetWithRows( 'user', $ulf->getByID( $this->getUser() ), TTi18n::gettext( 'Invalid Employee' ) ); // Pay Period if ( $this->getPayPeriod() !== false && $this->getPayPeriod() != TTUUID::getZeroID() ) { $pplf = TTnew( 'PayPeriodListFactory' ); /** @var PayPeriodListFactory $pplf */ $this->Validator->isResultSetWithRows( 'pay_period', $pplf->getByID( $this->getPayPeriod() ), TTi18n::gettext( 'Invalid Pay Period' ) ); } // Date $this->Validator->isDate( 'date_stamp', $this->getDateStamp(), TTi18n::gettext( 'Incorrect date' ) . '(a)' ); if ( $this->Validator->isError( 'date_stamp' ) == false ) { if ( $this->getDateStamp() == '' || $this->getDateStamp() <= 0 ) { $this->Validator->isTRUE( 'date_stamp', false, TTi18n::gettext( 'Incorrect date' ) . '(b)' ); } } // Branch if ( $this->getBranch() !== false && $this->getBranch() != TTUUID::getZeroID() ) { $blf = TTnew( 'BranchListFactory' ); /** @var BranchListFactory $blf */ $this->Validator->isResultSetWithRows( 'branch', $blf->getByID( $this->getBranch() ), TTi18n::gettext( 'Branch does not exist' ) ); } // Department if ( $this->getDepartment() !== false && $this->getDepartment() != TTUUID::getZeroID() ) { $dlf = TTnew( 'DepartmentListFactory' ); /** @var DepartmentListFactory $dlf */ $this->Validator->isResultSetWithRows( 'department', $dlf->getByID( $this->getDepartment() ), TTi18n::gettext( 'Department does not exist' ) ); } if ( getTTProductEdition() >= TT_PRODUCT_CORPORATE ) { // Job if ( $this->getJob() !== false && $this->getJob() != TTUUID::getZeroID() ) { $jlf = TTnew( 'JobListFactory' ); /** @var JobListFactory $jlf */ $this->Validator->isResultSetWithRows( 'job', $jlf->getByID( $this->getJob() ), TTi18n::gettext( 'Job does not exist' ) ); } // Job Item if ( $this->getJobItem() !== false && $this->getJobItem() != TTUUID::getZeroID() ) { $jilf = TTnew( 'JobItemListFactory' ); /** @var JobItemListFactory $jilf */ $this->Validator->isResultSetWithRows( 'job_item', $jilf->getByID( $this->getJobItem() ), TTi18n::gettext( 'Job Item does not exist' ) ); } // Punch Tag if ( $this->getPunchTag() !== false && $this->getPunchTag() != '' && $this->getPunchTag() != TTUUID::getZeroID() ) { $ptlf = TTnew( 'PunchTagListFactory' ); /** @var PunchTagListFactory $ptlf */ if ( is_array( $this->getPunchTag() ) ) { foreach ( $this->getPunchTag() as $punch_tag ) { $this->Validator->isResultSetWithRows( 'punch_tag_id', $ptlf->getByID( $punch_tag ), TTi18n::gettext( 'Invalid Punch Tag' ) ); } } else { $this->Validator->isResultSetWithRows( 'punch_tag_id', $ptlf->getByID( $this->getPunchTag() ), TTi18n::gettext( 'Invalid Punch Tag' ) ); } } // Quantity if ( $this->getQuantity() != '' ) { $this->Validator->isFloat( 'quantity', $this->getQuantity(), TTi18n::gettext( 'Incorrect quantity' ) ); } // Bad quantity if ( $this->getBadQuantity() != '' ) { $this->Validator->isFloat( 'bad_quantity', $this->getBadQuantity(), TTi18n::gettext( 'Incorrect bad quantity' ) ); } } // Total time if ( $this->getTotalTime() !== false ) { $this->Validator->isNumeric( 'total_time', $this->getTotalTime(), TTi18n::gettext( 'Incorrect total time' ) ); } // Actual total time if ( $this->getActualTotalTime() !== false ) { $this->Validator->isNumeric( 'actual_total_time', $this->getActualTotalTime(), TTi18n::gettext( 'Incorrect actual total time' ) ); } // Note if ( $this->getNote() != '' ) { $this->Validator->isLength( 'note', $this->getNote(), TTi18n::gettext( 'Note is too long' ), 0, 1024 ); } // // ABOVE: Validation code moved from set*() functions. // //See if the user_id changed, if so prevent it from being saved, as the user_id should never be changed on a punch_control record as it will cause problems with recalculating. if ( $this->getGenericOldDataValue( 'user_id' ) != false && $this->getUser() != $this->getGenericOldDataValue( 'user_id' ) ) { $this->Validator->isTRUE( 'user_id', false, TTi18n::gettext( 'Punch cannot be assigned to a different employee once created' ) ); } //Call this here so getShiftData can get the correct total time, before we call findUserDate. // **NOTE: this is done in preValidate() as well, it shouldn't need to be done in both places. //if ( $this->getEnableCalcTotalTime() == true ) { // $this->calcTotalTime(); //} // //if ( is_object( $this->getPunchObject() ) ) { // $this->findUserDate(); //} Debug::text( 'DateStamp: ' . $this->getDateStamp(), __FILE__, __LINE__, __METHOD__, 10 ); //Don't check for a valid pay period here, do that in PunchFactory->Validate(), as we need to allow users to delete punches that were created outside pay periods in legacy versions. if ( $this->getDeleted() == false && $this->getDateStamp() == false ) { $this->Validator->isTRUE( 'date_stamp', false, TTi18n::gettext( 'Date/Time is incorrect, or pay period does not exist for this date. Please create a pay period schedule and assign this employee to it if you have not done so already' ) ); } else if ( $this->getDateStamp() != false && is_object( $this->getPayPeriodObject() ) && $this->getPayPeriodObject()->getIsLocked() == true ) { $this->Validator->isTRUE( 'date_stamp', false, TTi18n::gettext( 'Pay Period is Currently Locked' ) ); } //Make sure the user isn't entering punches before the employees hire or after termination date, as its likely they wouldn't have a wage //set for that anyways and wouldn't get paid for it. //We must allow deleting punches after their termination date so timesheets can be cleaned up if necessary. if ( ( $this->getDeleted() == false && ( is_object( $this->getPunchObject() ) && $this->getPunchObject()->getDeleted() == false ) ) && $this->getDateStamp() != false && is_object( $this->getUserObject() ) ) { if ( $this->getUserObject()->getHireDate() != '' && TTDate::getBeginDayEpoch( $this->getDateStamp() ) < TTDate::getBeginDayEpoch( $this->getUserObject()->getHireDate() ) ) { $this->Validator->isTRUE( 'date_stamp', false, TTi18n::gettext( 'Punch is before employees hire date' ) ); } if ( $this->getUserObject()->getTerminationDate() != '' && TTDate::getEndDayEpoch( $this->getDateStamp() ) > TTDate::getEndDayEpoch( $this->getUserObject()->getTerminationDate() ) ) { $this->Validator->isTRUE( 'date_stamp', false, TTi18n::gettext( 'Punch is after employees termination date' ) ); } else if ( $this->getUserObject()->getStatus() != 10 && $this->getUserObject()->getTerminationDate() == '' ) { $this->Validator->isTRUE( 'user_id', false, TTi18n::gettext( 'Employee is not currently active' ) ); } } //Skip these checks if they are deleting a punch. if ( is_object( $this->getPunchObject() ) && $this->getPunchObject()->getDeleted() == false ) { $plf = $this->getPLFByPunchControlID(); if ( $plf !== null && ( ( $this->isNew() && $plf->getRecordCount() == 2 ) || $plf->getRecordCount() > 2 ) ) { //TTi18n::gettext('Punch Control can not have more than two punches. Please use the Add Punch button instead') //They might be trying to insert a punch inbetween two others? $this->Validator->isTRUE( 'punch_control', false, TTi18n::gettext( 'Time conflicts with another punch on this day (c)' ) ); } //Sometimes shift data won't return all the punches to proper check for conflicting punches. //So we need to make sure other punches assigned to this punch_control record are proper. //This fixes the bug of having shifts: 2:00AM Lunch Out, 2:30AM Lunch In, 6:00AM Out, 10:00PM In (in that order), then trying to move the 10PM punch to the open IN slot before the 2AM punch. if ( $plf->getRecordCount() > 0 ) { foreach ( $plf as $p_obj ) { if ( $p_obj->getID() != $this->getPunchObject()->getID() ) { if ( $this->getPunchObject()->getStatus() == 10 && $p_obj->getStatus() == 20 && $this->getPunchObject()->getTimeStamp() > $p_obj->getTimeStamp() ) { //Make sure we match on status==10 for both sides, otherwise this fails to catch the problem case. // Also test $p_obj->getStatus() == 20, to catch cases where a Break In punch is followed by a Lunch Out punch, but the Break In timestamp is AFTER the Lunch Out timestamp. $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'In punches cannot occur after an out punch, in the same punch pair (a)' ) ); } else if ( $this->getPunchObject()->getStatus() == 20 && $p_obj->getStatus() == 10 && $this->getPunchObject()->getTimeStamp() < $p_obj->getTimeStamp() ) { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'Out punches cannot occur before an in punch, in the same punch pair (a)' ) ); } } } } unset( $p_obj ); if ( $this->Validator->isValid() == true ) { //Don't bother checking these resource intensive issues if there are already validation errors. $shift_data = $this->getShiftData(); if ( is_array( $shift_data ) && $this->Validator->hasError( 'time_stamp' ) == false ) { foreach ( $shift_data['punches'] as $punch_data ) { //Make sure there aren't two In punches, or two Out punches in the same pair. //This fixes the bug where if you have an In punch, then click the blank cell below it //to add a new punch, but change the status from Out to In instead. if ( isset( $punches[$punch_data['punch_control_id']][$punch_data['status_id']] ) ) { if ( $punch_data['status_id'] == 10 ) { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'In punches cannot occur twice in the same punch pair, you may want to make this an out punch instead' ) . '(b)' ); } else { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'Out punches cannot occur twice in the same punch pair, you may want to make this an in punch instead' ) . '(b)' ); } } //Debug::text(' Current Punch Object: ID: '. $this->getPunchObject()->getId() .' TimeStamp: '. $this->getPunchObject()->getTimeStamp() .' Status: '. $this->getPunchObject()->getStatus(), __FILE__, __LINE__, __METHOD__, 10); //Debug::text(' Looping Punch Object: ID: '. $punch_data['id'] .' TimeStamp: '. $punch_data['time_stamp'] .' Status: '.$punch_data['status_id'], __FILE__, __LINE__, __METHOD__, 10); //Check for another punch that matches the timestamp and status. if ( $this->getPunchObject()->getID() != $punch_data['id'] ) { if ( $this->getPunchObject()->getTimeStamp() == $punch_data['time_stamp'] && $this->getPunchObject()->getStatus() == $punch_data['status_id'] ) { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'Time and status match that of another punch, this could be due to rounding' ) . ' (' . TTDate::getDate( 'DATE+TIME', $punch_data['time_stamp'] ) . ')' ); break; //Break the loop on validation error, so we don't get multiple errors that may be confusing. } } //Check for another punch that matches the timestamp and NOT status in the SAME punch pair. if ( $this->getPunchObject()->getID() != $punch_data['id'] && $this->getID() == $punch_data['punch_control_id'] ) { if ( $this->getPunchObject()->getTimeStamp() == $punch_data['time_stamp'] && $this->getPunchObject()->getStatus() != $punch_data['status_id'] ) { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'Time matches another punch in the same punch pair, this could be due to rounding' ) . ' (' . TTDate::getDate( 'DATE+TIME', $punch_data['time_stamp'] ) . ')' ); break; //Break the loop on validation error, so we don't get multiple errors that may be confusing. } } $punches[$punch_data['punch_control_id']][$punch_data['status_id']] = $punch_data; } unset( $punch_data ); if ( isset( $punches[$this->getID()] ) ) { Debug::text( 'Current Punch ID: ' . $this->getPunchObject()->getId() . ' Punch Control ID: ' . $this->getID() . ' Status: ' . $this->getPunchObject()->getStatus(), __FILE__, __LINE__, __METHOD__, 10 ); //Debug::Arr($punches, 'Punches Arr: ', __FILE__, __LINE__, __METHOD__, 10); if ( $this->getPunchObject()->getStatus() == 10 && isset( $punches[$this->getID()][20] ) && $this->getPunchObject()->getTimeStamp() > $punches[$this->getID()][20]['time_stamp'] ) { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'In punches cannot occur after an out punch, in the same punch pair' ) ); } else if ( $this->getPunchObject()->getStatus() == 20 && isset( $punches[$this->getID()][10] ) && $this->getPunchObject()->getTimeStamp() < $punches[$this->getID()][10]['time_stamp'] ) { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'Out punches cannot occur before an in punch, in the same punch pair' ) ); } else { Debug::text( 'bPunch does not match any other punch pair.', __FILE__, __LINE__, __METHOD__, 10 ); $punch_neighbors = Misc::getArrayNeighbors( $punches, $this->getID(), 'both' ); //Debug::Arr($punch_neighbors, ' Punch Neighbors: ', __FILE__, __LINE__, __METHOD__, 10); if ( isset( $punch_neighbors['next'] ) && isset( $punches[$punch_neighbors['next']] ) ) { Debug::text( 'Found Next Punch...', __FILE__, __LINE__, __METHOD__, 10 ); if ( ( isset( $punches[$punch_neighbors['next']][10] ) && $this->getPunchObject()->getTimeStamp() > $punches[$punch_neighbors['next']][10]['time_stamp'] ) || ( isset( $punches[$punch_neighbors['next']][20] ) && $this->getPunchObject()->getTimeStamp() > $punches[$punch_neighbors['next']][20]['time_stamp'] ) ) { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'Time conflicts with another punch on this day' ) . ' (a)' ); } } if ( isset( $punch_neighbors['prev'] ) && isset( $punches[$punch_neighbors['prev']] ) ) { Debug::text( 'Found prev Punch...', __FILE__, __LINE__, __METHOD__, 10 ); //This needs to take into account DST. Specifically if punches are like this: //03-Nov-12: IN: 10:00PM //04-Nov-12: OUT: 1:00AM L //04-Nov-12: IN: 1:30AM L //04-Nov-12: OUT: 6:30AM L //Since the 1AM to 2AM occur twice due to the "fall back" DST change, we need to allow those punches to be entered. if ( ( isset( $punches[$punch_neighbors['prev']][10] ) && ( $this->getPunchObject()->getTimeStamp() < $punches[$punch_neighbors['prev']][10]['time_stamp'] && TTDate::doesRangeSpanDST( $this->getPunchObject()->getTimeStamp(), $punches[$punch_neighbors['prev']][10]['time_stamp'] ) == false ) ) || ( isset( $punches[$punch_neighbors['prev']][20] ) && ( $this->getPunchObject()->getTimeStamp() < $punches[$punch_neighbors['prev']][20]['time_stamp'] && TTDate::doesRangeSpanDST( $this->getPunchObject()->getTimeStamp(), $punches[$punch_neighbors['prev']][20]['time_stamp'] ) == false ) ) ) { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'Time conflicts with another punch on this day' ) . ' (b)' ); } } } //Check to make sure punches don't exceed maximum shift time. $maximum_shift_time = $plf->getPayPeriodMaximumShiftTime( $this->getPunchObject()->getUser() ); Debug::text( 'Maximum shift time: ' . $maximum_shift_time, __FILE__, __LINE__, __METHOD__, 10 ); if ( $shift_data['total_time'] > $maximum_shift_time ) { $this->Validator->isTRUE( 'time_stamp', false, TTi18n::gettext( 'Punch exceeds maximum shift time of' ) . ' ' . TTDate::getTimeUnit( $maximum_shift_time ) . ' ' . TTi18n::getText( 'hrs set for this pay period schedule' ) ); } } unset( $punches ); } } } if ( getTTProductEdition() >= TT_PRODUCT_CORPORATE && $this->getEnableStrictJobValidation() == true ) { if ( TTUUID::isUUID( $this->getJob() ) && $this->getJob() != TTUUID::getZeroID() && $this->getJob() != TTUUID::getNotExistID() ) { $jlf = TTnew( 'JobListFactory' ); /** @var JobListFactory $jlf */ $jlf->getById( $this->getJob() ); if ( $jlf->getRecordCount() > 0 ) { $j_obj = $jlf->getCurrent(); if ( $this->getDateStamp() != false && $j_obj->isAllowedUser( $this->getUser(), $this->getBranch(), $this->getDepartment() ) == false ) { $this->Validator->isTRUE( 'job', false, TTi18n::gettext( 'Employee is not assigned to this job' ) ); } if ( $j_obj->isAllowedItem( $this->getJobItem() ) == false ) { $this->Validator->isTRUE( 'job_item', false, TTi18n::gettext( 'Task is not assigned to this job' ) ); } } } } if ( $ignore_warning == false ) { //Warn users if they are trying to insert punches too far in the future. if ( $this->getDateStamp() != false && $this->getDateStamp() > ( time() + ( 86400 * 366 ) ) ) { $this->Validator->Warning( 'date_stamp', TTi18n::gettext( 'Date is more than one year in the future' ) ); } //Check to see if timesheet is verified, if so show warning to notify the user. if ( is_object( $this->getPayPeriodScheduleObject() ) && $this->getPayPeriodScheduleObject()->getTimeSheetVerifyType() != 10 ) { //Find out if timesheet is verified or not. $pptsvlf = TTnew( 'PayPeriodTimeSheetVerifyListFactory' ); /** @var PayPeriodTimeSheetVerifyListFactory $pptsvlf */ $pptsvlf->getByPayPeriodIdAndUserId( $this->getPayPeriod(), $this->getUser() ); if ( $pptsvlf->getRecordCount() > 0 ) { //Pay period is verified $this->Validator->Warning( 'date_stamp', TTi18n::gettext( 'Pay period is already verified, saving these changes will require it to be reverified' ) ); } } } $this->validateCustomFields( ( is_object( $this->getUserObject() ) ? $this->getUserObject()->getCompany() : null ), $this->getEnableRequiredFieldCheck() ); return true; } /** * @return bool */ function preValidate() { if ( $this->getBranch() === false ) { $this->setBranch( TTUUID::getZeroID() ); } if ( $this->getDepartment() === false ) { $this->setDepartment( TTUUID::getZeroID() ); } if ( $this->getJob() === false ) { $this->setJob( TTUUID::getZeroID() ); } if ( $this->getJobItem() === false ) { $this->setJobItem( TTUUID::getZeroID() ); } if ( $this->getQuantity() === false ) { $this->setQuantity( 0 ); } if ( $this->getBadQuantity() === false ) { $this->setBadQuantity( 0 ); } if ( $this->getPayPeriod() == false ) { $this->setPayPeriod(); } if ( in_array( TTUUID::getNotExistID( 2 ), [ $this->getBranch(), $this->getDepartment(), $this->getJob(), $this->getDepartment() ], true ) ) { Debug::text( ' Branch/Dept/Job/Task is set to match that of the previous punch...', __FILE__, __LINE__, __METHOD__, 10 ); $previous_punch_obj = $this->getPunchObject()->getPreviousPunchObject( $this->getPunchObject()->getActualTimeStamp() ); if ( is_object( $previous_punch_obj ) ) { $previous_punch_control_obj = $previous_punch_obj->getPunchControlObject(); if ( is_object( $previous_punch_obj ) ) { Debug::text( ' Previous punch: Branch: ' . $previous_punch_control_obj->getBranch() . ' Dept: ' . $previous_punch_control_obj->getDepartment() . ' Job: ' . $previous_punch_control_obj->getJob() . ' Task: ' . $previous_punch_control_obj->getJobItem(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $this->getBranch() === TTUUID::getNotExistID( 2 ) ) { $this->setBranch( $previous_punch_control_obj->getBranch() ); } if ( $this->getDepartment() === TTUUID::getNotExistID( 2 ) ) { $this->setDepartment( $previous_punch_control_obj->getDepartment() ); } if ( $this->getJob() === TTUUID::getNotExistID( 2 ) ) { $this->setJob( $previous_punch_control_obj->getJob() ); } if ( $this->getJobItem() === TTUUID::getNotExistID( 2 ) ) { $this->setJobItem( $previous_punch_control_obj->getJobItem() ); } } } //If there is no previous punch, fall back to employee profile defaults. // FIXME: Perhaps fall back to schedule first, then employee profile defaults? if ( $previous_punch_obj == false || $previous_punch_control_obj == false ) { Debug::text( ' Previous punch does not exist, using default values instead...', __FILE__, __LINE__, __METHOD__, 10 ); if ( $this->getBranch() === TTUUID::getNotExistID( 2 ) ) { $this->setBranch( TTUUID::getNotExistID() ); } if ( $this->getDepartment() === TTUUID::getNotExistID( 2 ) ) { $this->setDepartment( TTUUID::getNotExistID() ); } if ( $this->getJob() === TTUUID::getNotExistID( 2 ) ) { $this->setJob( TTUUID::getNotExistID() ); } if ( $this->getJobItem() === TTUUID::getNotExistID( 2 ) ) { $this->setJobItem( TTUUID::getNotExistID() ); } } unset( $previous_punch_obj, $previous_punch_control_obj ); } //Set Job default Job Item if required. if ( getTTProductEdition() >= TT_PRODUCT_CORPORATE && TTUUID::isUUID( $this->getJob() ) && $this->getJob() != TTUUID::getZeroID() && ( $this->getJobItem() == TTUUID::getZeroID() || $this->getJobItem() == '' ) ) { Debug::text( ' Job is set (' . $this->getJob() . '), but no task is... Using default job item...', __FILE__, __LINE__, __METHOD__, 10 ); if ( is_object( $this->getJobObject() ) ) { Debug::text( ' Default Job Item: ' . $this->getJobObject()->getDefaultItem(), __FILE__, __LINE__, __METHOD__, 10 ); $this->setJobItem( $this->getJobObject()->getDefaultItem() ); } } //Call this here so getShiftData can get the correct total time, before we call findUserDate. if ( $this->getEnableCalcTotalTime() == true ) { $this->calcTotalTime(); } //Make sure this goes above findUserDate(), as it calls setUserDate() through setPunchObject() and can change the date the punches are assigned too if its run after findUserDate(). if ( $this->getDeleted() == false ) { $this->changePreviousPunchType(); } if ( is_object( $this->getPunchObject() ) ) { $this->findUserDate(); } return true; } /** * @return bool */ function calcUserDate() { if ( $this->getEnableCalcUserDateID() == true ) { $date_stamp = TTDate::getMiddleDayEpoch( $this->getDateStamp() ); //preSave should already be called before running this function. Debug::Text( ' Calculating User ID: ' . $this->getUser() . ' DateStamp: ' . $this->getDateStamp(), __FILE__, __LINE__, __METHOD__, 10 ); $shift_data = $this->getShiftData(); if ( is_array( $shift_data ) ) { //Don't re-arrange shifts until all punches are paired and we have enough information. //Thats what the count() % 2 is used for. if ( $this->getUser() != false && isset( $date_stamp ) && $date_stamp > 0 && ( isset( $shift_data['punch_control_ids'] ) && is_array( $shift_data['punch_control_ids'] ) ) && ( isset( $shift_data['punches'] ) && count( $shift_data['punches'] ) % 2 == 0 ) ) { Debug::Text( 'Assigning all punch_control_ids to User ID: ' . $this->getUser() . ' DateStamp: ' . $date_stamp, __FILE__, __LINE__, __METHOD__, 10 ); $this->old_date_stamps[] = $date_stamp; if ( $this->getOldDateStamp() != false ) { $this->old_date_stamps[] = $this->getOldDateStamp(); } $processed_punch_control_ids = []; foreach ( $shift_data['punch_control_ids'] as $punch_control_id ) { $pclf = TTnew( 'PunchControlListFactory' ); /** @var PunchControlListFactory $pclf */ $pclf->getById( $punch_control_id ); if ( $pclf->getRecordCount() == 1 ) { $processed_punch_control_ids[] = $punch_control_id; $pc_obj = $pclf->getCurrent(); if ( TTDate::getMiddleDayEpoch( $pc_obj->getDateStamp() ) != $date_stamp ) { Debug::Text( ' Saving Punch Control ID: ' . $punch_control_id . ' with new DateStamp: ' . $date_stamp . ' Old DateStamp: ' . $pc_obj->getDateStamp(), __FILE__, __LINE__, __METHOD__, 10 ); $this->old_date_stamps[] = $pc_obj->getDateStamp(); $pc_obj->setDateStamp( $date_stamp ); $pc_obj->setEnableStrictJobValidation( false ); //Make sure we relax as many validation criteria as possible when making this change since its often called from PunchControlFactory->postSave() and we can't show the errors to the user. $pc_obj->setEnableCalcUserDateTotal( true ); $pc_obj->setEnableCalcTotalTime( true ); //This is required to make sure Start/End timestamps are populated. This help fix strange bugs with OT being calculated incorrectly due to missing timestamps. if ( $pc_obj->isValid() == true ) { $pc_obj->Save(); } } //else { // Debug::Text( ' NOT Saving Punch Control ID, as DateStamp didnt change: ' . $punch_control_id, __FILE__, __LINE__, __METHOD__, 10 ); //} } } unset( $pclf, $pc_obj ); //Debug::Arr($this->old_date_stamps, 'aOld User Date IDs: ', __FILE__, __LINE__, __METHOD__, 10); //Handle cases where shift times change enough to cause shifts spanning midnight to be reassigned to different days. //For example the punches may look like this: // Nov 12th 1:00PM // Nov 12th 11:30PM // Nov 13th 12:30AM // Nov 13th 2:00AM //Then the Nov12th 11:30PM punch is modified to be say 2PM, the Nov 13th 12:30AM punch should then be moved to 13th rather than combined with the 12th. if ( count( $processed_punch_control_ids ) > 0 ) { $plf = TTNew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->getByUserIdAndDateStampAndNotPunchControlId( $this->getUser(), $date_stamp, $processed_punch_control_ids ); if ( $plf->getRecordCount() > 0 ) { foreach ( $plf as $p_obj ) { if ( !in_array( $p_obj->getPunchControlID(), $processed_punch_control_ids ) ) { Debug::Text( 'aPunches from other shifts exist on this day still... Punch ID: ' . $p_obj->getID(), __FILE__, __LINE__, __METHOD__, 10 ); $src_punch_control_obj = $p_obj->getPunchControlObject(); if ( is_object( $src_punch_control_obj ) ) { $src_punch_control_obj->setPunchObject( $p_obj ); if ( $src_punch_control_obj->isValid() == true ) { //We need to calculate new total time for the day and exceptions because we are never guaranteed that the gaps will be filled immediately after //in the case of a drag & drop or something. $src_punch_control_obj->setEnableStrictJobValidation( false ); //Make sure we relax as many validation criteria as possible when making this change since its often called from PunchControlFactory->postSave() and we can't show the errors to the user. $src_punch_control_obj->setEnableCalcUserDateID( false ); $src_punch_control_obj->setEnableCalcTotalTime( true ); $src_punch_control_obj->setEnableCalcSystemTotalTime( true ); $src_punch_control_obj->setEnableCalcWeeklySystemTotalTime( true ); $src_punch_control_obj->setEnableCalcUserDateTotal( true ); $src_punch_control_obj->setEnableCalcException( true ); if ( $src_punch_control_obj->isValid() == true ) { $src_punch_control_obj->Save(); $processed_punch_control_ids[] = $src_punch_control_obj->getID(); } } } else { Debug::Text( 'ERROR: Unable to get punch control object! Punch Control ID: ' . $p_obj->getPunchControlId(), __FILE__, __LINE__, __METHOD__, 10 ); } } } } unset( $plf, $src_punch_control_obj, $p_obj ); } Debug::Text( 'Returning TRUE', __FILE__, __LINE__, __METHOD__, 10 ); return true; } else { Debug::Text( 'Punches are not paired, not re-arranging days...', __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::Text( 'No shift data, check for other punches on the same day in case they need to be moved back...', __FILE__, __LINE__, __METHOD__, 10 ); //Handle cases where a punch pair was moved from one day to this day, then the punches that caused that were deleted, and now //it needs to be moved back to the original day. $plf = TTNew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->getByUserIdAndDateStamp( $this->getUser(), $date_stamp ); if ( $plf->getRecordCount() > 0 ) { foreach ( $plf as $p_obj ) { if ( $p_obj->getPunchControlId() == $this->getId() ) { //Skip any punches assigned to our current punch_control_id, as we assume it has already been calculated. continue; } Debug::Text( 'bPunches from other shifts exist on this day still... Punch ID: ' . $p_obj->getID(), __FILE__, __LINE__, __METHOD__, 10 ); $src_punch_control_obj = $p_obj->getPunchControlObject(); $src_punch_control_obj->setPunchObject( $p_obj ); if ( $src_punch_control_obj->isValid() == true ) { //We need to calculate new total time for the day and exceptions because we are never guaranteed that the gaps will be filled immediately after //in the case of a drag & drop or something. $src_punch_control_obj->setEnableStrictJobValidation( false ); //Make sure we relax as many validation criteria as possible when making this change since its often called from PunchControlFactory->postSave() and we can't show the errors to the user. $src_punch_control_obj->setEnableCalcUserDateID( false ); $src_punch_control_obj->setEnableCalcTotalTime( true ); $src_punch_control_obj->setEnableCalcSystemTotalTime( true ); $src_punch_control_obj->setEnableCalcWeeklySystemTotalTime( true ); $src_punch_control_obj->setEnableCalcUserDateTotal( true ); $src_punch_control_obj->setEnableCalcException( true ); if ( $src_punch_control_obj->isValid() == true ) { $src_punch_control_obj->Save(); $processed_punch_control_ids[] = $src_punch_control_obj->getID(); } } } } unset( $plf, $src_punch_control_obj, $p_obj ); return true; } } Debug::Text( 'Returning FALSE', __FILE__, __LINE__, __METHOD__, 10 ); return false; } /** * @return bool */ function calcUserDateTotal() { if ( $this->getEnableCalcUserDateTotal() == true ) { Debug::Text( ' Calculating User Date Total...', __FILE__, __LINE__, __METHOD__, 10 ); $udtlf = TTnew( 'UserDateTotalListFactory' ); /** @var UserDateTotalListFactory $udtlf */ //Always include OldDateStamp, as punches can move between days (timezone differences), and we need to always update proper records based on punch_control_id. $udtlf->getByUserIdAndDateStampAndOldDateStampAndPunchControlId( $this->getUser(), $this->getDateStamp(), $this->getOldDateStamp(), $this->getId() ); Debug::text( ' Checking for Conflicting User Date Total Records, count: ' . $udtlf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 ); if ( $this->getDeleted() == true ) { //Add a row to the user date total table, as "worked" hours. //Edit if it already exists and is not set as override. if ( $udtlf->getRecordCount() > 0 ) { foreach ( $udtlf as $udt_obj ) { if ( $udt_obj->getOverride() == false ) { Debug::text( ' Found Conflicting User Date Total Record, removing it before re-calc: ' . $udt_obj->getId(), __FILE__, __LINE__, __METHOD__, 10 ); $udt_obj->Delete( true ); } } } } else { if ( $udtlf->getRecordCount() > 0 ) { //Delete all but the first row, in case there happens to be multiple rows with the same punch_control_id? $found_first_record = false; foreach ( $udtlf as $udt_obj ) { //Only keep the first record for the current date stamp. Delete all other records, or records on other dates. //This is required due to getting records from OldDateStamp, as commented on above. if ( $found_first_record == false && TTDate::getMiddleDayEpoch( $udt_obj->getDateStamp() ) == TTDate::getMiddleDayEpoch( $this->getDateStamp() ) ) { $udtf = $udt_obj; $found_first_record = true; continue; } if ( $udt_obj->getOverride() == false ) { Debug::text( ' Found Conflicting User Date Total Records, removing it before re-calc: ID: ' . $udt_obj->getID() . ' Date: ' . TTDate::getDate( 'DATE', $udt_obj->getDateStamp() ), __FILE__, __LINE__, __METHOD__, 10 ); $udt_obj->Delete( true ); } else { Debug::text( ' Found overridden User Date Total Records, not removing...', __FILE__, __LINE__, __METHOD__, 10 ); } } } if ( !isset( $udtf ) ) { Debug::text( ' No Conflicting User Date Total Records, inserting the first one.', __FILE__, __LINE__, __METHOD__, 10 ); $udtf = TTnew( 'UserDateTotalFactory' ); /** @var UserDateTotalFactory $udtf */ } else { Debug::text( ' Updating UserDateTotal row ID: \'' . TTUUID::castUUID( $udtf->getId() ) . '\'', __FILE__, __LINE__, __METHOD__, 10 ); } $udtf->setUser( $this->getUser() ); $udtf->setDateStamp( $this->getDateStamp() ); $udtf->setPunchControlID( $this->getId() ); $udtf->setObjectType( 10 ); //Worked $udtf->setBranch( $this->getBranch() ); $udtf->setDepartment( $this->getDepartment() ); $udtf->setJob( $this->getJob() ); $udtf->setJobItem( $this->getJobItem() ); $udtf->setPunchTag( $this->getPunchTag() ); $udtf->setQuantity( $this->getQuantity() ); $udtf->setBadQuantity( $this->getBadQuantity() ); $udtf->setTotalTime( $this->getTotalTime() ); $udtf->setActualTotalTime( $this->getActualTotalTime() ); //We always need to make sure both Start/End timestamps are set, we can't necessarily get this //from just getPunchObject(), we have to get it from calcTotalTime() instead. if ( is_object( $this->in_punch_obj ) ) { $udtf->setStartType( $this->in_punch_obj->getType() ); $udtf->setStartTimeStamp( $this->in_punch_obj->getTimeStamp() ); } else { Debug::text( 'No IN PunchObject!', __FILE__, __LINE__, __METHOD__, 10 ); if ( is_object( $this->getPunchObject() ) && $this->getPunchObject()->getStatus() == 10 ) { Debug::text( ' Using passed PunchObject instead... Deleted: ' . $this->getPunchObject()->getDeleted(), __FILE__, __LINE__, __METHOD__, 10 ); //Make sure when deleting a punch we clear out the timestamp from the UDT record. if ( $this->getPunchObject()->getDeleted() == true ) { $udtf->setStartType( null ); $udtf->setStartTimeStamp( null ); } else { $udtf->setStartType( $this->getPunchObject()->getType() ); $udtf->setStartTimeStamp( $this->getPunchObject()->getTimeStamp() ); } } else { Debug::text( ' NOTICE: No PunchObject!', __FILE__, __LINE__, __METHOD__, 10 ); } } if ( is_object( $this->out_punch_obj ) ) { $udtf->setEndType( $this->out_punch_obj->getType() ); $udtf->setEndTimeStamp( $this->out_punch_obj->getTimeStamp() ); } else { Debug::text( 'No OUT PunchObject!', __FILE__, __LINE__, __METHOD__, 10 ); if ( is_object( $this->getPunchObject() ) && $this->getPunchObject()->getStatus() == 20 ) { Debug::text( ' Using passed PunchObject instead... Deleted: ' . $this->getPunchObject()->getDeleted(), __FILE__, __LINE__, __METHOD__, 10 ); //Make sure when deleting a punch we clear out the timestamp from the UDT record. if ( $this->getPunchObject()->getDeleted() == true ) { $udtf->setEndType( null ); $udtf->setEndTimeStamp( null ); } else { $udtf->setEndType( $this->getPunchObject()->getType() ); $udtf->setEndTimeStamp( $this->getPunchObject()->getTimeStamp() ); } } else { Debug::text( ' NOTICE: No PunchObject!', __FILE__, __LINE__, __METHOD__, 10 ); } } //Let smartReCalculate handle calculating totals/exceptions. if ( $udtf->isValid() ) { return $udtf->Save(); } else { Debug::text( 'ERROR: Validation error saving UDT row!', __FILE__, __LINE__, __METHOD__, 10 ); } } } return false; } /** * This function handles when th UI wants to drag and drop punches around the time sheet. * @param string $company_id UUID * @param string $src_punch_id UUID * @param string $dst_punch_id UUID * @param int $dst_status_id ID 10 (In), 20 (Out), this is the status of the row the punch is being dragged too, or the resulting status_id in *most* (not all) cases. It is really only needed when using the overwrite position setting, and dragging a punch to a blank cell. Other than that it can be left NULL. * @param int $position -1 (Before), 0 (Overwrite), 1 (After) * @param int $action 0 (Copy), 1 (Move) * @param int $dst_date EPOCH * @return bool */ function dragNdropPunch( $company_id, $src_punch_id, $dst_punch_id, $dst_status_id = null, $position = 0, $action = 0, $dst_date = null ) { /* FIXME: This needs to handle batches to be able to handle all the differnet corner cases. Operations to handle: - Moving punch from Out to In, or In to Out in same punch pair, this is ALWAYS a move, and not a copy. - Move punch from one pair to another in the same day, this can be a copy or move. - Check moving AND copying Out punch from one punch pair to In in another on the same day. ie: In 8:00AM, Out 1:00PM, Out 5:00PM. Move the 1PM punch to pair with 5PM. - Move punch from one day to another, inserting inbetween other punches if necessary. - Move punch from one day to another without any other punches. - Inserting BEFORE on a dst_punch_id that is an In punch doesn't do any splitting. - Inserting AFTER on a dst_punch_id that is on a Out punch doesn't do any splitting. - Overwriting should just take the punch time and overwrite the existing punch time. - The first thing this function does it check if there are two punches assigned to the punch control of the destination punch, if there is, it splits the punches across two punch_controls, it then attaches the src_punch_id to the same punch_control_id as the dst_punch_id. - If no dst_punch_id is specified, assume copying to a blank cell, just copy the punch to that date along with the punch_control? - Copying punches that span midnight work, however moving punches does not always since we don't move punches in batches, we do it one at a time, and when the first punch punch gets moved, it can cause other punches to follow it automatically. */ $dst_date = TTDate::getMiddleDayEpoch( $dst_date ); Debug::text( 'Src Punch ID: ' . $src_punch_id . ' Dst Punch ID: ' . $dst_punch_id . ' Dst Status ID: ' . $dst_status_id . ' Position: ' . $position . ' Action: ' . $action . ' Dst Date: ' . TTDate::getDATE( 'DATE+TIME', $dst_date ), __FILE__, __LINE__, __METHOD__, 10 ); $retval = false; $transaction_function = function () use ( $company_id, $src_punch_id, $dst_punch_id, $dst_status_id, $position, $action, $dst_date, $retval ) { //Get source and destination punch objects. $plf = TTnew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->setTransactionMode( 'REPEATABLE READ' ); //Required to help prevent duplicate simulataneous HTTP requests from causing duplicate user records or duplicate employee number/user_names. $plf->StartTransaction(); $plf->getByCompanyIDAndId( $company_id, $src_punch_id ); if ( $plf->getRecordCount() == 1 ) { $src_punch_obj = $plf->getCurrent(); $src_time_stamp = $src_punch_obj->getTimeStamp(); //Save this so we can add it to the audit log. $src_status_name = ( $src_punch_obj->getStatus() == 10 ? TTi18n::getText( 'In' ) : TTi18n::getText( 'Out' ) ); //Get the PunchControlObject as early as possible, before the punch is deleted, as it will be cleared even if Save(FALSE) is called below. $src_punch_control_obj = clone $src_punch_obj->getPunchControlObject(); if ( is_object( $src_punch_control_obj ) ) { $src_punch_date = TTDate::getMiddleDayEpoch( $src_punch_control_obj->getDateStamp() ); Debug::text( 'Found SRC punch ID: ' . $src_punch_id . ' Source Punch Date: ' . $src_punch_date, __FILE__, __LINE__, __METHOD__, 10 ); //Make sure the src punch date also does not match the dst_date. This can happen when a punch is dated 24-May but assigned to 25-May due to that being the day with most time worked, and the user is trying to drag it back to 24-May if ( TTDate::getMiddleDayEpoch( $src_punch_date ) != TTDate::getMiddleDayEpoch( $src_punch_obj->getTimeStamp() ) && TTDate::getMiddleDayEpoch( $dst_date ) != TTDate::getMiddleDayEpoch( $src_punch_obj->getTimeStamp() ) ) { Debug::text( 'Punch spans midnight... Source Punch Date: ' . TTDate::getDATE( 'DATE+TIME', $src_punch_date ) . ' Source Punch TimeStamp: ' . TTDate::getDATE( 'DATE+TIME', $src_punch_obj->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10 ); $dst_date_modifier = 86400; //Bump day by 24hrs. } else { $dst_date_modifier = 0; } //If we are moving the punch, we need to delete the source punch first so it doesn't conflict with the new punch. //Especially if we are just moving a punch to fill a gap in the same day. //If the punch being moved is in the same day, or within the same punch pair, we don't want to delete the source punch, instead we just modify //the necessary bits later on. So we need to short circuit the move functionality when copying/moving punches within the same day. if ( ( $action == 1 && $src_punch_id != $dst_punch_id && $src_punch_date != $dst_date ) || ( $action == 1 && $src_punch_id != $dst_punch_id && $src_punch_date == $dst_date ) //OR //( $action == 0 AND $src_punch_id != $dst_punch_id AND $src_punch_date == $dst_date ) //Since we have dst_status_id, we don't need to force-move punches even though the user selected copy. ) { //Move Debug::text( 'Deleting original punch ID: ' . $src_punch_id . ' User Date: ' . TTDate::getDate( 'DATE', $src_punch_control_obj->getDateStamp() ) . ' ID: ' . $src_punch_control_obj->getID(), __FILE__, __LINE__, __METHOD__, 10 ); $src_punch_obj->setUser( $src_punch_control_obj->getUser() ); $src_punch_obj->setDeleted( true ); $punch_image_data = $src_punch_obj->getImage(); //These aren't doing anything because they aren't acting on the PunchControl object? $src_punch_obj->setEnableCalcTotalTime( true ); $src_punch_obj->setEnableCalcSystemTotalTime( true ); $src_punch_obj->setEnableCalcWeeklySystemTotalTime( true ); $src_punch_obj->setEnableCalcUserDateTotal( true ); $src_punch_obj->setEnableCalcException( true ); $src_punch_obj->Save( false ); //Keep object around for later. } else { Debug::text( 'NOT Deleting original punch, either in copy mode or condition is not met...', __FILE__, __LINE__, __METHOD__, 10 ); } if ( $src_punch_id == $dst_punch_id || $dst_punch_id == '' ) { //Assume we are just moving a punch within the same punch pair, unless a new date is specfied. //However if we're simply splitting an existing punch pair, like dragging the Out punch from an In/Out pair into its own separate pair. if ( $src_punch_date != $dst_date || $src_punch_date == $dst_date && $dst_punch_id == '' ) { Debug::text( 'aCopying punch to new day...', __FILE__, __LINE__, __METHOD__, 10 ); //Moving punch to a new date. //Copy source punch to proper location by destination punch. $src_punch_obj->setId( false ); $src_punch_obj->setPunchControlId( $src_punch_control_obj->getNextInsertId() ); $src_punch_obj->setDeleted( false ); //Just in case it was marked deleted by the MOVE action. $new_time_stamp = TTDate::getTimeLockedDate( $src_punch_obj->getTimeStamp(), ( $dst_date + $dst_date_modifier ) ); Debug::text( 'SRC TimeStamp: ' . TTDate::getDate( 'DATE+TIME', $src_punch_obj->getTimeStamp() ) . ' DST TimeStamp: ' . TTDate::getDate( 'DATE+TIME', $new_time_stamp ), __FILE__, __LINE__, __METHOD__, 10 ); $src_punch_obj->setTimeStamp( $new_time_stamp, false ); $src_punch_obj->setActualTimeStamp( $new_time_stamp ); $src_punch_obj->setOriginalTimeStamp( $new_time_stamp ); if ( $dst_status_id != '' ) { $src_punch_obj->setStatus( $dst_status_id ); //Change the status to fit in the proper place. } //When drag&drop copying punches, clear some fields that shouldn't be copied. if ( $action == 0 ) { //Copy $src_punch_obj->setStation( null ); $src_punch_obj->setHasImage( false ); $src_punch_obj->setLongitude( null ); //Make sure we clear out long/lat as the location shouldn't carry across with copies. $src_punch_obj->setLatitude( null ); //Make sure we clear out long/lat as the location shouldn't carry across with copies. //When copying, make sure we clear the original created information to avoid confusion in the audit log. $src_punch_obj->setCreatedBy( null ); $src_punch_obj->setCreatedDate( null ); } else if ( isset( $punch_image_data ) && $punch_image_data != '' ) { $src_punch_obj->setImage( $punch_image_data ); } //When moving or copying, always touch the updated information. $src_punch_obj->setUpdatedBy( null ); $src_punch_obj->setUpdatedDate( null ); if ( $src_punch_obj->isValid() == true ) { $insert_id = $src_punch_obj->Save( false ); TTLog::addEntry( $src_punch_obj->getID(), 500, TTi18n::getText( 'Drag & Drop' ) . ': ' . TTi18n::getText( 'Action' ) . ': ' . ( $action == 0 ? TTi18n::getText( 'Copy' ) : TTi18n::getText( 'Move' ) ) . ' ' . TTi18n::getText( 'From' ) . ': ' . TTDate::getDate( 'DATE+TIME', $src_time_stamp ) . ' ' . TTi18n::getText( 'Status' ) . ': ' . $src_status_name, null, $src_punch_obj->getTable() ); $src_punch_control_obj->shift_data = null; //Need to clear the shift data so its obtained from the DB again, otherwise shifts will appear on strange days. $src_punch_control_obj->user_date_obj = null; //Need to clear user_date_obj from cache so a new one is obtained. $src_punch_control_obj->setId( $src_punch_obj->getPunchControlID() ); $src_punch_control_obj->setPunchObject( $src_punch_obj ); if ( $src_punch_control_obj->isValid() == true ) { Debug::Text( ' Punch Control is valid, saving...: ', __FILE__, __LINE__, __METHOD__, 10 ); //We need to calculate new total time for the day and exceptions because we are never guaranteed that the gaps will be filled immediately after //in the case of a drag & drop or something. $src_punch_control_obj->setEnableStrictJobValidation( false ); //Make sure we relax as many validation criteria as possible when making this change since its often called from PunchControlFactory->postSave() and we can't show the errors to the user. $src_punch_control_obj->setEnableCalcUserDateID( true ); $src_punch_control_obj->setEnableCalcTotalTime( true ); $src_punch_control_obj->setEnableCalcSystemTotalTime( true ); $src_punch_control_obj->setEnableCalcWeeklySystemTotalTime( true ); $src_punch_control_obj->setEnableCalcUserDateTotal( true ); $src_punch_control_obj->setEnableCalcException( true ); if ( $src_punch_control_obj->isValid() == true ) { if ( $src_punch_control_obj->Save( true, true ) == true ) { //Return newly inserted punch_id, so Flex can base other actions on it. $retval = $insert_id; } } } } } else { Debug::text( 'Copying punch within the same pair/day...', __FILE__, __LINE__, __METHOD__, 10 ); //Moving punch within the same punch pair. $src_punch_obj->setStatus( $src_punch_obj->getNextStatus() ); //Change just the punch status. //$src_punch_obj->setDeleted(FALSE); //Just in case it was marked deleted by the MOVE action. if ( $src_punch_obj->isValid() == true ) { //Return punch_id, so Flex can base other actions on it. $retval = $src_punch_obj->Save( false ); TTLog::addEntry( $src_punch_obj->getId(), 500, TTi18n::getText( 'Drag & Drop' ) . ': ' . TTi18n::getText( 'Action' ) . ': ' . ( $action == 0 ? TTi18n::getText( 'Copy' ) : TTi18n::getText( 'Move' ) ) . ' ' . TTi18n::getText( 'From' ) . ': ' . TTDate::getDate( 'DATE+TIME', $src_time_stamp ) . ' ' . TTi18n::getText( 'Status' ) . ': ' . $src_status_name, null, $src_punch_obj->getTable() ); $src_punch_control_obj->shift_data = null; //Need to clear the shift data so its obtained from the DB again, otherwise shifts will appear on strange days. $src_punch_control_obj->user_date_obj = null; //Need to clear user_date_obj from cache so a new one is obtained. $src_punch_control_obj->setId( $src_punch_obj->getPunchControlID() ); $src_punch_control_obj->setPunchObject( $src_punch_obj ); if ( $src_punch_control_obj->isValid() == true ) { Debug::Text( ' Punch Control is valid, saving...: ', __FILE__, __LINE__, __METHOD__, 10 ); //Need to make sure we calculate the exceptions if they are moving punches from in/out, as there is likely to be a missing punch exception either way. $src_punch_control_obj->setEnableStrictJobValidation( false ); //Make sure we relax as many validation criteria as possible when making this change since its often called from PunchControlFactory->postSave() and we can't show the errors to the user. $src_punch_control_obj->setEnableCalcUserDateID( false ); $src_punch_control_obj->setEnableCalcTotalTime( false ); $src_punch_control_obj->setEnableCalcSystemTotalTime( true ); $src_punch_control_obj->setEnableCalcWeeklySystemTotalTime( false ); $src_punch_control_obj->setEnableCalcUserDateTotal( false ); $src_punch_control_obj->setEnableCalcException( true ); if ( $src_punch_control_obj->isValid() == true ) { $src_punch_control_obj->Save( true, true ); } } } } } else { Debug::text( 'bCopying punch to new day...', __FILE__, __LINE__, __METHOD__, 10 ); $plf->getByCompanyIDAndId( $company_id, $dst_punch_id ); if ( $plf->getRecordCount() == 1 ) { Debug::text( 'Found DST punch ID: ' . $dst_punch_id, __FILE__, __LINE__, __METHOD__, 10 ); $dst_punch_obj = $plf->getCurrent(); $dst_punch_control_obj = $dst_punch_obj->getPunchControlObject(); Debug::text( 'aSRC TimeStamp: ' . TTDate::getDate( 'DATE+TIME', $src_punch_obj->getTimeStamp() ) . ' DST TimeStamp: ' . TTDate::getDate( 'DATE+TIME', $dst_punch_obj->getTimeStamp() ), __FILE__, __LINE__, __METHOD__, 10 ); $is_punch_control_split = false; if ( $position == 0 ) { //Overwrite Debug::text( 'Overwriting...', __FILE__, __LINE__, __METHOD__, 10 ); //All we need to do is update the time of the destination punch. $punch_obj = $dst_punch_obj; } else { //Before or After //Determine if the destination punch needs to split from another punch //Check to make sure that when splitting an existing punch pair, the new punch is after the IN punch and before the OUT punch. //Otherwise don't split the punch pair and just put it in its own pair. if ( ( $position == -1 && $dst_punch_obj->getStatus() == 20 && ( $dst_status_id == false || $src_punch_obj->getTimeStamp() < $dst_punch_obj->getTimeStamp() ) ) || ( $position == 1 && $dst_punch_obj->getStatus() == 10 && ( $dst_status_id == false || $src_punch_obj->getTimeStamp() > $dst_punch_obj->getTimeStamp() ) ) ) { //Before on Out punch, After on In Punch, Debug::text( 'Need to split destination punch out to its own Punch Control row...', __FILE__, __LINE__, __METHOD__, 10 ); $is_punch_control_split = PunchControlFactory::splitPunchControl( $dst_punch_obj->getPunchControlID() ); //Once a split occurs, we need to re-get the destination punch as the punch_control_id may have changed. //We could probably optimize this to only occur when the destination punch is an In punch, as the //Out punch is always the one to be moved to a new punch_control_id if ( $src_punch_obj->getStatus() != $dst_punch_obj->getStatus() ) { $plf->getByCompanyIDAndId( $company_id, $dst_punch_id ); if ( $plf->getRecordCount() == 1 ) { $dst_punch_obj = $plf->getCurrent(); Debug::text( 'Found DST punch ID: ' . $dst_punch_id . ' Punch Control ID: ' . $dst_punch_obj->getPunchControlID(), __FILE__, __LINE__, __METHOD__, 10 ); } } $punch_control_id = $dst_punch_obj->getPunchControlID(); } else { Debug::text( 'No Need to split destination punch, simply add a new punch/punch_control all on its own.', __FILE__, __LINE__, __METHOD__, 10 ); //Check to see if the src and dst punches are the same status though. $punch_control_id = $dst_punch_control_obj->getNextInsertId(); } //Take the source punch and base our new punch on that. $punch_obj = $src_punch_obj; //Copy source punch to proper location by destination punch. $punch_obj->setId( false ); $punch_obj->setDeleted( false ); //Just in case it was marked deleted by the MOVE action. $punch_obj->setPunchControlId( $punch_control_id ); } //$new_time_stamp = TTDate::getTimeLockedDate($src_punch_obj->getTimeStamp(), $dst_punch_obj->getTimeStamp()+$dst_date_modifier ); $new_time_stamp = TTDate::getTimeLockedDate( $src_punch_obj->getTimeStamp(), ( $dst_punch_obj->getPunchControlObject()->getDateStamp() + $dst_date_modifier ) ); Debug::text( 'SRC TimeStamp: ' . TTDate::getDate( 'DATE+TIME', $src_punch_obj->getTimeStamp() ) . ' DST TimeStamp: ' . TTDate::getDate( 'DATE+TIME', $dst_punch_obj->getTimeStamp() ) . ' New TimeStamp: ' . TTDate::getDate( 'DATE+TIME', $new_time_stamp ), __FILE__, __LINE__, __METHOD__, 10 ); $punch_obj->setTimeStamp( $new_time_stamp, false ); $punch_obj->setActualTimeStamp( $new_time_stamp ); $punch_obj->setOriginalTimeStamp( $new_time_stamp ); $punch_obj->setTransfer( false ); //Always set transfer to FALSE so we don't try to create In/Out punch automatically later. //When drag&drop copying punches, clear some fields that shouldn't be copied. if ( $action == 0 ) { //Copy $punch_obj->setStation( null ); $punch_obj->setHasImage( false ); $punch_obj->setLongitude( null ); //Make sure we clear out long/lat as the location shouldn't carry across with copies. $punch_obj->setLatitude( null ); //Make sure we clear out long/lat as the location shouldn't carry across with copies. //When copying, make sure we clear the original created information to avoid confusion in the audit log. $punch_obj->setCreatedBy( null ); $punch_obj->setCreatedDate( null ); } else if ( isset( $punch_image_data ) && $punch_image_data != '' ) { $punch_obj->setImage( $punch_image_data ); } //When moving or copying, always touch the updated information. $punch_obj->setUpdatedBy( null ); $punch_obj->setUpdatedDate( null ); //Need to take into account copying a Out punch and inserting it BEFORE another Out punch in a punch pair. //In this case a split needs to occur, and the status needs to stay the same. //Status also needs to stay the same when overwriting an existing punch. Debug::text( 'Punch Status: ' . $punch_obj->getStatus() . ' DST Punch Status: ' . $dst_punch_obj->getStatus() . ' Split Punch Control: ' . (int)$is_punch_control_split, __FILE__, __LINE__, __METHOD__, 10 ); if ( ( $position != 0 && $is_punch_control_split == false && $punch_obj->getStatus() == $dst_punch_obj->getStatus() && $punch_obj->getPunchControlID() == $dst_punch_obj->getPunchControlID() ) ) { Debug::text( 'Changing punch status to opposite: ' . $dst_punch_obj->getNextStatus(), __FILE__, __LINE__, __METHOD__, 10 ); $punch_obj->setStatus( $dst_punch_obj->getNextStatus() ); //Change the status to fit in the proper place. } if ( $punch_obj->isValid() == true ) { $insert_id = $punch_obj->Save( false ); TTLog::addEntry( $punch_obj->getId(), 500, TTi18n::getText( 'Drag & Drop' ) . ': ' . TTi18n::getText( 'Action' ) . ': ' . ( $action == 0 ? TTi18n::getText( 'Copy' ) : TTi18n::getText( 'Move' ) ) . ' ' . TTi18n::getText( 'From' ) . ': ' . TTDate::getDate( 'DATE+TIME', $src_time_stamp ) . ' ' . TTi18n::getText( 'Status' ) . ': ' . $src_status_name, null, $punch_obj->getTable() ); $dst_punch_control_obj->shift_data = null; //Need to clear the shift data so its obtained from the DB again, otherwise shifts will appear on strange days, or cause strange conflicts. $dst_punch_control_obj->setID( $punch_obj->getPunchControlID() ); $dst_punch_control_obj->setPunchObject( $punch_obj ); if ( $dst_punch_control_obj->isValid() == true ) { Debug::Text( ' Punch Control is valid, saving...: ', __FILE__, __LINE__, __METHOD__, 10 ); //We need to calculate new total time for the day and exceptions because we are never guaranteed that the gaps will be filled immediately after //in the case of a drag & drop or something. $dst_punch_control_obj->setEnableStrictJobValidation( false ); //Make sure we relax as many validation criteria as possible when making this change since its often called from PunchControlFactory->postSave() and we can't show the errors to the user. $dst_punch_control_obj->setEnableCalcUserDateID( true ); $dst_punch_control_obj->setEnableCalcTotalTime( true ); $dst_punch_control_obj->setEnableCalcSystemTotalTime( true ); $dst_punch_control_obj->setEnableCalcWeeklySystemTotalTime( true ); $dst_punch_control_obj->setEnableCalcUserDateTotal( true ); $dst_punch_control_obj->setEnableCalcException( true ); if ( $dst_punch_control_obj->isValid() == true ) { if ( $dst_punch_control_obj->Save( true, true ) == true ) { //Force isNew() lookup. //Return newly inserted punch_id, so Flex can base other actions on it. $retval = $insert_id; //$retval = TRUE; } } } } } } } else { Debug::text( 'Punch Control object does not exist, unable to continue... Punch Control ID: ' . $src_punch_obj->getPunchControlID(), __FILE__, __LINE__, __METHOD__, 10 ); } } if ( $retval == false ) { $plf->FailTransaction(); } //$plf->FailTransaction(); $plf->CommitTransaction(); $plf->setTransactionMode(); //Back to default isolation level. return [ $retval ]; }; [ $retval ] = $this->RetryTransaction( $transaction_function ); Debug::text( 'Returning: ' . (int)$retval, __FILE__, __LINE__, __METHOD__, 10 ); return $retval; } /** * When passed a punch_control_id, if it has two punches assigned to it, a new punch_control_id row is created and the punches are split between the two. * @param string $punch_control_id UUID * @return bool */ static function splitPunchControl( $punch_control_id ) { $retval = false; if ( $punch_control_id != '' ) { $plf = TTnew( 'PunchListFactory' ); /** @var PunchListFactory $plf */ $plf->StartTransaction(); $plf->getByPunchControlID( $punch_control_id, null, [ 'time_stamp' => 'desc' ] ); //Move out punch to new punch_control_id. if ( $plf->getRecordCount() == 2 ) { $pclf = TTnew( 'PunchControlListFactory' ); /** @var PunchControlListFactory $pclf */ $new_punch_control_id = $pclf->getNextInsertId(); Debug::text( ' Punch Control ID: ' . $punch_control_id . ' only has two punches assigned, splitting... New Punch Control ID: ' . $new_punch_control_id, __FILE__, __LINE__, __METHOD__, 10 ); $i = 0; foreach ( $plf as $p_obj ) { if ( $i == 0 ) { //First punch (out) //Get the PunchControl Object before we change to the new punch_control_id $pc_obj = $p_obj->getPunchControlObject(); $p_obj->setPunchControlId( $new_punch_control_id ); if ( $p_obj->isValid() == true ) { $p_obj->Save( false ); $pc_obj->setId( $new_punch_control_id ); $pc_obj->setPunchObject( $p_obj ); if ( $pc_obj->isValid() == true ) { Debug::Text( ' Punch Control is valid, saving Punch ID: ' . $p_obj->getID() . ' To new Punch Control ID: ' . $new_punch_control_id, __FILE__, __LINE__, __METHOD__, 10 ); //We need to calculate new total time for the day and exceptions because we are never guaranteed that the gaps will be filled immediately after //in the case of a drag & drop or something. $pc_obj->setEnableStrictJobValidation( false ); //Make sure we relax as many validation criteria as possible when making this change since its often called from PunchControlFactory->postSave() and we can't show the errors to the user. $pc_obj->setEnableCalcUserDateID( true ); $pc_obj->setEnableCalcTotalTime( true ); $pc_obj->setEnableCalcSystemTotalTime( false ); //Do this for In punch only. $pc_obj->setEnableCalcWeeklySystemTotalTime( false ); //Do this for In punch only. $pc_obj->setEnableCalcUserDateTotal( true ); $pc_obj->setEnableCalcException( true ); if ( $pc_obj->isValid() ) { $retval = $pc_obj->Save( true, true ); //Force isNew() lookup. } } } } else { //Second punch (in), need to recalculate user_date_total for this one to clear the total time, as well as recalculate the entire week //for system totals so those are updated as well. Debug::text( ' ReCalculating total time for In punch...', __FILE__, __LINE__, __METHOD__, 10 ); $pc_obj = $p_obj->getPunchControlObject(); $pc_obj->setEnableStrictJobValidation( false ); //Make sure we relax as many validation criteria as possible when making this change since its often called from PunchControlFactory->postSave() and we can't show the errors to the user. $pc_obj->setEnableCalcUserDateID( true ); $pc_obj->setEnableCalcTotalTime( true ); $pc_obj->setEnableCalcSystemTotalTime( true ); $pc_obj->setEnableCalcWeeklySystemTotalTime( true ); $pc_obj->setEnableCalcUserDateTotal( true ); $pc_obj->setEnableCalcException( true ); if ( $pc_obj->isValid() ) { $retval = $pc_obj->Save(); } } $i++; } } else { Debug::text( ' Punch Control ID: ' . $punch_control_id . ' only has one punch assigned, doing nothing...', __FILE__, __LINE__, __METHOD__, 10 ); } //$plf->FailTransaction(); $plf->CommitTransaction(); } return $retval; } /** * @return bool */ function postSave() { $this->removeCache( $this->getId() ); $this->calcUserDate(); $this->calcUserDateTotal(); //Check to see if timesheet is verified, if so unverify it on modified punch. //Make sure exceptions are calculated *after* this so TimeSheet Not Verified exceptions can be triggered again. if ( is_object( $this->getPayPeriodScheduleObject() ) && $this->getPayPeriodScheduleObject()->getTimeSheetVerifyType() != 10 ) { //Find out if timesheet is verified or not. $pptsvlf = TTnew( 'PayPeriodTimeSheetVerifyListFactory' ); /** @var PayPeriodTimeSheetVerifyListFactory $pptsvlf */ $pptsvlf->getByPayPeriodIdAndUserId( $this->getPayPeriod(), $this->getUser() ); if ( $pptsvlf->getRecordCount() > 0 ) { //Pay period is verified, delete all records and make log entry. Debug::text( 'Pay Period is verified, deleting verification records: ' . $pptsvlf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 ); foreach ( $pptsvlf as $pptsv_obj ) { if ( is_object( $this->getPunchObject() ) ) { TTLog::addEntry( $pptsv_obj->getId(), 500, TTi18n::getText( 'TimeSheet Modified After Verification' ) . ': ' . UserListFactory::getFullNameById( $this->getUser() ) . ' ' . TTi18n::getText( 'Punch' ) . ': ' . TTDate::getDate( 'DATE+TIME', $this->getPunchObject()->getTimeStamp() ), null, $pptsvlf->getTable() ); } $pptsv_obj->setDeleted( true ); if ( $pptsv_obj->isValid() ) { $pptsv_obj->Save(); } } } } if ( $this->getEnableCalcSystemTotalTime() == true && is_object( $this->getUserObject() ) ) { //old_date_stamps can contain other dates from calcUserDate() as well. $this->old_date_stamps[] = $this->getDateStamp(); //Make sure the current date is calculated if ( $this->getOldDateStamp() != '' ) { $this->old_date_stamps[] = $this->getOldDateStamp(); //Make sure the old date is calculated } UserDateTotalFactory::reCalculateDay( $this->getUserObject(), $this->old_date_stamps, $this->getEnableCalcException(), $this->getEnablePreMatureException() ); } return true; } /** * @param $data * @return bool */ function setObjectFromArray( $data ) { if ( is_array( $data ) ) { $data = $this->parseCustomFieldsFromArray( $data ); $variable_function_map = $this->getVariableToFunctionMap(); foreach ( $variable_function_map as $key => $function ) { if ( isset( $data[$key] ) ) { $function = 'set' . $function; switch ( $key ) { //Ignore any user_date_id, as we will figure it out on our own based on the time_stamp and pay period settings ($pcf->setEnableCalcUserDateID(TRUE)) //This breaks smartRecalculate() as it doesn't know the previous user_date_id to calculate. So when shifts are reassigned to new days //the old days are not recalculated properly. //case 'user_date_id': // break; case 'date_stamp': //HTML5 interface sends punch_date rather than date_stamp when saving a new punch. break; case 'punch_date': $this->setDateStamp( $data[$key] ); break; default: if ( method_exists( $this, $function ) ) { $this->$function( $data[$key] ); } break; } } } $this->setCreatedAndUpdatedColumns( $data ); return true; } return false; } /** * @param null $include_columns * @param bool $permission_children_ids * @return array */ function getObjectAsArray( $include_columns = null, $permission_children_ids = false ) { $data = []; $variable_function_map = $this->getVariableToFunctionMap(); if ( is_array( $variable_function_map ) ) { foreach ( $variable_function_map as $variable => $function_stub ) { if ( $include_columns == null || ( isset( $include_columns[$variable] ) && $include_columns[$variable] == true ) ) { $function = 'get' . $function_stub; switch ( $variable ) { case 'total_time': //Ignore total time, as its calculated later anyways, so if its set here it will cause a validation error. break; default: if ( method_exists( $this, $function ) ) { $data[$variable] = $this->$function(); } break; } } } $this->getPermissionColumns( $data, $this->getColumn( 'user_id' ), $this->getCreatedBy(), $permission_children_ids, $include_columns ); $this->getCreatedAndUpdatedColumns( $data, $include_columns ); $data = $this->getCustomFields( $this->getUserObject()->getCompany(), $data, $include_columns ); } return $data; } /** * @param $log_action * @return bool */ function addLog( $log_action ) { return TTLog::addEntry( $this->getId(), $log_action, TTi18n::getText( 'Punch Control - Employee' ) . ': ' . UserListFactory::getFullNameById( $this->getUser() ), null, $this->getTable(), $this ); } } ?>