Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70e1e7c73a |
@@ -2,46 +2,6 @@
|
|||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
[v1.1.2.0][v1.1.2.0] (2026-08-26) {#v1.1.2.0}
|
|
||||||
---------------------------------------------
|
|
||||||
|
|
||||||
### Neue Funktionen {#v1.1.2.0-features}
|
|
||||||
|
|
||||||
* Eine Automatische Backup-/Sicherungs-Funktion für die Datenbank wurde hinzugefügt. (ca5b719630, cadac4302d)
|
|
||||||
|
|
||||||
### Behobene Fehler {#v1.1.2.0-bugfixes}
|
|
||||||
|
|
||||||
* Wenn am heutigen Tag noch keine Lieferung übernommen wurde, wurden im Lieferungen-/Übernahme-Fenster (`DeliveryAdminWindow`) mit dem Filter "Nur heute" auch Lieferungen des vorhergehenden Tages angezeigt. (c1690bf104)
|
|
||||||
|
|
||||||
### Sonstiges {#v1.1.2.0-misc}
|
|
||||||
|
|
||||||
* Abhängigkeiten aktualisiert. (2360402124, 7add96dfd5)
|
|
||||||
|
|
||||||
[v1.1.2.0]: https://git.necronda.net/winzer/elwig/releases/tag/v1.1.2.0
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[v1.1.1.2][v1.1.1.2] (2026-08-25) {#v1.1.1.2}
|
|
||||||
---------------------------------------------
|
|
||||||
|
|
||||||
### Behobene Fehler {#v1.1.1.2-bugfixes}
|
|
||||||
|
|
||||||
* Auf Lieferscheinen (`DeliveryNote`) waren die Mengenangaben nicht rechtsbündig. (ff71e9fb7f)
|
|
||||||
* Beim Importieren von Mitgliedern mit Stamm-KG ohne Großlage kam es zu einem Fehler, obwohl die Großlage automatisch hätte zugewiesen werden müssen. (edf3678154)
|
|
||||||
|
|
||||||
### Sonstiges {#v1.1.1.2-misc}
|
|
||||||
|
|
||||||
* In Matzen werden für Planenw./Kipper und Lesewagen die Klassen automatisch gesetzt. (3366cc25eb)
|
|
||||||
* Im Lieferungen-Fenster (`DeliveryAdminWindow`) wird für "Nur heute" nicht mehr nur das Datum, sondern die letzten zusammenhängenden Lieferungen verwendet. (46e596ae23)
|
|
||||||
* Für Pamhagen wird als standard Wiege-Methode "brutto" verwendet. (0c4611024f)
|
|
||||||
* Für Waagen kann das automatische setzen von Datum und Uhrzeit nun per Opt-out ausgeschalten werden (Konfigurationsdatei -> `[scale.X]` -> `synctime = false`). (f450dc35b1)
|
|
||||||
|
|
||||||
[v1.1.1.2]: https://git.necronda.net/winzer/elwig/releases/tag/v1.1.1.2
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[v1.1.1.1][v1.1.1.1] (2026-08-24) {#v1.1.1.1}
|
[v1.1.1.1][v1.1.1.1] (2026-08-24) {#v1.1.1.1}
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -2,7 +2,8 @@
|
|||||||
x:Class="Elwig.App"
|
x:Class="Elwig.App"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:ctrl="clr-namespace:Elwig.Controls">
|
xmlns:ctrl="clr-namespace:Elwig.Controls"
|
||||||
|
Exit="Application_Exit">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<ctrl:BoolToStringConverter x:Key="BoolToStarConverter" FalseValue="" TrueValue="*"/>
|
<ctrl:BoolToStringConverter x:Key="BoolToStarConverter" FalseValue="" TrueValue="*"/>
|
||||||
<ctrl:WidthToMarginConverter x:Key="WidthToMarginConverter"/>
|
<ctrl:WidthToMarginConverter x:Key="WidthToMarginConverter"/>
|
||||||
|
|||||||
+28
-41
@@ -25,10 +25,11 @@ namespace Elwig {
|
|||||||
|
|
||||||
protected static App CurrentApp;
|
protected static App CurrentApp;
|
||||||
public static int NumWindows => CurrentApp.Windows.Count;
|
public static int NumWindows => CurrentApp.Windows.Count;
|
||||||
public static bool ForceShutdown { get; set; } = false;
|
public static bool ForceShutdown { get; private set; } = false;
|
||||||
|
|
||||||
private readonly DispatcherTimer _autoUpdateTimer = new() { Interval = TimeSpan.FromHours(1) };
|
private readonly DispatcherTimer _autoUpdateTimer = new() { Interval = TimeSpan.FromHours(1) };
|
||||||
private readonly DispatcherTimer _autoBackupTimer = new() { Interval = TimeSpan.FromMinutes(10) };
|
private readonly DispatcherTimer _autoBackupTimer = new() { Interval = TimeSpan.FromHours(1) };
|
||||||
|
private DatabaseBackupService? _databaseBackupService;
|
||||||
public readonly SerialPortWatcher SerialPortWatcher = new();
|
public readonly SerialPortWatcher SerialPortWatcher = new();
|
||||||
|
|
||||||
public static readonly string DataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Elwig");
|
public static readonly string DataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Elwig");
|
||||||
@@ -75,7 +76,7 @@ namespace Elwig {
|
|||||||
Config = new(Path.GetFullPath(args[1]));
|
Config = new(Path.GetFullPath(args[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextTimer.Tick += (sender, evt) => {
|
ContextTimer.Tick += (object? sender, EventArgs evt) => {
|
||||||
var ch = CurrentLastWrite;
|
var ch = CurrentLastWrite;
|
||||||
if (ch > LastChanged) {
|
if (ch > LastChanged) {
|
||||||
LastChanged = ch;
|
LastChanged = ch;
|
||||||
@@ -93,32 +94,34 @@ namespace Elwig {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await AppDbUpdater.CheckDb();
|
await AppDbUpdater.CheckDb();
|
||||||
} catch (FileNotFoundException exc) {
|
|
||||||
InteractionService.ShowException("Datenbank nicht gefunden", "Die Datenbank konnte nicht gefunden werden", exc);
|
|
||||||
} catch (Exception exc) {
|
} catch (Exception exc) {
|
||||||
if (Config.UpdateUrl != null && Utils.HasInternetConnectivity()) {
|
if (Config.UpdateUrl != null && Utils.HasInternetConnectivity()) {
|
||||||
await CheckForUpdates();
|
await CheckForUpdates();
|
||||||
}
|
}
|
||||||
InteractionService.ShowException("Fehlerhafte Datenbank", "Schwerwiegender Fehler in der Datenbank, unbedingt den Programmierern melden!", exc);
|
InteractionService.ShowException("Fehlerhafte Datenbank", "Fehlerhafte Datenbank", exc);
|
||||||
if (InteractionService.AskQuestion("Backup wiederherstellen", "Soll die Datenbank aus einem Backup wiederhergestellt werden?\n\nAchtung: Das kann zu Datenverlust führen!", false))
|
|
||||||
await BackupService.RestoreDatabase();
|
|
||||||
Shutdown();
|
Shutdown();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Config.DatabaseBackup) {
|
||||||
|
_databaseBackupService = new DatabaseBackupService(Config.DatabaseFile);
|
||||||
|
await _databaseBackupService.RunIfNeededAsync();
|
||||||
|
_autoBackupTimer.Tick += OnAutoBackupTimer;
|
||||||
|
_autoBackupTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
LastChanged = CurrentLastWrite;
|
LastChanged = CurrentLastWrite;
|
||||||
ContextTimer.Start();
|
ContextTimer.Start();
|
||||||
|
|
||||||
Dictionary<string, (string, string, int?, string?, string?, string?, string?, string?)> branches = [];
|
Dictionary<string, (string, string, int?, string?, string?, string?, string?, string?)> branches = [];
|
||||||
using (var ctx = new AppDbContext()) {
|
using (var ctx = new AppDbContext()) {
|
||||||
branches = await ctx.FetchBranches()
|
branches = ctx.FetchBranches()
|
||||||
.ToDictionaryAsync(b => b.Name.ToLower(), b => (b.ZwstId, b.Name, b.PostalDest?.AtPlz?.Plz, b.PostalDest?.AtPlz?.Ort.Name, b.Address, b.PhoneNr, b.FaxNr, b.MobileNr));
|
.ToDictionaryAsync(b => b.Name.ToLower(), b => (b.ZwstId, b.Name, b.PostalDest?.AtPlz?.Plz, b.PostalDest?.AtPlz?.Ort.Name, b.Address, b.PhoneNr, b.FaxNr, b.MobileNr))
|
||||||
|
.GetAwaiter().GetResult();
|
||||||
try {
|
try {
|
||||||
Client = new(ctx);
|
Client = new(ctx);
|
||||||
} catch (Exception exc) {
|
} catch (Exception exc) {
|
||||||
InteractionService.ShowException("Fehler", "Fehler beim Laden der Mandantendaten, unbedingt den Programmierern melden!", exc);
|
InteractionService.ShowException("Fehler", "Fehler beim Laden der Mandantendaten", exc);
|
||||||
if (InteractionService.AskQuestion("Backup wiederherstellen", "Soll die Datenbank aus einem Backup wiederhergestellt werden?\n\nAchtung: Das kann zu Datenverlust führen!", false))
|
|
||||||
await BackupService.RestoreDatabase();
|
|
||||||
Shutdown();
|
Shutdown();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -130,23 +133,9 @@ namespace Elwig {
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
Utils.RunBackground("PDF Initialization", async () => Pdf.Init());
|
Utils.RunBackground("PDF Initialization", () => Pdf.Init());
|
||||||
Utils.RunBackground("JSON Schema Initialization", BillingData.Init);
|
Utils.RunBackground("JSON Schema Initialization", BillingData.Init);
|
||||||
|
|
||||||
if (Config.BackupAuto) {
|
|
||||||
Utils.RunBackground("Automatisches Backup", async () => {
|
|
||||||
Directory.CreateDirectory(Config.BackupPath);
|
|
||||||
await Task.Delay(500);
|
|
||||||
await BackupService.TryDatabaseBackup();
|
|
||||||
});
|
|
||||||
_autoBackupTimer.Tick += new EventHandler((sender, evt) => {
|
|
||||||
Utils.RunBackground("Automatisches Backup", async () => {
|
|
||||||
await BackupService.TryDatabaseBackup();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
_autoBackupTimer.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Config.UpdateAuto && Config.UpdateUrl != null) {
|
if (Config.UpdateAuto && Config.UpdateUrl != null) {
|
||||||
if (Utils.HasInternetConnectivity()) {
|
if (Utils.HasInternetConnectivity()) {
|
||||||
Utils.RunBackground("Auto Updater", async () => {
|
Utils.RunBackground("Auto Updater", async () => {
|
||||||
@@ -166,7 +155,7 @@ namespace Elwig {
|
|||||||
foreach (var s in Config.Scales) {
|
foreach (var s in Config.Scales) {
|
||||||
try {
|
try {
|
||||||
var scale = Scale.FromConfig(s);
|
var scale = Scale.FromConfig(s);
|
||||||
if (s.SyncTime && scale is ICommandScale cmd) {
|
if (scale is ICommandScale cmd) {
|
||||||
try {
|
try {
|
||||||
await cmd.SetDateAndTime(DateTime.Now);
|
await cmd.SetDateAndTime(DateTime.Now);
|
||||||
} catch { }
|
} catch { }
|
||||||
@@ -199,7 +188,7 @@ namespace Elwig {
|
|||||||
Config.WeighingMode = WeighingMode.Net;
|
Config.WeighingMode = WeighingMode.Net;
|
||||||
} else if (Client.IsHaugsdorf || Client.IsSitzendorf) {
|
} else if (Client.IsHaugsdorf || Client.IsSitzendorf) {
|
||||||
Config.WeighingMode = WeighingMode.Box;
|
Config.WeighingMode = WeighingMode.Box;
|
||||||
} else if (Client.IsBaden || Client.IsGrInzersdorf || Client.IsPamhagen) {
|
} else if (Client.IsBaden || Client.IsGrInzersdorf) {
|
||||||
Config.WeighingMode = WeighingMode.Gross;
|
Config.WeighingMode = WeighingMode.Gross;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,21 +199,19 @@ namespace Elwig {
|
|||||||
window.Show();
|
window.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task OnClosing() {
|
private async void Application_Exit(object sender, ExitEventArgs evt) {
|
||||||
try {
|
_autoBackupTimer.Stop();
|
||||||
await BackupService.TryDatabaseBackup();
|
|
||||||
} catch (Exception exc) {
|
|
||||||
InteractionService.ShowException("Automatisches Backup", exc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnExit(ExitEventArgs evt) {
|
|
||||||
SerialPortWatcher.Dispose();
|
SerialPortWatcher.Dispose();
|
||||||
foreach (var s in EventScales) {
|
foreach (var s in EventScales) {
|
||||||
s.Dispose();
|
s.Dispose();
|
||||||
}
|
}
|
||||||
Pdf.Cleanup();
|
await Pdf.Cleanup();
|
||||||
base.OnExit(evt);
|
}
|
||||||
|
|
||||||
|
private async void OnAutoBackupTimer(object? sender, EventArgs evt) {
|
||||||
|
if (_databaseBackupService != null) {
|
||||||
|
await _databaseBackupService.RunIfNeededAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetBranch(Branch b) {
|
public static void SetBranch(Branch b) {
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ namespace Elwig.Documents {
|
|||||||
.AddCell(NewDeliveryMainTd(part.Quality.Name))
|
.AddCell(NewDeliveryMainTd(part.Quality.Name))
|
||||||
.AddCell(NewDeliveryMainTd($"{part.Oe:N0}", center: true))
|
.AddCell(NewDeliveryMainTd($"{part.Oe:N0}", center: true))
|
||||||
.AddCell(NewDeliveryMainTd($"{part.Kmw:N1}", center: true))
|
.AddCell(NewDeliveryMainTd($"{part.Kmw:N1}", center: true))
|
||||||
.AddCell(NewDeliveryMainTd($"{part.Weight:N0}", right: true));
|
.AddCell(NewDeliveryMainTd($"{part.Weight:N0}", center: true));
|
||||||
|
|
||||||
if (part.Cultivation != null) {
|
if (part.Cultivation != null) {
|
||||||
var cult = new KernedParagraph(8);
|
var cult = new KernedParagraph(8);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ namespace Elwig.Documents {
|
|||||||
public Document(string title) {
|
public Document(string title) {
|
||||||
Title = title;
|
Title = title;
|
||||||
Author = App.Client.NameFull;
|
Author = App.Client.NameFull;
|
||||||
Date = DateOnly.FromDateTime(DateTime.Today);
|
Date = DateOnly.FromDateTime(Utils.Today);
|
||||||
}
|
}
|
||||||
|
|
||||||
~Document() {
|
~Document() {
|
||||||
@@ -252,7 +252,7 @@ namespace Elwig.Documents {
|
|||||||
|
|
||||||
public async Task Print(int copies = 1) {
|
public async Task Print(int copies = 1) {
|
||||||
if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet");
|
if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet");
|
||||||
Pdf.Print(PdfPath, copies, IsDoublePaged);
|
await Pdf.Print(PdfPath, copies, IsDoublePaged);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Show() {
|
public void Show() {
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@
|
|||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||||
<ApplicationIcon>Resources\Images\Elwig.ico</ApplicationIcon>
|
<ApplicationIcon>Resources\Images\Elwig.ico</ApplicationIcon>
|
||||||
<Version>1.1.2.0</Version>
|
<Version>1.1.1.1</Version>
|
||||||
<SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<ApplicationManifest>App.manifest</ApplicationManifest>
|
<ApplicationManifest>App.manifest</ApplicationManifest>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="bblanchon.PDFium.Win32" Version="154.0.8021" />
|
<PackageReference Include="bblanchon.PDFium.Win32" Version="153.0.8009" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||||
<PackageReference Include="itext" Version="9.7.0" />
|
<PackageReference Include="itext" Version="9.7.0" />
|
||||||
<PackageReference Include="itext.bouncy-castle-adapter" Version="9.7.0" />
|
<PackageReference Include="itext.bouncy-castle-adapter" Version="9.7.0" />
|
||||||
|
|||||||
@@ -14,17 +14,10 @@ namespace Elwig.Helpers {
|
|||||||
private static int VersionOffset = 0;
|
private static int VersionOffset = 0;
|
||||||
|
|
||||||
public static async Task<Version> CheckDb() {
|
public static async Task<Version> CheckDb() {
|
||||||
if (!File.Exists(App.Config.DatabaseFile))
|
|
||||||
throw new FileNotFoundException($"Die Datei \"{App.Config.DatabaseFile}\" exisitiert nicht");
|
|
||||||
|
|
||||||
long? applId, schemaVers;
|
long? applId, schemaVers;
|
||||||
using (var cnx = await AppDbContext.ConnectAsync()) {
|
using (var cnx = await AppDbContext.ConnectAsync()) {
|
||||||
|
|
||||||
await cnx.QuickCheck();
|
|
||||||
|
|
||||||
applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
|
applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
|
||||||
if (applId != 0x454C5747)
|
if (applId != 0x454C5747) throw new Exception($"Invalid application_id in database (0x{applId:X08})");
|
||||||
throw new FileFormatException($"Invalid application_id in database (0x{applId:X08})");
|
|
||||||
|
|
||||||
schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0;
|
schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0;
|
||||||
VersionOffset = (int)(schemaVers % 100);
|
VersionOffset = (int)(schemaVers % 100);
|
||||||
@@ -83,16 +76,16 @@ namespace Elwig.Helpers {
|
|||||||
try {
|
try {
|
||||||
using var cnx = await AppDbContext.ConnectAsync();
|
using var cnx = await AppDbContext.ConnectAsync();
|
||||||
await cnx.ExecuteBatch("PRAGMA locking_mode = EXCLUSIVE");
|
await cnx.ExecuteBatch("PRAGMA locking_mode = EXCLUSIVE");
|
||||||
|
|
||||||
await cnx.IntegrityCheck();
|
|
||||||
await cnx.ForeignKeyCheck();
|
|
||||||
|
|
||||||
foreach (var script in toExecute) {
|
foreach (var script in toExecute) {
|
||||||
await cnx.ExecuteEmbeddedScript(asm, script);
|
await cnx.ExecuteEmbeddedScript(asm, script);
|
||||||
}
|
}
|
||||||
|
|
||||||
await cnx.IntegrityCheck();
|
var violations = await cnx.ForeignKeyCheck();
|
||||||
await cnx.ForeignKeyCheck();
|
if (violations.Length > 0) {
|
||||||
|
throw new Exception($"Foreign key violations ({violations.Length}):\n" + string.Join("\n", violations
|
||||||
|
.Take(50)
|
||||||
|
.Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}")));
|
||||||
|
}
|
||||||
|
|
||||||
await cnx.ExecuteBatch("VACUUM");
|
await cnx.ExecuteBatch("VACUUM");
|
||||||
await cnx.ExecuteBatch($"PRAGMA schema_version = {toVersion * 100 + VersionOffset}");
|
await cnx.ExecuteBatch($"PRAGMA schema_version = {toVersion * 100 + VersionOffset}");
|
||||||
|
|||||||
+5
-24
@@ -17,11 +17,10 @@ namespace Elwig.Helpers {
|
|||||||
public string? Filling;
|
public string? Filling;
|
||||||
public string? Limit;
|
public string? Limit;
|
||||||
public bool Required;
|
public bool Required;
|
||||||
public bool SyncTime;
|
|
||||||
public string? Log;
|
public string? Log;
|
||||||
public string? _Log;
|
public string? _Log;
|
||||||
|
|
||||||
public ScaleConfig(string id, string? type, string? model, string? cnx, string? empty, string? filling, string? limit, bool? required, bool? syncTime, string? log) {
|
public ScaleConfig(string id, string? type, string? model, string? cnx, string? empty, string? filling, string? limit, bool? required, string? log) {
|
||||||
Id = id;
|
Id = id;
|
||||||
Type = type;
|
Type = type;
|
||||||
Model = model;
|
Model = model;
|
||||||
@@ -30,7 +29,6 @@ namespace Elwig.Helpers {
|
|||||||
Filling = filling;
|
Filling = filling;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
Required = required ?? true;
|
Required = required ?? true;
|
||||||
SyncTime = syncTime ?? true;
|
|
||||||
_Log = log;
|
_Log = log;
|
||||||
Log = log != null ? Path.Combine(App.DataPath, log) : null;
|
Log = log != null ? Path.Combine(App.DataPath, log) : null;
|
||||||
}
|
}
|
||||||
@@ -39,26 +37,17 @@ namespace Elwig.Helpers {
|
|||||||
public class Config {
|
public class Config {
|
||||||
|
|
||||||
private static readonly string[] TrueValues = ["1", "true", "yes", "on"];
|
private static readonly string[] TrueValues = ["1", "true", "yes", "on"];
|
||||||
private static readonly string[] FalseValues = ["0", "false", "no", "off"];
|
|
||||||
|
|
||||||
private readonly string FileName;
|
private readonly string FileName;
|
||||||
|
|
||||||
public bool Debug;
|
public bool Debug;
|
||||||
public string DatabaseFile = Path.Combine(App.DataPath, "database.sqlite3");
|
public string DatabaseFile = App.DataPath + "database.sqlite3";
|
||||||
|
public bool DatabaseBackup = true;
|
||||||
public string? DatabaseLog = null;
|
public string? DatabaseLog = null;
|
||||||
public string? Branch = null;
|
public string? Branch = null;
|
||||||
public WeighingMode? WeighingMode;
|
public WeighingMode? WeighingMode;
|
||||||
|
|
||||||
public bool BackupAuto = true;
|
|
||||||
public string BackupPath = Path.Combine(App.DataPath, "backups");
|
|
||||||
public int BackupRetainHours = 36;
|
|
||||||
public int BackupRetainDays = 8;
|
|
||||||
public int BackupRetainWeeks = 6;
|
|
||||||
public int BackupRetainMonths = 18;
|
|
||||||
|
|
||||||
public string? UpdateUrl = null;
|
public string? UpdateUrl = null;
|
||||||
public bool UpdateAuto = false;
|
public bool UpdateAuto = false;
|
||||||
|
|
||||||
public string? SyncUrl = null;
|
public string? SyncUrl = null;
|
||||||
public string SyncUsername = "";
|
public string SyncUsername = "";
|
||||||
public string SyncPassword = "";
|
public string SyncPassword = "";
|
||||||
@@ -88,23 +77,16 @@ namespace Elwig.Helpers {
|
|||||||
var config = new ConfigurationBuilder().AddIniFile(FileName).Build();
|
var config = new ConfigurationBuilder().AddIniFile(FileName).Build();
|
||||||
|
|
||||||
DatabaseFile = Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, config["database:file"] ?? "database.sqlite3");
|
DatabaseFile = Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, config["database:file"] ?? "database.sqlite3");
|
||||||
|
var backup = config["database:backup"];
|
||||||
|
DatabaseBackup = backup == null || TrueValues.Contains(backup.ToLower());
|
||||||
var log = config["database:log"];
|
var log = config["database:log"];
|
||||||
DatabaseLog = log != null ? Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, log) : null;
|
DatabaseLog = log != null ? Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, log) : null;
|
||||||
Branch = config["general:branch"];
|
Branch = config["general:branch"];
|
||||||
Debug = TrueValues.Contains(config["general:debug"]?.ToLower());
|
Debug = TrueValues.Contains(config["general:debug"]?.ToLower());
|
||||||
var weighing = config["general:weighing"];
|
var weighing = config["general:weighing"];
|
||||||
WeighingMode = weighing != null && Enum.TryParse<WeighingMode>(weighing, true, out var w) ? w : null;
|
WeighingMode = weighing != null && Enum.TryParse<WeighingMode>(weighing, true, out var w) ? w : null;
|
||||||
|
|
||||||
BackupAuto = !FalseValues.Contains(config["backup:auto"]?.ToLower());
|
|
||||||
BackupPath = Path.Combine(Path.GetDirectoryName(DatabaseFile) ?? App.DataPath, config["backup:path"] ?? "backups");
|
|
||||||
BackupRetainHours = int.TryParse(config["backup:retain_hours"], out var hours) ? hours : 36;
|
|
||||||
BackupRetainDays = int.TryParse(config["backup:retain_days"], out var days) ? days : 8;
|
|
||||||
BackupRetainWeeks = int.TryParse(config["backup:retain_weeks"], out var weeks) ? weeks : 6;
|
|
||||||
BackupRetainMonths = int.TryParse(config["backup:retain_months"], out var months) ? months : 18;
|
|
||||||
|
|
||||||
UpdateUrl = config["update:url"];
|
UpdateUrl = config["update:url"];
|
||||||
UpdateAuto = TrueValues.Contains(config["update:auto"]?.ToLower());
|
UpdateAuto = TrueValues.Contains(config["update:auto"]?.ToLower());
|
||||||
|
|
||||||
SyncUrl = config["sync:url"];
|
SyncUrl = config["sync:url"];
|
||||||
SyncUsername = config["sync:username"] ?? "";
|
SyncUsername = config["sync:username"] ?? "";
|
||||||
SyncPassword = config["sync:password"] ?? "";
|
SyncPassword = config["sync:password"] ?? "";
|
||||||
@@ -126,7 +108,6 @@ namespace Elwig.Helpers {
|
|||||||
s, config[$"scale.{s}:type"], config[$"scale.{s}:model"], config[$"scale.{s}:connection"],
|
s, config[$"scale.{s}:type"], config[$"scale.{s}:model"], config[$"scale.{s}:connection"],
|
||||||
config[$"scale.{s}:empty"], config[$"scale.{s}:filling"], config[$"scale.{s}:limit"],
|
config[$"scale.{s}:empty"], config[$"scale.{s}:filling"], config[$"scale.{s}:limit"],
|
||||||
config[$"scale.{s}:required"] != null ? TrueValues.Contains(config[$"scale.{s}:required"]?.ToLower()) : null,
|
config[$"scale.{s}:required"] != null ? TrueValues.Contains(config[$"scale.{s}:required"]?.ToLower()) : null,
|
||||||
config[$"scale.{s}:synctime"] != null ? !FalseValues.Contains(config[$"scale.{s}:synctime"]?.ToLower()) : null,
|
|
||||||
config[$"scale.{s}:log"]
|
config[$"scale.{s}:log"]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Elwig.Helpers {
|
||||||
|
internal sealed class DatabaseBackupService {
|
||||||
|
|
||||||
|
private const int DailyBackupCount = 7;
|
||||||
|
private const int WeeklyBackupCount = 5;
|
||||||
|
|
||||||
|
private readonly string DatabaseFile;
|
||||||
|
private readonly string BackupDirectory;
|
||||||
|
private readonly string DatabaseName;
|
||||||
|
private readonly string BackupPrefix;
|
||||||
|
private readonly string LockFile;
|
||||||
|
private readonly Func<DateTime> Now;
|
||||||
|
private readonly SemaphoreSlim Gate = new(1, 1);
|
||||||
|
|
||||||
|
internal DatabaseBackupService(string databaseFile, Func<DateTime>? now = null) {
|
||||||
|
DatabaseFile = Path.GetFullPath(databaseFile);
|
||||||
|
BackupDirectory = Path.Combine(Path.GetDirectoryName(DatabaseFile)!, "backup");
|
||||||
|
DatabaseName = Path.GetFileNameWithoutExtension(DatabaseFile);
|
||||||
|
BackupPrefix = $"{DatabaseName}_";
|
||||||
|
LockFile = Path.Combine(BackupDirectory, $".{DatabaseName}.backup.lock");
|
||||||
|
Now = now ?? (() => DateTime.Now);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task RunIfNeededAsync() {
|
||||||
|
if (!await Gate.WaitAsync(0)) return;
|
||||||
|
try {
|
||||||
|
var slotDate = GetSlotDate(Now());
|
||||||
|
await Task.Run(() => Run(slotDate));
|
||||||
|
} catch {
|
||||||
|
// Automatic backups dont prevent Elwig from starting or running.
|
||||||
|
} finally {
|
||||||
|
Gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Run(DateOnly slotDate) {
|
||||||
|
Directory.CreateDirectory(BackupDirectory);
|
||||||
|
|
||||||
|
FileStream lockStream;
|
||||||
|
try {
|
||||||
|
lockStream = new FileStream(LockFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
|
||||||
|
} catch (IOException) {
|
||||||
|
// Another Elwig instance is currently creating/pruning backups.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (lockStream) {
|
||||||
|
CleanupTemporaryFiles();
|
||||||
|
|
||||||
|
var backupFile = GetBackupFile(slotDate);
|
||||||
|
if (!File.Exists(backupFile)) {
|
||||||
|
CreateBackup(backupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(backupFile)) {
|
||||||
|
PruneBackups(slotDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateBackup(string backupFile) {
|
||||||
|
var id = Guid.NewGuid().ToString("N");
|
||||||
|
var temporaryDatabase = Path.Combine(BackupDirectory, $".{DatabaseName}.backup-{id}.sqlite3.tmp");
|
||||||
|
var temporaryArchive = Path.Combine(BackupDirectory, $".{DatabaseName}.backup-{id}.zip.tmp");
|
||||||
|
|
||||||
|
try {
|
||||||
|
var sourceConnectionString = new SqliteConnectionStringBuilder {
|
||||||
|
DataSource = DatabaseFile,
|
||||||
|
Mode = SqliteOpenMode.ReadOnly,
|
||||||
|
Cache = SqliteCacheMode.Default,
|
||||||
|
Pooling = false,
|
||||||
|
}.ToString();
|
||||||
|
var destinationConnectionString = new SqliteConnectionStringBuilder {
|
||||||
|
DataSource = temporaryDatabase,
|
||||||
|
Mode = SqliteOpenMode.ReadWriteCreate,
|
||||||
|
Cache = SqliteCacheMode.Default,
|
||||||
|
Pooling = false,
|
||||||
|
}.ToString();
|
||||||
|
|
||||||
|
using (var source = new SqliteConnection(sourceConnectionString))
|
||||||
|
using (var destination = new SqliteConnection(destinationConnectionString)) {
|
||||||
|
source.Open();
|
||||||
|
destination.Open();
|
||||||
|
source.BackupDatabase(destination);
|
||||||
|
|
||||||
|
using var check = destination.CreateCommand();
|
||||||
|
check.CommandText = "PRAGMA quick_check";
|
||||||
|
if (!string.Equals(check.ExecuteScalar()?.ToString(), "ok", StringComparison.OrdinalIgnoreCase)) {
|
||||||
|
throw new InvalidDataException("The automatic database backup failed its SQLite integrity check.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var archive = ZipFile.Open(temporaryArchive, ZipArchiveMode.Create)) {
|
||||||
|
archive.CreateEntryFromFile(temporaryDatabase, "database.sqlite3", CompressionLevel.SmallestSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
File.Move(temporaryArchive, backupFile, false);
|
||||||
|
} catch (IOException) when (File.Exists(backupFile)) {
|
||||||
|
// A completed backup won a publish race. Its time slot is satisfied.
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
DeleteFile(temporaryDatabase);
|
||||||
|
DeleteFile(temporaryArchive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupTemporaryFiles() {
|
||||||
|
foreach (var file in Directory.EnumerateFiles(BackupDirectory, $".{DatabaseName}.backup-*.tmp")) {
|
||||||
|
DeleteFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void PruneBackups(DateOnly slotDate) {
|
||||||
|
var backups = GetManagedBackups().ToArray();
|
||||||
|
var retained = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var dailyStart = slotDate.AddDays(-(DailyBackupCount - 1));
|
||||||
|
foreach (var backup in backups.Where(b => b.Date >= dailyStart && b.Date <= slotDate)) {
|
||||||
|
retained.Add(backup.File);
|
||||||
|
}
|
||||||
|
|
||||||
|
var daysSinceMonday = ((int)slotDate.DayOfWeek + 6) % 7;
|
||||||
|
var currentWeekStart = slotDate.AddDays(-daysSinceMonday);
|
||||||
|
for (var i = 1; i <= WeeklyBackupCount; i++) {
|
||||||
|
var weekStart = currentWeekStart.AddDays(-7 * i);
|
||||||
|
RetainNewest(backups, retained, weekStart, weekStart.AddDays(7));
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentMonthStart = new DateOnly(slotDate.Year, slotDate.Month, 1);
|
||||||
|
foreach (var month in backups
|
||||||
|
.Where(b => b.Date < currentMonthStart)
|
||||||
|
.GroupBy(b => (b.Date.Year, b.Date.Month))) {
|
||||||
|
retained.Add(month.OrderByDescending(b => b.Date).First().File);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clock corrections should not cause future-dated backups to be deleted.
|
||||||
|
foreach (var backup in backups.Where(b => b.Date > slotDate)) {
|
||||||
|
retained.Add(backup.File);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var backup in backups.Where(b => !retained.Contains(b.File))) {
|
||||||
|
DeleteFile(backup.File);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RetainNewest(
|
||||||
|
IEnumerable<(string File, DateOnly Date)> backups,
|
||||||
|
ISet<string> retained,
|
||||||
|
DateOnly start,
|
||||||
|
DateOnly end
|
||||||
|
) {
|
||||||
|
var newest = backups
|
||||||
|
.Where(b => b.Date >= start && b.Date < end)
|
||||||
|
.OrderByDescending(b => b.Date)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (newest.File != null) retained.Add(newest.File);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<(string File, DateOnly Date)> GetManagedBackups() {
|
||||||
|
foreach (var file in Directory.EnumerateFiles(BackupDirectory, $"{BackupPrefix}*.sqlite3.zip")) {
|
||||||
|
var name = Path.GetFileName(file);
|
||||||
|
if (!name.StartsWith(BackupPrefix, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
!name.EndsWith(".sqlite3.zip", StringComparison.OrdinalIgnoreCase)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dateText = name[BackupPrefix.Length..^".sqlite3.zip".Length];
|
||||||
|
if (DateOnly.TryParseExact(dateText, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) {
|
||||||
|
yield return (file, date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetBackupFile(DateOnly date) {
|
||||||
|
return Path.Combine(BackupDirectory, $"{BackupPrefix}{date:yyyy-MM-dd}.sqlite3.zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteFile(string file) {
|
||||||
|
try {
|
||||||
|
File.Delete(file);
|
||||||
|
} catch {
|
||||||
|
// Cleanup and retention are best-effort and will be retried later.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static DateOnly GetSlotDate(DateTime timestamp) {
|
||||||
|
return DateOnly.FromDateTime(timestamp.Hour >= 3 ? timestamp : timestamp.AddDays(-1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,85 +18,73 @@ namespace Elwig.Helpers.Export {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async Task ExportSqlite(string filename, bool zipFile) {
|
public static async Task ExportSqlite(string filename, bool zipFile) {
|
||||||
var tmp = filename + ".tmp";
|
if (zipFile) {
|
||||||
try {
|
File.Delete(filename);
|
||||||
File.Delete(tmp);
|
using var zip = ZipFile.Open(filename, ZipArchiveMode.Create);
|
||||||
if (zipFile) {
|
|
||||||
using var zip = ZipFile.Open(tmp, ZipArchiveMode.Create);
|
|
||||||
|
|
||||||
var version = zip.CreateEntry("version", CompressionLevel.NoCompression);
|
var version = zip.CreateEntry("version", CompressionLevel.NoCompression);
|
||||||
using (var writer = new StreamWriter(version.Open(), Utils.UTF8)) {
|
using (var writer = new StreamWriter(version.Open(), Utils.UTF8)) {
|
||||||
await writer.WriteAsync("elwig:1");
|
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, tmp);
|
|
||||||
}
|
}
|
||||||
File.Move(tmp, filename, true);
|
|
||||||
} finally {
|
var (applId, userVers, schemaVers, size) = await GetMeta();
|
||||||
File.Delete(tmp);
|
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) {
|
public static async Task ExportSql(string filename, bool zipFile) {
|
||||||
var tmp = filename + ".tmp";
|
if (zipFile) {
|
||||||
try {
|
File.Delete(filename);
|
||||||
File.Delete(tmp);
|
using var zip = ZipFile.Open(filename, ZipArchiveMode.Create);
|
||||||
if (zipFile) {
|
|
||||||
using var zip = ZipFile.Open(tmp, ZipArchiveMode.Create);
|
|
||||||
|
|
||||||
var version = zip.CreateEntry("version", CompressionLevel.NoCompression);
|
var version = zip.CreateEntry("version", CompressionLevel.NoCompression);
|
||||||
using (var writer = new StreamWriter(version.Open(), Utils.UTF8)) {
|
using (var writer = new StreamWriter(version.Open(), Utils.UTF8)) {
|
||||||
await writer.WriteAsync("elwig:1");
|
await writer.WriteAsync("elwig:1");
|
||||||
}
|
}
|
||||||
|
|
||||||
var (applId, userVers, schemaVers, size) = await GetMeta();
|
var (applId, userVers, schemaVers, size) = await GetMeta();
|
||||||
var meta = zip.CreateEntry("meta.json", CompressionLevel.NoCompression);
|
var meta = zip.CreateEntry("meta.json", CompressionLevel.NoCompression);
|
||||||
using (var writer = new StreamWriter(meta.Open(), Utils.UTF8)) {
|
using (var writer = new StreamWriter(meta.Open(), Utils.UTF8)) {
|
||||||
var obj = new JsonObject {
|
var obj = new JsonObject {
|
||||||
["timestamp"] = $"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}",
|
["timestamp"] = $"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}",
|
||||||
["zwstid"] = App.ZwstId,
|
["zwstid"] = App.ZwstId,
|
||||||
["device"] = Environment.MachineName,
|
["device"] = Environment.MachineName,
|
||||||
["database"] = new JsonObject {
|
["database"] = new JsonObject {
|
||||||
["application_id"] = applId,
|
["application_id"] = applId,
|
||||||
["user_version"] = userVers,
|
["user_version"] = userVers,
|
||||||
["schema_version"] = schemaVers,
|
["schema_version"] = schemaVers,
|
||||||
["file_size"] = size,
|
["file_size"] = size,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await writer.WriteAsync(obj.ToJsonString(Utils.JsonOpts));
|
await writer.WriteAsync(obj.ToJsonString(Utils.JsonOpts));
|
||||||
}
|
}
|
||||||
|
|
||||||
var sql = zip.CreateEntry("database.sql", CompressionLevel.SmallestSize);
|
var sql = zip.CreateEntry("database.sql", CompressionLevel.SmallestSize);
|
||||||
using (var writer = new StreamWriter(sql.Open(), Utils.UTF8)) {
|
using (var writer = new StreamWriter(sql.Open(), Utils.UTF8)) {
|
||||||
await ExportSql(writer);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
using var stream = File.OpenWrite(tmp);
|
|
||||||
using var writer = new StreamWriter(stream, Utils.UTF8);
|
|
||||||
await ExportSql(writer);
|
await ExportSql(writer);
|
||||||
}
|
}
|
||||||
File.Move(tmp, filename, true);
|
} else {
|
||||||
} finally {
|
using var stream = File.OpenWrite(filename);
|
||||||
File.Delete(tmp);
|
using var writer = new StreamWriter(stream, Utils.UTF8);
|
||||||
|
await ExportSql(writer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using System.IO.Compression;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
namespace Elwig.Helpers.Export {
|
namespace Elwig.Helpers.Export {
|
||||||
public static class ElwigData {
|
public static class ElwigData {
|
||||||
@@ -35,16 +36,14 @@ namespace Elwig.Helpers.Export {
|
|||||||
public static async Task Import(IEnumerable<string> filenames, ImportMode mode) {
|
public static async Task Import(IEnumerable<string> filenames, ImportMode mode) {
|
||||||
try {
|
try {
|
||||||
Dictionary<string, Branch> branches;
|
Dictionary<string, Branch> branches;
|
||||||
Dictionary<int, AT_Kg> kgs;
|
|
||||||
Dictionary<int, int> currentDids;
|
Dictionary<int, int> currentDids;
|
||||||
Dictionary<string, int> currentLsNrs;
|
Dictionary<string, int> currentLsNrs;
|
||||||
Dictionary<int, List<WbRd>> currentWbRde;
|
Dictionary<int, List<WbRd>> currentWbRde;
|
||||||
Dictionary<int, WbKg> currentWbKgs;
|
Dictionary<int, AT_Kg> kgs;
|
||||||
List<WbGl> currentWbGls;
|
List<WbGl> currentWbGls;
|
||||||
|
|
||||||
using (var ctx = new AppDbContext()) {
|
using (var ctx = new AppDbContext()) {
|
||||||
branches = await ctx.FetchBranches().ToDictionaryAsync(b => b.ZwstId);
|
branches = await ctx.FetchBranches().ToDictionaryAsync(b => b.ZwstId);
|
||||||
kgs = await ctx.Katastralgemeinden.ToDictionaryAsync(k => k.KgNr);
|
|
||||||
currentDids = await ctx.Deliveries
|
currentDids = await ctx.Deliveries
|
||||||
.GroupBy(d => d.Year)
|
.GroupBy(d => d.Year)
|
||||||
.ToDictionaryAsync(g => g.Key, g => g.Max(d => d.DId));
|
.ToDictionaryAsync(g => g.Key, g => g.Max(d => d.DId));
|
||||||
@@ -53,8 +52,8 @@ namespace Elwig.Helpers.Export {
|
|||||||
currentWbRde = await ctx.WbRde
|
currentWbRde = await ctx.WbRde
|
||||||
.GroupBy(r => r.KgNr)
|
.GroupBy(r => r.KgNr)
|
||||||
.ToDictionaryAsync(g => g.Key, g => g.ToList());
|
.ToDictionaryAsync(g => g.Key, g => g.ToList());
|
||||||
currentWbKgs = await ctx.WbKgs.ToDictionaryAsync(k => k.KgNr);
|
|
||||||
currentWbGls = await ctx.WbGls.ToListAsync();
|
currentWbGls = await ctx.WbGls.ToListAsync();
|
||||||
|
kgs = await ctx.Katastralgemeinden.Include(k => k.WbKg).ToDictionaryAsync(k => k.KgNr);
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = new List<(
|
var data = new List<(
|
||||||
@@ -123,7 +122,6 @@ namespace Elwig.Helpers.Export {
|
|||||||
var obj = JsonNode.Parse(line)!.AsObject();
|
var obj = JsonNode.Parse(line)!.AsObject();
|
||||||
var (k, g) = obj.ToWbKg(currentWbGls);
|
var (k, g) = obj.ToWbKg(currentWbGls);
|
||||||
r.WbKgs.Add(k);
|
r.WbKgs.Add(k);
|
||||||
currentWbKgs[k.KgNr] = k;
|
|
||||||
if (g != null) {
|
if (g != null) {
|
||||||
currentWbGls[g.GlNr] = g;
|
currentWbGls[g.GlNr] = g;
|
||||||
r.WbGls.Add(g);
|
r.WbGls.Add(g);
|
||||||
@@ -137,7 +135,7 @@ namespace Elwig.Helpers.Export {
|
|||||||
string? line;
|
string? line;
|
||||||
while ((line = await reader.ReadLineAsync()) != null) {
|
while ((line = await reader.ReadLineAsync()) != null) {
|
||||||
var obj = JsonNode.Parse(line)!.AsObject();
|
var obj = JsonNode.Parse(line)!.AsObject();
|
||||||
var (m, b, telNrs, emailAddrs, timestamps) = obj.ToMember(kgs, currentWbKgs);
|
var (m, b, telNrs, emailAddrs, timestamps) = obj.ToMember(kgs);
|
||||||
r.Members.Add(m);
|
r.Members.Add(m);
|
||||||
if (b != null) r.BillingAddresses.Add(b);
|
if (b != null) r.BillingAddresses.Add(b);
|
||||||
r.TelephoneNumbers.AddRange(telNrs);
|
r.TelephoneNumbers.AddRange(telNrs);
|
||||||
@@ -165,7 +163,7 @@ namespace Elwig.Helpers.Export {
|
|||||||
string? line;
|
string? line;
|
||||||
while ((line = await reader.ReadLineAsync()) != null) {
|
while ((line = await reader.ReadLineAsync()) != null) {
|
||||||
var obj = JsonNode.Parse(line)!.AsObject();
|
var obj = JsonNode.Parse(line)!.AsObject();
|
||||||
var (contract, areaCom, wbrd, timestamps) = obj.ToAreaCom(kgs, currentWbKgs, currentWbRde);
|
var (contract, areaCom, wbrd, timestamps) = obj.ToAreaCom(currentWbRde);
|
||||||
r.Contracts.Add(contract);
|
r.Contracts.Add(contract);
|
||||||
r.AreaCommitments.Add(areaCom);
|
r.AreaCommitments.Add(areaCom);
|
||||||
if (wbrd != null) {
|
if (wbrd != null) {
|
||||||
@@ -184,7 +182,7 @@ namespace Elwig.Helpers.Export {
|
|||||||
string? line;
|
string? line;
|
||||||
while ((line = await reader.ReadLineAsync()) != null) {
|
while ((line = await reader.ReadLineAsync()) != null) {
|
||||||
var obj = JsonNode.Parse(line)!.AsObject();
|
var obj = JsonNode.Parse(line)!.AsObject();
|
||||||
var (contract, areaComs, wbrd, timestamps) = obj.ToAreaComContract(kgs, currentWbKgs, currentWbRde);
|
var (contract, areaComs, wbrd, timestamps) = obj.ToAreaComContract(currentWbRde);
|
||||||
r.Contracts.Add(contract);
|
r.Contracts.Add(contract);
|
||||||
r.AreaCommitments.AddRange(areaComs.Select(v => v.Item1));
|
r.AreaCommitments.AddRange(areaComs.Select(v => v.Item1));
|
||||||
if (wbrd != null) {
|
if (wbrd != null) {
|
||||||
@@ -205,7 +203,7 @@ namespace Elwig.Helpers.Export {
|
|||||||
string? line;
|
string? line;
|
||||||
while ((line = await reader.ReadLineAsync()) != null) {
|
while ((line = await reader.ReadLineAsync()) != null) {
|
||||||
var obj = JsonNode.Parse(line)!.AsObject();
|
var obj = JsonNode.Parse(line)!.AsObject();
|
||||||
var (d, parts, mods, rde, timestamps) = obj.ToDelivery(currentLsNrs, currentDids, kgs, currentWbKgs, currentWbRde);
|
var (d, parts, mods, rde, timestamps) = obj.ToDelivery(currentLsNrs, currentDids, kgs, currentWbRde);
|
||||||
r.Deliveries.Add(d);
|
r.Deliveries.Add(d);
|
||||||
r.DeliveryParts.AddRange(parts.Select(p => p.Item1));
|
r.DeliveryParts.AddRange(parts.Select(p => p.Item1));
|
||||||
r.Modifiers.AddRange(mods);
|
r.Modifiers.AddRange(mods);
|
||||||
@@ -682,11 +680,11 @@ namespace Elwig.Helpers.Export {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (Member, BillingAddr?, List<MemberTelNr>, List<MemberEmailAddr>, (DateTime CreatedAt, DateTime ModifiedAt)?) ToMember(this JsonNode json, Dictionary<int, AT_Kg> kgs, Dictionary<int, WbKg> currentWbKgs) {
|
public static (Member, BillingAddr?, List<MemberTelNr>, List<MemberEmailAddr>, (DateTime CreatedAt, DateTime ModifiedAt)?) ToMember(this JsonNode json, Dictionary<int, AT_Kg> kgs) {
|
||||||
var mgnr = json["mgnr"]!.AsValue().GetValue<int>();
|
var mgnr = json["mgnr"]!.AsValue().GetValue<int>();
|
||||||
var kgnr = json["default_kgnr"]?.AsValue().GetValue<int>();
|
var kgnr = json["default_kgnr"]?.AsValue().GetValue<int>();
|
||||||
if (kgnr != null && !currentWbKgs.Values.Any(k => k.KgNr == kgnr)) {
|
if (kgnr != null && !kgs.Values.Any(k => k.WbKg?.KgNr == kgnr)) {
|
||||||
throw new KeyNotFoundException($"Für KG {(kgs.TryGetValue(kgnr.Value, out var k) ? k.Name : "?")} ({kgnr:00000}) ist noch keine Großlage festgelegt!\n(Stammdaten \u2192 Herkunftshierarchie)");
|
throw new ArgumentException($"Für KG {(kgs.TryGetValue(kgnr.Value, out var k) ? k.Name : "?")} ({kgnr:00000}) ist noch keine Großlage festgelegt!\n(Stammdaten → Herkunftshierarchie)");
|
||||||
}
|
}
|
||||||
var createdAt = json["created_at"]?.AsValue().GetValue<string>();
|
var createdAt = json["created_at"]?.AsValue().GetValue<string>();
|
||||||
var modifiedAt = json["modified_at"]?.AsValue().GetValue<string>();
|
var modifiedAt = json["modified_at"]?.AsValue().GetValue<string>();
|
||||||
@@ -817,13 +815,10 @@ namespace Elwig.Helpers.Export {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (AreaComContract, List<(AreaCom, (DateTime CreatedAt, DateTime ModifiedAt)?)>, WbRd?, (DateTime CreatedAt, DateTime ModifiedAt)?) ToAreaComContract(this JsonNode json, Dictionary<int, AT_Kg> kgs, Dictionary<int, WbKg> currentWbKgs, Dictionary<int, List<WbRd>> riede) {
|
public static (AreaComContract, List<(AreaCom, (DateTime CreatedAt, DateTime ModifiedAt)?)>, WbRd?, (DateTime CreatedAt, DateTime ModifiedAt)?) ToAreaComContract(this JsonNode json, Dictionary<int, List<WbRd>> riede) {
|
||||||
var kgnr = json["kgnr"]!.AsValue().GetValue<int>();
|
var kgnr = json["kgnr"]!.AsValue().GetValue<int>();
|
||||||
var ried = json["ried"]?.AsValue().GetValue<string>();
|
var ried = json["ried"]?.AsValue().GetValue<string>();
|
||||||
var fbnr = json["fbnr"]!.AsValue()!.GetValue<int>();
|
var fbnr = json["fbnr"]!.AsValue()!.GetValue<int>();
|
||||||
if (!currentWbKgs.Values.Any(k => k.KgNr == kgnr)) {
|
|
||||||
throw new KeyNotFoundException($"Für KG {(kgs.TryGetValue(kgnr, out var k) ? k.Name : "?")} ({kgnr:00000}) ist noch keine Großlage festgelegt!\n(Stammdaten \u2192 Herkunftshierarchie)");
|
|
||||||
}
|
|
||||||
WbRd? rd = null;
|
WbRd? rd = null;
|
||||||
bool newRd = false;
|
bool newRd = false;
|
||||||
if (ried != null) {
|
if (ried != null) {
|
||||||
@@ -871,12 +866,9 @@ namespace Elwig.Helpers.Export {
|
|||||||
DateTime.ParseExact(modifiedAt, "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None)));
|
DateTime.ParseExact(modifiedAt, "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (AreaComContract, AreaCom, WbRd?, (DateTime CreatedAt, DateTime ModifiedAt)?) ToAreaCom(this JsonNode json, Dictionary<int, AT_Kg> kgs, Dictionary<int, WbKg> currentWbKgs, Dictionary<int, List<WbRd>> riede) {
|
public static (AreaComContract, AreaCom, WbRd?, (DateTime CreatedAt, DateTime ModifiedAt)?) ToAreaCom(this JsonNode json, Dictionary<int, List<WbRd>> riede) {
|
||||||
var kgnr = json["kgnr"]!.AsValue().GetValue<int>();
|
var kgnr = json["kgnr"]!.AsValue().GetValue<int>();
|
||||||
var ried = json["ried"]?.AsValue().GetValue<string>();
|
var ried = json["ried"]?.AsValue().GetValue<string>();
|
||||||
if (!currentWbKgs.Values.Any(k => k.KgNr == kgnr)) {
|
|
||||||
throw new KeyNotFoundException($"Für KG {(kgs.TryGetValue(kgnr, out var k) ? k.Name : "?")} ({kgnr:00000}) ist noch keine Großlage festgelegt!\n(Stammdaten \u2192 Herkunftshierarchie)");
|
|
||||||
}
|
|
||||||
WbRd? rd = null;
|
WbRd? rd = null;
|
||||||
bool newRd = false;
|
bool newRd = false;
|
||||||
if (ried != null) {
|
if (ried != null) {
|
||||||
@@ -965,7 +957,7 @@ namespace Elwig.Helpers.Export {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (Delivery, List<(DeliveryPart, (DateTime CreatedAt, DateTime ModifiedAt)?)>, 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, WbKg> currentWbKgs, Dictionary<int, List<WbRd>> riede) {
|
public static (Delivery, List<(DeliveryPart, (DateTime CreatedAt, DateTime ModifiedAt)?)>, 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 year = json["year"]!.AsValue().GetValue<int>();
|
||||||
var lsnr = json["lsnr"]!.AsValue().GetValue<string>();
|
var lsnr = json["lsnr"]!.AsValue().GetValue<string>();
|
||||||
var did = currentLsNrs.GetValueOrDefault(lsnr, -1);
|
var did = currentLsNrs.GetValueOrDefault(lsnr, -1);
|
||||||
@@ -991,9 +983,6 @@ namespace Elwig.Helpers.Export {
|
|||||||
}, [.. json["parts"]!.AsArray().Select(p => p!.AsObject()).Select<JsonObject, (DeliveryPart, (DateTime, DateTime)?)>(p => {
|
}, [.. json["parts"]!.AsArray().Select(p => p!.AsObject()).Select<JsonObject, (DeliveryPart, (DateTime, DateTime)?)>(p => {
|
||||||
var kgnr = p["kgnr"]?.AsValue().GetValue<int>();
|
var kgnr = p["kgnr"]?.AsValue().GetValue<int>();
|
||||||
var ried = p["ried"]?.AsValue().GetValue<string>();
|
var ried = p["ried"]?.AsValue().GetValue<string>();
|
||||||
if (kgnr != null && !currentWbKgs.Values.Any(k => k.KgNr == kgnr)) {
|
|
||||||
throw new KeyNotFoundException($"Für KG {(kgs.TryGetValue(kgnr.Value, out var k) ? k.Name : "?")} ({kgnr:00000}) ist noch keine Großlage festgelegt!\n(Stammdaten \u2192 Herkunftshierarchie)");
|
|
||||||
}
|
|
||||||
WbRd? rd = null;
|
WbRd? rd = null;
|
||||||
if (ried != null && kgnr != null) {
|
if (ried != null && kgnr != null) {
|
||||||
var rde = riede.GetValueOrDefault(kgnr.Value, []);
|
var rde = riede.GetValueOrDefault(kgnr.Value, []);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.IO.Hashing;
|
using System.IO.Hashing;
|
||||||
@@ -135,39 +134,11 @@ namespace Elwig.Helpers {
|
|||||||
return await cmd.ExecuteScalarAsync();
|
return await cmd.ExecuteScalarAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task QuickCheck(this SqliteConnection cnx) {
|
public static async Task<(string Table, long RowId, string Parent, long FkId)[]> ForeignKeyCheck(this SqliteConnection cnx) {
|
||||||
using var cmd = cnx.CreateCommand();
|
|
||||||
cmd.CommandText = "PRAGMA quick_check";
|
|
||||||
using var reader = await cmd.ExecuteReaderAsync();
|
|
||||||
var list = new List<string>();
|
|
||||||
while (await reader.ReadAsync()) {
|
|
||||||
list.Add(reader.GetString(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list.Count != 1 || list[0] != "ok") {
|
|
||||||
throw new FileFormatException($"Integrity problems ({list.Count}):\n" + string.Join("\n", list.Take(50)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task IntegrityCheck(this SqliteConnection cnx) {
|
|
||||||
using var cmd = cnx.CreateCommand();
|
|
||||||
cmd.CommandText = "PRAGMA integrity_check";
|
|
||||||
using var reader = await cmd.ExecuteReaderAsync();
|
|
||||||
var list = new List<string>();
|
|
||||||
while (await reader.ReadAsync()) {
|
|
||||||
list.Add(reader.GetString(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list.Count != 1 || list[0] != "ok") {
|
|
||||||
throw new FileFormatException($"Integrity problems ({list.Count}):\n" + string.Join("\n", list.Take(50)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task ForeignKeyCheck(this SqliteConnection cnx) {
|
|
||||||
using var cmd = cnx.CreateCommand();
|
using var cmd = cnx.CreateCommand();
|
||||||
cmd.CommandText = "PRAGMA foreign_key_check";
|
cmd.CommandText = "PRAGMA foreign_key_check";
|
||||||
using var reader = await cmd.ExecuteReaderAsync();
|
using var reader = await cmd.ExecuteReaderAsync();
|
||||||
var list = new List<(string Table, long RowId, string Parent, long FkId)>();
|
var list = new List<(string, long, string, long)>();
|
||||||
while (await reader.ReadAsync()) {
|
while (await reader.ReadAsync()) {
|
||||||
var table = reader.GetString(0);
|
var table = reader.GetString(0);
|
||||||
var rowid = reader.GetInt64(1);
|
var rowid = reader.GetInt64(1);
|
||||||
@@ -175,11 +146,7 @@ namespace Elwig.Helpers {
|
|||||||
var fkid = reader.GetInt64(3);
|
var fkid = reader.GetInt64(3);
|
||||||
list.Add((table, rowid, parent, fkid));
|
list.Add((table, rowid, parent, fkid));
|
||||||
}
|
}
|
||||||
|
return [.. list];
|
||||||
if (list.Count > 0) {
|
|
||||||
throw new InvalidDataException($"Foreign key violations ({list.Count}):\n" + string.Join("\n", list.Take(50)
|
|
||||||
.Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}")));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<T> Join<T>(this IEnumerable<T> src, Func<T> separatorFactory) {
|
public static IEnumerable<T> Join<T>(this IEnumerable<T> src, Func<T> separatorFactory) {
|
||||||
@@ -204,12 +171,5 @@ namespace Elwig.Helpers {
|
|||||||
grid.Children.Add(tb);
|
grid.Children.Add(tb);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (int Year, int Week) GetYearAndWeek(this DateTime date) {
|
|
||||||
return (ISOWeek.GetYear(date), ISOWeek.GetWeekOfYear(date));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static (int Year, int Tertial) GetYearAndTertial(this DateTime date) {
|
|
||||||
return (date.Month <= 7 ? date.Year - 1 : date.Year, date.Month == 12 || date.Month <= 3 ? 2 : date.Month <= 7 ? 3 : 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
using Elwig.Services;
|
using Elwig.Services;
|
||||||
using Elwig.Windows;
|
using Elwig.Windows;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Elwig.Helpers.Printing {
|
namespace Elwig.Helpers.Printing {
|
||||||
public static class Pdf {
|
public static class Pdf {
|
||||||
|
|
||||||
public static void Init(Action? evtHandler = null) {
|
public static Task Init(Action? evtHandler = null) {
|
||||||
PdfiumNative.FPDF_InitLibrary();
|
PdfiumNative.FPDF_InitLibrary();
|
||||||
evtHandler?.Invoke();
|
evtHandler?.Invoke();
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Cleanup() {
|
public static Task Cleanup() {
|
||||||
PdfiumNative.FPDF_DestroyLibrary();
|
PdfiumNative.FPDF_DestroyLibrary();
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Show(TempFile file, string title) {
|
public static void Show(TempFile file, string title) {
|
||||||
@@ -28,13 +31,14 @@ namespace Elwig.Helpers.Printing {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Print(string path, int copies = 1, bool doublePaged = false) {
|
public static Task Print(string path, int copies = 1, bool doublePaged = false) {
|
||||||
try {
|
try {
|
||||||
var printer = new PdfPrinter();
|
var printer = new PdfPrinter();
|
||||||
printer.Print(path, copies, doublePaged);
|
printer.Print(path, copies, doublePaged);
|
||||||
} catch (Exception exc) {
|
} catch (Exception exc) {
|
||||||
InteractionService.ShowException("Fehler beim Drucken", "Beim Drucken ist ein Fehler aufgetreten", exc);
|
InteractionService.ShowException("Fehler beim Drucken", "Beim Drucken ist ein Fehler aufgetreten", exc);
|
||||||
}
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ namespace Elwig.Helpers {
|
|||||||
public static int CurrentNextSeason => DateTime.Now.Year - (DateTime.Now.Month <= 3 ? 1 : 0);
|
public static int CurrentNextSeason => DateTime.Now.Year - (DateTime.Now.Month <= 3 ? 1 : 0);
|
||||||
public static int CurrentLastSeason => DateTime.Now.Year - (DateTime.Now.Month <= 6 ? 1 : 0);
|
public static int CurrentLastSeason => DateTime.Now.Year - (DateTime.Now.Month <= 6 ? 1 : 0);
|
||||||
public static int FollowingSeason => DateTime.Now.Year + (DateTime.Now.Month >= 11 ? 1 : 0);
|
public static int FollowingSeason => DateTime.Now.Year + (DateTime.Now.Month >= 11 ? 1 : 0);
|
||||||
|
public static DateTime Today => (DateTime.Now.Hour >= 3) ? DateTime.Today : DateTime.Today.AddDays(-1);
|
||||||
|
|
||||||
[GeneratedRegex("^serial://([A-Za-z0-9]+):([0-9]+)(,([5-9]),([NOEMSnoems]),(0|1|1\\.5|2|))?$", RegexOptions.Compiled)]
|
[GeneratedRegex("^serial://([A-Za-z0-9]+):([0-9]+)(,([5-9]),([NOEMSnoems]),(0|1|1\\.5|2|))?$", RegexOptions.Compiled)]
|
||||||
private static partial Regex GeneratedSerialRegex();
|
private static partial Regex GeneratedSerialRegex();
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
using Elwig.Helpers;
|
|
||||||
using Elwig.Helpers.Export;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Input;
|
|
||||||
|
|
||||||
namespace Elwig.Services {
|
|
||||||
public static class BackupService {
|
|
||||||
|
|
||||||
public static bool IsAnyBackupUpToDate {
|
|
||||||
get {
|
|
||||||
var lastDbWrite = File.GetLastWriteTime(App.Config.DatabaseFile);
|
|
||||||
lastDbWrite += new TimeSpan(0, 0, -1);
|
|
||||||
return Directory.GetFiles(App.Config.BackupPath, "????-??-??_??-??-??.*.zip")
|
|
||||||
.Any(n => DateTime.ParseExact(Path.GetFileName(n)[..19], "yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture, DateTimeStyles.None) >= lastDbWrite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task TryDatabaseBackup() {
|
|
||||||
if (IsAnyBackupUpToDate) return;
|
|
||||||
await BackupDatabase();
|
|
||||||
PruneBackups();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task BackupDatabase() {
|
|
||||||
await Database.ExportSql(Path.Combine(App.Config.BackupPath, $"{File.GetLastWriteTime(App.Config.DatabaseFile):yyyy-MM-dd_HH-mm-ss}.sql.zip"), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void PruneBackups() {
|
|
||||||
var retain = new HashSet<string>();
|
|
||||||
var backups = Directory.GetFiles(App.Config.BackupPath, "????-??-??_??-??-??.*.zip")
|
|
||||||
.Select(n => (FileName: n, Timestamp: DateTime.ParseExact(Path.GetFileName(n)[..19], "yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture, DateTimeStyles.None)))
|
|
||||||
.OrderByDescending(t => t.Timestamp).ToArray();
|
|
||||||
|
|
||||||
DateTime now;
|
|
||||||
|
|
||||||
// retain hourly backups
|
|
||||||
now = DateTime.Now;
|
|
||||||
for (int i = 0; i < App.Config.BackupRetainHours; i++, now = now.AddHours(-1)) {
|
|
||||||
foreach (var b in backups) {
|
|
||||||
if (b.Timestamp.Date == now.Date && b.Timestamp.Hour == now.Hour) {
|
|
||||||
retain.Add(b.FileName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// retain daily backups
|
|
||||||
now = DateTime.Now;
|
|
||||||
for (int i = 0; i < App.Config.BackupRetainDays; i++, now = now.AddDays(-1)) {
|
|
||||||
foreach (var b in backups) {
|
|
||||||
if (b.Timestamp.Date == now.Date) {
|
|
||||||
retain.Add(b.FileName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// retain weekly backups
|
|
||||||
now = DateTime.Now;
|
|
||||||
for (int i = 0; i < App.Config.BackupRetainWeeks; i++, now = now.AddDays(-7)) {
|
|
||||||
foreach (var b in backups) {
|
|
||||||
if (b.Timestamp.GetYearAndWeek() == now.GetYearAndWeek()) {
|
|
||||||
retain.Add(b.FileName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// retain monthly backups
|
|
||||||
now = DateTime.Now;
|
|
||||||
for (int i = 0; i < App.Config.BackupRetainMonths; i++, now = now.AddMonths(-1)) {
|
|
||||||
foreach (var b in backups) {
|
|
||||||
if (b.Timestamp.Year == now.Year && b.Timestamp.Month == now.Month) {
|
|
||||||
retain.Add(b.FileName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// retain tertial backups
|
|
||||||
now = new DateTime(DateTime.Now.Year + 1, 3, 1);
|
|
||||||
for (int i = 0; i < 600; i++, now = now.AddMonths(-4)) {
|
|
||||||
foreach (var b in backups) {
|
|
||||||
if (b.Timestamp.GetYearAndTertial() == now.GetYearAndTertial()) {
|
|
||||||
retain.Add(b.FileName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete non-retained files
|
|
||||||
foreach (var b in backups) {
|
|
||||||
if (retain.Contains(b.FileName)) continue;
|
|
||||||
try {
|
|
||||||
File.Delete(b.FileName);
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
// delete temp files
|
|
||||||
foreach (var f in Directory.GetFiles(App.Config.BackupPath, "*.tmp")) {
|
|
||||||
try {
|
|
||||||
File.Delete(f);
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task RestoreDatabase() {
|
|
||||||
try {
|
|
||||||
var d = new OpenFileDialog() {
|
|
||||||
Title = "Datenbank wiederherstellen - Elwig",
|
|
||||||
DefaultDirectory = App.Config.BackupPath,
|
|
||||||
InitialDirectory = App.Config.BackupPath,
|
|
||||||
DefaultExt = "sql.zip",
|
|
||||||
Filter = "SQLite-Datenbank (*.sqlite3, *.sqlite3.zip, *.sql, *.sql.zip)|*.sqlite3;*.sqlite3.zip;*.sql;*.sql.zip",
|
|
||||||
};
|
|
||||||
if (d.ShowDialog() == true) {
|
|
||||||
if (!InteractionService.AskContinue("Datenbank wiederherstellen", "Soll die Datenbank wirklich unwiederruflich durch die wiederhergestellte Version ersetzt werden?"))
|
|
||||||
return;
|
|
||||||
Mouse.OverrideCursor = Cursors.Wait;
|
|
||||||
await App.ReplaceDatabase(d.FileName);
|
|
||||||
}
|
|
||||||
} catch (Exception exc) {
|
|
||||||
InteractionService.ShowException(exc);
|
|
||||||
}
|
|
||||||
Mouse.OverrideCursor = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -45,7 +45,7 @@ namespace Elwig.Services {
|
|||||||
deliveryAncmtQuery = deliveryAncmtQuery.Where(a => a.Year == s.Year && a.DsNr == s.DsNr);
|
deliveryAncmtQuery = deliveryAncmtQuery.Where(a => a.Year == s.Year && a.DsNr == s.DsNr);
|
||||||
filterNames.Add($"{s.Date:dd.MM.yyyy} – {s.Branch.Name} – {s.Description}");
|
filterNames.Add($"{s.Date:dd.MM.yyyy} – {s.Branch.Name} – {s.Description}");
|
||||||
} else {
|
} else {
|
||||||
deliveryAncmtQuery = deliveryAncmtQuery.Where(a => a.Year == vm.FilterSeason && (!vm.FilterOnlyUpcoming || a.Schedule.DateString.CompareTo(DateTime.Today.ToString("yyyy-MM-dd")) >= 0));
|
deliveryAncmtQuery = deliveryAncmtQuery.Where(a => a.Year == vm.FilterSeason && (!vm.FilterOnlyUpcoming || a.Schedule.DateString.CompareTo(Utils.Today.ToString("yyyy-MM-dd")) >= 0));
|
||||||
filterNames.Add($"{vm.FilterSeason}");
|
filterNames.Add($"{vm.FilterSeason}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ namespace Elwig.Services {
|
|||||||
filterNames.Add($"{vm.FilterSeason}");
|
filterNames.Add($"{vm.FilterSeason}");
|
||||||
}
|
}
|
||||||
if (vm.FilterOnlyUpcoming) {
|
if (vm.FilterOnlyUpcoming) {
|
||||||
deliveryScheduleQuery = deliveryScheduleQuery.Where(s => s.DateString.CompareTo(DateTime.Today.ToString("yyyy-MM-dd")) >= 0);
|
deliveryScheduleQuery = deliveryScheduleQuery.Where(s => s.DateString.CompareTo(Utils.Today.ToString("yyyy-MM-dd")) >= 0);
|
||||||
filterNames.Add($"ab {DateTime.Today:dd.MM.yyyy}");
|
filterNames.Add($"ab {Utils.Today:dd.MM.yyyy}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var filterVar = new List<string>();
|
var filterVar = new List<string>();
|
||||||
|
|||||||
@@ -72,45 +72,6 @@ namespace Elwig.Services {
|
|||||||
vm.ManualWeighingReason = p.WeighingReason;
|
vm.ManualWeighingReason = p.WeighingReason;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (DateTime From, DateTime To)? GetFilterToday(DateTime today, IEnumerable<DateTime> timestamps) {
|
|
||||||
var filtered = timestamps.Where(t => t >= today.AddDays(-1) && t < today.AddDays(1)).Order().ToArray();
|
|
||||||
if (filtered.Length == 0) return null;
|
|
||||||
|
|
||||||
var latest = filtered.Last();
|
|
||||||
var since = filtered.Cast<DateTime>().Reverse().Aggregate(latest, (s, c) => s.Subtract(c).TotalHours <= 6 ? c : s);
|
|
||||||
latest += new TimeSpan(0, 1, -latest.Minute, -latest.Second, -latest.Millisecond, -latest.Microsecond);
|
|
||||||
since += new TimeSpan(0, 0, -since.Minute, -since.Second, -since.Millisecond, -since.Microsecond);
|
|
||||||
|
|
||||||
if (since.Subtract(today) <= new TimeSpan(-6, 0, 0) && latest.Subtract(today) < new TimeSpan(0, 0, 0))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (since, latest);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<(Expression<Func<Delivery, bool>>, string)> GetFilterToday(AppDbContext ctx) {
|
|
||||||
var today = DateTime.Today;
|
|
||||||
var timestamps = (await ctx.Deliveries
|
|
||||||
.Where(d => d.DateString.CompareTo(today.AddDays(-1).ToString("yyyy-MM-dd")) >= 0)
|
|
||||||
.OrderBy(d => d.DateString).ThenBy(d => d.TimeString)
|
|
||||||
.Select(d => d.DateString + " " + d.TimeString)
|
|
||||||
.ToListAsync())
|
|
||||||
.Select(s => DateTime.TryParseExact(s, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? (DateTime?)dt : null)
|
|
||||||
.Where(t => t.HasValue)
|
|
||||||
.Cast<DateTime>()
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var res = GetFilterToday(today, timestamps);
|
|
||||||
if (!res.HasValue) {
|
|
||||||
return (_ => false, today.ToString("dd.MM.yyyy"));
|
|
||||||
} else {
|
|
||||||
var from = res.Value.From;
|
|
||||||
var to = res.Value.To;
|
|
||||||
return (d => (d.DateString == from.ToString("yyyy-MM-dd") && (d.TimeString == null || d.TimeString.CompareTo(from.ToString("HH:mm:ss")) >= 0)) ||
|
|
||||||
(d.DateString.CompareTo(from.ToString("yyyy-MM-dd")) > 0),
|
|
||||||
from.Date == to.Date ? from.ToString("dd.MM.yyyy") : $"{from:dd.MM.yyyy \\a\\b HH:mm} / {to:dd.MM.yyyy \\b\\i\\s HH:mm}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<(List<string>, IQueryable<Delivery>, IQueryable<DeliveryPart>, Predicate<DeliveryPart>, List<string>)> GetFilters(this DeliveryAdminViewModel vm, AppDbContext ctx) {
|
public static async Task<(List<string>, IQueryable<Delivery>, IQueryable<DeliveryPart>, Predicate<DeliveryPart>, List<string>)> GetFilters(this DeliveryAdminViewModel vm, AppDbContext ctx) {
|
||||||
List<string> filterNames = [];
|
List<string> filterNames = [];
|
||||||
IQueryable<Delivery> deliveryQuery = ctx.Deliveries;
|
IQueryable<Delivery> deliveryQuery = ctx.Deliveries;
|
||||||
@@ -123,9 +84,10 @@ namespace Elwig.Services {
|
|||||||
filterNames.Add(vm.FilterMember.AdministrativeName);
|
filterNames.Add(vm.FilterMember.AdministrativeName);
|
||||||
}
|
}
|
||||||
if (vm.FilterTodayOnly) {
|
if (vm.FilterTodayOnly) {
|
||||||
var (pred, name) = await GetFilterToday(ctx);
|
deliveryQuery = deliveryQuery
|
||||||
deliveryQuery = deliveryQuery.Where(pred);
|
.Where(d => (d.DateString == Utils.Today.ToString("yyyy-MM-dd") && (d.TimeString == null || d.TimeString.CompareTo("03:00:00") > 0)) ||
|
||||||
filterNames.Add(name);
|
(d.DateString == Utils.Today.AddDays(1).ToString("yyyy-MM-dd") && (d.TimeString == null || d.TimeString.CompareTo("03:00:00") <= 0)));
|
||||||
|
filterNames.Add(Utils.Today.ToString("dd.MM.yyyy"));
|
||||||
} else if (!vm.FilterAllSeasons) {
|
} else if (!vm.FilterAllSeasons) {
|
||||||
deliveryQuery = deliveryQuery.Where(d => d.Year == vm.FilterSeason);
|
deliveryQuery = deliveryQuery.Where(d => d.Year == vm.FilterSeason);
|
||||||
filterNames.Add($"{vm.FilterSeason}");
|
filterNames.Add($"{vm.FilterSeason}");
|
||||||
@@ -768,9 +730,10 @@ namespace Elwig.Services {
|
|||||||
query = q;
|
query = q;
|
||||||
filterNames.AddRange(f);
|
filterNames.AddRange(f);
|
||||||
} else if (subject == ExportSubject.FromToday) {
|
} else if (subject == ExportSubject.FromToday) {
|
||||||
var (pred, name) = await GetFilterToday(ctx);
|
var date = $"{Utils.Today:yyyy-MM-dd}";
|
||||||
query = ctx.Deliveries.Where(pred).SelectMany(d => d.Parts);
|
query = ctx.DeliveryParts
|
||||||
filterNames.Add(name);
|
.Where(p => p.Delivery.DateString == date);
|
||||||
|
filterNames.Add($"{Utils.Today:dd.MM.yyyy}");
|
||||||
} else if (subject == ExportSubject.FromSeasonAndBranch) {
|
} else if (subject == ExportSubject.FromSeasonAndBranch) {
|
||||||
query = ctx.DeliveryParts
|
query = ctx.DeliveryParts
|
||||||
.Where(p => p.Year == Utils.CurrentLastSeason && p.Delivery.ZwstId == App.ZwstId);
|
.Where(p => p.Year == Utils.CurrentLastSeason && p.Delivery.ZwstId == App.ZwstId);
|
||||||
@@ -842,9 +805,10 @@ namespace Elwig.Services {
|
|||||||
query = q;
|
query = q;
|
||||||
filterNames.AddRange(f);
|
filterNames.AddRange(f);
|
||||||
} else if (subject == ExportSubject.FromToday) {
|
} else if (subject == ExportSubject.FromToday) {
|
||||||
var (pred, name) = await GetFilterToday(ctx);
|
var date = $"{Utils.Today:yyyy-MM-dd}";
|
||||||
query = ctx.Deliveries.Where(pred).SelectMany(d => d.Parts);
|
query = ctx.DeliveryParts
|
||||||
filterNames.Add(name);
|
.Where(p => p.Delivery.DateString == date);
|
||||||
|
filterNames.Add($"{Utils.Today:dd.MM.yyyy}");
|
||||||
} else {
|
} else {
|
||||||
throw new ArgumentException("Invalid value for ExportSubject");
|
throw new ArgumentException("Invalid value for ExportSubject");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using Elwig.Models;
|
|||||||
using Elwig.Models.Entities;
|
using Elwig.Models.Entities;
|
||||||
using Elwig.ViewModels;
|
using Elwig.ViewModels;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -12,7 +11,7 @@ namespace Elwig.Services {
|
|||||||
|
|
||||||
public static async Task InitInputs(this MemberBusinessSharesViewModel vm) {
|
public static async Task InitInputs(this MemberBusinessSharesViewModel vm) {
|
||||||
using var ctx = new AppDbContext();
|
using var ctx = new AppDbContext();
|
||||||
vm.DateNoticeString = $"{DateTime.Today:dd.MM.yyyy}";
|
vm.DateNoticeString = $"{Utils.Today:dd.MM.yyyy}";
|
||||||
vm.ValuePerShare = (await ctx.Seasons.OrderBy(s => s.Year).LastOrDefaultAsync())?.BusinessShareValue;
|
vm.ValuePerShare = (await ctx.Seasons.OrderBy(s => s.Year).LastOrDefaultAsync())?.BusinessShareValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ namespace Elwig.Windows {
|
|||||||
if (await ctx.FetchSeasons(Utils.CurrentYear).SingleOrDefaultAsync() == null) {
|
if (await ctx.FetchSeasons(Utils.CurrentYear).SingleOrDefaultAsync() == null) {
|
||||||
InteractionService.ShowWarning("Saison noch nicht erstellt",
|
InteractionService.ShowWarning("Saison noch nicht erstellt",
|
||||||
"Die Saison für das aktuelle Jahr wurde noch nicht erstellt. Neue Lieferungen können nicht abgespeichert werden.\n\n" +
|
"Die Saison für das aktuelle Jahr wurde noch nicht erstellt. Neue Lieferungen können nicht abgespeichert werden.\n\n" +
|
||||||
"(Stammdaten \u2192 Saisons \u2192 Neu anlegen...)");
|
"(Stammdaten -> Saisons -> Neu anlegen...)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -685,7 +685,7 @@ namespace Elwig.Windows {
|
|||||||
private async void TodayOnlyInput_Changed(object sender, RoutedEventArgs evt) {
|
private async void TodayOnlyInput_Changed(object sender, RoutedEventArgs evt) {
|
||||||
if (!HasContextLoaded) return;
|
if (!HasContextLoaded) return;
|
||||||
if (TodayOnlyInput.IsChecked == true && AllSeasonsInput.IsChecked == false) {
|
if (TodayOnlyInput.IsChecked == true && AllSeasonsInput.IsChecked == false) {
|
||||||
ViewModel.FilterSeason = DateTime.Today.Year;
|
ViewModel.FilterSeason = Utils.Today.Year;
|
||||||
ViewModel.FilterTodayOnly = true;
|
ViewModel.FilterTodayOnly = true;
|
||||||
}
|
}
|
||||||
await RefreshList();
|
await RefreshList();
|
||||||
@@ -1379,7 +1379,7 @@ namespace Elwig.Windows {
|
|||||||
var kl = mod.Where(m => m.Name.StartsWith("Klasse ")).Select(m => m.ModId).LastOrDefault("_")[0];
|
var kl = mod.Where(m => m.Name.StartsWith("Klasse ")).Select(m => m.ModId).LastOrDefault("_")[0];
|
||||||
if (ViewModel.IsUnloadingPumped && (kl == 'A' || kl == '_')) {
|
if (ViewModel.IsUnloadingPumped && (kl == 'A' || kl == '_')) {
|
||||||
kl = 'B';
|
kl = 'B';
|
||||||
} else if (ViewModel.IsUnloadingDumper && (kl == 'B' || kl == '_')) {
|
} else if (ViewModel.IsUnloadingDumper && kl == '_') {
|
||||||
kl = 'A';
|
kl = 'A';
|
||||||
} else {
|
} else {
|
||||||
kl = '_';
|
kl = '_';
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ namespace Elwig.Windows {
|
|||||||
list.ForEach(v => v.Schedule.AnnouncedWeightOverride = v.AnnouncedWeight);
|
list.ForEach(v => v.Schedule.AnnouncedWeightOverride = v.AnnouncedWeight);
|
||||||
var deliverySchedules = list.Select(v => v.Schedule).ToList();
|
var deliverySchedules = list.Select(v => v.Schedule).ToList();
|
||||||
ControlUtils.RenewItemsSource(DeliveryScheduleList, deliverySchedules
|
ControlUtils.RenewItemsSource(DeliveryScheduleList, deliverySchedules
|
||||||
.Where(s => !ViewModel.FilterOnlyUpcoming || s.DateString.CompareTo(DateTime.Today.ToString("yyyy-MM-dd")) >= 0)
|
.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);
|
.ToList(), DeliveryScheduleList_SelectionChanged, ViewModel.FilterFromAllSchedules ? ControlUtils.RenewSourceDefault.None : ControlUtils.RenewSourceDefault.First);
|
||||||
ControlUtils.RenewItemsSource(DeliveryScheduleInput, deliverySchedules, DeliveryScheduleInput_SelectionChanged);
|
ControlUtils.RenewItemsSource(DeliveryScheduleInput, deliverySchedules, DeliveryScheduleInput_SelectionChanged);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ namespace Elwig.Windows {
|
|||||||
public MailWindow(int? year = null) {
|
public MailWindow(int? year = null) {
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
using (var ctx = new AppDbContext()) {
|
using (var ctx = new AppDbContext()) {
|
||||||
Year = year ?? ctx.Seasons.OrderByDescending(s => s.Year).FirstOrDefault()?.Year ?? DateTime.Today.Year;
|
Year = year ?? ctx.Seasons.OrderByDescending(s => s.Year).FirstOrDefault()?.Year ?? Utils.Today.Year;
|
||||||
Title = $"Rundschreiben - Lese {Year} - Elwig";
|
Title = $"Rundschreiben - Lese {Year} - Elwig";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ namespace Elwig.Windows {
|
|||||||
PostalSender1.Text = App.Client.Sender1;
|
PostalSender1.Text = App.Client.Sender1;
|
||||||
PostalSender2.Text = App.Client.Sender2;
|
PostalSender2.Text = App.Client.Sender2;
|
||||||
PostalLocation.Text = App.BranchLocation;
|
PostalLocation.Text = App.BranchLocation;
|
||||||
PostalDate.Text = $"{DateTime.Today:dd.MM.yyyy}";
|
PostalDate.Text = $"{Utils.Today:dd.MM.yyyy}";
|
||||||
EmailSubjectInput.Text = App.Client.TextEmailSubject ?? "Rundschreiben";
|
EmailSubjectInput.Text = App.Client.TextEmailSubject ?? "Rundschreiben";
|
||||||
EmailBodyInput.Text = App.Client.TextEmailBody ?? "Sehr geehrtes Mitglied,\n\nim Anhang finden Sie das aktuelle Rundschreiben.\n\nIhre Winzergenossenschaft\n";
|
EmailBodyInput.Text = App.Client.TextEmailBody ?? "Sehr geehrtes Mitglied,\n\nim Anhang finden Sie das aktuelle Rundschreiben.\n\nIhre Winzergenossenschaft\n";
|
||||||
}
|
}
|
||||||
@@ -438,7 +438,7 @@ namespace Elwig.Windows {
|
|||||||
|
|
||||||
private void Date_LostFocus(object sender, RoutedEventArgs evt) {
|
private void Date_LostFocus(object sender, RoutedEventArgs evt) {
|
||||||
var res = Validator.CheckDate((TextBox)sender, true);
|
var res = Validator.CheckDate((TextBox)sender, true);
|
||||||
if (!res.IsValid) ((TextBox)sender).Text = $"{DateTime.Today:dd.MM.yyyy}";
|
if (!res.IsValid) ((TextBox)sender).Text = $"{Utils.Today:dd.MM.yyyy}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateRecipients(AppDbContext ctx) {
|
private async Task UpdateRecipients(AppDbContext ctx) {
|
||||||
|
|||||||
@@ -50,27 +50,22 @@ namespace Elwig.Windows {
|
|||||||
_syncTimer.Start();
|
_syncTimer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Window_Closing(object sender, CancelEventArgs evt) {
|
private void Window_Closing(object sender, CancelEventArgs evt) {
|
||||||
evt.Cancel = !App.ForceShutdown;
|
|
||||||
|
|
||||||
if (App.NumWindows > 1 && !App.ForceShutdown) {
|
if (App.NumWindows > 1 && !App.ForceShutdown) {
|
||||||
foreach (var w in Application.Current.Windows.Cast<Window>().Where(w => ((w as AdministrationWindow)?.IsEditing ?? false) || ((w as AdministrationWindow)?.IsCreating ?? false))) {
|
foreach (var w in App.Current.Windows.Cast<Window>().Where(w => ((w as AdministrationWindow)?.IsEditing ?? false) || ((w as AdministrationWindow)?.IsCreating ?? false))) {
|
||||||
try {
|
try {
|
||||||
w.Close();
|
w.Close();
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
Thread.Sleep(100);
|
Thread.Sleep(100);
|
||||||
if (App.NumWindows > 1 && !Application.Current.Windows.Cast<Window>().Any(w => ((w as AdministrationWindow)?.IsEditing ?? false) || ((w as AdministrationWindow)?.IsCreating ?? false))) {
|
if (App.NumWindows > 1 && !App.Current.Windows.Cast<Window>().Any(w => ((w as AdministrationWindow)?.IsEditing ?? false) || ((w as AdministrationWindow)?.IsCreating ?? false))) {
|
||||||
if (!InteractionService.AskConfirmation("Elwig beenden", "Es sind noch weitere Fenster geöffnet.\nSollen alle Fenster geschlossen werden?")) {
|
if (!InteractionService.AskConfirmation("Elwig beenden", "Es sind noch weitere Fenster geöffnet.\nSollen alle Fenster geschlossen werden?")) {
|
||||||
return;
|
evt.Cancel = true;
|
||||||
|
} else {
|
||||||
|
Application.Current.Shutdown();
|
||||||
}
|
}
|
||||||
App.ForceShutdown = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Mouse.OverrideCursor = Cursors.AppStarting;
|
|
||||||
await (Application.Current as App)!.OnClosing();
|
|
||||||
Application.Current.Shutdown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Menu_Help_About_Click(object sender, RoutedEventArgs evt) {
|
private void Menu_Help_About_Click(object sender, RoutedEventArgs evt) {
|
||||||
@@ -167,7 +162,7 @@ namespace Elwig.Windows {
|
|||||||
|
|
||||||
private async void Menu_Database_Backup_Click(object sender, RoutedEventArgs evt) {
|
private async void Menu_Database_Backup_Click(object sender, RoutedEventArgs evt) {
|
||||||
try {
|
try {
|
||||||
var filename = InteractionService.SaveFile("Datenbank-Sicherung", $"database_{DateTime.Today:yyyy-MM-dd}", "sql.zip");
|
var filename = InteractionService.SaveFile("Datenbank-Sicherung", $"database_{Utils.Today:yyyy-MM-dd}", "sql.zip");
|
||||||
if (filename != null) {
|
if (filename != null) {
|
||||||
if (!filename.EndsWith(".sql.zip")) filename += ".sql.zip";
|
if (!filename.EndsWith(".sql.zip")) filename += ".sql.zip";
|
||||||
Mouse.OverrideCursor = Cursors.Wait;
|
Mouse.OverrideCursor = Cursors.Wait;
|
||||||
@@ -182,7 +177,22 @@ namespace Elwig.Windows {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async void Menu_Database_Restore_Click(object sender, RoutedEventArgs evt) {
|
private async void Menu_Database_Restore_Click(object sender, RoutedEventArgs evt) {
|
||||||
await BackupService.RestoreDatabase();
|
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) {
|
||||||
|
if (!InteractionService.AskContinue("Datenbank wiederherstellen", "Soll die Datenbank wirklich unwiederruflich durch die wiederhergestellte Version ersetzt werden?"))
|
||||||
|
return;
|
||||||
|
Mouse.OverrideCursor = Cursors.Wait;
|
||||||
|
await App.ReplaceDatabase(d.FileName);
|
||||||
|
}
|
||||||
|
} catch (Exception exc) {
|
||||||
|
InteractionService.ShowException(exc);
|
||||||
|
}
|
||||||
|
Mouse.OverrideCursor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void SyncButton_Click(object sender, RoutedEventArgs evt) {
|
private async void SyncButton_Click(object sender, RoutedEventArgs evt) {
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ namespace Elwig.Windows {
|
|||||||
|
|
||||||
int mgnr;
|
int mgnr;
|
||||||
try {
|
try {
|
||||||
mgnr = await ViewModel.UpdateMemberHistory(ViewModel.SelectedHistoryEntry?.HistNr, IsEditing || ViewModel.DateNotice >= DateOnly.FromDateTime(DateTime.Today).AddDays(-14));
|
mgnr = await ViewModel.UpdateMemberHistory(ViewModel.SelectedHistoryEntry?.HistNr, IsEditing || ViewModel.DateNotice >= DateOnly.FromDateTime(Utils.Today).AddDays(-14));
|
||||||
App.HintContextChange();
|
App.HintContextChange();
|
||||||
} catch (Exception exc) {
|
} catch (Exception exc) {
|
||||||
InteractionService.ShowDbException("Geschäftsanteilbewegung aktualisieren", exc);
|
InteractionService.ShowDbException("Geschäftsanteilbewegung aktualisieren", exc);
|
||||||
|
|||||||
@@ -13,6 +13,5 @@ Data and configuration folder
|
|||||||
C:\ProgramData\Elwig\
|
C:\ProgramData\Elwig\
|
||||||
- config.ini : main configuration file
|
- config.ini : main configuration file
|
||||||
- database.sqlite3 : stores all data
|
- database.sqlite3 : stores all data
|
||||||
- backups\ : (automatic) backups of the database
|
|
||||||
- imported.txt : list of all imported *.elwig.zip files to not automatically import them again
|
- imported.txt : list of all imported *.elwig.zip files to not automatically import them again
|
||||||
- mails\ : sent/outgoing email log
|
- mails\ : sent/outgoing email log
|
||||||
|
|||||||
@@ -3,22 +3,15 @@
|
|||||||
; Only needed, if more than one branch is stored in database
|
; Only needed, if more than one branch is stored in database
|
||||||
;branch = Zweigstelle
|
;branch = Zweigstelle
|
||||||
;debug = true
|
;debug = true
|
||||||
;weighing = gross ; gross/net/box
|
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
; Relative or absolute path to database file
|
; Relative or absolute path to database file
|
||||||
file = database.sqlite3
|
file = database.sqlite3
|
||||||
|
; Enables automatic compressed database backups
|
||||||
|
backup = true
|
||||||
; Enables database logging
|
; Enables database logging
|
||||||
;log = db.log
|
;log = db.log
|
||||||
|
|
||||||
[backup]
|
|
||||||
;auto = false
|
|
||||||
;path = backups
|
|
||||||
;retain_hours = 36
|
|
||||||
;retain_days = 8
|
|
||||||
;retain_weeks = 6
|
|
||||||
;retain_months = 18
|
|
||||||
|
|
||||||
[update]
|
[update]
|
||||||
url = https://elwig.at/files/elwig/latest
|
url = https://elwig.at/files/elwig/latest
|
||||||
auto = true
|
auto = true
|
||||||
@@ -47,8 +40,6 @@ auto = true
|
|||||||
;limit = 3500
|
;limit = 3500
|
||||||
; Enables scale logging
|
; Enables scale logging
|
||||||
;log = waage.log
|
;log = waage.log
|
||||||
; Opt-out of auto time synchronization at startup
|
|
||||||
;synctime = false
|
|
||||||
|
|
||||||
;[scale.B]
|
;[scale.B]
|
||||||
;type = Avery-Async
|
;type = Avery-Async
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ About
|
|||||||
**Product:** Elwig
|
**Product:** Elwig
|
||||||
**Description:** Electronic Management for Vintners' Cooperatives
|
**Description:** Electronic Management for Vintners' Cooperatives
|
||||||
**Type:** ERP system
|
**Type:** ERP system
|
||||||
**Version:** 1.1.2.0 ([Changelog](./CHANGELOG.md))
|
**Version:** 1.1.1.1 ([Changelog](./CHANGELOG.md))
|
||||||
**License:** [GNU General Public License 3.0 (GPLv3)](./LICENSE)
|
**License:** [GNU General Public License 3.0 (GPLv3)](./LICENSE)
|
||||||
**Website:** https://elwig.at/
|
**Website:** https://elwig.at/
|
||||||
**Source code:** https://git.necronda.net/winzer/elwig
|
**Source code:** https://git.necronda.net/winzer/elwig
|
||||||
@@ -33,7 +33,7 @@ Packaging: [WiX Toolset](https://www.firegiant.com/wixtoolset/)
|
|||||||
**Produkt:** Elwig
|
**Produkt:** Elwig
|
||||||
**Beschreibung:** Elektronische Winzergenossenschaftsverwaltung
|
**Beschreibung:** Elektronische Winzergenossenschaftsverwaltung
|
||||||
**Typ:** Warenwirtschaftssystem (ERP-System)
|
**Typ:** Warenwirtschaftssystem (ERP-System)
|
||||||
**Version:** 1.1.2.0 ([Änderungsprotokoll](./CHANGELOG.md))
|
**Version:** 1.1.1.1 ([Änderungsprotokoll](./CHANGELOG.md))
|
||||||
**Lizenz:** [GNU General Public License 3.0 (GPLv3)](./LICENSE)
|
**Lizenz:** [GNU General Public License 3.0 (GPLv3)](./LICENSE)
|
||||||
**Website:** https://elwig.at/
|
**Website:** https://elwig.at/
|
||||||
**Quellcode:** https://git.necronda.net/winzer/elwig
|
**Quellcode:** https://git.necronda.net/winzer/elwig
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ namespace Tests.E2ETests {
|
|||||||
Session.App.FindElement(By.Name("Rundschreiben")).Click();
|
Session.App.FindElement(By.Name("Rundschreiben")).Click();
|
||||||
Thread.Sleep(Utils.WINDOW_OPEN_SLEEP);
|
Thread.Sleep(Utils.WINDOW_OPEN_SLEEP);
|
||||||
var window = Session.CreateWindowDriver("MailWindow");
|
var window = Session.CreateWindowDriver("MailWindow");
|
||||||
Assert.That(window.Title, Is.EqualTo($"Rundschreiben - Lese {DateTime.Today.Year} - Elwig"));
|
Assert.That(window.Title, Is.EqualTo($"Rundschreiben - Lese {Elwig.Helpers.Utils.Today.Year} - Elwig"));
|
||||||
window.Close();
|
window.Close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
<PackageReference Include="Appium.WebDriver" Version="4.4.5" />
|
<PackageReference Include="Appium.WebDriver" Version="4.4.5" />
|
||||||
<PackageReference Include="NReco.PdfRenderer" Version="1.6.0" />
|
<PackageReference Include="NReco.PdfRenderer" Version="1.6.0" />
|
||||||
<PackageReference Include="NUnit" Version="4.6.1" />
|
<PackageReference Include="NUnit" Version="4.6.1" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="6.3.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
|
||||||
<PackageReference Include="NUnit.Analyzers" Version="4.14.0">
|
<PackageReference Include="NUnit.Analyzers" Version="4.14.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace Tests.UnitTests.DocumentTests {
|
|||||||
"""));
|
"""));
|
||||||
Assert.That(text, Contains.Substring("0123463")); // Betriebsnummer
|
Assert.That(text, Contains.Substring("0123463")); // Betriebsnummer
|
||||||
Assert.That(text, Contains.Substring("pauschaliert"));
|
Assert.That(text, Contains.Substring("pauschaliert"));
|
||||||
Assert.That(text, Contains.Substring($"Wolkersdorf, am {DateTime.Today:dd.MM.yyyy}"));
|
Assert.That(text, Contains.Substring($"Wolkersdorf, am {Elwig.Helpers.Utils.Today:dd.MM.yyyy}"));
|
||||||
Assert.That(text, Contains.Substring("Traubengutschrift Max Mustermann – Probevariante"));
|
Assert.That(text, Contains.Substring("Traubengutschrift Max Mustermann – Probevariante"));
|
||||||
Assert.That(text, Contains.Substring("AT81 1234 5678 9012 3457"));
|
Assert.That(text, Contains.Substring("AT81 1234 5678 9012 3457"));
|
||||||
Assert.That(text, Contains.Substring("""
|
Assert.That(text, Contains.Substring("""
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace Tests.UnitTests.DocumentTests {
|
|||||||
"""));
|
"""));
|
||||||
Assert.That(text, Contains.Substring("0123463")); // Betriebsnummer
|
Assert.That(text, Contains.Substring("0123463")); // Betriebsnummer
|
||||||
Assert.That(text, Contains.Substring("pauschaliert"));
|
Assert.That(text, Contains.Substring("pauschaliert"));
|
||||||
Assert.That(text, Contains.Substring($"Wolkersdorf, am {DateTime.Today:dd.MM.yyyy}"));
|
Assert.That(text, Contains.Substring($"Wolkersdorf, am {Elwig.Helpers.Utils.Today:dd.MM.yyyy}"));
|
||||||
Assert.That(text, Contains.Substring("Anlieferungsbestätigung 2020"));
|
Assert.That(text, Contains.Substring("Anlieferungsbestätigung 2020"));
|
||||||
Assert.That(text, Contains.Substring("""
|
Assert.That(text, Contains.Substring("""
|
||||||
20201001X001 1 Grüner Veltliner QUW 73 15,0 ungeb.: 3 219 3 219 ☑
|
20201001X001 1 Grüner Veltliner QUW 73 15,0 ungeb.: 3 219 3 219 ☑
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ namespace Tests.UnitTests.DocumentTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[OneTimeSetUp]
|
[OneTimeSetUp]
|
||||||
public void SetupPrinting() {
|
public async Task SetupPrinting() {
|
||||||
Pdf.Init();
|
await Pdf.Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
[OneTimeTearDown]
|
[OneTimeTearDown]
|
||||||
public void TeardownPrinting() {
|
public async Task TeardownPrinting() {
|
||||||
Pdf.Cleanup();
|
await Pdf.Cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -842,151 +842,5 @@ namespace Tests.UnitTests.ServiceTests {
|
|||||||
Assert.That(await ctx.Deliveries.FindAsync(2022, 2), Is.Null);
|
Assert.That(await ctx.Deliveries.FindAsync(2022, 2), Is.Null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly DateTime[] DeliveryDates = [
|
|
||||||
new(2020, 10, 1, 17, 45, 0),
|
|
||||||
new(2020, 10, 1, 18, 30, 0),
|
|
||||||
|
|
||||||
new(2020, 10, 2, 8, 34, 0),
|
|
||||||
new(2020, 10, 2, 9, 45, 0),
|
|
||||||
new(2020, 10, 2, 10, 19, 0),
|
|
||||||
new(2020, 10, 2, 11, 22, 0),
|
|
||||||
new(2020, 10, 2, 13, 39, 0),
|
|
||||||
new(2020, 10, 2, 14, 2, 0),
|
|
||||||
new(2020, 10, 2, 15, 55, 0),
|
|
||||||
new(2020, 10, 2, 16, 28, 0),
|
|
||||||
new(2020, 10, 2, 17, 13, 0),
|
|
||||||
new(2020, 10, 2, 18, 10, 0),
|
|
||||||
|
|
||||||
new(2020, 10, 3, 8, 13, 0),
|
|
||||||
new(2020, 10, 3, 9, 39, 0),
|
|
||||||
new(2020, 10, 3, 10, 20, 0),
|
|
||||||
new(2020, 10, 3, 11, 46, 0),
|
|
||||||
new(2020, 10, 3, 13, 56, 0),
|
|
||||||
new(2020, 10, 3, 14, 4, 0),
|
|
||||||
new(2020, 10, 3, 15, 33, 0),
|
|
||||||
new(2020, 10, 3, 16, 23, 0),
|
|
||||||
new(2020, 10, 3, 17, 18, 0),
|
|
||||||
new(2020, 10, 3, 18, 19, 0),
|
|
||||||
new(2020, 10, 3, 19, 10, 0),
|
|
||||||
new(2020, 10, 3, 20, 25, 0),
|
|
||||||
|
|
||||||
new(2020, 10, 4, 5, 57, 0),
|
|
||||||
new(2020, 10, 4, 6, 13, 0),
|
|
||||||
new(2020, 10, 4, 7, 42, 0),
|
|
||||||
new(2020, 10, 4, 8, 34, 0),
|
|
||||||
new(2020, 10, 4, 9, 45, 0),
|
|
||||||
new(2020, 10, 4, 10, 19, 0),
|
|
||||||
new(2020, 10, 4, 11, 22, 0),
|
|
||||||
new(2020, 10, 4, 13, 39, 0),
|
|
||||||
new(2020, 10, 4, 14, 2, 0),
|
|
||||||
new(2020, 10, 4, 15, 55, 0),
|
|
||||||
new(2020, 10, 4, 16, 28, 0),
|
|
||||||
new(2020, 10, 4, 17, 13, 0),
|
|
||||||
new(2020, 10, 4, 18, 10, 0),
|
|
||||||
|
|
||||||
new(2020, 10, 5, 5, 57, 0),
|
|
||||||
new(2020, 10, 5, 6, 13, 0),
|
|
||||||
new(2020, 10, 5, 7, 42, 0),
|
|
||||||
new(2020, 10, 5, 8, 34, 0),
|
|
||||||
new(2020, 10, 5, 9, 45, 0),
|
|
||||||
new(2020, 10, 5, 10, 19, 0),
|
|
||||||
new(2020, 10, 5, 11, 22, 0),
|
|
||||||
new(2020, 10, 5, 13, 39, 0),
|
|
||||||
new(2020, 10, 5, 14, 2, 0),
|
|
||||||
new(2020, 10, 5, 15, 55, 0),
|
|
||||||
new(2020, 10, 5, 16, 28, 0),
|
|
||||||
new(2020, 10, 5, 17, 13, 0),
|
|
||||||
new(2020, 10, 5, 18, 10, 0),
|
|
||||||
new(2020, 10, 5, 19, 10, 0),
|
|
||||||
new(2020, 10, 5, 20, 25, 0),
|
|
||||||
|
|
||||||
new(2020, 10, 6, 16, 23, 0),
|
|
||||||
new(2020, 10, 6, 22, 24, 0),
|
|
||||||
new(2020, 10, 6, 23, 45, 0),
|
|
||||||
new(2020, 10, 7, 0, 32, 0),
|
|
||||||
new(2020, 10, 7, 1, 12, 0),
|
|
||||||
new(2020, 10, 7, 2, 16, 0),
|
|
||||||
new(2020, 10, 7, 3, 24, 0),
|
|
||||||
new(2020, 10, 7, 4, 9, 0),
|
|
||||||
new(2020, 10, 7, 5, 57, 0),
|
|
||||||
new(2020, 10, 7, 6, 41, 0),
|
|
||||||
new(2020, 10, 7, 7, 10, 0),
|
|
||||||
|
|
||||||
new(2020, 10, 7, 17, 23, 0),
|
|
||||||
new(2020, 10, 7, 18, 16, 0),
|
|
||||||
new(2020, 10, 7, 19, 39, 0),
|
|
||||||
new(2020, 10, 7, 20, 31, 0),
|
|
||||||
new(2020, 10, 7, 21, 23, 0),
|
|
||||||
new(2020, 10, 7, 22, 24, 0),
|
|
||||||
new(2020, 10, 7, 23, 45, 0),
|
|
||||||
new(2020, 10, 8, 0, 32, 0),
|
|
||||||
new(2020, 10, 8, 1, 12, 0),
|
|
||||||
new(2020, 10, 8, 2, 16, 0),
|
|
||||||
new(2020, 10, 8, 3, 24, 0),
|
|
||||||
new(2020, 10, 8, 4, 9, 0),
|
|
||||||
new(2020, 10, 8, 5, 57, 0),
|
|
||||||
new(2020, 10, 8, 6, 41, 0),
|
|
||||||
new(2020, 10, 8, 7, 10, 0),
|
|
||||||
|
|
||||||
new(2020, 10, 9, 7, 53, 0),
|
|
||||||
new(2020, 10, 9, 12, 36, 0),
|
|
||||||
new(2020, 10, 9, 16, 26, 0),
|
|
||||||
new(2020, 10, 9, 20, 19, 0),
|
|
||||||
];
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Test_GetFilterToday_01_Empty() {
|
|
||||||
Assert.That(DeliveryService.GetFilterToday(new(2020, 10, 10), DeliveryDates),
|
|
||||||
Is.Null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Test_GetFilterToday_02_Normal() {
|
|
||||||
Assert.That(DeliveryService.GetFilterToday(new(2020, 10, 2), DeliveryDates),
|
|
||||||
Is.EqualTo((new DateTime(2020, 10, 2, 8, 0, 0), new DateTime(2020, 10, 2, 19, 0, 0))));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Test_GetFilterToday_03_Overtime() {
|
|
||||||
Assert.That(DeliveryService.GetFilterToday(new(2020, 10, 3), DeliveryDates),
|
|
||||||
Is.EqualTo((new DateTime(2020, 10, 3, 8, 0, 0), new DateTime(2020, 10, 3, 21, 0, 0))));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Test_GetFilterToday_04_EarlyStart() {
|
|
||||||
Assert.That(DeliveryService.GetFilterToday(new(2020, 10, 4), DeliveryDates),
|
|
||||||
Is.EqualTo((new DateTime(2020, 10, 4, 5, 0, 0), new DateTime(2020, 10, 4, 19, 0, 0))));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Test_GetFilterToday_05_EarlyStartAndOvertime() {
|
|
||||||
Assert.That(DeliveryService.GetFilterToday(new(2020, 10, 5), DeliveryDates),
|
|
||||||
Is.EqualTo((new DateTime(2020, 10, 5, 5, 0, 0), new DateTime(2020, 10, 5, 21, 0, 0))));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Test_GetFilterToday_06_NightShift() {
|
|
||||||
using (Assert.EnterMultipleScope()) {
|
|
||||||
Assert.That(DeliveryService.GetFilterToday(new(2020, 10, 6), DeliveryDates),
|
|
||||||
Is.EqualTo((new DateTime(2020, 10, 6, 22, 0, 0), new DateTime(2020, 10, 7, 0, 0, 0))));
|
|
||||||
Assert.That(DeliveryService.GetFilterToday(new(2020, 10, 7), DeliveryDates.Where(d => d <= new DateTime(2020, 10, 7, 12, 0, 0))),
|
|
||||||
Is.EqualTo((new DateTime(2020, 10, 6, 22, 0, 0), new DateTime(2020, 10, 7, 8, 0, 0))));
|
|
||||||
Assert.That(DeliveryService.GetFilterToday(new(2020, 10, 7), DeliveryDates.Where(d => d <= new DateTime(2020, 10, 7, 12, 0, 0)).Append(new(2020, 10, 7, 15, 10, 0))),
|
|
||||||
Is.EqualTo((new DateTime(2020, 10, 7, 15, 0, 0), new DateTime(2020, 10, 7, 16, 0, 0))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Test_GetFilterToday_07_EarlyStartNightShift() {
|
|
||||||
using (Assert.EnterMultipleScope()) {
|
|
||||||
Assert.That(DeliveryService.GetFilterToday(new(2020, 10, 7), DeliveryDates),
|
|
||||||
Is.EqualTo((new DateTime(2020, 10, 7, 17, 0, 0), new DateTime(2020, 10, 8, 0, 0, 0))));
|
|
||||||
Assert.That(DeliveryService.GetFilterToday(new(2020, 10, 8), DeliveryDates),
|
|
||||||
Is.EqualTo((new DateTime(2020, 10, 7, 17, 0, 0), new DateTime(2020, 10, 8, 8, 0, 0))));
|
|
||||||
Assert.That(DeliveryService.GetFilterToday(new(2020, 10, 8), DeliveryDates.Append(new(2020, 10, 8, 15, 10, 0))),
|
|
||||||
Is.EqualTo((new DateTime(2020, 10, 8, 15, 0, 0), new DateTime(2020, 10, 8, 16, 0, 0))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user