From d56503ba8baff58bac55ce6386413d2d4c7eec39 Mon Sep 17 00:00:00 2001 From: Paul Mehrer Date: Fri, 13 Sep 2024 15:49:43 +0200 Subject: [PATCH] tweak(Timetracker TS searchCount) include HR data in summary, added cleared amount property --- tests/tine20/Timetracker/JsonTest.php | 29 +++++ tine20/Addressbook/Controller/Contact.php | 6 +- tine20/Addressbook/Frontend/Cli.php | 3 +- .../Controller/DataIntendedPurposeRecord.php | 1 + tine20/HumanResources/Controller/Contract.php | 3 + tine20/HumanResources/Controller/FreeTime.php | 3 + .../Controller/WorkingTimeScheme.php | 3 + tine20/OnlyOfficeIntegrator/Controller.php | 1 + .../Controller/AccessToken.php | 1 + tine20/Tasks/Controller/Task.php | 1 + tine20/Timetracker/Backend/Timesheet.php | 3 +- tine20/Timetracker/Controller/Timesheet.php | 104 +++++++++++++++++- tine20/Timetracker/Frontend/Json.php | 7 ++ tine20/Timetracker/Model/Timesheet.php | 9 +- tine20/Timetracker/Setup/Update/17.php | 16 ++- tine20/Timetracker/Setup/setup.xml | 2 +- .../Tinebase/Controller/Record/Abstract.php | 2 +- tine20/Tinebase/Model/Filter/Date.php | 8 +- tine20/Tinebase/Model/Filter/FilterGroup.php | 55 +++++++++ 19 files changed, 239 insertions(+), 18 deletions(-) diff --git a/tests/tine20/Timetracker/JsonTest.php b/tests/tine20/Timetracker/JsonTest.php index 0e6f74238e..b8fff4a4dd 100644 --- a/tests/tine20/Timetracker/JsonTest.php +++ b/tests/tine20/Timetracker/JsonTest.php @@ -202,6 +202,35 @@ public function testSearchTimeaccountsWithTAFilter() $this->assertEquals($timeaccountData['id'], $searchResult['filter'][0]['value']['id']); } + public function testExtendedTSSearchCountData() + { + $dailyWTRTest = new HumanResources_Controller_DailyWTReportTests(); + $dailyWTRTest->setUp(); + + $dailyWTRTest->testCalculateReportsForEmployeeTimesheetsWithStartAndEnd(); + + $employee = HumanResources_Controller_Employee::getInstance()->search( + Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_Employee::class, [ + [Tinebase_Model_Filter_Abstract::FIELD => 'account_id', Tinebase_Model_Filter_Abstract::OPERATOR => Tinebase_Model_Filter_Abstract::OP_EQUALS, Tinebase_Model_Filter_Abstract::VALUE => Tinebase_Core::getUser()->getId()] + ]))->getFirstRecord(); + + $fromUntil = ['from' => new Tinebase_DateTime('2018-08-01 00:00:00'), 'until' => new Tinebase_DateTime('2018-08-31 00:00:00')]; + $contract = HumanResources_Controller_Contract::getInstance()->getValidContracts($fromUntil, $employee->getId())->getFirstRecord(); + $contract->{HumanResources_Model_Contract::FLD_YEARLY_TURNOVER_GOAL} = 100; + HumanResources_Controller_Contract::getInstance()->update($contract); + + $result = $this->_json->searchTimesheets([ + ['field' => 'start_date', 'operator' => 'within', 'value' => $fromUntil], //['from' => '2018-08-01 00:00:00', 'until' => '2018-08-31 00:00:00']], + ['field' => 'account_id', 'operator' => 'equals', 'value' => Tinebase_Core::getUser()->getId()], + ], []); + + // 31 days out of 365 + $this->assertSame(round(100 * 31 / 365, 2), $result['turnOverGoal']); + // 23 working days in august 2018 + $this->assertSame(23 * 8 * 3600, $result['workingTimeTarget']); + $this->assertSame(0, $result['clearedAmount']); + } + /** * try to get a Timesheet with a timeaccount_id filter */ diff --git a/tine20/Addressbook/Controller/Contact.php b/tine20/Addressbook/Controller/Contact.php index bd934f2716..dae6a311f3 100644 --- a/tine20/Addressbook/Controller/Contact.php +++ b/tine20/Addressbook/Controller/Contact.php @@ -358,8 +358,7 @@ protected function _inspectAfterUpdate($updatedRecord, $record, $currentRecord) array('field' => $updatedRecord->getIdProperty(), 'operator' => 'equals', 'value' => $updatedRecord->getId()) )); - // record does not match the filter, attention searchCount returns a STRING! "1"... - if ($this->searchCount($filter) != 1) { + if ($this->searchCount($filter) !== 1) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' record did not match filter of syncBackend "' . $backendId . '"'); @@ -503,8 +502,7 @@ protected function _inspectAfterCreate($_createdRecord, Tinebase_Record_Interfac array('field' => $_createdRecord->getIdProperty(), 'operator' => 'equals', 'value' => $_createdRecord->getId()) )); - // record does not match the filter, attention searchCount returns a STRING! "1"... - if ($this->searchCount($filter) != 1) { + if ($this->searchCount($filter) !== 1) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' record did not match filter of syncBackend "' . $backendId . '"'); diff --git a/tine20/Addressbook/Frontend/Cli.php b/tine20/Addressbook/Frontend/Cli.php index d76efbeed7..f392ce3c23 100644 --- a/tine20/Addressbook/Frontend/Cli.php +++ b/tine20/Addressbook/Frontend/Cli.php @@ -100,8 +100,7 @@ public function syncbackends($_opts) array('field' => $contact->getIdProperty(), 'operator' => 'equals', 'value' => $contact->getId()) )); - // record does not match the filter, attention searchCount returns a STRING! "1"... - if ($controller->searchCount($filter) != 1) { + if ($controller->searchCount($filter) !== 1) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' record did not match filter of syncBackend "' . $backendId . '"'); diff --git a/tine20/GDPR/Controller/DataIntendedPurposeRecord.php b/tine20/GDPR/Controller/DataIntendedPurposeRecord.php index 1f35455c44..db8a1e5cb7 100644 --- a/tine20/GDPR/Controller/DataIntendedPurposeRecord.php +++ b/tine20/GDPR/Controller/DataIntendedPurposeRecord.php @@ -119,6 +119,7 @@ protected function checkAgreeWithdrawDates(GDPR_Model_DataIntendedPurposeRecord if (null !== $_record->getId()) { $filter[] = [TMFA::FIELD => TMCC::ID, TMFA::OPERATOR => 'not', TMFA::VALUE => $_record->getId()]; } + /** @phpstan-ignore-next-line */ if ($this->searchCount(Tinebase_Model_Filter_FilterGroup::getFilterForModel($this->_modelName, $filter)) > 0) { throw new Tinebase_Exception_Record_Validation('agreeDate and withdrawDate must not overlap'); } diff --git a/tine20/HumanResources/Controller/Contract.php b/tine20/HumanResources/Controller/Contract.php index be9f843147..1b6440d273 100644 --- a/tine20/HumanResources/Controller/Contract.php +++ b/tine20/HumanResources/Controller/Contract.php @@ -132,6 +132,7 @@ protected function _inspectBookedResources(HumanResources_Model_Contract $contra $dwtrCtrl = HumanResources_Controller_DailyWTReport::getInstance(); $dwtrRaii = new Tinebase_RAII($dwtrCtrl->assertPublicUsage()); + /** @phpstan-ignore-next-line */ if ($ftCtrl->searchCount(Tinebase_Model_Filter_FilterGroup::getFilterForModel( HumanResources_Model_FreeTime::class, array_merge([ ['field' => 'lastday_date', 'operator' => 'after_or_equals', 'value' => $contract->start_date], @@ -143,6 +144,7 @@ protected function _inspectBookedResources(HumanResources_Model_Contract $contra return false; } + /** @phpstan-ignore-next-line */ if ($dwtrCtrl->searchCount(Tinebase_Model_Filter_FilterGroup::getFilterForModel( HumanResources_Model_DailyWTReport::class, array_merge([ ['field' => 'date', 'operator' => 'after_or_equals', 'value' => $contract->start_date], @@ -257,6 +259,7 @@ protected function _checkDateOverlap(HumanResources_Model_Contract $_record) ['field' => 'start_date', 'operator' => 'before_or_equals', 'value' => $_record->end_date], ])); + /** @phpstan-ignore-next-line */ if ($this->searchCount($filter) > 0) { $translation = Tinebase_Translation::getTranslation($this->_applicationName); throw new Tinebase_Exception_SystemGeneric($translation->_('Contracts may not overlap')); diff --git a/tine20/HumanResources/Controller/FreeTime.php b/tine20/HumanResources/Controller/FreeTime.php index f38ecf444f..d9df13e96a 100644 --- a/tine20/HumanResources/Controller/FreeTime.php +++ b/tine20/HumanResources/Controller/FreeTime.php @@ -224,6 +224,7 @@ protected function _inspect(HumanResources_Model_FreeTime $record, ?HumanResourc $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], @@ -231,6 +232,7 @@ protected function _inspect(HumanResources_Model_FreeTime $record, ?HumanResourc ['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], @@ -270,6 +272,7 @@ protected function _inspectDelete(array $_ids) $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], diff --git a/tine20/HumanResources/Controller/WorkingTimeScheme.php b/tine20/HumanResources/Controller/WorkingTimeScheme.php index ec0f40239f..1e56cabbca 100644 --- a/tine20/HumanResources/Controller/WorkingTimeScheme.php +++ b/tine20/HumanResources/Controller/WorkingTimeScheme.php @@ -91,6 +91,7 @@ protected function _checkGrant($_record, $_action, $_throw = TRUE, $_errorMessag $divisionCtrl = HumanResources_Controller_Division::getInstance(); $oldValue = $divisionCtrl->doContainerACLChecks(false); try { + /** @phpstan-ignore-next-line */ if (0 < $divisionCtrl->searchCount($filter)) { return true; } @@ -100,6 +101,7 @@ protected function _checkGrant($_record, $_action, $_throw = TRUE, $_errorMessag } // if we see a contract with this working time scheme, we do see the working time scheme + /** @phpstan-ignore-next-line */ if (0 < HumanResources_Controller_Contract::getInstance()->searchCount( Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_Contract::class, [ ['field' => HumanResources_Model_Contract::FLD_WORKING_TIME_SCHEME, 'operator' => 'equals', 'value' => $_record->getId()], @@ -150,6 +152,7 @@ public function checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_act $divisionCtrl = HumanResources_Controller_Division::getInstance(); $oldValue = $divisionCtrl->doContainerACLChecks(false); try { + /** @phpstan-ignore-next-line */ if (0 < $divisionCtrl->searchCount($filter)) { // add where type !== individual $orWrapper->addFilter( diff --git a/tine20/OnlyOfficeIntegrator/Controller.php b/tine20/OnlyOfficeIntegrator/Controller.php index e5de39bd40..89df8e04d4 100644 --- a/tine20/OnlyOfficeIntegrator/Controller.php +++ b/tine20/OnlyOfficeIntegrator/Controller.php @@ -671,6 +671,7 @@ protected function doSave($requestData, $token) } // if there was no token created in the meantime, we allow the save anyway, otherwise we save a conflict + /** @phpstan-ignore-next-line */ if (OnlyOfficeIntegrator_Controller_AccessToken::getInstance()->searchCount( Tinebase_Model_Filter_FilterGroup::getFilterForModel(OnlyOfficeIntegrator_Model_AccessToken::class, [ ['field' => OnlyOfficeIntegrator_Model_AccessToken::FLDS_TOKEN, 'operator' => 'not', diff --git a/tine20/OnlyOfficeIntegrator/Controller/AccessToken.php b/tine20/OnlyOfficeIntegrator/Controller/AccessToken.php index d891d711fc..2b85ef23ea 100644 --- a/tine20/OnlyOfficeIntegrator/Controller/AccessToken.php +++ b/tine20/OnlyOfficeIntegrator/Controller/AccessToken.php @@ -147,6 +147,7 @@ public function reactivateTokens(Tinebase_Record_RecordSet $tokens) continue; } if ($timeLimit->isLater($token->{OnlyOfficeIntegrator_Model_AccessToken::FLDS_LAST_SEEN}) || + /** @phpstan-ignore-next-line */ $this->searchCount( Tinebase_Model_Filter_FilterGroup::getFilterForModel(OnlyOfficeIntegrator_Model_AccessToken::class, [ ['field' => OnlyOfficeIntegrator_Model_AccessToken::FLDS_NODE_ID, 'operator' => 'equals', diff --git a/tine20/Tasks/Controller/Task.php b/tine20/Tasks/Controller/Task.php index 0fee88b307..1771ecdb4a 100644 --- a/tine20/Tasks/Controller/Task.php +++ b/tine20/Tasks/Controller/Task.php @@ -69,6 +69,7 @@ protected function _checkGrant($_record, $_action, $_throw = TRUE, $_errorMessag if (!$result && (self::ACTION_GET === $_action || ($_oldRecord && self::ACTION_UPDATE === $_action))) { // check attendees for Tinebase_Core::getUser()->contact_id + /** @phpstan-ignore-next-line */ $result = Tasks_Controller_Attendee::getInstance()->searchCount( Tinebase_Model_Filter_FilterGroup::getFilterForModel(Tasks_Model_Attendee::class, [ [TMFA::FIELD => Tasks_Model_Attendee::FLD_TASK_ID, TMFA::OPERATOR => TMFA::OP_EQUALS, TMFA::VALUE => $_record->getId()], diff --git a/tine20/Timetracker/Backend/Timesheet.php b/tine20/Timetracker/Backend/Timesheet.php index bce5f22c85..169be3e012 100644 --- a/tine20/Timetracker/Backend/Timesheet.php +++ b/tine20/Timetracker/Backend/Timesheet.php @@ -94,7 +94,8 @@ public function __construct($_dbAdapter = NULL, $_options = array()) $this->_additionalSearchCountCols = array( 'is_billable_combined' => null, // taken from _foreignTables 'duration' => 'duration', - 'accounting_time_billable' => null // taken from _foreignTables + 'accounting_time_billable' => null, // taken from _foreignTables + Timetracker_Model_Timesheet::FLD_CLEARED_AMOUNT => Timetracker_Model_Timesheet::FLD_CLEARED_AMOUNT, ); $this->_foreignTables['is_billable_combined']['select'] = array( diff --git a/tine20/Timetracker/Controller/Timesheet.php b/tine20/Timetracker/Controller/Timesheet.php index 0b1ed03451..8de456e050 100644 --- a/tine20/Timetracker/Controller/Timesheet.php +++ b/tine20/Timetracker/Controller/Timesheet.php @@ -6,9 +6,11 @@ * @subpackage Controller * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3 * @author Philipp Schüle - * @copyright Copyright (c) 2007-2023 Metaways Infosystems GmbH (http://www.metaways.de) + * @copyright Copyright (c) 2007-2024 Metaways Infosystems GmbH (http://www.metaways.de) */ +use Tinebase_Model_Filter_Abstract as TMFA; + /** * Timesheet controller class for Timetracker application * @@ -259,7 +261,79 @@ protected function _checkDeadline(Timetracker_Model_Timesheet $_record, $_throwE } /****************************** overwritten functions ************************/ - + + public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter, $_action = self::ACTION_GET) + { + $result = parent::searchCount($_filter, $_action); + + if (class_exists('HumanResources_Config') && Tinebase_Application::getInstance()->isInstalled(HumanResources_Config::APP_NAME, true) + && ($periodFilter = $_filter->findFilterWithoutOr('start_date')) + && ($aFilter = $_filter->findFilterWithoutOr('account_id')) && $aFilter->getOperator() === TMFA::OP_EQUALS + && ($accountId = $aFilter->toArray()['value'] ?? null)) { + $oldEmployeeAcl = HumanResources_Controller_Employee::getInstance()->doContainerACLChecks(false); + try { + $employee = HumanResources_Controller_Employee::getInstance()->search( + Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_Employee::class, [ + [TMFA::FIELD => 'account_id', TMFA::OPERATOR => TMFA::OP_EQUALS, TMFA::VALUE => $accountId] + ]))->getFirstRecord(); + } finally { + HumanResources_Controller_Employee::getInstance()->doContainerACLChecks($oldEmployeeAcl); + } + + // ATTENTION employee has been retrieved without ACL check, this code here only used the employee id. Do not use employee data unless checking ACL first! + if ($employee) { + /** @var Tinebase_Model_Filter_Date $periodFilter */ + try { + $fromUntil = ['from' => $periodFilter->getStartOfPeriod(), 'until' => $periodFilter->getEndOfPeriod()]; + } catch (Tinebase_Exception_UnexpectedValue) { + return $result; + } + $from = $fromUntil['from']; + $from->hasTime(false); + $until = $fromUntil['until']; + $until->hasTime(false); + + // get contracts + $contracts = HumanResources_Controller_Contract::getInstance()->getValidContracts($fromUntil, $employee->getId()); + $turnOverGoal = 0; + /** @var HumanResources_Model_Contract $contract */ + foreach ($contracts as $contract) { + if (0 === ($yGoal = (int)$contract->{HumanResources_Model_Contract::FLD_YEARLY_TURNOVER_GOAL})) { + continue; + } + $f = ($from->isLater($contract->start_date) ? $from : $contract->start_date)->getClone(); + $u = (!$contract->end_date || $until->isEarlier($contract->end_date) ? $until : $contract->end_date) + ->getClone(); + + $multiplier = 0.0; + for (;(int)$f->format('Y') < (int)$u->format('Y'); $f->addYear(1)) { + $daysOfYear = $f->format('L') === '1' ? 366 : 365; + $multiplier += ($daysOfYear - (int)$f->format('z')) / $daysOfYear; + $f->setDate((int)$f->format('Y'), 1, 1); + } + $daysOfYear = $u->format('L') === '1' ? 366 : 365; + $multiplier += ((int)$u->format('z') - (int)$f->format('z') + 1) / $daysOfYear; + + $turnOverGoal += round($yGoal * $multiplier, 2); + } + $result['turnOverGoal'] = $turnOverGoal; + + // get dailyWTRs + $workingTarget = 0; + /** @var HumanResources_Model_DailyWTReport $dailyWTR */ + foreach (HumanResources_Controller_DailyWTReport::getInstance()->search(Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_DailyWTReport::class, [ + [TMFA::FIELD => 'employee_id', TMFA::OPERATOR => TMFA::OP_EQUALS, TMFA::VALUE => $employee->getId()], + [TMFA::FIELD => 'date', TMFA::OPERATOR => 'within', TMFA::VALUE => ['from' => $from, 'until' => $until]], + ])) as $dailyWTR) { + $workingTarget += $dailyWTR->getShouldWorkingTime(); + } + $result['workingTimeTarget'] = $workingTarget; + } + } + + return $result; + } + /** * inspect creation of one record * @@ -273,6 +347,7 @@ protected function _inspectBeforeCreate(Tinebase_Record_Interface $_record) /** @var Timetracker_Model_Timesheet $_record */ $this->_checkDeadline($_record); $this->_calculateTimes($_record); + $this->_calcClearedAmount($_record); } protected function _inspectAfterCreate($_createdRecord, Tinebase_Record_Interface $_record) @@ -297,7 +372,7 @@ protected function _inspectBeforeUpdate($_record, $_oldRecord) /** @var Timetracker_Model_Timesheet $_record */ $this->_checkDeadline($_record); $this->_calculateTimes($_record); - + $this->_calcClearedAmount($_record, $_oldRecord); } protected function _inspectAfterUpdate($updatedRecord, $record, $currentRecord) @@ -312,6 +387,29 @@ protected function _inspectAfterUpdate($updatedRecord, $record, $currentRecord) } } + protected function _calcClearedAmount(Timetracker_Model_Timesheet $ts, ?Timetracker_Model_Timesheet $oldTs = null): void + { + if (!$ts->is_cleared) { + $ts->{Timetracker_Model_Timesheet::FLD_CLEARED_AMOUNT} = null; + return; + } + if ($oldTs?->is_cleared) { + $ts->{Timetracker_Model_Timesheet::FLD_CLEARED_AMOUNT} = $oldTs?->{Timetracker_Model_Timesheet::FLD_CLEARED_AMOUNT}; + return; + } + + $taCtrl = Timetracker_Controller_Timeaccount::getInstance(); + $oldAcl = $taCtrl->doContainerACLChecks(false); + try { + /** @var Timetracker_Model_Timeaccount $ta */ + $ta = Timetracker_Controller_Timeaccount::getInstance()->get($ts->getIdFromProperty('timeaccount_id')); + } finally { + $taCtrl->doContainerACLChecks($oldAcl); + } + + $ts->{Timetracker_Model_Timesheet::FLD_CLEARED_AMOUNT} = round(($ts->accounting_time / 60) * (int)$ta->price, 2); + } + protected function _tsChanged(Timetracker_Model_Timesheet $record, ?Timetracker_Model_Timesheet $oldRecord = null) { $event = new Tinebase_Event_Record_Update(); diff --git a/tine20/Timetracker/Frontend/Json.php b/tine20/Timetracker/Frontend/Json.php index cdfd56f0e4..612bc4c42b 100644 --- a/tine20/Timetracker/Frontend/Json.php +++ b/tine20/Timetracker/Frontend/Json.php @@ -316,6 +316,13 @@ protected function _getSearchTotalCount($filter, $pagination, $controller, $tota $totalresult['totalsum'] = $result['sum_duration']; $totalresult['totalsumbillable'] = $result['sum_accounting_time_billable']; $totalresult['totalcount'] = $result['count']; + $totalresult['clearedAmount'] = (int)$result['sum_cleared_amount']; + if (isset($result['turnOverGoal'])) { + $totalresult['turnOverGoal'] = $result['turnOverGoal']; + } + if (isset($result['workingTimeTarget'])) { + $totalresult['workingTimeTarget'] = $result['workingTimeTarget']; + } return $totalresult; } else { diff --git a/tine20/Timetracker/Model/Timesheet.php b/tine20/Timetracker/Model/Timesheet.php index 431b84b6aa..8b927ef0ba 100644 --- a/tine20/Timetracker/Model/Timesheet.php +++ b/tine20/Timetracker/Model/Timesheet.php @@ -27,6 +27,8 @@ class Timetracker_Model_Timesheet extends Tinebase_Record_Abstract implements Sa { const MODEL_NAME_PART = 'Timesheet'; + public const FLD_CLEARED_AMOUNT = 'cleared_amount'; + /** * holds the configuration object (must be declared in the concrete class) * @@ -40,7 +42,7 @@ class Timetracker_Model_Timesheet extends Tinebase_Record_Abstract implements Sa * @var array */ protected static $_modelConfiguration = array( - 'version' => 9, + 'version' => 10, 'recordName' => 'Timesheet', 'recordsName' => 'Timesheets', // ngettext('Timesheet', 'Timesheets', n) 'hasRelations' => true, @@ -199,6 +201,11 @@ class Timetracker_Model_Timesheet extends Tinebase_Record_Abstract implements Sa 'shy' => true, 'copyOmit' => true, ), + self::FLD_CLEARED_AMOUNT => [ + self::TYPE => self::TYPE_MONEY, + self::LABEL => 'Cleared amount', // _('Cleared amount) + self::NULLABLE => true, + ], // TODO combine those three fields like this? // TODO create individual fields in MC and Doctrine Mapper? how to handle filter/validators/labels/...? // 'start' => array( diff --git a/tine20/Timetracker/Setup/Update/17.php b/tine20/Timetracker/Setup/Update/17.php index 85a54889b3..bba64aad35 100644 --- a/tine20/Timetracker/Setup/Update/17.php +++ b/tine20/Timetracker/Setup/Update/17.php @@ -17,8 +17,7 @@ class Timetracker_Setup_Update_17 extends Setup_Update_Abstract public const RELEASE017_UPDATE001 = __CLASS__ . '::update001'; public const RELEASE017_UPDATE002 = __CLASS__ . '::update002'; public const RELEASE017_UPDATE003 = __CLASS__ . '::update003'; - - + public const RELEASE017_UPDATE004 = __CLASS__ . '::update004'; static protected $_allUpdates = [ self::PRIO_NORMAL_APP_STRUCTURE => [ @@ -30,6 +29,10 @@ class Timetracker_Setup_Update_17 extends Setup_Update_Abstract self::CLASS_CONST => self::class, self::FUNCTION_CONST => 'update003', ], + self::RELEASE017_UPDATE004 => [ + self::CLASS_CONST => self::class, + self::FUNCTION_CONST => 'update004', + ], ], self::PRIO_NORMAL_APP_UPDATE => [ self::RELEASE017_UPDATE000 => [ @@ -104,4 +107,13 @@ public function update003() $this->addApplicationUpdate(Timetracker_Config::APP_NAME, '17.3', self::RELEASE017_UPDATE003); } + + public function update004() + { + Setup_SchemaTool::updateSchema([ + Timetracker_Model_Timesheet::class, + ]); + + $this->addApplicationUpdate(Timetracker_Config::APP_NAME, '17.4', self::RELEASE017_UPDATE004); + } } diff --git a/tine20/Timetracker/Setup/setup.xml b/tine20/Timetracker/Setup/setup.xml index a1dcad5cdb..4e2fe99306 100644 --- a/tine20/Timetracker/Setup/setup.xml +++ b/tine20/Timetracker/Setup/setup.xml @@ -2,7 +2,7 @@ Timetracker - 17.3 + 17.4 60 enabled diff --git a/tine20/Tinebase/Controller/Record/Abstract.php b/tine20/Tinebase/Controller/Record/Abstract.php index 46c4b81ec3..9c8d7e71ea 100644 --- a/tine20/Tinebase/Controller/Record/Abstract.php +++ b/tine20/Tinebase/Controller/Record/Abstract.php @@ -360,7 +360,7 @@ protected function _addDefaultFilter(Tinebase_Model_Filter_FilterGroup $_filter * * @param Tinebase_Model_Filter_FilterGroup $_filter * @param string $_action for right/acl check - * @return int + * @return int|array */ public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter, $_action = self::ACTION_GET) { diff --git a/tine20/Tinebase/Model/Filter/Date.php b/tine20/Tinebase/Model/Filter/Date.php index dc676ac822..7f6cb1ea9d 100644 --- a/tine20/Tinebase/Model/Filter/Date.php +++ b/tine20/Tinebase/Model/Filter/Date.php @@ -360,10 +360,12 @@ public function getStartOfPeriod() */ protected function _getStartAndEndOfPeriod() { - if ($this->_operator !== 'within') { - throw new Tinebase_Exception_UnexpectedValue('only within operator supported'); + if (!in_array($this->_operator, ['within', 'inweek', Tinebase_Model_Filter_Abstract::OP_EQUALS])) { + throw new Tinebase_Exception_UnexpectedValue('only within, inweek, equals operator supported'); + } + if (is_string($value = $this->_getDateValues($this->_operator, $this->_value))) { + $value = ['from' => $value, 'until' => $value]; } - $value = $this->_getDateValues($this->_operator, $this->_value); return $value; } diff --git a/tine20/Tinebase/Model/Filter/FilterGroup.php b/tine20/Tinebase/Model/Filter/FilterGroup.php index a05fa934a2..329d443a41 100644 --- a/tine20/Tinebase/Model/Filter/FilterGroup.php +++ b/tine20/Tinebase/Model/Filter/FilterGroup.php @@ -877,6 +877,45 @@ public function getFilter($_field, $_getAll = FALSE, $_recursive = FALSE) { return $this->_findFilter($_field, $_getAll, $_recursive); } + + public function findFilterWithoutOr(string $field): Tinebase_Model_Filter_Abstract|false|null + { + $isOr = self::CONDITION_OR === $this->_concatenationCondition; + $result = null; + $filterCount = 0; + + foreach ($this->_filterObjects as $filterObject) { + if ($filterObject instanceof Tinebase_Model_Filter_FilterGroup) { + if (!$filterObject->isEmptyRecursive()) { + ++$filterCount; + if (false === ($subResult = $filterObject->findFilterWithoutOr($field))) { + return false; + } + if (null !== $subResult) { + if (null !== $result) { + return false; + } + $result = $subResult; + } + } + } else { + ++$filterCount; + /** @var Tinebase_Model_Filter_Abstract $filterObject */ + if ($filterObject->getField() === $field) { + if ($result instanceof Tinebase_Model_Filter_Abstract) { + return false; + } + $result = $filterObject; + } + } + } + + if ($isOr && $filterCount > 1 && $result instanceof Tinebase_Model_Filter_Abstract) { + $result = false; + } + + return $result; + } /** * returns filter objects @@ -1003,6 +1042,22 @@ public function isEmpty() { return empty($this->_filterObjects); } + + public function isEmptyRecursive(): bool + { + $isEmpty = true; + foreach ($this->_filterObjects as $filterObject) { + if ($filterObject instanceof Tinebase_Model_Filter_FilterGroup) { + $isEmpty = $filterObject->isEmptyRecursive(); + } else { + $isEmpty = false; + } + if (!$isEmpty) { + break; + } + } + return $isEmpty; + } /** * returns true if filter for a field is set in this group