Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9e90b1e98 | |||
| 640dbf705e | |||
| 42121fe7da | |||
| d45c3f867f | |||
| a90be2644d | |||
| 01739ba42e | |||
| 7bea4d9ee0 | |||
| b6497fd422 | |||
| f141485537 | |||
| b31603554a | |||
| 90def81cc5 | |||
| 87903227b5 | |||
| 6d6776f0f9 | |||
| bf3cc2ea1e | |||
| c1697dc4f3 | |||
| 3b335a568e | |||
| beacdab54f | |||
| da05a49e10 | |||
| 5cec5b3556 | |||
| 9af498287d | |||
| 452f246f24 | |||
| e97c29db43 | |||
| 3419113dec | |||
| bf6297f63b | |||
| f228ba3019 | |||
| 495aa8a691 | |||
| 811916a8b9 | |||
| 3b6333a6a2 | |||
| 9b37330362 | |||
| 889a17b21c | |||
| ac6d559e5d | |||
| 6d80cca241 | |||
| 9dc225d3e4 | |||
| 0d513f7bff | |||
| b10c744bf9 | |||
| e6367da286 | |||
| 01f4480a08 | |||
| e9722c790c | |||
| af98c32026 | |||
| 7300b30cf5 | |||
| 428cd6ddc2 | |||
| 2de8af878b | |||
| 34d95eab9d | |||
| 548aeb2ce9 | |||
| 7edd888aa2 | |||
| a0d4f19f30 | |||
| 67ba342c28 | |||
| 1b69fcb16a | |||
| c8a95422af | |||
| 9d02f18bac | |||
| f9ee2cb120 | |||
| b27b89f599 | |||
| bfbd0a6a22 | |||
| e2de7a1f1c | |||
| 3f769eb7d7 | |||
| 8bc053053c | |||
| a0dcaf7b4f | |||
| 844fc5217a | |||
| 542afa5892 | |||
| 9e02b15ff1 | |||
| 463769b549 | |||
| 2c383d0c55 | |||
| 5ba74c8368 | |||
| 3c9b3c2db1 | |||
| 98f8907817 | |||
| 44dcc5e19f | |||
| d7012ebfa1 | |||
| a9b5317e79 | |||
| f02598760f | |||
| 4b8cd2a0d7 | |||
| 104798d4f2 | |||
| 4653a4f129 | |||
| 07f9a0f522 | |||
| 7e21d9e2e4 | |||
| 4e2eca295d | |||
| 1f165055c1 | |||
| 7b953fa73e | |||
| d3157e4d48 | |||
| 73fe4531cc | |||
| b6c03892b1 | |||
| 91950ad9fd | |||
| 77c3f388e7 | |||
| 6f3e1a8905 | |||
| 466c8a322c | |||
| ab7c7404e2 |
247
CHANGELOG.md
247
CHANGELOG.md
@@ -2,6 +2,253 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
[v1.0.3.0][v1.0.3.0] (2026-01-16) {#v1.0.3.0}
|
||||
---------------------------------------------
|
||||
|
||||
### Neue Funktionen {#v1.0.3.0-features}
|
||||
|
||||
* Bei Zu-/Abschlägen ist es nun möglich den konkreten Wert dieser auf Lieferscheinen nicht anzuzeigen (Stammdaten -> Saisons). (495aa8a691)
|
||||
* Bei Teillieferungen kann nun die Art der Anlieferung angegeben werden: Planenwagen/Kipper, Lesewagen, Kiste(n). (bf6297f63b, 3419113dec, 6d6776f0f9)
|
||||
* Im Lieferungen-Fenster (`DeliveryAdminWindow`) können nun die Gesamtmengen der gefilterten Lieferungen nach Mitglied und Sorte als Excel-Liste exportiert werden ("Liefermengen"). ([#73][i73], e97c29db43)
|
||||
* Im Datenbankabfragen-Fenster (`QueryWindow`) gibt es die Möglichkeit die Ergebnisse als CSV-Datei zu exportieren. (9af498287d, 5cec5b3556)
|
||||
* PDF-Dokumente wurden überarbeitet:
|
||||
* Traubengutschriften können nur noch ausgedruckt bzw. verschickt werden, wenn die zugehörige Auszahlungsvariante festgesetzt ist. (b31603554a)
|
||||
* Auf Dokumenten für interne Verwendung (alle außer an Mitglieder adressierte) wurden Kopf- und Fußzeile auf ein Minimum beschränkt. (f141485537)
|
||||
* Das Synchronisieren zwischen Zweigstellen ist für den Benutzer nun wesentlich vereinfacht. Geänderte Daten von Mitgliedern, Flächenbindungen oder Lieferungen werden beim Synchronisieren automatsich hochgeladen bzw. heruntergeladen. ([#66][i66], 3b335a568e)
|
||||
* Im Rundschreiben-Fenster (`MailWindow`) wurde ein Knopf zum Abbrechen des Generierens hinzugefügt. ([#50][i50])
|
||||
|
||||
### Behobene Fehler {#v1.0.3.0-bugfixes}
|
||||
|
||||
* Einzelne (nicht über das Rundschreiben-Fenster) verschickte E-Mails werde korrekt im Ausgangsprotokoll angeführt. (ac6d559e5d)
|
||||
* Bei automatischen Datenbank-Updates sind auftretende Fehler ignoriert worden. (f228ba3019)
|
||||
* Beim Aufteilen von Lieferungen sind Zu-/Abschläge nicht auf neu erstellen Teillieferungen übernommen worden. (da05a49e10)
|
||||
* Das Verbinden und Trennen von Waagen mittels Serial-/COM-Port ist nun bei laufendem Programm möglich. ([#71][i71])
|
||||
* In den Auszahlungsvarianten-Daten (`PaymentVariantSummary`) ist die statistische Summe der gebundenen und ungebundenen Menge ohne die Spalte _attributlos gebunden_ berechnet worden. (90def81cc5)
|
||||
* Seit [v0.13.7](#v0.13.7) (2025-01-21) wurde auch bei der ersten Auszahlung der Saison ein "Bisher berücksichtigt: € 0,00" auf den Traubengutschriften angeführt. (7bea4d9ee0)
|
||||
* Das Speichern der Parameter `MAIL_SEND_POSTAL` und `MAIL_SEND_EMAIL` war fehlerhaft. (01739ba42e)
|
||||
* Bessere Isolation der automatisierten Dokumenten-Tests. (640dbf705e)
|
||||
|
||||
### Sonstiges {#v1.0.3.0-misc}
|
||||
|
||||
* Im Lieferungen-Fenster (`DeliveryAdminWindow`) wird bei Zu-/Abschlägen nur "Alle" angezeigt, wenn mehr als 1 ausgewählt sind. (811916a8b9)
|
||||
* Im Auszahlungsvarianten-Fenster (`PaymentVariantsWindow`) wird die letzte Variante standardmäßig ausgewählt und beim Löschen wird um Bestätigung gebeten. (b6497fd422)
|
||||
* Abhängigkeiten aktualisiert. (889a17b21c, 9b37330362, 3b6333a6a2, d45c3f867f, 42121fe7da)
|
||||
|
||||
[v1.0.3.0]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.3.0
|
||||
[i50]: https://git.necronda.net/winzer/elwig/issues/50
|
||||
[i66]: https://git.necronda.net/winzer/elwig/issues/60
|
||||
[i71]: https://git.necronda.net/winzer/elwig/issues/71
|
||||
[i73]: https://git.necronda.net/winzer/elwig/issues/73
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.2.0][v1.0.2.0] (2025-11-10) {#v1.0.2.0}
|
||||
---------------------------------------------
|
||||
|
||||
### Neue Funktionen {#v1.0.2.0-features}
|
||||
|
||||
* Im Mitglieder-Fenster (`MemberAdminWindow`) können Kontaktdaten der Mitglieder als .vcf-Datei exportiert werden. (01f4480a08, 9dc225d3e4)
|
||||
|
||||
### Sonstiges {#v1.0.2.0-misc}
|
||||
|
||||
* Wenn ein Serial-/COM-Port-USB-Adapter an- oder abgesteckt wird, wird das nun automatisch erkannt. (e6367da286)
|
||||
* Abhängigkeiten aktualisiert. (b10c744bf9, 0d513f7bff)
|
||||
|
||||
[v1.0.2.0]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.2.0
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.1.5][v1.0.1.5] (2025-10-29) {#v1.0.1.5}
|
||||
---------------------------------------------
|
||||
|
||||
### Behobene Fehler {#v1.0.1.5-bugfixes}
|
||||
|
||||
* Im Rundschreiben-Fenster (`MailWindow`) kam es zu einem Absturz, wenn man das Fenster über den "Anlieferungsbestätigung"-Knopf im Leseabschluss-Abschnitt geöffnet hat. (af98c32026)
|
||||
|
||||
[v1.0.1.5]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.1.5
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.1.4][v1.0.1.4] (2025-10-28) {#v1.0.1.4}
|
||||
---------------------------------------------
|
||||
|
||||
### Behobene Fehler {#v1.0.1.4-bugfixes}
|
||||
|
||||
* Im Rundschreiben-Fenster (`MailWindow`) kam es zu einem Absturz, wenn man die Zustelloptionen "Post zusenden an Mitglieder, die keine E-Mail erhalten würden" und "E-Mail zusenden an niemanden" kombiniert hat. (2de8af878b)
|
||||
|
||||
### Sonstiges {#v1.0.1.4-misc}
|
||||
|
||||
* Im Auszahlungsvariante-Fenster (`ChartWindow`) gibt es keine Fehlermeldung mehr wenn nicht für alle Sorten ein Preis definiert ist, nur noch eine Warnung. (428cd6ddc2)
|
||||
|
||||
[v1.0.1.4]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.1.4
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.1.3][v1.0.1.3] (2025-10-13) {#v1.0.1.3}
|
||||
---------------------------------------------
|
||||
|
||||
### Neue Funktionen {#v1.0.1.3-features}
|
||||
|
||||
* In der Liste des Lieferungen-Fenster (`DeliveryAdminWindow`) werden
|
||||
* statt ausschließlich der Sorte auch Attribut und Bewirtschaftungsart angezeigt. (a0d4f19f30)
|
||||
* Kommentare der Lieferungen (und Teillieferungen) angezeigt. (548aeb2ce9)
|
||||
|
||||
### Sonstiges {#v1.0.1.3-misc}
|
||||
|
||||
* Verzögerung der Überprüfung auf automatische Updates auf einige Sekunden verlängert. (67ba342c28)
|
||||
* Verbesserung der Ladezeiten im Anmeldungen-Fenster (`DeliveryAncmtAdminWindow`). (7edd888aa2)
|
||||
|
||||
[v1.0.1.3]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.1.3
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.1.2][v1.0.1.2] (2025-09-25) {#v1.0.1.2}
|
||||
---------------------------------------------
|
||||
|
||||
### Behobene Fehler {#v1.0.1.2-bugfixes}
|
||||
|
||||
* Beim automatischen Importieren/Synchronisieren wird bei einem Fehlerfall der Benutzer verständigt, aber der Vorgang nicht abgebrochen. (9d02f18bac)
|
||||
|
||||
### Sonstiges {#v1.0.1.2-misc}
|
||||
|
||||
* Beim Sichern der Datenbank werden Meta-Informationen in der ZIP-Datei gespeichert. (c8a95422af)
|
||||
|
||||
[v1.0.1.2]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.1.2
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.1.1][v1.0.1.1] (2025-09-21) {#v1.0.1.1}
|
||||
---------------------------------------------
|
||||
|
||||
### Sonstiges {#v1.0.1.1-misc}
|
||||
|
||||
* Eingabe von Sorten und Qualitätsstufen im Übernahme-Fenster (`DeliveryAdminWindows`) verbessert. (e2de7a1f1c, b27b89f599)
|
||||
|
||||
[v1.0.1.1]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.1.1
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.1.0][v1.0.1.0] (2025-09-18) {#v1.0.1.0}
|
||||
---------------------------------------------
|
||||
|
||||
### Neue Funktionen {#v1.0.1.0-features}
|
||||
|
||||
* Neue Weinsorten gemäß Kürzelliste der Bundeskellereiinspektion hinzugefügt. (a0dcaf7b4f)
|
||||
|
||||
### Sonstiges {#v1.0.1.0-misc}
|
||||
|
||||
* HTTP-Anfragen haben jetzt das Feld `User-Agent` gesetzt. (844fc5217a)
|
||||
* Auto-Update-Funktion wird auch beim Erlangen von Netzwerkverbindung ausgeführt. (8bc053053c)
|
||||
|
||||
[v1.0.1.0]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.1.0
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.0.6][v1.0.0.6] (2025-09-17) {#v1.0.0.6}
|
||||
---------------------------------------------
|
||||
|
||||
### Behobene Fehler {#v1.0.0.6-bugfixes}
|
||||
|
||||
* Die automatische Erkennung des Wiege-Modus hat im WKW nicht funktioniert. (463769b549)
|
||||
|
||||
### Sonstiges {#v1.0.0.6-misc}
|
||||
|
||||
* Tippfehler im Über-Fenster (`AboutWindow`) behoben. (2c383d0c55)
|
||||
* `SaveFileDialog` mit Multi-File-Extensions verbessert. (9e02b15ff1)
|
||||
|
||||
[v1.0.0.6]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.6
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.0.5][v1.0.0.5] (2025-09-15) {#v1.0.0.5}
|
||||
---------------------------------------------
|
||||
|
||||
### Neue Funktionen {#v1.0.0.5-features}
|
||||
|
||||
* Die Datenbank kann im Haupt-Fenster (`MainWindow`) gesichert und wiederhergestellt werden. (f02598760f)
|
||||
|
||||
### Sonstiges {#v1.0.0.5-misc}
|
||||
|
||||
* In der WGM ist eine Auswahl eines Zu/-Abschlags im Übernahme-Fenster (`DeliveryAdminWindow`) nun erforderlich. (a9b5317e79)
|
||||
* In der Konfigurationsdatei kann im `[general]` Block `weighing = gross`, `weighing = net`, oder `weighing = box` angegeben werden. (d7012ebfa1)
|
||||
* Über-Fenser (`AboutWindow`) hinzugefügt. (3c9b3c2db1)
|
||||
* Abhängigkeiten aktualisiert. (44dcc5e19f, 98f8907817)
|
||||
|
||||
[v1.0.0.5]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.5
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.0.4][v1.0.0.4] (2025-09-01) {#v1.0.0.4}
|
||||
---------------------------------------------
|
||||
|
||||
### Behobene Fehler {#v1.0.0.4-bugfixes}
|
||||
|
||||
* Absturz beim Verschicken von einzelnen E-Mails außerhalb des Rundschreiben-Fensters (`MailWindow`) behoben. (07f9a0f522)
|
||||
|
||||
### Sonstiges {#v1.0.0.4-misc}
|
||||
|
||||
* Ladezeit des Fehler-Protokoll-Fensters (`LogWindow`) verbessert. (4653a4f129)
|
||||
* Im Übernahme-Fenster (`DeliveryAdminWindow`) ist es nun möglich die Qualitätsstufe zu verändern. (104798d4f2)
|
||||
|
||||
[v1.0.0.4]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.4
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.0.3][v1.0.0.3] (2025-08-11) {#v1.0.0.3}
|
||||
---------------------------------------------
|
||||
|
||||
### Neue Funktionen {#v1.0.0.3-features}
|
||||
|
||||
* Im Haupt-Fenster (`MainWindow`) ist es nun möglich die gesamte Datenbank zu hochzuladen bzw. herunterzuladen. ([#69][i69])
|
||||
|
||||
### Sonstiges {#v1.0.0.3-misc}
|
||||
|
||||
* Die Intigrität von Elwig-Export-Dateien (`.elwig.zip`) und anderen `.zip` Dateien wir nun überprüft. (d3157e4d48)
|
||||
* Die Herkunftshierarchie wird nun auch automatisch synchronisiert (benötigt für Stamm-KG bei Mitglied, Ried/KG bei Flächenbindung, und Herkunft bei Lieferung). ([#70][i70], b6c03892b1)
|
||||
* Abhängigkeiten aktualisiert. (1f165055c1, 4e2eca295d)
|
||||
|
||||
[v1.0.0.3]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.3
|
||||
[i69]: https://git.necronda.net/winzer/elwig/issues/69
|
||||
[i70]: https://git.necronda.net/winzer/elwig/issues/70
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.0.2][v1.0.0.2] (2025-08-05) {#v1.0.0.2}
|
||||
---------------------------------------------
|
||||
|
||||
### Sonstiges {#v1.0.0.2-misc}
|
||||
|
||||
* Explizit native SQLite-Bibliothek hinzugefügt. (77c3f388e7)
|
||||
|
||||
[v1.0.0.2]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.2
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.0.1][v1.0.0.1] (2025-08-05) {#v1.0.0.1}
|
||||
---------------------------------------------
|
||||
|
||||
### Sonstiges {#v1.0.0.1-misc}
|
||||
|
||||
* Abhängigkeiten aktualisiert. (466c8a322c)
|
||||
* Angepasste Fehlermeldung, wenn Importieren fehlschlägt. (ab7c7404e2)
|
||||
|
||||
[v1.0.0.1]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.1
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.0.0.0][v1.0.0.0] (2025-07-30) {#v1.0.0.0}
|
||||
---------------------------------------------
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="WineVarietyTemplateExpanded">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding SortId}" Foreground="{Binding Color}" MinWidth="36" Margin="0,0,10,0"/>
|
||||
<TextBlock Text="{Binding SortIdFormat}" Foreground="{Binding Color}" MinWidth="36" Margin="0,0,10,0"/>
|
||||
<TextBlock Text="{Binding Name}" Foreground="{Binding Color}"/>
|
||||
<TextBlock Text="{Binding CommentFormat}" FontSize="10" VerticalAlignment="Bottom" Margin="0,0,0,2"/>
|
||||
</StackPanel>
|
||||
@@ -55,6 +55,12 @@
|
||||
<TextBlock Text="{Binding ValueStr}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="PublicModifierTemplate">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Name}" MinWidth="250" Margin="0,0,10,0"/>
|
||||
<TextBlock Text="{Binding PublicValueStr}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="WineAttributeTemplate">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.IO;
|
||||
using Elwig.Helpers;
|
||||
using Elwig.Helpers.Weighing;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Threading;
|
||||
using System.Reflection;
|
||||
using Elwig.Helpers.Printing;
|
||||
using Elwig.Windows;
|
||||
using Elwig.Dialogs;
|
||||
using System.Threading.Tasks;
|
||||
using Elwig.Helpers;
|
||||
using Elwig.Helpers.Billing;
|
||||
using Elwig.Helpers.Export;
|
||||
using Elwig.Helpers.Printing;
|
||||
using Elwig.Helpers.Weighing;
|
||||
using Elwig.Models.Entities;
|
||||
using Elwig.Windows;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Elwig {
|
||||
public partial class App : Application {
|
||||
@@ -24,6 +26,7 @@ namespace Elwig {
|
||||
public static bool ForceShutdown { get; private set; } = false;
|
||||
|
||||
private readonly DispatcherTimer _autoUpdateTimer = new() { Interval = TimeSpan.FromHours(1) };
|
||||
public readonly SerialPortWatcher SerialPortWatcher = new();
|
||||
|
||||
public static readonly string DataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Elwig");
|
||||
public static readonly string MailsPath = Path.Combine(DataPath, "mails");
|
||||
@@ -46,9 +49,10 @@ namespace Elwig {
|
||||
public static string? BranchPhoneNr { get; private set; }
|
||||
public static string? BranchFaxNr { get; private set; }
|
||||
public static string? BranchMobileNr { get; private set; }
|
||||
public static IList<IScale> Scales { get; private set; }
|
||||
public static IList<ICommandScale> CommandScales => Scales.Where(s => s is ICommandScale).Cast<ICommandScale>().ToList();
|
||||
public static IList<IEventScale> EventScales => Scales.Where(s => s is IEventScale).Cast<IEventScale>().ToList();
|
||||
|
||||
public static IList<IScale> Scales { get; private set; } = [];
|
||||
public static IList<ICommandScale> CommandScales => [.. Scales.Where(s => s is ICommandScale).Cast<ICommandScale>()];
|
||||
public static IList<IEventScale> EventScales => [.. Scales.Where(s => s is IEventScale).Cast<IEventScale>()];
|
||||
public static ClientParameters Client { get; set; }
|
||||
|
||||
public static Dispatcher MainDispatcher { get; private set; }
|
||||
@@ -126,14 +130,18 @@ namespace Elwig {
|
||||
if (Config.UpdateAuto && Config.UpdateUrl != null) {
|
||||
if (Utils.HasInternetConnectivity()) {
|
||||
Utils.RunBackground("Auto Updater", async () => {
|
||||
await Task.Delay(500);
|
||||
await Task.Delay(1000);
|
||||
await CheckForUpdates();
|
||||
});
|
||||
}
|
||||
NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
|
||||
_autoUpdateTimer.Tick += new EventHandler(OnAutoUpdateTimer);
|
||||
_autoUpdateTimer.Start();
|
||||
}
|
||||
|
||||
SerialPortWatcher.SerialPortConnected += OnSerialPortConnected;
|
||||
SerialPortWatcher.SerialPortDisconnected += OnSerialPortDisconnected;
|
||||
|
||||
var list = new List<IScale>();
|
||||
foreach (var s in Config.Scales) {
|
||||
try {
|
||||
@@ -141,7 +149,7 @@ namespace Elwig {
|
||||
} catch (Exception e) {
|
||||
list.Add(new InvalidScale(s.Id));
|
||||
if (s.Required)
|
||||
MessageBox.Show($"Unable to create scale {s.Id}:\n\n{e.Message}", "Scale Error",
|
||||
MessageBox.Show($"Verbindung zu Waage {s.Id} konnte nicht hergestellt werden:\n\n{e.Message}", "Waagenfehler",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
@@ -149,7 +157,7 @@ namespace Elwig {
|
||||
|
||||
if (Config.Branch != null) {
|
||||
if (!branches.ContainsKey(Config.Branch.ToLower())) {
|
||||
MessageBox.Show("Invalid branch name in config!", "Invalid Branch Config", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show("Ungültige Zweigstelle in Konfigurationsdatei!", "Ungültige Zweigstelle", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
Shutdown();
|
||||
} else {
|
||||
SetBranch(branches[Config.Branch.ToLower()]);
|
||||
@@ -157,10 +165,20 @@ namespace Elwig {
|
||||
} else if (branches.Count == 1) {
|
||||
SetBranch(branches.First().Value);
|
||||
} else {
|
||||
MessageBox.Show("Unable to determine local branch!", "Invalid Branch Config", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show("Erkennen der lokalen Zweigstelle nicht möglich!", "Ungültige Zweigstelle", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
if (Config.WeighingMode == null) {
|
||||
if (Client.IsMatzen || Client.IsWolkersdorf) {
|
||||
Config.WeighingMode = WeighingMode.Net;
|
||||
} else if (Client.IsHaugsdorf || Client.IsSitzendorf) {
|
||||
Config.WeighingMode = WeighingMode.Box;
|
||||
} else if (Client.IsBaden || Client.IsGrInzersdorf) {
|
||||
Config.WeighingMode = WeighingMode.Gross;
|
||||
}
|
||||
}
|
||||
|
||||
base.OnStartup(evt);
|
||||
|
||||
var window = new MainWindow();
|
||||
@@ -168,6 +186,7 @@ namespace Elwig {
|
||||
}
|
||||
|
||||
private async void Application_Exit(object sender, ExitEventArgs evt) {
|
||||
SerialPortWatcher.Dispose();
|
||||
foreach (var s in EventScales) {
|
||||
s.Dispose();
|
||||
}
|
||||
@@ -217,6 +236,63 @@ namespace Elwig {
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs evt) {
|
||||
if (!evt.IsAvailable) return;
|
||||
if (Utils.HasInternetConnectivity()) {
|
||||
Utils.RunBackground("Auto Updater", async () => {
|
||||
await Task.Delay(2000);
|
||||
await CheckForUpdates();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSerialPortConnected(object? sender, string name) {
|
||||
for (var i = 0; i < Config.Scales.Count; i++) {
|
||||
var s = Config.Scales[i];
|
||||
if (s.Connection?.StartsWith($"serial://{name}:") ?? false) {
|
||||
if (Scales[i] is InvalidScale) {
|
||||
try {
|
||||
Scales[i] = Scale.FromConfig(s);
|
||||
MessageBox.Show($"Verbindung zu Waage {s.Id} wieder hergestellt!", $"Waage {s.Id}", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
} catch (Exception e) {
|
||||
Scales[i] = new InvalidScale(s.Id);
|
||||
MessageBox.Show($"Verbindung zu Waage {s.Id} konnte nicht hergestellt werden:\n\n{e.Message}", "Waagenfehler",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
} else if (Scales[i] is IEventScale) {
|
||||
MessageBox.Show($"Verbindung zu Waage {s.Id} wieder hergestellt!", $"Waage {s.Id}", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateScales();
|
||||
}
|
||||
|
||||
private void OnSerialPortDisconnected(object? sender, string name) {
|
||||
for (var i = 0; i < Config.Scales.Count; i++) {
|
||||
var s = Config.Scales[i];
|
||||
if ((s.Connection?.StartsWith($"serial://{name}:") ?? false) && Scales[i] is not InvalidScale) {
|
||||
MessageBox.Show($"Verbindung zu Waage {s.Id} unterbrochen!", $"Waagen {s.Id}", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
if (Scales[i] is ICommandScale) {
|
||||
try {
|
||||
Scales[i].Dispose();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
Scales[i] = new InvalidScale(s.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateScales();
|
||||
}
|
||||
|
||||
public static void UpdateScales() {
|
||||
foreach (Window w in CurrentApp.Windows) {
|
||||
if (w is DeliveryAdminWindow t && t.ViewModel.IsReceipt) {
|
||||
t.UpdateScales();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task CheckForUpdates(bool showAlert = false) {
|
||||
if (Config.UpdateUrl == null) return;
|
||||
var latest = await Utils.GetLatestInstallerUrl(Config.UpdateUrl);
|
||||
@@ -239,6 +315,19 @@ namespace Elwig {
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ReplaceDatabase(string filename) {
|
||||
try {
|
||||
await Task.Run(async () => {
|
||||
await Database.Import(filename);
|
||||
});
|
||||
MessageBox.Show("Das Ersetzen war erfolgreich!\n\nBitte starten Sie Elwig neu!", "Datenbank ersetzen", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
ForceShutdown = true;
|
||||
Current.Shutdown();
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show("Fehler beim Ersetzen:\n\n" + exc.Message, "Datenbank ersetzen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static T FocusWindow<T>(Func<T> constructor, Predicate<T>? selector = null) where T : Window {
|
||||
foreach (Window w in CurrentApp.Windows) {
|
||||
if (w is T t && (selector == null || selector(t))) {
|
||||
|
||||
@@ -124,19 +124,24 @@ namespace Elwig.Controls {
|
||||
SelectItemsReverse();
|
||||
var dmp = !string.IsNullOrEmpty(ListDisplayMemberPath) ? ListDisplayMemberPath : !string.IsNullOrEmpty(DisplayMemberPath) ? DisplayMemberPath : null;
|
||||
if (SelectedItems.Count == ItemsSource.Cast<object>().Count() && AllItemsSelectedContent != null) {
|
||||
_textBox.Text = AllItemsSelectedContent;
|
||||
AllItemsSelected = true;
|
||||
} else if (SelectedItems.Count == 0) {
|
||||
_textBox.Text = "";
|
||||
AllItemsSelected = false;
|
||||
} else {
|
||||
AllItemsSelected = null;
|
||||
}
|
||||
if (SelectedItems.Count > 1 && SelectedItems.Count == ItemsSource.Cast<object>().Count() && AllItemsSelectedContent != null) {
|
||||
_textBox.Text = AllItemsSelectedContent;
|
||||
} else if (SelectedItems.Count == 0) {
|
||||
_textBox.Text = "";
|
||||
} else {
|
||||
_textBox.Text = string.Join(Delimiter,
|
||||
dmp == null ? SelectedItems.Cast<object>() :
|
||||
SelectedItems.Cast<object>()
|
||||
.Select(i => i.GetType().GetProperty(dmp)?.GetValue(i))
|
||||
);
|
||||
AllItemsSelected = null;
|
||||
}
|
||||
|
||||
RaiseEvent(new SelectionChangedEventArgs(SelectionChangedEvent, evt.RemovedItems, evt.AddedItems));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,16 @@ namespace Elwig.Documents {
|
||||
Member = m;
|
||||
Location = App.BranchLocation;
|
||||
IncludeSender = includeSender;
|
||||
var c = App.Client;
|
||||
Header = $"<div class='name'>{c.Name}</div><div class='suffix'>{c.NameSuffix}</div><div class='type'>{c.NameTypeFull}</div>";
|
||||
Footer = Utils.GenerateFooter("<br/>", " \u00b7 ")
|
||||
.Item(c.NameFull).NextLine()
|
||||
.Item(c.Address).Item($"{c.Plz} {c.Ort}").Item("Österreich").Item("Tel.", c.PhoneNr).Item("Fax", c.FaxNr).NextLine()
|
||||
.Item(c.EmailAddress != null ? $"<a href=\"mailto:{c.Name} {c.NameSuffix} <{c.EmailAddress}>\">{c.EmailAddress}</a>" : null)
|
||||
.Item(c.Website != null ? $"<a href=\"http://{c.Website}/\">{c.Website}</a>" : null)
|
||||
.Item("Betriebs-Nr.", c.LfbisNr).Item("Bio-KSt.", c.OrganicAuthority).NextLine()
|
||||
.Item("UID", c.UstIdNr).Item("BIC", c.Bic).Item("IBAN", c.Iban)
|
||||
.ToString();
|
||||
var uid = (m.UstIdNr ?? "-") + (m.IsBuchführend ? "" : " <i>(pauschaliert)</i>");
|
||||
Aside = $"<table><colgroup><col span='1' style='width: 22.5mm;'/><col span='1' style='width: 42.5mm;'/></colgroup>" +
|
||||
$"<thead><tr><th colspan='2'>Mitglied</th></tr></thead><tbody>" +
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace Elwig.Documents {
|
||||
Data = data;
|
||||
Payment = p;
|
||||
Credit = p.Credit;
|
||||
IsPreview = Payment == null || Credit == null;
|
||||
var season = p.Variant.Season;
|
||||
if (considerCustomModifiers) {
|
||||
CustomPayment = ctx.CustomPayments.Find(p.Year, p.MgNr);
|
||||
@@ -48,7 +49,7 @@ namespace Elwig.Documents {
|
||||
if (CustomPayment?.ModComment != null) {
|
||||
MemberModifier = CustomPayment.ModComment;
|
||||
} else if (mod != null) {
|
||||
MemberModifier = $"{mod.Name} ({mod.ValueStr})";
|
||||
MemberModifier = $"{mod.Name} ({mod.PublicValueStr})";
|
||||
} else {
|
||||
MemberModifier = "Sonstige Zu-/Abschläge";
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 24pt;
|
||||
margin-top: 10mm;
|
||||
margin-top: 0;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 24pt;
|
||||
margin-top: 10mm;
|
||||
margin-top: 0;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 24pt;
|
||||
margin-top: 10mm;
|
||||
margin-top: 0;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
@if (part.Modifiers.Count() > 0) {
|
||||
var first = true;
|
||||
foreach (var mod in part.Modifiers) {
|
||||
<tr class="tight @(first ? "first" : "")"><td></td><td>@Raw(first ? "<i>Zu-/Abschläge:</i>" : "")</td><td colspan="3"><b>@mod.Name</b></td><td style="white-space: pre;">@mod.ValueStr</td></tr>
|
||||
<tr class="tight @(first ? "first" : "")"><td></td><td>@Raw(first ? "<i>Zu-/Abschläge:</i>" : "")</td><td colspan="3"><b>@mod.Name</b></td><td style="white-space: pre;">@mod.PublicValueStr</td></tr>
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Elwig.Helpers.Printing;
|
||||
using MimeKit;
|
||||
using System.Threading;
|
||||
|
||||
namespace Elwig.Documents {
|
||||
public abstract partial class Document : IDisposable {
|
||||
@@ -18,10 +19,11 @@ namespace Elwig.Documents {
|
||||
protected string? _pdfPath;
|
||||
protected string? PdfPath => _pdfPath ?? _pdfFile?.FilePath;
|
||||
public int? TotalPages { get; private set; }
|
||||
public int? Pages => TotalPages / (DoublePaged ? 2 : 1);
|
||||
public int? Pages => TotalPages / (IsDoublePaged ? 2 : 1);
|
||||
|
||||
public bool ShowFoldMarks = App.Config.Debug;
|
||||
public bool DoublePaged = false;
|
||||
public bool IsDoublePaged = false;
|
||||
public bool IsPreview = false;
|
||||
|
||||
public string DocumentsPath;
|
||||
public int CurrentNextSeason;
|
||||
@@ -38,15 +40,8 @@ namespace Elwig.Documents {
|
||||
CurrentNextSeason = Utils.CurrentNextSeason;
|
||||
Title = title;
|
||||
Author = c.NameFull;
|
||||
Header = $"<div class='name'>{c.Name}</div><div class='suffix'>{c.NameSuffix}</div><div class='type'>{c.NameTypeFull}</div>";
|
||||
Footer = Utils.GenerateFooter("<br/>", " \u00b7 ")
|
||||
.Item(c.NameFull).NextLine()
|
||||
.Item(c.Address).Item($"{c.Plz} {c.Ort}").Item("Österreich").Item("Tel.", c.PhoneNr).Item("Fax", c.FaxNr).NextLine()
|
||||
.Item(c.EmailAddress != null ? $"<a href=\"mailto:{c.Name} {c.NameSuffix} <{c.EmailAddress}>\">{c.EmailAddress}</a>" : null)
|
||||
.Item(c.Website != null ? $"<a href=\"http://{c.Website}/\">{c.Website}</a>" : null)
|
||||
.Item("Betriebs-Nr.", c.LfbisNr).Item("Bio-KSt.", c.OrganicAuthority).NextLine()
|
||||
.Item("UID", c.UstIdNr).Item("BIC", c.Bic).Item("IBAN", c.Iban)
|
||||
.ToString();
|
||||
Header = "";
|
||||
Footer = Utils.GenerateFooter("<br/>", " \u00b7 ").Item(c.NameFull).ToString();
|
||||
Date = DateOnly.FromDateTime(Utils.Today);
|
||||
}
|
||||
|
||||
@@ -104,7 +99,7 @@ namespace Elwig.Documents {
|
||||
return await Html.CompileRenderAsync(name, this); ;
|
||||
}
|
||||
|
||||
public async Task Generate(IProgress<double>? progress = null) {
|
||||
public async Task Generate(CancellationToken? cancelToken = null, IProgress<double>? progress = null) {
|
||||
if (_pdfFile != null)
|
||||
return;
|
||||
progress?.Report(0.0);
|
||||
@@ -114,36 +109,50 @@ namespace Elwig.Documents {
|
||||
var pdf = new TempFile("pdf");
|
||||
var tmpHtmls = new List<TempFile>();
|
||||
var tmpFiles = new List<string>();
|
||||
var n = m.Documents.Count();
|
||||
int i = 0;
|
||||
foreach (var doc in m.Documents) {
|
||||
if (doc is PdfDocument) {
|
||||
tmpFiles.Add(doc.PdfPath!);
|
||||
continue;
|
||||
try {
|
||||
var n = m.Documents.Count();
|
||||
int i = 0;
|
||||
foreach (var doc in m.Documents) {
|
||||
if (doc is PdfDocument) {
|
||||
tmpFiles.Add(doc.PdfPath!);
|
||||
continue;
|
||||
}
|
||||
if (cancelToken?.IsCancellationRequested ?? false)
|
||||
throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!");
|
||||
var tmpHtml = new TempFile("html");
|
||||
await File.WriteAllTextAsync(tmpHtml.FilePath, await doc.Render(), Utils.UTF8);
|
||||
tmpHtmls.Add(tmpHtml);
|
||||
tmpFiles.Add((doc is Letterhead ? "#" : "") + tmpHtml.FileName);
|
||||
i++;
|
||||
progress?.Report(GenerationProportion * 100 * i / n);
|
||||
}
|
||||
progress?.Report(GenerationProportion * 100);
|
||||
var pages = await Pdf.Convert(tmpFiles, pdf.FileName, IsDoublePaged, cancelToken, new Progress<double>(v => progress?.Report(GenerationProportion * 100 + v * (1 - GenerationProportion))));
|
||||
TotalPages = pages.Pages;
|
||||
_pdfFile = pdf;
|
||||
} catch {
|
||||
pdf.Dispose();
|
||||
throw;
|
||||
} finally {
|
||||
foreach (var tmp in tmpHtmls) {
|
||||
tmp.Dispose();
|
||||
}
|
||||
var tmpHtml = new TempFile("html");
|
||||
await File.WriteAllTextAsync(tmpHtml.FilePath, await doc.Render(), Utils.UTF8);
|
||||
tmpHtmls.Add(tmpHtml);
|
||||
tmpFiles.Add((doc is Letterhead ? "#" : "") + tmpHtml.FileName);
|
||||
i++;
|
||||
progress?.Report(GenerationProportion * 100 * i / n);
|
||||
}
|
||||
progress?.Report(GenerationProportion * 100);
|
||||
var pages = await Pdf.Convert(tmpFiles, pdf.FileName, DoublePaged, new Progress<double>(v => progress?.Report(GenerationProportion * 100 + v * (1 - GenerationProportion))));
|
||||
TotalPages = pages.Pages;
|
||||
foreach (var tmp in tmpHtmls) {
|
||||
tmp.Dispose();
|
||||
}
|
||||
_pdfFile = pdf;
|
||||
} else {
|
||||
if (cancelToken?.IsCancellationRequested ?? false)
|
||||
throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!");
|
||||
var pdf = new TempFile("pdf");
|
||||
using (var tmpHtml = new TempFile("html")) {
|
||||
try {
|
||||
using var tmpHtml = new TempFile("html");
|
||||
await File.WriteAllTextAsync(tmpHtml.FilePath, await Render(), Utils.UTF8);
|
||||
progress?.Report(50.0);
|
||||
var pages = await Pdf.Convert(tmpHtml.FilePath, pdf.FilePath, DoublePaged);
|
||||
var pages = await Pdf.Convert(tmpHtml.FilePath, pdf.FilePath, IsDoublePaged, cancelToken);
|
||||
TotalPages = pages.Pages;
|
||||
_pdfFile = pdf;
|
||||
} catch {
|
||||
pdf.Dispose();
|
||||
throw;
|
||||
}
|
||||
_pdfFile = pdf;
|
||||
}
|
||||
progress?.Report(100.0);
|
||||
}
|
||||
@@ -155,7 +164,7 @@ namespace Elwig.Documents {
|
||||
|
||||
public async Task Print(int copies = 1) {
|
||||
if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet");
|
||||
await Pdf.Print(PdfPath, copies, DoublePaged);
|
||||
await Pdf.Print(PdfPath, copies, IsDoublePaged);
|
||||
}
|
||||
|
||||
public void Show() {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\Document.css" />
|
||||
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\Document.Page.css" />
|
||||
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\Document.Table.css" />
|
||||
@if (Model.DoublePaged) {
|
||||
@if (Model.IsDoublePaged) {
|
||||
<style>
|
||||
@@page :left {
|
||||
margin: 25mm 25mm 35mm 20mm;
|
||||
@@ -34,14 +34,14 @@
|
||||
<div class="pre-footer">
|
||||
<span class="date">@($"{Model.Date:dddd, d. MMMM yyyy}")</span>
|
||||
<span class="doc-id">@Model.DocumentId</span>
|
||||
<span class="page"></span>
|
||||
<span><span class="page"></span>@Raw(Model.IsPreview ? " <b>(vorläufig)</b>" : "")</span>
|
||||
</div>
|
||||
<footer>@Raw(Model.Footer)</footer>
|
||||
</div>
|
||||
@if (Model.DoublePaged) {
|
||||
@if (Model.IsDoublePaged) {
|
||||
<div class="footer-wrapper left">
|
||||
<div class="pre-footer">
|
||||
<span class="page"></span>
|
||||
<span>@Raw(Model.IsPreview ? "<b>(vorläufig)</b> " : "")<span class="page"></span></span>
|
||||
<span class="doc-id">@Model.DocumentId</span>
|
||||
<span class="date">@($"{Model.Date:dddd, d. MMMM yyyy}")</span>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 24pt;
|
||||
margin-top: 10mm;
|
||||
margin-top: 0;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace Elwig.Documents {
|
||||
Data = data;
|
||||
CurrencySymbol = v.Season.Currency.Symbol ?? v.Season.Currency.Code;
|
||||
MemberNum = v.Credits.Count;
|
||||
IsPreview = MemberNum == 0;
|
||||
DeliveryNum = v.DeliveryPartPayments.DistinctBy(p => p.DeliveryPart.Delivery).Count();
|
||||
DeliveryPartNum = v.DeliveryPartPayments.Count;
|
||||
ModifierStat = AppDbContext.GetModifierStats(v.Year, v.AvNr).GetAwaiter().GetResult();
|
||||
|
||||
@@ -164,14 +164,14 @@
|
||||
<td class="number">@Utils.GetSign(considered)</td>
|
||||
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(considered):N2}")</td>
|
||||
<th class="lborder">Menge (gebunden):</th>
|
||||
<td colspan="2" class="number">@($"{Model.Data.Rows.Sum(r => r.Geb.Weight):N0}") kg</td>
|
||||
<td colspan="2" class="number">@($"{Model.Data.Rows.Sum(r => r.Geb.Weight + r.LowGeb.Weight):N0}") kg</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="2">Auszahlungsbetrag:</th>
|
||||
<td class="number tborder"></td>
|
||||
<td class="number tborder"><span class="fleft">@Model.CurrencySymbol</span>@($"{totalSum:N2}")</td>
|
||||
<th class="lborder">Gesamtmenge:</th>
|
||||
<td colspan="2" class="number tborder">@($"{Model.Data.Rows.Sum(r => r.Ungeb.Weight + r.Geb.Weight):N0}") kg</td>
|
||||
<td colspan="2" class="number tborder">@($"{Model.Data.Rows.Sum(r => r.Ungeb.Weight + r.LowGeb.Weight + r.Geb.Weight):N0}") kg</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 24pt;
|
||||
margin-top: 10mm;
|
||||
margin-top: 0;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 24pt;
|
||||
margin-top: 10mm;
|
||||
margin-top: 0;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
<ApplicationIcon>Resources\Images\Elwig.ico</ApplicationIcon>
|
||||
<Version>1.0.0.0</Version>
|
||||
<Version>1.0.3.0</Version>
|
||||
<SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
@@ -22,20 +22,23 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="LinqKit" Version="1.3.8" />
|
||||
<PackageReference Include="MailKit" Version="4.13.0" />
|
||||
<PackageReference Include="LinqKit" Version="1.3.9" />
|
||||
<PackageReference Include="MailKit" Version="4.14.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.36" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3351.48" />
|
||||
<PackageReference Include="NJsonSchema" Version="11.3.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
|
||||
<PackageReference Include="NJsonSchema" Version="11.5.2" />
|
||||
<PackageReference Include="PdfiumViewer" Version="2.13.0" />
|
||||
<PackageReference Include="PdfiumViewer.Native.x86_64.no_v8-no_xfa" Version="2018.4.8.256" />
|
||||
<PackageReference Include="RazorLight" Version="2.3.1" />
|
||||
<PackageReference Include="ScottPlot.WPF" Version="5.0.55" />
|
||||
<PackageReference Include="System.IO.Ports" Version="9.0.7" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.7" />
|
||||
<PackageReference Include="ScottPlot.WPF" Version="5.1.57" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.2" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="10.0.2" />
|
||||
<PackageReference Include="System.IO.Ports" Version="10.0.2" />
|
||||
<PackageReference Include="System.Management" Version="10.0.2" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -10,7 +10,6 @@ using Microsoft.Data.Sqlite;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Generic;
|
||||
using Elwig.Models.Dtos;
|
||||
using System.Reflection;
|
||||
using System.Data;
|
||||
|
||||
namespace Elwig.Helpers {
|
||||
@@ -67,7 +66,7 @@ namespace Elwig.Helpers {
|
||||
|
||||
public DbSet<OverUnderDeliveryRow> OverUnderDeliveryRows { get; private set; }
|
||||
public DbSet<AreaComUnderDeliveryRowSingle> AreaComUnderDeliveryRows { get; private set; }
|
||||
public DbSet<MemberDeliveryPerVariantRowSingle> MemberDeliveryPerVariantRows { get; private set; }
|
||||
public DbSet<MemberDeliveryPerVarietyRowSingle> MemberDeliveryPerVariantRows { get; private set; }
|
||||
public DbSet<MemberAreaComsRowSingle> MemberAreaComsRows { get; private set; }
|
||||
public DbSet<CreditNoteDeliveryRowSingle> CreditNoteDeliveryRows { get; private set; }
|
||||
public DbSet<CreditNoteRowSingle> CreditNoteRows { get; private set; }
|
||||
@@ -117,39 +116,6 @@ namespace Elwig.Helpers {
|
||||
return cnx;
|
||||
}
|
||||
|
||||
public static async Task ExecuteBatch(SqliteConnection cnx, string sql) {
|
||||
using var cmd = cnx.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
await (await cmd.ExecuteReaderAsync()).CloseAsync();
|
||||
}
|
||||
|
||||
public static async Task ExecuteEmbeddedScript(SqliteConnection cnx, Assembly asm, string name) {
|
||||
using var stream = asm.GetManifestResourceStream(name) ?? throw new FileNotFoundException("Unable to load embedded resource");
|
||||
using var reader = new StreamReader(stream);
|
||||
await ExecuteBatch(cnx, await reader.ReadToEndAsync());
|
||||
}
|
||||
|
||||
public static async Task<object?> ExecuteScalar(SqliteConnection cnx, string sql) {
|
||||
using var cmd = cnx.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
return await cmd.ExecuteScalarAsync();
|
||||
}
|
||||
|
||||
public static async Task<(string Table, long RowId, string Parent, long FkId)[]> ForeignKeyCheck(SqliteConnection cnx) {
|
||||
using var cmd = cnx.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA foreign_key_check";
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
var list = new List<(string, long, string, long)>();
|
||||
while (await reader.ReadAsync()) {
|
||||
var table = reader.GetString(0);
|
||||
var rowid = reader.GetInt64(1);
|
||||
var parent = reader.GetString(2);
|
||||
var fkid = reader.GetInt64(3);
|
||||
list.Add((table, rowid, parent, fkid));
|
||||
}
|
||||
return [.. list];
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
optionsBuilder.UseSqlite(ConnectionString);
|
||||
optionsBuilder.UseLazyLoadingProxies();
|
||||
@@ -248,13 +214,6 @@ namespace Elwig.Helpers {
|
||||
return c + 1;
|
||||
}
|
||||
|
||||
public async Task<WineQualLevel> GetWineQualityLevel(double kmw) {
|
||||
return await WineQualityLevels
|
||||
.Where(q => !q.IsPredicate && (q.MinKmw == null || q.MinKmw <= kmw))
|
||||
.OrderBy(q => q.MinKmw)
|
||||
.LastAsync();
|
||||
}
|
||||
|
||||
public void UpdateDeliveryPartModifiers(DeliveryPart part, IEnumerable<Modifier> oldModifiers, IEnumerable<Modifier> newModifiers) {
|
||||
foreach (var m in Modifiers.Where(m => m.Year == part.Year)) {
|
||||
var mod = new DeliveryPartModifier {
|
||||
|
||||
@@ -9,17 +9,17 @@ namespace Elwig.Helpers {
|
||||
public static class AppDbUpdater {
|
||||
|
||||
// Don't forget to update value in Tests/fetch-resources.bat!
|
||||
public static readonly int RequiredSchemaVersion = 32;
|
||||
public static readonly int RequiredSchemaVersion = 36;
|
||||
|
||||
private static int VersionOffset = 0;
|
||||
|
||||
public static async Task<Version> CheckDb() {
|
||||
using var cnx = AppDbContext.Connect();
|
||||
|
||||
var applId = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA application_id") ?? 0;
|
||||
var applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
|
||||
if (applId != 0x454C5747) throw new Exception($"Invalid application_id in database (0x{applId:X08})");
|
||||
|
||||
var schemaVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA schema_version") ?? 0;
|
||||
var schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0;
|
||||
VersionOffset = (int)(schemaVers % 100);
|
||||
if (VersionOffset != 0) {
|
||||
// schema was modified manually/externally
|
||||
@@ -27,12 +27,12 @@ namespace Elwig.Helpers {
|
||||
}
|
||||
await UpdateDbSchema(cnx, (int)(schemaVers / 100), RequiredSchemaVersion);
|
||||
|
||||
var userVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA user_version") ?? 0;
|
||||
var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version") ?? 0;
|
||||
var v = new Version((int)(userVers >> 24), (int)((userVers >> 16) & 0xFF), (int)((userVers >> 8) & 0xFF), (int)(userVers & 0xFF));
|
||||
|
||||
if (App.Version > v) {
|
||||
long vers = (App.Version.Major << 24) | (App.Version.Minor << 16) | (App.Version.Build << 8) | App.Version.Revision;
|
||||
await AppDbContext.ExecuteBatch(cnx, $"PRAGMA user_version = {vers}");
|
||||
await cnx.ExecuteBatch($"PRAGMA user_version = {vers}");
|
||||
}
|
||||
|
||||
return v;
|
||||
@@ -67,20 +67,20 @@ namespace Elwig.Helpers {
|
||||
if (toExecute.Count == 0)
|
||||
return;
|
||||
|
||||
await AppDbContext.ExecuteBatch(cnx, """
|
||||
await cnx.ExecuteBatch("""
|
||||
PRAGMA locking_mode = EXCLUSIVE;
|
||||
BEGIN EXCLUSIVE;
|
||||
""");
|
||||
foreach (var script in toExecute) {
|
||||
await AppDbContext.ExecuteEmbeddedScript(cnx, asm, script);
|
||||
await cnx.ExecuteEmbeddedScript(asm, script);
|
||||
}
|
||||
var violations = await AppDbContext.ForeignKeyCheck(cnx);
|
||||
var violations = await cnx.ForeignKeyCheck();
|
||||
if (violations.Length > 0) {
|
||||
throw new Exception($"Foreign key violations ({violations.Length}):\n" + string.Join("\n", violations
|
||||
.Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}")));
|
||||
}
|
||||
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
COMMIT;
|
||||
VACUUM;
|
||||
PRAGMA schema_version = {toVersion * 100 + VersionOffset};
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Elwig.Helpers.Billing {
|
||||
|
||||
public async Task FinishSeason() {
|
||||
using var cnx = await AppDbContext.ConnectAsync();
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
UPDATE season
|
||||
SET (start_date, end_date) = (SELECT MIN(date), MAX(date) FROM delivery WHERE year = {Year})
|
||||
WHERE year = {Year};
|
||||
@@ -37,7 +37,7 @@ namespace Elwig.Helpers.Billing {
|
||||
public async Task AutoAdjustBusinessShares(DateOnly date, int allowanceKg = 0, double allowanceBs = 0, int allowanceKgPerBs = 0, double allowanceRel = 0, int addMinBs = 1) {
|
||||
if (addMinBs < 1) addMinBs = 1;
|
||||
using var cnx = await AppDbContext.ConnectAsync();
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
UPDATE member
|
||||
SET business_shares = member.business_shares - h.business_shares
|
||||
FROM member_history h
|
||||
@@ -66,7 +66,7 @@ namespace Elwig.Helpers.Billing {
|
||||
|
||||
public async Task UnAdjustBusinessShares() {
|
||||
using var cnx = await AppDbContext.ConnectAsync();
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
UPDATE member
|
||||
SET business_shares = member.business_shares - h.business_shares
|
||||
FROM member_history h
|
||||
@@ -157,9 +157,9 @@ namespace Elwig.Helpers.Billing {
|
||||
lastMgNr = mgnr;
|
||||
}
|
||||
|
||||
await AppDbContext.ExecuteBatch(cnx, $"UPDATE delivery_part_bucket SET value = 0 WHERE year = {Year}");
|
||||
await cnx.ExecuteBatch($"UPDATE delivery_part_bucket SET value = 0 WHERE year = {Year}");
|
||||
if (inserts.Count > 0) {
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
|
||||
VALUES {string.Join(",\n ", inserts.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '{i.Item4}', {i.Item5})"))}
|
||||
ON CONFLICT DO UPDATE
|
||||
@@ -237,7 +237,7 @@ namespace Elwig.Helpers.Billing {
|
||||
if (needed == 0) break;
|
||||
}
|
||||
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
|
||||
VALUES {string.Join(",\n ", posChanges.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '', {i.Item4})"))}
|
||||
ON CONFLICT DO UPDATE
|
||||
|
||||
@@ -20,13 +20,19 @@ namespace Elwig.Helpers.Billing {
|
||||
Data = PaymentBillingData.FromJson(PaymentVariant.Data, Utils.GetVaributes(ctx, Year, onlyDelivered: false));
|
||||
}
|
||||
|
||||
public async Task Calculate(bool? honorGebunden = null, bool? allowAttrsIntoLower = null, bool? avoidUnderDeliveries = null) {
|
||||
public async Task Calculate(bool strictPrices = true, bool? honorGebunden = null, bool? allowAttrsIntoLower = null, bool? avoidUnderDeliveries = null) {
|
||||
using var cnx = await AppDbContext.ConnectAsync();
|
||||
using var tx = await cnx.BeginTransactionAsync();
|
||||
await CalculateBuckets(honorGebunden, allowAttrsIntoLower, avoidUnderDeliveries, cnx);
|
||||
await DeleteInDb(cnx);
|
||||
await SetCalcTime(cnx);
|
||||
await CalculatePrices(cnx);
|
||||
KeyNotFoundException? exception = null;
|
||||
try {
|
||||
await CalculatePrices(cnx, strictPrices);
|
||||
} catch (KeyNotFoundException e) {
|
||||
if (strictPrices) throw;
|
||||
exception = e;
|
||||
}
|
||||
if (Data.ConsiderDelieryModifiers) {
|
||||
await CalculateDeliveryModifiers(cnx);
|
||||
}
|
||||
@@ -34,26 +40,28 @@ namespace Elwig.Helpers.Billing {
|
||||
await CalculateMemberModifiers(cnx);
|
||||
}
|
||||
await tx.CommitAsync();
|
||||
if (exception != null)
|
||||
throw exception;
|
||||
}
|
||||
|
||||
public async Task Commit() {
|
||||
await Revert();
|
||||
using var cnx = await AppDbContext.ConnectAsync();
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
INSERT INTO credit (year, tgnr, mgnr, avnr, net_amount, prev_net_amount, vat, modifiers, prev_modifiers)
|
||||
SELECT s.year,
|
||||
COALESCE(t.tgnr, 0) + ROW_NUMBER() OVER(ORDER BY m.mgnr) AS tgnr,
|
||||
m.mgnr,
|
||||
v.avnr,
|
||||
ROUND(p.amount / POW(10, s.precision - 2)) AS net_amount,
|
||||
IIF(lc.amount >= 0, ROUND(lp.amount / POW(10, s.precision - 2)), 0) AS prev_net_amount,
|
||||
IIF(lc.amount < 0, 0, ROUND(lp.amount / POW(10, s.precision - 2))) AS prev_net_amount,
|
||||
IIF(m.buchführend, s.vat_normal, s.vat_flatrate) AS vat,
|
||||
ROUND(IIF({Data.ConsiderTotalPenalty}, COALESCE(b.total_penalty, 0), 0) / POW(10, s.precision - 2)) +
|
||||
ROUND(IIF({Data.ConsiderContractPenalties}, COALESCE(u.total_penalty, 0), 0) / POW(10, 4 - 2)) +
|
||||
ROUND(IIF({Data.ConsiderAutoBusinessShares}, -COALESCE(a.total_amount, 0), 0) / POW(10, s.precision - 2)) +
|
||||
IIF({Data.ConsiderCustomModifiers}, COALESCE(x.amount, 0), 0)
|
||||
AS modifiers,
|
||||
IIF(lc.amount >= 0, lc.modifiers, 0) AS prev_modifiers
|
||||
IIF(lc.amount < 0, 0, lc.modifiers) AS prev_modifiers
|
||||
FROM season s
|
||||
JOIN payment_variant v ON v.year = s.year
|
||||
LEFT JOIN payment_variant l ON l.year = s.year
|
||||
@@ -74,27 +82,27 @@ namespace Elwig.Helpers.Billing {
|
||||
LEFT JOIN payment_custom x ON (x.year, x.mgnr) = (s.year, m.mgnr)
|
||||
WHERE s.year = {Year} AND v.avnr = {AvNr};
|
||||
""");
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
UPDATE payment_variant SET test_variant = FALSE WHERE (year, avnr) = ({Year}, {AvNr});
|
||||
""");
|
||||
}
|
||||
|
||||
public async Task Revert() {
|
||||
using var cnx = await AppDbContext.ConnectAsync();
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
DELETE FROM credit WHERE (year, avnr) = ({Year}, {AvNr});
|
||||
UPDATE payment_variant SET test_variant = TRUE WHERE (year, avnr) = ({Year}, {AvNr});
|
||||
""");
|
||||
}
|
||||
|
||||
protected async Task SetCalcTime(SqliteConnection cnx) {
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
UPDATE payment_variant SET calc_time = UNIXEPOCH() WHERE (year, avnr) = ({Year}, {AvNr})
|
||||
""");
|
||||
}
|
||||
|
||||
protected async Task DeleteInDb(SqliteConnection cnx) {
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
DELETE FROM payment_delivery_part_bucket WHERE (year, avnr) = ({Year}, {AvNr});
|
||||
DELETE FROM payment_delivery_part WHERE (year, avnr) = ({Year}, {AvNr});
|
||||
DELETE FROM payment_member WHERE (year, avnr) = ({Year}, {AvNr});
|
||||
@@ -108,7 +116,7 @@ namespace Elwig.Helpers.Billing {
|
||||
var multiplier = 0.50;
|
||||
var includePredecessor = true;
|
||||
var modName = "Treue%";
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
INSERT INTO payment_member (year, avnr, mgnr, net_amount, mod_abs, mod_rel)
|
||||
SELECT c.year, {AvNr}, s.mgnr, 0,
|
||||
ROUND(s.sum * COALESCE(m.abs, 0)),
|
||||
@@ -130,7 +138,7 @@ namespace Elwig.Helpers.Billing {
|
||||
mod_rel = mod_rel + excluded.mod_rel
|
||||
""");
|
||||
}
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
INSERT INTO payment_member (year, avnr, mgnr, net_amount, mod_abs, mod_rel)
|
||||
SELECT x.year, {AvNr}, x.mgnr, 0, COALESCE(x.mod_abs * POW(10, s.precision - 2), 0), COALESCE(x.mod_rel, 0)
|
||||
FROM payment_custom x
|
||||
@@ -142,7 +150,8 @@ namespace Elwig.Helpers.Billing {
|
||||
""");
|
||||
}
|
||||
|
||||
protected async Task CalculatePrices(SqliteConnection cnx) {
|
||||
protected async Task CalculatePrices(SqliteConnection cnx, bool strict = true) {
|
||||
var invalid = new HashSet<string>();
|
||||
var parts = new List<(int Year, int DId, int DPNr, int BktNr, string SortId, string? AttrId, string? CultId, string Discr, int Value, double Oe, double Kmw, string QualId, bool AttrAreaCom)>();
|
||||
using (var cmd = cnx.CreateCommand()) {
|
||||
cmd.CommandText = $"""
|
||||
@@ -172,21 +181,31 @@ namespace Elwig.Helpers.Billing {
|
||||
var payAttrId = (part.Discr is "" or "_") ? null : part.Discr;
|
||||
var attrId = part.AttrAreaCom ? payAttrId : part.AttrId;
|
||||
var geb = !ungeb && (payAttrId == attrId || !part.AttrAreaCom);
|
||||
var price = Data.CalculatePrice(part.SortId, attrId, part.CultId, part.QualId, geb, part.Oe, part.Kmw);
|
||||
decimal price = 0;
|
||||
try {
|
||||
price = Data.CalculatePrice(part.SortId, attrId, part.CultId, part.QualId, geb, part.Oe, part.Kmw);
|
||||
} catch (KeyNotFoundException e) {
|
||||
invalid.Add(e.Message.Split('\'')[1]);
|
||||
}
|
||||
var priceL = PaymentVariant.Season.DecToDb(price);
|
||||
inserts.Add((part.Year, part.DId, part.DPNr, part.BktNr, priceL, priceL * part.Value));
|
||||
}
|
||||
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
var msg = invalid.Count == 0 ? null : "Für folgende Sorten wurde noch keine Preiskurve festgelegt: " + string.Join(", ", invalid);
|
||||
if (msg != null && strict)
|
||||
throw new KeyNotFoundException(msg);
|
||||
await cnx.ExecuteBatch($"""
|
||||
INSERT INTO payment_delivery_part_bucket (year, did, dpnr, bktnr, avnr, price, amount)
|
||||
VALUES {string.Join(",\n ", inserts.Select(i => $"({i.Year}, {i.DId}, {i.DPNr}, {i.BktNr}, {AvNr}, {i.Price}, {i.Amount})"))};
|
||||
""");
|
||||
if (msg != null)
|
||||
throw new KeyNotFoundException(msg);
|
||||
}
|
||||
|
||||
protected async Task CalculateDeliveryModifiers(SqliteConnection cnx) {
|
||||
var netMod = Data.NetWeightModifier.ToString().Replace(',', '.');
|
||||
var grossMod = Data.GrossWeightModifier.ToString().Replace(',', '.');
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
INSERT INTO payment_delivery_part (year, did, dpnr, avnr, net_amount, mod_abs, mod_rel)
|
||||
SELECT d.year, d.did, d.dpnr, {AvNr}, 0, 0, IIF(d.net_weight, {netMod}, {grossMod})
|
||||
FROM delivery_part d
|
||||
|
||||
@@ -19,14 +19,6 @@ namespace Elwig.Helpers {
|
||||
public bool IsSitzendorf => IsWinzerkeller && App.ZwstId == "S";
|
||||
public bool IsGrInzersdorf => IsWeinland;
|
||||
|
||||
public bool HasNetWeighing(string? zwstId) => IsMatzen || (IsWinzerkeller && zwstId == "W");
|
||||
public bool HasNetWeighing(Branch? b) => HasNetWeighing(b?.ZwstId);
|
||||
public bool HasNetWeighing() => HasNetWeighing(App.ZwstId);
|
||||
|
||||
public bool HasBoxWeighing(string? zwstId) => IsWinzerkeller && (zwstId != "W");
|
||||
public bool HasBoxWeighing(Branch? b) => HasBoxWeighing(b?.ZwstId);
|
||||
public bool HasBoxWeighing() => HasBoxWeighing(App.ZwstId);
|
||||
|
||||
public string NameToken;
|
||||
public string NameShort;
|
||||
public string Name;
|
||||
@@ -214,15 +206,15 @@ namespace Elwig.Helpers {
|
||||
case 1: orderingMemberList = "NAME"; break;
|
||||
case 2: orderingMemberList = "KG"; break;
|
||||
}
|
||||
string mailSendPostal = "MGNR";
|
||||
switch (MailOrdering) {
|
||||
string mailSendPostal = "WISH";
|
||||
switch (MailSendPostal) {
|
||||
case 0: mailSendPostal = "NONE"; break;
|
||||
case 1: mailSendPostal = "NO_EMAIL"; break;
|
||||
case 2: mailSendPostal = "WISH"; break;
|
||||
case 3: mailSendPostal = "ALL"; break;
|
||||
}
|
||||
string mailSendEmail = "MGNR";
|
||||
switch (MailOrdering) {
|
||||
string mailSendEmail = "WISH";
|
||||
switch (MailSendEmail) {
|
||||
case 0: mailSendEmail = "NONE"; break;
|
||||
case 1: mailSendEmail = "WISH"; break;
|
||||
case 2: mailSendEmail = "ALL"; break;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -5,6 +6,8 @@ using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Elwig.Helpers {
|
||||
|
||||
public enum WeighingMode { Gross, Net, Box }
|
||||
|
||||
public record struct ScaleConfig {
|
||||
public string Id;
|
||||
public string? Type;
|
||||
@@ -41,6 +44,7 @@ namespace Elwig.Helpers {
|
||||
public string DatabaseFile = App.DataPath + "database.sqlite3";
|
||||
public string? DatabaseLog = null;
|
||||
public string? Branch = null;
|
||||
public WeighingMode? WeighingMode;
|
||||
public string? UpdateUrl = null;
|
||||
public bool UpdateAuto = false;
|
||||
public string? SyncUrl = null;
|
||||
@@ -74,6 +78,8 @@ namespace Elwig.Helpers {
|
||||
DatabaseLog = log != null ? Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, log) : null;
|
||||
Branch = config["general:branch"];
|
||||
Debug = TrueValues.Contains(config["general:debug"]?.ToLower());
|
||||
var weighing = config["general:weighing"];
|
||||
WeighingMode = weighing != null && Enum.TryParse<WeighingMode>(weighing, true, out var w) ? w : null;
|
||||
UpdateUrl = config["update:url"];
|
||||
UpdateAuto = TrueValues.Contains(config["update:auto"]?.ToLower());
|
||||
SyncUrl = config["sync:url"];
|
||||
|
||||
@@ -15,7 +15,9 @@ namespace Elwig.Helpers.Export {
|
||||
protected readonly char Separator;
|
||||
protected string? Header;
|
||||
|
||||
public Csv(string filename, char separator = ';') : this(filename, separator, Utils.UTF8) { }
|
||||
public Csv(string filename, char separator = ';') :
|
||||
this(filename, separator, Utils.UTF8) {
|
||||
}
|
||||
|
||||
public Csv(string filename, char separator, Encoding encoding) {
|
||||
_writer = new StreamWriter(filename, false, encoding);
|
||||
@@ -58,4 +60,22 @@ namespace Elwig.Helpers.Export {
|
||||
|
||||
public abstract string FormatRow(T row);
|
||||
}
|
||||
|
||||
public class CsvSimple : Csv<IEnumerable<object?>> {
|
||||
|
||||
public CsvSimple(string filename, char separator, Encoding encoding) :
|
||||
base(filename, separator, encoding) {
|
||||
}
|
||||
|
||||
public CsvSimple(string filename, char separator = ';') :
|
||||
base(filename, separator) {
|
||||
}
|
||||
|
||||
public override string FormatRow(IEnumerable<object?> row) {
|
||||
return string.Join(Separator, row.Select(i => {
|
||||
var str = $"{i}";
|
||||
return str.Contains(Separator) || str.Contains('\n') ? $"\"{str.Replace("\"", "\"\"")}\"" : str;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
243
Elwig/Helpers/Export/Database.cs
Normal file
243
Elwig/Helpers/Export/Database.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Elwig.Helpers.Export {
|
||||
public static class Database {
|
||||
|
||||
private static async Task<(long? ApplicationId, string? UserVersion, long? SchemaVersion, long FileSize)> GetMeta() {
|
||||
long size = new FileInfo(App.Config.DatabaseFile).Length;
|
||||
using var cnx = await AppDbContext.ConnectAsync();
|
||||
var applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id");
|
||||
var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version");
|
||||
var schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version");
|
||||
return (applId, userVers != null ? $"{userVers >> 24}.{(userVers >> 16) & 0xFF}.{(userVers >> 8) & 0xFF}.{userVers & 0xFF}" : null, schemaVers, size);
|
||||
}
|
||||
|
||||
public static async Task ExportSqlite(string filename, bool zipFile) {
|
||||
if (zipFile) {
|
||||
File.Delete(filename);
|
||||
using var zip = ZipFile.Open(filename, ZipArchiveMode.Create);
|
||||
|
||||
var version = zip.CreateEntry("version", CompressionLevel.NoCompression);
|
||||
using (var writer = new StreamWriter(version.Open(), Utils.UTF8)) {
|
||||
await writer.WriteAsync("elwig:1");
|
||||
}
|
||||
|
||||
var (applId, userVers, schemaVers, size) = await GetMeta();
|
||||
var meta = zip.CreateEntry("meta.json", CompressionLevel.NoCompression);
|
||||
using (var writer = new StreamWriter(meta.Open(), Utils.UTF8)) {
|
||||
var obj = new JsonObject {
|
||||
["timestamp"] = $"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}",
|
||||
["zwstid"] = App.ZwstId,
|
||||
["device"] = Environment.MachineName,
|
||||
["database"] = new JsonObject {
|
||||
["application_id"] = applId,
|
||||
["user_version"] = userVers,
|
||||
["schema_version"] = schemaVers,
|
||||
["file_size"] = size,
|
||||
},
|
||||
};
|
||||
await writer.WriteAsync(obj.ToJsonString(Utils.JsonOpts));
|
||||
}
|
||||
|
||||
var db = zip.CreateEntryFromFile(App.Config.DatabaseFile, "database.sqlite3", CompressionLevel.SmallestSize);
|
||||
} else {
|
||||
File.Copy(App.Config.DatabaseFile, filename, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ExportSql(string filename, bool zipFile) {
|
||||
if (zipFile) {
|
||||
File.Delete(filename);
|
||||
using var zip = ZipFile.Open(filename, ZipArchiveMode.Create);
|
||||
|
||||
var version = zip.CreateEntry("version", CompressionLevel.NoCompression);
|
||||
using (var writer = new StreamWriter(version.Open(), Utils.UTF8)) {
|
||||
await writer.WriteAsync("elwig:1");
|
||||
}
|
||||
|
||||
var (applId, userVers, schemaVers, size) = await GetMeta();
|
||||
var meta = zip.CreateEntry("meta.json", CompressionLevel.NoCompression);
|
||||
using (var writer = new StreamWriter(meta.Open(), Utils.UTF8)) {
|
||||
var obj = new JsonObject {
|
||||
["timestamp"] = $"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}",
|
||||
["zwstid"] = App.ZwstId,
|
||||
["device"] = Environment.MachineName,
|
||||
["database"] = new JsonObject {
|
||||
["application_id"] = applId,
|
||||
["user_version"] = userVers,
|
||||
["schema_version"] = schemaVers,
|
||||
["file_size"] = size,
|
||||
},
|
||||
};
|
||||
await writer.WriteAsync(obj.ToJsonString(Utils.JsonOpts));
|
||||
}
|
||||
|
||||
var sql = zip.CreateEntry("database.sql", CompressionLevel.SmallestSize);
|
||||
using (var writer = new StreamWriter(sql.Open(), Utils.UTF8)) {
|
||||
await ExportSql(writer);
|
||||
}
|
||||
} else {
|
||||
using var stream = File.OpenWrite(filename);
|
||||
using var writer = new StreamWriter(stream, Utils.UTF8);
|
||||
await ExportSql(writer);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ExportSql(StreamWriter writer) {
|
||||
using var cnx = await AppDbContext.ConnectAsync();
|
||||
|
||||
var tables = new List<(string Name, string Sql)>();
|
||||
using (var cmd = cnx.CreateCommand()) {
|
||||
cmd.CommandText = "SELECT name, sql FROM sqlite_schema WHERE type = 'table'";
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync()) {
|
||||
tables.Add((reader.GetString(0), reader.GetString(1)));
|
||||
}
|
||||
}
|
||||
|
||||
var applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
|
||||
var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version") ?? 0;
|
||||
var schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0;
|
||||
|
||||
await writer.WriteLineAsync($"-- Elwig database dump, {DateTime.Now:yyyy-MM-dd, HH:mm:ss}");
|
||||
await writer.WriteLineAsync($"-- {Environment.MachineName}, Zwst. {App.BranchName}, {App.Client.Name}");
|
||||
await writer.WriteLineAsync("BEGIN TRANSACTION;");
|
||||
await writer.WriteLineAsync("PRAGMA foreign_keys=OFF;");
|
||||
await writer.WriteLineAsync($"PRAGMA application_id=0x{applId:X8};");
|
||||
await writer.WriteLineAsync($"PRAGMA user_version=0x{userVers:X8};");
|
||||
|
||||
foreach (var t in tables) {
|
||||
await writer.WriteAsync(t.Sql);
|
||||
await writer.WriteLineAsync(";");
|
||||
|
||||
var columnNames = new List<string>();
|
||||
using (var cmd = cnx.CreateCommand()) {
|
||||
cmd.CommandText = $"PRAGMA table_info({t.Name})";
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync()) {
|
||||
columnNames.Add(reader.GetString(1));
|
||||
}
|
||||
}
|
||||
|
||||
using (var cmd = cnx.CreateCommand()) {
|
||||
cmd.CommandText = $"SELECT {string.Join(',', columnNames)} FROM {t.Name}";
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
var columns = await reader.GetColumnSchemaAsync();
|
||||
var values = new object[reader.FieldCount];
|
||||
while (await reader.ReadAsync()) {
|
||||
await writer.WriteAsync($"INSERT INTO {t.Name} VALUES (");
|
||||
|
||||
reader.GetValues(values);
|
||||
for (int i = 0; i < columns.Count; i++) {
|
||||
var c = columns[i];
|
||||
var v = values[i];
|
||||
if (i > 0) await writer.WriteAsync(",");
|
||||
if (v == null || v is DBNull) {
|
||||
await writer.WriteAsync("NULL");
|
||||
} else if (c.DataTypeName == "TEXT") {
|
||||
await writer.WriteAsync($"'{v.ToString()?.Replace("'", "''")}'");
|
||||
} else {
|
||||
await writer.WriteAsync(v.ToString()?.Replace(',', '.'));
|
||||
}
|
||||
}
|
||||
|
||||
await writer.WriteLineAsync(");");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (var cmd = cnx.CreateCommand()) {
|
||||
cmd.CommandText = "SELECT sql FROM sqlite_schema WHERE type != 'table' AND sql IS NOT NULL";
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync()) {
|
||||
await writer.WriteAsync(reader.GetString(0));
|
||||
await writer.WriteLineAsync(";");
|
||||
}
|
||||
}
|
||||
|
||||
await writer.WriteLineAsync($"PRAGMA schema_version={schemaVers};");
|
||||
await writer.WriteLineAsync("PRAGMA foreign_keys=ON;");
|
||||
await writer.WriteLineAsync("COMMIT;");
|
||||
await writer.WriteLineAsync("VACUUM;");
|
||||
}
|
||||
|
||||
public static async Task Import(string filename) {
|
||||
if (filename.EndsWith(".sql")) {
|
||||
await ImportSql(filename, false);
|
||||
} else if (filename.EndsWith(".sql.zip")) {
|
||||
await ImportSql(filename, true);
|
||||
} else if (filename.EndsWith(".sqlite3")) {
|
||||
await ImportSqlite(filename, false);
|
||||
} else if (filename.EndsWith(".sqlite3.zip")) {
|
||||
await ImportSqlite(filename, true);
|
||||
} else {
|
||||
throw new ArgumentException($"Unknown file extension for importing: '{filename}'");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ImportSql(string filename, bool zipFile = false) {
|
||||
if (zipFile) {
|
||||
using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
|
||||
await zip.CheckIntegrity();
|
||||
foreach (var entry in zip.Entries) {
|
||||
if (entry.Name.EndsWith(".sql")) {
|
||||
using var stream = entry.Open();
|
||||
using var reader = new StreamReader(stream, Utils.UTF8);
|
||||
await ImportSql(reader);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new FileFormatException("ZIP archive has to contain at least one .sql file");
|
||||
} else {
|
||||
using var stream = File.Open(filename, FileMode.Open);
|
||||
using var reader = new StreamReader(stream, Utils.UTF8);
|
||||
await ImportSql(reader);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ImportSqlite(string filename, bool zipFile = false) {
|
||||
if (zipFile) {
|
||||
var newName = Path.ChangeExtension(App.Config.DatabaseFile, ".new.sqlite3");
|
||||
try {
|
||||
using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
|
||||
await zip.CheckIntegrity();
|
||||
foreach (var entry in zip.Entries) {
|
||||
if (entry.Name.EndsWith(".sqlite3")) {
|
||||
entry.ExtractToFile(newName);
|
||||
await ImportSqlite(newName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new FileFormatException("ZIP archive has to contain at least one .sqlite3 file");
|
||||
} finally {
|
||||
if (File.Exists(newName)) File.Delete(newName);
|
||||
}
|
||||
}
|
||||
|
||||
var oldName = Path.ChangeExtension(App.Config.DatabaseFile, ".old.sqlite3");
|
||||
File.Move(App.Config.DatabaseFile, oldName, true);
|
||||
File.Move(filename, App.Config.DatabaseFile, false);
|
||||
|
||||
using var cnx = await AppDbContext.ConnectAsync();
|
||||
await cnx.ExecuteBatch("VACUUM");
|
||||
}
|
||||
|
||||
public static async Task ImportSql(StreamReader reader) {
|
||||
var newName = Path.ChangeExtension(App.Config.DatabaseFile, ".new.sqlite3");
|
||||
File.Delete(newName);
|
||||
try {
|
||||
using (var cnx = await AppDbContext.ConnectAsync($"Data Source=\"{newName}\"; Mode=ReadWriteCreate; Foreign Keys=False; Cache=Default; Pooling=False")) {
|
||||
await cnx.ExecuteBatch(await reader.ReadToEndAsync());
|
||||
}
|
||||
await ImportSqlite(newName);
|
||||
} finally {
|
||||
if (File.Exists(newName)) File.Delete(newName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
@@ -18,8 +17,6 @@ namespace Elwig.Helpers.Export {
|
||||
|
||||
public static readonly string ImportedTxt = Path.Combine(App.DataPath, "imported.txt");
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOpts = new() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
|
||||
|
||||
public static async Task<string[]> GetImportedFiles() {
|
||||
try {
|
||||
return await File.ReadAllLinesAsync(ImportedTxt, Utils.UTF8);
|
||||
@@ -41,6 +38,7 @@ namespace Elwig.Helpers.Export {
|
||||
Dictionary<string, int> currentLsNrs;
|
||||
Dictionary<int, List<WbRd>> currentWbRde;
|
||||
Dictionary<int, AT_Kg> kgs;
|
||||
List<WbGl> currentWbGls;
|
||||
|
||||
using (var ctx = new AppDbContext()) {
|
||||
branches = await ctx.Branches.ToDictionaryAsync(b => b.ZwstId);
|
||||
@@ -52,6 +50,7 @@ namespace Elwig.Helpers.Export {
|
||||
currentWbRde = await ctx.WbRde
|
||||
.GroupBy(r => r.KgNr)
|
||||
.ToDictionaryAsync(g => g.Key, g => g.ToList());
|
||||
currentWbGls = await ctx.WbGls.ToListAsync();
|
||||
kgs = await ctx.Katastralgemeinden.Include(k => k.WbKg).ToDictionaryAsync(k => k.KgNr);
|
||||
}
|
||||
|
||||
@@ -62,6 +61,8 @@ namespace Elwig.Helpers.Export {
|
||||
List<MemberEmailAddr> EmailAddresses,
|
||||
List<AreaCom> AreaCommitments,
|
||||
List<WbRd> Riede,
|
||||
List<WbKg> WbKgs,
|
||||
List<WbGl> WbGls,
|
||||
List<Delivery> Deliveries,
|
||||
List<DeliveryPart> DeliveryParts,
|
||||
List<DeliveryPartModifier> Modifiers,
|
||||
@@ -73,81 +74,117 @@ namespace Elwig.Helpers.Export {
|
||||
int? DeliveryNum, string? DeliveryFilters)>();
|
||||
|
||||
foreach (var filename in filenames) {
|
||||
// TODO read encrypted files
|
||||
using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
|
||||
try {
|
||||
data.Add(new([], [], [], [], [], [], [], new([], [], [], [], new() {
|
||||
["member"] = [],
|
||||
["area_commitment"] = [],
|
||||
["delivery"] = [],
|
||||
})));
|
||||
var r = data[^1];
|
||||
|
||||
var version = zip.GetEntry("version");
|
||||
using (var reader = new StreamReader(version!.Open(), Utils.UTF8)) {
|
||||
if (await reader.ReadToEndAsync() != "elwig:1")
|
||||
throw new FileFormatException($"Ungültige Export-Datei ({filename})");
|
||||
}
|
||||
// TODO read encrypted files
|
||||
using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
|
||||
await zip.CheckIntegrity();
|
||||
|
||||
var metaJson = zip.GetEntry("meta.json");
|
||||
var meta = await JsonNode.ParseAsync(metaJson!.Open());
|
||||
var memberCount = meta!["members"]?["count"]?.AsValue().GetValue<int>();
|
||||
var memberFilters = meta!["members"]?["filters"]?.AsArray().Select(f => f!.AsValue().GetValue<string>()).ToArray();
|
||||
var areaComCount = meta!["area_commitments"]?["count"]?.AsValue().GetValue<int>();
|
||||
var areaComFilters = meta!["area_commitments"]?["filters"]?.AsArray().Select(f => f!.AsValue().GetValue<string>()).ToArray();
|
||||
var deliveryCount = meta!["deliveries"]?["count"]?.AsValue().GetValue<int>();
|
||||
var deliveryFilters = meta!["deliveries"]?["filters"]?.AsArray().Select(f => f!.AsValue().GetValue<string>()).ToArray();
|
||||
metaData.Add((Path.GetFileName(filename),
|
||||
meta["zwstid"]!.AsValue().GetValue<string>(), meta["device"]!.AsValue().GetValue<string>(),
|
||||
memberCount, memberFilters != null ? string.Join(" / ", memberFilters) : null,
|
||||
areaComCount, areaComFilters != null ? string.Join(" / ", areaComFilters) : null,
|
||||
deliveryCount, deliveryFilters != null ? string.Join(" / ", deliveryFilters) : null));
|
||||
|
||||
data.Add(new([], [], [], [], [], [], [], new([], [], new() {
|
||||
["member"] = [],
|
||||
["area_commitment"] = [],
|
||||
["delivery"] = [],
|
||||
})));
|
||||
var r = data[^1];
|
||||
|
||||
var membersJson = zip.GetEntry("members.json");
|
||||
if (membersJson != null) {
|
||||
using var reader = new StreamReader(membersJson.Open(), Utils.UTF8);
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null) {
|
||||
var obj = JsonNode.Parse(line)!.AsObject();
|
||||
var (m, b, telNrs, emailAddrs, timestamps) = obj.ToMember(kgs);
|
||||
r.Members.Add(m);
|
||||
if (b != null) r.BillingAddresses.Add(b);
|
||||
r.TelephoneNumbers.AddRange(telNrs);
|
||||
r.EmailAddresses.AddRange(emailAddrs);
|
||||
if (timestamps.HasValue)
|
||||
r.Timestamps["member"].Add((m.MgNr, 0, timestamps.Value.CreatedAt, timestamps.Value.ModifiedAt));
|
||||
var version = zip.GetEntry("version");
|
||||
using (var reader = new StreamReader(version!.Open(), Utils.UTF8)) {
|
||||
if (await reader.ReadToEndAsync() != "elwig:1")
|
||||
throw new FileFormatException($"Ungültige Elwig-Export-Datei ({filename})");
|
||||
}
|
||||
}
|
||||
|
||||
var areaComsJson = zip.GetEntry("area_commitments.json");
|
||||
if (areaComsJson != null) {
|
||||
using var reader = new StreamReader(areaComsJson.Open(), Utils.UTF8);
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null) {
|
||||
var obj = JsonNode.Parse(line)!.AsObject();
|
||||
var (areaCom, wbrd, timestamps) = obj.ToAreaCom(kgs, currentWbRde);
|
||||
r.AreaCommitments.Add(areaCom);
|
||||
if (wbrd != null) {
|
||||
currentWbRde[wbrd.KgNr].Add(wbrd);
|
||||
r.Riede.Add(wbrd);
|
||||
var metaJson = zip.GetEntry("meta.json");
|
||||
var meta = await JsonNode.ParseAsync(metaJson!.Open());
|
||||
var memberCount = meta!["members"]?["count"]?.AsValue().GetValue<int>();
|
||||
var memberFilters = meta!["members"]?["filters"]?.AsArray().Select(f => f!.AsValue().GetValue<string>()).ToArray();
|
||||
var areaComCount = meta!["area_commitments"]?["count"]?.AsValue().GetValue<int>();
|
||||
var areaComFilters = meta!["area_commitments"]?["filters"]?.AsArray().Select(f => f!.AsValue().GetValue<string>()).ToArray();
|
||||
var deliveryCount = meta!["deliveries"]?["count"]?.AsValue().GetValue<int>();
|
||||
var deliveryFilters = meta!["deliveries"]?["filters"]?.AsArray().Select(f => f!.AsValue().GetValue<string>()).ToArray();
|
||||
metaData.Add((Path.GetFileName(filename),
|
||||
meta["zwstid"]!.AsValue().GetValue<string>(), meta["device"]!.AsValue().GetValue<string>(),
|
||||
memberCount, memberFilters != null ? string.Join(" / ", memberFilters) : null,
|
||||
areaComCount, areaComFilters != null ? string.Join(" / ", areaComFilters) : null,
|
||||
deliveryCount, deliveryFilters != null ? string.Join(" / ", deliveryFilters) : null));
|
||||
|
||||
var wbKgsJson = zip.GetEntry("wb_kgs.json");
|
||||
if (wbKgsJson != null) {
|
||||
using var reader = new StreamReader(wbKgsJson.Open(), Utils.UTF8);
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null) {
|
||||
var obj = JsonNode.Parse(line)!.AsObject();
|
||||
var (k, g) = obj.ToWbKg(currentWbGls);
|
||||
r.WbKgs.Add(k);
|
||||
if (g != null) {
|
||||
currentWbGls[g.GlNr] = g;
|
||||
r.WbGls.Add(g);
|
||||
}
|
||||
}
|
||||
if (timestamps.HasValue)
|
||||
r.Timestamps["area_commitment"].Add((areaCom.FbNr, 0, timestamps.Value.CreatedAt, timestamps.Value.ModifiedAt));
|
||||
}
|
||||
}
|
||||
|
||||
var deliveriesJson = zip.GetEntry("deliveries.json");
|
||||
if (deliveriesJson != null) {
|
||||
using var reader = new StreamReader(deliveriesJson.Open(), Utils.UTF8);
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null) {
|
||||
var obj = JsonNode.Parse(line)!.AsObject();
|
||||
var (d, parts, mods, timestamps) = obj.ToDelivery(currentLsNrs, currentDids);
|
||||
r.Deliveries.Add(d);
|
||||
r.DeliveryParts.AddRange(parts);
|
||||
r.Modifiers.AddRange(mods);
|
||||
if (timestamps.HasValue)
|
||||
r.Timestamps["delivery"].Add((d.Year, d.DId, timestamps.Value.CreatedAt, timestamps.Value.ModifiedAt));
|
||||
var membersJson = zip.GetEntry("members.json");
|
||||
if (membersJson != null) {
|
||||
using var reader = new StreamReader(membersJson.Open(), Utils.UTF8);
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null) {
|
||||
var obj = JsonNode.Parse(line)!.AsObject();
|
||||
var (m, b, telNrs, emailAddrs, timestamps) = obj.ToMember(kgs);
|
||||
r.Members.Add(m);
|
||||
if (b != null) r.BillingAddresses.Add(b);
|
||||
r.TelephoneNumbers.AddRange(telNrs);
|
||||
r.EmailAddresses.AddRange(emailAddrs);
|
||||
if (timestamps.HasValue)
|
||||
r.Timestamps["member"].Add((m.MgNr, 0, timestamps.Value.CreatedAt, timestamps.Value.ModifiedAt));
|
||||
}
|
||||
}
|
||||
|
||||
var areaComsJson = zip.GetEntry("area_commitments.json");
|
||||
if (areaComsJson != null) {
|
||||
using var reader = new StreamReader(areaComsJson.Open(), Utils.UTF8);
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null) {
|
||||
var obj = JsonNode.Parse(line)!.AsObject();
|
||||
var (areaCom, wbrd, timestamps) = obj.ToAreaCom(currentWbRde);
|
||||
r.AreaCommitments.Add(areaCom);
|
||||
if (wbrd != null) {
|
||||
r.Riede.Add(wbrd);
|
||||
}
|
||||
if (timestamps.HasValue)
|
||||
r.Timestamps["area_commitment"].Add((areaCom.FbNr, 0, timestamps.Value.CreatedAt, timestamps.Value.ModifiedAt));
|
||||
}
|
||||
}
|
||||
|
||||
var deliveriesJson = zip.GetEntry("deliveries.json");
|
||||
if (deliveriesJson != null) {
|
||||
using var reader = new StreamReader(deliveriesJson.Open(), Utils.UTF8);
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null) {
|
||||
var obj = JsonNode.Parse(line)!.AsObject();
|
||||
var (d, parts, mods, rde, timestamps) = obj.ToDelivery(currentLsNrs, currentDids, kgs, currentWbRde);
|
||||
r.Deliveries.Add(d);
|
||||
r.DeliveryParts.AddRange(parts);
|
||||
r.Modifiers.AddRange(mods);
|
||||
r.Riede.AddRange(rde);
|
||||
if (timestamps.HasValue)
|
||||
r.Timestamps["delivery"].Add((d.Year, d.DId, timestamps.Value.CreatedAt, timestamps.Value.ModifiedAt));
|
||||
}
|
||||
}
|
||||
} catch (Exception exc) when (
|
||||
exc is InvalidDataException ||
|
||||
exc is FileFormatException ||
|
||||
exc is FileNotFoundException ||
|
||||
exc is IOException) {
|
||||
data.RemoveAt(data.Count - 1);
|
||||
var str = $"Die Elwig-Export-Datei '{Path.GetFileName(filename)}' konnte nicht verarbeitet werden und wird übersprungen.\n\n" + exc.Message;
|
||||
if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;
|
||||
MessageBox.Show(str, "Fehler beim Importieren", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
await AddImportedFiles(Path.GetFileName(filename));
|
||||
} catch (Exception exc) {
|
||||
data.RemoveAt(data.Count - 1);
|
||||
var str = $"Die Elwig-Export-Datei '{Path.GetFileName(filename)}' konnte nicht verarbeitet werden. Soll sie in Zukunft übersprungen werden?\n\n" + exc.Message;
|
||||
if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;
|
||||
var r = MessageBox.Show(str, "Fehler beim Importieren", MessageBoxButton.YesNo, MessageBoxImage.Error, MessageBoxResult.No);
|
||||
if (r == MessageBoxResult.Yes) {
|
||||
await AddImportedFiles(Path.GetFileName(filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,12 +193,18 @@ namespace Elwig.Helpers.Export {
|
||||
var importedAreaComs = new List<(string FileName, string ZwstId, string Device, int Imported, int NotImported, string Filters)>();
|
||||
var importedDeliveries = new List<(string FileName, string ZwstId, string Device, int New, int Overwritten, int NotImported, string Filters)>();
|
||||
|
||||
foreach (var ((members, billingAddresses, telephoneNumbers, emailAddresses, areaCommitments, riede, deliveries, deliveryParts, modifiers, timestamps), meta) in data.Zip(metaData)) {
|
||||
foreach (var ((members, billingAddresses, telephoneNumbers, emailAddresses, areaCommitments, riede, wbKgs, wbGls, deliveries, deliveryParts, modifiers, timestamps), meta) in data.Zip(metaData)) {
|
||||
var branch = branches[meta.ZwstId];
|
||||
var device = meta.Device;
|
||||
|
||||
using var ctx = new AppDbContext();
|
||||
|
||||
var kgnrs = wbKgs.Select(k => k.KgNr).ToList();
|
||||
var duplicateKgNrs = await ctx.WbKgs
|
||||
.Where(k => kgnrs.Contains(k.KgNr))
|
||||
.Select(k => k.KgNr)
|
||||
.ToListAsync();
|
||||
|
||||
var mgnrs = members.Select(m => m.MgNr).ToList();
|
||||
var duplicateMgNrs = await ctx.Members
|
||||
.Where(m => mgnrs.Contains(m.MgNr))
|
||||
@@ -215,6 +258,12 @@ namespace Elwig.Helpers.Export {
|
||||
importDuplicateDeliveries = ImportQuestion(branch.Name, device, "Lieferungen", true, duplicateLsNrs.Count);
|
||||
}
|
||||
|
||||
if (importDuplicateMembers || importNewMembers || importDuplicateDeliveries || importNewDeliveries) {
|
||||
ctx.AddRange(wbGls);
|
||||
ctx.UpdateRange(wbKgs.Where(k => duplicateKgNrs.Contains(k.KgNr)));
|
||||
ctx.AddRange(wbKgs.Where(k => !duplicateKgNrs.Contains(k.KgNr)));
|
||||
}
|
||||
|
||||
if (importDuplicateMembers) {
|
||||
ctx.RemoveRange(ctx.BillingAddresses.Where(a => duplicateMgNrs.Contains(a.MgNr)));
|
||||
ctx.RemoveRange(ctx.MemberTelephoneNrs.Where(n => duplicateMgNrs.Contains(n.MgNr)));
|
||||
@@ -296,7 +345,7 @@ namespace Elwig.Helpers.Export {
|
||||
$"mtime = {((DateTimeOffset)m.ModifiedAt.ToUniversalTime()).ToUnixTimeSeconds()} " +
|
||||
$"WHERE ({primaryKeys[e.Key]}) = ({m.Id1}, {m.Id2});"));
|
||||
using var cnx = AppDbContext.Connect();
|
||||
await AppDbContext.ExecuteBatch(cnx, $"""
|
||||
await cnx.ExecuteBatch($"""
|
||||
BEGIN;
|
||||
UPDATE client_parameter SET value = '0' WHERE param = 'ENABLE_TIME_TRIGGERS';
|
||||
{string.Join("\n", updateStmts)}
|
||||
@@ -334,9 +383,9 @@ namespace Elwig.Helpers.Export {
|
||||
"Importieren erfolgreich",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
} catch (Exception exc) {
|
||||
var str = "Der Eintrag konnte nicht in der Datenbank aktualisiert werden!\n\n" + exc.Message;
|
||||
var str = "Der Eintrag konnte nicht in der Datenbank aktualisiert werden!\n\nEvtl. muss die Datenbank manuell auf dieses Gerät kopieren werden.\n\n" + exc.Message;
|
||||
if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;
|
||||
MessageBox.Show(str, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show(str, "Fehler beim Importieren", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
@@ -351,26 +400,30 @@ namespace Elwig.Helpers.Export {
|
||||
) == MessageBoxResult.Yes;
|
||||
}
|
||||
|
||||
public static Task Export(string filename, IEnumerable<Member> members, IEnumerable<string> filters) {
|
||||
public static Task Export(string filename, IEnumerable<Member> members, IEnumerable<WbKg> wbKgs, IEnumerable<string> filters) {
|
||||
return new ElwigExport {
|
||||
Members = (members, filters)
|
||||
Members = (members, filters),
|
||||
WbKgs = (wbKgs, ["von exportierten Mitgliedern"]),
|
||||
}.Export(filename);
|
||||
}
|
||||
|
||||
public static Task Export(string filename, IEnumerable<Member> members, IEnumerable<AreaCom> areaComs, IEnumerable<string> filters) {
|
||||
public static Task Export(string filename, IEnumerable<Member> members, IEnumerable<AreaCom> areaComs, IEnumerable<WbKg> wbKgs, IEnumerable<string> filters) {
|
||||
return new ElwigExport {
|
||||
Members = (members, filters),
|
||||
AreaComs = (areaComs, ["von exportierten Mitgliedern"]),
|
||||
WbKgs = (wbKgs, ["von exportierten Mitgliedern und Flächenbindungen"]),
|
||||
}.Export(filename);
|
||||
}
|
||||
|
||||
public static Task Export(string filename, IEnumerable<Delivery> deliveries, IEnumerable<string> filters) {
|
||||
public static Task Export(string filename, IEnumerable<Delivery> deliveries, IEnumerable<WbKg> wbKgs, IEnumerable<string> filters) {
|
||||
return new ElwigExport {
|
||||
Deliveries = (deliveries, filters)
|
||||
Deliveries = (deliveries, filters),
|
||||
WbKgs = (wbKgs, ["von exportierten Lieferungen"]),
|
||||
}.Export(filename);
|
||||
}
|
||||
|
||||
public class ElwigExport {
|
||||
public (IEnumerable<WbKg> WbKgs, IEnumerable<string> Filters)? WbKgs { get; set; }
|
||||
public (IEnumerable<Member> Members, IEnumerable<string> Filters)? Members { get; set; }
|
||||
public (IEnumerable<AreaCom> AreaComs, IEnumerable<string> Filters)? AreaComs { get; set; }
|
||||
public (IEnumerable<Delivery> Deliveries, IEnumerable<string> Filters)? Deliveries { get; set; }
|
||||
@@ -391,6 +444,12 @@ namespace Elwig.Helpers.Export {
|
||||
["zwstid"] = App.ZwstId,
|
||||
["device"] = Environment.MachineName,
|
||||
};
|
||||
if (WbKgs != null) {
|
||||
obj["wb_kgs"] = new JsonObject {
|
||||
["count"] = WbKgs.Value.WbKgs.Count(),
|
||||
["filters"] = new JsonArray(WbKgs.Value.Filters.Select(f => (JsonNode)f).ToArray()),
|
||||
};
|
||||
}
|
||||
if (Members != null)
|
||||
obj["members"] = new JsonObject {
|
||||
["count"] = Members.Value.Members.Count(),
|
||||
@@ -407,34 +466,69 @@ namespace Elwig.Helpers.Export {
|
||||
["parts"] = Deliveries.Value.Deliveries.Sum(d => d.Parts.Count),
|
||||
["filters"] = new JsonArray(Deliveries.Value.Filters.Select(f => (JsonNode)f).ToArray()),
|
||||
};
|
||||
await writer.WriteAsync(obj.ToJsonString(JsonOpts));
|
||||
await writer.WriteAsync(obj.ToJsonString(Utils.JsonOpts));
|
||||
}
|
||||
|
||||
// TODO encrypt files
|
||||
if (WbKgs != null) {
|
||||
var json = zip.CreateEntry("wb_kgs.json", CompressionLevel.SmallestSize);
|
||||
using var writer = new StreamWriter(json.Open(), Utils.UTF8);
|
||||
foreach (var k in WbKgs.Value.WbKgs) {
|
||||
await writer.WriteLineAsync(k.ToJson().ToJsonString(Utils.JsonOpts));
|
||||
}
|
||||
}
|
||||
if (Members != null) {
|
||||
var json = zip.CreateEntry("members.json", CompressionLevel.SmallestSize);
|
||||
using var writer = new StreamWriter(json.Open(), Utils.UTF8);
|
||||
foreach (var m in Members.Value.Members) {
|
||||
await writer.WriteLineAsync(m.ToJson().ToJsonString(JsonOpts));
|
||||
await writer.WriteLineAsync(m.ToJson().ToJsonString(Utils.JsonOpts));
|
||||
}
|
||||
}
|
||||
if (AreaComs != null) {
|
||||
var json = zip.CreateEntry("area_commitments.json", CompressionLevel.SmallestSize);
|
||||
using var writer = new StreamWriter(json.Open(), Utils.UTF8);
|
||||
foreach (var c in AreaComs.Value.AreaComs) {
|
||||
await writer.WriteLineAsync(c.ToJson().ToJsonString(JsonOpts));
|
||||
await writer.WriteLineAsync(c.ToJson().ToJsonString(Utils.JsonOpts));
|
||||
}
|
||||
}
|
||||
if (Deliveries != null) {
|
||||
var json = zip.CreateEntry("deliveries.json", CompressionLevel.SmallestSize);
|
||||
using var writer = new StreamWriter(json.Open(), Utils.UTF8);
|
||||
foreach (var d in Deliveries.Value.Deliveries) {
|
||||
await writer.WriteLineAsync(d.ToJson().ToJsonString(JsonOpts));
|
||||
await writer.WriteLineAsync(d.ToJson().ToJsonString(Utils.JsonOpts));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static JsonObject ToJson(this WbKg k) {
|
||||
return new JsonObject {
|
||||
["kgnr"] = k.KgNr,
|
||||
["großlage"] = k.Gl?.Name,
|
||||
};
|
||||
}
|
||||
|
||||
public static (WbKg, WbGl?) ToWbKg(this JsonNode json, List<WbGl> gls) {
|
||||
var grosslage = json["großlage"]?.AsValue().GetValue<string>();
|
||||
WbGl? gl = null;
|
||||
bool newGl = false;
|
||||
if (grosslage != null) {
|
||||
gl = gls.FirstOrDefault(g => g.Name == grosslage);
|
||||
if (gl == null) {
|
||||
newGl = true;
|
||||
gl = new WbGl {
|
||||
GlNr = (gls.Count == 0 ? 1 : gls.Max(g => g.GlNr)) + 1,
|
||||
Name = grosslage,
|
||||
};
|
||||
gls[gl.GlNr] = gl;
|
||||
}
|
||||
}
|
||||
return (new WbKg {
|
||||
KgNr = json["kgnr"]!.AsValue().GetValue<int>(),
|
||||
GlNr = gl?.GlNr,
|
||||
}, newGl ? gl : null);
|
||||
}
|
||||
|
||||
public static JsonObject ToJson(this Member m) {
|
||||
return new JsonObject {
|
||||
["mgnr"] = m.MgNr,
|
||||
@@ -580,22 +674,23 @@ namespace Elwig.Helpers.Export {
|
||||
};
|
||||
}
|
||||
|
||||
public static (AreaCom, WbRd?, (DateTime CreatedAt, DateTime ModifiedAt)?) ToAreaCom(this JsonNode json, Dictionary<int, AT_Kg> kgs, Dictionary<int, List<WbRd>> riede) {
|
||||
public static (AreaCom, WbRd?, (DateTime CreatedAt, DateTime ModifiedAt)?) ToAreaCom(this JsonNode json, Dictionary<int, List<WbRd>> riede) {
|
||||
var kgnr = json["kgnr"]!.AsValue().GetValue<int>();
|
||||
var ried = json["ried"]?.AsValue().GetValue<string>();
|
||||
WbRd? rd = null;
|
||||
bool newRd = false;
|
||||
if (ried != null) {
|
||||
var rde = riede[kgnr] ?? throw new ArgumentException($"Für KG {(kgs.TryGetValue(kgnr, out var k) ? k.Name : "?")} ({kgnr:00000}) ist noch keine Großlage festgelegt!\n(Stammdaten → Herkunftshierarchie)");
|
||||
var rde = riede.GetValueOrDefault(kgnr, []);
|
||||
rd = rde.FirstOrDefault(r => r.Name == ried);
|
||||
if (rd == null) {
|
||||
newRd = true;
|
||||
rd = new WbRd {
|
||||
KgNr = kgnr,
|
||||
RdNr = (rde.Count == 0 ? 0 : rde.Max(r => r.RdNr)) + 1,
|
||||
RdNr = (rde.Count == 0 ? 1 : rde.Max(r => r.RdNr)) + 1,
|
||||
Name = ried,
|
||||
};
|
||||
rde.Add(rd);
|
||||
riede[rd.KgNr] = rde;
|
||||
}
|
||||
}
|
||||
var createdAt = json["created_at"]?.AsValue().GetValue<string>();
|
||||
@@ -608,7 +703,7 @@ namespace Elwig.Helpers.Export {
|
||||
Area = json["area"]!.AsValue().GetValue<int>(),
|
||||
KgNr = kgnr,
|
||||
GstNr = json["gstnr"]?.AsValue().GetValue<string>() ?? "-",
|
||||
RdNr = rd?.RdNr,
|
||||
RdNr = rd?.RdNr ?? json["rdnr"]?.AsValue().GetValue<int>(),
|
||||
YearFrom = json["year_from"]?.AsValue().GetValue<int>(),
|
||||
YearTo = json["year_to"]?.AsValue().GetValue<int>(),
|
||||
Comment = json["comment"]?.AsValue().GetValue<string>(),
|
||||
@@ -639,7 +734,7 @@ namespace Elwig.Helpers.Export {
|
||||
["qualid"] = p.QualId,
|
||||
["hkid"] = p.HkId,
|
||||
["kgnr"] = p.KgNr,
|
||||
["rdnr"] = p.RdNr,
|
||||
["ried"] = p.Rd?.Name,
|
||||
["net_weight"] = p.IsNetWeight,
|
||||
["manual_weighing"] = p.IsManualWeighing,
|
||||
["modids"] = new JsonArray(p.Modifiers.Select(m => (JsonNode)m.ModId).ToArray()),
|
||||
@@ -649,8 +744,8 @@ namespace Elwig.Helpers.Export {
|
||||
};
|
||||
if (p.IsSplCheck) obj["spl_check"] = p.IsSplCheck;
|
||||
if (p.IsHandPicked != null) obj["hand_picked"] = p.IsHandPicked;
|
||||
if (p.IsLesewagen != null) obj["lesewagen"] = p.IsLesewagen;
|
||||
if (p.IsGebunden != null) obj["gebunden"] = p.IsGebunden;
|
||||
if (p.Unloading != null) obj["unloading"] = p.Unloading;
|
||||
if (p.Temperature != null) obj["temperature"] = p.Temperature;
|
||||
if (p.Acid != null) obj["acid"] = p.Acid;
|
||||
if (p.ScaleId != null) obj["scale_id"] = p.ScaleId;
|
||||
@@ -664,7 +759,7 @@ namespace Elwig.Helpers.Export {
|
||||
};
|
||||
}
|
||||
|
||||
public static (Delivery, List<DeliveryPart>, List<DeliveryPartModifier>, (DateTime CreatedAt, DateTime ModifiedAt)?) ToDelivery(this JsonNode json, Dictionary<string, int> currentLsNrs, Dictionary<int, int> currentDids) {
|
||||
public static (Delivery, List<DeliveryPart>, List<DeliveryPartModifier>, List<WbRd>, (DateTime CreatedAt, DateTime ModifiedAt)?) ToDelivery(this JsonNode json, Dictionary<string, int> currentLsNrs, Dictionary<int, int> currentDids, Dictionary<int, AT_Kg> kgs, Dictionary<int, List<WbRd>> riede) {
|
||||
var year = json["year"]!.AsValue().GetValue<int>();
|
||||
var lsnr = json["lsnr"]!.AsValue().GetValue<string>();
|
||||
var did = currentLsNrs.GetValueOrDefault(lsnr, -1);
|
||||
@@ -675,6 +770,7 @@ namespace Elwig.Helpers.Export {
|
||||
currentLsNrs[lsnr] = did;
|
||||
var createdAt = json["created_at"]?.AsValue().GetValue<string>();
|
||||
var modifiedAt = json["modified_at"]?.AsValue().GetValue<string>();
|
||||
var wbRde = new List<WbRd>();
|
||||
return (new Delivery {
|
||||
Year = year,
|
||||
DId = did,
|
||||
@@ -686,37 +782,57 @@ namespace Elwig.Helpers.Export {
|
||||
MgNr = json["mgnr"]!.AsValue().GetValue<int>(),
|
||||
Comment = json["comment"]?.AsValue().GetValue<string>(),
|
||||
ImportedAt = DateTime.Now,
|
||||
}, json["parts"]!.AsArray().Select(p => p!.AsObject()).Select(p => new DeliveryPart {
|
||||
Year = year,
|
||||
DId = did,
|
||||
DPNr = p["dpnr"]!.AsValue().GetValue<int>(),
|
||||
SortId = p["sortid"]!.AsValue().GetValue<string>(),
|
||||
AttrId = p["attrid"]?.AsValue().GetValue<string>(),
|
||||
CultId = p["cultid"]?.AsValue().GetValue<string>(),
|
||||
Weight = p["weight"]!.AsValue().GetValue<int>(),
|
||||
Kmw = p["kmw"]!.AsValue().GetValue<double>(),
|
||||
QualId = p["qualid"]!.AsValue().GetValue<string>(),
|
||||
HkId = p["hkid"]!.AsValue().GetValue<string>(),
|
||||
KgNr = p["kgnr"]?.AsValue().GetValue<int>(),
|
||||
RdNr = p["rdnr"]?.AsValue().GetValue<int>(),
|
||||
IsNetWeight = p["net_weight"]!.AsValue().GetValue<bool>(),
|
||||
IsManualWeighing = p["manual_weighing"]!.AsValue().GetValue<bool>(),
|
||||
Comment = p["comment"]?.AsValue().GetValue<string>(),
|
||||
IsSplCheck = p["spl_check"]?.AsValue().GetValue<bool>() ?? false,
|
||||
IsHandPicked = p["hand_picked"]?.AsValue().GetValue<bool>(),
|
||||
IsLesewagen = p["lesewagen"]?.AsValue().GetValue<bool>(),
|
||||
IsGebunden = p["gebunden"]?.AsValue().GetValue<bool>(),
|
||||
Temperature = p["temperature"]?.AsValue().GetValue<double>(),
|
||||
Acid = p["acid"]?.AsValue().GetValue<double>(),
|
||||
ScaleId = p["scale_id"]?.AsValue().GetValue<string>(),
|
||||
WeighingData = p["weighing_data"]?.AsObject().ToJsonString(JsonOpts),
|
||||
WeighingReason = p["weighing_reason"]?.AsValue().GetValue<string>(),
|
||||
}, json["parts"]!.AsArray().Select(p => p!.AsObject()).Select(p => {
|
||||
var kgnr = p["kgnr"]!.AsValue().GetValue<int>();
|
||||
var ried = p["ried"]?.AsValue().GetValue<string>();
|
||||
WbRd? rd = null;
|
||||
if (ried != null) {
|
||||
var rde = riede.GetValueOrDefault(kgnr, []);
|
||||
rd = rde.FirstOrDefault(r => r.Name == ried);
|
||||
if (rd == null) {
|
||||
rd = new WbRd {
|
||||
KgNr = kgnr,
|
||||
RdNr = (rde.Count == 0 ? 1 : rde.Max(r => r.RdNr)) + 1,
|
||||
Name = ried,
|
||||
};
|
||||
rde.Add(rd);
|
||||
riede[rd.KgNr] = rde;
|
||||
wbRde.Add(rd);
|
||||
}
|
||||
}
|
||||
return new DeliveryPart {
|
||||
Year = year,
|
||||
DId = did,
|
||||
DPNr = p["dpnr"]!.AsValue().GetValue<int>(),
|
||||
SortId = p["sortid"]!.AsValue().GetValue<string>(),
|
||||
AttrId = p["attrid"]?.AsValue().GetValue<string>(),
|
||||
CultId = p["cultid"]?.AsValue().GetValue<string>(),
|
||||
Weight = p["weight"]!.AsValue().GetValue<int>(),
|
||||
Kmw = p["kmw"]!.AsValue().GetValue<double>(),
|
||||
QualId = p["qualid"]!.AsValue().GetValue<string>(),
|
||||
HkId = p["hkid"]!.AsValue().GetValue<string>(),
|
||||
KgNr = p["kgnr"]?.AsValue().GetValue<int>(),
|
||||
RdNr = rd?.RdNr ?? p["rdnr"]?.AsValue().GetValue<int>(),
|
||||
IsNetWeight = p["net_weight"]!.AsValue().GetValue<bool>(),
|
||||
IsManualWeighing = p["manual_weighing"]!.AsValue().GetValue<bool>(),
|
||||
Comment = p["comment"]?.AsValue().GetValue<string>(),
|
||||
IsSplCheck = p["spl_check"]?.AsValue().GetValue<bool>() ?? false,
|
||||
IsHandPicked = p["hand_picked"]?.AsValue().GetValue<bool>(),
|
||||
IsGebunden = p["gebunden"]?.AsValue().GetValue<bool>(),
|
||||
Unloading = p["unloading"]?.AsValue().GetValue<string>() ?? ((p["lesewagen"]?.AsValue().GetValue<bool>() ?? false) ? DeliveryPart.Pumped : null),
|
||||
Temperature = p["temperature"]?.AsValue().GetValue<double>(),
|
||||
Acid = p["acid"]?.AsValue().GetValue<double>(),
|
||||
ScaleId = p["scale_id"]?.AsValue().GetValue<string>(),
|
||||
WeighingData = p["weighing_data"]?.AsObject().ToJsonString(Utils.JsonOpts),
|
||||
WeighingReason = p["weighing_reason"]?.AsValue().GetValue<string>(),
|
||||
};
|
||||
}).ToList(), json["parts"]!.AsArray().SelectMany(p => p!["modids"]!.AsArray().Select(m => new DeliveryPartModifier {
|
||||
Year = year,
|
||||
DId = did,
|
||||
DPNr = p["dpnr"]!.AsValue().GetValue<int>(),
|
||||
ModId = m!.AsValue().GetValue<string>(),
|
||||
})).ToList(),
|
||||
wbRde,
|
||||
createdAt == null || modifiedAt == null ? null :
|
||||
(DateTime.ParseExact(createdAt, "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None),
|
||||
DateTime.ParseExact(modifiedAt, "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None)));
|
||||
|
||||
69
Elwig/Helpers/Export/VCard.cs
Normal file
69
Elwig/Helpers/Export/VCard.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Elwig.Models.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Elwig.Helpers.Export {
|
||||
public class VCard : IExporter<Member> {
|
||||
|
||||
public static string FileExtension => "vcf";
|
||||
|
||||
private readonly StreamWriter _writer;
|
||||
|
||||
public VCard(string filename) : this(filename, Utils.UTF8) { }
|
||||
|
||||
public VCard(string filename, Encoding encoding) {
|
||||
_writer = new StreamWriter(filename, false, encoding);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
GC.SuppressFinalize(this);
|
||||
_writer.Dispose();
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() {
|
||||
GC.SuppressFinalize(this);
|
||||
return _writer.DisposeAsync();
|
||||
}
|
||||
|
||||
public async Task ExportAsync(IEnumerable<Member> data, IProgress<double>? progress = null) {
|
||||
progress?.Report(0.0);
|
||||
int count = data.Count() + 1, i = 0;
|
||||
|
||||
foreach (var row in data) {
|
||||
var billingAddr = row.BillingAddress != null ? $"ADR;TYPE=work;LANGUAGE=de;LABEL=\"{Escape(row.BillingAddress.FullName)}\\n{Escape(row.BillingAddress.Address)}\\n{row.BillingAddress.PostalDest.AtPlz?.Plz} {Escape(row.BillingAddress.PostalDest.AtPlz?.Ort.Name)}\\nÖsterreich\":;;{Escape(row.BillingAddress.Address)};{Escape(row.BillingAddress.PostalDest.AtPlz?.Ort.Name)};;{row.BillingAddress.PostalDest.AtPlz?.Plz};Österreich\r\n" : null;
|
||||
var tel = string.Join("", row.TelephoneNumbers
|
||||
.Where(n => n.Type != "fax")
|
||||
.Select(n => $"TEL;TYPE={(n.Type == "mobile" ? "cell" : "voice")}:{Escape(n.Number)}\r\n"));
|
||||
var email = string.Join("", row.EmailAddresses.Select(a => $"EMAIL:{Escape(a.Address)}\r\n"));
|
||||
await _writer.WriteLineAsync($"""
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:mg{row.MgNr}@{App.Client.NameToken.ToLower()}.elwig.at
|
||||
NOTE:MgNr. {row.MgNr}
|
||||
FN:{Escape(row.AdministrativeName)}
|
||||
N:{Escape(row.Name)};{Escape(row.GivenName)};{Escape(row.MiddleName)};{Escape(row.Prefix)};{Escape(row.Suffix)}
|
||||
KIND:{(row.IsJuridicalPerson ? "org" : "individual")}
|
||||
ADR{(billingAddr == null ? "" : ";TYPE=home")};LANGUAGE=de;LABEL="{Escape(row.Address)}\n{row.PostalDest.AtPlz?.Plz} {Escape(row.PostalDest.AtPlz?.Ort.Name)}\nÖsterreich":;;{Escape(row.Address)};{Escape(row.PostalDest.AtPlz?.Ort.Name)};;{row.PostalDest.AtPlz?.Plz};Österreich
|
||||
{billingAddr}{tel}{email}REV:{row.ModifiedAt.ToUniversalTime():yyyyMMdd\THHmmss\Z}
|
||||
END:VCARD
|
||||
""");
|
||||
progress?.Report(100.0 * ++i / count);
|
||||
}
|
||||
|
||||
await _writer.FlushAsync();
|
||||
progress?.Report(100.0);
|
||||
}
|
||||
|
||||
public void Export(IEnumerable<Member> data, IProgress<double>? progress = null) {
|
||||
ExportAsync(data, progress).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static string? Escape(string? text) {
|
||||
return text?.Replace("\\", "\\\\").Replace(",", "\\,").Replace(";", "\\;").Replace("\n", "\\n").Replace("\r", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
namespace Elwig.Helpers {
|
||||
public enum ExportMode {
|
||||
Show, SaveList, SavePdf, Print, Email, Export, Upload
|
||||
Show, SaveList, SavePdf, Print, Email, Vcf, Export, Upload
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.IO.Hashing;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Elwig.Helpers {
|
||||
static partial class Extensions {
|
||||
public static partial class Extensions {
|
||||
|
||||
[LibraryImport("msvcrt.dll")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
|
||||
@@ -95,5 +100,50 @@ namespace Elwig.Helpers {
|
||||
await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken);
|
||||
progress.Report(100.0);
|
||||
}
|
||||
|
||||
public static async Task CheckIntegrity(this ZipArchive zip) {
|
||||
var crc = new Crc32();
|
||||
foreach (var entry in zip.Entries) {
|
||||
crc.Reset();
|
||||
using var stream = entry.Open();
|
||||
await crc.AppendAsync(stream);
|
||||
if (crc.GetCurrentHashAsUInt32() != entry.Crc32)
|
||||
throw new InvalidDataException($"CRC-32 mismatch in '{entry.FullName}'");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ExecuteBatch(this SqliteConnection cnx, string sql) {
|
||||
using var cmd = cnx.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
while (await reader.NextResultAsync()) ;
|
||||
}
|
||||
|
||||
public static async Task ExecuteEmbeddedScript(this SqliteConnection cnx, Assembly asm, string name) {
|
||||
using var stream = asm.GetManifestResourceStream(name) ?? throw new FileNotFoundException("Unable to load embedded resource");
|
||||
using var reader = new StreamReader(stream);
|
||||
await ExecuteBatch(cnx, await reader.ReadToEndAsync());
|
||||
}
|
||||
|
||||
public static async Task<object?> ExecuteScalar(this SqliteConnection cnx, string sql) {
|
||||
using var cmd = cnx.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
return await cmd.ExecuteScalarAsync();
|
||||
}
|
||||
|
||||
public static async Task<(string Table, long RowId, string Parent, long FkId)[]> ForeignKeyCheck(this SqliteConnection cnx) {
|
||||
using var cmd = cnx.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA foreign_key_check";
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
var list = new List<(string, long, string, long)>();
|
||||
while (await reader.ReadAsync()) {
|
||||
var table = reader.GetString(0);
|
||||
var rowid = reader.GetInt64(1);
|
||||
var parent = reader.GetString(2);
|
||||
var fkid = reader.GetInt64(3);
|
||||
list.Add((table, rowid, parent, fkid));
|
||||
}
|
||||
return [.. list];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
using System.Threading.Tasks;
|
||||
using Elwig.Windows;
|
||||
using System.Diagnostics;
|
||||
using PdfiumViewer;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing.Printing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using PdfiumViewer;
|
||||
using System.Drawing.Printing;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace Elwig.Helpers.Printing {
|
||||
public static class Pdf {
|
||||
|
||||
private static readonly string WinziPrint = new string[] { App.InstallPath }
|
||||
.Union(Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? [])
|
||||
.Select(x => Path.Combine(x, "WinziPrint.exe"))
|
||||
.Where(File.Exists)
|
||||
.FirstOrDefault() ?? throw new FileNotFoundException("WiniPrint executable not found");
|
||||
private static readonly string WinziPrint = (Assembly.GetEntryAssembly()?.Location.Contains(@"\bin\") ?? false) ?
|
||||
Path.Combine(Assembly.GetEntryAssembly()!.Location.Split(@"\bin\")[0], "../Installer/Files/WinziPrint.exe") :
|
||||
new string[] { App.InstallPath }
|
||||
.Union(Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? [])
|
||||
.Select(x => Path.Combine(x, "WinziPrint.exe"))
|
||||
.Where(File.Exists)
|
||||
.FirstOrDefault() ?? throw new FileNotFoundException("WiniPrint executable not found");
|
||||
private static Process? WinziPrintProc;
|
||||
public static bool IsReady => WinziPrintProc != null;
|
||||
|
||||
@@ -46,24 +50,45 @@ namespace Elwig.Helpers.Printing {
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static async Task<(int Pages, IEnumerable<int> PerDoc)> Convert(string htmlPath, string pdfPath, bool doublePaged = false, IProgress<double>? progress = null) {
|
||||
return await Convert([htmlPath], pdfPath, doublePaged, progress);
|
||||
public static async Task<(int Pages, IEnumerable<int> PerDoc)> Convert(string htmlPath, string pdfPath, bool doublePaged = false, CancellationToken? cancelToken = null, IProgress<double>? progress = null) {
|
||||
return await Convert([htmlPath], pdfPath, doublePaged, cancelToken, progress);
|
||||
}
|
||||
|
||||
public static async Task<(int Pages, IEnumerable<int> PerDoc)> Convert(IEnumerable<string> htmlPath, string pdfPath, bool doublePaged = false, IProgress<double>? progress = null) {
|
||||
public static async Task<(int Pages, IEnumerable<int> PerDoc)> Convert(IEnumerable<string> htmlPath, string pdfPath, bool doublePaged = false, CancellationToken? cancelToken = null, IProgress<double>? progress = null) {
|
||||
if (WinziPrintProc == null) throw new InvalidOperationException("The WinziPrint process has not been initialized yet");
|
||||
progress?.Report(0.0);
|
||||
using var client = new TcpClient("127.0.0.1", 30983);
|
||||
using var stream = client.GetStream();
|
||||
string cnxId;
|
||||
using var reader = new StreamReader(stream);
|
||||
var first = await reader.ReadLineAsync() ?? throw new IOException("Invalid response from WinziPrint");
|
||||
if (first.StartsWith("id:")) {
|
||||
cnxId = first[3..].Trim();
|
||||
} else {
|
||||
throw new IOException("Invalid response from WinziPrint");
|
||||
}
|
||||
await stream.WriteAsync(Utils.UTF8.GetBytes(
|
||||
"-e utf-8;-p;" + (doublePaged ? "-2;" : "") +
|
||||
$"{string.Join(';', htmlPath)};{pdfPath}" +
|
||||
"\r\n"));
|
||||
using var reader = new StreamReader(stream);
|
||||
bool cancelled = false;
|
||||
while (true) {
|
||||
if (!cancelled && (cancelToken?.IsCancellationRequested ?? false)) {
|
||||
try {
|
||||
using var cancelClient = new TcpClient("127.0.0.1", 30983);
|
||||
using var cancelStream = cancelClient.GetStream();
|
||||
using var cancelReader = new StreamReader(cancelStream);
|
||||
await cancelReader.ReadLineAsync();
|
||||
await cancelStream.WriteAsync(Utils.UTF8.GetBytes($"cancel;{cnxId}\r\n"));
|
||||
} catch { }
|
||||
cancelled = true;
|
||||
}
|
||||
var line = await reader.ReadLineAsync() ?? throw new IOException("Invalid response from WinziPrint");
|
||||
if (line.StartsWith("error:")) {
|
||||
throw new IOException($"WinziPrint: {line[6..].Trim()}");
|
||||
var msg = line[6..].Trim();
|
||||
if (msg == "aborted")
|
||||
throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!");
|
||||
throw new IOException($"WinziPrint: {msg}");
|
||||
} else if (line.StartsWith("progress:")) {
|
||||
var parts = line[9..].Trim().Split('/').Select(int.Parse).ToArray();
|
||||
progress?.Report(100.0 * parts[0] / parts[1]);
|
||||
|
||||
60
Elwig/Helpers/SerialPortWatcher.cs
Normal file
60
Elwig/Helpers/SerialPortWatcher.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.IO.Ports;
|
||||
using System.Linq;
|
||||
using System.Management;
|
||||
|
||||
namespace Elwig.Helpers {
|
||||
public sealed class SerialPortWatcher : IDisposable {
|
||||
|
||||
private readonly ManagementEventWatcher _deviceArrivalWatcher;
|
||||
private readonly ManagementEventWatcher _deviceRemovalWatcher;
|
||||
|
||||
private string[] _knownPorts;
|
||||
|
||||
public event EventHandler<string>? SerialPortConnected;
|
||||
public event EventHandler<string>? SerialPortDisconnected;
|
||||
|
||||
public SerialPortWatcher() {
|
||||
_knownPorts = SerialPort.GetPortNames();
|
||||
_deviceArrivalWatcher = new ManagementEventWatcher("SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 2");
|
||||
_deviceArrivalWatcher.EventArrived += OnDeviceArrived;
|
||||
_deviceRemovalWatcher = new ManagementEventWatcher("SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 3");
|
||||
_deviceRemovalWatcher.EventArrived += OnDeviceRemoved;
|
||||
_deviceArrivalWatcher.Start();
|
||||
_deviceRemovalWatcher.Start();
|
||||
|
||||
}
|
||||
|
||||
private void OnDeviceArrived(object sender, EventArrivedEventArgs evt) {
|
||||
App.MainDispatcher.Invoke(() => {
|
||||
// "synchronized"
|
||||
string[] currentPorts = SerialPort.GetPortNames();
|
||||
var newPorts = currentPorts.Except(_knownPorts).ToArray();
|
||||
foreach (var port in newPorts)
|
||||
SerialPortConnected?.Invoke(this, port);
|
||||
_knownPorts = currentPorts;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnDeviceRemoved(object sender, EventArrivedEventArgs evt) {
|
||||
App.MainDispatcher.Invoke(() => {
|
||||
// "synchronized"
|
||||
string[] currentPorts = SerialPort.GetPortNames();
|
||||
var removedPorts = _knownPorts.Except(currentPorts).ToArray();
|
||||
foreach (var port in removedPorts)
|
||||
SerialPortDisconnected?.Invoke(this, port);
|
||||
_knownPorts = currentPorts;
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
try {
|
||||
_deviceArrivalWatcher?.Stop();
|
||||
_deviceRemovalWatcher?.Stop();
|
||||
} finally {
|
||||
_deviceArrivalWatcher?.Dispose();
|
||||
_deviceRemovalWatcher?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.IO.Ports;
|
||||
using System.Net.Sockets;
|
||||
using Elwig.Dialogs;
|
||||
using System.Text;
|
||||
using System.Numerics;
|
||||
using Elwig.Models.Entities;
|
||||
using Elwig.Documents;
|
||||
using Elwig.Helpers.Billing;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.IO;
|
||||
using Elwig.Models;
|
||||
using Elwig.Models.Entities;
|
||||
using LinqKit;
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Reflection;
|
||||
using System.Collections;
|
||||
using Elwig.Documents;
|
||||
using MimeKit;
|
||||
using System.Windows.Input;
|
||||
using LinqKit;
|
||||
using System.Linq.Expressions;
|
||||
using Elwig.Models;
|
||||
using Microsoft.Win32;
|
||||
using MimeKit;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Markup;
|
||||
|
||||
namespace Elwig.Helpers {
|
||||
public static partial class Utils {
|
||||
|
||||
public static readonly Encoding UTF8 = new UTF8Encoding(false, true);
|
||||
public static readonly Encoding UTF8BOM = new UTF8Encoding(true, true);
|
||||
public static readonly JsonSerializerOptions JsonOpts = new() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
|
||||
|
||||
public static int CurrentYear => DateTime.Now.Year;
|
||||
public static int CurrentNextSeason => DateTime.Now.Year - (DateTime.Now.Month <= 3 ? 1 : 0);
|
||||
@@ -430,6 +432,8 @@ namespace Elwig.Helpers {
|
||||
var client = new HttpClient() {
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
};
|
||||
client.DefaultRequestHeaders.UserAgent.Clear();
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd($"Elwig/{App.Version} ({App.Client.NameToken}, {App.BranchName}, {Environment.MachineName}, {Environment.OSVersion})");
|
||||
client.DefaultRequestHeaders.Accept.Clear();
|
||||
if (accept != null)
|
||||
client.DefaultRequestHeaders.Accept.Add(new(accept));
|
||||
@@ -457,7 +461,7 @@ namespace Elwig.Helpers {
|
||||
url = "https://sync.elwig.at/" + url[25..];
|
||||
if (!url.EndsWith('/')) url += "/";
|
||||
using var client = GetHttpClient(username, password, accept: "application/json");
|
||||
var content = new StreamContent(new FileStream(zip, FileMode.Open, FileAccess.Read));
|
||||
using var content = new StreamContent(new FileStream(zip, FileMode.Open, FileAccess.Read));
|
||||
content.Headers.ContentType = new("application/zip");
|
||||
using var res = await client.PutAsync(url + Path.GetFileName(zip), content);
|
||||
res.EnsureSuccessStatusCode();
|
||||
@@ -498,10 +502,8 @@ namespace Elwig.Helpers {
|
||||
public static async Task<bool> SendEmail(Member member, string subject, string text, IEnumerable<Document> docs) {
|
||||
if (App.Config.Smtp == null)
|
||||
return false;
|
||||
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
|
||||
var success = await Task.Run(async () => {
|
||||
return await Task.Run(async () => {
|
||||
await AddSentMailBody(subject, text, 1);
|
||||
SmtpClient? client = null;
|
||||
try {
|
||||
client = await GetSmtpClient();
|
||||
@@ -519,6 +521,11 @@ namespace Elwig.Helpers {
|
||||
}
|
||||
msg.Body = body;
|
||||
await client!.SendAsync(msg);
|
||||
await AddSentMails([(
|
||||
"email", member.MgNr, member.AdministrativeName,
|
||||
member.EmailAddresses.OrderBy(a => a.Nr).Select(a => a.Address).ToArray(),
|
||||
subject, docs.Select(d => d.Title).ToArray()
|
||||
)]);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return false;
|
||||
@@ -529,22 +536,33 @@ namespace Elwig.Helpers {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Mouse.OverrideCursor = null;
|
||||
return success;
|
||||
}
|
||||
|
||||
public static async Task ExportDocument(Document doc, ExportMode mode, string? filename = null, (Member, string, string)? emailData = null) {
|
||||
public static async Task ExportDocument(Document doc, ExportMode mode, string? filename = null, (Member Member, string Subject, string Text)? emailData = null) {
|
||||
if (mode == ExportMode.Print && !App.Config.Debug) {
|
||||
if (doc.IsPreview) {
|
||||
MessageBox.Show("Dieses Dokument ist als vorläufig markiert und kann daher nicht ausgedruckt werden!",
|
||||
"Vorläufiges Dokument", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
await doc.Generate();
|
||||
await doc.Print();
|
||||
} else if (mode == ExportMode.Email && emailData is (Member, string, string) e) {
|
||||
if (doc.IsPreview) {
|
||||
MessageBox.Show("Dieses Dokument ist als vorläufig markiert und kann daher nicht verschickt werden!",
|
||||
"Vorläufiges Dokument", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
await doc.Generate();
|
||||
var success = await SendEmail(e.Item1, e.Item2, e.Item3, [doc]);
|
||||
var success = await SendEmail(e.Member, e.Subject, e.Text, [doc]);
|
||||
if (success)
|
||||
MessageBox.Show("Die E-Mail wurde erfolgreich verschickt!", "E-Mail verschickt",
|
||||
MessageBox.Show("Die E-Mail wurde erfolgreich verschickt!\n\nEs kann einige Minuten dauern, bis die E-Mail im Posteingang des Empfängers aufscheint.", "E-Mail verschickt",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
} else if (mode == ExportMode.SavePdf) {
|
||||
if (doc.IsPreview) {
|
||||
MessageBox.Show("Dieses Dokument ist als vorläufig markiert und sollte daher nicht langfristig gespeichert werden!",
|
||||
"Vorläufiges Dokument", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
var d = new SaveFileDialog() {
|
||||
FileName = $"{NormalizeFileName(filename ?? doc.Title)}.pdf",
|
||||
DefaultExt = "pdf",
|
||||
@@ -567,9 +585,7 @@ namespace Elwig.Helpers {
|
||||
Log = "Application",
|
||||
Source = ".NET Runtime",
|
||||
};
|
||||
return log.Entries.Cast<EventLogEntry>()
|
||||
.Where(e => e.Message.StartsWith("Application: Elwig.exe"))
|
||||
.ToList();
|
||||
return [.. log.Entries.OfType<EventLogEntry>().Where(e => e.InstanceId == 1026).Where(e => e.Message.StartsWith("Application: Elwig.exe"))];
|
||||
}
|
||||
|
||||
public static int GetEntityIdetifierForPk(params object?[] primaryKey) {
|
||||
@@ -659,9 +675,9 @@ namespace Elwig.Helpers {
|
||||
}
|
||||
|
||||
public static async Task<string?> FindSentMailBody(DateTime target) {
|
||||
var dt = $"{target:yyyy-MM-dd_HH-mm-ss}_";
|
||||
var dt = $"{target:yyyy-MM-dd_HH-mm-ss}";
|
||||
var filename = Directory.GetFiles(App.MailsPath, "????-??-??_??-??-??_*.txt")
|
||||
.Where(n => Path.GetFileName(n).CompareTo(dt) <= 0)
|
||||
.Where(n => Path.GetFileName(n)[..19].CompareTo(dt) <= 0)
|
||||
.Order()
|
||||
.LastOrDefault();
|
||||
if (filename == null)
|
||||
|
||||
@@ -15,15 +15,17 @@ namespace Elwig.Helpers.Weighing {
|
||||
public bool IsReady { get; private set; }
|
||||
public bool HasFillingClearance { get; private set; }
|
||||
|
||||
public event IEventScale.EventHandler<WeighingEventArgs> WeighingEvent;
|
||||
public event IEventScale.EventHandler<WeighingEventArgs>? WeighingEvent;
|
||||
|
||||
private bool IsRunning = true;
|
||||
private readonly Thread BackgroundThread;
|
||||
private readonly string Connection;
|
||||
|
||||
public AveryEventScale(string id, string model, string cnx, string? empty = null, string? filling = null, int? limit = null, string? log = null) :
|
||||
base(cnx, empty, filling, limit, log) {
|
||||
public AveryEventScale(string id, string model, string cnx, string? log = null, bool required = true) :
|
||||
base(cnx, null, null, null, log, true, !required) {
|
||||
ScaleId = id;
|
||||
Model = model;
|
||||
Connection = cnx;
|
||||
IsReady = true;
|
||||
HasFillingClearance = false;
|
||||
Stream.WriteTimeout = -1;
|
||||
@@ -50,19 +52,49 @@ namespace Elwig.Helpers.Weighing {
|
||||
var data = await Receive();
|
||||
if (data != null)
|
||||
RaiseWeighingEvent(new WeighingEventArgs(data.Value));
|
||||
} catch (ThreadInterruptedException) {
|
||||
// ignore
|
||||
} catch (IOException) {
|
||||
await Task.Delay(500);
|
||||
await Reconnect();
|
||||
} catch (TimeoutException) {
|
||||
await Task.Delay(500);
|
||||
await Reconnect();
|
||||
} catch (Exception ex) {
|
||||
MessageBox.Show($"Beim Wiegen ist ein Fehler Aufgetreten:\n\n{ex.Message}", "Waagenfehler",
|
||||
MessageBox.Show($"Beim Wiegen ist ein Fehler Aufgetreten:\n\n{ex.Message} ({ex.GetType().Name})", "Waagenfehler",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task Reconnect() {
|
||||
try { Reader.Close(); } catch { }
|
||||
try { Stream.Close(); } catch { }
|
||||
try { Serial?.Close(); } catch { }
|
||||
while (IsRunning) {
|
||||
try {
|
||||
if (Connection.StartsWith("serial:")) {
|
||||
Serial = Utils.OpenSerialConnection(Connection);
|
||||
Stream = Serial.BaseStream;
|
||||
} else if (Connection.StartsWith("tcp:")) {
|
||||
Tcp = Utils.OpenTcpConnection(Connection);
|
||||
Stream = Tcp.GetStream();
|
||||
}
|
||||
Reader = new(Stream, Encoding.ASCII, false, 512);
|
||||
break;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task<WeighingResult?> Receive() {
|
||||
var line = "";
|
||||
while (line.Length < 33) {
|
||||
var ch = Reader.Read();
|
||||
if (ch == -1) {
|
||||
return null;
|
||||
throw new IOException("Connection closed");
|
||||
} else if (line.Length > 0 || ch == ' ') {
|
||||
line += char.ToString((char)ch);
|
||||
}
|
||||
@@ -71,7 +103,7 @@ namespace Elwig.Helpers.Weighing {
|
||||
if (line == null || line == "") {
|
||||
return null;
|
||||
} else if (line.Length != 33 || line[0] != ' ' || line[9] != ' ' || line[15] != ' ' || line[20] != ' ' || line[32] != ' ') {
|
||||
throw new IOException($"Invalid event from scale: '{line}'");
|
||||
throw new FormatException($"Invalid event from scale: '{line}'");
|
||||
}
|
||||
|
||||
var date = line[ 1.. 9];
|
||||
@@ -81,7 +113,7 @@ namespace Elwig.Helpers.Weighing {
|
||||
var unit = line[30..32];
|
||||
|
||||
if (unit != "kg") {
|
||||
throw new IOException($"Unsupported unit in weighing event: '{unit}'");
|
||||
throw new WeighingException($"Unsupported unit in weighing event: '{unit}'");
|
||||
}
|
||||
|
||||
identNr = identNr.Length > 0 && identNr != "0" ? identNr : null;
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Elwig.Helpers.Weighing {
|
||||
var line = await Reader.ReadUntilAsync('\x03');
|
||||
if (LogPath != null) await File.AppendAllTextAsync(LogPath, $"{line}\r\n");
|
||||
if (line == null || line.Length < 4 || !line.StartsWith('\x02')) {
|
||||
throw new IOException("Invalid response from scale");
|
||||
throw new FormatException("Invalid response from scale");
|
||||
}
|
||||
|
||||
var status = line[1..3];
|
||||
@@ -45,9 +45,9 @@ namespace Elwig.Helpers.Weighing {
|
||||
switch (status[1]) {
|
||||
case 'M': msg = "Waage in Bewegung"; break;
|
||||
}
|
||||
throw new IOException($"Waagenfehler {status}: {msg}");
|
||||
throw new WeighingException($"Waagenfehler {status}: {msg}");
|
||||
} else if (status[0] != ' ') {
|
||||
throw new IOException($"Invalid response from scale (error code {status})");
|
||||
throw new WeighingException($"Invalid response from scale (error code {status})");
|
||||
}
|
||||
|
||||
return line[1..^1];
|
||||
@@ -57,7 +57,7 @@ namespace Elwig.Helpers.Weighing {
|
||||
await SendCommand(incIdentNr ? '\x05' : '?');
|
||||
string record = await ReceiveResponse();
|
||||
if (record.Length != 45)
|
||||
throw new IOException("Invalid response from scale: Received record has invalid size");
|
||||
throw new FormatException("Invalid response from scale: Received record has invalid size");
|
||||
var line = record[2..];
|
||||
|
||||
var brutto = line[ 0.. 7].Trim();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.IO.Ports;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
|
||||
namespace Elwig.Helpers.Weighing {
|
||||
public abstract class Scale : IDisposable {
|
||||
@@ -27,7 +28,7 @@ namespace Elwig.Helpers.Weighing {
|
||||
if (config.Type == "SysTec-IT") {
|
||||
return new SysTecITScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log);
|
||||
} else if (config.Type == "Avery-Async") {
|
||||
return new AveryEventScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log);
|
||||
return new AveryEventScale(config.Id, config.Model!, config.Connection!, config.Log, config.Required);
|
||||
} else if (config.Type == "Gassner") {
|
||||
return new GassnerScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log);
|
||||
} else {
|
||||
@@ -35,10 +36,17 @@ namespace Elwig.Helpers.Weighing {
|
||||
}
|
||||
}
|
||||
|
||||
protected Scale(string cnx, string? empty, string? filling, int? limit, string? log) {
|
||||
protected Scale(string cnx, string? empty, string? filling, int? limit, string? log, bool softFail = false, bool failSilent = false) {
|
||||
if (cnx.StartsWith("serial:")) {
|
||||
Serial = Utils.OpenSerialConnection(cnx);
|
||||
Stream = Serial.BaseStream;
|
||||
try {
|
||||
Serial = Utils.OpenSerialConnection(cnx);
|
||||
} catch (Exception e) {
|
||||
if (!softFail) throw;
|
||||
if (!failSilent)
|
||||
MessageBox.Show($"Verbindung zu Waage konnte nicht hergestellt werden:\n\n{e.Message}", "Waagenfehler",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
Stream = Serial?.BaseStream ?? Stream.Null;
|
||||
} else if (cnx.StartsWith("tcp:")) {
|
||||
Tcp = Utils.OpenTcpConnection(cnx);
|
||||
Stream = Tcp.GetStream();
|
||||
|
||||
@@ -34,14 +34,14 @@ namespace Elwig.Helpers.Weighing {
|
||||
var line = await Reader.ReadUntilAsync("\r\n");
|
||||
if (LogPath != null) await File.AppendAllTextAsync(LogPath, line);
|
||||
if (line == null || line.Length < 4 || !line.StartsWith('<') || !line.EndsWith(">\r\n")) {
|
||||
throw new IOException("Invalid response from scale");
|
||||
throw new FormatException("Invalid response from scale");
|
||||
}
|
||||
|
||||
var error = line[1..3];
|
||||
string msg = $"Unbekannter Fehler (Fehler code {error})";
|
||||
if (error[0] == '0') {
|
||||
if (error[1] != '0') {
|
||||
throw new IOException($"Invalid response from scale (error code {error})");
|
||||
throw new WeighingException($"Invalid response from scale (error code {error})");
|
||||
}
|
||||
} else if (error[0] == '1') {
|
||||
switch (error[1]) {
|
||||
@@ -52,21 +52,21 @@ namespace Elwig.Helpers.Weighing {
|
||||
case '6': msg = "Drucker nicht bereit"; break;
|
||||
case '7': msg = "Druckmuster enthält ungültiges Kommando"; break;
|
||||
}
|
||||
throw new IOException($"Waagenfehler {error}: {msg}");
|
||||
throw new WeighingException($"Waagenfehler {error}: {msg}");
|
||||
} else if (error[0] == '2') {
|
||||
switch (error[1]) {
|
||||
case '0': msg = "Brutto negativ"; break;
|
||||
}
|
||||
throw new IOException($"Fehler {error}: {msg}");
|
||||
throw new WeighingException($"Fehler {error}: {msg}");
|
||||
} else if (error[0] == '3') {
|
||||
switch (error[1]) {
|
||||
case '1': msg = "Übertragunsfehler"; break;
|
||||
case '2': msg = "Ungültiger Befehl"; break;
|
||||
case '3': msg = "Ungültiger Parameter"; break;
|
||||
}
|
||||
throw new IOException($"Kommunikationsfehler {error}: {msg}");
|
||||
throw new WeighingException($"Kommunikationsfehler {error}: {msg}");
|
||||
} else {
|
||||
throw new IOException($"Invalid response from scale (error code {error})");
|
||||
throw new WeighingException($"Invalid response from scale (error code {error})");
|
||||
}
|
||||
|
||||
return line[1..^3];
|
||||
@@ -76,7 +76,7 @@ namespace Elwig.Helpers.Weighing {
|
||||
await SendCommand(incIdentNr ? $"RN{InternalScaleNr}" : $"RM{InternalScaleNr}");
|
||||
string record = await ReceiveResponse();
|
||||
if (record.Length != 62)
|
||||
throw new IOException("Invalid response from scale: Received record has invalid size");
|
||||
throw new FormatException("Invalid response from scale: Received record has invalid size");
|
||||
var line = record[2..];
|
||||
|
||||
var status = line[ 0.. 2];
|
||||
@@ -94,9 +94,9 @@ namespace Elwig.Helpers.Weighing {
|
||||
var crc16 = line[52..60].Trim();
|
||||
|
||||
if (Utils.CalcCrc16Modbus(record[..54]) != ushort.Parse(crc16)) {
|
||||
throw new IOException($"Invalid response from scale: Invalid CRC16 checksum ({crc16} != {Utils.CalcCrc16Modbus(record[..54])})");
|
||||
throw new WeighingException($"Invalid response from scale: Invalid CRC16 checksum ({crc16} != {Utils.CalcCrc16Modbus(record[..54])})");
|
||||
} else if (unit != "kg") {
|
||||
throw new IOException($"Unsupported unit in weighing response: '{unit}'");
|
||||
throw new WeighingException($"Unsupported unit in weighing response: '{unit}'");
|
||||
}
|
||||
|
||||
identNr = identNr.Length > 0 && identNr != "0" ? identNr : null;
|
||||
|
||||
6
Elwig/Helpers/Weighing/WeighingException.cs
Normal file
6
Elwig/Helpers/Weighing/WeighingException.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Elwig.Helpers.Weighing {
|
||||
public class WeighingException(string? message = null) : Exception(message) {
|
||||
}
|
||||
}
|
||||
59
Elwig/Models/Dtos/MemberDeliveryData.cs
Normal file
59
Elwig/Models/Dtos/MemberDeliveryData.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Elwig.Models.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Elwig.Models.Dtos {
|
||||
public class MemberDeliveryData : DataTable<MemberDeliveryRow> {
|
||||
|
||||
private static readonly (string, string, string?, int?)[] FieldNames = [
|
||||
("MgNr", "MgNr.", null, 12),
|
||||
("Name1", "Name", null, 40),
|
||||
("Name2", "Vorname", null, 40),
|
||||
("Address", "Adresse", null, 60),
|
||||
("Plz", "PLZ", null, 10),
|
||||
("Locality", "Ort", null, 60),
|
||||
("Weight", "Geliefert", "kg", 22),
|
||||
];
|
||||
|
||||
public MemberDeliveryData(IEnumerable<MemberDeliveryRow> rows, List<string> filterNames) :
|
||||
base("Liefermengen Gesamt", "Liefermengen pro Mitglied", string.Join(" / ", filterNames), rows, FieldNames) {
|
||||
}
|
||||
|
||||
public static async Task<MemberDeliveryData> FromQuery(IQueryable<DeliveryPart> query, List<string> filterNames) {
|
||||
return new((await query
|
||||
.GroupBy(p => new {
|
||||
p.Delivery.MgNr,
|
||||
p.Delivery.Member.Name,
|
||||
p.Delivery.Member.GivenName,
|
||||
p.Delivery.Member.Address,
|
||||
p.Delivery.Member.PostalDest.AtPlz!.Plz,
|
||||
Ort = p.Delivery.Member.PostalDest.AtPlz!.Ort.Name,
|
||||
})
|
||||
.Select(g => new {
|
||||
g.Key,
|
||||
Weight = g.Sum(p => p.Weight),
|
||||
}).ToListAsync())
|
||||
.Select(g => new MemberDeliveryRow {
|
||||
MgNr = g.Key.MgNr,
|
||||
Name1 = g.Key.Name,
|
||||
Name2 = g.Key.GivenName,
|
||||
Address = g.Key.Address,
|
||||
Plz = g.Key.Plz,
|
||||
Locality = g.Key.Ort,
|
||||
Weight = g.Weight,
|
||||
}), filterNames);
|
||||
}
|
||||
}
|
||||
|
||||
public class MemberDeliveryRow {
|
||||
public required int MgNr;
|
||||
public required string Name1;
|
||||
public required string? Name2;
|
||||
public required string Address;
|
||||
public required int Plz;
|
||||
public required string Locality;
|
||||
public required int Weight;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
using Elwig.Models.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Elwig.Models.Dtos {
|
||||
public class MemberDeliveryPerVarietyData : DataTable<MemberDeliveryPerVariantRow> {
|
||||
public class MemberDeliveryPerVarietyData : DataTable<MemberDeliveryPerVarietyRow> {
|
||||
|
||||
private static readonly (string, string, string?, int)[] FieldNames = [
|
||||
private static readonly (string, string, string?, int?)[] FieldNames = [
|
||||
("MgNr", "MgNr.", null, 12),
|
||||
("Name1", "Name", null, 40),
|
||||
("Name2", "Vorname", null, 40),
|
||||
@@ -16,104 +16,63 @@ namespace Elwig.Models.Dtos {
|
||||
("Locality", "Ort", null, 60),
|
||||
("SortIds", "Sorte", null, 12),
|
||||
("AttrIds", "Attribut", null, 16),
|
||||
("CultIds", "Bewirt.", null, 16),
|
||||
("Weights", "Geliefert", "kg", 22),
|
||||
("Areas", "Fläche", "m²", 22),
|
||||
("Yields", "Ertrag", "kg/ha", 22),
|
||||
];
|
||||
|
||||
public MemberDeliveryPerVarietyData(IEnumerable<MemberDeliveryPerVariantRow> rows, int year) :
|
||||
base($"Liefermengen", $"Liefermengen pro Mitglied, Sorte und Attribut {year}", rows, FieldNames) {
|
||||
public MemberDeliveryPerVarietyData(IEnumerable<MemberDeliveryPerVarietyRow> rows, List<string> filterNames) :
|
||||
base("Liefermengen pro Sorte", "Liefermengen pro Mitglied, Sorte, Attribut und Bewirtschaftungsart", string.Join(" / ", filterNames), rows, FieldNames) {
|
||||
}
|
||||
|
||||
public static async Task<MemberDeliveryPerVarietyData> ForSeason(DbSet<MemberDeliveryPerVariantRowSingle> table, int year) {
|
||||
return new MemberDeliveryPerVarietyData(
|
||||
(await FromDbSet(table, year)).GroupBy(
|
||||
r => r.MgNr,
|
||||
(k, g) => new MemberDeliveryPerVariantRow(g)
|
||||
), year);
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<MemberDeliveryPerVariantRowSingle>> FromDbSet(DbSet<MemberDeliveryPerVariantRowSingle> table, int year) {
|
||||
return await table.FromSql($"""
|
||||
SELECT m.mgnr, m.name AS name_1,
|
||||
COALESCE(m.prefix || ' ', '') || m.given_name ||
|
||||
COALESCE(' ' || m.middle_names, '') || COALESCE(' ' || m.suffix, '') AS name_2,
|
||||
p.plz, o.name AS ort, m.address,
|
||||
v.bucket, v.weight, v.area
|
||||
FROM (
|
||||
SELECT c.year AS year,
|
||||
c.mgnr AS mgnr,
|
||||
c.bucket AS bucket,
|
||||
COALESCE(d.weight, 0) AS weight,
|
||||
COALESCE(c.area, 0) AS area
|
||||
FROM v_area_commitment_bucket_strict c
|
||||
LEFT JOIN v_delivery_bucket_strict d ON (d.year, d.mgnr, d.bucket) = (c.year, c.mgnr, c.bucket)
|
||||
WHERE c.year = {year}
|
||||
UNION
|
||||
SELECT d.year,
|
||||
d.mgnr,
|
||||
d.bucket,
|
||||
COALESCE(d.weight, 0),
|
||||
COALESCE(c.area, 0)
|
||||
FROM v_delivery_bucket_strict d
|
||||
LEFT JOIN v_area_commitment_bucket_strict c ON (c.year, c.mgnr, c.bucket) = (d.year, d.mgnr, d.bucket)
|
||||
WHERE d.year = {year}
|
||||
) v
|
||||
LEFT JOIN member m ON m.mgnr = v.mgnr
|
||||
LEFT JOIN AT_plz_dest p ON p.id = m.postal_dest
|
||||
LEFT JOIN AT_ort o ON o.okz = p.okz
|
||||
ORDER BY m.mgnr, v.bucket
|
||||
""").ToListAsync();
|
||||
public static async Task<MemberDeliveryPerVarietyData> FromQuery(IQueryable<DeliveryPart> query, List<string> filterNames) {
|
||||
return new((await query
|
||||
.GroupBy(p => new {
|
||||
p.Delivery.MgNr,
|
||||
p.Delivery.Member.Name,
|
||||
p.Delivery.Member.GivenName,
|
||||
p.Delivery.Member.Address,
|
||||
p.Delivery.Member.PostalDest.AtPlz!.Plz,
|
||||
Ort = p.Delivery.Member.PostalDest.AtPlz!.Ort.Name,
|
||||
p.SortId,
|
||||
p.AttrId,
|
||||
p.CultId,
|
||||
})
|
||||
.Select(g => new {
|
||||
g.Key,
|
||||
Weight = g.Sum(p => p.Weight),
|
||||
})
|
||||
.ToListAsync()).GroupBy(g => new {
|
||||
g.Key.MgNr,
|
||||
g.Key.Name,
|
||||
g.Key.GivenName,
|
||||
g.Key.Address,
|
||||
g.Key.Plz,
|
||||
g.Key.Ort,
|
||||
}).Select(g => new MemberDeliveryPerVarietyRow {
|
||||
MgNr = g.Key.MgNr,
|
||||
Name1 = g.Key.Name,
|
||||
Name2 = g.Key.GivenName,
|
||||
Address = g.Key.Address,
|
||||
Plz = g.Key.Plz,
|
||||
Locality = g.Key.Ort,
|
||||
SortIds = [.. g.Select(d => d.Key.SortId)],
|
||||
AttrIds = [.. g.Select(d => d.Key.AttrId)],
|
||||
CultIds = [.. g.Select(d => d.Key.CultId)],
|
||||
Weights = [.. g.Select(d => d.Weight)],
|
||||
}), filterNames);
|
||||
}
|
||||
}
|
||||
|
||||
public class MemberDeliveryPerVariantRow {
|
||||
public int MgNr;
|
||||
public string Name1;
|
||||
public string? Name2;
|
||||
public string Address;
|
||||
public int Plz;
|
||||
public string Locality;
|
||||
public string[] SortIds;
|
||||
public string[] AttrIds;
|
||||
public int[] Areas;
|
||||
public int[] Weights;
|
||||
public int?[] Yields => Weights.Zip(Areas).Select(i => i.Second > 0 ? (int?)i.First * 10_000 / i.Second : null).ToArray();
|
||||
|
||||
public MemberDeliveryPerVariantRow(IEnumerable<MemberDeliveryPerVariantRowSingle> rows) {
|
||||
var f = rows.First();
|
||||
MgNr = f.MgNr;
|
||||
Name1 = f.Name1;
|
||||
Name2 = f.Name2;
|
||||
Address = f.Address;
|
||||
Plz = f.Plz;
|
||||
Locality = f.Locality.Split(",")[0];
|
||||
SortIds = rows.Select(r => r.VtrgId[..2]).ToArray();
|
||||
AttrIds = rows.Select(r => r.VtrgId[2..]).ToArray();
|
||||
Areas = rows.Select(r => r.Area).ToArray();
|
||||
Weights = rows.Select(r => r.Weight).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
[Keyless]
|
||||
public class MemberDeliveryPerVariantRowSingle {
|
||||
[Column("mgnr")]
|
||||
public int MgNr { get; set; }
|
||||
[Column("name_1")]
|
||||
public required string Name1 { get; set; }
|
||||
[Column("name_2")]
|
||||
public string? Name2 { get; set; }
|
||||
[Column("address")]
|
||||
public required string Address { get; set; }
|
||||
[Column("plz")]
|
||||
public int Plz { get; set; }
|
||||
[Column("ort")]
|
||||
public required string Locality { get; set; }
|
||||
[Column("bucket")]
|
||||
public required string VtrgId { get; set; }
|
||||
[Column("area")]
|
||||
public int Area { get; set; }
|
||||
[Column("weight")]
|
||||
public int Weight { get; set; }
|
||||
public class MemberDeliveryPerVarietyRow {
|
||||
public required int MgNr;
|
||||
public required string Name1;
|
||||
public required string? Name2;
|
||||
public required string Address;
|
||||
public required int Plz;
|
||||
public required string Locality;
|
||||
public required string[] SortIds;
|
||||
public required string?[] AttrIds;
|
||||
public required string?[] CultIds;
|
||||
public required int[] Weights;
|
||||
}
|
||||
}
|
||||
|
||||
119
Elwig/Models/Dtos/MemberDeliveryYieldsPerVarietyData.cs
Normal file
119
Elwig/Models/Dtos/MemberDeliveryYieldsPerVarietyData.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Elwig.Models.Dtos {
|
||||
public class MemberDeliveryYieldsPerVarietyData : DataTable<MemberDeliveryYieldsPerVarietyRow> {
|
||||
|
||||
private static readonly (string, string, string?, int)[] FieldNames = [
|
||||
("MgNr", "MgNr.", null, 12),
|
||||
("Name1", "Name", null, 40),
|
||||
("Name2", "Vorname", null, 40),
|
||||
("Address", "Adresse", null, 60),
|
||||
("Plz", "PLZ", null, 10),
|
||||
("Locality", "Ort", null, 60),
|
||||
("SortIds", "Sorte", null, 12),
|
||||
("AttrIds", "Attribut", null, 16),
|
||||
("Weights", "Geliefert", "kg", 22),
|
||||
("Areas", "Fläche", "m²", 22),
|
||||
("Yields", "Ertrag", "kg/ha", 22),
|
||||
];
|
||||
|
||||
public MemberDeliveryYieldsPerVarietyData(IEnumerable<MemberDeliveryYieldsPerVarietyRow> rows, int year) :
|
||||
base($"Liefermengen", $"Liefermengen pro Mitglied, Sorte und Attribut {year}", rows, FieldNames) {
|
||||
}
|
||||
|
||||
public static async Task<MemberDeliveryYieldsPerVarietyData> ForSeason(DbSet<MemberDeliveryPerVarietyRowSingle> table, int year) {
|
||||
return new MemberDeliveryYieldsPerVarietyData(
|
||||
(await FromDbSet(table, year)).GroupBy(
|
||||
r => r.MgNr,
|
||||
(k, g) => new MemberDeliveryYieldsPerVarietyRow(g)
|
||||
), year);
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<MemberDeliveryPerVarietyRowSingle>> FromDbSet(DbSet<MemberDeliveryPerVarietyRowSingle> table, int year) {
|
||||
return await table.FromSql($"""
|
||||
SELECT m.mgnr, m.name AS name_1,
|
||||
COALESCE(m.prefix || ' ', '') || m.given_name ||
|
||||
COALESCE(' ' || m.middle_names, '') || COALESCE(' ' || m.suffix, '') AS name_2,
|
||||
p.plz, o.name AS ort, m.address,
|
||||
v.bucket, v.weight, v.area
|
||||
FROM (
|
||||
SELECT c.year AS year,
|
||||
c.mgnr AS mgnr,
|
||||
c.bucket AS bucket,
|
||||
COALESCE(d.weight, 0) AS weight,
|
||||
COALESCE(c.area, 0) AS area
|
||||
FROM v_area_commitment_bucket_strict c
|
||||
LEFT JOIN v_delivery_bucket_strict d ON (d.year, d.mgnr, d.bucket) = (c.year, c.mgnr, c.bucket)
|
||||
WHERE c.year = {year}
|
||||
UNION
|
||||
SELECT d.year,
|
||||
d.mgnr,
|
||||
d.bucket,
|
||||
COALESCE(d.weight, 0),
|
||||
COALESCE(c.area, 0)
|
||||
FROM v_delivery_bucket_strict d
|
||||
LEFT JOIN v_area_commitment_bucket_strict c ON (c.year, c.mgnr, c.bucket) = (d.year, d.mgnr, d.bucket)
|
||||
WHERE d.year = {year}
|
||||
) v
|
||||
LEFT JOIN member m ON m.mgnr = v.mgnr
|
||||
LEFT JOIN AT_plz_dest p ON p.id = m.postal_dest
|
||||
LEFT JOIN AT_ort o ON o.okz = p.okz
|
||||
ORDER BY m.mgnr, v.bucket
|
||||
""").ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class MemberDeliveryYieldsPerVarietyRow {
|
||||
public int MgNr;
|
||||
public string Name1;
|
||||
public string? Name2;
|
||||
public string Address;
|
||||
public int Plz;
|
||||
public string Locality;
|
||||
public string[] SortIds;
|
||||
public string[] AttrIds;
|
||||
public int[] Areas;
|
||||
public int[] Weights;
|
||||
public int?[] Yields => Weights.Zip(Areas).Select(i => i.Second > 0 ? (int?)i.First * 10_000 / i.Second : null).ToArray();
|
||||
|
||||
public MemberDeliveryYieldsPerVarietyRow(IEnumerable<MemberDeliveryPerVarietyRowSingle> rows) {
|
||||
var f = rows.First();
|
||||
MgNr = f.MgNr;
|
||||
Name1 = f.Name1;
|
||||
Name2 = f.Name2;
|
||||
Address = f.Address;
|
||||
Plz = f.Plz;
|
||||
Locality = f.Locality.Split(",")[0];
|
||||
SortIds = rows.Select(r => r.VtrgId[..2]).ToArray();
|
||||
AttrIds = rows.Select(r => r.VtrgId[2..]).ToArray();
|
||||
Areas = rows.Select(r => r.Area).ToArray();
|
||||
Weights = rows.Select(r => r.Weight).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
[Keyless]
|
||||
public class MemberDeliveryPerVarietyRowSingle {
|
||||
[Column("mgnr")]
|
||||
public int MgNr { get; set; }
|
||||
[Column("name_1")]
|
||||
public required string Name1 { get; set; }
|
||||
[Column("name_2")]
|
||||
public string? Name2 { get; set; }
|
||||
[Column("address")]
|
||||
public required string Address { get; set; }
|
||||
[Column("plz")]
|
||||
public int Plz { get; set; }
|
||||
[Column("ort")]
|
||||
public required string Locality { get; set; }
|
||||
[Column("bucket")]
|
||||
public required string VtrgId { get; set; }
|
||||
[Column("area")]
|
||||
public int Area { get; set; }
|
||||
[Column("weight")]
|
||||
public int Weight { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Elwig.Helpers;
|
||||
using Elwig.Helpers.Billing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -62,6 +63,11 @@ namespace Elwig.Models.Entities {
|
||||
[Column("comment")]
|
||||
public string? Comment { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public string[] Comments => [.. Parts.Select(p => p.Comment).Prepend(Comment).Where(c => c != null).Cast<string>()];
|
||||
[NotMapped]
|
||||
public string CommentsString => string.Join(" / ", Comments);
|
||||
|
||||
[Column("ctime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
|
||||
public long CTime { get; set; }
|
||||
[NotMapped]
|
||||
@@ -108,16 +114,16 @@ namespace Elwig.Models.Entities {
|
||||
public int Weight => Parts.Select(p => p.Weight).Sum();
|
||||
public int FilteredWeight => FilteredParts.Select(p => p.Weight).Sum();
|
||||
|
||||
public IEnumerable<string> SortIds => Parts
|
||||
.GroupBy(p => p.SortId)
|
||||
public IEnumerable<RawVaribute> Vaributes => Parts
|
||||
.GroupBy(p => (p.SortId, p.AttrId, p.CultId))
|
||||
.OrderByDescending(g => g.Select(p => p.Weight).Sum())
|
||||
.Select(g => g.Key);
|
||||
public IEnumerable<string> FilteredSortIds => FilteredParts
|
||||
.GroupBy(p => p.SortId)
|
||||
.Select(g => new RawVaribute(g.Key.SortId, g.Key.AttrId, g.Key.CultId));
|
||||
public IEnumerable<RawVaribute> FilteredVaributes => FilteredParts
|
||||
.GroupBy(p => (p.SortId, p.AttrId, p.CultId))
|
||||
.OrderByDescending(g => g.Select(p => p.Weight).Sum())
|
||||
.Select(g => g.Key);
|
||||
public string SortIdString => string.Join(", ", SortIds);
|
||||
public string FilteredSortIdString => string.Join(", ", FilteredSortIds);
|
||||
.Select(g => new RawVaribute(g.Key.SortId, g.Key.AttrId, g.Key.CultId));
|
||||
public string VaributeString => string.Join(", ", Vaributes);
|
||||
public string FilteredVaributeString => string.Join(", ", FilteredVaributes);
|
||||
|
||||
public Brush? Color => Parts.Select(p => p.Variety.Color).Distinct().SingleOrDefault();
|
||||
public Brush? FilteredColor => FilteredParts.Select(p => p.Variety.Color).Distinct().SingleOrDefault();
|
||||
|
||||
@@ -9,6 +9,11 @@ using System.Text.Json.Nodes;
|
||||
namespace Elwig.Models.Entities {
|
||||
[Table("delivery_part"), PrimaryKey("Year", "DId", "DPNr")]
|
||||
public class DeliveryPart : IDelivery {
|
||||
|
||||
public const string Dumper = "dumper";
|
||||
public const string Pumped = "pumped";
|
||||
public const string Box = "box";
|
||||
|
||||
[Column("year")]
|
||||
public int Year { get; set; }
|
||||
|
||||
@@ -86,12 +91,27 @@ namespace Elwig.Models.Entities {
|
||||
[Column("hand_picked")]
|
||||
public bool? IsHandPicked { get; set; }
|
||||
|
||||
[Column("lesewagen")]
|
||||
public bool? IsLesewagen { get; set; }
|
||||
|
||||
[Column("gebunden")]
|
||||
public bool? IsGebunden { get; set; }
|
||||
|
||||
[Column("unloading")]
|
||||
public string? Unloading { get; set; }
|
||||
[NotMapped]
|
||||
public bool IsDumper {
|
||||
get => Unloading == Dumper;
|
||||
set => Unloading = value ? Dumper : Unloading;
|
||||
}
|
||||
[NotMapped]
|
||||
public bool IsPumped {
|
||||
get => Unloading == Pumped;
|
||||
set => Unloading = value ? Pumped : Unloading;
|
||||
}
|
||||
[NotMapped]
|
||||
public bool IsBox {
|
||||
get => Unloading == Box;
|
||||
set => Unloading = value ? Box : Unloading;
|
||||
}
|
||||
|
||||
[Column("temperature")]
|
||||
public double? Temperature { get; set; }
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ namespace Elwig.Models.Entities {
|
||||
[Column("max_weight")]
|
||||
public int? MaxWeight { get; set; }
|
||||
[NotMapped]
|
||||
public int AnnouncedWeight => Announcements.Sum(a => a.Weight);
|
||||
public int AnnouncedWeight => AnnouncedWeightOverride ?? Announcements.Sum(a => a.Weight);
|
||||
[NotMapped]
|
||||
public int? AnnouncedWeightOverride { get; set; }
|
||||
[NotMapped]
|
||||
public double? Percent => (double)AnnouncedWeight / MaxWeight * 100;
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ namespace Elwig.Models.Entities {
|
||||
[Column("active")]
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
[Column("redacted")]
|
||||
public bool IsRedacted { get; set; }
|
||||
|
||||
[Column("ordering")]
|
||||
public int Ordering { get; set; }
|
||||
|
||||
@@ -46,6 +49,8 @@ namespace Elwig.Models.Entities {
|
||||
(Rel != null) ? $"{Utils.GetSign(Rel.Value)}{(Math.Abs(Rel.Value) < 0.1m ? "\u2007" : "")}{Math.Abs(Rel.Value):0.00##\u00a0%}" :
|
||||
"";
|
||||
|
||||
public string? PublicValueStr => IsRedacted ? null : ValueStr;
|
||||
|
||||
public override string ToString() {
|
||||
return Name;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ namespace Elwig.Models.Entities {
|
||||
[Table("wb_gl"), PrimaryKey("GlNr")]
|
||||
public class WbGl {
|
||||
[Column("glnr")]
|
||||
public int GlNr { get; private set; }
|
||||
public int GlNr { get; set; }
|
||||
|
||||
[Column("name")]
|
||||
public string Name { get; private set; } = null!;
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
[InverseProperty(nameof(WbKg.Gl))]
|
||||
public virtual ICollection<WbKg> Kgs { get; private set; } = null!;
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Elwig.Models.Entities {
|
||||
public virtual AT_Kg AtKg { get; private set; } = null!;
|
||||
|
||||
[ForeignKey("GlNr")]
|
||||
public virtual WbGl Gl { get; private set; } = null!;
|
||||
public virtual WbGl? Gl { get; private set; } = null!;
|
||||
|
||||
[InverseProperty(nameof(WbRd.Kg))]
|
||||
public virtual ICollection<WbRd> Rds { get; private set; } = null!;
|
||||
|
||||
@@ -11,16 +11,24 @@ namespace Elwig.Models.Entities {
|
||||
[Column("type")]
|
||||
public string Type { get; private set; } = null!;
|
||||
|
||||
[Column("max_qualid")]
|
||||
public string MaxQualId { get; private set; } = null!;
|
||||
|
||||
[ForeignKey("MaxQualId")]
|
||||
public virtual WineQualLevel MaxQualityLevel { get; private set; } = null!;
|
||||
|
||||
[Column("name")]
|
||||
public string Name { get; private set; } = null!;
|
||||
|
||||
[Column("comment")]
|
||||
public string? Comment { get; private set; }
|
||||
|
||||
public string SortIdFormat => IsQuw ? SortId : $"({SortId})";
|
||||
public string CommentFormat => (Comment != null) ? $" ({Comment})" : "";
|
||||
|
||||
public bool IsRed => Type == "R";
|
||||
public bool IsWhite => Type == "W";
|
||||
public bool IsQuw => MaxQualId == "QUW";
|
||||
public Brush? Color => IsWhite ? Brushes.DarkGreen : IsRed ? Brushes.DarkRed : null;
|
||||
|
||||
public WineVar() { }
|
||||
@@ -28,6 +36,7 @@ namespace Elwig.Models.Entities {
|
||||
public WineVar(string sortId, string name) {
|
||||
SortId = sortId;
|
||||
Name = name;
|
||||
MaxQualId = "QUW";
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
|
||||
@@ -9,7 +9,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<PublishDir>bin\Publish</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
|
||||
17
Elwig/Resources/Sql/32-33.sql
Normal file
17
Elwig/Resources/Sql/32-33.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- schema version 32 to 33
|
||||
|
||||
ALTER TABLE wine_variety ADD COLUMN max_qualid TEXT NOT NULL DEFAULT 'QUW';
|
||||
UPDATE wine_quality_level SET qualid = 'ALW' WHERE qualid = 'AUL';
|
||||
UPDATE wine_variety SET comment = 'Muscato' WHERE sortid = 'MO';
|
||||
|
||||
INSERT INTO wine_variety (sortid, type, max_qualid, name, comment) VALUES
|
||||
('DR', 'W', 'QUW', 'Donauriesling', NULL),
|
||||
('DV', 'W', 'QUW', 'Donauveltliner', NULL),
|
||||
('BN', 'W', 'RSW', 'Bronner', NULL),
|
||||
('CB', 'W', 'RSW', 'Cabernet Blanc', NULL),
|
||||
('CJ', 'R', 'RSW', 'Cabernet Jura', NULL),
|
||||
('JO', 'W', 'RSW', 'Johanniter', NULL),
|
||||
('OR', 'W', 'RSW', 'Orangetraube', NULL),
|
||||
('PI', 'R', 'RSW', 'Pinot Nova', NULL),
|
||||
('RE', 'R', 'RSW', 'Regent', NULL),
|
||||
('SI', 'W', 'RSW', 'Solaris', NULL);
|
||||
3
Elwig/Resources/Sql/33-34.sql
Normal file
3
Elwig/Resources/Sql/33-34.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- schema version 33 to 34
|
||||
|
||||
ALTER TABLE modifier ADD COLUMN redacted INTEGER NOT NULL CHECK (redacted IN (TRUE, FALSE)) DEFAULT FALSE;
|
||||
6
Elwig/Resources/Sql/34-35.sql
Normal file
6
Elwig/Resources/Sql/34-35.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- schema version 34 to 35
|
||||
|
||||
ALTER TABLE delivery_part ADD COLUMN unloading TEXT DEFAULT NULL;
|
||||
UPDATE delivery_part SET unloading = 'pumped' WHERE lesewagen;
|
||||
UPDATE delivery_part SET unloading = 'box' WHERE (SELECT zwstid IN ('H','S') FROM delivery d WHERE (d.year, d.did) = (delivery_part.year, delivery_part.did));
|
||||
ALTER TABLE delivery_part DROP COLUMN lesewagen;
|
||||
19
Elwig/Resources/Sql/35-36.sql
Normal file
19
Elwig/Resources/Sql/35-36.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- schema version 35 to 36
|
||||
|
||||
CREATE VIEW v_member AS
|
||||
SELECT m.mgnr, m.name,
|
||||
(COALESCE(m.prefix || ' ', '') || m.given_name || COALESCE(' ' || m.middle_names, '') || COALESCE(' ' || m.suffix, '')) AS other_names,
|
||||
m.address, p.plz, o.name AS locality,
|
||||
a.name AS billing_name, a.address AS billing_address, p2.plz AS billing_plz, o2.name AS billing_locality,
|
||||
k.name AS default_kg,
|
||||
GROUP_CONCAT(e.address, ', ') AS email_addresses
|
||||
FROM member m
|
||||
LEFT JOIN AT_plz_dest p ON p.id = m.postal_dest
|
||||
LEFT JOIN AT_ort o ON o.okz = p.okz
|
||||
LEFT JOIN member_billing_address a ON a.mgnr = m.mgnr
|
||||
LEFT JOIN AT_plz_dest p2 ON p2.id = a.postal_dest
|
||||
LEFT JOIN AT_ort o2 ON o2.okz = p2.okz
|
||||
LEFT JOIN AT_kg k ON k.kgnr = m.default_kgnr
|
||||
LEFT JOIN member_email_address e ON e.mgnr = m.mgnr
|
||||
GROUP BY m.mgnr
|
||||
ORDER BY m.mgnr;
|
||||
@@ -15,10 +15,8 @@ using LinqKit;
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.IO;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using System.Windows.Controls;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Elwig.Services {
|
||||
public static class DeliveryService {
|
||||
@@ -80,9 +78,9 @@ namespace Elwig.Services {
|
||||
vm.PartComment = p.Comment ?? "";
|
||||
vm.TemperatureString = (p.Temperature != null) ? $"{p.Temperature:N1}" : "";
|
||||
vm.AcidString = (p.Acid != null) ? $"{p.Acid:N1}" : "";
|
||||
vm.IsLesewagen = p.IsLesewagen ?? false;
|
||||
vm.IsHandPicked = p.IsHandPicked;
|
||||
vm.IsGebunden = p.IsGebunden;
|
||||
vm.Unloading = p.Unloading;
|
||||
|
||||
vm.ScaleId = p.ScaleId;
|
||||
vm.WeighingData = p.WeighingData;
|
||||
@@ -188,14 +186,54 @@ namespace Elwig.Services {
|
||||
prd = prd.And(p => p.IsNetWeight == true);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("gerebelt gewogen");
|
||||
} else if (e.Length >= 5 && e.Length <= 11 && "planenwagen".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
prd = prd.And(p => p.Unloading == DeliveryPart.Dumper);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("Planenw./Kipper");
|
||||
} else if (e.Length >= 6 && e.Length <= 12 && "!planenwagen".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
prd = prd.And(p => p.Unloading != DeliveryPart.Dumper);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("kein Planenw./Kipper");
|
||||
} else if (e.Length >= 4 && e.Length <= 6 && "kipper".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
prd = prd.And(p => p.Unloading == DeliveryPart.Dumper);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("Planenw./Kipper");
|
||||
} else if (e.Length >= 5 && e.Length <= 7 && "!kipper".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
prd = prd.And(p => p.Unloading != DeliveryPart.Dumper);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("kein Planenw./Kipper");
|
||||
} else if (e.Length >= 5 && e.Length <= 9 && "lesewagen".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
prd = prd.And(p => p.IsLesewagen == true);
|
||||
prd = prd.And(p => p.Unloading == DeliveryPart.Pumped);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("Lesewagen");
|
||||
} else if (e.Length >= 6 && e.Length <= 10 && "!lesewagen".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
prd = prd.And(p => p.IsLesewagen == false);
|
||||
prd = prd.And(p => p.Unloading != DeliveryPart.Pumped);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("kein Lesewagen");
|
||||
} else if (e.Length >= 5 && e.Length <= 6 && "kisten".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
prd = prd.And(p => p.Unloading == DeliveryPart.Box);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("Kisten");
|
||||
} else if (e.Length >= 6 && e.Length <= 7 && "!kisten".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
prd = prd.And(p => p.Unloading != DeliveryPart.Box);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("keine Kisten");
|
||||
} else if ("upload".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
prd = prd.And(p => (p.Delivery.XTime == null || p.Delivery.MTime > p.Delivery.XTime) && (p.Delivery.ITime == null || p.Delivery.MTime > p.Delivery.ITime));
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("geändert seit letztem Export");
|
||||
} else if ("!upload".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
prd = prd.And(p => !((p.Delivery.XTime == null || p.Delivery.MTime > p.Delivery.XTime) && (p.Delivery.ITime == null || p.Delivery.MTime > p.Delivery.ITime)));
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("unverändert seit letztem Export");
|
||||
} else if (">import".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
prd = prd.And(p => p.ITime != null && p.Delivery.MTime > p.Delivery.ITime);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("geändert seit letztem Import");
|
||||
} else if ("<import".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
prd = prd.And(p => p.Delivery.MTime <= p.Delivery.ITime);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("unverändert seit letztem Import");
|
||||
} else if (e.Length == 2 && var.ContainsKey(e.ToUpper())) {
|
||||
filterVar.Add(e.ToUpper());
|
||||
filter.RemoveAt(i--);
|
||||
@@ -483,8 +521,8 @@ namespace Elwig.Services {
|
||||
|
||||
IsNetWeight = vm.IsNetWeight,
|
||||
IsHandPicked = vm.IsHandPicked,
|
||||
IsLesewagen = vm.IsLesewagen,
|
||||
IsGebunden = vm.IsGebunden,
|
||||
Unloading = vm.Unloading,
|
||||
Temperature = vm.Temperature,
|
||||
Acid = vm.Acid,
|
||||
Comment = string.IsNullOrEmpty(vm.PartComment) ? null : vm.PartComment,
|
||||
@@ -571,6 +609,13 @@ namespace Elwig.Services {
|
||||
s.DPNr = dpnr++;
|
||||
s.Weight = w;
|
||||
ctx.Add(s);
|
||||
|
||||
ctx.AddRange(p.PartModifiers.Select(m => new DeliveryPartModifier() {
|
||||
Year = s.Year,
|
||||
DId = s.DId,
|
||||
DPNr = s.DPNr,
|
||||
ModId = m.ModId,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,6 +653,13 @@ namespace Elwig.Services {
|
||||
s.DPNr = dpnr++;
|
||||
s.Weight = w;
|
||||
ctx.Add(s);
|
||||
|
||||
ctx.AddRange(p.PartModifiers.Select(m => new DeliveryPartModifier() {
|
||||
Year = s.Year,
|
||||
DId = s.DId,
|
||||
DPNr = s.DPNr,
|
||||
ModId = m.ModId,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,6 +694,13 @@ namespace Elwig.Services {
|
||||
n.QualId = "WEI";
|
||||
n.HkId = "OEST";
|
||||
ctx.Add(n);
|
||||
|
||||
ctx.AddRange(p.PartModifiers.Select(m => new DeliveryPartModifier() {
|
||||
Year = n.Year,
|
||||
DId = n.DId,
|
||||
DPNr = n.DPNr,
|
||||
ModId = m.ModId,
|
||||
}));
|
||||
}
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
@@ -721,19 +780,30 @@ namespace Elwig.Services {
|
||||
FileName = subject == ExportSubject.Selected ? $"Lieferung_{vm.SelectedDelivery?.LsNr}.elwig.zip" : $"Lieferungen_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip",
|
||||
DefaultExt = "elwig.zip",
|
||||
Filter = "Elwig-Export-Datei (*.elwig.zip)|*.elwig.zip",
|
||||
Title = $"{DeliveryJournal.Name} speichern unter - Elwig"
|
||||
Title = $"{DeliveryJournal.Name} speichern unter - Elwig",
|
||||
AddExtension = false,
|
||||
};
|
||||
if (d.ShowDialog() == true) {
|
||||
if (!d.FileName.EndsWith(".elwig.zip")) d.FileName += ".elwig.zip";
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
try {
|
||||
await ElwigData.Export(d.FileName, await query
|
||||
var list = await query
|
||||
.Select(p => p.Delivery)
|
||||
.Distinct()
|
||||
.Include(d => d.Parts)
|
||||
.ThenInclude(p => p.PartModifiers)
|
||||
.Include(d => d.Parts).ThenInclude(p => p.PartModifiers)
|
||||
.Include(d => d.Parts).ThenInclude(p => p.Rd)
|
||||
.Include(d => d.Parts).ThenInclude(p => p.Kg!.Gl)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync(), filterNames);
|
||||
.ToListAsync();
|
||||
var wbKgs = list
|
||||
.SelectMany(d => d.Parts)
|
||||
.Where(p => p.Kg != null)
|
||||
.Select(p => p.Kg!)
|
||||
.DistinctBy(k => k.KgNr)
|
||||
.OrderBy(k => k.KgNr)
|
||||
.ToList();
|
||||
await ElwigData.Export(d.FileName, list, wbKgs, filterNames);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
@@ -743,32 +813,7 @@ namespace Elwig.Services {
|
||||
} else if (mode == ExportMode.Upload && App.Config.SyncUrl != null) {
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
try {
|
||||
var filename = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip";
|
||||
var path = Path.Combine(App.TempPath, filename);
|
||||
var list = await query
|
||||
.Select(p => p.Delivery)
|
||||
.Distinct()
|
||||
.Include(d => d.Parts)
|
||||
.ThenInclude(p => p.PartModifiers)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
if (list.Count == 0) {
|
||||
MessageBox.Show("Es wurden keine Lieferungen zum Hochladen ausgewählt!", "Lieferungen hochladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} else {
|
||||
await ElwigData.Export(path, list, filterNames);
|
||||
await Utils.UploadExportData(path, App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword);
|
||||
MessageBox.Show($"Hochladen von {list.Count:N0} Lieferungen erfolgreich!", "Lieferungen hochgeladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
} catch (HttpRequestException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (TaskCanceledException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
await SyncService.Upload(App.Config.SyncUrl, App.Config.SyncUrl, App.Config.SyncPassword, query, filterNames);
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
} else {
|
||||
@@ -919,6 +964,52 @@ namespace Elwig.Services {
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task GenerateDeliveryDataList(this DeliveryAdminViewModel vm, ExportSubject subject, ExportMode mode) {
|
||||
using var ctx = new AppDbContext();
|
||||
IQueryable<DeliveryPart> query;
|
||||
List<string> filterNames = [];
|
||||
if (subject == ExportSubject.FromFilters) {
|
||||
var (f, _, q, _, _) = await vm.GetFilters(ctx);
|
||||
query = q;
|
||||
filterNames.AddRange(f);
|
||||
} else if (subject == ExportSubject.FromSeason) {
|
||||
var year = vm.FilterSeason ?? Utils.CurrentLastSeason;
|
||||
query = ctx.DeliveryParts
|
||||
.Where(p => p.Year == year);
|
||||
filterNames.Add($"{year}");
|
||||
} else {
|
||||
throw new ArgumentException("Invalid value for ExportSubject");
|
||||
}
|
||||
|
||||
query = query
|
||||
.OrderBy(p => p.Delivery.MgNr)
|
||||
.ThenBy(p => p.SortId)
|
||||
.ThenBy(p => p.AttrId)
|
||||
.ThenBy(p => p.CultId);
|
||||
|
||||
var d = new SaveFileDialog() {
|
||||
FileName = $"Liefermengen.ods",
|
||||
DefaultExt = "ods",
|
||||
Filter = "OpenDocument Format Spreadsheet (*.ods)|*.ods",
|
||||
Title = $"Liefermengen speichern unter - Elwig"
|
||||
};
|
||||
if (d.ShowDialog() == true) {
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
try {
|
||||
using var ods = new OdsFile(d.FileName);
|
||||
var tblTotal = await MemberDeliveryData.FromQuery(query, filterNames);
|
||||
var tbl = await MemberDeliveryPerVarietyData.FromQuery(query, filterNames);
|
||||
await ods.AddTable(tblTotal);
|
||||
await ods.AddTable(tbl);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddToolTipCell(Grid grid, string text, int row, int col, int colSpan = 1, bool bold = false, bool alignRight = false, bool alignCenter = false) {
|
||||
var tb = new TextBlock() {
|
||||
Text = text,
|
||||
|
||||
@@ -9,9 +9,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
@@ -315,6 +313,22 @@ namespace Elwig.Services {
|
||||
memberQuery = memberQuery.Where(m => !m.ContactViaPost);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("nicht Kontaktart Post");
|
||||
} else if ("upload".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
memberQuery = memberQuery.Where(p => (p.XTime == null || p.MTime > p.XTime) && (p.ITime == null || p.MTime > p.ITime));
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("geändert seit letztem Export");
|
||||
} else if ("!upload".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
memberQuery = memberQuery.Where(p => !((p.XTime == null || p.MTime > p.XTime) && (p.ITime == null || p.MTime > p.ITime)));
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("unverändert seit letztem Export");
|
||||
} else if (">import".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
memberQuery = memberQuery.Where(p => p.MTime > p.ITime);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("geändert seit letztem Import");
|
||||
} else if ("<import".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
|
||||
memberQuery = memberQuery.Where(p => p.MTime <= p.ITime);
|
||||
filter.RemoveAt(i--);
|
||||
filterNames.Add("unverändert seit letztem Import");
|
||||
} else if (e.All(char.IsAsciiDigit) && mgnr.ContainsKey(e)) {
|
||||
filterMgNr.Add(int.Parse(e));
|
||||
filter.RemoveAt(i--);
|
||||
@@ -493,12 +507,12 @@ namespace Elwig.Services {
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
} else if (mode == ExportMode.Export) {
|
||||
} else if (mode == ExportMode.Vcf) {
|
||||
var d = new SaveFileDialog() {
|
||||
FileName = subject == ExportSubject.Selected ? $"Mitglied_{vm.SelectedMember?.MgNr}.elwig.zip" : $"Mitglieder_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip",
|
||||
DefaultExt = ".elwig.zip",
|
||||
Filter = "Elwig-Export-Datei (*.elwig.zip)|*.elwig.zip",
|
||||
Title = $"{MemberList.Name} speichern unter - Elwig"
|
||||
FileName = "Mitglieder.vcf",
|
||||
DefaultExt = "vcf",
|
||||
Filter = "vCard-Datei (*.vcf)|*.vcf",
|
||||
Title = "Kontakte speichern unter - Elwig"
|
||||
};
|
||||
if (d.ShowDialog() == true) {
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
@@ -511,11 +525,48 @@ namespace Elwig.Services {
|
||||
.Include(m => m.EmailAddresses)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
using var exporter = new VCard(d.FileName);
|
||||
await exporter.ExportAsync(members);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
} else if (mode == ExportMode.Export) {
|
||||
var d = new SaveFileDialog() {
|
||||
FileName = subject == ExportSubject.Selected ? $"Mitglied_{vm.SelectedMember?.MgNr}.elwig.zip" : $"Mitglieder_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip",
|
||||
DefaultExt = "elwig.zip",
|
||||
Filter = "Elwig-Export-Datei (*.elwig.zip)|*.elwig.zip",
|
||||
Title = $"{MemberList.Name} speichern unter - Elwig",
|
||||
AddExtension = false,
|
||||
};
|
||||
if (d.ShowDialog() == true) {
|
||||
if (!d.FileName.EndsWith(".elwig.zip")) d.FileName += ".elwig.zip";
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
try {
|
||||
var members = await query
|
||||
.OrderBy(m => m.MgNr)
|
||||
.Include(m => m.BillingAddress)
|
||||
.Include(m => m.TelephoneNumbers)
|
||||
.Include(m => m.EmailAddresses)
|
||||
.Include(m => m.DefaultWbKg!.Gl)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
var areaComs = await query
|
||||
.SelectMany(m => m.AreaCommitments)
|
||||
.Include(c => c.Rd)
|
||||
.Include(c => c.Kg.Gl)
|
||||
.ToListAsync();
|
||||
await ElwigData.Export(d.FileName, members, areaComs, filterNames);
|
||||
var wbKgs = members
|
||||
.Where(m => m.DefaultWbKg != null)
|
||||
.Select(m => m.DefaultWbKg!)
|
||||
.Union(areaComs.Select(c => c.Kg))
|
||||
.Distinct()
|
||||
.OrderBy(k => k.KgNr)
|
||||
.ToList();
|
||||
await ElwigData.Export(d.FileName, members, areaComs, wbKgs, filterNames);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
@@ -525,36 +576,7 @@ namespace Elwig.Services {
|
||||
} else if (mode == ExportMode.Upload && App.Config.SyncUrl != null) {
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
try {
|
||||
var filename = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip";
|
||||
var path = Path.Combine(App.TempPath, filename);
|
||||
var members = await query
|
||||
.OrderBy(m => m.MgNr)
|
||||
.Include(m => m.BillingAddress)
|
||||
.Include(m => m.TelephoneNumbers)
|
||||
.Include(m => m.EmailAddresses)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
var areaComs = await query
|
||||
.SelectMany(m => m.AreaCommitments)
|
||||
.Include(c => c.Rd)
|
||||
.ToListAsync();
|
||||
if (members.Count == 0) {
|
||||
MessageBox.Show("Es wurden keine Mitglieder zum Hochladen ausgewählt!", "Mitglieder hochladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} else {
|
||||
await ElwigData.Export(path, members, areaComs, filterNames);
|
||||
await Utils.UploadExportData(path, App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword);
|
||||
MessageBox.Show($"Hochladen von {members.Count:N0} Mitgliedern erfolgreich!", "Mitglieder hochgeladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
} catch (HttpRequestException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (TaskCanceledException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
await SyncService.Upload(App.Config.SyncUrl, App.Config.SyncUrl, App.Config.SyncPassword, query, filterNames);
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
} else {
|
||||
|
||||
277
Elwig/Services/SyncService.cs
Normal file
277
Elwig/Services/SyncService.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
using Elwig.Helpers;
|
||||
using Elwig.Helpers.Export;
|
||||
using Elwig.Models.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace Elwig.Services {
|
||||
public static class SyncService {
|
||||
|
||||
public static readonly Expression<Func<Member, bool>> ChangedMembers = (m) => ((m.XTime == null && m.MTime > 1751328000) || m.MTime > m.XTime) && (m.ITime == null || m.MTime > m.ITime);
|
||||
public static readonly Expression<Func<Delivery, bool>> ChangedDeliveries = (d) => ((d.XTime == null && d.MTime > 1751328000) || d.MTime > d.XTime) && (d.ITime == null || d.MTime > d.ITime);
|
||||
|
||||
public static async Task Upload(string url, string username, string password, IQueryable<Member> query, IEnumerable<string> filterNames) {
|
||||
try {
|
||||
var filename = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip";
|
||||
var path = Path.Combine(App.TempPath, filename);
|
||||
var members = await query
|
||||
.OrderBy(m => m.MgNr)
|
||||
.Include(m => m.BillingAddress)
|
||||
.Include(m => m.TelephoneNumbers)
|
||||
.Include(m => m.EmailAddresses)
|
||||
.Include(m => m.DefaultWbKg!.Gl)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
var areaComs = await query
|
||||
.SelectMany(m => m.AreaCommitments)
|
||||
.OrderBy(c => c.MgNr).ThenBy(c => c.FbNr)
|
||||
.Include(c => c.Rd)
|
||||
.Include(c => c.Kg.Gl)
|
||||
.ToListAsync();
|
||||
var wbKgs = members
|
||||
.Where(m => m.DefaultWbKg != null)
|
||||
.Select(m => m.DefaultWbKg!)
|
||||
.Union(areaComs.Select(c => c.Kg))
|
||||
.Distinct()
|
||||
.OrderBy(k => k.KgNr)
|
||||
.ToList();
|
||||
if (members.Count == 0) {
|
||||
MessageBox.Show("Es wurden keine Mitglieder zum Hochladen ausgewählt!", "Mitglieder hochladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} else {
|
||||
var exportedAt = DateTime.Now;
|
||||
await ElwigData.Export(path, members, areaComs, wbKgs, filterNames);
|
||||
await Utils.UploadExportData(path, url, username, password);
|
||||
await UpdateExportedAt(members, [], exportedAt);
|
||||
MessageBox.Show($"Hochladen von {members.Count:N0} Mitgliedern erfolgreich!", "Mitglieder hochgeladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
} catch (HttpRequestException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (TaskCanceledException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task Upload(string url, string username, string password, IQueryable<DeliveryPart> query, IEnumerable<string> filterNames) {
|
||||
try {
|
||||
var filename = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip";
|
||||
var path = Path.Combine(App.TempPath, filename);
|
||||
var list = await query
|
||||
.Select(p => p.Delivery)
|
||||
.Distinct()
|
||||
.Include(d => d.Parts).ThenInclude(p => p.PartModifiers).ThenInclude(m => m.Modifier)
|
||||
.Include(d => d.Parts).ThenInclude(p => p.Rd)
|
||||
.Include(d => d.Parts).ThenInclude(p => p.Kg!.Gl)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
var wbKgs = list
|
||||
.SelectMany(d => d.Parts)
|
||||
.Where(p => p.Kg != null)
|
||||
.Select(p => p.Kg!)
|
||||
.DistinctBy(k => k.KgNr)
|
||||
.OrderBy(k => k.KgNr)
|
||||
.ToList();
|
||||
if (list.Count == 0) {
|
||||
MessageBox.Show("Es wurden keine Lieferungen zum Hochladen ausgewählt!", "Lieferungen hochladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} else {
|
||||
var exportedAt = DateTime.Now;
|
||||
await ElwigData.Export(path, list, wbKgs, filterNames);
|
||||
await Utils.UploadExportData(path, url, username, password);
|
||||
await UpdateExportedAt([], list, exportedAt);
|
||||
MessageBox.Show($"Hochladen von {list.Count:N0} Lieferungen erfolgreich!", "Lieferungen hochgeladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
} catch (HttpRequestException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (TaskCanceledException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task UploadModified(string url, string username, string password) {
|
||||
try {
|
||||
var path = Path.Combine(App.TempPath, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip");
|
||||
List<Member> members;
|
||||
List<AreaCom> areaComs;
|
||||
List<Delivery> deliveries;
|
||||
using (var ctx = new AppDbContext()) {
|
||||
members = await ctx.Members
|
||||
.Where(ChangedMembers)
|
||||
.Include(m => m.BillingAddress)
|
||||
.Include(m => m.TelephoneNumbers)
|
||||
.Include(m => m.EmailAddresses)
|
||||
.Include(m => m.DefaultWbKg!.Gl)
|
||||
.OrderBy(m => m.MgNr)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
areaComs = await ctx.Members
|
||||
.Where(ChangedMembers)
|
||||
.SelectMany(m => m.AreaCommitments)
|
||||
.Include(c => c.Rd)
|
||||
.Include(c => c.Kg.Gl)
|
||||
.OrderBy(c => c.MgNr).ThenBy(c => c.FbNr)
|
||||
.ToListAsync();
|
||||
deliveries = await ctx.Deliveries
|
||||
.Where(ChangedDeliveries)
|
||||
.Include(d => d.Parts).ThenInclude(p => p.PartModifiers).ThenInclude(m => m.Modifier)
|
||||
.Include(d => d.Parts).ThenInclude(p => p.Rd)
|
||||
.Include(d => d.Parts).ThenInclude(p => p.Kg).ThenInclude(k => k!.Gl)
|
||||
.OrderBy(d => d.DateString).ThenBy(d => d.TimeString).ThenBy(d => d.LsNr)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
var wbKgs = members
|
||||
.Where(m => m.DefaultWbKg != null)
|
||||
.Select(m => m.DefaultWbKg!)
|
||||
.Union(areaComs.Select(c => c.Kg))
|
||||
.Union(deliveries.SelectMany(d => d.Parts)
|
||||
.Where(p => p.Kg != null)
|
||||
.Select(p => p.Kg!))
|
||||
.DistinctBy(k => k.KgNr)
|
||||
.OrderBy(k => k.KgNr)
|
||||
.ToList();
|
||||
if (members.Count == 0 && deliveries.Count == 0) {
|
||||
MessageBox.Show("Es gibt keine geänderten Mitglieder oder Lieferungen, die hochgeladen werden könnten!", "Mitglieder und Lieferungen hochladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
} else {
|
||||
var exportedAt = DateTime.Now;
|
||||
await (new ElwigData.ElwigExport {
|
||||
Members = (members, ["geändert seit letztem Export"]),
|
||||
AreaComs = (areaComs, ["von exportierten Mitgliedern"]),
|
||||
Deliveries = (deliveries, ["geändert seit letzem Export"]),
|
||||
WbKgs = (wbKgs, ["von exportierten Mitgliedern, Flächenbindungen und Lieferungen"]),
|
||||
}).Export(path);
|
||||
await Utils.UploadExportData(path, url, username, password);
|
||||
await UpdateExportedAt(members, deliveries, exportedAt);
|
||||
MessageBox.Show($"Hochladen von {members.Count:N0} Mitgliedern und {deliveries.Count:N0} Lieferungen erfolgreich!", "Mitglieder und Lieferungen hochladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
} catch (HttpRequestException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder und Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (TaskCanceledException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder und Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Mitglieder und Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task UploadBranchDeliveries(string url, string username, string password, int? year = null) {
|
||||
try {
|
||||
year ??= Utils.CurrentLastSeason;
|
||||
var path = Path.Combine(App.TempPath, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip");
|
||||
using var ctx = new AppDbContext();
|
||||
var deliveries = await ctx.Deliveries
|
||||
.Where(d => d.Year == year && d.ZwstId == App.ZwstId)
|
||||
.Include(d => d.Parts).ThenInclude(p => p.PartModifiers).ThenInclude(m => m.Modifier)
|
||||
.Include(d => d.Parts).ThenInclude(p => p.Rd)
|
||||
.Include(d => d.Parts).ThenInclude(p => p.Kg).ThenInclude(k => k!.Gl)
|
||||
.OrderBy(d => d.DateString).ThenBy(d => d.TimeString).ThenBy(d => d.LsNr)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
var wbKgs = deliveries
|
||||
.SelectMany(d => d.Parts)
|
||||
.Where(p => p.Kg != null)
|
||||
.Select(p => p.Kg!)
|
||||
.DistinctBy(k => k.KgNr)
|
||||
.ToList();
|
||||
if (deliveries.Count == 0) {
|
||||
MessageBox.Show("Es gibt keine Lieferungen, die hochgeladen werden können!", "Lieferungen hochladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} else {
|
||||
var exportedAt = DateTime.Now;
|
||||
await ElwigData.Export(path, deliveries, wbKgs, [$"{year}", $"Zweigstelle {App.BranchName}"]);
|
||||
await Utils.UploadExportData(path, url, username, password);
|
||||
await UpdateExportedAt([], deliveries, exportedAt);
|
||||
MessageBox.Show($"Hochladen von {deliveries.Count:N0} Lieferungen erfolgreich!", "Lieferungen hochladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
} catch (HttpRequestException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (TaskCanceledException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<List<(string Name, string Url)>> GetFilesToImport(string url, string username, string password) {
|
||||
var data = await Utils.GetExportMetaData(url, username, password);
|
||||
var files = data
|
||||
.Select(f => new {
|
||||
Name = f!["name"]!.AsValue().GetValue<string>(),
|
||||
Timestamp = f!["timestamp"] != null && DateTime.TryParseExact(f!["timestamp"]!.AsValue().GetValue<string>(), "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : (DateTime?)null,
|
||||
ZwstId = f!["meta"]?["zwstid"]?.AsValue().GetValue<string>() ?? f!["zwstid"]?.AsValue().GetValue<string>(),
|
||||
Device = f!["meta"]?["device"]?.AsValue().GetValue<string>(),
|
||||
Url = f!["url"]!.AsValue().GetValue<string>(),
|
||||
Size = f!["size"]!.AsValue().GetValue<long>(),
|
||||
})
|
||||
.Where(f => f.Timestamp >= new DateTime(Utils.CurrentLastSeason, 7, 1))
|
||||
.ToList();
|
||||
|
||||
var imported = await ElwigData.GetImportedFiles();
|
||||
return [.. files
|
||||
.Where(f => f.Device != Environment.MachineName && !imported.Contains(f.Name))
|
||||
.Select(f => (f.Name, f.Url))
|
||||
];
|
||||
}
|
||||
|
||||
public static async Task Download(string url, string username, string password) {
|
||||
try {
|
||||
var import = await GetFilesToImport(url, username, password);
|
||||
var paths = new List<string>();
|
||||
using (var client = Utils.GetHttpClient(username, password)) {
|
||||
foreach (var f in import) {
|
||||
var filename = Path.Combine(App.TempPath, f.Name);
|
||||
using var stream = new FileStream(filename, FileMode.Create);
|
||||
await client.DownloadAsync(f.Url, stream);
|
||||
paths.Add(filename);
|
||||
}
|
||||
}
|
||||
await ElwigData.Import(paths, ElwigData.ImportMode.FromBranches);
|
||||
} catch (HttpRequestException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Daten herunterladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (TaskCanceledException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Daten herunterladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Daten herunterladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task UpdateExportedAt(IEnumerable<Member> member, IEnumerable<Delivery> deliveries, DateTime dateTime) {
|
||||
var timestamp = ((DateTimeOffset)dateTime.ToUniversalTime()).ToUnixTimeSeconds();
|
||||
var mgnrs = string.Join(",", member.Select(m => $"{m.MgNr}").Append("0"));
|
||||
var dids = string.Join(",", deliveries.Select(d => $"({d.Year},{d.DId})").Append("(0,0)"));
|
||||
using (var cnx = await AppDbContext.ConnectAsync()) {
|
||||
await cnx.ExecuteBatch($"""
|
||||
BEGIN;
|
||||
UPDATE client_parameter SET value = '0' WHERE param = 'ENABLE_TIME_TRIGGERS';
|
||||
UPDATE member SET xtime = {timestamp} WHERE mgnr IN ({mgnrs});
|
||||
UPDATE area_commitment SET xtime = {timestamp} WHERE mgnr IN ({mgnrs});
|
||||
UPDATE delivery SET xtime = {timestamp} WHERE (year, did) IN ({dids});
|
||||
UPDATE delivery_part SET xtime = {timestamp} WHERE (year, did) IN ({dids});
|
||||
UPDATE client_parameter SET value = '1' WHERE param = 'ENABLE_TIME_TRIGGERS';
|
||||
COMMIT;
|
||||
""");
|
||||
}
|
||||
App.HintContextChange();
|
||||
}
|
||||
|
||||
public static async Task<bool> ChangesAvailable(AppDbContext ctx, string url, string username, string password) {
|
||||
return await ctx.Members.AnyAsync(ChangedMembers) || await ctx.Deliveries.AnyAsync(ChangedDeliveries) || (Utils.HasInternetConnectivity() && (await GetFilesToImport(url, username, password)).Count > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,10 +169,28 @@ namespace Elwig.ViewModels {
|
||||
set => AcidString = $"{value:0.0}";
|
||||
}
|
||||
[ObservableProperty]
|
||||
private bool _isLesewagen;
|
||||
[ObservableProperty]
|
||||
private bool? _isHandPicked;
|
||||
|
||||
public string? Unloading {
|
||||
get => IsUnloadingDumper ? DeliveryPart.Dumper : IsUnloadingPumped ? DeliveryPart.Pumped : IsUnloadingBox ? DeliveryPart.Box : null;
|
||||
set {
|
||||
switch (value) {
|
||||
case DeliveryPart.Dumper: IsUnloadingDumper = true; break;
|
||||
case DeliveryPart.Pumped: IsUnloadingPumped = true; break;
|
||||
case DeliveryPart.Box: IsUnloadingBox = true; break;
|
||||
default: IsUnloadingOther = true; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
[ObservableProperty]
|
||||
private bool _isUnloadingDumper;
|
||||
[ObservableProperty]
|
||||
private bool _isUnloadingPumped;
|
||||
[ObservableProperty]
|
||||
private bool _isUnloadingBox;
|
||||
[ObservableProperty]
|
||||
private bool _isUnloadingOther;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusMembers = "-";
|
||||
[ObservableProperty]
|
||||
|
||||
32
Elwig/Windows/AboutWindow.xaml
Normal file
32
Elwig/Windows/AboutWindow.xaml
Normal file
@@ -0,0 +1,32 @@
|
||||
<Window x:Class="Elwig.Windows.AboutWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Elwig.Windows"
|
||||
Title="Über - Elwig" Height="340" Width="460" ResizeMode="NoResize">
|
||||
<Grid>
|
||||
<TextBlock Margin="20,10" FontSize="12">
|
||||
<Bold>Produkt:</Bold> Elwig<LineBreak/>
|
||||
<Bold>Beschreibung:</Bold> Elektronische Winzergenossenschaftsverwaltung<LineBreak/>
|
||||
<Bold>Typ:</Bold> Warenwirtschaftssystem (ERP-System)<LineBreak/>
|
||||
<Bold>Version:</Bold> <Run x:Name="Version">0.0.0.0</Run> (<Hyperlink NavigateUri="https://elwig.at/changelog" RequestNavigate="Hyperlink_RequestNavigate">Änderungsprotokoll</Hyperlink>)<LineBreak/>
|
||||
<Bold>Lizenz:</Bold> <Hyperlink NavigateUri="https://www.gnu.org/licenses/gpl-3.0.html" RequestNavigate="Hyperlink_RequestNavigate">GNU General Public License 3.0 (GPLv3)</Hyperlink><LineBreak/>
|
||||
<Bold>Website:</Bold> <Hyperlink NavigateUri="https://elwig.at/" RequestNavigate="Hyperlink_RequestNavigate">https://elwig.at/</Hyperlink><LineBreak/>
|
||||
<Bold>Entwickler:</Bold> Lorenz Stechauner, Thomas Hilscher<LineBreak/>
|
||||
<Bold>Kontakt:</Bold> <Hyperlink NavigateUri="mailto:lorenz.stechauner@necronda.net" RequestNavigate="Hyperlink_RequestNavigate">lorenz.stechauner@necronda.net</Hyperlink>, <Hyperlink NavigateUri="mailto:thomas.hilscher@gmail.com" RequestNavigate="Hyperlink_RequestNavigate">thomas.hilscher@gmail.com</Hyperlink><LineBreak/>
|
||||
<Bold>Quellcode:</Bold> <Hyperlink NavigateUri="https://git.necronda.net/winzer/elwig" RequestNavigate="Hyperlink_RequestNavigate">https://git.necronda.net/winzer/elwig</Hyperlink><LineBreak/>
|
||||
<Bold>Entwicklungszeitraum:</Bold> 2022–2025<LineBreak/>
|
||||
<LineBreak/>
|
||||
<Bold>Verwendete Technologien:</Bold><LineBreak/>
|
||||
Programmiersprache: C#<LineBreak/>
|
||||
Framework: Windows Presentation Framework (WPF)<LineBreak/>
|
||||
Datenbank: <Hyperlink NavigateUri="https://sqlite.org/" RequestNavigate="Hyperlink_RequestNavigate">SQLite</Hyperlink><LineBreak/>
|
||||
PDF-Erstellung: <Hyperlink NavigateUri="https://weasyprint.org/" RequestNavigate="Hyperlink_RequestNavigate">WeasyPrint</Hyperlink>, <Hyperlink NavigateUri="https://github.com/toddams/RazorLight" RequestNavigate="Hyperlink_RequestNavigate">RazorLight</Hyperlink>, <Hyperlink NavigateUri="https://github.com/pvginkel/PdfiumViewer" RequestNavigate="Hyperlink_RequestNavigate">PdfiumViewer</Hyperlink><LineBreak/>
|
||||
Paketierung: <Hyperlink NavigateUri="https://www.firegiant.com/wixtoolset/" RequestNavigate="Hyperlink_RequestNavigate">WiX Toolset</Hyperlink>
|
||||
</TextBlock>
|
||||
|
||||
<Image Source="\Resources\Images\Elwig.png" RenderOptions.BitmapScalingMode="HighQuality" Height="64"
|
||||
HorizontalAlignment="Right" Margin="10" VerticalAlignment="Top"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
17
Elwig/Windows/AboutWindow.xaml.cs
Normal file
17
Elwig/Windows/AboutWindow.xaml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Navigation;
|
||||
|
||||
namespace Elwig.Windows {
|
||||
public partial class AboutWindow : Window {
|
||||
|
||||
public AboutWindow() {
|
||||
InitializeComponent();
|
||||
Version.Text = App.Version.ToString();
|
||||
}
|
||||
|
||||
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) {
|
||||
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@ using System.Windows.Input;
|
||||
namespace Elwig.Windows {
|
||||
public abstract class AdministrationWindow : ContextWindow {
|
||||
|
||||
protected Control[] ExemptInputs { private get; set; }
|
||||
protected Control[] RequiredInputs { private get; set; }
|
||||
protected Control[] ExemptInputs { get; set; }
|
||||
protected Control[] RequiredInputs { get; set; }
|
||||
|
||||
private bool _isEditing;
|
||||
private bool _isCreating;
|
||||
@@ -166,8 +166,10 @@ namespace Elwig.Windows {
|
||||
Valid[input] = false;
|
||||
} else if (input is ComboBox cb && cb.SelectedItem == null && cb.ItemsSource != null && cb.ItemsSource.Cast<object>().Any()) {
|
||||
ControlUtils.SetInputInvalid(input);
|
||||
Valid[input] = false;
|
||||
} else if (input is ListBox lb && lb.SelectedItem == null && lb.ItemsSource != null && lb.ItemsSource.Cast<object>().Any()) {
|
||||
ControlUtils.SetInputInvalid(input);
|
||||
Valid[input] = false;
|
||||
} else if (input is CheckBox ckb && ((ckb.IsThreeState && ckb.IsChecked == null) || (!ckb.IsThreeState && ckb.IsChecked != true))) {
|
||||
ControlUtils.SetInputInvalid(input);
|
||||
Valid[input] = false;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
xmlns:local="clr-namespace:Elwig.Windows"
|
||||
xmlns:ctrl="clr-namespace:Elwig.Controls"
|
||||
mc:Ignorable="d"
|
||||
Title="Stammdaten - Elwig" Height="500" MinHeight="400" Width="850" MinWidth="810"
|
||||
Title="Stammdaten - Elwig" Height="520" MinHeight="400" Width="860" MinWidth="810"
|
||||
Loaded="Window_Loaded">
|
||||
<Window.Resources>
|
||||
<Style TargetType="Label">
|
||||
@@ -532,6 +532,10 @@
|
||||
<CheckBox x:Name="SeasonModifierActiveInput" Content="In Übernahme-Fenster anzeigen"
|
||||
Grid.Column="1" Grid.ColumnSpan="2" Margin="10,134,10,10" HorizontalAlignment="Left" VerticalAlignment="Top"
|
||||
Checked="SeasonModifier_Changed" Unchecked="SeasonModifier_Changed"/>
|
||||
|
||||
<CheckBox x:Name="SeasonModifierRedactedInput" Content="Wert auf Lieferschein verbergen"
|
||||
Grid.Column="1" Grid.ColumnSpan="2" Margin="10,154,10,10" HorizontalAlignment="Left" VerticalAlignment="Top"
|
||||
Checked="SeasonModifier_Changed" Unchecked="SeasonModifier_Changed"/>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</Grid>
|
||||
|
||||
@@ -135,12 +135,14 @@ namespace Elwig.Windows {
|
||||
SeasonModifierRelInput.Text = "";
|
||||
SeasonModifierAbsInput.Text = "";
|
||||
SeasonModifierActiveInput.IsChecked = false;
|
||||
SeasonModifierRedactedInput.IsChecked = false;
|
||||
} else {
|
||||
SeasonModifierIdInput.Text = mod.ModId;
|
||||
SeasonModifierNameInput.Text = mod.Name;
|
||||
SeasonModifierRelInput.Text = (mod.Rel * 100)?.ToString() ?? "";
|
||||
SeasonModifierAbsInput.Text = mod.Abs?.ToString() ?? "";
|
||||
SeasonModifierActiveInput.IsChecked = mod.IsActive;
|
||||
SeasonModifierRedactedInput.IsChecked = mod.IsRedacted;
|
||||
}
|
||||
_modUpdate = false;
|
||||
}
|
||||
@@ -157,6 +159,7 @@ namespace Elwig.Windows {
|
||||
mod.Rel = decimal.TryParse(SeasonModifierRelInput.Text, out var vRel) ? vRel / 100 : null;
|
||||
mod.AbsValue = decimal.TryParse(SeasonModifierAbsInput.Text, out var vAbs) ? Utils.DecToDb(vAbs, s.Precision) : null;
|
||||
mod.IsActive = SeasonModifierActiveInput.IsChecked ?? false;
|
||||
mod.IsRedacted = SeasonModifierRedactedInput.IsChecked ?? false;
|
||||
|
||||
CollectionViewSource.GetDefaultView(_modList).Refresh();
|
||||
UpdateButtons();
|
||||
|
||||
@@ -664,8 +664,10 @@ namespace Elwig.Windows {
|
||||
try {
|
||||
await Task.Run(async () => {
|
||||
var b = new BillingVariant(PaymentVar.Year, PaymentVar.AvNr);
|
||||
await b.Calculate();
|
||||
await b.Calculate(false);
|
||||
});
|
||||
} catch (KeyNotFoundException exc) {
|
||||
MessageBox.Show(exc.Message, "Noch nicht alle Preise festgelegt", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Berechnungsfehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
|
||||
@@ -129,6 +129,10 @@
|
||||
<MenuItem x:Name="Menu_DeliveryDepreciationList_PrintSeason" Header="...von Saison drucken"
|
||||
Click="Menu_DeliveryDepreciationList_PrintSeason_Click"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Liefermengen" x:Name="Menu_DeliveryDataList">
|
||||
<MenuItem x:Name="Menu_DeliveryDataList_SaveFilters" Header="...aus Filtern speichern... (Excel)"
|
||||
Click="Menu_DeliveryDataList_SaveFilters_Click"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Statistik" x:Name="Menu_Statistics">
|
||||
<MenuItem Header="Qualitätsstatistik..." x:Name="Menu_Statistics_WineQuality">
|
||||
<MenuItem x:Name="Menu_Statistics_WineQuality_ShowFilters" Header="...aus Filtern anzeigen (PDF)"
|
||||
@@ -222,6 +226,7 @@
|
||||
<Bold>Uhrzeit</Bold>: z.B. 06:00-08:00, 18:00-, ...<LineBreak/>
|
||||
<Bold>Handwiegung</Bold>: handw[iegung], !Handw[iegung] (alle ohne Handwiegung)<LineBreak/>
|
||||
<Bold>Handlese</Bold>: Handl[ese], !handl[ese] (alle ohne Handlese)<LineBreak/>
|
||||
<Bold>Anlieferung</Bold>: Plane[nwagen]/Kipp[er], !plane[nwagen]/!kipp[er], Lesew[agen], !lesew[agen], kiste[n], !kiste[n]<LineBreak/>
|
||||
<Bold>Gebunden</Bold>: geb[unden], ungeb[unden], !geb[unden], !ungeb[unden]<LineBreak/>
|
||||
<Bold>Gerebelt</Bold>: gerebelt, !Gerebelt (nicht gerebelt gewogen)<LineBreak/>
|
||||
<Bold>Freitext</Bold>: z.B. Lieferscheinnummern, Anmerkung, "quw" (sucht nach dem Text "quw")
|
||||
@@ -265,13 +270,18 @@
|
||||
</Style>
|
||||
</DataGridTextColumn.CellStyle>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Header="Sorte" Binding="{Binding FilteredSortIdString}" Width="50">
|
||||
<DataGridTextColumn Header="Sorte" Binding="{Binding FilteredVaributeString}" Width="60">
|
||||
<DataGridTextColumn.CellStyle>
|
||||
<Style>
|
||||
<Setter Property="TextBlock.Foreground" Value="{Binding FilteredColor}"/>
|
||||
<Setter Property="TextBlock.TextAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</DataGridTextColumn.CellStyle>
|
||||
<DataGridTextColumn.ElementStyle>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||
</Style>
|
||||
</DataGridTextColumn.ElementStyle>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Header="Menge" Binding="{Binding FilteredWeight, StringFormat='{}{0:N0} kg '}" Width="75">
|
||||
<DataGridTextColumn.CellStyle>
|
||||
@@ -290,6 +300,7 @@
|
||||
<DataGridTextColumn Header="LsNr." Binding="{Binding LsNr}" Width="120"/>
|
||||
<DataGridTextColumn Header="Mitglied" Binding="{Binding Member.AdministrativeName}" Width="180"/>
|
||||
<DataGridTextColumn Header="Zu-/Abschläge" Binding="{Binding FilteredModifiersString}" Width="150"/>
|
||||
<DataGridTextColumn Header="Kommentar" Binding="{Binding CommentsString}" Width="150"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
@@ -363,8 +374,9 @@
|
||||
<Grid Grid.Row="1" Grid.Column="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="0.625*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="0.875*"/>
|
||||
<RowDefinition Height="0.875*"/>
|
||||
<RowDefinition Height="0.25*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -398,7 +410,7 @@
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="Lieferung" Grid.Column="0" Grid.Row="1" Grid.RowSpan="2" Margin="5,5,5,5">
|
||||
<GroupBox Header="Lieferung" Grid.Column="0" Grid.Row="1" Grid.RowSpan="3" Margin="5,5,5,5">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="100"/>
|
||||
@@ -557,7 +569,7 @@
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="Sonstiges" Grid.Column="1" Grid.Row="3" Margin="5,5,5,10">
|
||||
<GroupBox Header="Sonstiges" Grid.Column="1" Grid.Row="3" Grid.RowSpan="2" Margin="5,5,5,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="100"/>
|
||||
@@ -586,18 +598,31 @@
|
||||
TextChanged="TemperatureAcidInput_TextChanged" LostFocus="TemperatureAcidInput_LostFocus"
|
||||
Grid.Column="1" Margin="0,100,10,10" VerticalAlignment="Top"/>
|
||||
|
||||
<CheckBox x:Name="LesewagenInput" IsChecked="{Binding IsLesewagen, Mode=TwoWay}"
|
||||
Content="Lesewagen" Margin="10,75,0,0" Grid.Column="2"
|
||||
VerticalAlignment="Top" HorizontalAlignment="Left"
|
||||
Checked="LesewagenInput_Changed" Unchecked="LesewagenInput_Changed"/>
|
||||
<CheckBox x:Name="HandPickedInput" IsChecked="{Binding IsHandPicked, Mode=TwoWay}"
|
||||
Content="Handlese" Margin="10,105,0,0" Grid.Column="2" IsThreeState="True"
|
||||
Content="Handlese" Margin="10,135,10,0" Grid.Column="0" Grid.ColumnSpan="2" IsThreeState="True"
|
||||
VerticalAlignment="Top" HorizontalAlignment="Left"
|
||||
Checked="HandPickedInput_Changed" Unchecked="HandPickedInput_Changed" Indeterminate="HandPickedInput_Changed"/>
|
||||
|
||||
<RadioButton x:Name="UnloadingDumperInput" GroupName="Unloading" IsChecked="{Binding IsUnloadingDumper, Mode=TwoWay}"
|
||||
Content="Planenw./Kipper" Margin="10,75,0,0" Grid.Column="2"
|
||||
VerticalAlignment="Top" HorizontalAlignment="Left"
|
||||
Checked="UnloadingInput_Checked" Unchecked="UnloadingInput_Unchecked"/>
|
||||
<RadioButton x:Name="UnloadingPumpedInput" GroupName="Unloading" IsChecked="{Binding IsUnloadingPumped, Mode=TwoWay}"
|
||||
Content="Lesewagen" Margin="10,95,0,0" Grid.Column="2"
|
||||
VerticalAlignment="Top" HorizontalAlignment="Left"
|
||||
Checked="UnloadingInput_Checked" Unchecked="UnloadingInput_Unchecked"/>
|
||||
<RadioButton x:Name="UnloadingBoxInput" GroupName="Unloading" IsChecked="{Binding IsUnloadingBox, Mode=TwoWay}"
|
||||
Content="Kiste(n)" Margin="10,115,0,0" Grid.Column="2"
|
||||
VerticalAlignment="Top" HorizontalAlignment="Left"
|
||||
Checked="UnloadingInput_Checked" Unchecked="UnloadingInput_Unchecked"/>
|
||||
<RadioButton x:Name="UnloadingOtherInput" GroupName="Unloading" IsChecked="{Binding IsUnloadingOther, Mode=TwoWay}"
|
||||
Content="Andere/Unbek." Margin="10,135,0,0" Grid.Column="2"
|
||||
VerticalAlignment="Top" HorizontalAlignment="Left"
|
||||
Checked="UnloadingInput_Checked" Unchecked="UnloadingInput_Unchecked"/>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="Herkunft" Grid.Column="0" Grid.Row="3" Margin="5,5,5,10">
|
||||
<GroupBox Header="Herkunft" Grid.Column="0" Grid.Row="4" Margin="5,5,5,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="100"/>
|
||||
|
||||
@@ -8,6 +8,7 @@ using Elwig.ViewModels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
@@ -34,6 +35,8 @@ namespace Elwig.Windows {
|
||||
|
||||
private readonly Button[] WeighingButtons;
|
||||
|
||||
private List<WineQualLevel> WineQualityLevels = [];
|
||||
|
||||
public DeliveryAdminWindow(bool receipt = false) {
|
||||
InitializeComponent();
|
||||
CommandBindings.Add(new CommandBinding(CtrlF, FocusSearchInput));
|
||||
@@ -89,6 +92,10 @@ namespace Elwig.Windows {
|
||||
foreach (var s in App.EventScales) {
|
||||
s.WeighingEvent += Scale_Weighing;
|
||||
}
|
||||
if (App.Client.IsMatzen) {
|
||||
RequiredInputs = [.. RequiredInputs, ModifiersInput];
|
||||
}
|
||||
ModifiersInput.ItemTemplate = (DataTemplate)ModifiersInput.FindResource("PublicModifierTemplate");
|
||||
} else {
|
||||
WeighingManualButton.Visibility = Visibility.Hidden;
|
||||
WeighingAButton.Visibility = Visibility.Hidden;
|
||||
@@ -128,7 +135,7 @@ namespace Elwig.Windows {
|
||||
NewDeliveryButton_Click(null, null);
|
||||
using var ctx = new AppDbContext();
|
||||
if (ctx.Seasons.Find(Utils.CurrentYear) == null) {
|
||||
MessageBox.Show("Die Saison für das aktuelle Jahr wurde noch nicht erstellt. Neue Lieferungen können nicht abgespeichert werden.",
|
||||
MessageBox.Show("Die Saison für das aktuelle Jahr wurde noch nicht erstellt. Neue Lieferungen können nicht abgespeichert werden.\n\n(Stammdaten -> Saisons -> Neu anlegen...)",
|
||||
"Saison noch nicht erstellt", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
@@ -253,7 +260,7 @@ namespace Elwig.Windows {
|
||||
await App.Client.UpdateValues();
|
||||
}
|
||||
|
||||
private async void Menu_Statistic_Locality_SaveFilters_Click(object sender, RoutedEventArgs evt)=>
|
||||
private async void Menu_Statistic_Locality_SaveFilters_Click(object sender, RoutedEventArgs evt) =>
|
||||
await ViewModel.GenerateLocalityStatistics(DeliveryService.ExportSubject.FromFilters);
|
||||
|
||||
private async void Menu_DeliveryDepreciationList_SaveFilters_Click(object sender, RoutedEventArgs evt) =>
|
||||
@@ -273,11 +280,15 @@ namespace Elwig.Windows {
|
||||
private async void Menu_DeliveryDepreciationList_PrintSeason_Click(object sender, RoutedEventArgs evt) =>
|
||||
await ViewModel.GenerateDeliveryDepreciationList(DeliveryService.ExportSubject.FromSeason, ExportMode.Print);
|
||||
|
||||
private async void Menu_DeliveryDataList_SaveFilters_Click(object sender, RoutedEventArgs evt) =>
|
||||
await ViewModel.GenerateDeliveryDataList(DeliveryService.ExportSubject.FromFilters, ExportMode.SaveList);
|
||||
|
||||
private void Menu_Settings_EnableFreeEditing_Checked(object sender, RoutedEventArgs evt) {
|
||||
if (IsEditing || IsCreating) {
|
||||
DateInput.IsReadOnly = false;
|
||||
TimeInput.IsReadOnly = false;
|
||||
BranchInput.IsEnabled = true;
|
||||
GerebeltGewogenInput.IsEnabled = true;
|
||||
if (IsCreating) ViewModel.Time = "";
|
||||
OnSecondPassed(null, null);
|
||||
}
|
||||
@@ -287,6 +298,7 @@ namespace Elwig.Windows {
|
||||
DateInput.IsReadOnly = true;
|
||||
TimeInput.IsReadOnly = true;
|
||||
BranchInput.IsEnabled = false;
|
||||
GerebeltGewogenInput.IsEnabled = App.Config.WeighingMode != WeighingMode.Net;
|
||||
OnSecondPassed(null, null);
|
||||
}
|
||||
|
||||
@@ -301,7 +313,7 @@ namespace Elwig.Windows {
|
||||
}
|
||||
|
||||
private void InitialDefaultInputs() {
|
||||
if (App.Client.HasNetWeighing(ViewModel.Branch)) {
|
||||
if (App.Config.WeighingMode == WeighingMode.Net) {
|
||||
GerebeltGewogenInput.IsEnabled = false;
|
||||
SetDefaultValue(GerebeltGewogenInput, true);
|
||||
} else {
|
||||
@@ -310,15 +322,20 @@ namespace Elwig.Windows {
|
||||
UnsetDefaultValue(GerebeltGewogenInput);
|
||||
}
|
||||
|
||||
if (App.Client.HasBoxWeighing(ViewModel.Branch)) {
|
||||
LesewagenInput.IsEnabled = false;
|
||||
SetDefaultValue(LesewagenInput, false);
|
||||
if (App.Config.WeighingMode == WeighingMode.Box) {
|
||||
SetDefaultValue(UnloadingDumperInput, false);
|
||||
SetDefaultValue(UnloadingPumpedInput, false);
|
||||
SetDefaultValue(UnloadingBoxInput, true);
|
||||
SetDefaultValue(UnloadingOtherInput, false);
|
||||
UnloadingBoxInput.IsChecked = true;
|
||||
} else {
|
||||
LesewagenInput.IsEnabled = true;
|
||||
UnsetDefaultValue(LesewagenInput);
|
||||
UnsetDefaultValue(UnloadingDumperInput);
|
||||
UnsetDefaultValue(UnloadingPumpedInput);
|
||||
UnsetDefaultValue(UnloadingBoxInput);
|
||||
UnsetDefaultValue(UnloadingOtherInput);
|
||||
}
|
||||
|
||||
if (!App.Client.HasNetWeighing(ViewModel.Branch)) {
|
||||
if (App.Config.WeighingMode != WeighingMode.Net) {
|
||||
HandPickedInput.IsThreeState = false;
|
||||
UnsetDefaultValue(HandPickedInput);
|
||||
} else {
|
||||
@@ -344,13 +361,13 @@ namespace Elwig.Windows {
|
||||
ClearOriginalValues();
|
||||
ClearDefaultValues();
|
||||
|
||||
ViewModel.IsNetWeight = App.Client.HasNetWeighing(ViewModel.Branch);
|
||||
ViewModel.IsLesewagen = false;
|
||||
ViewModel.IsHandPicked = !App.Client.HasNetWeighing(ViewModel.Branch) ? true : null;
|
||||
ViewModel.IsNetWeight = App.Config.WeighingMode == WeighingMode.Net;
|
||||
ViewModel.IsHandPicked = App.Config.WeighingMode != WeighingMode.Net ? true : null;
|
||||
ViewModel.Unloading = null;
|
||||
ViewModel.IsGebunden = null;
|
||||
InitialDefaultInputs();
|
||||
|
||||
WineQualityLevelInput.IsEnabled = false;
|
||||
//WineQualityLevelInput.IsEnabled = false; // disable wine quality level input in Übernahme
|
||||
ValidateRequiredInputs();
|
||||
}
|
||||
|
||||
@@ -478,7 +495,8 @@ namespace Elwig.Windows {
|
||||
var cultList = await ctx.WineCultivations.OrderBy(a => a.Name).Cast<object>().ToListAsync();
|
||||
cultList.Insert(0, new NullItem(""));
|
||||
ControlUtils.RenewItemsSource(CultivationInput, cultList, null, ControlUtils.RenewSourceDefault.First);
|
||||
ControlUtils.RenewItemsSource(WineQualityLevelInput, await ctx.WineQualityLevels.ToListAsync());
|
||||
WineQualityLevels = await ctx.WineQualityLevels.ToListAsync();
|
||||
ControlUtils.RenewItemsSource(WineQualityLevelInput, WineQualityLevels);
|
||||
ControlUtils.RenewItemsSource(ModifiersInput, await ctx.Modifiers
|
||||
.Where(m => m.Year == y && (!IsCreating || m.IsActive))
|
||||
.OrderBy(m => m.Ordering)
|
||||
@@ -1139,6 +1157,7 @@ namespace Elwig.Windows {
|
||||
DateInput.IsReadOnly = !Menu_Settings_EnableFreeEditing.IsChecked;
|
||||
TimeInput.IsReadOnly = !Menu_Settings_EnableFreeEditing.IsChecked;
|
||||
BranchInput.IsEnabled = Menu_Settings_EnableFreeEditing.IsChecked;
|
||||
GerebeltGewogenInput.IsEnabled = App.Config.WeighingMode == WeighingMode.Net || Menu_Settings_EnableFreeEditing.IsChecked;
|
||||
}
|
||||
|
||||
private void DisableWeighingButtons() {
|
||||
@@ -1158,6 +1177,19 @@ namespace Elwig.Windows {
|
||||
WeighingDButton.IsEnabled = n > 3 && App.CommandScales[3].IsReady;
|
||||
}
|
||||
|
||||
public void UpdateScales() {
|
||||
if (!ViewModel.IsReceipt) return;
|
||||
foreach (var s in App.EventScales) {
|
||||
s.WeighingEvent -= Scale_Weighing;
|
||||
s.WeighingEvent += Scale_Weighing;
|
||||
}
|
||||
if (WeighingManualButton.IsEnabled) {
|
||||
EnableWeighingButtons();
|
||||
} else {
|
||||
DisableWeighingButtons();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateLsNr() {
|
||||
if (string.IsNullOrEmpty(ViewModel.Date) || ViewModel.Branch == null) {
|
||||
ViewModel.LsNr = "";
|
||||
@@ -1201,6 +1233,7 @@ namespace Elwig.Windows {
|
||||
AttributeInput.SelectedIndex = 0;
|
||||
CultivationInput.SelectedIndex = 0;
|
||||
}
|
||||
UpdateWineQualityLevels();
|
||||
}
|
||||
|
||||
private void SortIdInput_TextChanged(object sender, TextChangedEventArgs evt) {
|
||||
@@ -1214,19 +1247,39 @@ namespace Elwig.Windows {
|
||||
private void WineVarietyInput_SelectionChanged(object sender, SelectionChangedEventArgs evt) {
|
||||
if (WineVarietyInput.SelectedItem is WineVar s)
|
||||
ViewModel.SortId = s.SortId;
|
||||
UpdateWineQualityLevels();
|
||||
if (!ViewModel.WineVar?.IsQuw ?? false) {
|
||||
App.MainDispatcher.BeginInvoke(() => {
|
||||
MessageBox.Show("Die eingegebene Sorte darf nicht als Qualitätswein\nübernommen werden!", "Kein Qualitätswein",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private WineQualLevel GetWineQualityLevel(double kmw, string? maxQualId = null) {
|
||||
return WineQualityLevels
|
||||
.Where(q => !q.IsPredicate && (q.MinKmw == null || q.MinKmw <= kmw))
|
||||
.Where(q => maxQualId == null || q.QualId == "WEI" || q.QualId == maxQualId)
|
||||
.OrderBy(q => q.MinKmw)
|
||||
.Last();
|
||||
}
|
||||
|
||||
private void UpdateWineQualityLevels() {
|
||||
using var ctx = new AppDbContext();
|
||||
if (!GetInputValid(GradationKmwInput)) {
|
||||
UnsetDefaultValue(WineQualityLevelInput);
|
||||
ComboBox_SelectionChanged(WineQualityLevelInput, null);
|
||||
WineQualityLevelInput.ItemsSource = ctx.WineQualityLevels.Where(q => q.QualId == "WEI").ToList();
|
||||
WineQualityLevelInput.ItemsSource = WineQualityLevels;
|
||||
WineQualityLevelInput.SelectedItem = null;
|
||||
return;
|
||||
}
|
||||
var kmw = (double)ViewModel.GradationKmw!;
|
||||
WineQualityLevelInput.ItemsSource = ctx.WineQualityLevels.Where(q => q.MinKmw == null || q.MinKmw <= kmw).ToList();
|
||||
var qual = ctx.GetWineQualityLevel(kmw).GetAwaiter().GetResult();
|
||||
var max = ViewModel.WineVar?.MaxQualId;
|
||||
var quw = ViewModel.WineVar?.IsQuw ?? true;
|
||||
WineQualityLevelInput.ItemsSource = WineQualityLevels
|
||||
.Where(q => q.MinKmw == null || q.MinKmw <= kmw)
|
||||
.Where(q => quw || q.QualId == "WEI" || q.QualId == max)
|
||||
.ToList();
|
||||
var qual = GetWineQualityLevel(kmw, !quw ? max : null);
|
||||
SetDefaultValue(WineQualityLevelInput, qual);
|
||||
if (WineQualityLevelInput.SelectedItem == null || (WineQualityLevelInput.SelectedItem is WineQualLevel selected && !selected.IsPredicate)) {
|
||||
ControlUtils.SelectItem(WineQualityLevelInput, qual);
|
||||
@@ -1280,42 +1333,73 @@ namespace Elwig.Windows {
|
||||
|
||||
private void ModifiersInput_SelectionChanged(object sender, SelectionChangedEventArgs evt) {
|
||||
if (!IsEditing && !IsCreating) return;
|
||||
var mod = ModifiersInput.SelectedItems.Cast<Modifier>().ToList();
|
||||
var mod = ViewModel.Modifiers.ToList();
|
||||
var source = ViewModel.ModifiersSource.ToList();
|
||||
if (App.Client.IsMatzen) {
|
||||
var kl = mod.Where(m => m.Name.StartsWith("Klasse "));
|
||||
if (kl.Count() > 1) {
|
||||
App.MainDispatcher.BeginInvoke(() => {
|
||||
foreach (var r in kl.Take(kl.Count() - 1))
|
||||
ModifiersInput.SelectedItems.Remove(r);
|
||||
ViewModel.Modifiers.Remove(r);
|
||||
});
|
||||
}
|
||||
} else if (App.Client.IsWinzerkeller) {
|
||||
if (mod.Any(m => m.Name.Contains("Lesewagen"))) {
|
||||
ViewModel.IsLesewagen = true;
|
||||
} else {
|
||||
ViewModel.IsLesewagen = false;
|
||||
if (source.Any(m => m.Name.Contains("Lesewagen"))) {
|
||||
if (mod.Any(m => m.Name.Contains("Lesewagen"))) {
|
||||
ViewModel.IsUnloadingPumped = true;
|
||||
} else {
|
||||
ViewModel.IsUnloadingPumped = false;
|
||||
}
|
||||
}
|
||||
} else if (App.Client.IsBaden) {
|
||||
if (source.Any(m => m.Name.Contains("Kiste"))) {
|
||||
if (mod.Any(m => m.Name.Contains("Kiste"))) {
|
||||
ViewModel.IsUnloadingBox = true;
|
||||
} else {
|
||||
ViewModel.IsUnloadingBox = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LesewagenInput_Changed(object sender, RoutedEventArgs evt) {
|
||||
private void UnloadingInput_Checked(object sender, RoutedEventArgs evt) {
|
||||
if (!IsEditing && !IsCreating) return;
|
||||
var mod = ModifiersInput.SelectedItems.Cast<Modifier>().ToList();
|
||||
var source = ModifiersInput.ItemsSource.Cast<Modifier>().ToList();
|
||||
var lw = LesewagenInput.IsChecked == true;
|
||||
var mod = ViewModel.Modifiers.ToList();
|
||||
var source = ViewModel.ModifiersSource.ToList();
|
||||
if (App.Client.IsMatzen) {
|
||||
var kl = mod.Where(m => m.Name.StartsWith("Klasse ")).Select(m => m.ModId).LastOrDefault("A")[0];
|
||||
if (lw) kl++; else kl--;
|
||||
var newKl = source.FirstOrDefault(m => m.ModId == kl.ToString());
|
||||
if (newKl != null) ModifiersInput.SelectedItems.Add(newKl);
|
||||
var kl = mod.Where(m => m.Name.StartsWith("Klasse ")).Select(m => m.ModId).LastOrDefault("_")[0];
|
||||
if (ViewModel.IsUnloadingPumped && (kl == 'A' || kl == '_')) {
|
||||
kl = 'B';
|
||||
} else if (ViewModel.IsUnloadingDumper && kl == '_') {
|
||||
kl = 'A';
|
||||
} else {
|
||||
kl = '_';
|
||||
}
|
||||
var newKl = source.FirstOrDefault(m => m?.ModId == kl.ToString(), null);
|
||||
if (newKl != null) ViewModel.Modifiers.Add(newKl);
|
||||
} else if (App.Client.IsWinzerkeller) {
|
||||
if (lw && !mod.Any(m => m.Name.Contains("Lesewagen"))) {
|
||||
ModifiersInput.SelectedItems.Add(source.First(m => m.Name.Contains("Lesewagen")));
|
||||
} else if (!lw && mod.Any(m => m.Name.Contains("Lesewagen"))) {
|
||||
ModifiersInput.SelectedItems.Remove(mod.First(m => m.Name.Contains("Lesewagen")));
|
||||
if (source.Any(m => m.Name.Contains("Lesewagen"))) {
|
||||
if (ViewModel.IsUnloadingPumped && !mod.Any(m => m.Name.Contains("Lesewagen"))) {
|
||||
ViewModel.Modifiers.Add(source.First(m => m.Name.Contains("Lesewagen")));
|
||||
} else if (!ViewModel.IsUnloadingPumped && mod.Any(m => m.Name.Contains("Lesewagen"))) {
|
||||
ViewModel.Modifiers.Remove(mod.First(m => m.Name.Contains("Lesewagen")));
|
||||
}
|
||||
}
|
||||
} else if (App.Client.IsBaden) {
|
||||
if (source.Any(m => m.Name.Contains("Kiste"))) {
|
||||
if (ViewModel.IsUnloadingBox && !mod.Any(m => m.Name.Contains("Kiste"))) {
|
||||
ViewModel.Modifiers.Add(source.First(m => m.Name.Contains("Kiste")));
|
||||
} else if (!ViewModel.IsUnloadingBox && mod.Any(m => m.Name.Contains("Kiste"))) {
|
||||
ViewModel.Modifiers.Remove(mod.First(m => m.Name.Contains("Kiste")));
|
||||
}
|
||||
}
|
||||
}
|
||||
CheckBox_Changed(sender, evt);
|
||||
RadioButton_Changed(sender, evt);
|
||||
}
|
||||
|
||||
private void UnloadingInput_Unchecked(object sender, RoutedEventArgs evt) {
|
||||
if (!IsEditing && !IsCreating) return;
|
||||
RadioButton_Changed(sender, evt);
|
||||
}
|
||||
|
||||
private void UpdateWineOrigin() {
|
||||
@@ -1366,8 +1450,7 @@ namespace Elwig.Windows {
|
||||
AbgewertetInput.IsChecked = false;
|
||||
return;
|
||||
}
|
||||
using var ctx = new AppDbContext();
|
||||
var defQual = ctx.GetWineQualityLevel(double.Parse(GradationKmwInput.Text)).GetAwaiter().GetResult();
|
||||
var defQual = GetWineQualityLevel(ViewModel.GradationKmw!.Value, !(ViewModel.WineVar?.IsQuw ?? true) ? ViewModel.WineVar?.MaxQualId : null);
|
||||
AbgewertetInput.IsChecked = !qual.IsPredicate && defQual != qual;
|
||||
}
|
||||
|
||||
@@ -1389,17 +1472,17 @@ namespace Elwig.Windows {
|
||||
}
|
||||
|
||||
private void GerebeltGewogenInput_Changed(object sender, RoutedEventArgs evt) {
|
||||
if ((IsEditing || IsCreating) && !App.Client.HasNetWeighing(ViewModel.Branch)) {
|
||||
if ((IsEditing || IsCreating) && App.Config.WeighingMode != WeighingMode.Net) {
|
||||
HandPickedInput.IsChecked = !GerebeltGewogenInput.IsChecked;
|
||||
}
|
||||
if (!ViewModel.IsReceipt || App.Client.HasNetWeighing(ViewModel.Branch)) {
|
||||
if (!ViewModel.IsReceipt || App.Config.WeighingMode == WeighingMode.Net) {
|
||||
GerebeltGewogenInput.IsChecked ??= false;
|
||||
}
|
||||
CheckBox_Changed(sender, evt);
|
||||
}
|
||||
|
||||
private void HandPickedInput_Changed(object sender, RoutedEventArgs evt) {
|
||||
if ((IsEditing || IsCreating) && !App.Client.HasNetWeighing(ViewModel.Branch)) {
|
||||
if ((IsEditing || IsCreating) && App.Config.WeighingMode != WeighingMode.Net) {
|
||||
GerebeltGewogenInput.IsChecked = !HandPickedInput.IsChecked;
|
||||
}
|
||||
CheckBox_Changed(sender, evt);
|
||||
|
||||
@@ -83,14 +83,19 @@ namespace Elwig.Windows {
|
||||
|
||||
private async Task RefreshDeliveryScheduleList() {
|
||||
using var ctx = new AppDbContext();
|
||||
var deliverySchedules = await ctx.DeliverySchedules
|
||||
var list = await ctx.DeliverySchedules
|
||||
.Where(s => s.Year == ViewModel.FilterSeason)
|
||||
.Include(s => s.Branch)
|
||||
.Include(s => s.Announcements)
|
||||
.OrderBy(s => s.DateString)
|
||||
.ThenBy(s => s.Branch.Name)
|
||||
.ThenBy(s => s.Description)
|
||||
.Select(s => new {
|
||||
Schedule = s,
|
||||
AnnouncedWeight = s.Announcements.Sum(a => a.Weight)
|
||||
})
|
||||
.ToListAsync();
|
||||
list.ForEach(v => v.Schedule.AnnouncedWeightOverride = v.AnnouncedWeight);
|
||||
var deliverySchedules = list.Select(v => v.Schedule).ToList();
|
||||
ControlUtils.RenewItemsSource(DeliveryScheduleList, deliverySchedules
|
||||
.Where(s => !ViewModel.FilterOnlyUpcoming || s.DateString.CompareTo(Utils.Today.ToString("yyyy-MM-dd")) >= 0)
|
||||
.ToList(), DeliveryScheduleList_SelectionChanged, ViewModel.FilterFromAllSchedules ? ControlUtils.RenewSourceDefault.None : ControlUtils.RenewSourceDefault.First);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Elwig.Helpers;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Elwig.Windows {
|
||||
public partial class LogWindow : Window {
|
||||
@@ -11,28 +13,34 @@ namespace Elwig.Windows {
|
||||
WindowState = WindowState.Maximized;
|
||||
}
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs evt) {
|
||||
var log = Utils.GetLogEntries();
|
||||
EventList.ItemsSource = log
|
||||
.Select(e => new {
|
||||
Event = e,
|
||||
Lines = e.Message.Split('\n').ToArray(),
|
||||
})
|
||||
.Select(e => new {
|
||||
e.Event,
|
||||
Exception = e.Lines.FirstOrDefault(l => l.StartsWith("Exception Info: "))?[16..].Trim().Split(':', 2),
|
||||
Location = e.Lines.FirstOrDefault(l => l.StartsWith(" at Elwig."))?[5..].Trim(),
|
||||
})
|
||||
.Select(e => new {
|
||||
e.Event,
|
||||
e.Exception,
|
||||
ExceptionName = e.Exception?[0].Trim(),
|
||||
ExceptionMessage = e.Exception?.Length >= 2 ? e.Exception?[1].Trim() : null,
|
||||
e.Location,
|
||||
})
|
||||
.OrderByDescending(e => e.Event.TimeGenerated)
|
||||
.ToList();
|
||||
EventList.SelectedIndex = 0;
|
||||
private async void Window_Loaded(object sender, RoutedEventArgs evt) {
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
var list = Utils.GetLogEntries()
|
||||
.Select(e => new {
|
||||
Event = e,
|
||||
Lines = e.Message.Split('\n').ToArray(),
|
||||
})
|
||||
.Select(e => new {
|
||||
e.Event,
|
||||
Exception = e.Lines.FirstOrDefault(l => l.StartsWith("Exception Info: "))?[16..].Trim().Split(':', 2),
|
||||
Location = e.Lines.FirstOrDefault(l => l.StartsWith(" at Elwig."))?[5..].Trim(),
|
||||
})
|
||||
.Select(e => new {
|
||||
e.Event,
|
||||
e.Exception,
|
||||
ExceptionName = e.Exception?[0].Trim(),
|
||||
ExceptionMessage = e.Exception?.Length >= 2 ? e.Exception?[1].Trim() : null,
|
||||
e.Location,
|
||||
})
|
||||
.OrderByDescending(e => e.Event.TimeGenerated)
|
||||
.ToList();
|
||||
await App.MainDispatcher.BeginInvoke(() => {
|
||||
EventList.ItemsSource = list;
|
||||
EventList.SelectedIndex = 0;
|
||||
});
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
|
||||
private void EventList_SelectionChanged(object sender, RoutedEventArgs evt) {
|
||||
|
||||
@@ -302,6 +302,9 @@
|
||||
<Button x:Name="GenerateButton" Content="Generieren"
|
||||
Grid.Row="0" Grid.Column="0" FontSize="14"
|
||||
Click="GenerateButton_Click"/>
|
||||
<Button x:Name="AbortButton" Content="Abbrechen" Visibility="Hidden" IsEnabled="False"
|
||||
Grid.Row="0" Grid.Column="0" FontSize="14"
|
||||
Click="AbortButton_Click"/>
|
||||
<ProgressBar x:Name="ProgressBar"
|
||||
Grid.Row="2" Grid.Column="0" SnapsToDevicePixels="True"/>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -61,6 +62,8 @@ namespace Elwig.Windows {
|
||||
protected Dictionary<Member, List<Document>>? PrintMemberDocuments;
|
||||
protected Dictionary<Member, List<Document>>? EmailDocuments;
|
||||
|
||||
private CancellationTokenSource? CancelGeneration;
|
||||
|
||||
public static readonly DependencyProperty PostalAllCountProperty = DependencyProperty.Register(nameof(PostalAllCount), typeof(int), typeof(MailWindow));
|
||||
public int PostalAllCount {
|
||||
get => (int)GetValue(PostalAllCountProperty);
|
||||
@@ -594,20 +597,32 @@ namespace Elwig.Windows {
|
||||
}
|
||||
|
||||
private void Window_Closed(object sender, EventArgs evt) {
|
||||
CancelGeneration?.Dispose();
|
||||
DisposeDocs();
|
||||
}
|
||||
|
||||
private async void AbortButton_Click(object sender, RoutedEventArgs evt) {
|
||||
AbortButton.IsEnabled = false;
|
||||
CancelGeneration?.Cancel();
|
||||
}
|
||||
|
||||
private async void GenerateButton_Click(object sender, RoutedEventArgs evt) {
|
||||
LockInputs();
|
||||
PreviewButton.IsEnabled = false;
|
||||
PrintButton.IsEnabled = false;
|
||||
EmailButton.IsEnabled = false;
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
AbortButton.IsEnabled = true;
|
||||
AbortButton.Visibility = Visibility.Visible;
|
||||
GenerateButton.IsEnabled = false;
|
||||
GenerateButton.Visibility = Visibility.Hidden;
|
||||
|
||||
DisposeDocs();
|
||||
await UpdateClientParameters();
|
||||
|
||||
CancelGeneration?.Dispose();
|
||||
CancelGeneration = new();
|
||||
|
||||
using var ctx = new AppDbContext();
|
||||
|
||||
var doublePaged = DoublePagedInput.IsChecked == true;
|
||||
@@ -662,6 +677,9 @@ namespace Elwig.Windows {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
UnlockInputs();
|
||||
GenerateButton.IsEnabled = true;
|
||||
GenerateButton.Visibility = Visibility.Visible;
|
||||
AbortButton.IsEnabled = false;
|
||||
AbortButton.Visibility = Visibility.Hidden;
|
||||
Mouse.OverrideCursor = null;
|
||||
return;
|
||||
}
|
||||
@@ -679,6 +697,9 @@ namespace Elwig.Windows {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
UnlockInputs();
|
||||
GenerateButton.IsEnabled = true;
|
||||
GenerateButton.Visibility = Visibility.Visible;
|
||||
AbortButton.IsEnabled = false;
|
||||
AbortButton.Visibility = Visibility.Hidden;
|
||||
Mouse.OverrideCursor = null;
|
||||
return;
|
||||
}
|
||||
@@ -733,12 +754,27 @@ namespace Elwig.Windows {
|
||||
}).ToList()
|
||||
}).ToList();
|
||||
|
||||
var hasPreviewDocs = memberDocs.Any(m => m.Docs.Any(d => d.Doc.IsPreview));
|
||||
if (hasPreviewDocs) {
|
||||
var res = MessageBox.Show("Einige der ausgewählten Dokumente sind nur als vorläufig zu betrachten und können daher nicht verschickt/ausgedruckt werden!\n\nDies könnte an einer noch nicht festgesetzen Auszahlungsvariante liegen.\n\nSoll mit dem Generieren fortgefahren werden?",
|
||||
"Vorläufige Dokumente", MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel);
|
||||
if (res != MessageBoxResult.OK) {
|
||||
UnlockInputs();
|
||||
GenerateButton.IsEnabled = true;
|
||||
GenerateButton.Visibility = Visibility.Visible;
|
||||
AbortButton.IsEnabled = false;
|
||||
AbortButton.Visibility = Visibility.Hidden;
|
||||
Mouse.OverrideCursor = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var printMode = PostalAllInput.IsChecked == true ? 3 :
|
||||
PostalWishInput.IsChecked == true ? 2 :
|
||||
PostalNoEmailInput.IsChecked == true ? 1 : 0;
|
||||
var emailMode = EmailAllInput.IsChecked == true ? 2 : EmailWishInput.IsChecked == true ? 1 : 0;
|
||||
|
||||
double printNum = printMode == 3 ? PostalAllCount : printMode == 2 ? PostalWishCount : printMode == 2 ? PostalNoEmailCount : 0;
|
||||
double printNum = printMode == 3 ? PostalAllCount : printMode == 2 ? PostalWishCount : printMode == 1 ? PostalNoEmailCount : 0;
|
||||
double emailNum = emailMode == 2 ? EmailAllCount : emailMode == 1 ? EmailWishCount : 0;
|
||||
double totalNum = printNum + emailNum;
|
||||
|
||||
@@ -747,7 +783,7 @@ namespace Elwig.Windows {
|
||||
.ToDictionary(d => d.Member, m => {
|
||||
var docs = m.Docs.Select(d => d.Doc).ToList();
|
||||
foreach (var doc in docs) {
|
||||
doc!.DoublePaged = false;
|
||||
doc!.IsDoublePaged = false;
|
||||
if (doc is BusinessDocument b) {
|
||||
b.IncludeSender = false;
|
||||
b.Location = location;
|
||||
@@ -759,7 +795,7 @@ namespace Elwig.Windows {
|
||||
try {
|
||||
foreach (var item1 in email.Select((e, i) => new { Index = i, e.Key, e.Value })) {
|
||||
foreach (var item2 in item1.Value.Select((d, i) => new { Index = i, Doc = d })) {
|
||||
await item2.Doc.Generate(new Progress<double>(v => {
|
||||
await item2.Doc.Generate(CancelGeneration.Token, new Progress<double>(v => {
|
||||
ProgressBar.Value = v * (item2.Index + 1) / item1.Value.Count / totalNum + 100.0 * item1.Index / totalNum;
|
||||
}));
|
||||
}
|
||||
@@ -768,6 +804,9 @@ namespace Elwig.Windows {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
UnlockInputs();
|
||||
GenerateButton.IsEnabled = true;
|
||||
GenerateButton.Visibility = Visibility.Visible;
|
||||
AbortButton.IsEnabled = false;
|
||||
AbortButton.Visibility = Visibility.Hidden;
|
||||
Mouse.OverrideCursor = null;
|
||||
return;
|
||||
}
|
||||
@@ -788,7 +827,7 @@ namespace Elwig.Windows {
|
||||
docs.Insert(0, new Letterhead(m.Member));
|
||||
}
|
||||
docs.ForEach(doc => {
|
||||
doc.DoublePaged = doublePaged;
|
||||
doc.IsDoublePaged = doublePaged;
|
||||
if (doc is BusinessDocument b)
|
||||
b.Location = location;
|
||||
});
|
||||
@@ -801,8 +840,8 @@ namespace Elwig.Windows {
|
||||
if (printDocs.Count > 0) {
|
||||
try {
|
||||
var print = Document.Merge(printDocs);
|
||||
print.DoublePaged = doublePaged;
|
||||
await print.Generate(new Progress<double>(v => {
|
||||
print.IsDoublePaged = doublePaged;
|
||||
await print.Generate(CancelGeneration.Token, new Progress<double>(v => {
|
||||
ProgressBar.Value = 100.0 * emailNum / totalNum + v * printNum / totalNum;
|
||||
}));
|
||||
PrintDocument = print;
|
||||
@@ -811,6 +850,9 @@ namespace Elwig.Windows {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
UnlockInputs();
|
||||
GenerateButton.IsEnabled = true;
|
||||
GenerateButton.Visibility = Visibility.Visible;
|
||||
AbortButton.IsEnabled = false;
|
||||
AbortButton.Visibility = Visibility.Hidden;
|
||||
Mouse.OverrideCursor = null;
|
||||
return;
|
||||
}
|
||||
@@ -822,10 +864,13 @@ namespace Elwig.Windows {
|
||||
|
||||
UnlockInputs();
|
||||
GenerateButton.IsEnabled = true;
|
||||
GenerateButton.Visibility = Visibility.Visible;
|
||||
AbortButton.IsEnabled = false;
|
||||
AbortButton.Visibility = Visibility.Hidden;
|
||||
Mouse.OverrideCursor = null;
|
||||
PreviewButton.IsEnabled = true;
|
||||
PrintButton.IsEnabled = PrintDocument != null;
|
||||
EmailButton.IsEnabled = EmailDocuments != null && App.Config.Smtp != null;
|
||||
PrintButton.IsEnabled = PrintDocument != null && !hasPreviewDocs;
|
||||
EmailButton.IsEnabled = EmailDocuments != null && !hasPreviewDocs && App.Config.Smtp != null;
|
||||
}
|
||||
|
||||
private async void PreviewButton_Click(object sender, RoutedEventArgs evt) {
|
||||
@@ -929,13 +974,13 @@ namespace Elwig.Windows {
|
||||
await Utils.AddSentMails([(
|
||||
"email", m.MgNr, m.AdministrativeName,
|
||||
m.EmailAddresses.OrderBy(a => a.Nr).Select(a => a.Address).ToArray(),
|
||||
subject,
|
||||
docs.Select(d => d.Title).ToArray()
|
||||
subject, docs.Select(d => d.Title).ToArray()
|
||||
)]);
|
||||
}
|
||||
});
|
||||
|
||||
MessageBox.Show("Erfolgreich alle E-Mails verschickt!", "Rundschreiben verschicken", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
MessageBox.Show("Erfolgreich alle E-Mails verschickt!\n\nEs kann einige Minuten dauern, bis die E-Mails in den Posteingängen der Empfänger aufscheinen.", "Rundschreiben verschicken",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} finally {
|
||||
@@ -954,7 +999,7 @@ namespace Elwig.Windows {
|
||||
AvaiableDocumentsList.SelectedIndex = 1;
|
||||
if (AvaiableDocumentsList.SelectedItem is not string s || SelectedDocs.Any(d => d.Type == DocType.DeliveryConfirmation))
|
||||
return;
|
||||
SelectedDocs.Add(new(DocType.DeliveryConfirmation, s, (Year, DocumentNonDeliverersInput.IsChecked == true)));
|
||||
SelectedDocs.Add(new(DocType.DeliveryConfirmation, s, Year));
|
||||
SelectedDocumentsList.SelectedIndex = SelectedDocs.Count - 1;
|
||||
RecipientsDeliveryMembersInput.IsChecked = true;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,17 @@
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
<MenuItem Header="Datenbank sichern..." Click="Menu_Database_Backup_Click">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" FontSize="16" Text=""/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Datenbank wiederherstellen..." Click="Menu_Database_Restore_Click">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" FontSize="16" Text=""/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
<MenuItem Header="Abfragen stellen" Click="Menu_Database_Query_Click">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" FontSize="16" Text=""/>
|
||||
@@ -41,6 +52,34 @@
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" FontSize="16" Text=""/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
<MenuItem x:Name="Menu_Database_Upload" Header="Datenbank hochladen..." Click="Menu_Database_Upload_Click">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" FontSize="16" Text=""/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="Menu_Database_Download" Header="Datenbank herunterladen..." Click="Menu_Database_Download_Click" IsEnabled="False">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" FontSize="16" Text=""/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Synchronisieren" x:Name="Menu_Sync" IsEnabled="false">
|
||||
<MenuItem x:Name="Menu_Sync_Download" Header="Mitgliederdaten und Lieferungen herunterladen" Click="Menu_Sync_Download_Click">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" FontSize="16" Text=""/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="Menu_Sync_UploadBranchDeliveries" Header="Lieferungen dieser Saison/Zweigstelle hochladen" Click="Menu_Sync_UploadBranchDeliveries_Click">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" FontSize="16" Text=""/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="Menu_Sync_UploadModified" Header="Geänderte Mitglieder und Lieferungen hochladen" Click="Menu_Sync_UploadModified_Click">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" FontSize="16" Text=""/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Waage">
|
||||
<MenuItem Header="Datum und Uhrzeit setzen" Click="Menu_Scale_SetDateTime_Click">
|
||||
@@ -50,7 +89,7 @@
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="HelpMenu" Header="Hilfe">
|
||||
<MenuItem Header="Über">
|
||||
<MenuItem Header="Über" Click="Menu_Help_About_Click">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock FontFamily="Segoe MDL2 Assets" FontSize="16" Text=""/>
|
||||
</MenuItem.Icon>
|
||||
@@ -152,16 +191,18 @@
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="DownloadButton" Click="DownloadButton_Click"
|
||||
Margin="310,135,0,0" Padding="0.375,0.5,0,0" Height="30" Width="30"
|
||||
Content="" FontFamily="Segoe MDL2 Assets" FontSize="16"
|
||||
HorizontalContentAlignment="Center"
|
||||
ToolTip="Lieferungen und Mitgliederdaten anderer Zweigstellen herunterladen"/>
|
||||
<Button x:Name="UploadButton" Click="UploadButton_Click"
|
||||
<Button x:Name="SyncButton" Click="SyncButton_Click"
|
||||
Margin="375,135,0,0" Padding="1.0,0.5,0,0" Height="30" Width="30"
|
||||
Content="" FontFamily="Segoe MDL2 Assets" FontSize="16"
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="16"
|
||||
HorizontalContentAlignment="Center"
|
||||
ToolTip="Lieferungen dieser Zweigstelle hochladen"/>
|
||||
ToolTip="Geänderte Mitgliederdaten und Lieferungen synchronisieren">
|
||||
<Button.Content>
|
||||
<Grid TextElement.FontFamily="Segoe MDL2 Assets">
|
||||
<TextBlock x:Name="SyncButton_1" Text=""/>
|
||||
<TextBlock x:Name="SyncButton_2" Text="" Foreground="DarkOrange"/>
|
||||
</Grid>
|
||||
</Button.Content>
|
||||
</Button>
|
||||
|
||||
<Expander x:Name="SeasonFinish" Header="Leseabschluss" SnapsToDevicePixels="True"
|
||||
Expanded="SeasonFinish_Expanded" Collapsed="SeasonFinish_Collapsed"
|
||||
|
||||
@@ -2,26 +2,30 @@ using Elwig.Helpers;
|
||||
using Elwig.Helpers.Billing;
|
||||
using Elwig.Helpers.Export;
|
||||
using Elwig.Models.Dtos;
|
||||
using Elwig.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Elwig.Windows {
|
||||
public partial class MainWindow : ContextWindow {
|
||||
|
||||
private readonly DispatcherTimer _syncTimer = new() { Interval = TimeSpan.FromHours(1) };
|
||||
|
||||
public MainWindow() {
|
||||
InitializeComponent();
|
||||
var v = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
@@ -29,12 +33,21 @@ namespace Elwig.Windows {
|
||||
if (App.Client.Client == null) VersionField.Text += " (Unbekannt)";
|
||||
Menu_Help_Update.IsEnabled = App.Config.UpdateUrl != null;
|
||||
Menu_Help_Smtp.IsEnabled = App.Config.Smtp != null;
|
||||
DownloadButton.Visibility = App.Config.SyncUrl != null ? Visibility.Visible : Visibility.Hidden;
|
||||
UploadButton.Visibility = App.Config.SyncUrl != null ? Visibility.Visible : Visibility.Hidden;
|
||||
Menu_Sync.IsEnabled = App.Config.SyncUrl != null;
|
||||
SyncButton.Visibility = App.Config.SyncUrl != null ? Visibility.Visible : Visibility.Hidden;
|
||||
Menu_Database_Upload.IsEnabled = App.Config.SyncUrl != null;
|
||||
Menu_Database_Download.IsEnabled = App.Config.SyncUrl != null;
|
||||
}
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs evt) {
|
||||
SeasonInput.Value = Utils.CurrentLastSeason;
|
||||
|
||||
if (Utils.HasInternetConnectivity()) {
|
||||
CheckSync(200);
|
||||
}
|
||||
NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
|
||||
_syncTimer.Tick += new EventHandler(OnSyncTimer);
|
||||
_syncTimer.Start();
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, CancelEventArgs evt) {
|
||||
@@ -57,6 +70,11 @@ namespace Elwig.Windows {
|
||||
}
|
||||
}
|
||||
|
||||
private void Menu_Help_About_Click(object sender, RoutedEventArgs evt) {
|
||||
var w = new AboutWindow();
|
||||
w.Show();
|
||||
}
|
||||
|
||||
private async void Menu_Help_Update_Click(object sender, RoutedEventArgs evt) {
|
||||
await App.CheckForUpdates(true);
|
||||
}
|
||||
@@ -144,7 +162,92 @@ namespace Elwig.Windows {
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
|
||||
private async void DownloadButton_Click(object sender, RoutedEventArgs evt) {
|
||||
private async void Menu_Database_Backup_Click(object sender, RoutedEventArgs evt) {
|
||||
try {
|
||||
var d = new SaveFileDialog() {
|
||||
Title = "Datenbank sichern - Elwig",
|
||||
FileName = $"database_{Utils.Today:yyyy-MM-dd}.sql.zip",
|
||||
DefaultExt = "sql.zip",
|
||||
Filter = "Komprimierte SQL-Datei (*.sql.zip)|*.sql.zip",
|
||||
AddExtension = false,
|
||||
};
|
||||
if (d.ShowDialog() == true) {
|
||||
if (!d.FileName.EndsWith(".sql.zip")) d.FileName += ".sql.zip";
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
await Database.ExportSql(d.FileName, true);
|
||||
});
|
||||
}
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
|
||||
private async void Menu_Database_Restore_Click(object sender, RoutedEventArgs evt) {
|
||||
try {
|
||||
var d = new OpenFileDialog() {
|
||||
Title = "Datenbank wiederherstellen - Elwig",
|
||||
DefaultExt = "sql.zip",
|
||||
Filter = "SQLite-Datenbank (*.sqlite3, *.sqlite3.zip, *.sql, *.sql.zip)|*.sqlite3;*.sqlite3.zip;*.sql;*.sql.zip",
|
||||
};
|
||||
if (d.ShowDialog() == true) {
|
||||
var res = MessageBox.Show("Soll die Datenbank wirklich unwiederruflich durch die wiederhergestellte Version ersetzt werden?", "Datenbank wiederherstellen",
|
||||
MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel);
|
||||
if (res != MessageBoxResult.OK)
|
||||
return;
|
||||
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await App.ReplaceDatabase(d.FileName);
|
||||
}
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
|
||||
private async void SyncButton_Click(object sender, RoutedEventArgs evt) {
|
||||
if (App.Config.SyncUrl == null)
|
||||
return;
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
await SyncService.Download(App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword);
|
||||
await SyncService.UploadModified(App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword);
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
|
||||
private async void Menu_Sync_Download_Click(object sender, RoutedEventArgs evt) {
|
||||
if (App.Config.SyncUrl == null)
|
||||
return;
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
await SyncService.Download(App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword);
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
|
||||
private async void Menu_Sync_UploadBranchDeliveries_Click(object sender, RoutedEventArgs evt) {
|
||||
if (App.Config.SyncUrl == null)
|
||||
return;
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
await SyncService.UploadBranchDeliveries(App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword);
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
|
||||
private async void Menu_Sync_UploadModified_Click(object sender, RoutedEventArgs evt) {
|
||||
if (App.Config.SyncUrl == null)
|
||||
return;
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
await SyncService.UploadModified(App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword);
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
|
||||
private async void Menu_Database_Download_Click(object sender, RoutedEventArgs evt) {
|
||||
if (App.Config.SyncUrl == null)
|
||||
return;
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
@@ -154,72 +257,74 @@ namespace Elwig.Windows {
|
||||
var files = data
|
||||
.Select(f => new {
|
||||
Name = f!["name"]!.AsValue().GetValue<string>(),
|
||||
Timestamp = f!["timestamp"] != null && DateTime.TryParseExact(f!["timestamp"]!.AsValue().GetValue<string>(), "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : (DateTime?)null,
|
||||
ZwstId = f!["meta"]?["zwstid"]?.AsValue().GetValue<string>() ?? f!["zwstid"]?.AsValue().GetValue<string>(),
|
||||
Device = f!["meta"]?["device"]!.AsValue().GetValue<string>(),
|
||||
Timestamp = f!["modified"] != null && DateTime.TryParseExact(f!["modified"]!.AsValue().GetValue<string>(), "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : (DateTime?)null,
|
||||
Url = f!["url"]!.AsValue().GetValue<string>(),
|
||||
Size = f!["size"]!.AsValue().GetValue<long>(),
|
||||
})
|
||||
.Where(f => f.Timestamp >= new DateTime(Utils.CurrentLastSeason, 7, 1))
|
||||
.Where(f => f.Name.StartsWith("database.") && f.Name.EndsWith(".zip"))
|
||||
.OrderBy(f => f.Size)
|
||||
.ToList();
|
||||
|
||||
var imported = await ElwigData.GetImportedFiles();
|
||||
var import = files
|
||||
.Where(f => f.Device != Environment.MachineName && !imported.Contains(f.Name))
|
||||
.ToList();
|
||||
var paths = new List<string>();
|
||||
using (var client = Utils.GetHttpClient(App.Config.SyncUsername, App.Config.SyncPassword)) {
|
||||
foreach (var f in import) {
|
||||
var filename = Path.Combine(App.TempPath, f.Name);
|
||||
using var stream = new FileStream(filename, FileMode.Create);
|
||||
await client.DownloadAsync(f.Url, stream);
|
||||
paths.Add(filename);
|
||||
}
|
||||
if (files.Count == 0) {
|
||||
MessageBox.Show("Die Datenbank wurde noch nicht vom Hauptgerät hochgeladen!", "Datenbank herunterladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
await ElwigData.Import(paths, ElwigData.ImportMode.FromBranches);
|
||||
var file = files[0];
|
||||
|
||||
var res = MessageBox.Show($"Es wurde eine komprimierte Datenbank (ca. {file.Size / 1024 / 1024} MB) vom {file.Timestamp:dd.MM.yyyy, HH:mm} gefunden.\n\nWollen Sie wirklich die aktuelle Datenbank unwiederruflich\nlöschen und durch die gefundene ersetzen?\n\nDas kann zu Datenverlust führen!", "Datenbank herunterladen",
|
||||
MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel);
|
||||
if (res != MessageBoxResult.OK)
|
||||
return;
|
||||
|
||||
var filename = Path.Combine(App.TempPath, file.Name);
|
||||
using (var client = Utils.GetHttpClient(App.Config.SyncUsername, App.Config.SyncPassword)) {
|
||||
using var stream = new FileStream(filename, FileMode.Create);
|
||||
await client.DownloadAsync(file.Url, stream);
|
||||
}
|
||||
|
||||
res = MessageBox.Show("Die Datenbank wurde erfolgreich heruntergeladen!\n\nSoll die Datenbank wirklich unwiederruflich ersetzt werden?\n\nWenn Sie unsicher sind sprechen Sie sich mit dem Benutzer des Hauptgerätes ab!", "Datenbank herunterladen",
|
||||
MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel);
|
||||
if (res != MessageBoxResult.OK)
|
||||
return;
|
||||
|
||||
await App.MainDispatcher.BeginInvoke(async () => {
|
||||
await App.ReplaceDatabase(filename);
|
||||
});
|
||||
} catch (HttpRequestException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Datenbank herunterladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (TaskCanceledException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Datenbank herunterladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show(exc.Message, "Datenbank herunterladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
|
||||
private async void UploadButton_Click(object sender, RoutedEventArgs evt) {
|
||||
private async void Menu_Database_Upload_Click(object sender, RoutedEventArgs evt) {
|
||||
if (App.Config.SyncUrl == null)
|
||||
return;
|
||||
|
||||
var res = MessageBox.Show("Sind Sie wirklich sicher, dass Sie die Datenbank dieses\nGerätes hochladen möchten? Das sollte nur vom Hauptgerät aus passieren!", "Datenbank hochladen",
|
||||
MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel);
|
||||
if (res != MessageBoxResult.OK)
|
||||
return;
|
||||
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
try {
|
||||
var path = Path.Combine(App.TempPath, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip");
|
||||
using var ctx = new AppDbContext();
|
||||
var deliveries = await ctx.Deliveries
|
||||
.Where(d => d.Year == Utils.CurrentLastSeason && d.ZwstId == App.ZwstId)
|
||||
.Include(d => d.Parts)
|
||||
.ThenInclude(p => p.PartModifiers)
|
||||
.OrderBy(d => d.DateString)
|
||||
.ThenBy(d => d.TimeString)
|
||||
.ThenBy(d => d.LsNr)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
if (deliveries.Count == 0) {
|
||||
MessageBox.Show("Es gibt keine Lieferungen, die hochgeladen werden können!", "Lieferungen hochladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} else {
|
||||
await ElwigData.Export(path, deliveries, [$"{Utils.CurrentLastSeason}", $"Zweigstelle {App.BranchName}"]);
|
||||
await Utils.UploadExportData(path, App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword);
|
||||
MessageBox.Show($"Hochladen von {deliveries.Count:N0} Lieferungen erfolgreich!", "Lieferungen hochladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
var path = Path.Combine(App.TempPath, "database.sql.zip");
|
||||
await Database.ExportSql(path, true);
|
||||
await Utils.UploadExportData(path, App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword);
|
||||
MessageBox.Show($"Hochladen der gesamten Datenbank erfolgreich!", "Datenbank hochladen",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
} catch (HttpRequestException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Datenbank hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (TaskCanceledException exc) {
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Datenbank hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
MessageBox.Show(exc.Message, "Datenbank hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
@@ -251,9 +356,42 @@ namespace Elwig.Windows {
|
||||
App.FocusMailWindow();
|
||||
}
|
||||
|
||||
protected override Task OnRenewContext(AppDbContext ctx) {
|
||||
protected async override Task OnRenewContext(AppDbContext ctx) {
|
||||
SeasonInput_TextChanged(null, null);
|
||||
return Task.CompletedTask;
|
||||
CheckSync();
|
||||
}
|
||||
|
||||
private void OnSyncTimer(object? sender, EventArgs? evt) {
|
||||
if (Utils.HasInternetConnectivity()) {
|
||||
CheckSync();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs evt) {
|
||||
if (!evt.IsAvailable) return;
|
||||
if (Utils.HasInternetConnectivity()) {
|
||||
CheckSync(1000);
|
||||
}
|
||||
}
|
||||
|
||||
private async void CheckSync(int delay = 0) {
|
||||
if (App.Config.SyncUrl == null) return;
|
||||
Utils.RunBackground("Daten Synchronisieren", async () => {
|
||||
await Task.Delay(delay);
|
||||
var ch = false;
|
||||
using (var ctx = new AppDbContext()) {
|
||||
ch = await SyncService.ChangesAvailable(ctx, App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword);
|
||||
}
|
||||
await App.MainDispatcher.BeginInvoke(() => {
|
||||
if (ch) {
|
||||
SyncButton_1.Text = "\uEA6A";
|
||||
SyncButton_2.Text = "\uEA81";
|
||||
} else {
|
||||
SyncButton_1.Text = "\uE895";
|
||||
SyncButton_2.Text = "";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void SeasonFinish_Expanded(object sender, RoutedEventArgs evt) {
|
||||
@@ -436,7 +574,7 @@ namespace Elwig.Windows {
|
||||
App.HintContextChange();
|
||||
|
||||
using var ctx = new AppDbContext();
|
||||
var tbl = await MemberDeliveryPerVarietyData.ForSeason(ctx.MemberDeliveryPerVariantRows, year);
|
||||
var tbl = await MemberDeliveryYieldsPerVarietyData.ForSeason(ctx.MemberDeliveryPerVariantRows, year);
|
||||
using var ods = new OdsFile(d.FileName);
|
||||
await ods.AddTable(tbl);
|
||||
} catch (Exception exc) {
|
||||
|
||||
@@ -164,6 +164,14 @@
|
||||
<MenuItem x:Name="Menu_Export_UploadAll" Header="...von allen Mitgliedern hochladen"
|
||||
Click="Menu_Export_UploadAll_Click"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Kontakte">
|
||||
<MenuItem x:Name="Menu_Contacts_Selected" Header="...von ausgewähltem Mitglied speichern..." IsEnabled="False"
|
||||
Click="Menu_Contacts_Selected_Click"/>
|
||||
<MenuItem x:Name="Menu_Contacts_Filters" Header="...aus Filtern speichern..."
|
||||
Click="Menu_Contacts_Filters_Click"/>
|
||||
<MenuItem x:Name="Menu_Contacts_All" Header="...von allen Mitgliedern speichern..."
|
||||
Click="Menu_Contacts_All_Click"/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<Grid Grid.Row="1" Margin="5,0,0,0">
|
||||
|
||||
@@ -319,9 +319,11 @@ namespace Elwig.Windows {
|
||||
if (MemberList.SelectedItem is Member m) {
|
||||
Menu_Export_ExportSelected.IsEnabled = !IsEditing && !IsCreating;
|
||||
Menu_Export_UploadSelected.IsEnabled = !IsEditing && !IsCreating && App.Config.SyncUrl != null;
|
||||
Menu_Contacts_Selected.IsEnabled = !IsEditing && !IsCreating;
|
||||
} else {
|
||||
Menu_Export_ExportSelected.IsEnabled = false;
|
||||
Menu_Export_UploadSelected.IsEnabled = false;
|
||||
Menu_Contacts_Selected.IsEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,6 +651,13 @@ namespace Elwig.Windows {
|
||||
await ViewModel.GenerateMemberList(MemberService.ExportSubject.Selected, ExportMode.Upload);
|
||||
}
|
||||
|
||||
private async void Menu_Contacts_All_Click(object sender, RoutedEventArgs evt) =>
|
||||
await ViewModel.GenerateMemberList(MemberService.ExportSubject.All, ExportMode.Vcf);
|
||||
private async void Menu_Contacts_Filters_Click(object sender, RoutedEventArgs evt) =>
|
||||
await ViewModel.GenerateMemberList(MemberService.ExportSubject.FromFilters, ExportMode.Vcf);
|
||||
private async void Menu_Contacts_Selected_Click(object sender, RoutedEventArgs evt) =>
|
||||
await ViewModel.GenerateMemberList(MemberService.ExportSubject.Selected, ExportMode.Vcf);
|
||||
|
||||
private async void Menu_List_Order_Click(object sender, RoutedEventArgs evt) {
|
||||
Menu_List.IsSubmenuOpen = true;
|
||||
if (sender == Menu_List_OrderMgNr) {
|
||||
|
||||
@@ -55,6 +55,9 @@ namespace Elwig.Windows {
|
||||
.OrderBy(v => v.AvNr)
|
||||
.Include(v => v.Season.Currency)
|
||||
.ToListAsync());
|
||||
if (PaymentVariantList.SelectedItem == null && PaymentVariantList.Items.Count > 0) {
|
||||
PaymentVariantList.SelectedIndex = PaymentVariantList.Items.Count - 1;
|
||||
}
|
||||
Update();
|
||||
}
|
||||
|
||||
@@ -119,6 +122,11 @@ namespace Elwig.Windows {
|
||||
private async void DeleteButton_Click(object sender, RoutedEventArgs evt) {
|
||||
if (PaymentVariantList.SelectedItem is not PaymentVar v || !v.TestVariant)
|
||||
return;
|
||||
var res = MessageBox.Show(
|
||||
$"Soll die Auszahlungsvariante \"{v.Name}\" wirklich unwiderruflich gelöscht werden?",
|
||||
"Auszahlungsvariante löschen", MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel);
|
||||
if (res != MessageBoxResult.OK)
|
||||
return;
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
try {
|
||||
await PaymentVariantService.DeletePaymentVariant(v.Year, v.AvNr);
|
||||
|
||||
@@ -5,27 +5,35 @@
|
||||
Title="Datenbankabfragen - Elwig" Height="450" Width="800" MinWidth="400" MinHeight="300">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="1*" MinHeight="100"/>
|
||||
<RowDefinition Height="5"/>
|
||||
<RowDefinition Height="3*" MinHeight="100"/>
|
||||
<RowDefinition Height="1*" MinHeight="50"/>
|
||||
<RowDefinition Height="5"/>
|
||||
<RowDefinition Height="1*" MinHeight="50"/>
|
||||
<RowDefinition Height="5"/>
|
||||
<RowDefinition Height="6*" MinHeight="100"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBox x:Name="QueryInput" Text="SELECT * FROM v_delivery"
|
||||
<TextBox x:Name="QueryInput" Text="SELECT * FROM v_member" Grid.Row="1" Grid.RowSpan="3"
|
||||
AcceptsReturn="True" VerticalScrollBarVisibility="Visible" TextWrapping="Wrap"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10,10,120,5"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10,5,120,5"
|
||||
FontFamily="Cascadia Code Light" FontSize="13">
|
||||
<TextBox.InputBindings>
|
||||
<KeyBinding Key="Return" Modifiers="Control" Command="{Binding EnterCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:QueryWindow}}}" />
|
||||
<KeyBinding Key="Return" Modifiers="Control" Command="{Binding EnterCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:QueryWindow}}}" />
|
||||
<KeyBinding Key="S" Modifiers="Control" Command="{Binding SaveCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:QueryWindow}}}" />
|
||||
</TextBox.InputBindings>
|
||||
</TextBox>
|
||||
<Button x:Name="QueryButton" Content="Abfragen"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Stretch" Margin="10,10,10,5"
|
||||
<Button x:Name="QueryButton" Content="Abfragen" Grid.Row="1"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Stretch" Margin="10,5,10,0"
|
||||
Click="QueryButton_Click" Width="100"
|
||||
FontSize="14"/>
|
||||
<Button x:Name="SaveButton" Content="Speichern" Grid.Row="3"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Stretch" Margin="10,0,10,5"
|
||||
Click="SaveButton_Click" Width="100"
|
||||
FontSize="14"/>
|
||||
|
||||
<GridSplitter Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
|
||||
<GridSplitter Grid.Row="4" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
|
||||
|
||||
<DataGrid x:Name="DataList" Grid.Row="2"
|
||||
<DataGrid x:Name="DataList" Grid.Row="5"
|
||||
AutoGenerateColumns="False" HeadersVisibility="Column" IsReadOnly="True" GridLinesVisibility="None" SelectionMode="Extended"
|
||||
CanUserDeleteRows="False" CanUserResizeRows="False" CanUserAddRows="False"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10,5,10,10"/>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using Elwig.Helpers;
|
||||
using Elwig.Helpers.Export;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -13,7 +16,12 @@ namespace Elwig.Windows {
|
||||
|
||||
private ICommand? _enterCommand;
|
||||
public ICommand EnterCommand => _enterCommand ??= new ActionCommand(async () => {
|
||||
await ExecuteQuery();
|
||||
await DisplayQuery();
|
||||
});
|
||||
|
||||
private ICommand? _saveCommand;
|
||||
public ICommand SaveCommand => _saveCommand ??= new ActionCommand(async () => {
|
||||
await SaveQuery();
|
||||
});
|
||||
|
||||
|
||||
@@ -22,33 +30,45 @@ namespace Elwig.Windows {
|
||||
}
|
||||
|
||||
private async void QueryButton_Click(object sender, RoutedEventArgs evt) {
|
||||
await ExecuteQuery();
|
||||
await DisplayQuery();
|
||||
}
|
||||
|
||||
private async Task ExecuteQuery() {
|
||||
private async void SaveButton_Click(object sender, RoutedEventArgs evt) {
|
||||
await SaveQuery();
|
||||
}
|
||||
|
||||
private async Task DisplayQuery() {
|
||||
try {
|
||||
await ExecuteQuery(QueryInput.Text);
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await DisplayQuery(QueryInput.Text);
|
||||
Mouse.OverrideCursor = null;
|
||||
} catch (Exception e) {
|
||||
Mouse.OverrideCursor = null;
|
||||
MessageBox.Show(e.Message, "Fehler beim Ausführen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteQuery(string sqlQuery) {
|
||||
private async Task SaveQuery() {
|
||||
await SaveQuery(QueryInput.Text);
|
||||
}
|
||||
|
||||
private static async Task<(IList<DbColumn>, IEnumerable<object[]>)> ExecuteQuery(string sqlQuery) {
|
||||
var rows = new List<object[]>();
|
||||
IList<DbColumn> header;
|
||||
|
||||
using (var cnx = await AppDbContext.ConnectAsync()) {
|
||||
var cmd = cnx.CreateCommand();
|
||||
cmd.CommandText = sqlQuery;
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
header = await reader.GetColumnSchemaAsync();
|
||||
while (await reader.ReadAsync()) {
|
||||
var values = new object[reader.FieldCount];
|
||||
reader.GetValues(values);
|
||||
rows.Add(values);
|
||||
}
|
||||
using var cnx = await AppDbContext.ConnectAsync();
|
||||
using var cmd = cnx.CreateCommand();
|
||||
cmd.CommandText = sqlQuery;
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
var header = await reader.GetColumnSchemaAsync();
|
||||
while (await reader.ReadAsync()) {
|
||||
var values = new object[reader.FieldCount];
|
||||
reader.GetValues(values);
|
||||
rows.Add(values);
|
||||
}
|
||||
return (header, rows);
|
||||
}
|
||||
|
||||
private async Task DisplayQuery(string sqlQuery) {
|
||||
var (header, rows) = await ExecuteQuery(sqlQuery);
|
||||
var styleRight = new Style();
|
||||
styleRight.Setters.Add(new Setter(TextBlock.TextAlignmentProperty, TextAlignment.Right));
|
||||
|
||||
@@ -63,5 +83,27 @@ namespace Elwig.Windows {
|
||||
}
|
||||
DataList.ItemsSource = rows;
|
||||
}
|
||||
|
||||
private static async Task SaveQuery(string sqlQuery) {
|
||||
var d = new SaveFileDialog() {
|
||||
FileName = $"Abfrage.csv",
|
||||
DefaultExt = "csv",
|
||||
Filter = "CSV-Datei (*.csv)|*.csv",
|
||||
Title = $"Datenbank Abfrage speichern unter - Elwig"
|
||||
};
|
||||
if (d.ShowDialog() == true) {
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
await Task.Run(async () => {
|
||||
try {
|
||||
var (header, rows) = await ExecuteQuery(sqlQuery);
|
||||
using var csv = new CsvSimple(d.FileName, ';', Utils.UTF8BOM);
|
||||
await csv.ExportAsync(rows.Prepend([.. header.Select(h => h.ColumnName)]));
|
||||
} catch (Exception exc) {
|
||||
MessageBox.Show(exc.Message, "Fehler beim Ausführen", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
});
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
40
README.md
40
README.md
@@ -5,3 +5,43 @@ Elwig
|
||||
**El**ektronische **Wi**nzer**g**enossenschaftsverwaltung (Electronic Management for Vintners' Cooperatives).
|
||||
|
||||
Further information may be found on [the website](https://elwig.at).
|
||||
|
||||
|
||||
About
|
||||
=====
|
||||
|
||||
**Product:** Elwig
|
||||
**Description:** Electronic Management for Vintners' Cooperatives
|
||||
**Type:** ERP system
|
||||
**Version:** 1.0.3.0 ([Changelog](./CHANGELOG.md))
|
||||
**License:** [GNU General Public License 3.0 (GPLv3)](./LICENSE)
|
||||
**Website:** https://elwig.at/
|
||||
**Source code:** https://git.necronda.net/winzer/elwig
|
||||
**Developement period:** 2022–2025
|
||||
|
||||
**Technology Stack:**
|
||||
Language: C#
|
||||
Framework: Windows Presentation Framework (WPF)
|
||||
Database: [SQLite](https://sqlite.org/)
|
||||
PDF creation: [WeasyPrint](https://weasyprint.org/), [RazorLight](https://github.com/toddams/RazorLight), [PdfiumViewer](https://github.com/pvginkel/PdfiumViewer)
|
||||
Packaging: [WiX Toolset](https://www.firegiant.com/wixtoolset/)
|
||||
|
||||
|
||||
Über
|
||||
====
|
||||
|
||||
**Produkt:** Elwig
|
||||
**Beschreibung:** Elektronische Winzergenossenschaftsverwaltung
|
||||
**Typ:** Warenwirtschaftssystem (ERP-System)
|
||||
**Version:** 1.0.3.0 ([Änderungsprotokoll](./CHANGELOG.md))
|
||||
**Lizenz:** [GNU General Public License 3.0 (GPLv3)](./LICENSE)
|
||||
**Website:** https://elwig.at/
|
||||
**Quellcode:** https://git.necronda.net/winzer/elwig
|
||||
**Entwicklungszeitraum:** 2022–2025
|
||||
|
||||
**Verwendete Technologien:**
|
||||
Programmiersprache: C#
|
||||
Framework: Windows Presentation Framework (WPF)
|
||||
Datenbank: [SQLite](https://sqlite.org/)
|
||||
PDF-Erstellung: [WeasyPrint](https://weasyprint.org/), [RazorLight](https://github.com/toddams/RazorLight), [PdfiumViewer](https://github.com/pvginkel/PdfiumViewer)
|
||||
Paketierung: [WiX Toolset](https://www.firegiant.com/wixtoolset/)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</Target>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Installer\Installer.wixproj" />
|
||||
<PackageReference Include="WixToolset.Bal.wixext" Version="6.0.1" />
|
||||
<PackageReference Include="WixToolset.Util.wixext" Version="6.0.1" />
|
||||
<PackageReference Include="WixToolset.Bal.wixext" Version="6.0.2" />
|
||||
<PackageReference Include="WixToolset.Util.wixext" Version="6.0.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -14,8 +14,8 @@ namespace Tests {
|
||||
public async Task Setup_1_Database() {
|
||||
AppDbContext.ConnectionStringOverride = $"Data Source=ElwigTestDB; Mode=Memory; Foreign Keys=True; Cache=Shared";
|
||||
Connection = await AppDbContext.ConnectAsync();
|
||||
await AppDbContext.ExecuteEmbeddedScript(Connection, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.Create.sql");
|
||||
await AppDbContext.ExecuteEmbeddedScript(Connection, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.Insert.sql");
|
||||
await Connection.ExecuteEmbeddedScript(Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.Create.sql");
|
||||
await Connection.ExecuteEmbeddedScript(Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.Insert.sql");
|
||||
}
|
||||
|
||||
[OneTimeSetUp]
|
||||
|
||||
@@ -16,9 +16,9 @@ namespace Tests.E2ETests {
|
||||
public static async Task SetupDatabase() {
|
||||
if (File.Exists(Utils.TestDatabasePath)) File.Delete(Utils.TestDatabasePath);
|
||||
using var cnx = await AppDbContext.ConnectAsync($"Data Source=\"{Utils.TestDatabasePath}\"; Mode=ReadWriteCreate; Foreign Keys=True; Cache=Default; Pooling=False");
|
||||
await AppDbContext.ExecuteEmbeddedScript(cnx, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.Create.sql");
|
||||
await AppDbContext.ExecuteEmbeddedScript(cnx, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.Insert.sql");
|
||||
await AppDbContext.ExecuteEmbeddedScript(cnx, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.E2EInsert.sql");
|
||||
await cnx.ExecuteEmbeddedScript(Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.Create.sql");
|
||||
await cnx.ExecuteEmbeddedScript(Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.Insert.sql");
|
||||
await cnx.ExecuteEmbeddedScript(Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.E2EInsert.sql");
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Tests.E2ETests {
|
||||
|
||||
public const int WINDOW_OPEN_SLEEP = 2000;
|
||||
|
||||
public static readonly string ApplicationPath = Path.GetFullPath(@"..\..\..\..\Elwig\bin\Debug\net8.0-windows\Elwig.exe");
|
||||
public static readonly string ApplicationPath = Path.GetFullPath(@"..\..\..\..\Elwig\bin\Debug\net10.0-windows\Elwig.exe");
|
||||
public static readonly string ConfigPath = Path.GetFullPath(@"..\..\..\..\Tests\config.test.ini");
|
||||
public static readonly string TestDatabasePath = Path.GetFullPath(@"..\..\..\..\Tests\ElwigTestDB.sqlite3");
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
@@ -19,12 +19,12 @@
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="Appium.WebDriver" Version="4.4.5" />
|
||||
<PackageReference Include="NReco.PdfRenderer" Version="1.6.0" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.9.2">
|
||||
<PackageReference Include="NUnit" Version="4.4.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.11.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -12,13 +12,13 @@ namespace Tests.UnitTests.DocumentTests {
|
||||
[OneTimeSetUp]
|
||||
public async Task SetupDatabase() {
|
||||
Connection = await AppDbContext.ConnectAsync();
|
||||
await AppDbContext.ExecuteEmbeddedScript(Connection, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.DocumentInsert.sql");
|
||||
await Connection.ExecuteEmbeddedScript(Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.DocumentInsert.sql");
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public async Task TeardownDatabase() {
|
||||
if (Connection == null) return;
|
||||
await AppDbContext.ExecuteEmbeddedScript(Connection, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.DocumentDelete.sql");
|
||||
await Connection.ExecuteEmbeddedScript(Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.DocumentDelete.sql");
|
||||
await Connection.DisposeAsync();
|
||||
Connection = null;
|
||||
}
|
||||
|
||||
@@ -24,13 +24,13 @@ namespace Tests.UnitTests.HelperTests {
|
||||
[OneTimeSetUp]
|
||||
public async Task SetupDatabase() {
|
||||
Connection = await AppDbContext.ConnectAsync();
|
||||
await AppDbContext.ExecuteEmbeddedScript(Connection, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.BillingInsert.sql");
|
||||
await Connection.ExecuteEmbeddedScript(Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.BillingInsert.sql");
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public async Task TeardownDatabase() {
|
||||
if (Connection == null) return;
|
||||
await AppDbContext.ExecuteEmbeddedScript(Connection, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.BillingDelete.sql");
|
||||
await Connection.ExecuteEmbeddedScript(Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.BillingDelete.sql");
|
||||
await Connection.DisposeAsync();
|
||||
Connection = null;
|
||||
}
|
||||
@@ -72,7 +72,7 @@ namespace Tests.UnitTests.HelperTests {
|
||||
[TearDown]
|
||||
public async Task CleanupDatabasePayment() {
|
||||
if (Connection == null) return;
|
||||
await AppDbContext.ExecuteBatch(Connection, """
|
||||
await Connection.ExecuteBatch("""
|
||||
DELETE FROM credit;
|
||||
DELETE FROM payment_variant;
|
||||
DELETE FROM delivery_part_bucket;
|
||||
@@ -115,7 +115,7 @@ namespace Tests.UnitTests.HelperTests {
|
||||
}
|
||||
|
||||
private Task InsertPaymentVariant(int year, int avnr, string data) {
|
||||
return AppDbContext.ExecuteBatch(Connection!, $"""
|
||||
return Connection!.ExecuteBatch($"""
|
||||
INSERT INTO payment_variant (year, avnr, name, date, transfer_date, test_variant, calc_time, data)
|
||||
VALUES ({year}, {avnr}, 'Test', '2021-01-15', NULL, TRUE, NULL, '{data}');
|
||||
""");
|
||||
@@ -188,7 +188,7 @@ namespace Tests.UnitTests.HelperTests {
|
||||
Assert.That(payment["GV"], Is.EqualTo(10_000));
|
||||
});
|
||||
|
||||
await b.Calculate(false, false, false);
|
||||
await b.Calculate(true, false, false, false);
|
||||
var prices = await GetMemberDeliveryPrices(year, mgnr);
|
||||
Assert.Multiple(() => {
|
||||
Assert.That(prices, Has.Count.EqualTo(7));
|
||||
@@ -234,7 +234,7 @@ namespace Tests.UnitTests.HelperTests {
|
||||
Assert.That(payment["GV"], Is.EqualTo(8_000));
|
||||
});
|
||||
|
||||
await b.Calculate(true, false, false);
|
||||
await b.Calculate(true, true, false, false);
|
||||
var prices = await GetMemberDeliveryPrices(year, mgnr);
|
||||
Assert.Multiple(() => {
|
||||
Assert.That(prices, Has.Count.EqualTo(6));
|
||||
|
||||
@@ -11,13 +11,13 @@ namespace Tests.UnitTests.ServiceTests {
|
||||
[OneTimeSetUp]
|
||||
public async Task SetupDatabase() {
|
||||
Connection = await AppDbContext.ConnectAsync();
|
||||
await AppDbContext.ExecuteEmbeddedScript(Connection, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.ServiceInsert.sql");
|
||||
await Connection.ExecuteEmbeddedScript(Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.ServiceInsert.sql");
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public async Task TeardownDatabase() {
|
||||
if (Connection == null) return;
|
||||
await AppDbContext.ExecuteEmbeddedScript(Connection, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.ServiceDelete.sql");
|
||||
await Connection.ExecuteEmbeddedScript(Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.ServiceDelete.sql");
|
||||
await Connection.DisposeAsync();
|
||||
Connection = null;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
public void Test_03_Moving() {
|
||||
Mock.Weight = 1_000;
|
||||
Mock.Error = "moving";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await Scale!.Weigh());
|
||||
Assert.That(ex.Message, Contains.Substring("Waage in Bewegung"));
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
public void Test_04_Overloaded() {
|
||||
Mock.Weight = 10_000;
|
||||
Mock.Error = "overloaded";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await Scale!.Weigh());
|
||||
Assert.That(ex.Message, Contains.Substring("Waage in Überlast"));
|
||||
}
|
||||
|
||||
@@ -87,14 +87,14 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
public void Test_05_InvalidResponse() {
|
||||
Mock.Weight = 1_000;
|
||||
Mock.Error = "invalid";
|
||||
Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
Assert.ThrowsAsync<FormatException>(async () => await Scale!.Weigh());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_06_InvalidCrc() {
|
||||
Mock.Weight = 1_000;
|
||||
Mock.Error = "crc";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await Scale!.Weigh());
|
||||
Assert.That(ex.Message, Contains.Substring("Invalid CRC16 checksum"));
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
public void Test_07_InvalidUnit() {
|
||||
Mock.Weight = 1_000;
|
||||
Mock.Error = "unit";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await Scale!.Weigh());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
Mock.Weight = 1_000;
|
||||
Mock.Tare = 41;
|
||||
Mock.Error = "moving";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await Scale!.Weigh());
|
||||
Assert.That(ex.Message, Contains.Substring("Waage in Bewegung"));
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
Mock.Weight = 1_000;
|
||||
Mock.Tare = 41;
|
||||
Mock.Error = "invalid";
|
||||
Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
Assert.ThrowsAsync<FormatException>(async () => await Scale!.Weigh());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
public void Test_03_Moving() {
|
||||
Mock.Weight = 1_000;
|
||||
Mock.Error = "moving";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await Scale!.Weigh());
|
||||
Assert.That(ex.Message, Contains.Substring("Waage in Bewegung"));
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
public void Test_04_Overloaded() {
|
||||
Mock.Weight = 10_000;
|
||||
Mock.Error = "overloaded";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await Scale!.Weigh());
|
||||
Assert.That(ex.Message, Contains.Substring("Waage in Überlast"));
|
||||
}
|
||||
|
||||
@@ -87,14 +87,14 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
public void Test_05_InvalidResponse() {
|
||||
Mock.Weight = 1_000;
|
||||
Mock.Error = "invalid";
|
||||
Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
Assert.ThrowsAsync<FormatException>(async () => await Scale!.Weigh());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_06_InvalidCrc() {
|
||||
Mock.Weight = 1_000;
|
||||
Mock.Error = "crc";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await Scale!.Weigh());
|
||||
Assert.That(ex.Message, Contains.Substring("Invalid CRC16 checksum"));
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
public void Test_07_InvalidUnit() {
|
||||
Mock.Weight = 1_000;
|
||||
Mock.Error = "unit";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await Scale!.Weigh());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
Mock.Weight = 1_000;
|
||||
Mock.Tare = 41;
|
||||
Mock.Error = "moving";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await Scale!.Weigh());
|
||||
Assert.That(ex.Message, Contains.Substring("Waage in Bewegung"));
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
Mock.Weight = 1_000;
|
||||
Mock.Tare = 41;
|
||||
Mock.Error = "invalid";
|
||||
Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
|
||||
Assert.ThrowsAsync<FormatException>(async () => await Scale!.Weigh());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
public void Test_03_Moving() {
|
||||
MockA.Weight = 1_000;
|
||||
MockA.Error = "moving";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await ScaleA!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await ScaleA!.Weigh());
|
||||
Assert.That(ex.Message, Contains.Substring("Waage in Bewegung"));
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
public void Test_04_Overloaded() {
|
||||
MockA.Weight = 10_000;
|
||||
MockA.Error = "overloaded";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await ScaleA!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await ScaleA!.Weigh());
|
||||
Assert.That(ex.Message, Contains.Substring("Waage in Überlast"));
|
||||
}
|
||||
|
||||
@@ -107,14 +107,14 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
public void Test_05_InvalidResponse() {
|
||||
MockA.Weight = 1_000;
|
||||
MockA.Error = "invalid";
|
||||
Assert.ThrowsAsync<IOException>(async () => await ScaleA!.Weigh());
|
||||
Assert.ThrowsAsync<FormatException>(async () => await ScaleA!.Weigh());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_06_InvalidCrc() {
|
||||
MockA.Weight = 1_000;
|
||||
MockA.Error = "crc";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await ScaleA!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await ScaleA!.Weigh());
|
||||
Assert.That(ex.Message, Contains.Substring("Invalid CRC16 checksum"));
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace Tests.UnitTests.WeighingTests {
|
||||
public void Test_07_InvalidUnit() {
|
||||
MockA.Weight = 1_000;
|
||||
MockA.Error = "unit";
|
||||
IOException ex = Assert.ThrowsAsync<IOException>(async () => await ScaleA!.Weigh());
|
||||
var ex = Assert.ThrowsAsync<WeighingException>(async () => await ScaleA!.Weigh());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
curl --fail -s -L "https://elwig.at/files/create.sql?v=32" -u "elwig:ganzGeheim123!" -o "Resources\Sql\Create.sql"
|
||||
curl --fail -s -L "https://elwig.at/files/create.sql?v=36" -u "elwig:ganzGeheim123!" -o "Resources\Sql\Create.sql"
|
||||
|
||||
Reference in New Issue
Block a user