diff --git a/.idea/.idea.TinyInvoice/.idea/riderPublish.xml b/.idea/.idea.TinyInvoice/.idea/riderPublish.xml new file mode 100644 index 0000000..8e8355d --- /dev/null +++ b/.idea/.idea.TinyInvoice/.idea/riderPublish.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/Server/Components/Pages/InvoiceListPage.razor b/Server/Components/Pages/InvoiceListPage.razor index e812110..f17c2f0 100644 --- a/Server/Components/Pages/InvoiceListPage.razor +++ b/Server/Components/Pages/InvoiceListPage.razor @@ -21,9 +21,10 @@ @($"{invoice.TotalNetto:N2} €") Netto

Bearbeiten + @if (invoice.DeletionAllowed) { - + } diff --git a/Server/Components/Pages/InvoiceListPage.razor.cs b/Server/Components/Pages/InvoiceListPage.razor.cs index f3b4d4c..44aadc2 100644 --- a/Server/Components/Pages/InvoiceListPage.razor.cs +++ b/Server/Components/Pages/InvoiceListPage.razor.cs @@ -1,4 +1,6 @@ using Microsoft.AspNetCore.Components; +using QuestPDF.Fluent; +using QuestPDF.Infrastructure; using Server.Data; using Server.Model; @@ -19,6 +21,7 @@ namespace Server.Components.Pages { parameters.SetParameterProperties(this); + await SettingsData.LoadAsync(); Invoices = await InvoiceData.LoadAllAsync(); await base.SetParametersAsync(ParameterView.Empty); @@ -41,5 +44,22 @@ namespace Server.Components.Pages } #endregion + + #region Private Method GenerateDocumentAsync + + /// + /// Generates a PDF document asynchronously for the given invoice. + /// + /// The invoice model for which to generate the document. + /// A task representing the asynchronous document generation operation. + private Task GenerateDocumentAsync(InvoiceModel invoice) + { + QuestPDF.Settings.License = LicenseType.Community; + var doc = new InvoiceDocument(invoice); + doc.GeneratePdfAndShow(); + return Task.CompletedTask; + } + + #endregion } } \ No newline at end of file diff --git a/Server/Extensions.cs b/Server/Extensions.cs index c36a9d7..5d3507c 100644 --- a/Server/Extensions.cs +++ b/Server/Extensions.cs @@ -1,7 +1,25 @@ -namespace Server +using QuestPDF.Fluent; +using QuestPDF.Infrastructure; + +namespace Server { public static class Extensions { - public static bool IsNullOrWhiteSpace(this string? value) => string.IsNullOrWhiteSpace(value); + #region Public Method TextSmall + + /// Adds a small text element to the specified container with an optional bold style. + /// The container to which the text element will be added. + /// The text content to be displayed. + /// Indicates whether the text should be displayed in bold. Defaults to false. + public static void TextSmall(this IContainer container, string? text, bool bold = false) + { + container.Text(txt => + { + var spanDescriptor = txt.Span(text).FontSize(8); + if (bold) spanDescriptor.SemiBold(); + }); + } + + #endregion } } \ No newline at end of file diff --git a/Server/Model/InvoiceDocument.cs b/Server/Model/InvoiceDocument.cs index 8563513..9ad5481 100644 --- a/Server/Model/InvoiceDocument.cs +++ b/Server/Model/InvoiceDocument.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Net.Mime; +using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; @@ -13,13 +14,19 @@ namespace Server.Model public void Compose(IDocumentContainer container) { + if(File.Exists("NotoSans-VariableFont_wdth,wght.ttf")) + FontManager.RegisterFont(File.OpenRead("NotoSans-VariableFont_wdth,wght.ttf")); + container.Page(page => { page.Size(PageSizes.A4); page.Margin(10, Unit.Millimetre); page.MarginLeft(20, Unit.Millimetre); page.PageColor(Colors.White); - page.DefaultTextStyle(x => x.FontSize(12).FontFamily(Fonts.Calibri).Light()); + page.DefaultTextStyle(x => x + .FontSize(10) + .FontFamily("NotoSans") + .Light()); page.Header().Element(ComposeHeader); page.Content().Element(ComposeContent); @@ -30,38 +37,126 @@ namespace Server.Model private void ComposeContent(IContainer container) { - container.PaddingVertical(40).Column(column => + container.PaddingVertical(40).Column(outerColumn => { - column.Spacing(5); + outerColumn.Item().Element(ComposeItemTable); + + outerColumn.Item().Row(row => + { + row.RelativeItem().PaddingRight(10).Column(col => + { + if (!string.IsNullOrWhiteSpace(Model.Comment)) + col.Item().PaddingTop(25).Element(ComposeComments); + }); + + row.AutoItem().Column(col => + { + col.Item().Element(ComposeTaxTable); + }); + }); + + outerColumn.Spacing(5); + }); + } - column.Item().Element(ComposeTable); + private void ComposeTaxTable(IContainer container) + { + container.PaddingTop(25).Table(table => + { + table.ColumnsDefinition(c => + { + c.ConstantColumn(70); + c.ConstantColumn(25); + c.ConstantColumn(70); + c.ConstantColumn(45); + }); + + table.Header(header => + { + header.Cell(); + header.Cell().AlignRight().TextSmall("", true); + header.Cell().AlignRight().TextSmall("Zwischensumme", true); + header.Cell().AlignRight().TextSmall("", true); + + header.Cell().Element(c => c.BorderBottom(1).BorderColor(Colors.Black)); + header.Cell().Element(c => c.BorderBottom(1).BorderColor(Colors.Black)).AlignRight().TextSmall("USt. %", true); + header.Cell().Element(c => c.BorderBottom(1).BorderColor(Colors.Black)).AlignRight().TextSmall("(ohne USt.)", true); + header.Cell().Element(c => c.BorderBottom(1).BorderColor(Colors.Black)).AlignRight().TextSmall("USt.", true); + }); - if (!string.IsNullOrWhiteSpace(Model.Comment)) - column.Item().PaddingTop(25).Element(ComposeComments); + var totalTax = 0d; + + // 7% + var tax7Items = Model.Items?.Where(x => x.TaxType == TaxType.Tax7).ToList() ?? []; + if (tax7Items.Count > 0) + { + var tax7Netto = tax7Items.Sum(x => x.PriceNetto * x.Quantity); + var tax7Tax = tax7Netto * 0.07; + totalTax += tax7Tax; + + table.Cell(); + table.Cell().AlignRight().TextSmall("7%"); + table.Cell().AlignRight().TextSmall($"{tax7Netto:N2} €"); + table.Cell().AlignRight().TextSmall($"{tax7Tax:N2} €"); + } + + // 19% + var tax19Items = Model.Items?.Where(x => x.TaxType == TaxType.Tax19).ToList() ?? []; + if (tax19Items.Count > 0) + { + var tax19Netto = tax19Items.Sum(x => x.PriceNetto * x.Quantity); + var tax19Tax = tax19Netto * 0.19; + totalTax += tax19Tax; + + table.Cell(); + table.Cell().AlignRight().TextSmall("19%"); + table.Cell().AlignRight().TextSmall($"{tax19Netto:N2} €"); + table.Cell().AlignRight().TextSmall($"{tax19Tax:N2} €"); + } + + // SUMME + table.Cell().Element(c => c.BorderTop(1).BorderColor(Colors.Black)).TextSmall("USt. Gesamt"); + table.Cell().Element(c => c.BorderTop(1).BorderColor(Colors.Black)); + table.Cell().Element(c => c.BorderTop(1).BorderColor(Colors.Black)).AlignRight().TextSmall($"{Model.Items?.Sum(x => x.PriceNetto * x.Quantity):N2} €"); + table.Cell().Element(c => c.BorderTop(1).BorderColor(Colors.Black)).AlignRight().TextSmall($"{totalTax:N2} €"); }); } - private void ComposeTable(IContainer container) + private void ComposeItemTable(IContainer container) { container.Table(table => { // Define columns table.ColumnsDefinition(columns => { - columns.ConstantColumn(30, Unit.Millimetre); columns.RelativeColumn(3); - columns.RelativeColumn(); - columns.RelativeColumn(); + columns.ConstantColumn(20, Unit.Millimetre); + + columns.ConstantColumn(24, Unit.Millimetre); + columns.ConstantColumn(15, Unit.Millimetre); + columns.ConstantColumn(24, Unit.Millimetre); + columns.ConstantColumn(24, Unit.Millimetre); }); // Describe header table.Header(header => { - header.Cell().Element(CellStyle).AlignCenter().Text("Menge"); - header.Cell().Element(CellStyle).Text("Bezeichnung"); - header.Cell().Element(CellStyle).AlignRight().Text("Einzelpreis"); - header.Cell().Element(CellStyle).AlignRight().Text("Gesamtpreis"); - + + header.Cell().Text("").SemiBold(); + header.Cell().Text("").SemiBold(); + header.Cell().AlignRight().Text("Einzelpreis").SemiBold(); + header.Cell().Text("").SemiBold(); + header.Cell().AlignRight().Text("Einzelpreis").SemiBold(); + header.Cell().AlignRight().Text("Gesamtpreis").SemiBold(); + + + header.Cell().Element(CellStyle).Text("Beschreibung").SemiBold(); + header.Cell().Element(CellStyle).AlignCenter().Text("Menge").SemiBold(); + header.Cell().Element(CellStyle).AlignRight().Text("(ohne USt.)").SemiBold(); + header.Cell().Element(CellStyle).AlignRight().Text("USt. %").SemiBold(); + header.Cell().Element(CellStyle).AlignRight().Text("(inkl USt.)").SemiBold(); + header.Cell().Element(CellStyle).AlignRight().Text("(inkl USt.)").SemiBold(); + static IContainer CellStyle(IContainer container) { return container.DefaultTextStyle(x => x.SemiBold()).BorderBottom(1).BorderColor(Colors.Black); @@ -71,54 +166,28 @@ namespace Server.Model // Describe content foreach (var item in Model.Items ?? []) { - table.Cell().Element(CellStyle).AlignCenter().Text(txt => txt.Span($"{item.Quantity:N2}")); + + // Beschreibung table.Cell().Element(CellStyle).Column(column => { column.Item().Text(item.Name); - if(!string.IsNullOrWhiteSpace(item.Description)) column.Item().Text(txt => - { - txt.Span(item.Description).FontSize(8); - }); + if(!string.IsNullOrWhiteSpace(item.Description)) + column.Item().TextSmall(item.Description); }); + + table.Cell().Element(CellStyle).AlignCenter().Text(txt => txt.Span($"{item.Quantity:N2}")); // string as currency format table.Cell().Element(CellStyle).AlignRight().Text($"{item.PriceNetto:N2} €"); - table.Cell().Element(CellStyle).AlignRight().Text($"{item.PriceNetto * item.Quantity:N2} €"); + table.Cell().Element(CellStyle).AlignRight().Text($"{(int)item.TaxType}%"); + table.Cell().Element(CellStyle).AlignRight().Text($"{item.PriceBrutto:N2} €"); + table.Cell().Element(CellStyle).AlignRight().Text($"{item.PriceBrutto * item.Quantity:N2} €"); continue; } - - // Gesamt Netto - table.Cell().ColumnSpan(3).PaddingTop(10).Text("Gesamt Netto"); - table.Cell().PaddingTop(10).AlignRight().Text($"{Model.TotalNetto:N2} €"); - // 7% - var tax7Items = Model.Items?.Where(x => x.TaxType == TaxType.Tax7).ToList() ?? []; - if (tax7Items.Count > 0) - { - var tax7Netto = tax7Items.Sum(x => x.PriceNetto * x.Quantity); - var tax7Tax = tax7Netto * 0.07; - - table.Cell().ColumnSpan(2).Text("zzgl. 7% USt. auf"); - table.Cell().AlignRight().Text($"{tax7Netto:N2} €"); - table.Cell().AlignRight().Text($"{tax7Tax:N2} €"); - } - - // 19% - var tax19Items = Model.Items?.Where(x => x.TaxType == TaxType.Tax19).ToList() ?? []; - if (tax19Items.Count > 0) - { - var tax19Netto = tax19Items.Sum(x => x.PriceNetto * x.Quantity); - var tax19Tax = tax19Netto * 0.19; - table.Cell().ColumnSpan(2).Text("zzgl. 19% USt. auf"); - table.Cell().AlignRight().Text($"{tax19Netto:N2} €"); - table.Cell().AlignRight().Text($"{tax19Tax:N2} €"); - } - - - - table.Cell().ColumnSpan(3).Element(CellStyle).PaddingTop(10).Text("Rechnungsbetrag").Medium(); + table.Cell().ColumnSpan(5).Element(CellStyle).PaddingTop(10).Text("Rechnungsbetrag").Medium(); table.Cell().Element(CellStyle).PaddingTop(10).AlignRight().Text($"{Model.Items.Sum(x=> x.PriceBrutto*x.Quantity):N2} €").Medium(); return; @@ -140,37 +209,27 @@ namespace Server.Model { container.Column(outerColumn => { - outerColumn.Item().DefaultTextStyle(style => style.FontSize(10)).Row(row => + outerColumn.Item().Row(row => { row.RelativeItem().Column(column => { - column.Item().Text(txt => txt.Span(Model.Seller?.Name).Bold()); - column.Item().Text(txt => txt.Span(Model.Seller?.Street)); - column.Item().Text(txt => txt.Span($"{Model.Seller?.Zip} {Model.Seller?.City}")); + column.Item().TextSmall(Model.Seller?.Name, true); + column.Item().TextSmall(Model.Seller?.Street); + column.Item().TextSmall($"{Model.Seller?.Zip} {Model.Seller?.City}"); }); row.RelativeItem().Column(column => { - if(!string.IsNullOrWhiteSpace(Model.Seller?.Phone)) - column.Item().Text(txt => txt.Span($"Tel.: {Model.Seller?.Phone}")); - - if(!string.IsNullOrWhiteSpace(Model.Seller?.Email)) - column.Item().Text(txt => txt.Span($"E-Mail: {Model.Seller?.Email}")); - - if(!string.IsNullOrWhiteSpace(Model.Seller?.Web)) - column.Item().Text(txt => txt.Span($"Web: {Model.Seller?.Web}")); + if(!string.IsNullOrWhiteSpace(Model.Seller?.Phone)) column.Item().TextSmall($"Tel.: {Model.Seller?.Phone}"); + if(!string.IsNullOrWhiteSpace(Model.Seller?.Email)) column.Item().TextSmall($"E-Mail: {Model.Seller?.Email}"); + if(!string.IsNullOrWhiteSpace(Model.Seller?.Web)) column.Item().TextSmall($"Web: {Model.Seller?.Web}"); }); row.RelativeItem().Column(column => { - if(!string.IsNullOrWhiteSpace(Model.PaymentData?.BankName)) - column.Item().Text(txt => txt.Span(Model.PaymentData?.BankName)); - - if(!string.IsNullOrWhiteSpace(Model.PaymentData?.Iban)) - column.Item().Text(txt => txt.Span($"IBAN: {Model.PaymentData?.Iban}")); - - if(!string.IsNullOrWhiteSpace(Model.PaymentData?.Bic)) - column.Item().Text(txt => txt.Span($"BIC: {Model.PaymentData?.Bic}")); + if(!string.IsNullOrWhiteSpace(Model.PaymentData?.BankName)) column.Item().TextSmall(Model.PaymentData?.BankName); + if(!string.IsNullOrWhiteSpace(Model.PaymentData?.Iban)) column.Item().TextSmall($"IBAN: {Model.PaymentData?.Iban}"); + if(!string.IsNullOrWhiteSpace(Model.PaymentData?.Bic)) column.Item().TextSmall($"BIC: {Model.PaymentData?.Bic}"); }); }); @@ -187,9 +246,9 @@ namespace Server.Model { container.Column(outerColumn => { - outerColumn.Item().AlignCenter().Row(row => + outerColumn.Item().Row(row => { - row.RelativeItem().AlignCenter().Column(col => + row.RelativeItem().Column(col => { if (!string.IsNullOrWhiteSpace(SettingsData.Instance.Logo) && File.Exists(SettingsData.Instance.Logo)) @@ -198,13 +257,18 @@ namespace Server.Model col.Item().Height(25, Unit.Millimetre); }); + + row.RelativeItem().AlignMiddle().PaddingRight(10, Unit.Millimetre).Background(Colors.Red.Medium).Column(col => + { + col.Item().AlignRight().Text("www.example.com"); + }); }); outerColumn.Item().PaddingTop(15, Unit.Millimetre).Row(row => { row.AutoItem().Column(column => { - column.Item().Text(Model.Seller?.ToString()).Style(new TextStyle().FontSize(8).SemiBold().Underline()); + column.Item().PaddingBottom(5).TextSmall(Model.Seller?.ToString(), true); if(!string.IsNullOrWhiteSpace(Model.Customer?.Name)) column.Item().Text(Model.Customer?.Name); if(!string.IsNullOrWhiteSpace(Model.Customer?.Name2)) column.Item().Text(Model.Customer?.Name2); column.Item().Text(Model.Customer?.Street); @@ -213,27 +277,24 @@ namespace Server.Model row.RelativeItem(); - row.AutoItem().DefaultTextStyle(style => style.FontSize(10)).Column(column => + row.AutoItem().Column(col => { - column.Item().Text(txt => + col.Item().Table(table => { - txt.Span("Steuer-Nr:").Bold().Underline(); - txt.Span(" "); - txt.Span(Model.Seller?.TaxId); - }); + table.ColumnsDefinition(c => + { + c.ConstantColumn(50); + c.ConstantColumn(50); + }); - column.Item().Text(txt => - { - txt.Span("Datum:").Bold().Underline(); - txt.Span(" "); - txt.Span(Model.IssueDate.ToShortDateString()); - }); - - column.Item().Text(txt => - { - txt.Span("Rechnung:").Bold(); - txt.Span(" "); - txt.Span($"#{Model.InvoiceId}"); + table.Cell().TextSmall("Steuer-Nr:", true); + table.Cell().TextSmall(Model.Seller?.TaxId); + + table.Cell().TextSmall("Datum:", true); + table.Cell().TextSmall(Model.IssueDate.ToShortDateString()); + + table.Cell().TextSmall("Rechnung:", true); + table.Cell().TextSmall($"#{Model.InvoiceId}"); }); }); }); diff --git a/Server/Model/InvoiceModel.cs b/Server/Model/InvoiceModel.cs index 415ea8d..e974293 100644 --- a/Server/Model/InvoiceModel.cs +++ b/Server/Model/InvoiceModel.cs @@ -69,6 +69,7 @@ public enum TaxType { - Tax19, Tax7 + Tax19 = 19, + Tax7 = 7 } } \ No newline at end of file diff --git a/Server/NotoSans-VariableFont_wdth,wght.ttf b/Server/NotoSans-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..ceb7b2f Binary files /dev/null and b/Server/NotoSans-VariableFont_wdth,wght.ttf differ