Release 2.36.1 (#5540)

* fix weekly hours for new entries with non-global activities
* bump packages
* prevent creating invoices with 0 entries
This commit is contained in:
Kevin Papst
2025-06-16 17:53:27 +02:00
committed by GitHub
parent da10099248
commit 59dc82351e
5 changed files with 136 additions and 91 deletions

93
composer.lock generated
View File

@@ -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",

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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, [

View File

@@ -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<Customer>
* @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');