tine20/HumanResources/BL/AttendanceRecorder/TimeSheet.php
Rama main · réplica de lectura del repositorio original.
<?php declare(strict_types=1);
/**
* Tine 2.0
*
* @package HumanResources
* @subpackage BL
* @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
* @author Paul Mehrer <p.mehrer@metaways.de>
* @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);
}
}