From 59dc82351eccf40a52b61685c25d0ff52d52d0cd Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Mon, 16 Jun 2025 17:53:27 +0200 Subject: [PATCH] Release 2.36.1 (#5540) * fix weekly hours for new entries with non-global activities * bump packages * prevent creating invoices with 0 entries --- composer.lock | 93 +++++++++++----------- src/Command/InvoiceCreateCommand.php | 42 +++++----- src/Constants.php | 4 +- src/Form/Type/QuickEntryWeekType.php | 18 +++++ tests/Command/InvoiceCreateCommandTest.php | 70 +++++++++++----- 5 files changed, 136 insertions(+), 91 deletions(-) diff --git a/composer.lock b/composer.lock index 3da546ae..32f2db29 100644 --- a/composer.lock +++ b/composer.lock @@ -665,16 +665,16 @@ }, { "name": "doctrine/dbal", - "version": "3.9.4", + "version": "3.9.5", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "ec16c82f20be1a7224e65ac67144a29199f87959" + "reference": "4a4e2eed3134036ee36a147ee0dac037dfa17868" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/ec16c82f20be1a7224e65ac67144a29199f87959", - "reference": "ec16c82f20be1a7224e65ac67144a29199f87959", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/4a4e2eed3134036ee36a147ee0dac037dfa17868", + "reference": "4a4e2eed3134036ee36a147ee0dac037dfa17868", "shasum": "" }, "require": { @@ -687,14 +687,14 @@ "psr/log": "^1|^2|^3" }, "require-dev": { - "doctrine/coding-standard": "12.0.0", + "doctrine/coding-standard": "13.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "2.1.1", + "phpstan/phpstan": "2.1.17", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "9.6.22", - "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.10.2", + "phpunit/phpunit": "9.6.23", + "slevomat/coding-standard": "8.16.2", + "squizlabs/php_codesniffer": "3.13.1", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0" }, @@ -756,7 +756,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.4" + "source": "https://github.com/doctrine/dbal/tree/3.9.5" }, "funding": [ { @@ -772,7 +772,7 @@ "type": "tidelift" } ], - "time": "2025-01-16T08:28:55+00:00" + "time": "2025-06-15T22:40:05+00:00" }, { "name": "doctrine/deprecations", @@ -824,16 +824,16 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.14.0", + "version": "2.14.1", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "ca6a7350b421baf7fbdefbf9f4993292ed18effb" + "reference": "0a118222ab21ba05bd9f17fbaa6a3800865274f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/ca6a7350b421baf7fbdefbf9f4993292ed18effb", - "reference": "ca6a7350b421baf7fbdefbf9f4993292ed18effb", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/0a118222ab21ba05bd9f17fbaa6a3800865274f4", + "reference": "0a118222ab21ba05bd9f17fbaa6a3800865274f4", "shasum": "" }, "require": { @@ -860,9 +860,9 @@ "require-dev": { "doctrine/annotations": "^1 || ^2", "doctrine/cache": "^1.11 || ^2.0", - "doctrine/coding-standard": "^12", + "doctrine/coding-standard": "^13", "doctrine/deprecations": "^1.0", - "doctrine/orm": "^2.17 || ^3.0", + "doctrine/orm": "^2.17 || ^3.1", "friendsofphp/proxy-manager-lts": "^1.0", "phpstan/phpstan": "2.1.1", "phpstan/phpstan-phpunit": "2.0.3", @@ -926,7 +926,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.14.0" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.14.1" }, "funding": [ { @@ -942,7 +942,7 @@ "type": "tidelift" } ], - "time": "2025-03-22T17:28:21+00:00" + "time": "2025-06-09T20:22:28+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", @@ -1463,16 +1463,16 @@ }, { "name": "doctrine/orm", - "version": "2.20.3", + "version": "2.20.4", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "17d28b5c4cac212ca92730d9a26e32a0b1516126" + "reference": "71550106d491c3f888636b731c805473de3c8583" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/17d28b5c4cac212ca92730d9a26e32a0b1516126", - "reference": "17d28b5c4cac212ca92730d9a26e32a0b1516126", + "url": "https://api.github.com/repos/doctrine/orm/zipball/71550106d491c3f888636b731c805473de3c8583", + "reference": "71550106d491c3f888636b731c805473de3c8583", "shasum": "" }, "require": { @@ -1559,9 +1559,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.20.3" + "source": "https://github.com/doctrine/orm/tree/2.20.4" }, - "time": "2025-05-02T17:07:53+00:00" + "time": "2025-06-09T20:24:12+00:00" }, { "name": "doctrine/persistence", @@ -3997,16 +3997,16 @@ }, { "name": "phpoffice/math", - "version": "0.2.0", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/Math.git", - "reference": "fc2eb6d1a61b058d5dac77197059db30ee3c8329" + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc2eb6d1a61b058d5dac77197059db30ee3c8329", - "reference": "fc2eb6d1a61b058d5dac77197059db30ee3c8329", + "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", "shasum": "" }, "require": { @@ -4043,9 +4043,9 @@ ], "support": { "issues": "https://github.com/PHPOffice/Math/issues", - "source": "https://github.com/PHPOffice/Math/tree/0.2.0" + "source": "https://github.com/PHPOffice/Math/tree/0.3.0" }, - "time": "2024-08-12T07:30:45+00:00" + "time": "2025-05-29T08:31:49+00:00" }, { "name": "phpoffice/phpspreadsheet", @@ -4154,44 +4154,43 @@ }, { "name": "phpoffice/phpword", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PHPWord.git", - "reference": "8392134ce4b5dba65130ba956231a1602b848b7f" + "reference": "6d75328229bc93790b37e93741adf70646cea958" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/8392134ce4b5dba65130ba956231a1602b848b7f", - "reference": "8392134ce4b5dba65130ba956231a1602b848b7f", + "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/6d75328229bc93790b37e93741adf70646cea958", + "reference": "6d75328229bc93790b37e93741adf70646cea958", "shasum": "" }, "require": { "ext-dom": "*", + "ext-gd": "*", "ext-json": "*", "ext-xml": "*", + "ext-zip": "*", "php": "^7.1|^8.0", - "phpoffice/math": "^0.2" + "phpoffice/math": "^0.3" }, "require-dev": { - "dompdf/dompdf": "^2.0", - "ext-gd": "*", + "dompdf/dompdf": "^2.0 || ^3.0", "ext-libxml": "*", - "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.3", - "mpdf/mpdf": "^8.1", + "mpdf/mpdf": "^7.0 || ^8.0", "phpmd/phpmd": "^2.13", - "phpstan/phpstan-phpunit": "@stable", + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", "phpunit/phpunit": ">=7.0", "symfony/process": "^4.4 || ^5.0", "tecnickcom/tcpdf": "^6.5" }, "suggest": { "dompdf/dompdf": "Allows writing PDF", - "ext-gd2": "Allows adding images", "ext-xmlwriter": "Allows writing OOXML and ODF", - "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template", - "ext-zip": "Allows writing OOXML and ODF" + "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template" }, "type": "library", "autoload": { @@ -4201,7 +4200,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0" + "LGPL-3.0-only" ], "authors": [ { @@ -4257,9 +4256,9 @@ ], "support": { "issues": "https://github.com/PHPOffice/PHPWord/issues", - "source": "https://github.com/PHPOffice/PHPWord/tree/1.3.0" + "source": "https://github.com/PHPOffice/PHPWord/tree/1.4.0" }, - "time": "2024-08-30T18:03:42+00:00" + "time": "2025-06-05T10:32:36+00:00" }, { "name": "phpstan/phpdoc-parser", diff --git a/src/Command/InvoiceCreateCommand.php b/src/Command/InvoiceCreateCommand.php index 1cf73822..11c13f97 100644 --- a/src/Command/InvoiceCreateCommand.php +++ b/src/Command/InvoiceCreateCommand.php @@ -41,12 +41,12 @@ final class InvoiceCreateCommand extends Command private bool $previewUniqueFile = false; public function __construct( - private ServiceInvoice $serviceInvoice, - private CustomerRepository $customerRepository, - private ProjectRepository $projectRepository, - private InvoiceTemplateRepository $invoiceTemplateRepository, - private UserRepository $userRepository, - private EventDispatcherInterface $eventDispatcher + private readonly ServiceInvoice $serviceInvoice, + private readonly CustomerRepository $customerRepository, + private readonly ProjectRepository $projectRepository, + private readonly InvoiceTemplateRepository $invoiceTemplateRepository, + private readonly UserRepository $userRepository, + private readonly EventDispatcherInterface $eventDispatcher ) { parent::__construct(); } @@ -256,9 +256,6 @@ final class InvoiceCreateCommand extends Command /** * @param Project[] $projects - * @param InvoiceQuery $defaultQuery - * @param InputInterface $input - * @param OutputInterface $output * @return Invoice[] * @throws \Exception */ @@ -287,10 +284,16 @@ final class InvoiceCreateCommand extends Command $query->setTemplate($tpl); try { + $model = $this->serviceInvoice->createModel($query); + // this check makes sure to only fetch invoices with records + if (\count($model->getEntries()) === 0) { + continue; + } + if (null !== $this->previewDirectory) { - $invoices[] = $this->saveInvoicePreview($this->serviceInvoice->renderInvoice($this->serviceInvoice->createModel($query), $this->eventDispatcher)); + $invoices[] = $this->saveInvoicePreview($this->serviceInvoice->renderInvoice($model, $this->eventDispatcher)); } else { - $invoices[] = $this->serviceInvoice->createInvoice($this->serviceInvoice->createModel($query), $this->eventDispatcher); + $invoices[] = $this->serviceInvoice->createInvoice($model, $this->eventDispatcher); } } catch (\Exception $ex) { $io->error(\sprintf('Failed to create invoice for project "%s" with: %s', $project->getName(), $ex->getMessage())); @@ -334,8 +337,6 @@ final class InvoiceCreateCommand extends Command /** * @param Customer[] $customers - * @param InvoiceQuery $defaultQuery - * @param InputInterface $input * @return Invoice[] * @throws \Exception */ @@ -358,10 +359,16 @@ final class InvoiceCreateCommand extends Command $query->setTemplate($tpl); try { + $model = $this->serviceInvoice->createModel($query); + // this check makes sure to only fetch invoices with records + if (\count($model->getEntries()) === 0) { + continue; + } + if (null !== $this->previewDirectory) { - $invoices[] = $this->saveInvoicePreview($this->serviceInvoice->renderInvoice($this->serviceInvoice->createModel($query), $this->eventDispatcher)); + $invoices[] = $this->saveInvoicePreview($this->serviceInvoice->renderInvoice($model, $this->eventDispatcher)); } else { - $invoices[] = $this->serviceInvoice->createInvoice($this->serviceInvoice->createModel($query), $this->eventDispatcher); + $invoices[] = $this->serviceInvoice->createInvoice($model, $this->eventDispatcher); } } catch (\Exception $ex) { $io->error(\sprintf('Failed to create invoice for customer "%s" with: %s', $customer->getName(), $ex->getMessage())); @@ -372,10 +379,7 @@ final class InvoiceCreateCommand extends Command } /** - * @param InputInterface $input - * @param OutputInterface $output * @param Invoice[] $invoices - * @return int */ protected function renderInvoiceResult(InputInterface $input, OutputInterface $output, array $invoices): int { @@ -449,7 +453,6 @@ final class InvoiceCreateCommand extends Command } /** - * @param InvoiceQuery $invoiceQuery * @return Customer[] */ private function getActiveCustomers(InvoiceQuery $invoiceQuery): array @@ -467,7 +470,6 @@ final class InvoiceCreateCommand extends Command } /** - * @param InvoiceQuery $invoiceQuery * @return Project[] */ private function getActiveProjects(InvoiceQuery $invoiceQuery): array diff --git a/src/Constants.php b/src/Constants.php index 698f2ec2..9f61cf34 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -17,11 +17,11 @@ final class Constants /** * The current release version */ - public const VERSION = '2.36.0'; + public const VERSION = '2.36.1'; /** * The current release: major * 10000 + minor * 100 + patch */ - public const VERSION_ID = 23600; + public const VERSION_ID = 23601; /** * The software name */ diff --git a/src/Form/Type/QuickEntryWeekType.php b/src/Form/Type/QuickEntryWeekType.php index 2d0b5f08..7d98d7a9 100644 --- a/src/Form/Type/QuickEntryWeekType.php +++ b/src/Form/Type/QuickEntryWeekType.php @@ -76,6 +76,24 @@ final class QuickEntryWeekType extends AbstractType }; $builder->addEventListener(FormEvents::PRE_SET_DATA, $activityFunction); + // make sure to pre-fill the form, so non-global activities can be loaded for the select project + $activityPreSubmitFunction = function (FormEvent $event) use ($activityOptions) { + $data = $event->getData(); + + if (\is_array($data)) { + if (!empty($data['project'])) { + $activityOptions['projects'] = [$data['project']]; + } + + if (!empty($data['activity'])) { + $activityOptions['activities'] = [$data['activity']]; + } + } + + $event->getForm()->add('activity', ActivityType::class, $activityOptions); + }; + $builder->addEventListener(FormEvents::PRE_SUBMIT, $activityPreSubmitFunction); + $builder->add('metaFields', MetaFieldsCollectionType::class); $builder->add('timesheets', CollectionType::class, [ diff --git a/tests/Command/InvoiceCreateCommandTest.php b/tests/Command/InvoiceCreateCommandTest.php index 135f8cf9..c88295e4 100644 --- a/tests/Command/InvoiceCreateCommandTest.php +++ b/tests/Command/InvoiceCreateCommandTest.php @@ -158,20 +158,21 @@ class InvoiceCreateCommandTest extends KernelTestCase public function testCreateWithInvalidCustomer(): void { - $this->assertCommandErrors(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--customer' => 3, '--template' => 'x'], 'Unknown customer ID: 3'); + $this->assertCommandErrors(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--customer' => PHP_INT_MAX, '--template' => 'x'], 'Unknown customer ID: ' . PHP_INT_MAX); } public function testCreateWithInvalidProject(): void { - $this->assertCommandErrors(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--project' => 3, '--template' => 'x'], 'Unknown project ID: 3'); + $this->assertCommandErrors(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--project' => PHP_INT_MAX, '--template' => 'x'], 'Unknown project ID: ' . PHP_INT_MAX); } public function testCreateInvoice(): void { - $fixture = new InvoiceTemplateFixtures(); - $this->importFixture($fixture); + $start = new \DateTime('2020-01-01'); - $commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--customer' => 1, '--template' => 'Invoice', '--start' => '2020-01-01', '--end' => '2020-03-01']); + $customer = $this->prepareFixtures($start)[0]; + + $commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--customer' => $customer->getId(), '--template' => 'Invoice', '--start' => '2020-01-01', '--end' => '2020-03-01']); $output = $commandTester->getDisplay(); self::assertStringContainsString('Created 1 invoice(s)', $output); @@ -179,15 +180,13 @@ class InvoiceCreateCommandTest extends KernelTestCase self::assertStringContainsString('| Customer', $output); self::assertStringContainsString('| Total', $output); self::assertStringContainsString('| Filename', $output); - self::assertStringContainsString('0 EUR', $output); - self::assertStringContainsString('/tests/_data/invoices/' . ((new \DateTime())->format('Y')) . '-001-Test.html |', $output); + self::assertStringContainsString('/tests/_data/invoices/' . ((new \DateTime())->format('Y')) . '-001', $output); } /** - * @param \DateTime $start - * @return array + * @return array{0: Customer, 1: Project} */ - protected function prepareFixtures(\DateTime $start): array + protected function prepareFixtures(\DateTime $start, bool $withTimesheets = true): array { $fixture = new InvoiceTemplateFixtures(); $invoiceTemplate = $this->importFixture($fixture); @@ -204,14 +203,16 @@ class InvoiceCreateCommandTest extends KernelTestCase $fixture->setAmount(1); $projects = $this->importFixture($fixture); - $fixture = new TimesheetFixtures(); - $fixture->setUser($this->getUserByName(UserFixtures::USERNAME_SUPER_ADMIN)); - $fixture->setAmount(20); - $fixture->setStartDate($start); - $fixture->setProjects($projects); - $this->importFixture($fixture); + if ($withTimesheets) { + $fixture = new TimesheetFixtures(); + $fixture->setUser($this->getUserByName(UserFixtures::USERNAME_SUPER_ADMIN)); + $fixture->setAmount(20); + $fixture->setStartDate($start); + $fixture->setProjects($projects); + $this->importFixture($fixture); + } - return [$customer]; + return [$customer, $projects[0]]; } public function testCreateInvoiceByCustomer(): void @@ -232,15 +233,27 @@ class InvoiceCreateCommandTest extends KernelTestCase $start = new \DateTime('-2 months'); $end = new \DateTime(); - $imports = $this->prepareFixtures($start); - $customer = $imports[0]->getId(); + $customer = $this->prepareFixtures($start)[0]; - $commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--customer' => $customer . ',1', '--start' => $start->format('Y-m-d'), '--end' => $end->format('Y-m-d')]); + $commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--customer' => $customer->getId(), '--start' => $start->format('Y-m-d'), '--end' => $end->format('Y-m-d')]); $output = $commandTester->getDisplay(); self::assertStringContainsString('Created 1 invoice(s) ', $output); } + public function testCreateInvoiceByCustomerIdWithoutTimesheets(): void + { + $start = new \DateTime('-2 months'); + $end = new \DateTime(); + + $customer = $this->prepareFixtures($start, false)[0]; + + $commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--customer' => $customer->getId(), '--start' => $start->format('Y-m-d'), '--end' => $end->format('Y-m-d')]); + + $output = $commandTester->getDisplay(); + self::assertStringContainsString('No invoice was generated', $output); + } + public function testCreateInvoiceByProject(): void { $start = new \DateTime('-2 months'); @@ -259,14 +272,27 @@ class InvoiceCreateCommandTest extends KernelTestCase $start = new \DateTime('-2 months'); $end = new \DateTime(); - $this->prepareFixtures($start); + $project = $this->prepareFixtures($start)[1]; - $commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--exported' => 'all', '--project' => '1', '--template' => 'Invoice', '--start' => $start->format('Y-m-d'), '--end' => $end->format('Y-m-d')]); + $commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--exported' => 'all', '--project' => $project->getId(), '--template' => 'Invoice', '--start' => $start->format('Y-m-d'), '--end' => $end->format('Y-m-d')]); $output = $commandTester->getDisplay(); self::assertStringContainsString('Created 1 invoice(s) ', $output); } + public function testCreateInvoiceByProjectIdWithoutTimesheets(): void + { + $start = new \DateTime('-2 months'); + $end = new \DateTime(); + + $project = $this->prepareFixtures($start, false)[1]; + + $commandTester = $this->createInvoice(['--user' => UserFixtures::USERNAME_SUPER_ADMIN, '--exported' => 'all', '--project' => $project->getId(), '--template' => 'Invoice', '--start' => $start->format('Y-m-d'), '--end' => $end->format('Y-m-d')]); + + $output = $commandTester->getDisplay(); + self::assertStringContainsString('No invoice was generated', $output); + } + public function testCreateInvoiceByProjectWithPreview(): void { $start = new \DateTime('-2 months');