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