diff --git a/src/Invoice/Calculator/ActivityInvoiceCalculator.php b/src/Invoice/Calculator/ActivityInvoiceCalculator.php
new file mode 100644
index 00000000..22a23229
--- /dev/null
+++ b/src/Invoice/Calculator/ActivityInvoiceCalculator.php
@@ -0,0 +1,51 @@
+model->getEntries();
+ if (empty($entries)) {
+ return [];
+ }
+
+ /** @var Timesheet[] $timesheets */
+ $timesheets = [];
+
+ foreach ($entries as $entry) {
+ if (!isset($timesheets[$entry->getActivity()->getId()])) {
+ $timesheets[$entry->getActivity()->getId()] = new Timesheet();
+ }
+ $timesheet = $timesheets[$entry->getActivity()->getId()];
+ $this->mergeTimesheets($timesheet, $entry);
+ }
+
+ return array_values($timesheets);
+ }
+
+ /**
+ * @return string
+ */
+ public function getId(): string
+ {
+ return 'activity';
+ }
+}
diff --git a/tests/Invoice/Calculator/ActivityInvoiceCalculatorTest.php b/tests/Invoice/Calculator/ActivityInvoiceCalculatorTest.php
new file mode 100644
index 00000000..1e2928c0
--- /dev/null
+++ b/tests/Invoice/Calculator/ActivityInvoiceCalculatorTest.php
@@ -0,0 +1,129 @@
+assertEmptyModel(new ActivityInvoiceCalculator());
+ }
+
+ public function testWithMultipleEntries()
+ {
+ $customer = new Customer();
+ $template = new InvoiceTemplate();
+ $template->setVat(19);
+
+ $user = $this->getMockBuilder(User::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
+ $user->method('getId')->willReturn(1);
+
+ $activity1 = $this->getMockBuilder(Activity::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
+ $activity1->method('getId')->willReturn(1);
+
+ $activity2 = $this->getMockBuilder(Activity::class)->setMethods(['getId'])->disableOriginalConstructor()->getMock();
+ $activity2->method('getId')->willReturn(2);
+
+ $timesheet = new Timesheet();
+ $timesheet
+ ->setDuration(3600)
+ ->setRate(293.27)
+ ->setUser($user)
+ ->setActivity($activity1)
+ ;
+
+ $timesheet2 = new Timesheet();
+ $timesheet2
+ ->setDuration(400)
+ ->setRate(84.75)
+ ->setUser($user)
+ ->setActivity($activity2)
+ ;
+
+ $timesheet3 = new Timesheet();
+ $timesheet3
+ ->setDuration(1800)
+ ->setRate(111.11)
+ ->setUser($user)
+ ->setActivity($activity1)
+ ;
+
+ $timesheet4 = new Timesheet();
+ $timesheet4
+ ->setDuration(400)
+ ->setRate(1947.99)
+ ->setUser($user)
+ ->setActivity($activity2)
+ ;
+
+ $timesheet5 = new Timesheet();
+ $timesheet5
+ ->setDuration(400)
+ ->setRate(84)
+ ->setUser(new User())
+ ->setActivity(new Activity())
+ ;
+
+ $entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5];
+
+ $query = new InvoiceQuery();
+ $query->setActivity($activity1);
+
+ $model = new InvoiceModel();
+ $model->setCustomer($customer);
+ $model->setTemplate($template);
+ $model->setEntries($entries);
+ $model->setQuery($query);
+
+ $sut = new ActivityInvoiceCalculator();
+ $sut->setModel($model);
+
+ $this->assertEquals('activity', $sut->getId());
+ $this->assertEquals(3000.13, $sut->getTotal());
+ $this->assertEquals(19, $sut->getVat());
+ $this->assertEquals('EUR', $sut->getCurrency());
+ $this->assertEquals(2521.12, $sut->getSubtotal());
+ $this->assertEquals(6600, $sut->getTimeWorked());
+ $this->assertEquals(3, count($sut->getEntries()));
+
+ $entries = $sut->getEntries();
+ $this->assertEquals(404.38, $entries[0]->getRate());
+ $this->assertEquals(2032.74, $entries[1]->getRate());
+ $this->assertEquals(84, $entries[2]->getRate());
+ }
+
+ public function testDescriptionByTimesheet()
+ {
+ $this->assertDescription(new ActivityInvoiceCalculator(), false, false);
+ }
+
+ public function testDescriptionByActivity()
+ {
+ $this->assertDescription(new ActivityInvoiceCalculator(), false, true);
+ }
+
+ public function testDescriptionByProject()
+ {
+ $this->assertDescription(new ActivityInvoiceCalculator(), true, false);
+ }
+}
diff --git a/translations/invoice-calculator.de.xliff b/translations/invoice-calculator.de.xliff
index cd635696..a5b65df7 100644
--- a/translations/invoice-calculator.de.xliff
+++ b/translations/invoice-calculator.de.xliff
@@ -18,6 +18,10 @@
user
Benutzer gruppiert - ein Eintrag pro Benutzer
+
+ activity
+ Aktivitäten gruppiert - ein Eintrag pro Aktivität
+