diff --git a/src/Invoice/Renderer/RendererTrait.php b/src/Invoice/Renderer/RendererTrait.php index 79e142d2..f7f1ccc8 100644 --- a/src/Invoice/Renderer/RendererTrait.php +++ b/src/Invoice/Renderer/RendererTrait.php @@ -138,17 +138,23 @@ trait RendererTrait $project = $timesheet->getProject(); $customer = $project->getCustomer(); + $begin = $timesheet->getBegin(); + $end = $timesheet->getEnd(); + return [ 'entry.description' => $description, 'entry.amount' => $amount, 'entry.rate' => $this->getFormattedMoney($hourlyRate), 'entry.total' => $this->getFormattedMoney($rate), 'entry.duration' => $timesheet->getDuration(), - 'entry.begin' => $this->getFormattedDateTime($timesheet->getBegin()), - 'entry.begin_timestamp' => $timesheet->getBegin()->getTimestamp(), - 'entry.end' => $this->getFormattedDateTime($timesheet->getEnd()), - 'entry.end_timestamp' => $timesheet->getEnd()->getTimestamp(), - 'entry.date' => $this->getFormattedDateTime($timesheet->getBegin()), + 'entry.duration_minutes' => number_format($timesheet->getDuration() / 60), + 'entry.begin' => $this->getFormattedDateTime($begin), + 'entry.begin_time' => date("H:i", $begin->getTimestamp()), + 'entry.begin_timestamp' => $begin->getTimestamp(), + 'entry.end' => $this->getFormattedDateTime($end), + 'entry.end_time' => date("H:i", $end->getTimestamp()), + 'entry.end_timestamp' => $end->getTimestamp(), + 'entry.date' => $this->getFormattedDateTime($begin), 'entry.user_id' => $user->getId(), 'entry.user_name' => $user->getUsername(), 'entry.user_alias' => $user->getAlias(), diff --git a/tests/Invoice/Renderer/DebugRenderer.php b/tests/Invoice/Renderer/DebugRenderer.php new file mode 100644 index 00000000..8a5680d6 --- /dev/null +++ b/tests/Invoice/Renderer/DebugRenderer.php @@ -0,0 +1,94 @@ +format('d.m.Y'); + } + + /** + * @param $amount + * @return mixed + */ + protected function getFormattedMoney($amount) + { + return $amount; + } + + /** + * @param \DateTime $date + * @return mixed + */ + protected function getFormattedMonthName(\DateTime $date) + { + return $date->format('m'); + } + + /** + * @param $seconds + * @return mixed + */ + protected function getFormattedDuration($seconds) + { + return $seconds; + } + + /** + * Render the given InvoiceDocument with the data from the InvoiceModel into a stupid array for testing only. + * + * @param InvoiceDocument $document + * @param InvoiceModel $model + * @return Response + */ + public function render(InvoiceDocument $document, InvoiceModel $model): Response + { + $result = [ + 'model' => $this->modelToReplacer($model), + 'entries' => [], + ]; + + foreach ($model->getCalculator()->getEntries() as $entry) { + $result['entries'][] = $this->timesheetToArray($entry); + } + + return new Response(json_encode($result)); + } +} diff --git a/tests/Invoice/Renderer/DebugRendererTest.php b/tests/Invoice/Renderer/DebugRendererTest.php new file mode 100644 index 00000000..02d00cb7 --- /dev/null +++ b/tests/Invoice/Renderer/DebugRendererTest.php @@ -0,0 +1,130 @@ +getInvoiceModel(), '1,947.99', 5, 5, 1, 2, 2]; + yield [$this->getInvoiceModelOneEntry(), '293.27', 1, 1, 0, 1, 0]; + } + + /** + * @dataProvider getTestModel + */ + public function testRender(InvoiceModel $model, $expectedRate, $expectedRows, $expectedDescriptions, $expectedUser1, $expectedUser2, $expectedUser3) + { + $document = new InvoiceDocument(new \SplFileInfo(__DIR__ . '/DebugRenderer.php')); + $sut = new DebugRenderer(); + /** @var Response $response */ + $response = $sut->render($document, $model); + $data = json_decode($response->getContent(), true); + + $this->assertModelStructure($data['model']); + $rows = $data['entries']; + $this->assertEquals($expectedRows, count($rows)); + + foreach($rows as $row) { + $this->assertEntryStructure($row); + } + + // TODO check values or formats? + } + + protected function assertModelStructure(array $model) + { + $keys = [ + 'invoice.due_date', + 'invoice.date', + 'invoice.number', + 'invoice.currency', + 'invoice.vat', + 'invoice.tax', + 'invoice.total_time', + 'invoice.total', + 'invoice.subtotal', + 'template.name', + 'template.company', + 'template.address', + 'template.title', + 'template.payment_terms', + 'template.due_days', + 'query.begin', + 'query.end', + 'query.month', + 'query.year', + 'customer.address', + 'customer.name', + 'customer.contact', + 'customer.company', + 'customer.country', + 'customer.number', + 'customer.homepage', + 'customer.comment', + ]; + + foreach($keys as $key) { + $this->assertArrayHasKey($key, $model); + } + + $expectedKeys = array_merge([], $keys); + sort($expectedKeys); + $givenKeys = array_keys($model); + sort($givenKeys); + + $this->assertEquals(count($keys), count($givenKeys)); + $this->assertEquals($expectedKeys, $givenKeys); + } + + protected function assertEntryStructure(array $model) + { + $keys = [ + 'entry.description', + 'entry.amount', + 'entry.rate', + 'entry.total', + 'entry.duration', + 'entry.duration_minutes', + 'entry.begin', + 'entry.begin_time', + 'entry.begin_timestamp', + 'entry.end', + 'entry.end_time', + 'entry.end_timestamp', + 'entry.date', + 'entry.user_id', + 'entry.user_name', + 'entry.user_alias', + 'entry.activity', + 'entry.activity_id', + 'entry.project', + 'entry.customer', + 'entry.project_id', + 'entry.customer_id', + ]; + + foreach($keys as $key) { + $this->assertArrayHasKey($key, $model); + } + + $expectedKeys = array_merge([], $keys); + sort($expectedKeys); + $givenKeys = array_keys($model); + sort($givenKeys); + + $this->assertEquals(count($keys), count($givenKeys)); + $this->assertEquals($expectedKeys, $givenKeys); + } +} diff --git a/var/docs/invoices.md b/var/docs/invoices.md index b0bacb11..ecefb80e 100644 --- a/var/docs/invoices.md +++ b/var/docs/invoices.md @@ -93,61 +93,64 @@ See below in `Template variables` to find out which variables you can use in you You can use the following global variables in your templates: -| Key | Description | -|---|---| -| ${invoice.due_date} | The due date for the invoice payment | -| ${invoice.date} | The creation date of this invoice | -| ${invoice.number} | The generated invoice number | -| ${invoice.currency} | The invoice currency | -| ${invoice.total_time} | The total working time (entries with a fixed rate are always calculated with 1) | -| ${invoice.total} | The invoices total (including tax) | -| ${invoice.subtotal} | The invoices subtotal (excluding tax) | -| ${invoice.vat} | The VAT in percent for this invoice | -| ${invoice.tax} | The tax of the invoice amount | -| ${template.name} | The invoice name, as configured in your template | -| ${template.company} | The company name, as configured in your template | -| ${template.address} | The invoicing address, as configured in your template | -| ${template.title} | The invoice title, as configured in your template | -| ${template.payment_terms} | Your payment terms, usage might differ from template to template | -| ${template.due_days} | The amount of days for the payment, starting with the day of creating the invoice | -| ${query.begin} | The query begin as formatted short date | -| ${query.end} | The query end as formatted short date | -| ${query.month} | The month for this query (begin date) | -| ${query.year} | The year for this query (begin date) | -| ${customer.address} | The customer address | -| ${customer.name} | The customer name | -| ${customer.contact} | The customer contac | -| ${customer.company} | The customer company | -| ${customer.number} | The customer number | -| ${customer.country} | The customer country | -| ${customer.homepage} | The customer homepage | -| ${customer.comment} | The customer comment | +| Key | Description | Example | +|---|---|---| +| ${invoice.due_date} | The due date for the invoice payment | | +| ${invoice.date} | The creation date of this invoice | | +| ${invoice.number} | The generated invoice number | | +| ${invoice.currency} | The invoice currency | | +| ${invoice.total_time} | The total working time (entries with a fixed rate are always calculated with 1) | | +| ${invoice.total} | The invoices total (including tax) | | +| ${invoice.subtotal} | The invoices subtotal (excluding tax) | | +| ${invoice.vat} | The VAT in percent for this invoice | | +| ${invoice.tax} | The tax of the invoice amount | | +| ${template.name} | The invoice name, as configured in your template | | +| ${template.company} | The company name, as configured in your template | | +| ${template.address} | The invoicing address, as configured in your template | | +| ${template.title} | The invoice title, as configured in your template | | +| ${template.payment_terms} | Your payment terms, usage might differ from template to template | | +| ${template.due_days} | The amount of days for the payment, starting with the day of creating the invoice | | +| ${query.begin} | The query begin as formatted short date | | +| ${query.end} | The query end as formatted short date | | +| ${query.month} | The month for this query (begin date) | | +| ${query.year} | The year for this query (begin date) | | +| ${customer.address} | The customer address | | +| ${customer.name} | The customer name | | +| ${customer.contact} | The customer contact | | +| ${customer.company} | The customer company | | +| ${customer.number} | The customer number | | +| ${customer.country} | The customer country | | +| ${customer.homepage} | The customer homepage | | +| ${customer.comment} | The customer comment | | ### Timesheet entry variables -For each timesheet entry you can use the following variables: +For each timesheet entry you can use the variables from the following table. -| Key | Description | -|---|---| -| ${entry.description} | The entries description | -| ${entry.amount} | The amount for this entry (normally the amount of hours) | -| ${entry.rate} | The rate for one unit of the entry (normally one hour) | -| ${entry.total} | The total rate for this entry | -| ${entry.duration} | The duration in seconds | -| ${entry.begin} | The begin date - _format may change and include the time in the future_ | -| ${entry.begin_timestamp} | The timestamp for the begin of this entry | -| ${entry.end} | The begin date - _format may change and include the time in the future_ | -| ${entry.end_timestamp} | The timestamp for the end of this entry | -| ${entry.date} | The start date when this record was created | -| ${entry.user_id} | The user ID | -| ${entry.user_name} | The username | -| ${entry.user_alias} | The user alias | -| ${entry.activity} | Activity name | -| ${entry.activity_id} | Activity ID | -| ${entry.project} | Project name | -| ${entry.project_id} | Project ID | -| ${entry.customer} | Customer name | -| ${entry.customer_id} | Customer ID | +| Key | Description | Example | +|---|---|---| +| ${entry.description} | The entries description | _foo bar_ | +| ${entry.amount} | The format duration/amount for this entry | 02:47 h | +| ${entry.rate} | The rate for one unit of the entry (normally one hour) | 100 | +| ${entry.total} | The total rate for this entry | 278,33 | +| ${entry.duration} | The duration in seconds | 10020 | +| ${entry.duration_minutes} | The duration in minutes with no decimals | 167 | +| ${entry.begin} | The begin date (format depends on the users language) | 27.10.2018 | +| ${entry.begin_time} | The formatted time for the begin of this entry | 14:57 | +| ${entry.begin_timestamp} | The timestamp for the begin of this entry | 1542016273 | +| ${entry.end} | The begin date (format depends on the users language) | 27.10.2018 | +| ${entry.end_time} | The formatted time for the end of this entry | 17:44 | +| ${entry.end_timestamp} | The timestamp for the end of this entry | 1542016273 | +| ${entry.date} | The start date when this record was created | 27.10.2018 | +| ${entry.user_id} | The user ID | 1 | +| ${entry.user_name} | The username | susan_super | +| ${entry.user_alias} | The user alias | Susan Miller | +| ${entry.activity} | Activity name | Post production | +| ${entry.activity_id} | Activity ID | 124 | +| ${entry.project} | Project name | Nemesis | +| ${entry.project_id} | Project ID | 10 | +| ${entry.customer} | Customer name | Acme Studios | +| ${entry.customer_id} | Customer ID | 3 | ## Configure search path