diff --git a/src/.env.example b/src/.env.example --- a/src/.env.example +++ b/src/.env.example @@ -105,5 +105,8 @@ COMPANY_EMAIL= COMPANY_LOGO= +VAT_COUNTRIES=CH,LI +VAT_RATE=7.7 + KB_ACCOUNT_DELETE= KB_ACCOUNT_SUSPENDED= diff --git a/src/app/Documents/Receipt.php b/src/app/Documents/Receipt.php --- a/src/app/Documents/Receipt.php +++ b/src/app/Documents/Receipt.php @@ -127,6 +127,7 @@ $company = $this->companyData(); if (self::$fakeMode) { + $country = 'CH'; $customer = [ 'id' => $this->wallet->owner->id, 'wallet_id' => $this->wallet->id, @@ -156,6 +157,7 @@ ]); } else { $customer = $this->customerData(); + $country = $this->wallet->owner->getSetting('country'); $items = $this->wallet->payments() ->where('status', PaymentProvider::STATUS_PAID) @@ -166,18 +168,33 @@ ->get(); } + $vatRate = \config('app.vat.rate'); + $vatCountries = explode(',', \config('app.vat.countries')); + $vatCountries = array_map('strtoupper', array_map('trim', $vatCountries)); + + if (!$country || !in_array(strtoupper($country), $vatCountries)) { + $vatRate = 0; + } + + $totalVat = 0; $total = 0; - $items = $items->map(function ($item) use (&$total, $appName) { - $total += $item->amount; + $items = $items->map(function ($item) use (&$total, &$totalVat, $appName, $vatRate) { + $amount = $item->amount; + + if ($vatRate > 0) { + $amount = round($amount * ((100 - $vatRate) / 100)); + $totalVat += $item->amount - $amount; + } + + $total += $amount; + return [ - 'amount' => sprintf('%.2f %s', $item->amount / 100, $this->wallet->currency), + 'amount' => $this->wallet->money($amount), 'description' => \trans('documents.receipt-item-desc', ['site' => $appName]), 'date' => $item->updated_at->toDateString(), ]; }); - $total = sprintf('%.2f %s', $total / 100, $this->wallet->currency); - // Load the template $view = view('documents.receipt') ->with([ @@ -186,7 +203,11 @@ 'company' => $company, 'customer' => $customer, 'items' => $items, - 'total' => $total, + 'subTotal' => $this->wallet->money($total), + 'total' => $this->wallet->money($total + $totalVat), + 'totalVat' => $this->wallet->money($totalVat), + 'vatRate' => preg_replace('/([.,]00|0|[.,])$/', '', sprintf('%.2f', $vatRate)), + 'vat' => $vatRate > 0, ]); return $view; diff --git a/src/config/app.php b/src/config/app.php --- a/src/config/app.php +++ b/src/config/app.php @@ -253,4 +253,9 @@ 'email' => env('COMPANY_EMAIL'), 'logo' => env('COMPANY_LOGO'), ], + + 'vat' => [ + 'countries' => env('VAT_COUNTRIES'), + 'rate' => (float) env('VAT_RATE'), + ], ]; diff --git a/src/resources/lang/en/documents.php b/src/resources/lang/en/documents.php --- a/src/resources/lang/en/documents.php +++ b/src/resources/lang/en/documents.php @@ -33,4 +33,6 @@ 'receipt-title' => "Receipt for :month :year", 'receipt-item-desc' => ":site Services", + 'subtotal' => "Subtotal", + 'vat' => "VAT (:rate%)", ]; diff --git a/src/resources/sass/document.scss b/src/resources/sass/document.scss --- a/src/resources/sass/document.scss +++ b/src/resources/sass/document.scss @@ -62,6 +62,12 @@ font-size: 10pt; } + .price { + width: 150px; + text-align: right; + white-space: nowrap; + } + td.logo { width: 1%; } @@ -73,6 +79,11 @@ tr.total { background-color: #f4f4f4; } + + tr.vat td, + tr.subtotal td { + border-bottom: 0; + } } #footer { @@ -106,7 +117,3 @@ .align-left { text-align: left; } - -.amount { - white-space: nowrap; -} diff --git a/src/resources/views/documents/receipt.blade.php b/src/resources/views/documents/receipt.blade.php --- a/src/resources/views/documents/receipt.blade.php +++ b/src/resources/views/documents/receipt.blade.php @@ -36,18 +36,28 @@ {{ __('documents.date') }} {{ __('documents.description') }} - {{ __('documents.amount') }} + {{ __('documents.amount') }} @foreach ($items as $item) {{ $item['date'] }} {{ $item['description'] }} - {{ $item['amount'] }} + {{ $item['amount'] }} @endforeach +@if ($vat) + + {{ __('documents.subtotal') }} + {{ $subTotal }} + + + {{ __('documents.vat', ['rate' => $vatRate]) }} + {{ $totalVat }} + +@endif - {{ __('documents.total') }} - {{ $total }} + {{ __('documents.total') }} + {{ $total }} diff --git a/src/tests/Feature/Documents/ReceiptTest.php b/src/tests/Feature/Documents/ReceiptTest.php --- a/src/tests/Feature/Documents/ReceiptTest.php +++ b/src/tests/Feature/Documents/ReceiptTest.php @@ -24,11 +24,9 @@ } /** - * Test receipt HTML output - * - * @return void + * Test receipt HTML output (without VAT) */ - public function testHtmlOutput() + public function testHtmlOutput(): void { $appName = \config('app.name'); $wallet = $this->getTestData(); @@ -64,21 +62,21 @@ $this->assertCount(3, $cells); $this->assertSame('2020-05-01', $this->getNodeContent($cells[0])); $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); - $this->assertSame('12.34 CHF', $this->getNodeContent($cells[2])); + $this->assertSame('12,34 CHF', $this->getNodeContent($cells[2])); $cells = $records[2]->getElementsByTagName('td'); $this->assertCount(3, $cells); $this->assertSame('2020-05-10', $this->getNodeContent($cells[0])); $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); - $this->assertSame('0.01 CHF', $this->getNodeContent($cells[2])); + $this->assertSame('0,01 CHF', $this->getNodeContent($cells[2])); $cells = $records[3]->getElementsByTagName('td'); $this->assertCount(3, $cells); $this->assertSame('2020-05-31', $this->getNodeContent($cells[0])); $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); - $this->assertSame('1.00 CHF', $this->getNodeContent($cells[2])); + $this->assertSame('1,00 CHF', $this->getNodeContent($cells[2])); $summaryCells = $records[4]->getElementsByTagName('td'); $this->assertCount(2, $summaryCells); $this->assertSame('Total', $this->getNodeContent($summaryCells[0])); - $this->assertSame('13.35 CHF', $this->getNodeContent($summaryCells[1])); + $this->assertSame('13,35 CHF', $this->getNodeContent($summaryCells[1])); // Customer data $customer = $dom->getElementById('customer'); @@ -90,7 +88,7 @@ $this->assertTrue(strpos($customerIdents, "Account ID {$wallet->id}") !== false); $this->assertTrue(strpos($customerIdents, "Customer No. {$wallet->owner->id}") !== false); - // TODO: Company details in the footer + // Company details in the footer $footer = $dom->getElementById('footer'); $footerOutput = $footer->textContent; $this->assertStringStartsWith(\config('app.company.details'), $footerOutput); @@ -98,11 +96,61 @@ } /** + * Test receipt HTML output (with VAT) + */ + public function testHtmlOutputVat(): void + { + \config(['app.vat.rate' => 7.7]); + \config(['app.vat.countries' => 'ch']); + + $appName = \config('app.name'); + $wallet = $this->getTestData('CH'); + $receipt = new Receipt($wallet, 2020, 5); + $html = $receipt->htmlOutput(); + + $this->assertStringStartsWith('', $html); + + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->loadHTML($html); + + // The main table content + $content = $dom->getElementById('content'); + $records = $content->getElementsByTagName('tr'); + $this->assertCount(7, $records); + + $cells = $records[1]->getElementsByTagName('td'); + $this->assertCount(3, $cells); + $this->assertSame('2020-05-01', $this->getNodeContent($cells[0])); + $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); + $this->assertSame('11,39 CHF', $this->getNodeContent($cells[2])); + $cells = $records[2]->getElementsByTagName('td'); + $this->assertCount(3, $cells); + $this->assertSame('2020-05-10', $this->getNodeContent($cells[0])); + $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); + $this->assertSame('0,01 CHF', $this->getNodeContent($cells[2])); + $cells = $records[3]->getElementsByTagName('td'); + $this->assertCount(3, $cells); + $this->assertSame('2020-05-31', $this->getNodeContent($cells[0])); + $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); + $this->assertSame('0,92 CHF', $this->getNodeContent($cells[2])); + $subtotalCells = $records[4]->getElementsByTagName('td'); + $this->assertCount(2, $subtotalCells); + $this->assertSame('Subtotal', $this->getNodeContent($subtotalCells[0])); + $this->assertSame('12,32 CHF', $this->getNodeContent($subtotalCells[1])); + $vatCells = $records[5]->getElementsByTagName('td'); + $this->assertCount(2, $vatCells); + $this->assertSame('VAT (7.7%)', $this->getNodeContent($vatCells[0])); + $this->assertSame('1,03 CHF', $this->getNodeContent($vatCells[1])); + $totalCells = $records[6]->getElementsByTagName('td'); + $this->assertCount(2, $totalCells); + $this->assertSame('Total', $this->getNodeContent($totalCells[0])); + $this->assertSame('13,35 CHF', $this->getNodeContent($totalCells[1])); + } + + /** * Test receipt PDF output - * - * @return void */ - public function testPdfOutput() + public function testPdfOutput(): void { $wallet = $this->getTestData(); $receipt = new Receipt($wallet, 2020, 5); @@ -117,9 +165,11 @@ /** * Prepare data for a test * + * @param string $country User country code + * * @return \App\Wallet */ - protected function getTestData() + protected function getTestData(string $country = null): Wallet { Bus::fake(); @@ -128,6 +178,7 @@ 'first_name' => 'Firstname', 'last_name' => 'Lastname', 'billing_address' => "Test Unicode Straße 150\n10115 Berlin", + 'country' => $country ]); $wallet = $user->wallets()->first();