From 636a51e72188e96111ae0065e8b24025fd1207e0 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Sun, 2 Nov 2025 12:34:41 +0100 Subject: [PATCH] group invoices by project and activity (#5675) --- .../ProjectActivityInvoiceCalculator.php | 41 +++++ .../ProjectUserInvoiceCalculator.php | 11 +- .../ProjectActivityInvoiceCalculatorTest.php | 165 ++++++++++++++++++ 3 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 src/Invoice/Calculator/ProjectActivityInvoiceCalculator.php create mode 100644 tests/Invoice/Calculator/ProjectActivityInvoiceCalculatorTest.php diff --git a/src/Invoice/Calculator/ProjectActivityInvoiceCalculator.php b/src/Invoice/Calculator/ProjectActivityInvoiceCalculator.php new file mode 100644 index 00000000..3b43f54d --- /dev/null +++ b/src/Invoice/Calculator/ProjectActivityInvoiceCalculator.php @@ -0,0 +1,41 @@ +getProject()?->getId(), + $invoiceItem->getActivity()?->getId() + ]; + } + + protected function mergeSumInvoiceItem(InvoiceItem $invoiceItem, ExportableItem $entry): void + { + $project = $entry->getProject(); + if ($project !== null) { + $invoiceItem->setDescription($project->getInvoiceText() ?? $project->getName()); + } + } + + public function getId(): string + { + return 'project_activity'; + } +} diff --git a/src/Invoice/Calculator/ProjectUserInvoiceCalculator.php b/src/Invoice/Calculator/ProjectUserInvoiceCalculator.php index 9a2ba6c7..ed3d5b10 100644 --- a/src/Invoice/Calculator/ProjectUserInvoiceCalculator.php +++ b/src/Invoice/Calculator/ProjectUserInvoiceCalculator.php @@ -44,14 +44,9 @@ final class ProjectUserInvoiceCalculator extends AbstractSumInvoiceCalculator im protected function mergeSumInvoiceItem(InvoiceItem $invoiceItem, ExportableItem $entry): void { - if ($entry->getProject() === null) { - return; - } - - if ($entry->getProject()->getInvoiceText() !== null) { - $invoiceItem->setDescription($entry->getProject()->getInvoiceText()); - } else { - $invoiceItem->setDescription($entry->getProject()->getName()); + $project = $entry->getProject(); + if ($project !== null) { + $invoiceItem->setDescription($project->getInvoiceText() ?? $project->getName()); } } diff --git a/tests/Invoice/Calculator/ProjectActivityInvoiceCalculatorTest.php b/tests/Invoice/Calculator/ProjectActivityInvoiceCalculatorTest.php new file mode 100644 index 00000000..86b5c33b --- /dev/null +++ b/tests/Invoice/Calculator/ProjectActivityInvoiceCalculatorTest.php @@ -0,0 +1,165 @@ +setVat(19); + + $user1 = $this->getMockBuilder(User::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock(); + $user1->method('getId')->willReturn(1); + + $user2 = $this->getMockBuilder(User::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock(); + $user2->method('getId')->willReturn(2); + + $project1 = $this->getMockBuilder(Project::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock(); + $project1->method('getId')->willReturn(1); + + $project2 = $this->getMockBuilder(Project::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock(); + $project2->method('getId')->willReturn(2); + + $project3 = $this->getMockBuilder(Project::class)->onlyMethods(['getId'])->disableOriginalConstructor()->getMock(); + $project3->method('getId')->willReturn(3); + + $activity1 = $this->getMockBuilder(Activity::class)->onlyMethods(['getId', 'getName'])->disableOriginalConstructor()->getMock(); + $activity1->method('getId')->willReturn(1); + $activity1->method('getName')->willReturn('sdsd'); + + $activity2 = $this->getMockBuilder(Activity::class)->onlyMethods(['getId', 'getName'])->disableOriginalConstructor()->getMock(); + $activity2->method('getId')->willReturn(2); + $activity2->method('getName')->willReturn('bar'); + + $activity3 = $this->getMockBuilder(Activity::class)->onlyMethods(['getId', 'getName'])->disableOriginalConstructor()->getMock(); + $activity3->method('getId')->willReturn(3); + $activity3->method('getName')->willReturn('foo'); + + $timesheet = new Timesheet(); + $timesheet->setBegin(new DateTime('2018-11-29')); + $timesheet->setEnd(new DateTime()); + $timesheet->setDuration(3600); + $timesheet->setRate(293.27); + $timesheet->setUser($user1); + $timesheet->setActivity($activity1); + $timesheet->setProject($project1); + + $timesheet2 = new Timesheet(); + $timesheet2->setBegin(new DateTime('2018-11-28')); + $timesheet2->setEnd(new DateTime()); + $timesheet2->setDuration(400); + $timesheet2->setRate(84.75); + $timesheet2->setUser($user1); + $timesheet2->setActivity($activity2); + $timesheet2->setProject($project2); + + $timesheet3 = new Timesheet(); + $timesheet3->setBegin(new DateTime('2018-11-29')); + $timesheet3->setEnd(new DateTime()); + $timesheet3->setDuration(1800); + $timesheet3->setRate(111.11); + $timesheet3->setUser($user1); + $timesheet3->setActivity($activity1); + $timesheet3->setProject($project1); + + $timesheet4 = new Timesheet(); + $timesheet4->setBegin(new DateTime('2018-11-08')); + $timesheet4->setEnd(new DateTime()); + $timesheet4->setDuration(400); + $timesheet4->setRate(1947.99); + $timesheet4->setUser($user1); + $timesheet4->setActivity($activity3); + $timesheet4->setProject($project3); + + $timesheet5 = new Timesheet(); + $timesheet5->setBegin(new DateTime('2018-11-28')); + $timesheet5->setEnd(new DateTime()); + $timesheet5->setDuration(400); + $timesheet5->setRate(84); + $timesheet5->setUser($user1); + $timesheet5->setActivity($activity2); + $timesheet5->setProject($project3); + + $timesheet6 = new Timesheet(); + $timesheet6->setBegin(new DateTime('2018-11-27')); + $timesheet6->setEnd(new DateTime()); + $timesheet6->setDuration(1000); + $timesheet6->setRate(100); + $timesheet6->setUser($user2); + $timesheet6->setActivity($activity3); + $timesheet6->setProject($project2); + + $entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5, $timesheet6]; + + $query = new InvoiceQuery(); + $query->setProjects([$project1]); + + $model = (new InvoiceModelFactoryFactory($this))->create()->createModel(new DebugFormatter(), $customer, $template, $query); + $model->addEntries($entries); + + $sut = $this->getCalculator(); + $sut->setModel($model); + + self::assertEquals('project_activity', $sut->getId()); + self::assertEquals(3119.13, $sut->getTotal()); + $this->assertTax($sut, 19); + self::assertEquals('EUR', $model->getCurrency()); + self::assertEquals(2621.12, $sut->getSubtotal()); + self::assertEquals(7600, $sut->getTimeWorked()); + + $entries = $sut->getEntries(); + self::assertCount(5, $entries); + + self::assertEquals('2018-11-08', $entries[0]->getBegin()?->format('Y-m-d')); + self::assertEquals('2018-11-27', $entries[1]->getBegin()?->format('Y-m-d')); + self::assertEquals('2018-11-28', $entries[2]->getBegin()?->format('Y-m-d')); + self::assertEquals('2018-11-28', $entries[3]->getBegin()?->format('Y-m-d')); + self::assertEquals('2018-11-29', $entries[4]->getBegin()?->format('Y-m-d')); + + self::assertEquals(1947.99, $entries[0]->getRate()); + self::assertEquals(100, $entries[1]->getRate()); + self::assertEquals(84.75, $entries[2]->getRate()); + self::assertEquals(84, $entries[3]->getRate()); + self::assertEquals(404.38, $entries[4]->getRate()); + } + + public function testDescriptionByProject(): void + { + $this->assertDescription($this->getCalculator(), true, false); + } +}