From 6235112f74a3963912e94bae5eab6a3b0e30059c Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 9 Jan 2025 11:18:12 +0100 Subject: [PATCH] Send invoices by email. --- app/Http/Controllers/Api/MailController.php | 52 ++++++++ app/Http/Controllers/EController.php | 41 +++++-- app/Http/Controllers/InvoiceController.php | 18 ++- app/Http/Controllers/PdfController.php | 21 +++- app/Mail/InvoiceMail.php | 63 ++++++++++ app/Models/Invoice.php | 13 +- app/TenantMail.php | 32 +++++ lang/de/common.php | 5 + lang/de/configuration.php | 7 ++ lang/de/form.php | 1 + lang/de/invoice.php | 9 +- .../views/components/mail-icon.blade.php | 3 + resources/views/invoice/index.blade.php | 16 ++- resources/views/invoice/mail.blade.php | 113 ++++++++++++++++++ resources/views/invoice/show.blade.php | 1 + resources/views/mail/invoice/sent.blade.php | 3 + resources/views/option/index.blade.php | 91 +++++++++++++- routes/api.php | 1 + routes/web.php | 1 + 19 files changed, 468 insertions(+), 23 deletions(-) create mode 100644 app/Http/Controllers/Api/MailController.php create mode 100644 app/Mail/InvoiceMail.php create mode 100644 app/TenantMail.php create mode 100644 resources/views/components/mail-icon.blade.php create mode 100644 resources/views/invoice/mail.blade.php create mode 100644 resources/views/mail/invoice/sent.blade.php diff --git a/app/Http/Controllers/Api/MailController.php b/app/Http/Controllers/Api/MailController.php new file mode 100644 index 0000000..376c4c8 --- /dev/null +++ b/app/Http/Controllers/Api/MailController.php @@ -0,0 +1,52 @@ +mailer = TenantMail::get(); + } + + /** + * Send the invoice mail for the given request. + */ + public function sendInvoice(Request $request): JsonResponse + { + $invoice = Invoice::find($request->id); + $invoiceMail = new InvoiceMail($invoice); + $invoiceMail->subject($request->Subject); + $invoiceMail->body = $request->Body; + + try { + $this->mailer->to($request->To) + ->cc($request->Cc) + ->Bcc($request->Bcc) + ->send($invoiceMail); + } catch (\Exception $exception) { + return response()->json(['status' => 'error', 'message' => $exception->getMessage()], 500); + } + + $invoice->update(['status' => 'sent']); + + return response()->json($invoice); + } +} diff --git a/app/Http/Controllers/EController.php b/app/Http/Controllers/EController.php index 85822fb..bd51849 100644 --- a/app/Http/Controllers/EController.php +++ b/app/Http/Controllers/EController.php @@ -8,22 +8,45 @@ use Symfony\Component\HttpFoundation\StreamedResponse; class EController extends Controller { + /** + * Return an invoice as download for the specified resource. + */ public function downloadInvoice(int $id): StreamedResponse { $invoice = Invoice::find($id); - - $taxes = []; - foreach ($invoice->items as $item) { - if (!isset($taxes[$item->tax])) { - $taxes[$item->tax] = ['tax' => 0, 'taxable' => 0]; - } - $taxes[$item->tax]['tax'] += round($item->price * $item->amount * $item->tax / 100, 2, PHP_ROUND_HALF_UP); - $taxes[$item->tax]['taxable'] += $item->price * $item->amount; - } + $taxes = self::buildTaxes($invoice->items); return response()->streamDownload(function () use ($invoice, $taxes) { echo view('xml.invoice', ['invoice' => $invoice, 'options' => Option::optionsAsObject(), 'taxes' => $taxes]); }, 'test.xml', ['Content-Type' => 'application/xml']); } + + /** + * Return a rendered xml invoice as string to build an email attachment from it for the specified resource. + */ + public function attachInvoice(int $id): string + { + $invoice = Invoice::find($id); + $taxes = self::buildTaxes($invoice->items); + + return view('xml.invoice', ['invoice' => $invoice, 'options' => Option::optionsAsObject(), 'taxes' => $taxes])->render(); + } + + /** + * Build taxes for the given invoice items grouped by tax. + */ + private static function buildTaxes($items): array + { + $taxes = []; + foreach ($items as $item) { + if (!isset($taxes[$item->tax])) { + $taxes[$item->tax] = ['tax' => 0, 'taxable' => 0]; + } + $taxes[$item->tax]['tax'] += round($item->price * $item->amount * $item->tax / 100, 2, PHP_ROUND_HALF_UP); + $taxes[$item->tax]['taxable'] += $item->price * $item->amount; + } + + return $taxes; + } } diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 2081732..187d236 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -3,14 +3,14 @@ namespace App\Http\Controllers; use App\Models\Invoice; -use Illuminate\Http\Request; +use Illuminate\Contracts\View\View; class InvoiceController extends Controller { /** * Display a listing of the resource. */ - public function index() + public function index(): View { return view('invoice.index'); } @@ -18,7 +18,7 @@ class InvoiceController extends Controller /** * Show the form for creating a new resource. */ - public function create() + public function create(): View { return view('invoice.create'); } @@ -26,7 +26,7 @@ class InvoiceController extends Controller /** * Display the specified resource. */ - public function show(Invoice $invoice) + public function show(Invoice $invoice): View { return view('invoice.show', ['invoice' => $invoice]); } @@ -38,4 +38,14 @@ class InvoiceController extends Controller { // } + + /** + * Show the form for sending the specified invoice. + */ + public function mail(int $id): View + { + $invoice = Invoice::find($id); + + return view('invoice.mail', ['invoice' => $invoice]); + } } diff --git a/app/Http/Controllers/PdfController.php b/app/Http/Controllers/PdfController.php index 1bbea51..e2e900d 100644 --- a/app/Http/Controllers/PdfController.php +++ b/app/Http/Controllers/PdfController.php @@ -9,10 +9,29 @@ use Illuminate\Http\Response; class PdfController extends Controller { + /** + * Return an invoice as download for the specified resource. + */ public function downloadInvoice(int $invoice_id): Response + { + return $this->buildInvoicePdf($invoice_id)->stream(); + } + + /** + * Return a rendered pdf invoice as string to build an email attachment from it for the specified resource. + */ + public function attachInvoice(int $invoice_id): string + { + return $this->buildInvoicePdf($invoice_id)->output(); + } + + /** + * Render the pdf view for the given invoice. + */ + protected function buildInvoicePdf(int $invoice_id): \Barryvdh\DomPDF\PDF { $invoice = Invoice::find($invoice_id); - return Pdf::loadView('pdfs.invoice', ['invoice' => $invoice->load(['address', 'delivery']), 'options' => Option::optionsAsObject()])->stream(); + return Pdf::loadView('pdfs.invoice', ['invoice' => $invoice->load(['address', 'delivery']), 'options' => Option::optionsAsObject()]); } } diff --git a/app/Mail/InvoiceMail.php b/app/Mail/InvoiceMail.php new file mode 100644 index 0000000..8a3a99c --- /dev/null +++ b/app/Mail/InvoiceMail.php @@ -0,0 +1,63 @@ +body = $value; + } + } + + public string $pdf; + + public string $xml; + + /** + * Create a new message instance. + */ + public function __construct(protected Invoice $invoice) + { + $pdf = new PdfController(); + $this->pdf = $pdf->attachInvoice($invoice->id); + + $xml = new EController(); + $this->xml = $xml->attachInvoice($invoice->id); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + html: 'mail.invoice.sent', + with: ['html' => $this->body], + ); + } + + /** + * Get the attachments for the message. + */ + public function attachments(): array + { + return [ + Attachment::fromData(fn() => $this->pdf, __('invoice.Invoice') . '_' . $this->invoice->number . '.pdf') + ->withMime('application/pdf'), + Attachment::fromData(fn() => $this->xml, __('invoice.Invoice') . '_' . $this->invoice->number . '.xml') + ->withMime('application/xml'), + ]; + } +} diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 35f3435..2d21443 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -32,9 +32,20 @@ class Invoice extends Model */ protected $appends = [ 'created', - 'number' + 'number', + 'localized_state' ]; + /** + * Get the invoice state as translated string. + * + * @return string + */ + public function getLocalizedStateAttribute(): string + { + return __('invoice.state_' . $this->status); + } + /** * Get the invoice number formatted */ diff --git a/app/TenantMail.php b/app/TenantMail.php new file mode 100644 index 0000000..a308efc --- /dev/null +++ b/app/TenantMail.php @@ -0,0 +1,32 @@ + $options->mail_transport, + 'host' => $options->mail_host, + 'port' => $options->mail_port, + 'encryption' => (property_exists($options, 'mail_encryption')) ? $options->mail_encryption : null, + 'username' => (property_exists($options, 'mail_encryption')) ? $options->mail_username : null, + 'password' => (property_exists($options, 'mail_encryption')) ? $options->mail_password : null, + ]); + + $mailer->alwaysFrom($options->email, $options->representative); + + return $mailer; + } +} diff --git a/lang/de/common.php b/lang/de/common.php index f552101..a35e61f 100644 --- a/lang/de/common.php +++ b/lang/de/common.php @@ -33,5 +33,10 @@ return [ 'Email Password Reset Link' => 'Email Passwort-Reset Link', 'Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.' => 'Passwort vergessen? Kein Problem. Teilen Sie uns einfach Ihre E-Mail-Adresse mit und wir senden Ihnen per E-Mail einen Link zum Zurücksetzen Ihres Passworts, mit dem Sie ein neues Passwort auswählen können.', 'Date' => 'Datum', + 'Email-To' => 'An', + 'Email-Bcc' => 'Bcc', + 'Email-Cc' => 'Cc', + 'Email-Subject' => 'Betreff', + 'Email-Body' => 'Nachricht', ]; diff --git a/lang/de/configuration.php b/lang/de/configuration.php index fb79dd7..f29571b 100644 --- a/lang/de/configuration.php +++ b/lang/de/configuration.php @@ -44,5 +44,12 @@ return [ 'Color' => 'Farbe', 'Company logo' => 'Firmenlogo', 'Activity' => 'Tätigkeitsfeld', + 'Mail delivery' => 'E-Mail Versand', + 'Mail transport' => 'Versand Typ', + 'Mail host' => 'Server Adresse', + 'Mail port' => 'Port', + 'Mail encryption' => 'Verschlüsselung', + 'Mail username' => 'Benutzername', + 'Mail password' => 'Passwort', ]; diff --git a/lang/de/form.php b/lang/de/form.php index dbb1736..29fb22f 100644 --- a/lang/de/form.php +++ b/lang/de/form.php @@ -19,5 +19,6 @@ return [ 'SaveAndContinue' => 'Speichern und Weiter', 'Saved' => 'Gespeichert', 'Cancel' => 'Abbrechen', + 'Send' => 'Senden', ]; diff --git a/lang/de/invoice.php b/lang/de/invoice.php index ed76e91..5cd2683 100644 --- a/lang/de/invoice.php +++ b/lang/de/invoice.php @@ -43,6 +43,13 @@ return [ 'Item total price short' => 'G-Preis', 'Net long' => 'Netto zuzüglich MwSt.', 'Gross long' => 'Gesamtpreis inkl. gesetzlicher MwSt.', - 'Final sentence' => 'Bitte überweisen Sie den fälligen Rechnungsbetrag in Höhe von :sum bis spätestens :date auf das unten genannte Konto.' + 'Final sentence' => 'Bitte überweisen Sie den fälligen Rechnungsbetrag in Höhe von :sum bis spätestens :date auf das unten genannte Konto.', + 'state_created' => 'Erstellt', + 'state_sent' => 'Gesendet', + 'Mail' => 'E-Mail', + 'Send email to your customer with attachments.' => 'E-Mail mit Anlagen an den Kunden versenden.', + 'Invoice body' => 'Sehr geehrte Kundin, sehr geehrter Kunde\n\nim Anhang erhalten Sie die Rechnung :invoice_number.\n\nMit freundlichen Grüßen', + 'Send Pdf' => 'Rechnung als Pdf versenden', + 'Send Xml' => 'Rechnung als Xml versenden', ]; diff --git a/resources/views/components/mail-icon.blade.php b/resources/views/components/mail-icon.blade.php new file mode 100644 index 0000000..a467ac9 --- /dev/null +++ b/resources/views/components/mail-icon.blade.php @@ -0,0 +1,3 @@ +merge(['class' => 'size-8 p-1']) }}> + + diff --git a/resources/views/invoice/index.blade.php b/resources/views/invoice/index.blade.php index a9776a9..a59c4d3 100644 --- a/resources/views/invoice/index.blade.php +++ b/resources/views/invoice/index.blade.php @@ -55,18 +55,22 @@
-
-
+
+
-
-
{{ __('invoice.Sum') }}
-
+
+ +
+
{{ __('invoice.Net') }}
+
{{ __('invoice.Tax') }}
-
+
+
{{ __('invoice.Sum') }}
+
diff --git a/resources/views/invoice/mail.blade.php b/resources/views/invoice/mail.blade.php new file mode 100644 index 0000000..02f7c5e --- /dev/null +++ b/resources/views/invoice/mail.blade.php @@ -0,0 +1,113 @@ + + +
+

+ {{ __('invoice.Invoice') }} {{ $invoice->number }} +

+
+
+ +
+
+ +
+
+
+
+

+ {{ __('invoice.Mail') }} +

+

+ {{ __("invoice.Send email to your customer with attachments.") }} +

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ {{ __('form.Send') }} +
+ +
+
+
+
+
+
+ +
+ + diff --git a/resources/views/invoice/show.blade.php b/resources/views/invoice/show.blade.php index 4229406..f9f9629 100644 --- a/resources/views/invoice/show.blade.php +++ b/resources/views/invoice/show.blade.php @@ -5,6 +5,7 @@ {{ __('invoice.Invoice') }} {{ $invoice->number }}

+

diff --git a/resources/views/mail/invoice/sent.blade.php b/resources/views/mail/invoice/sent.blade.php new file mode 100644 index 0000000..40fb867 --- /dev/null +++ b/resources/views/mail/invoice/sent.blade.php @@ -0,0 +1,3 @@ + +{!! nl2br($html) !!} + diff --git a/resources/views/option/index.blade.php b/resources/views/option/index.blade.php index 8653ae4..ddb9fe6 100644 --- a/resources/views/option/index.blade.php +++ b/resources/views/option/index.blade.php @@ -199,7 +199,7 @@
- +
@@ -233,6 +233,95 @@
+ + +
+
+
+
+ + {{ __('configuration.Mail delivery') }} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+
+
+
+ +
{{ __('form.Save') }}
diff --git a/routes/api.php b/routes/api.php index 63ab152..a3ac8b0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -29,6 +29,7 @@ Route::group(['as' => 'api.'], function () { Route::apiResource('/invoice.item', InvoiceItemController::class)->shallow(); Route::get('/option', [OptionController::class, 'index'])->name('option.index'); Route::post('/option', [OptionController::class, 'store'])->name('option.store'); + Route::post('/sendInvoice', [\App\Http\Controllers\Api\MailController::class, 'sendInvoice'])->name('sendInvoice'); }); }); diff --git a/routes/web.php b/routes/web.php index c57574f..8739c22 100644 --- a/routes/web.php +++ b/routes/web.php @@ -28,6 +28,7 @@ Route::middleware('auth')->group(function () { Route::get('/option', [OptionController::class, 'index'])->name('option.index'); Route::get('/invoice/{id}/pdf-download', [PdfController::class, 'downloadInvoice'])->name('invoice.pdfDownload'); Route::get('/invoice/{id}/xml-download', [EController::class, 'downloadInvoice'])->name('invoice.eDownload'); + Route::get('/invoice/{id}/mail', [InvoiceController::class, 'mail'])->name('invoice.mail'); }); require __DIR__.'/auth.php';