Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70e1e7c73a | ||
|
|
7d5d7d9307 | ||
|
|
b468bbce31 | ||
|
|
a79631c586 | ||
|
|
20b6e11b7f |
@@ -2,6 +2,30 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
[v1.1.1.1][v1.1.1.1] (2026-08-24) {#v1.1.1.1}
|
||||
---------------------------------------------
|
||||
|
||||
### Sonstiges {#v1.1.1.1-misc}
|
||||
|
||||
* Beim Export der Flächenbindungen im Haupt-Fenster (`MainWindow`) werden neben der zusammengefassten gebundenen Fläche nun auch die jeweiligen Flächenindungen exportiert. (b468bbce31)
|
||||
|
||||
[v1.1.1.1]: https://git.necronda.net/winzer/elwig/releases/tag/v1.1.1.1
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.1.1.0][v1.1.1.0] (2026-08-19) {#v1.1.1.0}
|
||||
---------------------------------------------
|
||||
|
||||
### Sonstiges {#v1.1.1.0-misc}
|
||||
|
||||
* Bei der BKI-Traubentransportscheinliste werden Volllieferanten/Vollablieferer nun auch als solche markiert. (20b6e11b7f)
|
||||
|
||||
[v1.1.1.0]: https://git.necronda.net/winzer/elwig/releases/tag/v1.1.1.0
|
||||
|
||||
|
||||
|
||||
|
||||
[v1.1.0.3][v1.1.0.3] (2026-08-17) {#v1.1.0.3}
|
||||
---------------------------------------------
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace Elwig {
|
||||
public static bool ForceShutdown { get; private set; } = false;
|
||||
|
||||
private readonly DispatcherTimer _autoUpdateTimer = new() { Interval = TimeSpan.FromHours(1) };
|
||||
private readonly DispatcherTimer _autoBackupTimer = new() { Interval = TimeSpan.FromHours(1) };
|
||||
private DatabaseBackupService? _databaseBackupService;
|
||||
public readonly SerialPortWatcher SerialPortWatcher = new();
|
||||
|
||||
public static readonly string DataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Elwig");
|
||||
@@ -101,6 +103,13 @@ namespace Elwig {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Config.DatabaseBackup) {
|
||||
_databaseBackupService = new DatabaseBackupService(Config.DatabaseFile);
|
||||
await _databaseBackupService.RunIfNeededAsync();
|
||||
_autoBackupTimer.Tick += OnAutoBackupTimer;
|
||||
_autoBackupTimer.Start();
|
||||
}
|
||||
|
||||
LastChanged = CurrentLastWrite;
|
||||
ContextTimer.Start();
|
||||
|
||||
@@ -191,6 +200,7 @@ namespace Elwig {
|
||||
}
|
||||
|
||||
private async void Application_Exit(object sender, ExitEventArgs evt) {
|
||||
_autoBackupTimer.Stop();
|
||||
SerialPortWatcher.Dispose();
|
||||
foreach (var s in EventScales) {
|
||||
s.Dispose();
|
||||
@@ -198,6 +208,12 @@ namespace Elwig {
|
||||
await Pdf.Cleanup();
|
||||
}
|
||||
|
||||
private async void OnAutoBackupTimer(object? sender, EventArgs evt) {
|
||||
if (_databaseBackupService != null) {
|
||||
await _databaseBackupService.RunIfNeededAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetBranch(Branch b) {
|
||||
SetBranch((b.ZwstId, b.Name, b.PostalDest?.AtPlz?.Plz, b.PostalDest?.AtPlz?.Ort.Name, b.Address, b.PhoneNr, b.FaxNr, b.MobileNr));
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
<ApplicationIcon>Resources\Images\Elwig.ico</ApplicationIcon>
|
||||
<Version>1.1.0.3</Version>
|
||||
<Version>1.1.1.1</Version>
|
||||
<SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<ApplicationManifest>App.manifest</ApplicationManifest>
|
||||
|
||||
@@ -69,7 +69,8 @@ namespace Elwig.Helpers {
|
||||
public DbSet<OverUnderDeliveryRow> OverUnderDeliveryRows { get; private set; }
|
||||
public DbSet<AreaComUnderDeliveryRowSingle> AreaComUnderDeliveryRows { get; private set; }
|
||||
public DbSet<MemberDeliveryPerVarietyRowSingle> MemberDeliveryPerVariantRows { get; private set; }
|
||||
public DbSet<MemberAreaComsRowSingle> MemberAreaComsRows { get; private set; }
|
||||
public DbSet<MemberAreaComTotalsRowSingle> MemberAreaComTotalsRows { get; private set; }
|
||||
public DbSet<MemberAreaComsRow> MemberAreaComsRows { get; private set; }
|
||||
public DbSet<CreditNoteDeliveryRowSingle> CreditNoteDeliveryRows { get; private set; }
|
||||
public DbSet<CreditNoteRowSingle> CreditNoteRows { get; private set; }
|
||||
public DbSet<WeightBreakdownRow> WeightBreakDownRows { get; private set; }
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Elwig.Helpers {
|
||||
public static class AppDbUpdater {
|
||||
|
||||
// Don't forget to update value in Tests/fetch-resources.bat!
|
||||
public static readonly int RequiredSchemaVersion = 40;
|
||||
public static readonly int RequiredSchemaVersion = 41;
|
||||
|
||||
private static int VersionOffset = 0;
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace Elwig.Helpers {
|
||||
|
||||
public bool Debug;
|
||||
public string DatabaseFile = App.DataPath + "database.sqlite3";
|
||||
public bool DatabaseBackup = true;
|
||||
public string? DatabaseLog = null;
|
||||
public string? Branch = null;
|
||||
public WeighingMode? WeighingMode;
|
||||
@@ -76,6 +77,8 @@ namespace Elwig.Helpers {
|
||||
var config = new ConfigurationBuilder().AddIniFile(FileName).Build();
|
||||
|
||||
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"];
|
||||
DatabaseLog = log != null ? Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, log) : null;
|
||||
Branch = config["general:branch"];
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using System;
|
||||
|
||||
namespace Elwig.Helpers.Export {
|
||||
|
||||
using Row = Tuple<(string?, string?, string?, string?, string, int, string, int), (string, int, string, string, string, int, string, double, double)>;
|
||||
using Row = Tuple<(string?, string?, string?, string?, string, int, string, bool, int), (string, int, string, string, string, int, string, double, double)>;
|
||||
|
||||
public class Bki : Csv<Row> {
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Elwig.Helpers.Export {
|
||||
using var cnx = await AppDbContext.ConnectAsync();
|
||||
using var cmd = cnx.CreateCommand();
|
||||
cmd.CommandText = $"""
|
||||
SELECT lfbis_nr, name, other_names, billing_name, address, plz, ort, area,
|
||||
SELECT lfbis_nr, name, other_names, billing_name, address, plz, ort, vollablieferer, area,
|
||||
date, weight, type, sortid, qualid, year, hkid, kmw, oe
|
||||
FROM v_bki_delivery
|
||||
WHERE year = {year}
|
||||
@@ -38,8 +38,8 @@ namespace Elwig.Helpers.Export {
|
||||
List<Row> rows = [];
|
||||
while (await r.ReadAsync()) {
|
||||
rows.Add(new(
|
||||
(r.IsDBNull(0) ? null : r.GetString(0), r.IsDBNull(1) ? null : r.GetString(1), r.IsDBNull(2) ? null : r.GetString(2), r.IsDBNull(3) ? null : r.GetString(3), r.GetString(4), r.GetInt32(5), r.GetString(6), r.GetInt32(7)),
|
||||
(r.GetString(8), r.GetInt32(9), r.GetString(10), r.GetString(11), r.GetString(12), r.GetInt32(13), r.GetString(14), r.GetDouble(15), r.GetDouble(16))
|
||||
(r.IsDBNull(0) ? null : r.GetString(0), r.IsDBNull(1) ? null : r.GetString(1), r.IsDBNull(2) ? null : r.GetString(2), r.IsDBNull(3) ? null : r.GetString(3), r.GetString(4), r.GetInt32(5), r.GetString(6), r.GetBoolean(7), r.GetInt32(8)),
|
||||
(r.GetString(9), r.GetInt32(10), r.GetString(11), r.GetString(12), r.GetString(13), r.GetInt32(14), r.GetString(15), r.GetDouble(16), r.GetDouble(17))
|
||||
));
|
||||
}
|
||||
|
||||
@@ -52,14 +52,14 @@ namespace Elwig.Helpers.Export {
|
||||
|
||||
public override string FormatRow(Row row) {
|
||||
var (member, delivery) = row;
|
||||
var (lfBisNr, familyName, name, billingName, address, plz, ort, area) = member;
|
||||
var (lfBisNr, familyName, name, billingName, address, plz, ort, vollablieferer, area) = member;
|
||||
var (date, weight, type, sortid, qualid, year, hkid, kmw, oe) = delivery;
|
||||
|
||||
var (n1, n2) = billingName == null ? (familyName, name) : Utils.SplitName(billingName, familyName);
|
||||
var (a1, a2) = Utils.SplitAddress(address);
|
||||
var memberData = $"{lfBisNr};{n1};{n2};{a1};\t{a2};{plz};{ort}";
|
||||
var deliveryData = $"{string.Join(".", date.Split("-").Reverse())};{weight};TB;{(type == "W" ? "J" : "")};{(type == "R" ? "J" : "")};{sortid};;;{qualid};{year};{hkid};{kmw:0.0};{oe:0}";
|
||||
var vollData = $"N;;;{area / 10_000.0}";
|
||||
var vollData = $"{(vollablieferer ? "J" : "N")};{(vollablieferer ? area / 10_000.0 : null):N4};{(vollablieferer ? area / 10_000.0 : null):N4};{area / 10_000.0:N4}";
|
||||
|
||||
return $"{_clientData};{memberData};{deliveryData};{vollData}";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Elwig.Models.Dtos {
|
||||
class MemberAreaComTotalsData : DataTable<MemberAreaComTotalsRow> {
|
||||
|
||||
private static readonly (string, string, string?, int)[] FieldNames = [
|
||||
("MgNr", "MgNr.", null, 12),
|
||||
("Name1", "Name", null, 40),
|
||||
("Name2", "Vorname", null, 40),
|
||||
("Address", "Adresse", null, 60),
|
||||
("Plz", "PLZ", null, 10),
|
||||
("Locality", "Ort", null, 60),
|
||||
("SortIds", "Sorte", null, 12),
|
||||
("AttrIds", "Attribut", null, 16),
|
||||
("Areas", "Fläche", "m²", 22),
|
||||
("DeliveryObligations", "Lieferpflicht", "kg", 22),
|
||||
("DeliveryRights", "Lieferrecht", "kg", 22),
|
||||
];
|
||||
|
||||
public MemberAreaComTotalsData(IEnumerable<MemberAreaComTotalsRow> rows, int year) :
|
||||
base($"Gebundene Fläche", $"Gebundene Fläche pro Mitglied {year}", rows, FieldNames) {
|
||||
}
|
||||
|
||||
public static async Task<MemberAreaComTotalsData> ForSeason(DbSet<MemberAreaComTotalsRowSingle> table, int year) {
|
||||
return new MemberAreaComTotalsData(
|
||||
(await FromDbSet(table, year)).GroupBy(
|
||||
r => r.MgNr,
|
||||
(k, g) => new MemberAreaComTotalsRow(g)
|
||||
), year);
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<MemberAreaComTotalsRowSingle>> FromDbSet(DbSet<MemberAreaComTotalsRowSingle> table, int year) {
|
||||
return await table.FromSql($"""
|
||||
SELECT m.mgnr, m.name AS name_1,
|
||||
COALESCE(m.prefix || ' ', '') || m.given_name ||
|
||||
COALESCE(' ' || m.middle_names, '') || COALESCE(' ' || m.suffix, '') AS name_2,
|
||||
p.plz, o.name AS ort, m.address,
|
||||
c.bucket, c.area, c.min_kg, c.max_kg
|
||||
FROM v_area_commitment_bucket_strict c
|
||||
LEFT JOIN member m ON m.mgnr = c.mgnr
|
||||
LEFT JOIN AT_plz_dest p ON p.id = m.postal_dest
|
||||
LEFT JOIN AT_ort o ON o.okz = p.okz
|
||||
WHERE c.year = {year}
|
||||
ORDER BY m.mgnr, c.bucket
|
||||
""").ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class MemberAreaComTotalsRow {
|
||||
public int MgNr;
|
||||
public string Name1;
|
||||
public string? Name2;
|
||||
public string Address;
|
||||
public int Plz;
|
||||
public string Locality;
|
||||
public string[] SortIds;
|
||||
public string[] AttrIds;
|
||||
public int[] Areas;
|
||||
public int[] DeliveryObligations;
|
||||
public int[] DeliveryRights;
|
||||
|
||||
public MemberAreaComTotalsRow(IEnumerable<MemberAreaComTotalsRowSingle> rows) {
|
||||
var f = rows.First();
|
||||
MgNr = f.MgNr;
|
||||
Name1 = f.Name1;
|
||||
Name2 = f.Name2;
|
||||
Address = f.Address;
|
||||
Plz = f.Plz;
|
||||
Locality = f.Locality.Split(",")[0];
|
||||
SortIds = [.. rows.Select(r => r.VtrgId[..2])];
|
||||
AttrIds = [.. rows.Select(r => r.VtrgId[2..])];
|
||||
Areas = [.. rows.Select(r => r.Area)];
|
||||
DeliveryObligations = [.. rows.Select(r => r.MinKg)];
|
||||
DeliveryRights = [.. rows.Select(r => r.MaxKg)];
|
||||
}
|
||||
}
|
||||
|
||||
[Keyless]
|
||||
public class MemberAreaComTotalsRowSingle {
|
||||
[Column("mgnr")]
|
||||
public int MgNr { get; set; }
|
||||
[Column("name_1")]
|
||||
public required string Name1 { get; set; }
|
||||
[Column("name_2")]
|
||||
public string? Name2 { get; set; }
|
||||
[Column("address")]
|
||||
public required string Address { get; set; }
|
||||
[Column("plz")]
|
||||
public int Plz { get; set; }
|
||||
[Column("ort")]
|
||||
public required string Locality { get; set; }
|
||||
[Column("bucket")]
|
||||
public required string VtrgId { get; set; }
|
||||
[Column("area")]
|
||||
public int Area { get; set; }
|
||||
[Column("min_kg")]
|
||||
public int MinKg { get; set; }
|
||||
[Column("max_kg")]
|
||||
public int MaxKg { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -14,73 +14,49 @@ namespace Elwig.Models.Dtos {
|
||||
("Address", "Adresse", null, 60),
|
||||
("Plz", "PLZ", null, 10),
|
||||
("Locality", "Ort", null, 60),
|
||||
("SortIds", "Sorte", null, 12),
|
||||
("AttrIds", "Attribut", null, 16),
|
||||
("Areas", "Fläche", "m²", 22),
|
||||
("DeliveryObligations", "Lieferpflicht", "kg", 22),
|
||||
("DeliveryRights", "Lieferrecht", "kg", 22),
|
||||
("SortId", "Sorte", null, 12),
|
||||
("AttrId", "Attribut", null, 16),
|
||||
("CultId", "Bewirt.", null, 16),
|
||||
("GstNr", "Parzelle(n)", null, 25),
|
||||
("Area", "Fläche", "m²", 22),
|
||||
("MinKg", "Lieferpflicht", "kg", 22),
|
||||
("MaxKg", "Lieferrecht", "kg", 22),
|
||||
];
|
||||
|
||||
public MemberAreaComsData(IEnumerable<MemberAreaComsRow> rows, int year) :
|
||||
base($"Flächenbindungen", $"Flächenbindungen pro Mitglied {year}", rows, FieldNames) {
|
||||
}
|
||||
|
||||
public static async Task<MemberAreaComsData> ForSeason(DbSet<MemberAreaComsRowSingle> table, int year) {
|
||||
return new MemberAreaComsData(
|
||||
(await FromDbSet(table, year)).GroupBy(
|
||||
r => r.MgNr,
|
||||
(k, g) => new MemberAreaComsRow(g)
|
||||
), year);
|
||||
public static async Task<MemberAreaComsData> ForSeason(DbSet<MemberAreaComsRow> table, int year) {
|
||||
return new MemberAreaComsData(await FromDbSet(table, year), year);
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<MemberAreaComsRowSingle>> FromDbSet(DbSet<MemberAreaComsRowSingle> table, int year) {
|
||||
private static async Task<IEnumerable<MemberAreaComsRow>> FromDbSet(DbSet<MemberAreaComsRow> table, int year) {
|
||||
return await table.FromSql($"""
|
||||
SELECT m.mgnr, m.name AS name_1,
|
||||
COALESCE(m.prefix || ' ', '') || m.given_name ||
|
||||
COALESCE(' ' || m.middle_names, '') || COALESCE(' ' || m.suffix, '') AS name_2,
|
||||
p.plz, o.name AS ort, m.address,
|
||||
c.bucket, c.area, c.min_kg, c.max_kg
|
||||
FROM v_area_commitment_bucket_strict c
|
||||
t.sortid, t.attrid, c.cultid,
|
||||
c.gstnr, c.area,
|
||||
CAST(ROUND(area * COALESCE(t.min_kg_per_ha, 0) / 10000.0, 0) AS INTEGER) AS min_kg,
|
||||
CAST(ROUND(area * MIN(COALESCE(a.max_kg_per_ha, s.max_kg_per_ha), s.max_kg_per_ha) / 10000.0, 0) AS INTEGER) AS max_kg
|
||||
FROM v_virtual_season s, area_commitment c
|
||||
JOIN area_commitment_type t ON t.vtrgid = c.vtrgid
|
||||
LEFT JOIN wine_attribute a ON a.attrid = t.attrid
|
||||
LEFT JOIN member m ON m.mgnr = c.mgnr
|
||||
LEFT JOIN AT_plz_dest p ON p.id = m.postal_dest
|
||||
LEFT JOIN AT_ort o ON o.okz = p.okz
|
||||
WHERE c.year = {year}
|
||||
ORDER BY m.mgnr, c.bucket
|
||||
WHERE s.year = {year} AND
|
||||
(year_from IS NULL OR year_from <= s.year) AND
|
||||
(year_to IS NULL OR year_to >= s.year)
|
||||
ORDER BY c.mgnr, t.sortid, t.attrid, c.cultid, c.gstnr;
|
||||
""").ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class MemberAreaComsRow {
|
||||
public int MgNr;
|
||||
public string Name1;
|
||||
public string? Name2;
|
||||
public string Address;
|
||||
public int Plz;
|
||||
public string Locality;
|
||||
public string[] SortIds;
|
||||
public string[] AttrIds;
|
||||
public int[] Areas;
|
||||
public int[] DeliveryObligations;
|
||||
public int[] DeliveryRights;
|
||||
|
||||
public MemberAreaComsRow(IEnumerable<MemberAreaComsRowSingle> rows) {
|
||||
var f = rows.First();
|
||||
MgNr = f.MgNr;
|
||||
Name1 = f.Name1;
|
||||
Name2 = f.Name2;
|
||||
Address = f.Address;
|
||||
Plz = f.Plz;
|
||||
Locality = f.Locality.Split(",")[0];
|
||||
SortIds = rows.Select(r => r.VtrgId[..2]).ToArray();
|
||||
AttrIds = rows.Select(r => r.VtrgId[2..]).ToArray();
|
||||
Areas = rows.Select(r => r.Area).ToArray();
|
||||
DeliveryObligations = rows.Select(r => r.MinKg).ToArray();
|
||||
DeliveryRights = rows.Select(r => r.MaxKg).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
[Keyless]
|
||||
public class MemberAreaComsRowSingle {
|
||||
public class MemberAreaComsRow {
|
||||
[Column("mgnr")]
|
||||
public int MgNr { get; set; }
|
||||
[Column("name_1")]
|
||||
@@ -93,8 +69,14 @@ namespace Elwig.Models.Dtos {
|
||||
public int Plz { get; set; }
|
||||
[Column("ort")]
|
||||
public required string Locality { get; set; }
|
||||
[Column("bucket")]
|
||||
public required string VtrgId { get; set; }
|
||||
[Column("sortid")]
|
||||
public required string SortId { get; set; }
|
||||
[Column("attrid")]
|
||||
public string? AttrId { get; set; }
|
||||
[Column("cultid")]
|
||||
public string? CultId { get; set; }
|
||||
[Column("gstnr")]
|
||||
public required string GstNr { get; set; }
|
||||
[Column("area")]
|
||||
public int Area { get; set; }
|
||||
[Column("min_kg")]
|
||||
|
||||
@@ -118,7 +118,7 @@ namespace Elwig.Models.Entities {
|
||||
[Column("acid")]
|
||||
public double? Acid { get; set; }
|
||||
|
||||
[Column("scale_id")]
|
||||
[Column("scale_terminal")]
|
||||
public string? ScaleTerminalId { get; set; }
|
||||
|
||||
[Column("weighing_data")]
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
-- schema version 40 to 41
|
||||
|
||||
PRAGMA writable_schema = ON;
|
||||
|
||||
UPDATE sqlite_schema SET sql = REPLACE(sql, 'scale_id ', 'scale_terminal')
|
||||
WHERE type = 'table' AND name = 'delivery_part';
|
||||
|
||||
DROP VIEW v_bki_delivery;
|
||||
DROP VIEW v_bki_member;
|
||||
|
||||
CREATE VIEW v_bki_member AS
|
||||
SELECT s.year, m.mgnr, m.lfbis_nr, m.name,
|
||||
(COALESCE(m.prefix || ' ', '') || m.given_name || COALESCE(' ' || m.middle_names, '') || COALESCE(' ' || m.suffix, '')) AS other_names,
|
||||
a.name AS billing_name, COALESCE(a.address, m.address) AS address,
|
||||
COALESCE(a.country, m.country) AS country, COALESCE(a.postal_dest, m.postal_dest) AS postal_dest,
|
||||
m.volllieferant AS vollablieferer,
|
||||
SUM(COALESCE(IIF((c.year_from IS NULL OR c.year_from <= s.year) AND (c.year_to IS NULL OR c.year_to >= s.year), c.area, NULL), 0)) AS area
|
||||
FROM season s, member m
|
||||
LEFT JOIN member_billing_address a ON a.mgnr = m.mgnr
|
||||
LEFT JOIN area_commitment c ON c.mgnr = m.mgnr
|
||||
GROUP BY s.year, m.mgnr;
|
||||
|
||||
CREATE VIEW v_bki_delivery AS
|
||||
SELECT m.lfbis_nr, m.name, m.other_names, m.billing_name,
|
||||
m.address, plz.plz, IIF(INSTR(o.name, ',') = 0, o.name, SUBSTR(o.name, 1, INSTR(o.name, ',') - 1)) AS ort,
|
||||
d.date, d.weight, v.type, v.sortid, d.qualid, d.year, d.hkid, d.kmw, d.oe,
|
||||
m.vollablieferer, m.area
|
||||
FROM v_delivery d
|
||||
JOIN v_bki_member m ON (m.year, m.mgnr) = (d.year, d.mgnr)
|
||||
JOIN postal_dest pd ON (pd.country, pd.id) = (m.country, m.postal_dest)
|
||||
LEFT JOIN AT_plz_dest ap ON (ap.country, ap.id) = (pd.country, pd.id)
|
||||
LEFT JOIN AT_plz plz ON plz.plz = ap.plz
|
||||
LEFT JOIN AT_ort o ON o.okz = ap.okz
|
||||
JOIN wine_variety v ON v.sortid = d.sortid
|
||||
ORDER BY d.date, d.time;
|
||||
|
||||
PRAGMA writable_schema = OFF;
|
||||
@@ -538,9 +538,11 @@ namespace Elwig.Windows {
|
||||
App.HintContextChange();
|
||||
|
||||
using var ctx = new AppDbContext();
|
||||
var tbl = await MemberAreaComsData.ForSeason(ctx.MemberAreaComsRows, year);
|
||||
var tbl1 = await MemberAreaComTotalsData.ForSeason(ctx.MemberAreaComTotalsRows, year);
|
||||
var tbl2 = await MemberAreaComsData.ForSeason(ctx.MemberAreaComsRows, year);
|
||||
using var ods = new OdsFile(filename);
|
||||
await ods.AddTable(tbl);
|
||||
await ods.AddTable(tbl1);
|
||||
await ods.AddTable(tbl2);
|
||||
} catch (Exception exc) {
|
||||
InteractionService.ShowException(exc);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
[database]
|
||||
; Relative or absolute path to database file
|
||||
file = database.sqlite3
|
||||
; Enables automatic compressed database backups
|
||||
backup = true
|
||||
; Enables database logging
|
||||
;log = db.log
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ About
|
||||
**Product:** Elwig
|
||||
**Description:** Electronic Management for Vintners' Cooperatives
|
||||
**Type:** ERP system
|
||||
**Version:** 1.1.0.3 ([Changelog](./CHANGELOG.md))
|
||||
**Version:** 1.1.1.1 ([Changelog](./CHANGELOG.md))
|
||||
**License:** [GNU General Public License 3.0 (GPLv3)](./LICENSE)
|
||||
**Website:** https://elwig.at/
|
||||
**Source code:** https://git.necronda.net/winzer/elwig
|
||||
@@ -33,7 +33,7 @@ Packaging: [WiX Toolset](https://www.firegiant.com/wixtoolset/)
|
||||
**Produkt:** Elwig
|
||||
**Beschreibung:** Elektronische Winzergenossenschaftsverwaltung
|
||||
**Typ:** Warenwirtschaftssystem (ERP-System)
|
||||
**Version:** 1.1.0.3 ([Änderungsprotokoll](./CHANGELOG.md))
|
||||
**Version:** 1.1.1.1 ([Änderungsprotokoll](./CHANGELOG.md))
|
||||
**Lizenz:** [GNU General Public License 3.0 (GPLv3)](./LICENSE)
|
||||
**Website:** https://elwig.at/
|
||||
**Quellcode:** https://git.necronda.net/winzer/elwig
|
||||
|
||||
@@ -45,7 +45,7 @@ INSERT INTO delivery (mgnr, year, did, date, time, zwstid, lnr) VALUES
|
||||
(101, 2020, 1, '2020-10-01', NULL, 'X', 1),
|
||||
(101, 2020, 2, '2020-10-01', NULL, 'X', 2),
|
||||
(101, 2020, 3, '2020-10-01', NULL, 'X', 3);
|
||||
INSERT INTO delivery_part (year, did, dpnr, sortid, attrid, cultid, weight, kmw, qualid, hkid, kgnr, net_weight, manual_weighing, spl_check, scale_id, weighing_data, weighing_reason) VALUES
|
||||
INSERT INTO delivery_part (year, did, dpnr, sortid, attrid, cultid, weight, kmw, qualid, hkid, kgnr, net_weight, manual_weighing, spl_check, scale_terminal, weighing_data, weighing_reason) VALUES
|
||||
(2020, 1, 1, 'GV', 'K', NULL, 4000, 17, 'KAB', 'WLNO', 06109, TRUE, FALSE, FALSE, NULL, NULL, NULL),
|
||||
(2020, 1, 2, 'GV', NULL, NULL, 4000, 16, 'QUW', 'WLNO', 06109, TRUE, FALSE, FALSE, NULL, NULL, NULL),
|
||||
(2020, 2, 1, 'GV', NULL, 'B', 4000, 15, 'QUW', 'WLNO', 06109, TRUE, FALSE, FALSE, NULL, NULL, NULL),
|
||||
@@ -60,7 +60,7 @@ INSERT INTO delivery (mgnr, year, did, date, time, zwstid, lnr) VALUES
|
||||
(101, 2021, 1, '2021-10-01', NULL, 'X', 1),
|
||||
(101, 2021, 2, '2021-10-01', NULL, 'X', 2),
|
||||
(101, 2021, 3, '2021-10-01', NULL, 'X', 3);
|
||||
INSERT INTO delivery_part (year, did, dpnr, sortid, attrid, cultid, weight, kmw, qualid, hkid, kgnr, gebunden, net_weight, manual_weighing, spl_check, scale_id, weighing_data, weighing_reason) VALUES
|
||||
INSERT INTO delivery_part (year, did, dpnr, sortid, attrid, cultid, weight, kmw, qualid, hkid, kgnr, gebunden, net_weight, manual_weighing, spl_check, scale_terminal, weighing_data, weighing_reason) VALUES
|
||||
(2021, 1, 1, 'GV', 'K', NULL, 4000, 17, 'KAB', 'WLNO', 06109, TRUE, TRUE, FALSE, FALSE, NULL, NULL, NULL),
|
||||
(2021, 1, 2, 'GV', NULL, NULL, 4000, 16, 'QUW', 'WLNO', 06109, FALSE, TRUE, FALSE, FALSE, NULL, NULL, NULL),
|
||||
(2021, 2, 1, 'GV', NULL, 'B', 4000, 15, 'QUW', 'WLNO', 06109, TRUE, TRUE, FALSE, FALSE, NULL, NULL, NULL),
|
||||
|
||||
@@ -39,7 +39,7 @@ INSERT INTO delivery (mgnr, year, did, date, time, zwstid, lnr) VALUES
|
||||
(104, 2020, 11, '2020-10-03', '14:39:22', 'X', 2),
|
||||
(104, 2020, 12, '2020-10-03', '15:15:41', 'X', 3);
|
||||
|
||||
INSERT INTO delivery_part (year, did, dpnr, sortid, attrid, cultid, weight, kmw, qualid, hkid, kgnr, net_weight, manual_weighing, spl_check, scale_id, weighing_data, weighing_reason) VALUES
|
||||
INSERT INTO delivery_part (year, did, dpnr, sortid, attrid, cultid, weight, kmw, qualid, hkid, kgnr, net_weight, manual_weighing, spl_check, scale_terminal, weighing_data, weighing_reason) VALUES
|
||||
(2020, 1, 1, 'GV', NULL, NULL, 3219, 15.0, 'QUW', 'WLNO', 06109, TRUE, FALSE, FALSE, '1', '{"id":"321","nr":321,"gross_weight":3219,"tare_weight":0,"net_weight":3219,"date":"2020-10-01","time":"09:02:46"}', NULL),
|
||||
(2020, 2, 1, 'GV', 'K', NULL, 2987, 17.5, 'KAB', 'WLNO', 06109, TRUE, FALSE, FALSE, NULL, NULL, NULL),
|
||||
(2020, 2, 2, 'GV', 'K', NULL, 1873, 17.7, 'KAB', 'WLNO', 06109, TRUE, FALSE, FALSE, NULL, NULL, NULL),
|
||||
|
||||
@@ -76,7 +76,7 @@ INSERT INTO delivery (mgnr, year, did, date, time, zwstid, lnr) VALUES
|
||||
(101, 2023, 18, '2023-10-10', NULL, 'X', 2),
|
||||
(101, 2023, 19, '2023-10-10', NULL, 'X', 3);
|
||||
|
||||
INSERT INTO delivery_part (year, did, dpnr, sortid, attrid, cultid, weight, kmw, qualid, hkid, kgnr, net_weight, manual_weighing, spl_check, scale_id, weighing_data, weighing_reason) VALUES
|
||||
INSERT INTO delivery_part (year, did, dpnr, sortid, attrid, cultid, weight, kmw, qualid, hkid, kgnr, net_weight, manual_weighing, spl_check, scale_terminal, weighing_data, weighing_reason) VALUES
|
||||
(2021, 1, 1, 'GV', NULL, NULL, 3500, 15.0, 'QUW', 'WLNO', 15224, TRUE, FALSE, FALSE, NULL, NULL, NULL),
|
||||
(2021, 2, 1, 'GV', NULL, NULL, 4000, 17.0, 'KAB', 'WLNO', 15224, TRUE, FALSE, FALSE, NULL, NULL, NULL),
|
||||
(2021, 2, 2, 'GV', NULL, NULL, 4000, 16.0, 'QUW', 'WLNO', 15224, TRUE, FALSE, FALSE, NULL, NULL, NULL),
|
||||
|
||||
@@ -1 +1 @@
|
||||
curl --fail -s -L "https://elwig.at/files/create.sql?v=40" -u "elwig:ganzGeheim123!" -o "Resources\Sql\Create.sql"
|
||||
curl --fail -s -L "https://elwig.at/files/create.sql?v=41" -u "elwig:ganzGeheim123!" -o "Resources\Sql\Create.sql"
|
||||
|
||||
Reference in New Issue
Block a user