diff --git a/ucalc.sln b/ucalc.sln index 334ef9b..b0bdd94 100644 --- a/ucalc.sln +++ b/ucalc.sln @@ -3,18 +3,24 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30128.74 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ucalc", "ucalc\ucalc.csproj", "{E565705A-BAF4-4653-99A1-199C314A21EC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ucalc", "ucalc\ucalc.csproj", "{E565705A-BAF4-4653-99A1-199C314A21EC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {E565705A-BAF4-4653-99A1-199C314A21EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E565705A-BAF4-4653-99A1-199C314A21EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E565705A-BAF4-4653-99A1-199C314A21EC}.Debug|x64.ActiveCfg = Debug|x64 + {E565705A-BAF4-4653-99A1-199C314A21EC}.Debug|x64.Build.0 = Debug|x64 {E565705A-BAF4-4653-99A1-199C314A21EC}.Release|Any CPU.ActiveCfg = Release|Any CPU {E565705A-BAF4-4653-99A1-199C314A21EC}.Release|Any CPU.Build.0 = Release|Any CPU + {E565705A-BAF4-4653-99A1-199C314A21EC}.Release|x64.ActiveCfg = Release|x64 + {E565705A-BAF4-4653-99A1-199C314A21EC}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ucalc/Constants.cs b/ucalc/Constants.cs index 5543894..01a525f 100644 --- a/ucalc/Constants.cs +++ b/ucalc/Constants.cs @@ -47,9 +47,9 @@ namespace UCalc 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("14pt"); + PrintDefaultFontSize = (double) converter.ConvertFromInvariantString("11pt"); + PrintSubjectFontSize = (double) converter.ConvertFromInvariantString("12pt"); + PrintNewlineFontSize = (double) converter.ConvertFromInvariantString("11pt"); // ReSharper restore PossibleNullReferenceException DinA4Padding = new Thickness(dinA4MarginLeftRight, dinA4MarginTopBottom, dinA4MarginLeftRight, diff --git a/ucalc/Controls/PrintableDocument.cs b/ucalc/Controls/PrintableDocument.cs index b8ed7c0..946f1d4 100644 --- a/ucalc/Controls/PrintableDocument.cs +++ b/ucalc/Controls/PrintableDocument.cs @@ -1,109 +1,443 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Packaging; using System.Linq; +using System.Printing; +using System.Windows; using System.Windows.Controls; using System.Windows.Documents; -using System.Windows.Markup; +using System.Windows.Media; using System.Windows.Xps.Packaging; using System.Windows.Xps.Serialization; +using Microsoft.Win32; +using UCalc.Data; namespace UCalc.Controls { + public class NoPrinterException : Exception + { + public NoPrinterException(string message) : base(message) + { + } + } + 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 readonly FlowDocument _flowDocument; + private readonly FixedDocumentSequence _previewDocument; + private XpsDocument _fixedDocument; - private int PageCount => - _document.GetFixedDocumentSequence()!.References.First().GetDocument(false)!.Pages.Count; - - public PrintableDocument(IDocumentPaginatorSource source) + public PrintableDocument(Billing billing, IEnumerable tenants) { _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); + _flowDocument = CreateFlowDocument(billing, tenants); + _previewDocument = CreatePreview(_flowDocument); } public void Dispose() { - ((IDisposable) _document).Dispose(); - ((IDisposable) _package).Dispose(); + ((IDisposable) _fixedDocument).Dispose(); PackageStore.RemovePackage(DocUri); + ((IDisposable) _package).Dispose(); + } + + private static Size? GetPrinterMediaSize() + { + var localPrintServer = new LocalPrintServer(); + + var size = localPrintServer.GetPrintQueues() + .Select(printQueue => printQueue.DefaultPrintTicket?.PageMediaSize) + .FirstOrDefault(mediaSize => mediaSize is {PageMediaSizeName: PageMediaSizeName.ISOA4}); + if (size == null || !size.Width.HasValue || !size.Height.HasValue) + { + return null; + } + + return new Size(size.Width!.Value, size.Height!.Value); } public void PreviewIn(DocumentViewer viewer) { - viewer.Document = _document.GetFixedDocumentSequence(); + viewer.Document = _previewDocument; } public void Print(string description) { - var dialog = new PrintDialog {UserPageRangeEnabled = true, MinPage = 1, MaxPage = (uint) PageCount}; + var version = (int) Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + "CurrentMajorVersionNumber", 8); - if (dialog.ShowDialog() == true) + if (version == 10) { - var documentSequence = _document.GetFixedDocumentSequence(); - Package convPackage = null; - XpsDocument convDocument = null; + PrintDocumentImageableArea imageableArea = null; + var pageRangeSelection = PageRangeSelection.AllPages; + PageRange pageRange; - try + var writer = PrintQueue.CreateXpsDocumentWriter(description, ref imageableArea, ref pageRangeSelection, + ref pageRange); + if (writer == null) { - if (dialog.PageRangeSelection == PageRangeSelection.UserPages) + return; + } + + var clonedDocument = CloneFlowDocument(_flowDocument); + clonedDocument.PagePadding = Constants.DinA4Padding; + clonedDocument.ColumnWidth = double.PositiveInfinity; + + var paginator = ((IDocumentPaginatorSource) clonedDocument).DocumentPaginator; + paginator.PageSize = new Size(imageableArea.MediaSizeWidth, imageableArea.MediaSizeHeight); + + paginator.ComputePageCount(); + + if (pageRangeSelection == PageRangeSelection.UserPages) + { + if (pageRange.PageFrom > paginator.PageCount || pageRange.PageTo > paginator.PageCount || + pageRange.PageFrom > pageRange.PageTo) { - convDocument = ExtractPages(_document, dialog.PageRange.PageFrom - 1, - dialog.PageRange.PageTo - 1, out convPackage); - documentSequence = convDocument.GetFixedDocumentSequence(); + MessageBox.Show("Die eingegebenen Seitenzahlen sind ungültig!", "Ungültig!", + MessageBoxButton.OK, MessageBoxImage.Error); + return; } - dialog.PrintDocument(documentSequence!.DocumentPaginator, description); + paginator = new LimitedPaginator(paginator, pageRange.PageFrom - 1, pageRange.PageTo - 1); } - finally + + writer.Write(paginator); + } + else + { + var appDataPath = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}/UCalc"; + var path = $"{appDataPath}\\print.xps"; + + if (File.Exists(path)) { - convDocument?.Close(); - convPackage?.Close(); + File.Delete(path); } + + var xpsDocument = new XpsDocument(path, FileAccess.ReadWrite); + using var manager = new XpsSerializationManager(new XpsPackagingPolicy(xpsDocument), false); + + var paginator = ((IDocumentPaginatorSource) _flowDocument).DocumentPaginator; + manager.SaveAsXaml(paginator); + + xpsDocument.Close(); + + var process = new Process(); + process.StartInfo = new ProcessStartInfo(path) + { + UseShellExecute = true + }; + process.Start(); } } - private static XpsDocument ExtractPages(XpsDocument source, int fromPage, int toPage, out Package package) + private FixedDocumentSequence CreatePreview(IDocumentPaginatorSource document) { - package = Package.Open(new MemoryStream(), FileMode.Create, FileAccess.ReadWrite); + _fixedDocument = new XpsDocument(_package, CompressionOption.SuperFast, DocUri.AbsoluteUri); + using var manager = new XpsSerializationManager(new XpsPackagingPolicy(_fixedDocument), false); - var docUri = new Uri("pack://mietrechnerTempTicket.xps"); - PackageStore.RemovePackage(docUri); - PackageStore.AddPackage(docUri, package); + var paginator = document.DocumentPaginator; + manager.SaveAsXaml(paginator); + return _fixedDocument.GetFixedDocumentSequence(); + } - var document = new XpsDocument(package, CompressionOption.SuperFast, docUri.AbsoluteUri); - var pages = source.GetFixedDocumentSequence()!.References.First().GetDocument(false)!.Pages; + private static FlowDocument CloneFlowDocument(FlowDocument document) + { + var sourceRange = new TextRange(document.ContentStart, document.ContentEnd); - var documentReference = new DocumentReference(); - var fixedDocument = new FixedDocument(); - documentReference.SetDocument(fixedDocument); + using var stream = new MemoryStream(); + sourceRange.Save(stream, DataFormats.Xaml); - for (var i = fromPage; i <= toPage; ++i) + var targetDocument = new FlowDocument(); + var targetRange = new TextRange(targetDocument.ContentStart, targetDocument.ContentEnd); + targetRange.Load(stream, DataFormats.Xaml); + + return targetDocument; + } + + private class LimitedPaginator : DocumentPaginator + { + private readonly DocumentPaginator _paginator; + private readonly int _minPage; + private readonly int _maxPage; + + public LimitedPaginator(DocumentPaginator paginator, int minPage, int maxPage) { - var page = pages[i]; - var pageContentChild = new PageContent {Source = page.Source}; - ((IUriContext) pageContentChild).BaseUri = ((IUriContext) page).BaseUri; - pageContentChild.GetPageRoot(false); - fixedDocument.Pages.Add(pageContentChild); + _paginator = paginator; + _minPage = minPage; + _maxPage = maxPage; } - var documentSequence = new FixedDocumentSequence(); - documentSequence.References.Add(documentReference); + public override DocumentPage GetPage(int pageNumber) + { + return _paginator.GetPage(_minPage + pageNumber); + } - var writer = XpsDocument.CreateXpsDocumentWriter(document); - writer.Write(documentSequence); + public override bool IsPageCountValid => _minPage < _paginator.PageCount && + _maxPage < _paginator.PageCount && + _minPage <= _maxPage; + + public override int PageCount => _maxPage - _minPage + 1; + + public override Size PageSize + { + get => _paginator.PageSize; + set => _paginator.PageSize = value; + } + + public override IDocumentPaginatorSource Source => _paginator.Source; + } + + private static FlowDocument CreateFlowDocument(Billing billing, IEnumerable tenants) + { + var size = GetPrinterMediaSize(); + if (!size.HasValue) + { + throw new NoPrinterException("Failed to query printer"); + } + + var document = new FlowDocument + { + PageWidth = size.Value.Width, + PageHeight = size.Value.Height, + PagePadding = Constants.DinA4Padding, + ColumnWidth = double.PositiveInfinity + }; + + foreach (var tenant in tenants) + { + var result = BillingCalculator.CalculateForTenant(billing, tenant); + AddPagesForTenant(document, billing, tenant, result); + } return document; } + + 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 - 1)) + {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 static void AddPagesForTenant(FlowDocument document, Billing billing, 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 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)}); + } + + void AddTextLeftRight(string leftText, string rightText) + { + var row2 = new TableRow(); + rowGroup.Rows.Add(row2); + + var cell2 = new TableCell(); + row2.Cells.Add(cell2); + + cell2.Blocks.Add(new Paragraph(new Run(leftText))); + + cell2 = new TableCell + { + TextAlignment = TextAlignment.Right + }; + row2.Cells.Add(cell2); + + cell2.Blocks.Add(new Paragraph(new Run(rightText))); + } + + AddTextLeftRight( + $"{billing.Landlord.Name}\n" + + $"{billing.Landlord.Address.Street} {billing.Landlord.Address.HouseNumber}\n" + + $"{billing.Landlord.Address.Postcode} {billing.Landlord.Address.City}\n" + + $"Telefon: {billing.Landlord.Phone}" + + (string.IsNullOrEmpty(billing.Landlord.MailAddress) ? "" : $"\nEmail: {billing.Landlord.MailAddress}"), + DateTime.Now.ToString(Constants.DateFormat) + ); + + AddLineBreaks(rowGroup, 2); + + AddText( + rowGroup, + $"{tenant.Salutation.AsString()} {tenant.Name}\n" + + $"{billing.House.Address.Street} {billing.House.Address.HouseNumber}\n" + + $"{billing.House.Address.Postcode} {billing.House.Address.City}" + ); + + AddLineBreaks(rowGroup, 4); + + AddText(rowGroup, $"{tenant.Salutation.AsString()} {tenant.Name}"); + + AddLineBreaks(rowGroup, 1); + + var startDate = billing.StartDate; + if (tenant.EntryDate.HasValue && tenant.EntryDate.Value > startDate) + { + startDate = tenant.EntryDate.Value; + } + + var endDate = billing.EndDate; + if (tenant.DepartureDate.HasValue && tenant.DepartureDate.Value < endDate) + { + endDate = tenant.DepartureDate.Value; + } + + AddText( + rowGroup, + $"Nebenkostenabrechnung vom {startDate.ToString(Constants.DateFormat)} zum {endDate.ToString(Constants.DateFormat)}", + Constants.PrintSubjectFontSize + ); + + AddLineBreaks(rowGroup, 1); + + if (!string.IsNullOrEmpty(tenant.CustomMessage1)) + { + AddText(rowGroup, tenant.CustomMessage1); + + AddLineBreaks(rowGroup, 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(rowGroup, 1); + + if (result.TotalAmount > 0) + { + AddText( + rowGroup, + $"Bitte überweisen Sie den einmaligen Betrag von {result.TotalAmount.CeilToString()} € auf das untenstehende Konto." + ); + } + else + { + AddText( + rowGroup, + $"Der einmalige Betrag von {(result.TotalAmount * -1).CeilToString()} € wird in den nächsten Tagen auf Ihr Konto überwiesen." + ); + } + + AddLineBreaks(rowGroup, 1); + + if (!string.IsNullOrEmpty(tenant.CustomMessage2)) + { + AddText(rowGroup, tenant.CustomMessage2); + AddLineBreaks(rowGroup, 1); + } + + AddText(rowGroup, "Kontoverbindung:"); + AddText(rowGroup, $"IBAN: {billing.Landlord.BankAccount.Iban}"); + AddText(rowGroup, $"BIC: {billing.Landlord.BankAccount.Bic}"); + AddText(rowGroup, $"Name der Bank: {billing.Landlord.BankAccount.BankName}"); + + AddLineBreaks(rowGroup, 2); + + AddText(rowGroup, "Mit freundlichen Grüßen"); + + if (result.Costs.Any(t => t.Key.DisplayInBill)) + { + AddPagesForTenantDetails(document, result); + } + } + + private static void AddPagesForTenantDetails(FlowDocument document, 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); + + AddText(rowGroup, "Details zur Berechnung:"); + + AddLineBreaks(rowGroup, 1); + + foreach (var (cost, costResult) in result.Costs) + { + if (!cost.DisplayInBill) + { + continue; + } + + AddText(rowGroup, costResult.Details); + } + } } } \ No newline at end of file diff --git a/ucalc/PrintWindow.xaml b/ucalc/PrintWindow.xaml index 03dc0a5..826b694 100644 --- a/ucalc/PrintWindow.xaml +++ b/ucalc/PrintWindow.xaml @@ -17,15 +17,6 @@ Icon="logo.ico"> - - - -