diff --git a/ucalc/BillingWindow.xaml.cs b/ucalc/BillingWindow.xaml.cs index 2209a42..1d8df98 100644 --- a/ucalc/BillingWindow.xaml.cs +++ b/ucalc/BillingWindow.xaml.cs @@ -186,17 +186,17 @@ namespace UCalc } } - public bool Print() + public void Print() { if (Model.Root.Errors.Count > 0) { MessageBox.Show( "Bitte beheben Sie zuerst die angezeigten Fehler, bevor Sie das Dokument drucken können.", "Fehler!", MessageBoxButton.OK, MessageBoxImage.Error); - return false; + return; } - throw new NotImplementedException(); + new PrintWindow(Model) {Owner = this}.ShowDialog(); } private void OnDetailsTabSelected(object sender, RoutedEventArgs e) diff --git a/ucalc/Constants.cs b/ucalc/Constants.cs index c44ebbc..cb2cfd5 100644 --- a/ucalc/Constants.cs +++ b/ucalc/Constants.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Immutable; using System.Linq; +using System.Windows; using System.Windows.Media; using UCalc.Controls; using UCalc.Data; @@ -26,5 +27,33 @@ namespace UCalc public static readonly ImmutableList CostDivisionStrs = ((CostDivision[]) Enum.GetValues(typeof(CostDivision))).Select(value => value.AsString()).ToImmutableList(); + + public static readonly double DinA4Width; + public static readonly double DinA4Height; + public static readonly Thickness DinA4Padding; + + public static double DinA4ContentWidth => DinA4Width - DinA4Padding.Left - DinA4Padding.Right; + + public static readonly double PrintDefaultFontSize; + public static readonly double PrintSubjectFontSize; + public static readonly double PrintNewlineFontSize; + + static Constants() + { + var converter = new LengthConverter(); + // ReSharper disable PossibleNullReferenceException + DinA4Width = (double) converter.ConvertFromInvariantString("29.7cm"); + DinA4Height = (double) converter.ConvertFromInvariantString("42cm"); + var dinA4MarginLeftRight = (double) converter.ConvertFromInvariantString("3.18cm"); + var dinA4MarginTopBottom = (double) converter.ConvertFromInvariantString("2.54cm"); + + PrintDefaultFontSize = (double) converter.ConvertFromInvariantString("14pt"); + PrintSubjectFontSize = (double) converter.ConvertFromInvariantString("16pt"); + PrintNewlineFontSize = (double) converter.ConvertFromInvariantString("4pt"); + // ReSharper restore PossibleNullReferenceException + + DinA4Padding = new Thickness(dinA4MarginLeftRight, dinA4MarginTopBottom, dinA4MarginLeftRight, + dinA4MarginTopBottom); + } } } \ No newline at end of file diff --git a/ucalc/Controls/PrintableDocument.cs b/ucalc/Controls/PrintableDocument.cs new file mode 100644 index 0000000..b8ed7c0 --- /dev/null +++ b/ucalc/Controls/PrintableDocument.cs @@ -0,0 +1,109 @@ +using System; +using System.IO; +using System.IO.Packaging; +using System.Linq; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Markup; +using System.Windows.Xps.Packaging; +using System.Windows.Xps.Serialization; + +namespace UCalc.Controls +{ + public class PrintableDocument : IDisposable + { + private static readonly Uri DocUri = new Uri($"pack://mietrechner{new Guid()}.xps"); + private readonly Package _package; + private readonly XpsDocument _document; + + private int PageCount => + _document.GetFixedDocumentSequence()!.References.First().GetDocument(false)!.Pages.Count; + + public PrintableDocument(IDocumentPaginatorSource source) + { + _package = Package.Open(new MemoryStream(), FileMode.Create, FileAccess.ReadWrite); + + PackageStore.AddPackage(DocUri, _package); + + _document = new XpsDocument(_package, CompressionOption.SuperFast, DocUri.AbsoluteUri); + var manager = new XpsSerializationManager(new XpsPackagingPolicy(_document), false); + + var paginator = source.DocumentPaginator; + manager.SaveAsXaml(paginator); + } + + public void Dispose() + { + ((IDisposable) _document).Dispose(); + ((IDisposable) _package).Dispose(); + PackageStore.RemovePackage(DocUri); + } + + public void PreviewIn(DocumentViewer viewer) + { + viewer.Document = _document.GetFixedDocumentSequence(); + } + + public void Print(string description) + { + var dialog = new PrintDialog {UserPageRangeEnabled = true, MinPage = 1, MaxPage = (uint) PageCount}; + + if (dialog.ShowDialog() == true) + { + var documentSequence = _document.GetFixedDocumentSequence(); + Package convPackage = null; + XpsDocument convDocument = null; + + try + { + if (dialog.PageRangeSelection == PageRangeSelection.UserPages) + { + convDocument = ExtractPages(_document, dialog.PageRange.PageFrom - 1, + dialog.PageRange.PageTo - 1, out convPackage); + documentSequence = convDocument.GetFixedDocumentSequence(); + } + + dialog.PrintDocument(documentSequence!.DocumentPaginator, description); + } + finally + { + convDocument?.Close(); + convPackage?.Close(); + } + } + } + + private static XpsDocument ExtractPages(XpsDocument source, int fromPage, int toPage, out Package package) + { + package = Package.Open(new MemoryStream(), FileMode.Create, FileAccess.ReadWrite); + + var docUri = new Uri("pack://mietrechnerTempTicket.xps"); + PackageStore.RemovePackage(docUri); + PackageStore.AddPackage(docUri, package); + + var document = new XpsDocument(package, CompressionOption.SuperFast, docUri.AbsoluteUri); + var pages = source.GetFixedDocumentSequence()!.References.First().GetDocument(false)!.Pages; + + var documentReference = new DocumentReference(); + var fixedDocument = new FixedDocument(); + documentReference.SetDocument(fixedDocument); + + for (var i = fromPage; i <= toPage; ++i) + { + var page = pages[i]; + var pageContentChild = new PageContent {Source = page.Source}; + ((IUriContext) pageContentChild).BaseUri = ((IUriContext) page).BaseUri; + pageContentChild.GetPageRoot(false); + fixedDocument.Pages.Add(pageContentChild); + } + + var documentSequence = new FixedDocumentSequence(); + documentSequence.References.Add(documentReference); + + var writer = XpsDocument.CreateXpsDocumentWriter(document); + writer.Write(documentSequence); + + return document; + } + } +} \ No newline at end of file diff --git a/ucalc/Data/BillingCalculator.cs b/ucalc/Data/BillingCalculator.cs index 2b9c8e8..0094ac5 100644 --- a/ucalc/Data/BillingCalculator.cs +++ b/ucalc/Data/BillingCalculator.cs @@ -10,15 +10,17 @@ namespace UCalc.Data { public Tenant Tenant { get; } public IReadOnlyDictionary Costs { get; } + public decimal SubTotalAmount { get; } public decimal TotalAmount { get; } public string Details { get; } public string DetailsForLandlord { get; } public TenantCalculationResult(Tenant tenant, IReadOnlyDictionary costs, - decimal totalAmount, string details, string detailsForLandlord) + decimal subTotalAmount, decimal totalAmount, string details, string detailsForLandlord) { Tenant = tenant; Costs = costs; + SubTotalAmount = subTotalAmount; TotalAmount = totalAmount; Details = details; DetailsForLandlord = detailsForLandlord; @@ -616,6 +618,7 @@ namespace UCalc.Data var str = $"Zwischensumme: {totalAmount.CeilToString()} €\n"; str += $"Bereits bezahlt: {tenant.PaidRent.CeilToString()} €\n\n"; + var subTotalAmount = totalAmount; totalAmount -= tenant.PaidRent; totalAmount = totalAmount.Ceil2(); @@ -624,7 +627,7 @@ namespace UCalc.Data details.Append(str); detailsLandlord.Append(str); - return new TenantCalculationResult(tenant, costResults, totalAmount, details.ToString(), + return new TenantCalculationResult(tenant, costResults, subTotalAmount, totalAmount, details.ToString(), detailsLandlord.ToString()); } } diff --git a/ucalc/Pages/SideBar.xaml.cs b/ucalc/Pages/SideBar.xaml.cs index b6dc024..021d511 100644 --- a/ucalc/Pages/SideBar.xaml.cs +++ b/ucalc/Pages/SideBar.xaml.cs @@ -50,7 +50,8 @@ namespace UCalc.Pages private void OnAboutClick(object sender, RoutedEventArgs e) { - throw new System.NotImplementedException(); + MessageBox.Show("MietRechner Version 2.0\n\nCopyright © 2020 by Tobias Erbshäußer", "Über MietRechner", + MessageBoxButton.OK, MessageBoxImage.Information); } } } \ No newline at end of file diff --git a/ucalc/PrintWindow.xaml b/ucalc/PrintWindow.xaml new file mode 100644 index 0000000..03dc0a5 --- /dev/null +++ b/ucalc/PrintWindow.xaml @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ucalc/PrintWindow.xaml.cs b/ucalc/PrintWindow.xaml.cs new file mode 100644 index 0000000..feb2615 --- /dev/null +++ b/ucalc/PrintWindow.xaml.cs @@ -0,0 +1,345 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Documents; +using System.Windows.Media; +using UCalc.Annotations; +using UCalc.Controls; +using UCalc.Data; +using UCalc.Models; + +namespace UCalc +{ + public class TenantMenuItem : INotifyPropertyChanged + { + public Tenant Tenant { get; } + public bool None { get; } + private bool _selected; + + public bool Selected + { + get => _selected; + set + { + if (value == _selected) + { + return; + } + + _selected = value; + OnPropertyChanged(); + } + } + + public TenantMenuItem(Tenant tenant, bool none) + { + Tenant = tenant; + None = none; + Selected = tenant != null; + } + + public string Name => Tenant != null ? Tenant.Name : None ? "Kein Mieter" : "Alle Mieter"; + + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + private void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + + public partial class PrintWindow + { + public Billing Billing { get; } + public IReadOnlyList TenantMenuItems { get; } + private PrintableDocument _document; + + + public PrintWindow(Model model) + { + Billing = model.Dump(); + TenantMenuItems = new[] {new TenantMenuItem(null, false), new TenantMenuItem(null, true)}.Concat( + Billing.Tenants.Select(tenant => new TenantMenuItem(tenant, false))).ToList(); + + foreach (var item in TenantMenuItems) + { + item.PropertyChanged += OnSelectedTenantChanged; + } + + InitializeComponent(); + + Preview(); + } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + + _document?.Dispose(); + } + + private void Preview() + { + Document.Blocks.Clear(); + + foreach (var item in TenantMenuItems) + { + if (!item.Selected) + { + continue; + } + + var result = BillingCalculator.CalculateForTenant(Billing, item.Tenant); + PreviewForSingleTenant(item.Tenant, result); + } + + _document?.Dispose(); + _document = new PrintableDocument(Document); + _document.PreviewIn(Viewer); + } + + private static void AddLineBreaks(TableRowGroup rowGroup, int count) + { + var row = new TableRow(); + rowGroup.Rows.Add(row); + + var cell = new TableCell {ColumnSpan = 2}; + row.Cells.Add(cell); + cell.Blocks.Add(new Paragraph(new Run(new string('\n', count)) + {FontSize = Constants.PrintNewlineFontSize})); + } + + private static void AddText(TableRowGroup rowGroup, string text, double? fontSize = null, + bool alignRight = false) + { + var row = new TableRow(); + rowGroup.Rows.Add(row); + + var cell = new TableCell {ColumnSpan = 2}; + row.Cells.Add(cell); + + if (alignRight) + { + cell.TextAlignment = TextAlignment.Right; + } + + var paragraph = new Paragraph(new Run(text)); + cell.Blocks.Add(paragraph); + + if (fontSize != null) + { + paragraph.FontSize = fontSize.Value; + } + } + + private void PreviewForSingleTenant(Tenant tenant, TenantCalculationResult result) + { + var section = new Section {BreakPageBefore = true, FontSize = Constants.PrintDefaultFontSize}; + Document.Blocks.Add(section); + + var table = new Table {CellSpacing = 0}; + section.Blocks.Add(table); + table.Columns.Add(new TableColumn {Width = new GridLength(1, GridUnitType.Star)}); + table.Columns.Add(new TableColumn {Width = new GridLength(1, GridUnitType.Star)}); + + var rowGroup = new TableRowGroup(); + table.RowGroups.Add(rowGroup); + + void AddLineBreaks(int count) + { + PrintWindow.AddLineBreaks(rowGroup, count); + } + + void AddText(string text, double? fontSize = null, bool alignRight = false) + { + PrintWindow.AddText(rowGroup, text, fontSize, alignRight); + } + + void AddCost(string name, decimal amount, bool isLast = false) + { + var row2 = new TableRow(); + rowGroup.Rows.Add(row2); + + if (isLast) + { + row2.Background = Brushes.LightGray; + } + + var cell2 = new TableCell + {BorderBrush = Brushes.Gray, BorderThickness = new Thickness(1, 1, 0, isLast ? 1 : 0)}; + row2.Cells.Add(cell2); + + cell2.Blocks.Add(new Paragraph(new Run(name)) {Padding = new Thickness(6)}); + + cell2 = new TableCell + { + BorderBrush = Brushes.Gray, BorderThickness = new Thickness(1, 1, 1, isLast ? 1 : 0), + TextAlignment = TextAlignment.Right + }; + row2.Cells.Add(cell2); + + cell2.Blocks.Add(new Paragraph(new Run($"{amount.CeilToString()} €")) {Padding = new Thickness(6)}); + } + + AddText( + $"{Billing.Landlord.Salutation.AsString()} {Billing.Landlord.Name}\n{DateTime.Now.ToString(Constants.DateFormat)}\n\n{Billing.Landlord.Address.Street} {Billing.Landlord.Address.HouseNumber}\n{Billing.Landlord.Address.Postcode} {Billing.Landlord.Address.City}\nTelefon: {Billing.Landlord.Phone}\nEmail: {Billing.Landlord.MailAddress}"); + + AddLineBreaks(2); + + AddText( + $"{tenant.Salutation.AsString()} {tenant.Name}\n{Billing.House.Address.Street} {Billing.House.Address.HouseNumber}\n{Billing.House.Address.Postcode} {Billing.House.Address.City}", + null, true); + + AddLineBreaks(4); + + AddText($"{tenant.Salutation.AsString()} {tenant.Name}"); + + AddLineBreaks(1); + + AddText( + $"Nebenkostenabrechnung vom {Billing.StartDate.ToString(Constants.DateFormat)} zum {Billing.EndDate.ToString(Constants.DateFormat)}", + Constants.PrintSubjectFontSize); + + AddLineBreaks(1); + + if (!string.IsNullOrEmpty(tenant.CustomMessage1)) + { + AddText(tenant.CustomMessage1); + + AddLineBreaks(1); + } + + foreach (var (cost, costResult) in result.Costs) + { + AddCost(cost.Name, costResult.TotalAmount); + } + + AddCost("Zwischensumme", result.SubTotalAmount); + AddCost("Bereits gezahlt", tenant.PaidRent); + AddCost(result.TotalAmount > 0 ? "Einmalige Nachzahlung" : "Einmalige Rückzahlung", result.TotalAmount, + true); + + AddLineBreaks(2); + + if (result.TotalAmount > 0) + { + AddText( + $"Bitte überweisen Sie den einmaligen Betrag von {result.TotalAmount.CeilToString()} € auf das untenstehende Konto."); + } + else + { + AddText( + $"Der einmalige Betrag von {result.TotalAmount.CeilToString()} € wird in den nächsten Tagen auf Ihr Konto überwiesen."); + } + + AddLineBreaks(1); + + if (!string.IsNullOrEmpty(tenant.CustomMessage2)) + { + AddText(tenant.CustomMessage2); + AddLineBreaks(1); + } + + AddText("Kontoverbindung:"); + AddText($"IBAN: {Billing.Landlord.BankAccount.Iban}"); + AddText($"BIC: {Billing.Landlord.BankAccount.Bic}"); + AddText($"Name der Bank: {Billing.Landlord.BankAccount.BankName}"); + + AddLineBreaks(2); + + AddText("Mit freundlichen Grüßen"); + + if (result.Costs.Any(t => t.Key.DisplayInBill)) + { + PreviewForSingleTenantDetails(result); + } + } + + private void PreviewForSingleTenantDetails(TenantCalculationResult result) + { + var section = new Section {BreakPageBefore = true, FontSize = Constants.PrintDefaultFontSize}; + Document.Blocks.Add(section); + + var table = new Table {CellSpacing = 0}; + section.Blocks.Add(table); + table.Columns.Add(new TableColumn {Width = new GridLength(1, GridUnitType.Star)}); + table.Columns.Add(new TableColumn {Width = new GridLength(1, GridUnitType.Star)}); + + var rowGroup = new TableRowGroup(); + table.RowGroups.Add(rowGroup); + + void AddLineBreaks(int count) + { + PrintWindow.AddLineBreaks(rowGroup, count); + } + + void AddText(string text, double? fontSize = null, bool alignRight = false) + { + PrintWindow.AddText(rowGroup, text, fontSize, alignRight); + } + + AddText("Details zur Berechnung:"); + + AddLineBreaks(1); + + foreach (var (cost, costResult) in result.Costs) + { + if (!cost.DisplayInBill) + { + continue; + } + + AddText(costResult.Details); + } + } + + private void OnPrintClick(object sender, RoutedEventArgs e) + { + _document.Print( + $"MietRechner Abrechnung {Billing.StartDate.ToString(Constants.DateFormat)} - {Billing.EndDate.ToString(Constants.DateFormat)}"); + } + + private void OnSelectedTenantChanged(object sender, PropertyChangedEventArgs e) + { + Preview(); + } + + private void OnTenantSelectorClick(object sender, RoutedEventArgs e) + { + var button = (Button) sender; + var contextMenu = button.ContextMenu; + // ReSharper disable once PossibleNullReferenceException + contextMenu.PlacementTarget = button; + contextMenu.Placement = PlacementMode.Bottom; + contextMenu.IsOpen = true; + + e.Handled = true; + } + + private void OnTenantMenuItemClick(object sender, RoutedEventArgs e) + { + var item = (TenantMenuItem) ((MenuItem) sender).DataContext; + + if (item.Tenant == null) + { + foreach (var item2 in TenantMenuItems) + { + if (item2.Tenant != null) + { + item2.Selected = !item.None; + } + } + } + else + { + item.Selected = !item.Selected; + } + } + } +} \ No newline at end of file diff --git a/ucalc/ucalc.csproj b/ucalc/ucalc.csproj index 48118e6..afebfd1 100644 --- a/ucalc/ucalc.csproj +++ b/ucalc/ucalc.csproj @@ -49,6 +49,9 @@ + + + @@ -94,6 +97,9 @@ CostEntryDetailsWindow.xaml + + PrintWindow.xaml +