* @copyright Copyright (c) 2022 Metaways Infosystems GmbH (http://www.metaways.de) * */ use Tinebase_ModelConfiguration_Const as MCC; /** * * @package HumanResources * @subpackage BL */ class HumanResources_BL_AttendanceRecorder_TimeSheet implements Tinebase_BL_ElementInterface, HumanResources_BL_AttendanceRecorder_UndoInterface { /** @var HumanResources_Model_BLAttendanceRecorder_TimeSheetConfig */ protected $_config; /** @var Timetracker_Model_Timeaccount */ protected $_staticTA; protected $_staticTAid; protected $_allowOtherTAs; protected $_fillOtherDevices; protected $_tsPublicRAII; protected $_doRounding; protected $_roundingToMin; protected $_roundingPauseThreshold; protected $_roundingByClock; public function __construct(HumanResources_Model_BLAttendanceRecorder_TimeSheetConfig $_config) { $this->_config = $_config; if ($_config->{HumanResources_Model_BLAttendanceRecorder_TimeSheetConfig::FLD_STATIC_TA}) { if ($_config->{HumanResources_Model_BLAttendanceRecorder_TimeSheetConfig::FLD_STATIC_TA} instanceof Tinebase_Record_Interface) { $this->_staticTA = $_config->{HumanResources_Model_BLAttendanceRecorder_TimeSheetConfig::FLD_STATIC_TA}; } else { $this->_staticTAid = $_config->getIdFromProperty(HumanResources_Model_BLAttendanceRecorder_TimeSheetConfig::FLD_STATIC_TA); } } $this->_allowOtherTAs = (bool)$_config->{HumanResources_Model_BLAttendanceRecorder_TimeSheetConfig::FLD_ALLOW_OTHER_TA}; $this->_tsPublicRAII = new Tinebase_RAII(Timetracker_Controller_Timesheet::getInstance()->assertPublicUsage()); $this->_roundingToMin = intval($_config->{HumanResources_Model_BLAttendanceRecorder_TimeSheetConfig::FLD_ROUNDING_TO_MIN}); $this->_doRounding = $this->_roundingToMin > 0; $this->_roundingByClock = (bool)$_config->{HumanResources_Model_BLAttendanceRecorder_TimeSheetConfig::FLD_ROUNDING_BY_CLOCK}; $this->_roundingPauseThreshold = intval($_config->{HumanResources_Model_BLAttendanceRecorder_TimeSheetConfig::FLD_ROUNDING_PAUSE_THRESHOLD}) * 60; } /** * @param Tinebase_BL_PipeContext $_context * @param Tinebase_BL_DataInterface $_data */ public function execute(Tinebase_BL_PipeContext $_context, Tinebase_BL_DataInterface $_data) { /** @var HumanResources_BL_AttendanceRecorder_Data $_data */ if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__); $employeeRaii = new Tinebase_RAII(HumanResources_Controller_Employee::getInstance()->assertPublicUsage()); $timesheetRaii = new Tinebase_RAII(Timetracker_Controller_Timesheet::getInstance()->assertPublicUsage()); $timeaccountRaii = new Tinebase_RAII(Timetracker_Controller_Timeaccount::getInstance()->assertPublicUsage()); $oldUser = Tinebase_Core::getUser(); $oldUserRaii = new Tinebase_RAII(fn() => Tinebase_Core::getUser()->getId() !== $oldUser->getId() ? Tinebase_Core::setUser($oldUser) : null); foreach (array_unique($_data->data->{HumanResources_Model_AttendanceRecord::FLD_ACCOUNT_ID}) as $accountId) { if (Tinebase_Core::getUser()->getId() !== $accountId) { Tinebase_Core::setUser(Tinebase_User::getInstance()->getFullUserById($accountId)); } // read config, not property here! if (!$this->_config->{HumanResources_Model_BLAttendanceRecorder_TimeSheetConfig::FLD_STATIC_TA}) { $employee = HumanResources_Controller_Employee::getInstance()->getEmployeeByUser($accountId); $this->_staticTA = HumanResources_Controller_WorkingTimeScheme::getInstance() ->getWorkingTimeAccount($employee); } $accountData = $_data->data->filter(HumanResources_Model_AttendanceRecord::FLD_ACCOUNT_ID, $accountId); foreach (array_unique($accountData->{HumanResources_Model_AttendanceRecord::FLD_REFID}) as $refId) { $tsRs = null; $prevRecord = null; $refIdRecords = $accountData->filter(HumanResources_Model_AttendanceRecord::FLD_REFID, $refId); /** @var HumanResources_Model_AttendanceRecord $record */ foreach ($refIdRecords as $record) { while (!$tsRs && isset($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['id'])) { $tsRs = Timetracker_Controller_Timesheet::getInstance()->getMultiple($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['id']); if (0 === $tsRs->count() || null === $tsRs->find(fn ($ts) => $ts->start_date->format('Y-m-d') === $record->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP}->getClone()->setTimezone(Tinebase_Core::getUserTimezone())->format('Y-m-d'), null)) { if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' ts not found!'); $tsRs = null; break; } $tsRs->sort(function(Timetracker_Model_Timesheet $ts1, Timetracker_Model_Timesheet $ts2): int { return $ts1->start_date->compare($ts2->start_date); }); Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($tsRs); $prevRecord = $record; continue 2; } if (!$tsRs) { if (HumanResources_Model_AttendanceRecord::TYPE_CLOCK_IN !== $record->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) { // shouldn't happen, we create faulty ts and be done with it $record->{HumanResources_Model_AttendanceRecord::FLD_BLPROCESSED} = true; $record->{HumanResources_Model_AttendanceRecord::FLD_STATUS} = HumanResources_Model_AttendanceRecord::STATUS_FAULTY; $this->createTimeSheet($record, $record->{HumanResources_Model_AttendanceRecord::FLD_TYPE}); $refIdRecords->removeById($record->getId()); $prevRecord = null; } else { // check if we need to close an absence TS for WT if (HumanResources_Model_AttendanceRecorderDevice::SYSTEM_WORKING_TIME_ID === $record->getIdFromProperty(HumanResources_Model_AttendanceRecord::FLD_DEVICE_ID)) { $lastClose = HumanResources_Controller_AttendanceRecord::getInstance()->search( Tinebase_Model_Filter_FilterGroup::getFilterForModel( HumanResources_Model_AttendanceRecord::class, [ ['field' => HumanResources_Model_AttendanceRecord::FLD_DEVICE_ID, 'operator' => 'equals', 'value' => HumanResources_Model_AttendanceRecorderDevice::SYSTEM_WORKING_TIME_ID], ['field' => HumanResources_Model_AttendanceRecord::FLD_ACCOUNT_ID, 'operator' => 'equals', 'value' => $record->getIdFromProperty(HumanResources_Model_AttendanceRecord::FLD_ACCOUNT_ID)], ['field' => HumanResources_Model_AttendanceRecord::FLD_TYPE, 'operator' => 'equals', 'value' => HumanResources_Model_AttendanceRecord::TYPE_CLOCK_OUT], ['field' => HumanResources_Model_AttendanceRecord::FLD_STATUS, 'operator' => 'equals', 'value' => HumanResources_Model_AttendanceRecord::STATUS_CLOSED], ]), new Tinebase_Model_Pagination(['sort' => HumanResources_Model_AttendanceRecord::FLD_SEQUENCE, 'dir' => 'DESC', 'limit' => 1]))->getFirstRecord(); while ($lastClose && isset($lastClose->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['fttTS']['id'])) { $ids = []; try { /** @var Timetracker_Model_Timesheet $absenceTS */ $absenceTS = Timetracker_Controller_Timesheet::getInstance() ->get($lastClose->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['fttTS']['id']); } catch (Tinebase_Exception_NotFound $tenf) { break; } /** @var Tinebase_DateTime $absenceClockIn */ $absenceClockIn = $record->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP} ->getClone()->setTimezone(Tinebase_Core::getUserTimezone()); $absenceClockOut = new Tinebase_DateTime($absenceTS->start_date . ' ' . $absenceTS->start_time, Tinebase_Core::getUserTimezone()); while ($absenceClockOut->format('Y-m-d') !== $absenceClockIn->format('Y-m-d')) { $absenceTS->end_time = '00:00:00'; if ($absenceTS->getId()) { $absenceTS = Timetracker_Controller_Timesheet::getInstance()->update($absenceTS); $ids[$absenceTS->getId()] = $absenceTS->seq == 2 ? 2 : false; } else { $absenceTS = Timetracker_Controller_Timesheet::getInstance()->create($absenceTS); $ids[$absenceTS->getId()] = 1; } $absenceTS->setId(null); $absenceTS->start_time = '00:00:00'; $absenceTS->start_date = $absenceClockOut->addDay(1)->format('Y-m-d'); $absenceTS->description = ''; } $absenceTS->end_time = $absenceClockIn->format('H:i:00'); if (empty($absenceTS->notes) || is_array($absenceTS->notes)) { $absenceTS->notes = new Tinebase_Record_RecordSet(Tinebase_Model_Note::class, (array)$absenceTS->notes); } $absenceTS->notes->addRecord(new Tinebase_Model_Note([ 'note' => sprintf(Tinebase_Translation::getTranslation(HumanResources_Config::APP_NAME)->_('Clock in: %1$s'), $absenceTS->end_time), 'note_type_id' => Tinebase_Model_Note::SYSTEM_NOTE_NAME_NOTE, ])); if (empty($absenceTS->description)) { $absenceTS->description = self::getTSDescription($record); } if ($absenceTS->getId()) { $absenceTS = Timetracker_Controller_Timesheet::getInstance()->update($absenceTS); $ids[$absenceTS->getId()] = $absenceTS->seq == 2 ? 2 : false; } else { $absenceTS = Timetracker_Controller_Timesheet::getInstance()->create($absenceTS); $ids[$absenceTS->getId()] = 1; } $record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['fttTS']['ids'] = $ids; break; } } $tsRs = new Tinebase_Record_RecordSet(Timetracker_Model_Timesheet::class, [$this->createTimeSheet($record)]); $prevRecord = $record; } continue; } elseif (isset($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['id'])) { if (empty(array_diff($tsRs->getArrayOfIds(), (array)$record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['id']))) { $prevRecord = $record; continue; } unset($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]); } if (!$this->updateTimeSheet($tsRs, $record, $prevRecord)) { $this->calculateTS($tsRs, $refIdRecords, true); $tsRs = null; break; } else { $prevRecord = $record; } } if ($tsRs) { $this->calculateTS($tsRs, $refIdRecords); } } } unset($oldUserRaii); unset($employeeRaii); unset($timesheetRaii); unset($timeaccountRaii); } protected function updateTimeSheet(Tinebase_Record_RecordSet $tsRs, HumanResources_Model_AttendanceRecord $record, HumanResources_Model_AttendanceRecord $prevRecord): bool { if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__); static $translate; if (null === $translate) { $translate = Tinebase_Translation::getTranslation(HumanResources_Config::APP_NAME); } $tz = Tinebase_Core::getUserTimezone(); $record->setTimezone($tz, false); $tzRaii = new Tinebase_RAII(function() use($record) { $record->setTimezone('UTC', false); }); $idsRaii = new Tinebase_RAII(function() use(&$record, &$tsRs) { $record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['id'] = $tsRs->getArrayOfIds(); }); if (isset($prevRecord->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['changed'])) { $record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['changed'] = $prevRecord->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['changed']; } /** make record dirty */ $record->{HumanResources_Model_AttendanceRecord::FLD_BLPROCESSED} = (bool)$record->{HumanResources_Model_AttendanceRecord::FLD_BLPROCESSED}; if (HumanResources_Model_AttendanceRecord::STATUS_FAULTY === $record->{HumanResources_Model_AttendanceRecord::FLD_STATUS} || HumanResources_Model_AttendanceRecord::STATUS_FAULTY === $prevRecord->{HumanResources_Model_AttendanceRecord::FLD_STATUS}) { $tsRs->need_for_clarification = true; } foreach ($tsRs as $ts) { if (empty($ts->description)) { $ts->description = self::getTSDescription($record); } if (empty($ts->notes) || is_array($ts->notes)) { $ts->notes = new Tinebase_Record_RecordSet(Tinebase_Model_Note::class, (array)$ts->notes); } } /** @var Timetracker_Model_Timesheet $lastTs */ $lastTs = $tsRs->getLastRecord(); $fillTsForDayJumps = function(HumanResources_Model_AttendanceRecord $record) use ($tsRs, &$lastTs) { while (strcmp($lastTs->start_date->format('Y-m-d'), $record->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP}->format('Y-m-d')) < 0) { $lastTs = clone $lastTs; $lastTs->setId(null); $lastTs->start_time = '00:00:00'; $lastTs->end_time = null; $lastTs->duration = 0; $lastTs->start_date->addDay(1); $lastTs->description = self::getTSDescription($record); $lastTs->notes = null; $tsRs->addRecord($lastTs = Timetracker_Controller_Timesheet::getInstance()->create($lastTs)); $lastTs->notes = new Tinebase_Record_RecordSet(Tinebase_Model_Note::class); } }; if (HumanResources_Model_AttendanceRecord::TYPE_CLOCK_OUT === $record->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) { $fillTsForDayJumps($record); if ($record->{HumanResources_Model_AttendanceRecord::FLD_FREETIMETYPE_ID}) { $fttId = $record->getIdFromProperty(HumanResources_Model_AttendanceRecord::FLD_FREETIMETYPE_ID); $ftt = HumanResources_Controller_FreeTimeType::getInstance()->get($fttId); $lastTs->{HumanResources_Model_FreeTimeType::TT_TS_SYSCF_CLOCK_OUT_REASON} = $fttId; // FIXME write test or check if test is available if ($ftt->enable_timetracking) { if ($ftt->timeaccount) { $fttTA = Timetracker_Controller_Timeaccount::getInstance()->get($ftt->getIdFromProperty('timeaccount')); } else { $fttTA = $this->getTA($record); } $startDate = $record->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP}->getClone() ->setTimezone(Tinebase_Core::getUserTimezone()); $fttTS = Timetracker_Controller_Timesheet::getInstance()->create(new Timetracker_Model_Timesheet([ 'account_id' => $record->getIdFromProperty(HumanResources_Model_AttendanceRecord::FLD_ACCOUNT_ID), 'timeaccount_id' => $fttTA->getId(), 'is_billable' => $fttTA->is_billable, 'start_date' => $startDate, 'start_time' => $startDate->format('H:i:00'), 'end_time' => $startDate->format('H:i:00'), 'duration' => 0, HumanResources_Model_FreeTimeType::TT_TS_SYSCF_ABSENCE_REASON => $fttId, 'description' => self::getTSDescription($record), 'notes' => new Tinebase_Record_RecordSet(Tinebase_Model_Note::class, [ new Tinebase_Model_Note([ 'note' => sprintf(Tinebase_Translation::getTranslation(HumanResources_Config::APP_NAME)->_('Clock in: %1$s'), $startDate->format('H:i:s')), 'note_type_id' => Tinebase_Model_Note::SYSTEM_NOTE_NAME_NOTE, ])]), ], true)); $record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['fttTS']['id'] = $fttTS->getId(); } $lastTs->{HumanResources_Model_FreeTimeType::TT_TS_SYSCF_CLOCK_OUT_REASON} = $fttId; } $lastTs->notes->addRecord(new Tinebase_Model_Note([ 'note' => sprintf($translate->_('Clock out: %1$s'), $record->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP}->format('H:i:s')), 'note_type_id' => Tinebase_Model_Note::SYSTEM_NOTE_NAME_NOTE, ])); return false; } if (HumanResources_Model_AttendanceRecord::TYPE_CLOCK_IN === $record->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) { if (strcmp($lastTs->start_date->format('Y-m-d'), $record->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP}->format('Y-m-d')) < 0) { $tsRs->addRecord($lastTs = $this->createTimeSheet($record)); $lastTs->notes = new Tinebase_Record_RecordSet(Tinebase_Model_Note::class); } $lastTs->notes->addRecord(new Tinebase_Model_Note([ 'note' => sprintf($translate->_('Clock in: %1$s'), $record->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP}->format('H:i:s')), 'note_type_id' => Tinebase_Model_Note::SYSTEM_NOTE_NAME_NOTE, ])); if (HumanResources_Model_AttendanceRecord::TYPE_CLOCK_PAUSED !== $prevRecord->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) { //this shouldn't happen ... $tsRs->need_for_clarification = true; } } else { // pause $fillTsForDayJumps($record); $lastTs->notes->addRecord(new Tinebase_Model_Note([ 'note' => sprintf($translate->_('Clock pause: %1$s'), $record->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP}->format('H:i:s')), 'note_type_id' => Tinebase_Model_Note::SYSTEM_NOTE_NAME_NOTE, ])); if (HumanResources_Model_AttendanceRecord::TYPE_CLOCK_IN !== $prevRecord->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) { //this shouldn't happen ... $tsRs->need_for_clarification = true; } } unset($idsRaii); unset($tzRaii); return true; } protected function calculateTS(Tinebase_Record_RecordSet $tsRs, Tinebase_Record_RecordSet $records, bool $close = false) { if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__); $tz = Tinebase_Core::getUserTimezone(); $clockedIn = false; $slots = []; if ($close) { $records->{HumanResources_Model_AttendanceRecord::FLD_BLPROCESSED} = 1; } $lastRecord = null; /** @var HumanResources_Model_AttendanceRecord $record */ foreach ($records as $record) { if (isset($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['seq'])) { $lastRecord = $record; } switch ($record->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) { case HumanResources_Model_AttendanceRecord::TYPE_CLOCK_IN: if ($clockedIn) break; $clockedIn = true; $slots[] = ['start' => $record->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP}->getClone()->setTimezone($tz)]; break; case HumanResources_Model_AttendanceRecord::TYPE_CLOCK_OUT: if ($clockedIn) { $slots[count($slots)-1]['end'] = $record->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP}->getClone()->setTimezone($tz); $clockedIn = false; } break 2; case HumanResources_Model_AttendanceRecord::TYPE_CLOCK_PAUSED: if ($clockedIn) { $slots[count($slots)-1]['end'] = $record->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP}->getClone()->setTimezone($tz); $clockedIn = false; } break; default: throw new Tinebase_Exception_NotImplemented('haiiiya uncle roger puts his foot down!'); } } if ($clockedIn) { unset($slots[count($slots)-1]); } if ($this->_doRounding) { $this->roundSlots($slots); } $tsRs->duration = 0; $processedIds = []; // Collect timesheet IDs that were manually changed (seq increased after last BL processing) // to preserve their start_time during recalculation $changedTsIds = []; /** @var HumanResources_Model_AttendanceRecord $rec */ foreach ($records as $rec) { if (isset($rec->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['seq'])) { $recordSeqMap = $rec->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['seq']; foreach ($tsRs as $ts) { if (isset($recordSeqMap[$ts->getId()]) && $ts->seq > $recordSeqMap[$ts->getId()]) { $changedTsIds[] = $ts->getId(); break; } } } } if (empty($slots)) { $startDate = $records->getFirstRecord()->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP}->getClone()->setTimezone($tz)->format('Y-m-d'); /** @var Timetracker_Model_Timesheet $ts */ $ts = $tsRs->find(function (Timetracker_Model_Timesheet $ts) use ($startDate) { return $ts->start_date->format('Y-m-d') === $startDate; }, null); $processedIds[] = $ts->getId(); } else { reset($slots); $lastStartDate = null; foreach ($slots as $slot) { $startDate = $slot['start']->format('Y-m-d'); if ($lastStartDate === $startDate) { $sameDay = true; } else { $sameDay = false; } /** @var Timetracker_Model_Timesheet $ts */ $ts = $tsRs->find(function (Timetracker_Model_Timesheet $ts) use ($startDate) { return $ts->start_date->format('Y-m-d') === $startDate; }, null); $processedIds[] = $ts->getId(); if (!$sameDay) { $slotStartTime = $slot['start']->format('H:i:00'); if (!in_array($ts->getId(), $changedTsIds, true)) { $ts->start_time = $slotStartTime; } } else { $ts->duration += intval(ceil((((new Tinebase_DateTime($ts->start_date->format('Y-m-d ') . $ts->end_time))->getTimestamp() - (new Tinebase_DateTime($ts->start_date->format('Y-m-d ') . $ts->start_time))->getTimestamp()) / 60))); $ts->start_time = null; $ts->end_time = null; } while (strcmp($startDate, $slot['end']->format('Y-m-d')) < 0) { if (!$sameDay) { $ts->end_time = '00:00:00'; } else { $ts->duration += intval(ceil(((new Tinebase_DateTime($ts->start_date->format('Y-m-d ') . $slot['end']->format('H:i:00')))->getTimestamp() - (new Tinebase_DateTime($ts->start_date->format('Y-m-d ') . $slot['start']->format('H:i:00')))->getTimestamp()) / 60)); $sameDay = false; } $startDate = $ts->start_date->getClone()->addDay(1)->format('Y-m-d'); $ts = $tsRs->find(function (Timetracker_Model_Timesheet $ts) use ($startDate) { return $ts->start_date->format('Y-m-d') === $startDate; }, null); $processedIds[] = $ts->getId(); $ts->start_time = '00:00:00'; } if (!$sameDay) { $ts->end_time = $slot['end']->format('H:i:00'); } else { $ts->duration += intval(ceil(((new Tinebase_DateTime($ts->start_date->format('Y-m-d ') . $slot['end']->format('H:i:00')))->getTimestamp() - (new Tinebase_DateTime($ts->start_date->format('Y-m-d ') . $slot['start']->format('H:i:00')))->getTimestamp()) / 60)); } $lastStartDate = $startDate; } } $toDelete = array_diff($tsRs->getArrayOfIds(), $processedIds); foreach ($toDelete as $id) { $tsRs->removeById($id); } if (!empty($toDelete)) { Timetracker_Controller_Timesheet::getInstance()->delete($toDelete); } if ($lastRecord) { $changed = $lastRecord->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['changed'] ?? []; } else { $changed = []; } foreach ($tsRs as $ts) { if ($lastRecord && isset($lastRecord->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['seq'][$ts->getId()]) && $ts->seq > $lastRecord->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['seq'][$ts->getId()]) { $changed[$ts->getId()] = true; } $updated = Timetracker_Controller_Timesheet::getInstance()->update($ts); $ts->seq = $updated->seq; } foreach ($records as $record) { $record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['id'] = $tsRs->getArrayOfIds(); $record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['seq'] = $tsRs->getIdPropertyMap('seq'); if (!empty($changed)) { $record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['changed'] = $changed; } if (HumanResources_Model_AttendanceRecord::TYPE_CLOCK_OUT === $record->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) { break; } } } protected function roundSlots(array &$slots) { foreach ($slots as $idx => $slot) { $slot['start']->setSecond(0); if (($sec = $slot['end']->format('s')) > 0) { $slot['end']->addSecond(60 - $sec); } if ($slot['end']->getTimestamp() === $slot['start']->getTimestamp()) { unset($slots[$idx]); } } if ($this->_roundingPauseThreshold) { foreach ($slots as $idx => &$slot) { if (0 === $idx) continue; if ($slot['start']->getTimestamp() - $slots[$idx-1]['end']->getTimestamp() < $this->_roundingPauseThreshold) { $slot['start'] = $slots[$idx-1]['start']; unset($slots[$idx-1]); } } unset($slot); } foreach ($slots as $slot) { if ($this->_roundingByClock) { if ($this->_roundingToMin > 59) { $slot['start']->setMinute(0); $durationMin = ceil(($slot['end']->getTimestamp() - $slot['start']->getTimestamp()) / 60); if (($modMin = $durationMin % $this->_roundingToMin) > 0) { $slot['end']->addMinute($this->_roundingToMin - $modMin); } } else { $slot['start']->setMinute( $this->_roundingToMin * floor($slot['start']->format('i') / $this->_roundingToMin)); if (0 !== (int)$slot['end']->format('i')) { $slot['end']->setMinute( $end = ($this->_roundingToMin * ceil($slot['end']->format('i') / $this->_roundingToMin)) % 60); if (0 === $end) { $slot['end']->addHour(1); } } } } else { $durationMin = ceil(($slot['end']->getTimestamp() - $slot['start']->getTimestamp()) / 60); if (($modMin = $durationMin % $this->_roundingToMin) > 0) { $slot['end']->addMinute($this->_roundingToMin - $modMin); } } } } protected function getTA(HumanResources_Model_AttendanceRecord $record): Timetracker_Model_Timeaccount { // read config, not property here! if (!$this->_config->{HumanResources_Model_BLAttendanceRecorder_TimeSheetConfig::FLD_STATIC_TA} && isset($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timeaccount::class])) { /** @var Timetracker_Model_Timeaccount $ta */ $ta = Timetracker_Controller_Timeaccount::getInstance()->get($record ->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timeaccount::class]); return $ta; } elseif ($this->_staticTA) { return $this->_staticTA; } else { $this->_staticTA = Timetracker_Controller_Timeaccount::getInstance()->get($this->_staticTAid); return $this->_staticTA; } } protected function createTimeSheet(HumanResources_Model_AttendanceRecord $record, ?string $type = null): Timetracker_Model_Timesheet { if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__); static $translate; if (null === $translate) { $translate = Tinebase_Translation::getTranslation(HumanResources_Config::APP_NAME); } $ta = $this->getTA($record); /** @var Tinebase_DateTime $date */ $date = $record->{HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP}->getClone()->setTimezone(Tinebase_Core::getUserTimezone()); $ts = new Timetracker_Model_Timesheet([ 'account_id' => $record->getIdFromProperty(HumanResources_Model_AttendanceRecord::FLD_ACCOUNT_ID), 'timeaccount_id' => $ta->getId(), 'is_billable' => $ta->is_billable, 'start_date' => $date, 'start_time' => $date->format('H:i:00'), 'end_time' => $date->format('H:i:00'), 'duration' => 0, 'notes' => new Tinebase_Record_RecordSet(Tinebase_Model_Note::class), 'description' => self::getTSDescription($record), ], true); $note = sprintf($translate->_($type ? (HumanResources_Model_AttendanceRecord::TYPE_CLOCK_OUT === $type ? 'Clock out: %1$s' : 'Clock pause: %1$s') : 'Clock in: %1$s'), $date->format('H:i:s')); $ts->notes->addRecord(new Tinebase_Model_Note([ 'note' => $note, 'note_type_id' => Tinebase_Model_Note::SYSTEM_NOTE_NAME_NOTE, ])); if ($record->{HumanResources_Model_AttendanceRecord::FLD_FREETIMETYPE_ID}) { $ts->{HumanResources_Model_FreeTimeType::TT_TS_SYSCF_CLOCK_OUT_REASON} = $record->getIdFromProperty(HumanResources_Model_AttendanceRecord::FLD_FREETIMETYPE_ID); } if (HumanResources_Model_AttendanceRecord::STATUS_FAULTY === $record->{HumanResources_Model_AttendanceRecord::FLD_STATUS}) { $ts->need_for_clarification = true; } /** @var Timetracker_Model_Timesheet $ts */ $ts = Timetracker_Controller_Timesheet::getInstance()->create($ts); // doesnt make the record dirty! $record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['id'] = [$ts->getId()]; $record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['seq'] = [$ts->getId() => $ts->seq]; // this does $record->{HumanResources_Model_AttendanceRecord::FLD_STATUS} = (string)$record->{HumanResources_Model_AttendanceRecord::FLD_STATUS}; return $ts; } public static function getTSDescription(HumanResources_Model_AttendanceRecord $record): string { return isset($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][HumanResources_Config_AttendanceRecorder::METADATA_TS_DESCRIPTION]) ? $record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][HumanResources_Config_AttendanceRecorder::METADATA_TS_DESCRIPTION] : Tinebase_Translation::getTranslation(HumanResources_Config::APP_NAME)->translate( HumanResources_Config::getInstance()->{HumanResources_Config::ATTENDANCE_RECORDER_DEFAULT_TS_DESCRIPTION} ); } public function undo(Tinebase_Record_RecordSet $data): void { if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__); $tsCtrl = Timetracker_Controller_Timesheet::getInstance(); $tsCtrlRaii = new Tinebase_RAII($tsCtrl->assertPublicUsage()); $tsData = []; /** @var HumanResources_Model_AttendanceRecord $record */ foreach ($data as $record) { // this makes the record dirty $record->{HumanResources_Model_AttendanceRecord::FLD_BLPROCESSED} = 0; if (isset($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['id'])) { foreach ((array)$record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['id'] as $id) { if (isset($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['changed'][$id])) { $tsData[$id] = ['changed' => true]; } else { $tsData[$id] = [ 'seq' => $record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['seq'][$id] ]; } } } if (isset($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['fttTS']['id'])) { $tsData[$record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['fttTS']['id']] = ['seq' => 1]; } if (isset($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['fttTS']['ids'])) { foreach ($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]['fttTS']['ids'] as $id => $val) { if ($val) { $tsData[$id] = ['seq' => $val]; } else { $tsData[$id] = ['changed' => true]; } } } // xprops doesn't dirty! unset($record->xprops()[HumanResources_Model_AttendanceRecord::META_DATA][Timetracker_Model_Timesheet::class]); } foreach ($tsData as $id => $tsd) { $ts = null; try { if (isset($tsd['changed']) || (int)($ts = $tsCtrl->get($id))->seq > (int)$tsd['seq']) { unset($tsData[$id]); if (!$ts) { $ts = $tsCtrl->get($id); } if (!$ts->need_for_clarification) { $ts->need_for_clarification = true; $tsCtrl->update($ts); } } } catch (Tinebase_Exception_NotFound $tenf) {} } if (!empty($ids = array_keys($tsData))) { $tsCtrl->delete($ids); } unset($tsCtrlRaii); } }