From 41811925be879bc0f0e9196e497be23412a89563 Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Mon, 7 Jul 2025 21:46:19 +0200 Subject: [PATCH] PaymentVariantsWindow: Add ViewModel and Service --- Elwig/Services/PaymentVariantService.cs | 346 +++++++++++++ Elwig/ViewModels/AreaComAdminViewModel.cs | 2 +- Elwig/ViewModels/DeliveryAdminViewModel.cs | 2 +- .../ViewModels/DeliveryAncmtAdminViewModel.cs | 2 +- .../DeliveryScheduleAdminViewModel.cs | 2 +- Elwig/ViewModels/MemberAdminViewModel.cs | 2 +- Elwig/ViewModels/PaymentVariantsViewModel.cs | 95 ++++ Elwig/Windows/PaymentVariantsWindow.xaml | 94 ++-- Elwig/Windows/PaymentVariantsWindow.xaml.cs | 466 +++--------------- 9 files changed, 579 insertions(+), 432 deletions(-) create mode 100644 Elwig/Services/PaymentVariantService.cs create mode 100644 Elwig/ViewModels/PaymentVariantsViewModel.cs diff --git a/Elwig/Services/PaymentVariantService.cs b/Elwig/Services/PaymentVariantService.cs new file mode 100644 index 0000000..d57829d --- /dev/null +++ b/Elwig/Services/PaymentVariantService.cs @@ -0,0 +1,346 @@ +using Elwig.Documents; +using Elwig.Helpers; +using Elwig.Helpers.Billing; +using Elwig.Helpers.Export; +using Elwig.Models.Dtos; +using Elwig.Models.Entities; +using Elwig.ViewModels; +using Microsoft.EntityFrameworkCore; +using Microsoft.Win32; +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; + +namespace Elwig.Services { + public static class PaymentVariantService { + + private static readonly JsonSerializerOptions JsonOpt = new() { WriteIndented = true }; + + public static void ClearInputs(this PaymentVariantsViewModel vm) { + vm.IsPaymentVariantSelected = false; + vm.EditText = "Bearbeiten"; + vm.SaveIsEnabled = false; + vm.DeleteIsEnabled = false; + vm.CalculateIsEnabled = false; + vm.CommitIsEnabled = false; + vm.CommitVisibility = Visibility.Visible; + vm.RevertIsEnabled = false; + vm.RevertVisibility = Visibility.Hidden; + vm.Arrow = "\xF0AF"; + vm.ExportIsEnabled = false; + + vm.IsReadOnly = true; + vm.DataIsReadOnly = true; + vm.BillingData = null; + vm.Name = ""; + vm.Comment = ""; + vm.Date = null; + vm.TransferDate = null; + vm.WeightModifier = null; + vm.ConsiderModifiers = false; + vm.ConsiderPenalties = false; + vm.ConsiderPenalty = false; + vm.ConsiderAuto = false; + vm.ConsiderCustom = false; + vm.IsEnabled = false; + vm.Data = ""; + + vm.StatusModifierSum = "-"; + vm.StatusTotalSum = "-"; + vm.StatusVatSum = "-"; + vm.StatusDeductionSum = "-"; + vm.StatusPaymentSum = "-"; + } + + public static void FillInputs(this PaymentVariantsViewModel vm, PaymentVar v) { + var locked = !v.TestVariant; + vm.IsPaymentVariantSelected = true; + vm.SaveIsEnabled = !locked; + vm.DeleteIsEnabled = !locked; + vm.CalculateIsEnabled = !locked; + vm.CommitIsEnabled = !locked && !vm.SeasonLocked; + vm.CommitVisibility = !locked ? Visibility.Visible : Visibility.Hidden; + vm.RevertIsEnabled = locked && !vm.SeasonLocked; + vm.RevertVisibility = locked ? Visibility.Visible : Visibility.Hidden; + vm.Arrow = locked ? "\xF0B0" : "\xF0AF"; + vm.EditText = locked ? "Ansehen" : "Bearbeiten"; + vm.ExportIsEnabled = locked; + + vm.IsReadOnly = false; + vm.DataIsReadOnly = locked; + vm.Name = v.Name; + vm.Comment = v.Comment ?? ""; + vm.Date = v.Date; + vm.TransferDate = v.TransferDate; + try { + vm.BillingData = BillingData.FromJson(v.Data); + vm.ConsiderModifiers = vm.BillingData.ConsiderDelieryModifiers; + vm.ConsiderPenalties = vm.BillingData.ConsiderContractPenalties; + vm.ConsiderPenalty = vm.BillingData.ConsiderTotalPenalty; + vm.ConsiderAuto = vm.BillingData.ConsiderAutoBusinessShares; + vm.ConsiderCustom = vm.BillingData.ConsiderCustomModifiers; + if (vm.BillingData.NetWeightModifier != 0) { + vm.WeightModifier = Math.Round(vm.BillingData.NetWeightModifier * 100.0, 8); + } else if (vm.BillingData.GrossWeightModifier != 0) { + vm.WeightModifier = Math.Round(vm.BillingData.GrossWeightModifier * 100.0, 8); + } else { + vm.WeightModifier = null; + } + vm.Data = JsonSerializer.Serialize(vm.BillingData.Data, JsonOpt); + } catch { + vm.BillingData = null; + vm.ConsiderModifiers = false; + vm.ConsiderPenalties = false; + vm.ConsiderPenalty = false; + vm.ConsiderAuto = false; + vm.ConsiderCustom = false; + vm.WeightModifier = null; + vm.Data = v.Data; + } + vm.IsEnabled = !locked; + + vm.StatusModifierSum = "..."; + vm.StatusTotalSum = "..."; + vm.StatusVatSum = "..."; + vm.StatusDeductionSum = "..."; + vm.StatusPaymentSum = "..."; + Utils.RunBackground("Variantendaten laden", async () => { + await vm.UpdateSums(v); + }); + } + + private static async Task UpdateSums(this PaymentVariantsViewModel vm, PaymentVar v) { + if (App.MainDispatcher == null) + return; + var modText = "-"; + var totalText = "-"; + var vatText = "-"; + var deductionText = "-"; + var paymentText = "-"; + + using var ctx = new AppDbContext(); + var sym = v.Season.Currency.Symbol ?? v.Season.Currency.Code; + + var modSum = await ctx.PaymentDeliveryParts + .Where(p => p.Year == v.Year && p.AvNr == v.AvNr) + .SumAsync(p => p.AmountValue - p.NetAmountValue); + modText = $"{v.Season.DecFromDb(modSum):N2} {sym}"; + + var totalSum = await ctx.MemberPayments + .Where(p => p.Year == v.Year && p.AvNr == v.AvNr) + .SumAsync(p => p.AmountValue); + totalText = $"{v.Season.DecFromDb(totalSum):N2} {sym}"; + + await App.MainDispatcher.BeginInvoke(() => { + if (vm.SelectedPaymentVariant != v) + return; + vm.StatusModifierSum = modText; + vm.StatusTotalSum = totalText; + }); + + var credits = ctx.Credits.Where(c => c.Year == v.Year && c.AvNr == v.AvNr); + if (!credits.Any()) { + long lastTotalSum = 0; + decimal vatSum = 0; + var currentPayments = await ctx.MemberPayments + .Where(p => p.Year == v.Year && p.AvNr == v.AvNr) + .ToDictionaryAsync(p => p.MgNr); + var lastV = await ctx.PaymentVariants + .Where(l => l.Year == v.Year && !l.TestVariant) + .OrderByDescending(l => l.TransferDateString ?? l.DateString) + .ThenByDescending(l => l.AvNr) + .FirstOrDefaultAsync(); + if (lastV != null) { + var lastPayments = await ctx.MemberPayments + .Where(p => p.Year == v.Year && p.AvNr == lastV.AvNr) + .ToDictionaryAsync(p => p.MgNr); + lastTotalSum = lastPayments.Sum(e => e.Value.AmountValue); + foreach (int mgnr in currentPayments.Keys) { + var c = currentPayments[mgnr]; + var l = lastPayments[mgnr]; + vatSum += (c.Amount - l.Amount) * (decimal)(c.Member.IsBuchführend ? v.Season.VatNormal : v.Season.VatFlatrate); + } + } else { + vatSum = currentPayments.Sum(e => e.Value.Amount * (decimal)(e.Value.Member.IsBuchführend ? v.Season.VatNormal : v.Season.VatFlatrate)); + } + vatText = $"~{vatSum:N2} {sym}"; + deductionText = $"- {sym}"; + paymentText = $"~{v.Season.DecFromDb(totalSum - lastTotalSum) + vatSum:N2} {sym}"; + } else { + // all values in the credit table are stored with precision 2! + totalText = $"{Utils.DecFromDb(await credits.SumAsync(c => c.NetAmountValue), 2):N2} {sym}"; + vatText = $"{Utils.DecFromDb(await credits.SumAsync(c => c.VatAmountValue), 2):N2} {sym}"; + deductionText = $"{-Utils.DecFromDb(await credits.SumAsync(c => c.ModifiersValue ?? 0), 2):N2} {sym}"; + paymentText = $"{Utils.DecFromDb(await credits.SumAsync(c => c.AmountValue), 2):N2} {sym}"; + } + + await App.MainDispatcher.BeginInvoke(() => { + if (vm.SelectedPaymentVariant != v) + return; + vm.StatusModifierSum = modText; + vm.StatusTotalSum = totalText; + vm.StatusVatSum = vatText; + vm.StatusDeductionSum = deductionText; + vm.StatusPaymentSum = paymentText; + }); + } + + public static async Task GenerateSummary(PaymentVar v, ExportMode mode) { + Mouse.OverrideCursor = Cursors.AppStarting; + try { + using var ctx = new AppDbContext(); + var data = await PaymentVariantSummaryData.ForPaymentVariant(v, ctx.PaymentVariantSummaryRows); + using var doc = new PaymentVariantSummary((await ctx.PaymentVariants.FindAsync(v.Year, v.AvNr))!, data); + await Utils.ExportDocument(doc, mode); + } catch (Exception exc) { + MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error); + } + Mouse.OverrideCursor = null; + } + + public static async Task GenerateEbics(int year, int avnr) { + using var ctx = new AppDbContext(); + var v = (await ctx.PaymentVariants.FindAsync(year, avnr))!; + + var withoutIban = v.Credits.Count(c => c.Member.Iban == null); + if (withoutIban > 0) { + var r = MessageBox.Show($"Achtung: Für {withoutIban:N0} Mitglieder ist kein IBAN hinterlegt.\n\nDiese werden NICHT exportiert.", + "Mitglieder ohne IBAN", MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel); + if (r != MessageBoxResult.OK) return; + } + var withNegAmount = v.Credits.Count(c => c.Amount <= 0); + if (withNegAmount > 0) { + var r = MessageBox.Show($"Achtung: Es gibt {withNegAmount:N0} Traubengutschriften mit negativem Betrag.\n\nDiese werden NICHT exportiert.", + "Traubengutschriften mit negativem Betrag", MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.OK); + if (r != MessageBoxResult.OK) return; + } + + var d = new SaveFileDialog() { + FileName = $"{App.Client.NameToken}-Überweisungsdaten-{v.Year}-{v.Name.Trim().Replace(' ', '-')}.{Ebics.FileExtension}", + DefaultExt = Ebics.FileExtension, + Filter = "EBICS-Datei (*.xml)|*.xml", + Title = $"Überweisungsdaten speichern unter - Elwig", + }; + if (d.ShowDialog() == true) { + Mouse.OverrideCursor = Cursors.AppStarting; + try { + using var e = new Ebics(v, d.FileName, App.Client.ExportEbicsVersion, (Ebics.AddressMode)App.Client.ExportEbicsAddress); + await e.ExportAsync(Transaction.FromPaymentVariant(v)); + } catch (Exception exc) { + MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error); + } + Mouse.OverrideCursor = null; + } + } + + public static async Task GenerateAccountingList(int year, int avnr, string name) { + var d = new SaveFileDialog() { + FileName = $"{App.Client.NameToken}-Buchungsliste-{year}-{name.Trim().Replace(' ', '-')}.ods", + DefaultExt = "ods", + Filter = "OpenDocument Format Spreadsheet (*.ods)|*.ods", + Title = $"Buchungsliste speichern unter - Elwig" + }; + if (d.ShowDialog() == true) { + Mouse.OverrideCursor = Cursors.AppStarting; + try { + using var ctx = new AppDbContext(); + var tbl = await CreditNoteData.ForPaymentVariant(ctx, year, avnr); + using var ods = new OdsFile(d.FileName); + await ods.AddTable(tbl); + } catch (Exception exc) { + MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error); + } + Mouse.OverrideCursor = null; + } + } + + public static async Task CreatePaymentVariant(int year) { + PaymentVar? v; + using (var ctx = new AppDbContext()) { + v = new PaymentVar { + Year = year, + AvNr = await ctx.NextAvNr(year), + Name = "Neue Auszahlungsvariante", + TestVariant = true, + DateString = $"{DateTime.Today:yyyy-MM-dd}", + Data = "{\"mode\": \"elwig\", \"version\": 1, \"payment\": {}, \"curves\": []}", + }; + ctx.Add(v); + await ctx.SaveChangesAsync(); + } + App.HintContextChange(); + return v; + } + + public static async Task Duplicate(this PaymentVar orig) { + PaymentVar? n; + using (var ctx = new AppDbContext()) { + n = new PaymentVar { + Year = orig.Year, + AvNr = await ctx.NextAvNr(orig.Year), + Name = $"{orig.Name} (Kopie)", + TestVariant = true, + DateString = $"{DateTime.Today:yyyy-MM-dd}", + Data = orig.Data, + }; + + ctx.Add(n); + await ctx.SaveChangesAsync(); + } + App.HintContextChange(); + return n; + } + + public static async Task<(int, int)> UpdatePaymentVariant(this PaymentVariantsViewModel vm, int? oldYear, int? oldAvNr) { + var year = oldYear ?? Utils.CurrentYear; + int avnr = 0; + + var d = App.Config.Debug ? BillingData.FromJson(vm.Data ?? "{}") : vm.BillingData!; + d.ConsiderDelieryModifiers = vm.ConsiderModifiers; + d.ConsiderContractPenalties = vm.ConsiderPenalties; + d.ConsiderTotalPenalty = vm.ConsiderPenalty; + d.ConsiderAutoBusinessShares = vm.ConsiderAuto; + d.ConsiderCustomModifiers = vm.ConsiderCustom; + var modVal = vm.WeightModifier ?? 0; + d.NetWeightModifier = modVal > 0 ? modVal / 100.0 : 0; + d.GrossWeightModifier = modVal < 0 ? modVal / 100.0 : 0; + + using (var ctx = new AppDbContext()) { + var v = new PaymentVar { + Year = year, + AvNr = oldAvNr ?? await ctx.NextAvNr(year), + Name = vm.Name!, + DateString = $"{vm.Date!.Value:yyyy-MM-dd}", + TransferDate = vm.TransferDate, + TestVariant = vm.SelectedPaymentVariant?.TestVariant ?? true, + CalcTimeUnix = vm.SelectedPaymentVariant?.CalcTimeUnix, + Comment = string.IsNullOrEmpty(vm.Comment) ? null : vm.Comment, + Data = JsonSerializer.Serialize(d.Data), + }; + avnr = v.AvNr; + ctx.Update(v); + await ctx.SaveChangesAsync(); + } + vm.WeightModifierChanged = false; + App.HintContextChange(); + return (year, avnr); + } + + public static async Task DeletePaymentVariant(int year, int avnr) { + using (var ctx = new AppDbContext()) { + var v = (await ctx.PaymentVariants.FindAsync(year, avnr))!; + ctx.Remove(v); + await ctx.SaveChangesAsync(); + } + App.HintContextChange(); + } + + public static async Task Calculate(int year, int avnr) { + var b = new BillingVariant(year, avnr); + await b.Calculate(); + } + } +} diff --git a/Elwig/ViewModels/AreaComAdminViewModel.cs b/Elwig/ViewModels/AreaComAdminViewModel.cs index cfc2076..c1fec97 100644 --- a/Elwig/ViewModels/AreaComAdminViewModel.cs +++ b/Elwig/ViewModels/AreaComAdminViewModel.cs @@ -15,7 +15,7 @@ namespace Elwig.ViewModels { [ObservableProperty] private string? _searchQuery = ""; public List TextFilter { - get => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0)]; + get => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0) ?? []]; set => SearchQuery = string.Join(' ', value.Where(e => e.Length > 0)); } diff --git a/Elwig/ViewModels/DeliveryAdminViewModel.cs b/Elwig/ViewModels/DeliveryAdminViewModel.cs index 6e9ed35..c2e3ac7 100644 --- a/Elwig/ViewModels/DeliveryAdminViewModel.cs +++ b/Elwig/ViewModels/DeliveryAdminViewModel.cs @@ -18,7 +18,7 @@ namespace Elwig.ViewModels { [ObservableProperty] private string? _searchQuery = ""; - public List TextFilter => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0)]; + public List TextFilter => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0) ?? []]; [ObservableProperty] private string? _lastScaleError; diff --git a/Elwig/ViewModels/DeliveryAncmtAdminViewModel.cs b/Elwig/ViewModels/DeliveryAncmtAdminViewModel.cs index 24beed6..2888b9d 100644 --- a/Elwig/ViewModels/DeliveryAncmtAdminViewModel.cs +++ b/Elwig/ViewModels/DeliveryAncmtAdminViewModel.cs @@ -10,7 +10,7 @@ namespace Elwig.ViewModels { [ObservableProperty] private string? _searchQuery = ""; - public List TextFilter => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0)]; + public List TextFilter => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0) ?? []]; [ObservableProperty] private bool _filterOnlyUpcoming; diff --git a/Elwig/ViewModels/DeliveryScheduleAdminViewModel.cs b/Elwig/ViewModels/DeliveryScheduleAdminViewModel.cs index 25f365c..6b2f006 100644 --- a/Elwig/ViewModels/DeliveryScheduleAdminViewModel.cs +++ b/Elwig/ViewModels/DeliveryScheduleAdminViewModel.cs @@ -11,7 +11,7 @@ namespace Elwig.ViewModels { [ObservableProperty] private string? _searchQuery = ""; - public List TextFilter => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0)]; + public List TextFilter => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0) ?? []]; [ObservableProperty] private bool _filterOnlyUpcoming; diff --git a/Elwig/ViewModels/MemberAdminViewModel.cs b/Elwig/ViewModels/MemberAdminViewModel.cs index ca66959..d10871c 100644 --- a/Elwig/ViewModels/MemberAdminViewModel.cs +++ b/Elwig/ViewModels/MemberAdminViewModel.cs @@ -18,7 +18,7 @@ namespace Elwig.ViewModels { [ObservableProperty] private string? _searchQuery = ""; - public List TextFilter => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0)]; + public List TextFilter => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0) ?? []]; [ObservableProperty] private bool _showOnlyActiveMembers; diff --git a/Elwig/ViewModels/PaymentVariantsViewModel.cs b/Elwig/ViewModels/PaymentVariantsViewModel.cs new file mode 100644 index 0000000..3bd97de --- /dev/null +++ b/Elwig/ViewModels/PaymentVariantsViewModel.cs @@ -0,0 +1,95 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Elwig.Helpers.Billing; +using Elwig.Models.Entities; +using System; +using System.Collections.Generic; +using System.Windows; + +namespace Elwig.ViewModels { + public partial class PaymentVariantsViewModel : ObservableObject { + + [ObservableProperty] + private PaymentVar? _selectedPaymentVariant; + [ObservableProperty] + private IEnumerable _paymentVariants = []; + + public BillingData? BillingData; + public bool SeasonLocked; + public bool WeightModifierChanged; + + [ObservableProperty] + private string _name = ""; + [ObservableProperty] + private string _comment = ""; + [ObservableProperty] + private string _dateString = ""; + public DateOnly? Date { + get => DateOnly.TryParseExact(DateString, "dd.MM.yyyy", out var d) ? d : null; + set => DateString = $"{value:dd.MM.yyyy}"; + } + [ObservableProperty] + private string _transferDateString = ""; + public DateOnly? TransferDate { + get => DateOnly.TryParseExact(TransferDateString, "dd.MM.yyyy", out var d) ? d : null; + set => TransferDateString = $"{value:dd.MM.yyyy}"; + } + [ObservableProperty] + private string _weightModifierString = ""; + public double? WeightModifier { + get => double.TryParse(WeightModifierString, out var d) ? d : null; + set => WeightModifierString = $"{value}"; + } + [ObservableProperty] + private string _data = ""; + [ObservableProperty] + private bool _considerModifiers; + [ObservableProperty] + private bool _considerPenalties; + [ObservableProperty] + private bool _considerPenalty; + [ObservableProperty] + private bool _considerAuto; + [ObservableProperty] + private bool _considerCustom; + + [ObservableProperty] + private bool _isPaymentVariantSelected; + [ObservableProperty] + private bool _isReadOnly = true; + [ObservableProperty] + private bool _dataIsReadOnly = true; + [ObservableProperty] + private bool _isEnabled = false; + [ObservableProperty] + private bool _saveIsEnabled = false; + [ObservableProperty] + private bool _deleteIsEnabled = false; + [ObservableProperty] + private bool _calculateIsEnabled = false; + [ObservableProperty] + private bool _exportIsEnabled = false; + [ObservableProperty] + private bool _commitIsEnabled = false; + [ObservableProperty] + private Visibility _commitVisibility = Visibility.Visible; + [ObservableProperty] + private bool _revertIsEnabled = false; + [ObservableProperty] + private Visibility _revertVisibility = Visibility.Hidden; + [ObservableProperty] + private string _arrow = "\xF0AF"; + [ObservableProperty] + private string _editText = "Bearbeiten"; + + [ObservableProperty] + private string _statusModifierSum = "-"; + [ObservableProperty] + private string _statusTotalSum = "-"; + [ObservableProperty] + private string _statusVatSum = "-"; + [ObservableProperty] + private string _statusDeductionSum = "-"; + [ObservableProperty] + private string _statusPaymentSum = "-"; + } +} diff --git a/Elwig/Windows/PaymentVariantsWindow.xaml b/Elwig/Windows/PaymentVariantsWindow.xaml index 30a199b..f6c8a13 100644 --- a/Elwig/Windows/PaymentVariantsWindow.xaml +++ b/Elwig/Windows/PaymentVariantsWindow.xaml @@ -5,8 +5,12 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Elwig.Windows" + xmlns:vm="clr-namespace:Elwig.ViewModels" xmlns:ctrl="clr-namespace:Elwig.Controls" Title="Auszahlungsvarianten - Elwig" Height="480" Width="850" MinHeight="400" MinWidth="850"> + + + -