tine20/HumanResources/Controller/DailyWTReport.php
Rama main · réplica de lectura del repositorio original.
<?php
/**
* DailyWorkingTimeReport controller for HumanResources application
*
* @package HumanResources
* @subpackage Controller
* @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
* @author Philipp Schüle <p.schuele@metaways.de>
* @copyright Copyright (c) 2018-2025 Metaways Infosystems GmbH (http://www.metaways.de)
*
*/
use Tinebase_Model_Filter_Abstract as TMFA;
/**
* DailyWorkingTimeReport controller class for HumanResources application
*
* @package HumanResources
* @subpackage Controller
*/
class HumanResources_Controller_DailyWTReport extends Tinebase_Controller_Record_Abstract
{
/** @use Tinebase_Controller_SingletonTrait<HumanResources_Controller_DailyWTReport> */
use Tinebase_Controller_SingletonTrait;
use HumanResources_Controller_CheckFilterACLEmployeeTrait;
const RC_ALLOW_IS_CLEARED = 'allowIsCleared';
const RC_JSON_REQUEST = 'jsonRequest';
/**
* @var HumanResources_Model_Employee
*/
protected $_employee = null;
/**
* @var Tinebase_DateTime
*/
protected $_startDate = null;
/**
* @var Tinebase_DateTime
*/
protected $_endDate = null;
/**
* @var Tinebase_DateTime
*/
protected $_currentDate = null;
/**
* @var Tinebase_Record_RecordSet
*/
protected $_oldReports = null;
protected $_reportResult = null;
protected $_wtsBLPipes = [];
protected $_feastDays = [];
protected $_monthlyWTR = [];
public $lastReportCalculationResult = null;
protected $_getMultipleGrant = [HumanResources_Model_DivisionGrants::READ_TIME_DATA];
protected $_requiredFilterACLget = [HumanResources_Model_DivisionGrants::READ_TIME_DATA];
protected $_requiredFilterACLupdate = [HumanResources_Model_DivisionGrants::UPDATE_TIME_DATA];
protected $_requiredFilterACLsync = [HumanResources_Model_DivisionGrants::READ_TIME_DATA];
protected $_requiredFilterACLexport = [HumanResources_Model_DivisionGrants::READ_TIME_DATA];
protected $allowCorrectionUpdate = false;
public $iterationResult;
/**
* the constructor
*
* don't use the constructor. use the singleton
*/
protected function __construct()
{
$this->_applicationName = 'HumanResources';
$this->_modelName = HumanResources_Model_DailyWTReport::class;
$this->_backend = new Tinebase_Backend_Sql(array(
'modelName' => $this->_modelName,
'tableName' => 'humanresources_wt_dailyreport',
'modlogActive' => true
));
$this->_purgeRecords = false;
$this->_resolveCustomFields = true;
$this->_doContainerACLChecks = true;
$this->_traitCheckFilterACLRight = HumanResources_Acl_Rights::MANAGE_WORKINGTIME;
}
protected function _checkGrant($_record, $_action, $_throw = TRUE, $_errorMessage = 'No Permission.', $_oldRecord = NULL)
{
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_WORKINGTIME)) {
return true;
}
switch ($_action) {
case self::ACTION_GET:
try {
HumanResources_Controller_Employee::getInstance()->get($_record->getIdFromProperty(HumanResources_Model_DailyWTReport::FLDS_EMPLOYEE_ID));
} catch (Tinebase_Exception_AccessDenied $e) {
if ($_throw) {
throw new Tinebase_Exception_AccessDenied($_errorMessage);
} else {
return false;
}
}
return true;
case self::ACTION_CREATE:
case self::ACTION_UPDATE:
case self::ACTION_DELETE:
$_action = HumanResources_Model_DivisionGrants::UPDATE_TIME_DATA;
break;
}
return parent::_checkGrant($_record, $_action, $_throw, $_errorMessage, $_oldRecord);
}
/**
* DailyWorkingTimeReports are calculated once a day by a scheduler job. New
* reports are created and all reports from this and the last month which
* don't have their is_cleared flag set get updated. Older reports can be
* created/updated manually in the UI
*
* All employees that have employment_end IS NULL or emplyoment_end AFTER now() - 2 months will be included
* in the calculation. Only days during which the employees have a valid contract will create a DailyWTReport
*
* @param bool $force
* @return boolean
*/
public function calculateAllReports($force = false)
{
if (! HumanResources_Config::getInstance()->featureEnabled(
HumanResources_Config::FEATURE_CALCULATE_DAILY_REPORTS) &&
! HumanResources_Config::getInstance()->featureEnabled(
HumanResources_Config::FEATURE_WORKING_TIME_ACCOUNTING)
) {
if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(
__METHOD__ . '::' . __LINE__ . ' FEATURE_WORKING_TIME_ACCOUNTING/FEATURE_CALCULATE_DAILY_REPORTS disabled - Skipping.'
);
return true;
}
if (false === Tinebase_Core::acquireMultiServerLock(__METHOD__)) {
return true;
}
try {
$filter = Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_Employee::class, [
['field' => 'employment_end', 'operator' => 'after', 'value' => Tinebase_DateTime::now()->subMonth(2)],
], '', [Tinebase_Model_Filter_Date::AFTER_OR_IS_NULL => true]);
$containerFilter = new Tinebase_Model_Filter_DelegatedAcl('division_id', null, null, [
'modelName' => HumanResources_Model_Employee::class
]);
$containerFilter->setRequiredGrants([HumanResources_Model_DivisionGrants::READ_TIME_DATA]);
$filter->addFilter($containerFilter);
$oFilter = Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_Employee::class, []);
$oFilter->addFilterGroup($filter);
$ids = array_merge(
HumanResources_Controller_Employee::getInstance()->search($oFilter, null, false, true),
HumanResources_Controller_Employee::getInstance()->search(
Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_Employee::class, [
['field' => 'account_id', 'operator' => 'equals', 'value' => Tinebase_Core::getUser()->getId()]
]), null, false, true)
);
$iterator = new Tinebase_Record_Iterator(array(
'iteratable' => $this,
'controller' => HumanResources_Controller_Employee::getInstance(),
'filter' => Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_Employee::class, [
['field' => 'id', 'operator' => 'in', 'value' => $ids]
]),
'function' => 'calculateReportsForEmployees',
));
$this->iterationResult = $iterator->iterate($force);
} finally {
Tinebase_Core::releaseMultiServerLock(__METHOD__);
}
return true;
}
/**
* @param Tinebase_Record_RecordSet $_records
* @param bool $force
* @return array
*/
public function calculateReportsForEmployees(Tinebase_Record_RecordSet $_records, $force = false)
{
$result = [];
foreach ($_records as $employee) {
$result[$employee->getId()] = $this->calculateReportsForEmployee($employee, null, null, $force);
}
$this->lastReportCalculationResult = $result;
return $result;
}
/**
* iterates over the dates provided or defaulting to beginning of last month until end of yesterday
* gets the existing reports for each day. If the report of a day is_cleared, the day will be skipped
* if the employee does not have a valid contract for that day, the day will be skipped
* the BLPipe from the valid contracts working time scheme will be created and fed with data
* if there was no existing report or if the report changed, it will be persisted
*
* @param HumanResources_Model_Employee $employee
* @param null|Tinebase_DateTime $startDate
* @param null|Tinebase_DateTime $endDate
* @param bool $force
* @return array
*
* @todo use an result object as return value?
*/
public function calculateReportsForEmployee(
HumanResources_Model_Employee $employee,
?\Tinebase_DateTime $startDate = null,
?\Tinebase_DateTime $endDate = null,
bool $force = false
) {
if (HumanResources_Controller_Employee::getInstance()->doContainerACLChecks()) {
if ($employee->getIdFromProperty('account_id') !== Tinebase_Core::getUser()->getId() ||
!HumanResources_Controller_Employee::getInstance()->checkGrant($employee,
HumanResources_Model_DivisionGrants::READ_OWN_DATA, false)) {
HumanResources_Controller_Employee::getInstance()->checkGrant($employee,
HumanResources_Model_DivisionGrants::READ_TIME_DATA);
}
}
$lockId = __METHOD__ . $employee->getId();
if (false === Tinebase_Core::acquireMultiServerLock($lockId)) {
return [];
}
$multiServerLockRAII = new Tinebase_RAII(function() use($lockId) {
Tinebase_Core::releaseMultiServerLock($lockId);
});
// we should never run in FE context, so we reset the RC and use RAII to restate it
$oldRC = $this->_requestContext;
$that = $this;
$this->_requestContext = [];
$rcRaii = new Tinebase_RAII(function() use ($oldRC, $that) {
$that->_requestContext = $oldRC;
});
$oldAcl = $this->doContainerACLChecks(false);
$oldMonthAcl = HumanResources_Controller_MonthlyWTReport::getInstance()->doContainerACLChecks(false);
$oldDivisionAcl = HumanResources_Controller_Division::getInstance()->doContainerACLChecks(false);
$aclRaii = new Tinebase_RAII(fn() =>
HumanResources_Controller_DailyWTReport::getInstance()->doContainerACLChecks($oldAcl) &
HumanResources_Controller_MonthlyWTReport::getInstance()->doContainerACLChecks($oldMonthAcl) &
HumanResources_Controller_Division::getInstance()->doContainerACLChecks($oldDivisionAcl)
);
// init some member vars
$this->_monthlyWTR = [];
$this->_employee = $employee;
// since startDate / endDate are somewhat flexible, better not cache to much, they may differ for different employees
$this->_feastDays = [];
// first we get all data. We do this in a transaction to get a proper snapshot
$dataReadTransaction = new Tinebase_TransactionManager_Handle();
$contractRaii = new Tinebase_RAII(HumanResources_Controller_Contract::getInstance()->assertPublicUsage());
$rs = new Tinebase_Record_RecordSet(HumanResources_Model_Employee::class, [$employee]);
Tinebase_ModelConfiguration::resolveRecordsPropertiesForRecordSet($rs,
HumanResources_Model_Employee::getConfiguration());
// expand required properties
$expander = new Tinebase_Record_Expander(HumanResources_Model_Employee::class, [
Tinebase_Record_Expander::EXPANDER_PROPERTIES => [
'contracts' => [
Tinebase_Record_Expander::EXPANDER_PROPERTIES => [
'working_time_scheme' => [Tinebase_Record_Expander::GET_DELETED => true]
],
],
],
]);
$expander->expand($rs);
unset($contractRaii);
if ($startDate && $lastClearedReport = $this->_getLastClearedWTR()) {
$lastClearedReport = (new Tinebase_DateTime($lastClearedReport->{HumanResources_Model_MonthlyWTReport::FLDS_MONTH}
. '-01 00:00:00'))->addMonth(1);
if ($lastClearedReport->isLater($startDate)) {
$lastClearedReport->hasTime(false);
$startDate = $lastClearedReport;
}
}
$this->_startDate = $startDate ?: $this->_getStartDate($force);
$this->_endDate = $endDate ?: $this->_getEndDate();
$this->_reportResult = [
'created' => 0,
'updated' => 0,
'errors' => 0,
];;
if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(
__METHOD__ . '::' . __LINE__ . ' Calculating Daily Reports for ' . $employee->getTitle()
. ' (From: ' . $this->_startDate->toString()
. ' Until: ' . $this->_endDate->toString() . ')'
);
$existingReports = $this->_getEmployeesReports();
$timeSheets = $this->_getEmployeesTimesheets();
$freeTimes = $this->_getEmployeesFreeTimes();
$wtTAid = HumanResources_Controller_WorkingTimeScheme::getInstance()
->getWorkingTimeAccount($this->_employee)->getId();
$dataReadTransaction->commit();
for ($this->_currentDate = clone $this->_startDate; $this->_endDate->isLaterOrEquals($this->_currentDate);
$this->_currentDate->addDay(1)) {
// we need those two also in an error case
$dateStr = $this->_currentDate->format('Y-m-d');
$monthlyWTR = null;
// then we calculate each day in a transaction, see dailyTransaction
try {
$dailyTransaction = new Tinebase_TransactionManager_Handle();
/** @var HumanResources_Model_DailyWTReport $oldReport */
$oldReport = null;
if (isset($existingReports[$dateStr])) {
$existingReports[$dateStr] = $this->get($existingReports[$dateStr]->getId());
$oldReport = $existingReports[$dateStr];
if ($oldReport->is_cleared) {
if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' old report for day ' .
$this->_currentDate->toString() . ' is already cleared, skipping');
$dailyTransaction->commit();
continue;
}
}
if (null === ($contract = $this->_employee->getValidContract($this->_currentDate))) {
if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE))
Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Employee ' .
$employee->getId() . ' ' . $employee->getTitle() . ' has no valid contract at ' .
$this->_currentDate->toString());
if (isset($existingReports[$dateStr])) {
/** @var HumanResources_Model_DailyWTReport $oldReport */
$oldReport = $existingReports[$dateStr]->getCleanClone();
$oldReport->calculation_failure = 1;
$oldReport->system_remark =
Tinebase_Translation::getTranslation(HumanResources_Config::APP_NAME)
->_('No valid contract for this date');
$this->update($oldReport);
$this->_reportResult['errors'] += 1;
// need to make sure monthly WTR gets recalculated, fill cache here:
$this->_getOrCreateMonthlyWTR();
}
$dailyTransaction->commit();
continue;
}
$monthlyWTR = $this->_getOrCreateMonthlyWTR();
if (false === ($blPipe = $this->_getBLPipe($contract->working_time_scheme))) {
if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ .
'::' . __LINE__ . 'employees ' . $employee->getId() . ' ' . $employee->getTitle() .
' Contract has no valid working time scheme for daily reporting at ' .
$this->_currentDate->toString());
if (isset($existingReports[$dateStr])) {
$oldReport = $existingReports[$dateStr]->getCleanClone();
$oldReport->calculation_failure = 1;
$oldReport->system_remark =
Tinebase_Translation::getTranslation(HumanResources_Config::APP_NAME)
->_('No valid blpipe for the working time scheme of this contract on this date');
$this->update($oldReport);
$this->_reportResult['errors'] += 1;
}
$dailyTransaction->commit();
continue;
}
$blPipe->recycle();
$blPipeData = new HumanResources_BL_DailyWTReport_Data();
$blPipeData->workingTimeModel = $contract->working_time_scheme;
$blPipeData->date = $this->_currentDate->getClone();
if (isset($freeTimes[$dateStr])) {
$blPipeData->freeTimes = $freeTimes[$dateStr];
}
$blPipeData->bankHoliday = $contract->feast_calendar_id ?
$this->_getFeastTimes($dateStr, $contract->feast_calendar_id) : null;
$blPipeData->result = $oldReport ? $oldReport->getCleanClone() : /* @phpstan-ignore-line */
new HumanResources_Model_DailyWTReport([
'employee_id' => $employee,
'monthlywtreport' => $monthlyWTR->getId(),
'date' => clone $this->_currentDate,
]);
$blPipeData->result->relations = new Tinebase_Record_RecordSet(Tinebase_Model_Relation::class);
$blPipeData->allowTimesheetOverlap = true;
if ($blPipe->hasInstanceOf(HumanResources_BL_DailyWTReport_BreakTime::class) ||
$blPipe->hasInstanceOf(HumanResources_BL_DailyWTReport_LimitWorkingTime::class)) {
$blPipeData->allowTimesheetOverlap = false;
}
/** @var HumanResources_BL_DailyWTReport_ConvertTsPtWtToTimeSlot $tsConvert */
$tsConvert = $blPipe->getFirstElementOfClass(HumanResources_BL_DailyWTReport_ConvertTsPtWtToTimeSlot::class);
$tsConvert->setTimeSheets(isset($timeSheets[$dateStr]) ? $timeSheets[$dateStr] : null);
$blPipe->execute($blPipeData, false);
if (null === $oldReport) {
$this->create($blPipeData->result);
$dailyTransaction->commit();
$this->_reportResult['created'] += 1;
} else {
if (!$blPipeData->result->diff($oldReport)->isEmpty()) {
$this->update($blPipeData->result);
$dailyTransaction->commit();
$this->_reportResult['updated'] += 1;
} else {
// just cleanup
$dailyTransaction->commit();
}
}
} catch (Exception $e) {
Tinebase_Exception::log($e);
Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__
. ' Could not create daily wt report for employee ' . $employee->getTitle()
. ' (Date: '. $this->_currentDate->toString() . ')');
$this->_reportResult['errors'] += 1;
if (isset($existingReports[$dateStr])) {
$oldReport = $existingReports[$dateStr]->getCleanClone();
$oldReport->calculation_failure = 1;
$oldReport->system_remark =
Tinebase_Translation::getTranslation(HumanResources_Config::APP_NAME)
->_('unexpected error: ') . $e->getMessage();
$this->update($oldReport);
} else {
if (null !== $monthlyWTR) {
$this->create(new HumanResources_Model_DailyWTReport([
'employee_id' => $employee,
'monthlywtreport' => $monthlyWTR->getId(),
'date' => clone $this->_currentDate,
'calculation_failure' => 1,
'system_remark' =>
Tinebase_Translation::getTranslation(HumanResources_Config::APP_NAME)
->_('unexpected error: ') . $e->getMessage(),
]));
} else {
Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' no monthly WTR available!');
}
}
$dailyTransaction->commit();
}
unset($dailyTransaction); // will trigger a rollback if not committed
}
if (count($this->_monthlyWTR) > 0) {
// import sort first! we need to recalculate the oldest month (that will trigger recalculation of all following months!)
ksort($this->_monthlyWTR);
HumanResources_Controller_MonthlyWTReport::getInstance()->recalculateReport(current($this->_monthlyWTR));
}
// to satisfy unused variable check
unset($rcRaii);
unset($multiServerLockRAII);
unset($aclRaii);
return $this->_reportResult;
}
/**
* @param HumanResources_Model_WorkingTimeScheme $_wts
* @return Tinebase_BL_Pipe | false
*/
protected function _getBLPipe(HumanResources_Model_WorkingTimeScheme $_wts)
{
if (!isset($this->_wtsBLPipes[$_wts->getId()])) {
$rs = new Tinebase_Record_RecordSet(HumanResources_Model_BLDailyWTReport_Config::class);
$record = new HumanResources_Model_BLDailyWTReport_Config([
HumanResources_Model_BLDailyWTReport_Config::FLDS_CLASSNAME =>
HumanResources_Model_BLDailyWTReport_ConvertTsPtWtToTimeSlot::class,
HumanResources_Model_BLDailyWTReport_Config::FLDS_CONFIG_RECORD => [],
]);
$record->runConvertToRecord();
$rs->addRecord($record);
if (! $_wts->blpipe instanceof Tinebase_Record_RecordSet) {
$_wts->blpipe = new Tinebase_Record_RecordSet(HumanResources_Model_BLDailyWTReport_Config::class);
}
$rs->merge($_wts->blpipe->getClone(true));
$record = new HumanResources_Model_BLDailyWTReport_Config([
HumanResources_Model_BLDailyWTReport_Config::FLDS_CLASSNAME =>
HumanResources_Model_BLDailyWTReport_PopulateReportConfig::class,
HumanResources_Model_BLDailyWTReport_Config::FLDS_CONFIG_RECORD => [],
]);
$record->runConvertToRecord();
$rs->addRecord($record);
$this->_wtsBLPipes[$_wts->getId()] = new Tinebase_BL_Pipe($rs);
}
return $this->_wtsBLPipes[$_wts->getId()];
}
/**
* @param string $dateStr
* @param string $feastCalendarId
* @return Tinebase_Model_BankHoliday|null
*/
protected function _getFeastTimes($dateStr, $feastCalendarId)
{
if (!isset($this->_feastDays[$feastCalendarId])) {
$this->_feastDays[$feastCalendarId] = [];
foreach (Tinebase_Controller_BankHoliday::getInstance()->search(
Tinebase_Model_Filter_FilterGroup::getFilterForModel(Tinebase_Model_BankHoliday::class, [
[TMFA::FIELD => Tinebase_Model_BankHoliday::FLD_CALENDAR_ID, TMFA::OPERATOR => 'equals', TMFA::VALUE => $feastCalendarId],
[TMFA::FIELD => Tinebase_Model_BankHoliday::FLD_DATE, TMFA::OPERATOR => 'after_or_equals', TMFA::VALUE => $this->_startDate->toString()],
[TMFA::FIELD => Tinebase_Model_BankHoliday::FLD_DATE, TMFA::OPERATOR => 'before_or_equals', TMFA::VALUE => $this->_endDate->toString()],
])) as $bankHoliday) {
$this->_feastDays[$feastCalendarId][$bankHoliday->{Tinebase_Model_BankHoliday::FLD_DATE}->format('Y-m-d')] = $bankHoliday;
}
}
return isset($this->_feastDays[$feastCalendarId][$dateStr]) ? $this->_feastDays[$feastCalendarId][$dateStr]
: null;
}
protected function _getLastClearedWTR()
{
return HumanResources_Controller_MonthlyWTReport::getInstance()->search(
Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_MonthlyWTReport::class, [
['field' => HumanResources_Model_MonthlyWTReport::FLDS_EMPLOYEE_ID, 'operator' => 'equals', 'value' => $this->_employee->getId()],
['field' => HumanResources_Model_MonthlyWTReport::FLDS_IS_CLEARED, 'operator' => 'equals', 'value' => true]
]), new Tinebase_Model_Pagination([
'sort' => HumanResources_Model_MonthlyWTReport::FLDS_MONTH,
'dir' => 'DESC',
'limit' => 1
]))->getFirstRecord();
}
/**
* @param bool $force
* @return Tinebase_DateTime
*/
protected function _getStartDate($force)
{
$default = Tinebase_Model_Filter_Date::getFirstDayOf(Tinebase_Model_Filter_Date::MONTH_LAST);
if ($lastClearedReport = $this->_getLastClearedWTR()) {
$start_date = (new Tinebase_DateTime($lastClearedReport->{HumanResources_Model_MonthlyWTReport::FLDS_MONTH}
. '-01 00:00:00'))->addMonth(1);
} elseif ($this->_employee->contracts instanceof Tinebase_Record_RecordSet && count($this->_employee->contracts) > 0) {
$this->_employee->contracts->sort('start_date');
if ($this->_employee->contracts->getFirstRecord()->start_date instanceof Tinebase_DateTime) {
$start_date = clone $this->_employee->contracts->getFirstRecord()->start_date;
} else {
return $default;
}
} else {
return $default;
}
if ($force) {
return $start_date;
}
$lastReport = HumanResources_Controller_MonthlyWTReport::getInstance()->search(
Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_MonthlyWTReport::class, [
['field' => HumanResources_Model_MonthlyWTReport::FLDS_EMPLOYEE_ID, 'operator' => 'equals', 'value' => $this->_employee->getId()],
['field' => HumanResources_Model_MonthlyWTReport::FLDS_IS_CLEARED, 'operator' => 'equals', 'value' => false]
]), new Tinebase_Model_Pagination([
'sort' => HumanResources_Model_MonthlyWTReport::FLDS_MONTH,
'dir' => 'DESC',
'limit' => 1
]))->getFirstRecord();
if ($lastReport) {
$time = ($lastReport->{HumanResources_Model_MonthlyWTReport::FLDS_LAST_CALCULATION} ?: $lastReport->creation_time)->getCLone()->subMinute(5);
if ($this->_employee->contracts->filter(function(HumanResources_Model_Contract $c) use($time) {
if ($c->last_modified_time && $c->last_modified_time->isLater($time)) return true;
return false;
})->count() === 0) {
// no contract changes
// check TS
$filterData = [
['field' => 'account_id', 'operator' => 'equals', 'value' => $this->_employee->account_id],
['field' => 'start_date', 'operator' => 'after_or_equals', 'value' => $start_date->format('Y-m-d')],
['field' => 'start_date', 'operator' => 'before_or_equals', 'value' => $this->_getEndDate()->format('Y-m-d')],
];
// fetch all timesheets of an employee that changed after last calculation and that start_date after time of interest
$filter = Tinebase_Model_Filter_FilterGroup::getFilterForModel(
Timetracker_Model_Timesheet::class,
$filterData
);
$filter->addFilterGroup(Tinebase_Model_Filter_FilterGroup::getFilterForModel(
Timetracker_Model_Timesheet::class,
[
['field' => 'last_modified_time', 'operator' => 'after_or_equals', 'value' => $time],
['field' => 'creation_time', 'operator' => 'after_or_equals', 'value' => $time],
],
'OR'
));
if (($firstTS = Timetracker_Controller_Timesheet::getInstance()->search($filter, new Tinebase_Model_Pagination([
'sort' => 'start_date',
'dir' => 'ASC',
'limit' => 1
]))->getFirstRecord()) && $firstTS->start_date->isLater($start_date)) {
$chgDataDate = $firstTS->start_date;
} else {
$chgDataDate = new Tinebase_DateTime($lastReport->month . '-01 00:00:00');
}
// check FreeTime
$filterData = [
['field' => 'employee_id', 'operator' => 'equals', 'value' => $this->_employee->getId()],
['field' => 'lastday_date', 'operator' => 'after_or_equals', 'value' => $start_date->format('Y-m-d')],
['field' => 'firstday_date', 'operator' => 'before_or_equals', 'value' => $this->_getEndDate()->format('Y-m-d')],
];
// fetch all freetimes of an employee that changed after last calculation and that lastday_date after time of interest
$filter = Tinebase_Model_Filter_FilterGroup::getFilterForModel(
HumanResources_Model_FreeTime::class,
$filterData
);
$filter->addFilterGroup(Tinebase_Model_Filter_FilterGroup::getFilterForModel(
HumanResources_Model_FreeTime::class,
[
['field' => 'last_modified_time', 'operator' => 'after_or_equals', 'value' => $time],
['field' => 'creation_time', 'operator' => 'after_or_equals', 'value' => $time],
],
'OR'
));
if (($firstFT = HumanResources_Controller_FreeTime::getInstance()->search($filter, new Tinebase_Model_Pagination([
'sort' => 'firstday_date',
'dir' => 'ASC',
'limit' => 1
]))->getFirstRecord()) && $firstFT->firstday_date->isEarlier($chgDataDate)) {
$chgDataDate = $firstFT->firstday_date;
}
$start_date = $chgDataDate;
}
}
return $start_date;
}
/**
* @return Tinebase_DateTime
*/
protected function _getEndDate()
{
return Tinebase_DateTime::now()->setTime(23,59,59);
}
/**
* @return HumanResources_Model_MonthlyWTReport
* @throws Tinebase_Exception_Record_DefinitionFailure
* @throws Tinebase_Exception_Record_Validation
*/
protected function _getOrCreateMonthlyWTR()
{
$month = $this->_currentDate->format('Y-m');
if (!isset($this->_monthlyWTR[$month])) {
$monthlyWTR = HumanResources_Controller_MonthlyWTReport::getInstance()->search(
Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_MonthlyWTReport::class, [
['field' => HumanResources_Model_MonthlyWTReport::FLDS_EMPLOYEE_ID, 'operator' => 'equals',
'value' => $this->_employee->getId()],
['field' => HumanResources_Model_MonthlyWTReport::FLDS_MONTH, 'operator' => 'equals',
'value' => $month],
]))->getFirstRecord();
if (null === $monthlyWTR) {
$monthlyWTR = HumanResources_Controller_MonthlyWTReport::getInstance()->create(
new HumanResources_Model_MonthlyWTReport([
HumanResources_Model_MonthlyWTReport::FLDS_EMPLOYEE_ID => $this->_employee->getId(),
HumanResources_Model_MonthlyWTReport::FLDS_MONTH => $month,
]));
}
$this->_monthlyWTR[$month] = $monthlyWTR;
}
return $this->_monthlyWTR[$month];
}
/**
* returns the employees DailyWTReports within interval as an array indexed by Y-m-d
*
* @return array
*/
protected function _getEmployeesReports()
{
$filter = Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_DailyWTReport::class, [
['field' => 'employee_id', 'operator' => 'AND', 'value' => [
['field' => ':id', 'operator' => 'equals', 'value' => $this->_employee->getId()]
]],
['field' => 'date', 'operator' => 'after_or_equals', 'value' => $this->_startDate->format('Y-m-d')],
['field' => 'date', 'operator' => 'before_or_equals', 'value' => $this->_endDate->format('Y-m-d')],
]);
$result = [];
/** @var HumanResources_Model_DailyWTReport $dwtr */
foreach (HumanResources_Controller_DailyWTReport::getInstance()->search($filter, null, new Tinebase_Record_Expander(
HumanResources_Model_DailyWTReport::class, [
Tinebase_Record_Expander::EXPANDER_PROPERTIES => [
'employee_id' => [],
]
]
)) as $dwtr) {
$dwtr->relations = Tinebase_Relations::getInstance()->getRelations(
HumanResources_Model_DailyWTReport::class,
Tinebase_Model_Relation::DEFAULT_RECORD_BACKEND,
$dwtr->getId());
$result[$dwtr->date->format('Y-m-d')] = $dwtr;
}
return $result;
}
/**
* returns the employees timesheets within interval as an array of RecordSets indexed by Y-m-d
*
* @return array
*/
protected function _getEmployeesTimesheets()
{
$filterData = [
['field' => 'account_id', 'operator' => 'equals', 'value' => $this->_employee->account_id],
['field' => 'process_status', 'operator' => 'equals', 'value' => Timetracker_Config::TS_PROCESS_STATUS_ACCEPTED],
['field' => 'start_date', 'operator' => 'after_or_equals', 'value' => $this->_startDate->format('Y-m-d')],
['field' => 'start_date', 'operator' => 'before_or_equals', 'value' => $this->_endDate->format('Y-m-d')],
];
// fetch all accepted timesheets of an employee within current start/end date
$filter = Tinebase_Model_Filter_FilterGroup::getFilterForModel(
Timetracker_Model_Timesheet::class,
$filterData
);
$taPublicUsage = new Tinebase_RAII(Timetracker_Controller_Timeaccount::getInstance()->assertPublicUsage());
$tsCtrl = Timetracker_Controller_Timesheet::getInstance();
$tsPublicUsage = new Tinebase_RAII($tsCtrl->assertPublicUsage());
$timeSheets = $tsCtrl->search($filter);
(new Tinebase_Record_Expander(Timetracker_Model_Timesheet::class, [
Tinebase_Record_Expander::EXPANDER_PROPERTIES => [
HumanResources_Model_FreeTimeType::TT_TS_SYSCF_ABSENCE_REASON => [],
HumanResources_Model_FreeTimeType::TT_TS_SYSCF_CLOCK_OUT_REASON => [],
],
]))->expand($timeSheets);
// remove timesheets that have an absence reason with no wage_type assigned
$timeSheets = $timeSheets->filter(function(Timetracker_Model_Timesheet $ts) {
return empty($ts->{HumanResources_Model_FreeTimeType::TT_TS_SYSCF_ABSENCE_REASON}) ||
!empty($ts->{HumanResources_Model_FreeTimeType::TT_TS_SYSCF_ABSENCE_REASON}->wage_type);
});
$result = [];
/** @var Timetracker_Model_Timesheet $ts */
foreach ($timeSheets as $ts) {
$day = $ts->start_date->format('Y-m-d');
if (!isset($result[$day])) {
$result[$day] = new Tinebase_Record_RecordSet(Timetracker_Model_Timesheet::class, []);
}
$result[$day]->addRecord($ts);
}
unset($taPublicUsage);
unset($tsPublicUsage);
return $result;
}
/**
* returns the employees timesheets within interval as an array of RecordSets indexed by Y-m-d
*
* @return array
*/
protected function _getEmployeesFreeTimes()
{
$start = $this->_startDate->format('Y-m-d');
$end = $this->_endDate->format('Y-m-d');
$filterData = [
['field' => 'employee_id', 'operator' => 'equals', 'value' => $this->_employee->getId()],
['field' => 'lastday_date', 'operator' => 'after_or_equals', 'value' => $start],
['field' => 'firstday_date', 'operator' => 'before_or_equals', 'value' => $end],
['field' => HumanResources_Model_FreeTime::FLD_PROCESS_STATUS, 'operator' => 'equals', 'value' => HumanResources_Config::FREE_TIME_PROCESS_STATUS_ACCEPTED],
];
// fetch all freetime of an employee between start and end
$filter = Tinebase_Model_Filter_FilterGroup::getFilterForModel(
HumanResources_Model_FreeTimeFilter::class,
$filterData
);
$freeTimes = HumanResources_Controller_FreeTime::getInstance()->search($filter);
if ($freeTimes->count() === 0) return [];
Tinebase_ModelConfiguration::resolveRecordsPropertiesForRecordSet($freeTimes,
HumanResources_Model_FreeTime::getConfiguration());
// expand required properties
$expander = new Tinebase_Record_Expander(HumanResources_Model_FreeTime::class, [
Tinebase_Record_Expander::EXPANDER_PROPERTIES => [
'type' => [],
],
]);
$expander->expand($freeTimes);
$result = [];
/** @var HumanResources_Model_FreeTime $ft */
foreach ($freeTimes as $ft) {
/** @var HumanResources_Model_FreeDay $fd */
foreach ($ft->freedays as $fd) {
$day = $fd->date->format('Y-m-d');
if (!$fd->sickoverwrite && $day >= $start && $day <= $end) {
if (!isset($result[$day])) {
$result[$day] = new Tinebase_Record_RecordSet(HumanResources_Model_FreeTime::class, []);
}
$result[$day]->addRecord($ft);
}
}
}
return $result;
}
/**
* inspect creation of one record (before create)
*
* @param Tinebase_Record_Interface $_record
* @return void
*/
protected function _inspectBeforeCreate(Tinebase_Record_Interface $_record)
{
if (isset($this->_requestContext[self::RC_JSON_REQUEST])) {
// _("daily wt reports can't be created")
throw new Tinebase_Exception_SystemGeneric("daily wt reports can't be created", 600, HumanResources_Config::APP_NAME);
}
}
/**
* inspect update of one record (before update)
*
* @param Tinebase_Record_Interface $_record the update record
* @param Tinebase_Record_Interface $_oldRecord the current persistent record
* @return void
* @throws Tinebase_Exception_Record_NotAllowed
*/
protected function _inspectBeforeUpdate($_record, $_oldRecord)
{
parent::_inspectBeforeUpdate($_record, $_oldRecord);
if (isset($this->_requestContext[self::RC_JSON_REQUEST])) {
$allowedProperties = [
'evaluation_period_start_correction' => true,
'evaluation_period_end_correction' => true,
'working_time_target_correction' => true,
'user_remark' => true,
];
if ($this->allowCorrectionUpdate) {
$allowedProperties[HumanResources_Model_MonthlyWTReport::FLDS_WORKING_TIME_CORRECTION] = true;
}
foreach ($_record->getFields() as $prop) {
if (!isset($allowedProperties[$prop])) {
$_record->{$prop} = $_oldRecord->{$prop};
}
}
}
if (($_record->is_cleared || $_oldRecord->is_cleared) && (!isset($this->_requestContext[self::RC_ALLOW_IS_CLEARED]) ||
!$this->_requestContext[self::RC_ALLOW_IS_CLEARED])) {
// _('It is not allowed to update a cleared report')
throw new Tinebase_Exception_SystemGeneric('It is not allowed to update a cleared report', 600, HumanResources_Config::APP_NAME);
}
}
/**
* inspect update of one record (after update)
*
* @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 _inspectAfterUpdate($updatedRecord, $record, $currentRecord)
{
foreach (['evaluation_period_start_correction',
'evaluation_period_end_correction',
'working_time_correction',
'working_time_target_correction'] as $prop) {
if ($currentRecord->{$prop} !== $updatedRecord->{$prop}) {
$employee = HumanResources_Controller_Employee::getInstance()->get($updatedRecord->employee_id);
$this->calculateReportsForEmployee($employee, $updatedRecord->date, $updatedRecord->date);
break;
}
}
}
public function recalcCorrection(string $id)
{
$correction = 0;
foreach (HumanResources_Controller_WTRCorrection::getInstance()->search(
Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_WTRCorrection::class,[
['field' => HumanResources_Model_WTRCorrection::FLD_WTR_DAILY, 'operator' => 'equals', 'value' => $id],
['field' => HumanResources_Model_WTRCorrection::FLD_STATUS, 'operator' => 'equals', 'value' => HumanResources_Config::WTR_CORRECTION_STATUS_ACCEPTED],
])) as $c) {
$correction += intval($c->{HumanResources_Model_WTRCorrection::FLD_CORRECTION});
}
$oldAclVal = $this->doContainerACLChecks(false);
$oldCorrectionVal = $this->allowCorrectionUpdate;
$this->allowCorrectionUpdate = true;
$raii = new Tinebase_RAII(function() use($oldAclVal, $oldCorrectionVal) {
$this->doContainerACLChecks($oldAclVal);
$this->allowCorrectionUpdate = $oldCorrectionVal;
});
$record = $this->get($id);
if (intval($record->{HumanResources_Model_MonthlyWTReport::FLDS_WORKING_TIME_CORRECTION}) !== $correction) {
$record->{HumanResources_Model_MonthlyWTReport::FLDS_WORKING_TIME_CORRECTION} = $correction;
$this->update($record);
}
unset($raii);
}
/**
* implement logic for each controller in this function
*
* @param Tinebase_Event_Abstract $_eventObject
*/
protected function _handleEvent(Tinebase_Event_Abstract $_eventObject)
{
switch (get_class($_eventObject)) {
case Tinebase_Event_Record_Delete::class:
if ($_eventObject->observable instanceof Timetracker_Model_Timesheet) {
if (!HumanResources_Config::getInstance()->featureEnabled(
HumanResources_Config::FEATURE_CALCULATE_DAILY_REPORTS) &&
!HumanResources_Config::getInstance()->featureEnabled(
HumanResources_Config::FEATURE_WORKING_TIME_ACCOUNTING)
) {
if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(
__METHOD__ . '::' . __LINE__ . ' FEATURE_WORKING_TIME_ACCOUNTING/FEATURE_CALCULATE_DAILY_REPORTS disabled - Skipping.'
);
break;
}
$accountId = $_eventObject->observable->getIdFromProperty('account_id');
static $earliestDeletionDate = [];
if (!isset($earliestDeletionDate[$accountId])) {
$earliestDeletionDate[$accountId] = $_eventObject->observable->start_date;
Tinebase_TransactionManager::getInstance()->registerAfterCommitCallback(function() use(&$earliestDeletionDate) {
// context async / sync
$context = (array)HumanResources_Controller_DailyWTReport::getInstance()->getRequestContext();
if (isset($context['tsSyncron'])) {
foreach ($earliestDeletionDate as $accountId => $date) {
$this->calculateReportsForAccountId($accountId, $date);
}
} else {
foreach ($earliestDeletionDate as $accountId => $date) {
Tinebase_ActionQueue::getInstance()->queueAction(HumanResources_Controller_DailyWTReport::class
. '.calculateReportsForAccountId', $accountId, $date);
}
}
});
} elseif ($earliestDeletionDate[$accountId]->isEarlier($_eventObject->observable->start_date)) {
$earliestDeletionDate[$accountId] = $_eventObject->observable->start_date;
}
}
break;
case Tinebase_Event_Record_Update::class:
if ($_eventObject->observable instanceof Timetracker_Model_Timesheet) {
if (! HumanResources_Config::getInstance()->featureEnabled(
HumanResources_Config::FEATURE_CALCULATE_DAILY_REPORTS) &&
! HumanResources_Config::getInstance()->featureEnabled(
HumanResources_Config::FEATURE_WORKING_TIME_ACCOUNTING)
) {
if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(
__METHOD__ . '::' . __LINE__ . ' FEATURE_WORKING_TIME_ACCOUNTING/FEATURE_CALCULATE_DAILY_REPORTS disabled - Skipping.'
);
break;
}
if (Timetracker_Controller_Timesheet::getInstance()->isTSDateChanged($_eventObject->observable, $_eventObject->oldRecord)) {
$date = $_eventObject->oldRecord ?
($_eventObject->oldRecord->start_date->isEarlier($_eventObject->observable->start_date) ?
$_eventObject->oldRecord->start_date : $_eventObject->observable->start_date)
: $_eventObject->observable->start_date;
// context async / sync
$context = (array)HumanResources_Controller_DailyWTReport::getInstance()->getRequestContext();
if (isset($context['tsSyncron'])) {
$this->calculateReportsForAccountId($_eventObject->observable->account_id, $date);
} else {
Tinebase_ActionQueue::getInstance()->queueAction(HumanResources_Controller_DailyWTReport::class
. '.calculateReportsForAccountId', $_eventObject->observable->account_id, $date);
}
}
} elseif ($_eventObject->observable instanceof HumanResources_Model_FreeTime) {
/* order matters start */
$tsRaii = new Tinebase_RAII(Timetracker_Controller_Timesheet::getInstance()->assertPublicUsage());
$mRaii = new Tinebase_RAII(HumanResources_Controller_MonthlyWTReport::getInstance()->assertPublicUsage());
$dRaii = new Tinebase_RAII($this->assertPublicUsage());
/* order matters end */
$this->calculateReportsForEmployee(HumanResources_Controller_Employee::getInstance()->get(
$_eventObject->observable->getIdFromProperty('employee_id')), $_eventObject->observable->start_date);
/* order matters start */
unset($dRaii);
unset($mRaii);
unset($tsRaii);
/* order matters end */
}
break;
}
}
public function calculateReportsForAccountId(string $accountId, string $date): void
{
$oldUser = Tinebase_Core::getUser();
if ($oldUser->accountLoginName !== Tinebase_User::SYSTEM_USER_CRON) {
$oldUserRaii = new Tinebase_RAII(function() use($oldUser) {
Tinebase_Core::setUser($oldUser);
});
$cronUser = Tinebase_User::createSystemUser(Tinebase_User::SYSTEM_USER_CRON);
Tinebase_Core::setUser($cronUser);
}
/** @var HumanResources_Model_Employee $employee */
$employee = HumanResources_Controller_Employee::getInstance()->search(Tinebase_Model_Filter_FilterGroup::getFilterForModel(
HumanResources_Model_Employee::class, [
['field' => 'account_id', 'operator' => 'equals', 'value' => $accountId],
]
))->getFirstRecord();
if ($employee) {
$this->calculateReportsForEmployee($employee, new Tinebase_DateTime($date));
}
unset($oldUserRaii);
}
}