From 60a49cd18e68e4dbc201f62a1128f03c7636df07 Mon Sep 17 00:00:00 2001 From: ccheng Date: Wed, 19 Feb 2025 07:22:39 +0100 Subject: [PATCH 1/2] fix(Timetracker/Timesheet): recreate invoice position sfter update cleared timesheet --- tests/tine20/Sales/InvoiceJsonTests.php | 80 +++++++++++++++++++++ tine20/Sales/Controller/Invoice.php | 3 +- tine20/Timetracker/Controller/Timesheet.php | 16 +++-- tine20/Timetracker/Model/Timeaccount.php | 2 +- 4 files changed, 95 insertions(+), 6 deletions(-) diff --git a/tests/tine20/Sales/InvoiceJsonTests.php b/tests/tine20/Sales/InvoiceJsonTests.php index b3164831c22..7994d7baefa 100644 --- a/tests/tine20/Sales/InvoiceJsonTests.php +++ b/tests/tine20/Sales/InvoiceJsonTests.php @@ -301,6 +301,86 @@ public function testClearing() } } + /** + * tests if timeaccounts/timesheets get cleared if the invoice get billed + */ + public function testRecreateInvoicePositionAfterUpdateClearedTimesheet() + { + $this->_createFullFixtures(); + + // the whole year, 12 months + $date = clone $this->_referenceDate; + $date->addMonth(6); + $this->_invoiceController->createAutoInvoices($date); + + $timeaccountFilter = array( + array('field' => 'foreignRecord', 'operator' => 'AND', 'value' => array( + 'appName' => 'Sales', + 'linkType' => 'relation', + 'modelName' => 'Customer', + 'filters' => array( + array('field' => 'name', 'operator' => 'equals', 'value' => 'Customer3') + ) + )) + + ); + // test if timesheets get cleared + $invoices = $this->_uit->searchInvoices($timeaccountFilter, array()); + + $invoiceIds = array(); + + $this->assertEquals(1, $invoices['totalcount']); + + foreach($invoices['results'] as $invoice) { + $invoiceIds[] = $invoice['id']; + // fetch invoice by get to have all relations set + $invoice = $this->_uit->getInvoice($invoice['id']); + $invoice['cleared'] = 'CLEARED'; + $this->_uit->saveInvoice($invoice); + } + $invoiceId = $invoices['results'][0]['id']; + $this->assertEquals(0,$invoice['price_net']); + + Timetracker_Controller_Timesheet::destroyInstance(); + $tsController = Timetracker_Controller_Timesheet::getInstance(); + $timesheets = $tsController->search( + Tinebase_Model_Filter_FilterGroup::getFilterForModel(Timetracker_Model_Timesheet::class, [ + ['field' => 'invoice_id', 'operator' => 'equals', 'value' => $invoiceId], + ['field' => 'start_time', 'operator' => 'equals', 'value' => '09:20:00'], + ['field' => 'start_date', 'operator' => 'equals', 'value' => '2024-05-08'], + ] + )); + + foreach($timesheets as $timesheet) { + $this->assertTrue(in_array($timesheet->invoice_id, $invoiceIds), 'the invoice id must be set!'); + $this->assertEquals(1, $timesheet->is_cleared); + } + + $invoice = $this->_uit->getInvoice($invoiceId); + $this->assertEquals(1, count($invoice['positions'])); + $position = $invoice['positions'][0]; + $this->assertEquals('2024-05', $position['month']); + $this->assertEquals(7.0, $position['quantity']); + + //test update ts date after status set to is_cleared + Timetracker_Controller_Timesheet::getInstance()->setRequestContext(['confirm' => true]); + $timesheets[0]->start_time = '09:00:00'; + $timesheets[0]->end_time = '12:00:00'; + $timesheet = $tsController->update($timesheets[0]); + $this->assertEquals(1, $timesheet['is_cleared']); + $this->assertEquals($invoiceId, $timesheet['invoice_id']); + + // get the invoice again + $invoices = $this->_uit->searchInvoices($timeaccountFilter, array()); + $invoice = $this->_uit->getInvoice($invoices['results'][0]['id']); + + $this->assertEquals(1, count($invoice['positions'])); + $position = $invoice['positions'][0]; + + $this->assertEquals('2024-05', $position['month']); + $this->assertEquals(10.0, $position['quantity']); + } + /** * tests if delete timesheet throw exception when timesheet is cleared with invoice id */ diff --git a/tine20/Sales/Controller/Invoice.php b/tine20/Sales/Controller/Invoice.php index 015daf0beb4..05e4403b927 100644 --- a/tine20/Sales/Controller/Invoice.php +++ b/tine20/Sales/Controller/Invoice.php @@ -537,7 +537,8 @@ protected function _findInvoicePositionsAndInvoiceInterval(&$billableAccountable foreach ($billableAccountables as &$ba) { $ba['partOfInvoice'] = false; - if (! $ba['ac']->isBillable($this->_currentMonthToBill, $this->_currentBillingContract, $ba['pa'])) { + //fixme: isBillable only filtered timesheets with is_cleared = 0, how to make cleared timesheet billable ? + if (!$ba['ac']->isBillable($this->_currentMonthToBill, $this->_currentBillingContract, $ba['pa'])) { if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) { Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' isBillable failed for the accountable ' . $ba['ac']->getId() . ' of contract "' . $this->_currentBillingContract->number . '"'); } diff --git a/tine20/Timetracker/Controller/Timesheet.php b/tine20/Timetracker/Controller/Timesheet.php index cb8dadaaebe..bbb846c689c 100644 --- a/tine20/Timetracker/Controller/Timesheet.php +++ b/tine20/Timetracker/Controller/Timesheet.php @@ -375,10 +375,9 @@ protected function _inspectBeforeUpdate($_record, $_oldRecord) $this->_calcClearedAmount($_record, $_oldRecord); if ($this->_isTSDateChanged($_record, $_oldRecord) && $_record->is_cleared && !empty($_record->invoice_id)) { - $relation = Tinebase_Relations::getInstance()->getRelations('Sales_Model_Invoice', 'Sql', $_record->invoice_id, 'sibling', ['CONTRACT'], 'Sales_Model_Contract') - ->getFirstRecord(); - $contract = Sales_Controller_Contract::getInstance()->get($relation->related_id); - Sales_Controller_Invoice::getInstance()->createAutoInvoices(null, $contract, true); + //reset invoicing related fields to find the invoice positions + $_record->is_cleared = false; + $_record->invoice_id = ''; } } @@ -389,6 +388,15 @@ protected function _inspectAfterUpdate($updatedRecord, $record, $currentRecord) /** @var Timetracker_Model_Timesheet $updatedRecord */ if ($this->_isTSDateChanged($updatedRecord, $currentRecord)) { $this->_tsChanged($updatedRecord, $currentRecord); + //need to clear timesheet after generate invoice position + if (!$updatedRecord->is_cleared && empty($updatedRecord->invoice_id) && !empty($currentRecord->invoice_id)) { + $result = Sales_Controller_Invoice::getInstance()->checkForUpdate($currentRecord->invoice_id); + if (in_array($currentRecord->invoice_id, $result)) { + $updatedRecord->is_cleared = true; + $updatedRecord->invoice_id = $currentRecord->invoice_id; + $this->getBackend()->update($updatedRecord); + } + } } } diff --git a/tine20/Timetracker/Model/Timeaccount.php b/tine20/Timetracker/Model/Timeaccount.php index 7dc35fe5a81..3a5f6295e2a 100644 --- a/tine20/Timetracker/Model/Timeaccount.php +++ b/tine20/Timetracker/Model/Timeaccount.php @@ -854,7 +854,7 @@ public function needsInvoiceRecreation(Tinebase_DateTime $date, Sales_Model_Prod $timesheets = Timetracker_Controller_Timesheet::getInstance()->search($filter); foreach($timesheets as $timesheet) { - if ($timesheet->last_modified_time && $timesheet->last_modified_time->isLater($invoice->creation_time)) { + if ($timesheet->last_modified_time && $timesheet->last_modified_time->isLater($invoice->creation_time) && !$timesheet->is_cleared) { return true; } } From 8061393fd6b0a4a343d0ccbd9a1ba6b916ae1d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Sch=C3=BCle?= Date: Wed, 19 Feb 2025 11:01:04 +0100 Subject: [PATCH 2/2] conf(ci/php_jobs): also run ad tests on changes in Tinebase_Group --- ci/gitlab-ci/test_php_jobs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/gitlab-ci/test_php_jobs.yml b/ci/gitlab-ci/test_php_jobs.yml index ebdbb47a620..6f98c4f1335 100644 --- a/ci/gitlab-ci/test_php_jobs.yml +++ b/ci/gitlab-ci/test_php_jobs.yml @@ -237,6 +237,7 @@ php-unit-all-tests-ldap-source-parallel: - "tine20/Tinebase/User.php" - "tine20/Tinebase/User/*.php" - "tine20/Tinebase/User/LdapPlugin/*.php" + - "tine20/Tinebase/Group.php" - "tine20/Tinebase/Group/*.php" - "tine20/Tinebase/Group/LdapPlugin/*.php" - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_LABELS =~ /php-unit-all-tests-ad-source/