Compare commits

...

14 Commits

13 changed files with 129 additions and 98 deletions

View File

@ -12,15 +12,14 @@ namespace Elwig.Documents {
public string? Text = App.Client.TextDeliveryConfirmation; public string? Text = App.Client.TextDeliveryConfirmation;
public Dictionary<string, (string, int, int, int, int)> MemberBins; public Dictionary<string, (string, int, int, int, int)> MemberBins;
public DeliveryConfirmation(AppDbContext ctx, int year, Member m) : public DeliveryConfirmation(AppDbContext ctx, int year, Member m, IEnumerable<DeliveryPart>? deliveries = null) :
base($"Anlieferungsbestätigung {year}", m) { base($"Anlieferungsbestätigung {year}", m) {
Year = year; Year = year;
ShowDateAndLocation = true; ShowDateAndLocation = true;
UseBillingAddress = true; UseBillingAddress = true;
IncludeSender = true; IncludeSender = true;
// FIXME footer in merged documents DocumentId = $"Anl.-Best. {Year}/{m.MgNr}";
//DocumentId = $"Anl.-Best. {Year}/{m.MgNr}"; Deliveries = deliveries ?? ctx.DeliveryParts.FromSqlRaw($"""
Deliveries = ctx.DeliveryParts.FromSqlRaw($"""
SELECT p.* SELECT p.*
FROM v_delivery v FROM v_delivery v
JOIN delivery_part p ON (p.year, p.did, p.dpnr) = (v.year, v.did, v.dpnr) JOIN delivery_part p ON (p.year, p.did, p.dpnr) = (v.year, v.did, v.dpnr)

View File

@ -3,14 +3,12 @@ using System.Threading.Tasks;
using System.IO; using System.IO;
using Elwig.Helpers; using Elwig.Helpers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Linq; using System.Linq;
namespace Elwig.Documents { namespace Elwig.Documents {
public abstract partial class Document : IDisposable { public abstract partial class Document : IDisposable {
private TempFile? _pdfFile = null; private TempFile? _pdfFile = null;
private string? _renderedHtml = null;
public bool ShowFoldMarks = App.Config.Debug; public bool ShowFoldMarks = App.Config.Debug;
@ -39,18 +37,6 @@ namespace Elwig.Documents {
Date = DateTime.Today; Date = DateTime.Today;
} }
[GeneratedRegex(@"</body>.*?</footer>\s*</div>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)]
private static partial Regex GeneratedDocumentHeaderRegex();
private static readonly Regex DocumentHeaderRegex = GeneratedDocumentHeaderRegex();
[GeneratedRegex(@"<style>.*?/style>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)]
private static partial Regex GeneratedHtmlStyleRegex();
private static readonly Regex HtmlStyleRegex = GeneratedHtmlStyleRegex();
[GeneratedRegex(@"<link[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)]
private static partial Regex GeneratedHtmlLinkRegex();
private static readonly Regex HtmlLinkRegex = GeneratedHtmlLinkRegex();
~Document() { ~Document() {
Dispose(); Dispose();
} }
@ -61,34 +47,11 @@ namespace Elwig.Documents {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
public static async Task<Document> Merge(IEnumerable<Document> docs) { public static Document Merge(IEnumerable<Document> docs) {
string html = ""; return new MergedDocument(docs);
var styles = new List<string>();
foreach (var d in docs) {
var h = await d.Render();
var s = HtmlStyleRegex.Matches(h).Select(m => m.Value).ToList();
var l = HtmlLinkRegex.Matches(h).Select(m => m.Value).ToList();
if (s.All(styles.Contains)) {
h = HtmlStyleRegex.Replace(h, "");
} else {
styles.AddRange(s);
}
if (l.All(styles.Contains)) {
h = HtmlLinkRegex.Replace(h, "");
} else {
styles.AddRange(l);
}
html += h;
}
html = DocumentHeaderRegex.Replace(html, "<div class='document-break'/>");
return new InternalDocument("Mehrere Dokumente") {
_renderedHtml = html,
};
} }
private async Task<string> Render() { private async Task<string> Render() {
if (_renderedHtml != null)
return _renderedHtml;
string name; string name;
if (this is BusinessLetter) { if (this is BusinessLetter) {
name = "BusinessLetter"; name = "BusinessLetter";
@ -109,18 +72,40 @@ namespace Elwig.Documents {
} }
private async Task<string> Render(string name) { private async Task<string> Render(string name) {
_renderedHtml = await Html.CompileRenderAsync(name, this); return await Html.CompileRenderAsync(name, this); ;
return _renderedHtml;
} }
public async Task Generate() { public async Task Generate(IProgress<double>? progress = null) {
progress?.Report(0.0);
if (this is MergedDocument m) {
var pdf = new TempFile("pdf");
var tmpHtmls = new List<TempFile>();
var n = m.Documents.Count();
int i = 0;
foreach (var doc in m.Documents) {
var tmpHtml = new TempFile("html");
await File.WriteAllTextAsync(tmpHtml.FilePath, await doc.Render(), Utils.UTF8);
tmpHtmls.Add(tmpHtml);
i++;
progress?.Report(50.0 * i / n);
}
progress?.Report(50.0);
await Pdf.Convert(tmpHtmls.Select(f => f.FileName), pdf.FileName, new Progress<double>(v => progress?.Report(50.0 + v / 2)));
foreach (var tmp in tmpHtmls) {
tmp.Dispose();
}
_pdfFile = pdf;
} else {
var pdf = new TempFile("pdf"); var pdf = new TempFile("pdf");
using (var tmpHtml = new TempFile("html")) { using (var tmpHtml = new TempFile("html")) {
await File.WriteAllTextAsync(tmpHtml.FilePath, await Render(), Utils.UTF8); await File.WriteAllTextAsync(tmpHtml.FilePath, await Render(), Utils.UTF8);
progress?.Report(50.0);
await Pdf.Convert(tmpHtml.FilePath, pdf.FilePath); await Pdf.Convert(tmpHtml.FilePath, pdf.FilePath);
} }
_pdfFile = pdf; _pdfFile = pdf;
} }
progress?.Report(100.0);
}
public void SaveTo(string pdfPath) { public void SaveTo(string pdfPath) {
if (_pdfFile == null) throw new InvalidOperationException("Pdf file has not been generated yet"); if (_pdfFile == null) throw new InvalidOperationException("Pdf file has not been generated yet");
@ -137,8 +122,11 @@ namespace Elwig.Documents {
Pdf.Show(_pdfFile.NewReference(), Title + (this is BusinessDocument b ? $" - {b.Member.Name}" : "")); Pdf.Show(_pdfFile.NewReference(), Title + (this is BusinessDocument b ? $" - {b.Member.Name}" : ""));
} }
private class InternalDocument : Document { private class MergedDocument : Document {
public InternalDocument(string title) : base(title) { } public IEnumerable<Document> Documents;
public MergedDocument(IEnumerable<Document> docs) : base("Mehrere Dokumente") {
Documents = docs;
}
} }
} }
} }

View File

@ -2,44 +2,57 @@ using System.Threading.Tasks;
using Elwig.Helpers; using Elwig.Helpers;
using Elwig.Windows; using Elwig.Windows;
using System.Diagnostics; using System.Diagnostics;
using Balbarak.WeasyPrint;
using System; using System;
using System.IO; using System.IO;
using System.Collections.Generic;
using System.Windows;
using System.Text.RegularExpressions;
using System.Linq;
namespace Elwig.Documents { namespace Elwig.Documents {
public static class Pdf { public static class Pdf {
private static readonly string PdfToPrinter = App.ExePath + "PDFtoPrinter.exe"; private static readonly string PdfToPrinter = App.ExePath + "PDFtoPrinter.exe";
private static readonly FilesManager WeasyPrintManager = new(); private static readonly string WinziPrint = App.ExePath + "WinziPrint.exe";
private static string? WeasyPrintPython = null; private static Process? WinziPrintProc;
private static string? WeasyPrintDir => WeasyPrintManager.FolderPath; public static bool IsReady => WinziPrintProc != null;
public static bool IsReady => WeasyPrintPython != null && WeasyPrintDir != null;
public static async Task Init(Action evtHandler) { public static async Task Init(Action evtHandler) {
if (!WeasyPrintManager.IsFilesExsited()) { var p = new Process() { StartInfo = new() {
await WeasyPrintManager.InitFilesAsync(); FileName = WinziPrint,
} Arguments = $"-p -e utf-8 -d \"{App.TempPath}\" -",
WeasyPrintPython = Path.Combine(WeasyPrintManager.FolderPath, "python.exe"); CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true
} };
p.Start();
WinziPrintProc = p;
evtHandler(); evtHandler();
} }
public static async Task Convert(string htmlPath, string pdfPath) { public static async Task<IEnumerable<int>> Convert(string htmlPath, string pdfPath, IProgress<double>? progress = null) {
var p = new Process() { StartInfo = new() { return await Convert(new string[] { htmlPath }, pdfPath, progress);
FileName = WeasyPrintPython, }
CreateNoWindow = true,
WorkingDirectory = WeasyPrintDir, public static async Task<IEnumerable<int>> Convert(IEnumerable<string> htmlPath, string pdfPath, IProgress<double>? progress = null) {
RedirectStandardError = true, if (WinziPrintProc == null) throw new InvalidOperationException("The WinziPrint process has not been initialized yet");
} }; progress?.Report(0.0);
p.StartInfo.EnvironmentVariables["PATH"] = "Scripts;gtk3;" + Environment.GetEnvironmentVariable("PATH"); await WinziPrintProc.StandardInput.WriteLineAsync($"{string.Join(';', htmlPath)};{pdfPath}");
p.StartInfo.ArgumentList.Add("scripts/weasyprint.exe"); while (true) {
p.StartInfo.ArgumentList.Add("-e"); var line = await WinziPrintProc.StandardOutput.ReadLineAsync() ?? throw new IOException("Invalid response from WinziPrint");
p.StartInfo.ArgumentList.Add("utf8"); if (line.StartsWith("error:")) {
p.StartInfo.ArgumentList.Add(htmlPath); MessageBox.Show(line[6..].Trim(), "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
p.StartInfo.ArgumentList.Add(pdfPath); return Array.Empty<int>();
p.Start(); } else if (line.StartsWith("progress:")) {
await p.WaitForExitAsync(); var parts = line[9..].Trim().Split('/').Select(int.Parse).ToArray();
var stderr = await p.StandardError.ReadToEndAsync(); progress?.Report(100.0 * parts[0] / parts[1]);
if (p.ExitCode != 0) throw new Exception(stderr); } else if (line.StartsWith("success:")) {
var m = Regex.Match(line, @"\(([0-9, ]+)\)");
return m.Groups[1].Value.Split(", ").Select(int.Parse);
}
}
} }
public static void Show(TempFile file, string title) { public static void Show(TempFile file, string title) {

View File

@ -7,7 +7,7 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<PreserveCompilationContext>true</PreserveCompilationContext> <PreserveCompilationContext>true</PreserveCompilationContext>
<ApplicationIcon>elwig.ico</ApplicationIcon> <ApplicationIcon>elwig.ico</ApplicationIcon>
<Version>0.4.2</Version> <Version>0.4.3</Version>
<SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages> <SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages>
</PropertyGroup> </PropertyGroup>
@ -16,7 +16,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Balbarak.WeasyPrint" Version="2.0.2" />
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.1" /> <PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.1" />
<PackageReference Include="ini-parser" Version="2.5.2" /> <PackageReference Include="ini-parser" Version="2.5.2" />
<PackageReference Include="LinqKit" Version="1.2.4" /> <PackageReference Include="LinqKit" Version="1.2.4" />

View File

@ -181,19 +181,30 @@ namespace Elwig.Helpers.Billing {
} }
} }
var changes = new List<(int, int, int, int)>(); var negChanges = new List<(int, int, int, int)>();
var posChanges = new List<(int, int, int, int)>();
foreach (var (did, dpnr, _, w) in fittingDeliveries) { foreach (var (did, dpnr, _, w) in fittingDeliveries) {
int v = Math.Min(needed, w); int v = Math.Min(needed, w);
needed -= v; needed -= v;
changes.Add((did, dpnr, 1, v)); posChanges.Add((did, dpnr, 1, v));
changes.Add((did, dpnr, 2, w - v)); negChanges.Add((did, dpnr, 2, w - v));
if (needed == 0) break; if (needed == 0) break;
} }
using (var cmd = cnx.CreateCommand()) { using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $""" cmd.CommandText = $"""
INSERT INTO delivery_part_bin (year, did, dpnr, binnr, discr, value) INSERT INTO delivery_part_bin (year, did, dpnr, binnr, discr, value)
VALUES {string.Join(",\n ", changes.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '', {i.Item4})"))} VALUES {string.Join(",\n ", posChanges.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '', {i.Item4})"))}
ON CONFLICT DO UPDATE
SET value = value + excluded.value
""";
await cmd.ExecuteNonQueryAsync();
}
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
INSERT INTO delivery_part_bin (year, did, dpnr, binnr, discr, value)
VALUES {string.Join(",\n ", negChanges.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '', {i.Item4})"))}
ON CONFLICT DO UPDATE ON CONFLICT DO UPDATE
SET value = excluded.value SET value = excluded.value
"""; """;

View File

@ -5,6 +5,7 @@ namespace Elwig.Helpers {
public sealed class TempFile : IDisposable { public sealed class TempFile : IDisposable {
private int Usages = 0; private int Usages = 0;
public string FilePath { get; private set; } public string FilePath { get; private set; }
public string FileName => Path.GetFileName(FilePath);
public TempFile() : this(null) { } public TempFile() : this(null) { }

View File

@ -1,13 +1,12 @@
<local:ContextWindow x:Class="Elwig.Dialogs.DeliveryConfirmationsDialog" <local:ContextWindow x:Class="Elwig.Dialogs.DeliveryConfirmationsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Windows" xmlns:local="clr-namespace:Elwig.Windows"
mc:Ignorable="d" mc:Ignorable="d"
ResizeMode="NoResize"
Loaded="Window_Loaded" Loaded="Window_Loaded"
Title="Anlieferungsbestätingungen - Elwig" Height="400" Width="600"> Title="Anlieferungsbestätingungen - Elwig" Height="500" Width="800" MinHeight="400" MinWidth="600">
<Grid> <Grid>
<GroupBox Header="Sortieren nach" Margin="10,10,10,10" Width="180" Height="80" VerticalAlignment="Top" HorizontalAlignment="Left"> <GroupBox Header="Sortieren nach" Margin="10,10,10,10" Width="180" Height="80" VerticalAlignment="Top" HorizontalAlignment="Left">
<StackPanel Margin="5,5,0,5"> <StackPanel Margin="5,5,0,5">
@ -24,6 +23,8 @@
<TextBox x:Name="TextElement" TextWrapping="Wrap" VerticalScrollBarVisibility="Visible" AcceptsReturn="True" <TextBox x:Name="TextElement" TextWrapping="Wrap" VerticalScrollBarVisibility="Visible" AcceptsReturn="True"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="200,10,10,10" Height="Auto"/> HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="200,10,10,10" Height="Auto"/>
<ProgressBar x:Name="ProgressBar" Margin="10,10,10,106" Height="27" Width="180"
VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
<Button x:Name="TestButton" Content="Stichprobe" FontSize="14" Width="180" Margin="10,10,10,74" Height="27" Tag="Print" IsEnabled="False" <Button x:Name="TestButton" Content="Stichprobe" FontSize="14" Width="180" Margin="10,10,10,74" Height="27" Tag="Print" IsEnabled="False"
Click="TestButton_Click" Click="TestButton_Click"
VerticalAlignment="Bottom" HorizontalAlignment="Left"/> VerticalAlignment="Bottom" HorizontalAlignment="Left"/>

View File

@ -10,17 +10,19 @@ using System.Windows;
using System.Windows.Input; using System.Windows.Input;
namespace Elwig.Dialogs { namespace Elwig.Dialogs {
public partial class DeliveryConfirmationsDialog : ContextWindow { public partial class DeliveryConfirmationsWindow : ContextWindow {
public readonly int Year; public readonly int Year;
public DeliveryConfirmationsDialog(int year) { public DeliveryConfirmationsWindow(int year) {
InitializeComponent(); InitializeComponent();
Year = year; Year = year;
Title = $"Anlieferungsbestätigungen - Lese {Year} - Elwig"; Title = $"Anlieferungsbestätigungen - Lese {Year} - Elwig";
TextElement.Text = App.Client.TextDeliveryConfirmation; TextElement.Text = App.Client.TextDeliveryConfirmation;
if (!App.Config.Debug) { if (!App.Config.Debug) {
TestButton.Visibility = Visibility.Hidden; TestButton.Visibility = Visibility.Hidden;
var m = ProgressBar.Margin;
ProgressBar.Margin = new(m.Left, m.Top, m.Right, m.Bottom - 32);
} }
} }
@ -82,8 +84,21 @@ namespace Elwig.Dialogs {
list = list.Where((_, n) => n % 10 == r); list = list.Where((_, n) => n % 10 == r);
} }
using var doc = await Document.Merge(list.Select(m => new DeliveryConfirmation(Context, Year, m))); ; var deliveries = await Context.DeliveryParts.FromSqlRaw($"""
await doc.Generate(); SELECT p.*
FROM v_delivery v
JOIN delivery_part p ON (p.year, p.did, p.dpnr) = (v.year, v.did, v.dpnr)
WHERE v.year = {Year}
ORDER BY v.sortid, v.abgewertet ASC,
COALESCE(LENGTH(v.attributes), 0) ASC, attribute_prio DESC, COALESCE(v.attributes, '~'),
v.kmw DESC, v.lsnr, v.dpnr
""")
.ToListAsync();
using var doc = Document.Merge(list.Select(m => new DeliveryConfirmation(Context, Year, m, deliveries.Where(d => d.Delivery.MgNr == m.MgNr).ToList()))); ;
await doc.Generate(new Progress<double>(v => {
ProgressBar.Value = v;
}));
Mouse.OverrideCursor = null; Mouse.OverrideCursor = null;
if (mode < 2) { if (mode < 2) {

View File

@ -317,7 +317,7 @@ namespace Elwig.Windows {
.ThenBy(m => m.MgNr); .ThenBy(m => m.MgNr);
break; break;
} }
using var doc = await Document.Merge((await members.ToListAsync()).Select(m => new Letterhead(m))); using var doc = Document.Merge((await members.ToListAsync()).Select(m => new Letterhead(m)));
await doc.Generate(); await doc.Generate();
Mouse.OverrideCursor = null; Mouse.OverrideCursor = null;
if (App.Config.Debug) { if (App.Config.Debug) {

View File

@ -51,8 +51,8 @@ namespace Elwig.Windows {
private void DeliveryConfirmationButton_Click(object sender, RoutedEventArgs evt) { private void DeliveryConfirmationButton_Click(object sender, RoutedEventArgs evt) {
if (SeasonInput.Value is not int year) if (SeasonInput.Value is not int year)
return; return;
var d = new DeliveryConfirmationsDialog(year); var w = new DeliveryConfirmationsWindow(year);
d.Show(); w.Show();
} }
private async void OverUnderDeliveryButton_Click(object sender, RoutedEventArgs evt) { private async void OverUnderDeliveryButton_Click(object sender, RoutedEventArgs evt) {

View File

@ -3,3 +3,4 @@
mkdir "C:\ProgramData\Elwig\resources" mkdir "C:\ProgramData\Elwig\resources"
copy /b /y Documents\*.css "C:\ProgramData\Elwig\resources" copy /b /y Documents\*.css "C:\ProgramData\Elwig\resources"
copy /b /y Documents\*.cshtml "C:\ProgramData\Elwig\resources" copy /b /y Documents\*.cshtml "C:\ProgramData\Elwig\resources"
::copy /b /y ..\Installer\Files\*.exe "C:\Program Files\Elwig\"

Binary file not shown.

View File

@ -7,6 +7,9 @@
<Component Directory="ConfigFolder" Permanent="true" NeverOverwrite="true"> <Component Directory="ConfigFolder" Permanent="true" NeverOverwrite="true">
<File Source="$(ProjectDir)\Files\config.ini" Id="config.ini"/> <File Source="$(ProjectDir)\Files\config.ini" Id="config.ini"/>
</Component> </Component>
<Component Directory="InstallFolder">
<File Source="$(ProjectDir)\Files\WinziPrint.exe" Id="WinziPrint.exe"/>
</Component>
<Component Directory="InstallFolder"> <Component Directory="InstallFolder">
<File Source="$(TargetDir)\PDFtoPrinter.exe" Id="PDFtoPrinter.exe"/> <File Source="$(TargetDir)\PDFtoPrinter.exe" Id="PDFtoPrinter.exe"/>
</Component> </Component>