* @copyright Copyright (c) 2012-2023 Metaways Infosystems GmbH (http://www.metaways.de) * */ /** * FreeTime controller class for HumanResources application * * @package HumanResources * @subpackage Controller */ class HumanResources_Controller_FreeTime extends Tinebase_Controller_Record_Abstract { /** @use Tinebase_Controller_SingletonTrait */ use Tinebase_Controller_SingletonTrait; use HumanResources_Controller_CheckFilterACLEmployeeTrait; protected $_getMultipleGrant = [HumanResources_Model_DivisionGrants::READ_CHANGE_REQUEST]; protected $_requiredFilterACLget = [HumanResources_Model_DivisionGrants::READ_CHANGE_REQUEST]; protected $_requiredFilterACLupdate = [HumanResources_Model_DivisionGrants::UPDATE_CHANGE_REQUEST]; protected $_requiredFilterACLsync = [HumanResources_Model_DivisionGrants::READ_CHANGE_REQUEST]; protected $_requiredFilterACLexport = [HumanResources_Model_DivisionGrants::READ_CHANGE_REQUEST]; /** * the constructor * * don't use the constructor. use the singleton */ protected function __construct() { $this->_applicationName = 'HumanResources'; $this->_backend = new HumanResources_Backend_FreeTime(); $this->_modelName = HumanResources_Model_FreeTime::class; $this->_purgeRecords = TRUE; // activate this if you want to use containers $this->_doContainerACLChecks = true; $this->_traitGetOwnGrants = [ HumanResources_Model_DivisionGrants::READ_OWN_DATA, HumanResources_Model_DivisionGrants::CREATE_OWN_CHANGE_REQUEST ]; } protected function _checkGrant($_record, $_action, $_throw = TRUE, $_errorMessage = 'No Permission.', $_oldRecord = NULL) { /** @var HumanResources_Model_FreeTime $_record */ if (!$this->_doContainerACLChecks) { return true; } // if we have manage_employee right, we have all grants if (Tinebase_Core::getUser()->hasRight(HumanResources_Config::APP_NAME, HumanResources_Acl_Rights::MANAGE_EMPLOYEE)) { return true; } if (parent::_checkGrant($_record, HumanResources_Model_DivisionGrants::UPDATE_EMPLOYEE_DATA, false) || parent::_checkGrant($_record, HumanResources_Model_DivisionGrants::GRANT_ADMIN, false)) { return true; } if (self::ACTION_DELETE !== $_action && parent::_checkGrant($_record, HumanResources_Model_DivisionGrants::UPDATE_CHANGE_REQUEST, false)) { return true; } switch ($_action) { case self::ACTION_GET: if (parent::_checkGrant($_record, HumanResources_Model_DivisionGrants::READ_CHANGE_REQUEST, false) || ($this->_checkOwnEmployee($_record) && (parent::_checkGrant($_record, HumanResources_Model_DivisionGrants::CREATE_OWN_CHANGE_REQUEST, false) || parent::_checkGrant($_record, HumanResources_Model_DivisionGrants::READ_OWN_DATA, false) ))) { return true; } break; case self::ACTION_CREATE: case self::ACTION_UPDATE: case self::ACTION_DELETE: if (HumanResources_Config::FREE_TIME_PROCESS_STATUS_REQUESTED === $_record->{HumanResources_Model_FreeTime::FLD_PROCESS_STATUS} && (parent::_checkGrant($_record, HumanResources_Model_DivisionGrants::CREATE_CHANGE_REQUEST, false) || (parent::_checkGrant($_record, HumanResources_Model_DivisionGrants::CREATE_OWN_CHANGE_REQUEST, false) && $this->_checkOwnEmployee($_record)))) { return true; } break; case self::ACTION_DELETE: break; } throw new Tinebase_Exception_AccessDenied($_errorMessage); } protected function _checkOwnEmployee(HumanResources_Model_FreeTime $record): bool { $ctrl = HumanResources_Controller_Employee::getInstance(); $oldValue = $ctrl->doContainerACLChecks(false); $raii = new Tinebase_RAII(function() use($oldValue, $ctrl) { $ctrl->doContainerACLChecks($oldValue); }); $result = $ctrl->get($record->getIdFromProperty('employee_id'))->account_id === Tinebase_Core::getUser()->getId(); unset($raii); return $result; } /** * returns remaining vacation days for given employee (mixed accounts) * * @param string|HumanResources_Model_Employee $employeeId * @param DateTime $actualUntil | vacation days are computed as taken vacation until this date, null means forever (all scheduled days count as taken) * @param DateTime $expiryReferenceDate | reference day for vacation_expiary_date evaluation * @return int */ public function getRemainingVacationDays($employeeId, ?\DateTime $actualUntil = null, ?\DateTime $expiryReferenceDate = null) { $accountController = HumanResources_Controller_Account::getInstance(); $currentAccountYear = ($actualUntil ?: Tinebase_DateTime::now())->format('Y'); $currentAccount = $accountController->getByEmployeeYear($employeeId, $currentAccountYear); $actualUntil = $actualUntil ?: Tinebase_DateTime::now()->addYear(100); $expiryReferenceDate = $expiryReferenceDate ?: Tinebase_DateTime::now(); $currentVacations = $currentAccount ? $accountController->resolveVacation($currentAccount, $actualUntil) : ['actual_remaining_vacation_days' => 0]; $remainingPreviousVacationDays = 0; $previousAccount = $accountController->getByEmployeeYear($employeeId, --$currentAccountYear); if ($previousAccount) { $previousVacations = $accountController->resolveVacation($previousAccount, $actualUntil); $remainingPreviousVacationDays = ($previousVacations['vacation_expiary_date'] > $expiryReferenceDate ? $previousVacations['actual_remaining_vacation_days'] : 0); } return $currentVacations['actual_remaining_vacation_days'] + $remainingPreviousVacationDays; } /** * returns taken vacation days for given period & employee (mixed accounts) * * @param string|HumanResources_Model_Employee $employeeId * @param DateTime[] $period [from => ..., until => ...] * @return Tinebase_Record_RecordSet of HumanResources_Model_FreeDay */ public function getTakenVacationDays($employeeId, $period) { $employeeId = $employeeId instanceof HumanResources_Model_Employee ? $employeeId->getId() : $employeeId; $freeTimes = $this->search(new HumanResources_Model_FreeTimeFilter([ ['field' => 'employee_id', 'operator' => 'equals', 'value' => $employeeId], ['field' => 'type', 'operator' => 'equals', 'value' => HumanResources_Model_FreeTimeType::ID_VACATION], ['field' => HumanResources_Model_FreeTime::FLD_PROCESS_STATUS, 'operator' => 'equals', 'value' => HumanResources_Config::FREE_TIME_PROCESS_STATUS_ACCEPTED], ])); return HumanResources_Controller_FreeDay::getInstance()->search(new HumanResources_Model_FreeDayFilter([ ['field' => 'freetime_id', 'operator' => 'equals', 'value' => $freeTimes->getArrayOfIds()], ['field' => 'date', 'operator' => 'within', 'value' => $period] ])); } /** * inspect update of one record (after setReleatedData) * * @param Tinebase_Record_Interface $updatedRecord the just updated record * @param Tinebase_Record_Interface $record the update record * @param Tinebase_Record_Interface $currentRecord the current record (before update) * @return void */ protected function _inspectAfterSetRelatedDataUpdate($updatedRecord, $record, $currentRecord) { $this->_inspect($updatedRecord, $currentRecord); if ($updatedRecord->{HumanResources_Model_FreeTime::FLD_PROCESS_STATUS} !== $currentRecord->{HumanResources_Model_FreeTime::FLD_PROCESS_STATUS}) { foreach ($updatedRecord->freedays as $freeday) { HumanResources_Controller_FreeDay::getInstance()->inspectFreeDay($freeday); } } } /** * inspect create of one record (after setReleatedData) * * @param Tinebase_Record_Interface $createdRecord the just updated record * @param Tinebase_Record_Interface $record the update record * @return void */ protected function _inspectAfterSetRelatedDataCreate($createdRecord, $record) { $this->_inspect($createdRecord); } protected function _inspect(HumanResources_Model_FreeTime $record, ?HumanResources_Model_FreeTime $oldRecord = null) { if (!$record->freedays instanceof Tinebase_Record_RecordSet) { $record->freedays = HumanResources_Controller_FreeDay::getInstance()->search( Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_FreeDay::class, [ ['field' => 'freetime_id', 'operator' => 'equals', 'value' => $record->getId()], ]) ); } $freeDays = $record->freedays; // update first and last date $freeDays->sort('date', 'ASC'); $record->firstday_date = clone $freeDays->getFirstRecord()->date; $record->lastday_date = clone $freeDays->getLastRecord()->date; $ftt = HumanResources_Controller_FreeTimeType::getInstance()->get($record->getIdFromProperty('type')); if (in_array($ftt->getIdFromProperty('wage_type'), [ HumanResources_Model_WageType::ID_SICK, HumanResources_Model_WageType::ID_SICK_CHILD, HumanResources_Model_WageType::ID_SICK_SICKPAY, ])) { $freeDays->filter('sickoverwrite', true)->sickoverwrite = false; if ((null === $oldRecord && HumanResources_Config::FREE_TIME_PROCESS_STATUS_ACCEPTED === $record->{HumanResources_Model_FreeTime::FLD_PROCESS_STATUS}) || ($oldRecord?->{HumanResources_Model_FreeTime::FLD_PROCESS_STATUS} !== $record->{HumanResources_Model_FreeTime::FLD_PROCESS_STATUS} && ( HumanResources_Config::FREE_TIME_PROCESS_STATUS_ACCEPTED === $record->{HumanResources_Model_FreeTime::FLD_PROCESS_STATUS} || HumanResources_Config::FREE_TIME_PROCESS_STATUS_ACCEPTED === $oldRecord?->{HumanResources_Model_FreeTime::FLD_PROCESS_STATUS} ))) { $this->_handleOverwrittenVacation($record, $oldRecord); } } elseif (!empty($freeDays)) { $this->_markSickOverwrites($record); } foreach ($freeDays as $freeDay) { if ($freeDay->isDirty()) { HumanResources_Controller_FreeDay::getInstance()->update($freeDay); } } $record->days_count = $freeDays->filter('sickoverwrite', false)->count(); $this->_backend->update($record); $dwtrCtrl = HumanResources_Controller_DailyWTReport::getInstance(); $dwtrRaii = new Tinebase_RAII($dwtrCtrl->assertPublicUsage()); /** @phpstan-ignore-next-line */ if (($oldRecord && $dwtrCtrl->searchCount(Tinebase_Model_Filter_FilterGroup::getFilterForModel( HumanResources_Model_DailyWTReport::class,[ ['field' => 'date', 'operator' => 'after_or_equals', 'value' => $oldRecord->firstday_date], ['field' => 'employee_id', 'operator' => 'equals', 'value' => $oldRecord->employee_id], ['field' => 'is_cleared', 'operator' => 'equals', 'value' => true], ['field' => 'date', 'operator' => 'before_or_equals', 'value' => $oldRecord->lastday_date], ])) > 0) || /** @phpstan-ignore-next-line */ $dwtrCtrl->searchCount(Tinebase_Model_Filter_FilterGroup::getFilterForModel( HumanResources_Model_DailyWTReport::class,[ ['field' => 'date', 'operator' => 'after_or_equals', 'value' => $record->firstday_date], ['field' => 'employee_id', 'operator' => 'equals', 'value' => $record->employee_id], ['field' => 'is_cleared', 'operator' => 'equals', 'value' => true], ['field' => 'date', 'operator' => 'before_or_equals', 'value' => $record->lastday_date], ])) > 0) { throw new Tinebase_Exception_SystemGeneric('wtr during affected time is already booked, no changes possible anymore'); // _('wtr during affected time is already booked, no changes possible anymore') } unset($dwtrRaii); if ((null === $oldRecord && $record->{HumanResources_Model_FreeTime::FLD_PROCESS_STATUS} !== \HumanResources_Config::FREE_TIME_PROCESS_STATUS_ACCEPTED) || ($oldRecord && $oldRecord->{HumanResources_Model_FreeTime::FLD_PROCESS_STATUS} !== \HumanResources_Config::FREE_TIME_PROCESS_STATUS_ACCEPTED && $record->{HumanResources_Model_FreeTime::FLD_PROCESS_STATUS} !== \HumanResources_Config::FREE_TIME_PROCESS_STATUS_ACCEPTED)) { return; } $event = new Tinebase_Event_Record_Update(); $event->observable = clone $record; if ($oldRecord && $oldRecord->firstday_date && (!$record->firstday_date || $oldRecord->firstday_date->isEarlier($record->firstday_date))) { $event->observable->firstday_date = clone $oldRecord->firstday_date; } Tinebase_TransactionManager::getInstance()->registerAfterCommitCallback(function() use($event) { Tinebase_Record_PersistentObserver::getInstance()->fireEvent($event); }); } protected function _inspectDelete(array $_ids) { $_ids = parent::_inspectDelete($_ids); $dwtrCtrl = HumanResources_Controller_DailyWTReport::getInstance(); $dwtrRaii = new Tinebase_RAII($dwtrCtrl->assertPublicUsage()); /** @var HumanResources_Model_FreeTime $freeTime */ foreach ($this->getMultiple($_ids, true) as $freeTime) { /** @phpstan-ignore-next-line */ if ($dwtrCtrl->searchCount(Tinebase_Model_Filter_FilterGroup::getFilterForModel( HumanResources_Model_DailyWTReport::class,[ ['field' => 'date', 'operator' => 'after_or_equals', 'value' => $freeTime->firstday_date], ['field' => 'employee_id', 'operator' => 'equals', 'value' => $freeTime->employee_id], ['field' => 'is_cleared', 'operator' => 'equals', 'value' => true], ['field' => 'date', 'operator' => 'before_or_equals', 'value' => $freeTime->lastday_date], ])) > 0) { unset($_ids[array_search($freeTime->getId(), $_ids)]); } } unset($dwtrRaii); return $_ids; } /** * inspect creation of one record (before create) * * @param Tinebase_Record_Interface $_record * @return void */ protected function _inspectBeforeCreate(Tinebase_Record_Interface $_record) { if (is_array($_record->employee_id)) { $_record->employee_id = $_record->employee_id['id']; } } protected function _markSickOverwrites(HumanResources_Model_FreeTime $_record) { if (!$_record->freedays instanceof Tinebase_Record_RecordSet || $_record->freedays->count() === 0) { return; } $sickDays = HumanResources_Controller_FreeDay::getInstance()->search( Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_FreeDay::class, [ ['field' => 'freetime_id', 'operator' => 'definedBy', 'value' => [ ['field' => 'type', 'operator' => 'definedBy', 'value' => [ ['field' => 'wage_type', 'operator' => 'in', 'value' => [ HumanResources_Model_WageType::ID_SICK, HumanResources_Model_WageType::ID_SICK_CHILD, HumanResources_Model_WageType::ID_SICK_SICKPAY, ]], ]], ['field' => 'employee_id', 'operator' => 'equals', 'value' => $_record->getIdFromProperty('employee_id')], ['field' => 'firstday_date', 'operator' => 'before_or_equals', 'value' => $_record->lastday_date], ['field' => 'lastday_date', 'operator' => 'after_or_equals', 'value' => $_record->firstday_date], ['field' => 'id', 'operator' => 'not', 'value' => $_record->getId()], ['field' => HumanResources_Model_FreeTime::FLD_PROCESS_STATUS, 'operator' => 'equals', 'value' => HumanResources_Config::FREE_TIME_PROCESS_STATUS_ACCEPTED], ]], ]), null, false, ['date']); foreach ($_record->freedays as $freeday) { if (isset($sickDays[$freeday->date->format('Y-m-d')])) { if (!$freeday->sickoverwrite) { $freeday->sickoverwrite = true; } } elseif ($freeday->sickoverwrite) { $freeday->sickoverwrite = false; } } } /** * finds non sickness free days within the range of the given sickness records. * then updates them, that will mark their days properly as overwritten or not */ protected function _handleOverwrittenVacation(HumanResources_Model_FreeTime $_record, ?HumanResources_Model_FreeTime $_oldRecord) { if (!$_oldRecord || !$_oldRecord->lastday_date || ($_record->lastday_date && $_oldRecord->lastday_date < $_record->lastday_date)) { $lastday_date = $_record->lastday_date; } else { $lastday_date = $_oldRecord->lastday_date; } if (!$_oldRecord || !$_oldRecord->firstday_date || ($_record->firstday_date && $_oldRecord->firstday_date > $_record->firstday_date)) { $firstday_date = $_record->firstday_date; } else { $firstday_date = $_oldRecord->firstday_date; } foreach ($this->search( Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_FreeTime::class, [ ['field' => 'type', 'operator' => 'definedBy', 'value' => [ ['field' => 'wage_type', 'operator' => 'notin', 'value' => [ HumanResources_Model_WageType::ID_SICK, HumanResources_Model_WageType::ID_SICK_CHILD, HumanResources_Model_WageType::ID_SICK_SICKPAY, ]], ]], ['field' => 'employee_id', 'operator' => 'equals', 'value' => $_record->getIdFromProperty('employee_id')], ['field' => 'firstday_date', 'operator' => 'before_or_equals', 'value' => $lastday_date], ['field' => 'lastday_date', 'operator' => 'after_or_equals', 'value' => $firstday_date], ['field' => 'id', 'operator' => 'not', 'value' => $_record->getId()], ])) as $freeTime) { $this->update($freeTime); } } }