Ctrl K
ComunidadGuías…

tine20/HumanResources/Controller/DailyWTReport.php

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<?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);
    }
}