From 7a20c1d222914b73973bb4f8bf58dfe33f74c12f Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Wed, 1 Jul 2026 11:01:15 +0200 Subject: [PATCH] [#20][#80] Elwig: Update member_history and add different types of shares --- Elwig/Documents/BusinessDocument.cs | 20 +- Elwig/Documents/CreditNote.cs | 20 +- Elwig/Documents/DeliveryConfirmation.cs | 12 +- Elwig/Documents/DeliveryNote.cs | 12 +- Elwig/Documents/MemberDataSheet.cs | 16 +- Elwig/Documents/MemberList.cs | 2 +- Elwig/Helpers/AppDbContext.cs | 3 + Elwig/Helpers/AppDbUpdater.cs | 2 +- Elwig/Helpers/Billing/Billing.cs | 40 +-- Elwig/Helpers/Billing/BillingVariant.cs | 10 +- Elwig/Helpers/ClientParameters.cs | 10 +- Elwig/Helpers/Export/ElwigData.cs | 95 +++++- Elwig/Models/Dtos/CreditNoteData.cs | 30 +- Elwig/Models/Dtos/CreditNoteDeliveryData.cs | 22 +- Elwig/Models/Dtos/MemberDeliveryData.cs | 2 +- Elwig/Models/Dtos/MemberListData.cs | 14 +- Elwig/Models/Dtos/OverUnderDeliveryData.cs | 94 +++++- Elwig/Models/Entities/Member.cs | 20 +- Elwig/Models/Entities/MemberHistory.cs | 52 ++- Elwig/Models/Entities/Season.cs | 39 ++- Elwig/Resources/Sql/39-40.sql | 298 ++++++++++++++++++ Elwig/Services/MemberService.cs | 13 +- Elwig/Services/SyncService.cs | 9 +- Elwig/ViewModels/MemberAdminViewModel.cs | 2 +- Elwig/Windows/BaseDataWindow.xaml | 10 +- Elwig/Windows/BaseDataWindow.xaml.Season.cs | 50 +-- Elwig/Windows/BaseDataWindow.xaml.cs | 24 +- Elwig/Windows/ChartWindow.xaml.cs | 4 +- Elwig/Windows/MemberAdminWindow.xaml.cs | 10 +- Elwig/Windows/PaymentAdjustmentWindow.xaml | 18 +- Elwig/Windows/PaymentAdjustmentWindow.xaml.cs | 86 ++--- Tests/Resources/Sql/BillingInsert.sql | 6 +- Tests/Resources/Sql/DocumentInsert.sql | 4 +- Tests/Resources/Sql/ServiceInsert.sql | 8 +- .../ServiceTests/MemberServiceTest.cs | 4 +- Tests/fetch-resources.bat | 2 +- 36 files changed, 801 insertions(+), 262 deletions(-) create mode 100644 Elwig/Resources/Sql/39-40.sql diff --git a/Elwig/Documents/BusinessDocument.cs b/Elwig/Documents/BusinessDocument.cs index 402784c..7dd498b 100644 --- a/Elwig/Documents/BusinessDocument.cs +++ b/Elwig/Documents/BusinessDocument.cs @@ -263,7 +263,7 @@ namespace Elwig.Documents { } protected Table NewBucketTable( - Season season, Dictionary buckets, int deliveredWeight, + Season season, Dictionary buckets, int deliveredWeightRed, int deliveredWeightWhite, bool includeDelivery = true, bool includePayment = false, bool isTiny = false, IEnumerable? filter = null ) { @@ -316,9 +316,21 @@ namespace Elwig.Documents { .Where(b => !fbVars.Contains(b.Key)) .OrderBy(b => b.Value.Name); - tbl.AddCell(NewBucketTh("Gesamtlieferung lt. gez. GA", isTiny: isTiny)); - tbl.AddCells(FormatRow(Member.BusinessShares * season.MinKgPerBusinessShare, Member.BusinessShares * season.MaxKgPerBusinessShare, - deliveredWeight, isGa: true, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny)); + if (Member.Shares != 0 || (Member.SharesRed == 0 && Member.SharesWhite == 0)) { + tbl.AddCell(NewBucketTh("Gesamtlieferung lt. gez. GA", isTiny: isTiny)); + tbl.AddCells(FormatRow(Member.Shares * (season.MinKgPerShare ?? 0), Member.Shares * (season.MaxKgPerShare ?? 0), + deliveredWeightRed + deliveredWeightWhite, isGa: true, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny)); + } + + if (Member.SharesRed != 0 || Member.SharesWhite != 0) { + tbl.AddCell(NewBucketTh("Gesamtlieferung lt. gez. GA (rot)", isTiny: isTiny)); + tbl.AddCells(FormatRow(Member.SharesRed * (season.MinKgPerShareRed ?? season.MinKgPerShare ?? 0), Member.SharesRed * (season.MaxKgPerShareRed ?? season.MaxKgPerShare ?? 0), + deliveredWeightRed, isGa: true, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny)); + + tbl.AddCell(NewBucketTh("Gesamtlieferung lt. gez. GA (weiß)", isTiny: isTiny)); + tbl.AddCells(FormatRow(Member.SharesWhite * (season.MinKgPerShareWhite ?? season.MinKgPerShare ?? 0), Member.SharesWhite * (season.MaxKgPerShareWhite ?? season.MaxKgPerShare ?? 0), + deliveredWeightWhite, isGa: true, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny)); + } if (fbs.Any()) { tbl.AddCell(NewBucketSubHdr("Flächenbindungen" + (vtr.Any() ? " (inkl. Verträge)" : "") + ":", includePayment ? 8 : 7, isTiny: isTiny)); diff --git a/Elwig/Documents/CreditNote.cs b/Elwig/Documents/CreditNote.cs index cac5909..2ffa8e3 100644 --- a/Elwig/Documents/CreditNote.cs +++ b/Elwig/Documents/CreditNote.cs @@ -86,19 +86,27 @@ namespace Elwig.Documents { } if (ConsiderTotalPenalty) { - var total = _data.Rows.SelectMany(r => r.Buckets).Sum(b => b.Value); - var totalUnderDelivery = total - Member.BusinessShares * season.MinKgPerBusinessShare; - MemberTotalUnderDelivery = totalUnderDelivery < 0 ? totalUnderDelivery * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) - (season.PenaltyPerBsAmount * Math.Floor(-(decimal)totalUnderDelivery / season.MinKgPerBusinessShare) ?? 0) : 0; + var weights = _data.Rows.Select(r => new {r.Type, Weight = r.Buckets.Sum(b => b.Value)}).GroupBy(r => r.Type).ToDictionary(r => r.Key, g => g.Sum(r => r.Weight)); + var red = weights.GetValueOrDefault("R", 0); + var white = weights.GetValueOrDefault("W", 0); + var total = red + white; + var underDeliveryTotal = total - Member.Shares * (season.MinKgPerShare ?? 0); + var underDeliveryRed = red - Member.SharesRed * (season.MinKgPerShareRed ?? season.MinKgPerShare ?? 0); + var underDeliveryWhite = white - Member.SharesWhite * (season.MinKgPerShareWhite ?? season.MinKgPerShare ?? 0); + MemberTotalUnderDelivery = + (underDeliveryTotal < 0 ? underDeliveryTotal * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) - (season.PenaltyPerShareAmount * Math.Floor(-(decimal)underDeliveryTotal / (season.MinKgPerShare ?? 0)) ?? 0) : 0) + + (underDeliveryRed < 0 ? underDeliveryRed * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) - (season.PenaltyPerShareAmount * Math.Floor(-(decimal)underDeliveryRed / (season.MinKgPerShareRed ?? season.MinKgPerShare ?? 0)) ?? 0) : 0) + + (underDeliveryWhite < 0 ? underDeliveryWhite * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) - (season.PenaltyPerShareAmount * Math.Floor(-(decimal)underDeliveryWhite / (season.MinKgPerShareWhite ?? season.MinKgPerShare ?? 0)) ?? 0) : 0); if (total == 0) - MemberTotalUnderDelivery -= (season.PenaltyNone ?? 0) + (season.PenaltyPerBsNone * Member.BusinessShares ?? 0); + MemberTotalUnderDelivery -= (season.PenaltyNone ?? 0) + (season.PenaltyPerShareNone * (Member.Shares + Member.SharesRed + Member.SharesWhite) ?? 0); } if (ConsiderAutoBusinessShares) { var fromDate = $"{season.Year}-01-01"; var toDate = $"{season.Year}-12-31"; MemberAutoBusinessShares = await ctx.MemberHistory - .Where(h => h.MgNr == Member.MgNr && h.Type == "auto") + .Where(h => h.ToMgNr == Member.MgNr && h.Reason == "auto") .Where(h => h.DateString.CompareTo(fromDate) >= 0 && h.DateString.CompareTo(toDate) <= 0) - .SumAsync(h => h.BusinessShares); + .SumAsync(h => h.Shares); MemberAutoBusinessSharesAmount = MemberAutoBusinessShares * (-season.BusinessShareValue ?? 0); } if (ConsiderContractPenalties) { diff --git a/Elwig/Documents/DeliveryConfirmation.cs b/Elwig/Documents/DeliveryConfirmation.cs index 499fa08..29fc7a0 100644 --- a/Elwig/Documents/DeliveryConfirmation.cs +++ b/Elwig/Documents/DeliveryConfirmation.cs @@ -18,7 +18,8 @@ namespace Elwig.Documents { private readonly int _year; public Season? Season; - public int MemberDeliveredWeight; + public int MemberDeliveredWeightRed; + public int MemberDeliveredWeightWhite; public DeliveryConfirmationDeliveryData? Data; public string? Text = App.Client.TextDeliveryConfirmation; public Dictionary MemberBuckets = []; @@ -36,10 +37,13 @@ namespace Elwig.Documents { protected override async Task LoadData(AppDbContext ctx) { await base.LoadData(ctx); Season = await ctx.FetchSeasons(_year).SingleOrDefaultAsync() ?? throw new ArgumentException("Invalid season"); - MemberDeliveredWeight = await ctx.Deliveries + var weights = await ctx.Deliveries .Where(d => d.Year == Season.Year && d.MgNr == Member.MgNr) .SelectMany(d => d.Parts) - .SumAsync(p => p.Weight); + .GroupBy(p => p.Variety.Type) + .ToDictionaryAsync(g => g.Key, g => g.Sum(p => p.Weight)); + MemberDeliveredWeightRed = weights.GetValueOrDefault("R", 0); + MemberDeliveredWeightWhite = weights.GetValueOrDefault("W", 0); MemberBuckets = await ctx.GetMemberBuckets(Season.Year, Member.MgNr); MemberStats = await AppDbContext.GetMemberStats(Season.Year, Member.MgNr); Data ??= await DeliveryConfirmationDeliveryData.ForMember(ctx.DeliveryParts, Season.Year, Member); @@ -63,7 +67,7 @@ namespace Elwig.Documents { doc.Add(NewDeliveryListTable(Data)); doc.Add(NewWeightsTable(MemberStats) .SetMarginTopMM(10).SetKeepTogether(true)); - doc.Add(NewBucketTable(Season, MemberBuckets, MemberDeliveredWeight, includePayment: true) + doc.Add(NewBucketTable(Season, MemberBuckets, MemberDeliveredWeightRed, MemberDeliveredWeightWhite, includePayment: true) .SetMarginTopMM(10).SetKeepTogether(true)); if (Text != null) { diff --git a/Elwig/Documents/DeliveryNote.cs b/Elwig/Documents/DeliveryNote.cs index 8e0da19..f275f04 100644 --- a/Elwig/Documents/DeliveryNote.cs +++ b/Elwig/Documents/DeliveryNote.cs @@ -19,7 +19,8 @@ namespace Elwig.Documents { public Delivery Delivery; public string? Text; - public int MemberDeliveredWeight; + public int MemberDeliveredWeightRed; + public int MemberDeliveredWeightWhite; public Dictionary MemberBuckets = []; // 0 - none @@ -59,9 +60,12 @@ namespace Elwig.Documents { protected override async Task LoadData(AppDbContext ctx) { await base.LoadData(ctx); - MemberDeliveredWeight = await ctx.DeliveryParts + var weights = await ctx.DeliveryParts .Where(d => d.Year == Delivery.Year && d.Delivery.MgNr == Member.MgNr) - .SumAsync(p => p.Weight); + .GroupBy(p => p.Variety.Type) + .ToDictionaryAsync(g => g.Key, g => g.Sum(p => p.Weight)); + MemberDeliveredWeightRed = weights.GetValueOrDefault("R", 0); + MemberDeliveredWeightWhite = weights.GetValueOrDefault("W", 0); MemberBuckets = await ctx.GetMemberBuckets(Delivery.Year, Member.MgNr) ?? []; } @@ -81,7 +85,7 @@ namespace Elwig.Documents { doc.Add(new KernedParagraph($"Anmerkung zur Lieferung: {Delivery.Comment}", 10).SetMarginsMM(5, 0, 0, 0)); } if (DisplayStats > 0) { - doc.Add(NewBucketTable(Delivery.Season, MemberBuckets, MemberDeliveredWeight, isTiny: true, + doc.Add(NewBucketTable(Delivery.Season, MemberBuckets, MemberDeliveredWeightRed, MemberDeliveredWeightWhite, isTiny: true, filter: DisplayStats > 2 ? null : DisplayStats == 1 ? [] : Delivery.Parts.Select(p => p.SortId).Distinct().ToList()) .SetKeepTogether(true) .SetMarginsMM(5, 0, 0, 0)); diff --git a/Elwig/Documents/MemberDataSheet.cs b/Elwig/Documents/MemberDataSheet.cs index 3fb64c9..13528f8 100644 --- a/Elwig/Documents/MemberDataSheet.cs +++ b/Elwig/Documents/MemberDataSheet.cs @@ -18,7 +18,8 @@ namespace Elwig.Documents { public new static string Name => "Stammdatenblatt"; public Season? Season; - public int MemberDeliveredWeight; + public int MemberDeliveredWeightRed; + public int MemberDeliveredWeightWhite; public Dictionary MemberBuckets = []; public List ActiveAreaCommitments = []; @@ -40,17 +41,20 @@ namespace Elwig.Documents { ActiveAreaCommitments = await Member.ActiveAreaCommitments(ctx) .Include(c => c.Contract).ThenInclude(c => c.Revisions) .ToListAsync(); - MemberDeliveredWeight = await ctx.Deliveries + var weights = await ctx.Deliveries .Where(d => d.Year == Season.Year && d.MgNr == Member.MgNr) .SelectMany(d => d.Parts) - .SumAsync(p => p.Weight); + .GroupBy(p => p.Variety.Type) + .ToDictionaryAsync(g => g.Key, g => g.Sum(p => p.Weight)); + MemberDeliveredWeightRed = weights.GetValueOrDefault("R", 0); + MemberDeliveredWeightWhite = weights.GetValueOrDefault("W", 0); } protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) { if (Season == null) throw new Exception("Call LoadData before RenderBody"); base.RenderBody(doc, pdf); doc.Add(NewMemberData(Season).SetMarginBottomMM(5)); - doc.Add(NewBucketTable(Season, MemberBuckets, MemberDeliveredWeight, includeDelivery: false)); + doc.Add(NewBucketTable(Season, MemberBuckets, MemberDeliveredWeightRed, MemberDeliveredWeightWhite, includeDelivery: false)); if (ActiveAreaCommitments.Count != 0) { bool firstOnPage = false; if (pdf.GetNumberOfPages() == 1) { @@ -132,6 +136,8 @@ namespace Elwig.Documents { .AddCell(NewTd(i < subTbl2.Count ? subTbl2[i][1] : "", colspan: 2)); } + var shares = (Member.Shares != 0 || (Member.SharesRed == 0 && Member.SharesWhite == 0) ? $"{Member.Shares:N0}" : "") + + (Member.SharesRed != 0 || Member.SharesWhite != 0 ? (Member.Shares != 0 ? " / " : "") + $"{Member.SharesRed:N0} (rot) / {Member.SharesWhite:N0} (weiß)" : ""); tbl.AddCell(NewDataHdr("Betrieb", colspan: 6)) .AddCell(NewDataTh("Betriebs-Nr.:")).AddCell(NewTd(Member.LfbisNr)) .AddCell(NewDataTh("UID:", colspan: 2)).AddCell(NewTd(Member.UstIdNr, colspan: 2)) @@ -143,7 +149,7 @@ namespace Elwig.Documents { .AddCell(NewDataHdr("Genossenschaft", colspan: 6)) .AddCell(NewDataTh("Status:")).AddCell(NewTd(new KernedParagraph(Member.IsActive ? "Aktiv " : "Nicht aktiv ", 10) .Add(Normal("(" + (Member.ExitDate != null ? $"{Member.EntryDate:dd.MM.yyyy}\u2013{Member.ExitDate:dd.MM.yyyy}" : $"seit {Member.EntryDate:dd.MM.yyyy}") + ")", 8)))) - .AddCell(NewDataTh("Geschäftsanteile:", colspan: 2)).AddCell(NewTd($"{Member.BusinessShares:N0}", colspan: 2)) + .AddCell(NewDataTh("Geschäftsanteile:", colspan: 2)).AddCell(NewTd(shares, colspan: 2)) .AddCell(NewDataTh("Stamm-Zweigstelle:")).AddCell(NewTd(Member.Branch?.Name)) .AddCell(NewDataTh("Volllieferant:", colspan: 2)).AddCell(NewTd(Member.IsVollLieferant ? "Ja" : "Nein", colspan: 2)) .AddCell(NewDataTh("Zusendungen per\u2026")).AddCell(NewTd(new KernedParagraph(10) diff --git a/Elwig/Documents/MemberList.cs b/Elwig/Documents/MemberList.cs index b490364..ce92382 100644 --- a/Elwig/Documents/MemberList.cs +++ b/Elwig/Documents/MemberList.cs @@ -86,7 +86,7 @@ namespace Elwig.Documents { .AddCell(NewTd($"{m.Plz}", 8)) .AddCell(NewTd(m.Locality, 6)) .AddCell(NewTd(m.LfbisNr ?? "", 8)) - .AddCell(NewTd($"{m.BusinessShares:N0}", 8, right: true).SetPaddingLeft(0).SetPaddingRight(0)) + .AddCell(NewTd($"{m.SharesTotal:N0}", 8, right: true).SetPaddingLeft(0).SetPaddingRight(0)) .AddCell(NewTd(m.DefaultKg ?? "", 6)); if (AreaComFilters.Length > 0) { foreach (var v in AreaComFilters) { diff --git a/Elwig/Helpers/AppDbContext.cs b/Elwig/Helpers/AppDbContext.cs index 37ecca5..c86046a 100644 --- a/Elwig/Helpers/AppDbContext.cs +++ b/Elwig/Helpers/AppDbContext.cs @@ -230,6 +230,9 @@ namespace Elwig.Helpers { modelBuilder.Entity().Navigation(a => a.Schedule).AutoInclude(); modelBuilder.Entity().Navigation(a => a.Variety).AutoInclude(); modelBuilder.Entity().Navigation(s => s.Branch).AutoInclude(); + modelBuilder.Entity().Navigation(s => s.FromMember).AutoInclude(); + modelBuilder.Entity().Navigation(s => s.ToMember).AutoInclude(); + modelBuilder.Entity().Navigation(s => s.Currency).AutoInclude(); } public override void Dispose() { diff --git a/Elwig/Helpers/AppDbUpdater.cs b/Elwig/Helpers/AppDbUpdater.cs index 6251407..d36988d 100644 --- a/Elwig/Helpers/AppDbUpdater.cs +++ b/Elwig/Helpers/AppDbUpdater.cs @@ -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 = 39; + public static readonly int RequiredSchemaVersion = 40; private static int VersionOffset = 0; diff --git a/Elwig/Helpers/Billing/Billing.cs b/Elwig/Helpers/Billing/Billing.cs index 6f8eaed..5fd6ba0 100644 --- a/Elwig/Helpers/Billing/Billing.cs +++ b/Elwig/Helpers/Billing/Billing.cs @@ -50,45 +50,29 @@ namespace Elwig.Helpers.Billing { """); } - public async Task AutoAdjustBusinessShares(DateOnly date, int allowanceKg = 0, double allowanceBs = 0, int allowanceKgPerBs = 0, double allowanceRel = 0, int addMinBs = 1) { - if (addMinBs < 1) addMinBs = 1; + public async Task AutoAdjustBusinessShares(DateOnly date, int allowanceKg = 0, double allowanceShares = 0, int allowanceKgPerShare = 0, double allowanceRel = 0, int addMinShares = 1) { + if (addMinShares < 1) addMinShares = 1; using var cnx = await AppDbContext.ConnectAsync(); await cnx.ExecuteBatch($""" - UPDATE member - SET business_shares = member.business_shares - h.business_shares - FROM member_history h - WHERE h.date = '{Year}-11-30' AND h.type = 'auto' AND h.mgnr = member.mgnr AND member.active; - - INSERT INTO member_history (mgnr, date, type, business_shares) - SELECT u.mgnr, - '{date:yyyy-MM-dd}', - 'auto', - CEIL((u.diff - {allowanceKg}.0 - {allowanceKgPerBs}.0 * u.business_shares) / s.max_kg_per_bs - - {allowanceBs.ToString(CultureInfo.InvariantCulture)} - - {allowanceRel.ToString(CultureInfo.InvariantCulture)} * u.business_shares) AS bs + DELETE FROM member_history WHERE source = 'elwig' AND reason = 'auto' AND SUBSTR(date, 1, 4) = '{Year}'; + INSERT INTO member_history (histnr, from_mgnr, from_type, to_mgnr, to_type, date, reason, source, shares, value_per_share, currency) + SELECT COALESCE((SELECT MAX(histnr) AS histnr FROM member_history), 0) + ROW_NUMBER() OVER(ORDER BY m.mgnr), + NULL, NULL, u.mgnr, 1, '{date:yyyy-MM-dd}', 'auto', 'elwig', + CEIL((u.diff - {allowanceKg}.0 - {allowanceKgPerShare}.0 * u.shares) / s.max_kg_per_share + - {allowanceShares.ToString(CultureInfo.InvariantCulture)} + - {allowanceRel.ToString(CultureInfo.InvariantCulture)} * u.shares) AS adjust_shares, + s.share_value / POW(10, s.precision - 2), s.currency FROM v_total_under_delivery u JOIN season s ON s.year = u.year JOIN member m ON m.mgnr = u.mgnr - WHERE s.year = {Year} AND bs >= {addMinBs} AND m.active - ON CONFLICT DO UPDATE - SET business_shares = excluded.business_shares; - - UPDATE member - SET business_shares = member.business_shares + h.business_shares - FROM member_history h - WHERE h.date = '{Year}-11-30' AND h.type = 'auto' AND h.mgnr = member.mgnr; + WHERE s.year = {Year} AND adjust_shares >= {addMinShares} AND m.active """); } public async Task UnAdjustBusinessShares() { using var cnx = await AppDbContext.ConnectAsync(); await cnx.ExecuteBatch($""" - UPDATE member - SET business_shares = member.business_shares - h.business_shares - FROM member_history h - WHERE h.date = '{Year}-11-30' AND h.type = 'auto' AND h.mgnr = member.mgnr AND member.active; - - DELETE FROM member_history WHERE date = '{Year}-11-30' AND type = 'auto'; + DELETE FROM member_history WHERE source = 'elwig' AND reason = 'auto' AND SUBSTR(date, 1, 4) = '{Year}'; """); } diff --git a/Elwig/Helpers/Billing/BillingVariant.cs b/Elwig/Helpers/Billing/BillingVariant.cs index c8b7f7f..3ba69d6 100644 --- a/Elwig/Helpers/Billing/BillingVariant.cs +++ b/Elwig/Helpers/Billing/BillingVariant.cs @@ -76,7 +76,7 @@ namespace Elwig.Helpers.Billing { IIF(m.buchführend, s.vat_normal, s.vat_flatrate) AS vat, ROUND(IIF({Data.ConsiderTotalPenalty}, COALESCE(b.total_penalty, 0), 0) / POW(10, s.precision - 2)) + ROUND(IIF({Data.ConsiderContractPenalties}, COALESCE(u.total_penalty, 0), 0) / POW(10, 4 - 2)) + - ROUND(IIF({Data.ConsiderAutoBusinessShares}, -COALESCE(a.total_amount, 0), 0) / POW(10, s.precision - 2)) + + ROUND(IIF({Data.ConsiderAutoBusinessShares}, -COALESCE(a.total_amount, 0), 0)) + IIF({Data.ConsiderCustomModifiers}, COALESCE(x.amount, 0), 0) AS modifiers, IIF(lc.amount < 0, 0, lc.modifiers) AS prev_modifiers @@ -137,11 +137,11 @@ namespace Elwig.Helpers.Billing { await cnx.ExecuteBatch($""" INSERT INTO payment_member (year, avnr, mgnr, net_amount, mod_abs, mod_rel) SELECT c.year, {AvNr}, s.mgnr, 0, - ROUND(s.sum * COALESCE(m.abs, 0)), + ROUND(s.weight_total * COALESCE(m.abs, 0)), COALESCE(m.rel, 0) FROM (SELECT {Year} AS year, m.mgnr, - ROUND(AVG(COALESCE(a.sum, b.sum)) * {multiplier}) AS baseline, - COUNT(*) = {lastYears} AND MIN(COALESCE(a.sum, b.sum)) > 0 AS allowed + ROUND(AVG(COALESCE(a.weight_total, b.weight_total)) * {multiplier}) AS baseline, + COUNT(*) = {lastYears} AND MIN(COALESCE(a.weight_total, b.weight_total)) > 0 AS allowed FROM member m LEFT JOIN v_stat_member a ON a.mgnr = m.mgnr FULL OUTER JOIN v_stat_member b ON b.mgnr = m.predecessor_mgnr AND b.year = a.year AND {(includePredecessor ? "TRUE" : "FALSE")} @@ -150,7 +150,7 @@ namespace Elwig.Helpers.Billing { HAVING allowed) c JOIN v_stat_member s ON (s.year, s.mgnr) = (c.year, c.mgnr) LEFT JOIN modifier m ON m.year = c.year AND m.name LIKE '{modName}' - WHERE sum >= baseline + WHERE weight_total >= baseline ON CONFLICT DO UPDATE SET mod_abs = mod_abs + excluded.mod_abs, mod_rel = mod_rel + excluded.mod_rel diff --git a/Elwig/Helpers/ClientParameters.cs b/Elwig/Helpers/ClientParameters.cs index 6f199e8..9eaec57 100644 --- a/Elwig/Helpers/ClientParameters.cs +++ b/Elwig/Helpers/ClientParameters.cs @@ -75,7 +75,7 @@ namespace Elwig.Helpers { public int ExportEbicsVersion; public int ExportEbicsAddress; - public (int? AllowanceKg, double? AllowanceBs, int? AllowanceKgPerBs, double? AllowancePercent, int? MinBs) AutoAdjustBs; + public (int? AllowanceKg, double? AllowanceShares, int? AllowanceKgPerShare, double? AllowancePercent, int? MinShares) AutoAdjustShares; public ClientParameters(AppDbContext ctx) : this(ctx.ClientParameters.ToDictionary(e => e.Param, e => e.Value)) { } @@ -176,7 +176,7 @@ namespace Elwig.Helpers { } var autoAdjust = (parameters.GetValueOrDefault("AUTOADJUST_BUSINESSSHARES") ?? "").Split(';'); - AutoAdjustBs = autoAdjust.Length == 5 ? ( + AutoAdjustShares = autoAdjust.Length == 5 ? ( int.TryParse(autoAdjust[0], out var v1) ? v1 : null, double.TryParse(autoAdjust[1], out var v2) ? v2 : null, int.TryParse(autoAdjust[2], out var v3) ? v3 : null, @@ -235,9 +235,9 @@ namespace Elwig.Helpers { case 1: exportEbicsAddress = "LINES"; break; case 2: exportEbicsAddress = "FULL"; break; } - string autoAdjust = $"{AutoAdjustBs.AllowanceKg};{AutoAdjustBs.AllowanceBs?.ToString(CultureInfo.InvariantCulture)};" + - $"{AutoAdjustBs.AllowanceKgPerBs};{AutoAdjustBs.AllowancePercent?.ToString(CultureInfo.InvariantCulture)};" + - $"{AutoAdjustBs.MinBs}"; + string autoAdjust = $"{AutoAdjustShares.AllowanceKg};{AutoAdjustShares.AllowanceShares?.ToString(CultureInfo.InvariantCulture)};" + + $"{AutoAdjustShares.AllowanceKgPerShare};{AutoAdjustShares.AllowancePercent?.ToString(CultureInfo.InvariantCulture)};" + + $"{AutoAdjustShares.MinShares}"; return [ ("CLIENT_NAME_TOKEN", NameToken), ("CLIENT_NAME_SHORT", NameShort), diff --git a/Elwig/Helpers/Export/ElwigData.cs b/Elwig/Helpers/Export/ElwigData.cs index e3fc10f..925ece4 100644 --- a/Elwig/Helpers/Export/ElwigData.cs +++ b/Elwig/Helpers/Export/ElwigData.cs @@ -59,6 +59,7 @@ namespace Elwig.Helpers.Export { List BillingAddresses, List TelephoneNumbers, List EmailAddresses, + List MemberHistory, List AreaCommitments, List Contracts, List Riede, @@ -76,7 +77,7 @@ namespace Elwig.Helpers.Export { foreach (var filename in filenames) { try { - data.Add(new([], [], [], [], [], [], [], new([], [], [], [], [], new() { + data.Add(new([], [], [], [], [], [], [], new([], [], [], [], [], [], new() { ["member"] = [], ["area_commitment_contract"] = [], ["area_commitment"] = [], @@ -142,6 +143,17 @@ namespace Elwig.Helpers.Export { } } + var historyJson = zip.GetEntry("member_history.json"); + if (historyJson != null) { + using var reader = new StreamReader(historyJson.Open(), Utils.UTF8); + string? line; + while ((line = await reader.ReadLineAsync()) != null) { + var obj = JsonNode.Parse(line)!.AsObject(); + var h = obj.ToMemberHistory(); + r.MemberHistory.Add(h); + } + } + // legacy area commitments var areaComsJson = zip.GetEntry("area_commitments.json"); if (areaComsJson != null) { @@ -222,7 +234,7 @@ namespace Elwig.Helpers.Export { var importedAreaComs = new List<(string FileName, string ZwstId, string Device, int New, int Overwritten, int NotImported, string? Filters)>(); var importedDeliveries = new List<(string FileName, string ZwstId, string Device, int New, int Overwritten, int NotImported, string? Filters)>(); - foreach (var ((members, billingAddresses, telephoneNumbers, emailAddresses, areaCommitments, contracts, riede, wbKgs, wbGls, deliveries, deliveryParts, modifiers, timestamps), meta) in data.Zip(metaData)) { + foreach (var ((members, billingAddresses, telephoneNumbers, emailAddresses, history, areaCommitments, contracts, riede, wbKgs, wbGls, deliveries, deliveryParts, modifiers, timestamps), meta) in data.Zip(metaData)) { var branch = branches[meta.ZwstId]; var device = meta.Device; @@ -234,6 +246,12 @@ namespace Elwig.Helpers.Export { .Select(k => k.KgNr) .ToListAsync(); + var histNrs = history.Select(h => h.HistNr).ToList(); + var duplicateHistNrs = await ctx.MemberHistory + .Where(h => histNrs.Contains(h.HistNr)) + .Select(h => h.HistNr) + .ToListAsync(); + var mgnrs = members.Select(m => m.MgNr).ToList(); var duplicateMgNrs = await ctx.Members .Where(m => mgnrs.Contains(m.MgNr)) @@ -323,6 +341,11 @@ namespace Elwig.Helpers.Export { importedMembers.Add((meta.FileName, meta.ZwstId, meta.Device, n, o, members.Count - n - o, meta.MemberFilters)); } + if (importDuplicateMembers || importNewMembers) { + ctx.UpdateRange(history.Where(h => duplicateHistNrs.Contains(h.HistNr))); + ctx.AddRange(history.Where(h => !duplicateHistNrs.Contains(h.HistNr))); + } + if (importDuplicateContracts) { ctx.RemoveRange(ctx.AreaCommitments.IgnoreAutoIncludes().Where(c => duplicateFbNrs.Contains(c.FbNr))); ctx.UpdateRange(contracts.Where(c => duplicateFbNrs.Contains(c.FbNr))); @@ -379,7 +402,9 @@ namespace Elwig.Helpers.Export { importedDeliveries.Add((meta.FileName, meta.ZwstId, meta.Device, n, o, deliveries.Count - n - o, meta.DeliveryFilters)); } + await ctx.Database.ExecuteSqlAsync($"UPDATE client_parameter SET value = '0' WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS'"); await ctx.SaveChangesAsync(); + await ctx.Database.ExecuteSqlAsync($"UPDATE client_parameter SET value = '1' WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS'"); var primaryKeys = new Dictionary() { ["member"] = "mgnr, 0, 0", @@ -442,16 +467,18 @@ namespace Elwig.Helpers.Export { $"{branch} (Gerät {device}) {(duplicate ? "überschrieben" : "importiert")} werden?", true); } - public static Task Export(string filename, IEnumerable members, IEnumerable wbKgs, IEnumerable filters) { + public static Task Export(string filename, IEnumerable members, IEnumerable history, IEnumerable wbKgs, IEnumerable filters) { return new ElwigExport { Members = (members, filters), + History = (history, ["von exportierten Mitgliedern"]), WbKgs = (wbKgs, ["von exportierten Mitgliedern"]), }.Export(filename); } - public static Task Export(string filename, IEnumerable members, IEnumerable areaComs, IEnumerable wbKgs, IEnumerable filters) { + public static Task Export(string filename, IEnumerable members, IEnumerable history, IEnumerable areaComs, IEnumerable wbKgs, IEnumerable filters) { return new ElwigExport { Members = (members, filters), + History = (history, ["von exportierten Mitgliedern"]), AreaComs = (areaComs, ["von exportierten Mitgliedern"]), WbKgs = (wbKgs, ["von exportierten Mitgliedern und Flächenbindungen"]), }.Export(filename); @@ -467,6 +494,7 @@ namespace Elwig.Helpers.Export { public class ElwigExport { public (IEnumerable WbKgs, IEnumerable Filters)? WbKgs { get; set; } public (IEnumerable Members, IEnumerable Filters)? Members { get; set; } + public (IEnumerable History, IEnumerable Filters)? History { get; set; } public (IEnumerable AreaComs, IEnumerable Filters)? AreaComs { get; set; } public (IEnumerable Deliveries, IEnumerable Filters)? Deliveries { get; set; } @@ -486,17 +514,21 @@ namespace Elwig.Helpers.Export { ["zwstid"] = App.ZwstId, ["device"] = Environment.MachineName, }; - if (WbKgs != null) { + if (WbKgs != null) obj["wb_kgs"] = new JsonObject { ["count"] = WbKgs.Value.WbKgs.Count(), ["filters"] = new JsonArray(WbKgs.Value.Filters.Select(f => (JsonNode)f).ToArray()), }; - } if (Members != null) obj["members"] = new JsonObject { ["count"] = Members.Value.Members.Count(), ["filters"] = new JsonArray(Members.Value.Filters.Select(f => (JsonNode)f).ToArray()), }; + if (History != null) + obj["member_history"] = new JsonObject { + ["count"] = History.Value.History.Count(), + ["filters"] = new JsonArray(History.Value.Filters.Select(f => (JsonNode)f).ToArray()), + }; if (AreaComs != null) obj["area_commitment_contracts"] = new JsonObject { ["count"] = AreaComs.Value.AreaComs.Count(), @@ -527,6 +559,13 @@ namespace Elwig.Helpers.Export { await writer.WriteLineAsync(m.ToJson().ToJsonString(Utils.JsonOpts)); } } + if (History != null) { + var json = zip.CreateEntry("member_history.json", CompressionLevel.SmallestSize); + using var writer = new StreamWriter(json.Open(), Utils.UTF8); + foreach (var h in History.Value.History) { + await writer.WriteLineAsync(h.ToJson().ToJsonString(Utils.JsonOpts)); + } + } if (AreaComs != null) { var json = zip.CreateEntry("area_commitment_contracts.json", CompressionLevel.SmallestSize); using var writer = new StreamWriter(json.Open(), Utils.UTF8); @@ -585,7 +624,10 @@ namespace Elwig.Helpers.Export { ["birthday"] = m.Birthday, ["entry_date"] = m.EntryDate != null ? $"{m.EntryDate:yyyy-MM-dd}" : null, ["exit_date"] = m.ExitDate != null ? $"{m.ExitDate:yyyy-MM-dd}" : null, - ["business_shares"] = m.BusinessShares, + ["shares"] = m.Shares, + ["shares_red"] = m.SharesRed, + ["shares_white"] = m.SharesWhite, + ["shares_dormant"] = m.SharesDormant, ["accounting_nr"] = m.AccountingNr, ["zwstid"] = m.ZwstId, ["lfbis_nr"] = m.LfbisNr, @@ -654,7 +696,10 @@ namespace Elwig.Helpers.Export { Birthday = json["birthday"]?.AsValue().GetValue(), EntryDateString = json["entry_date"]?.AsValue().GetValue(), ExitDateString = json["exit_date"]?.AsValue().GetValue(), - BusinessShares = json["business_shares"]?.AsValue().GetValue() ?? 0, + Shares = json["shares"]?.AsValue().GetValue() ?? json["business_shares"]?.AsValue().GetValue() ?? 0, + SharesRed = json["shares_red"]?.AsValue().GetValue() ?? 0, + SharesWhite = json["shares_white"]?.AsValue().GetValue() ?? 0, + SharesDormant = json["shares_dormant"]?.AsValue().GetValue() ?? 0, AccountingNr = json["accounting_nr"]?.AsValue().GetValue(), ZwstId = json["zwstid"]?.AsValue().GetValue(), LfbisNr = json["lfbis_nr"]?.AsValue().GetValue(), @@ -699,6 +744,40 @@ namespace Elwig.Helpers.Export { DateTime.ParseExact(modifiedAt, "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None))); } + public static JsonObject ToJson(this MemberHistory h) { + return new JsonObject { + ["histnr"] = h.HistNr, + ["from_mgnr"] = h.FromMgNr, + ["from_type"] = h.FromType, + ["to_mgnr"] = h.ToMgNr, + ["to_type"] = h.ToType, + ["date"] = h.DateString, + ["reason"] = h.Reason, + ["source"] = h.Source, + ["shares"] = h.Shares, + ["value_per_share"] = h.ValuePerShare, + ["currency"] = h.CurrencyCode, + ["comment"] = h.Comment, + }; + } + + public static MemberHistory ToMemberHistory(this JsonNode json) { + return new MemberHistory { + HistNr = json["histnr"]!.AsValue().GetValue(), + FromMgNr = json["from_mgnr"]?.AsValue().GetValue(), + FromType = json["from_type"]?.AsValue().GetValue(), + ToMgNr = json["to_mgnr"]?.AsValue().GetValue(), + ToType = json["to_type"]?.AsValue().GetValue(), + DateString = json["date"]!.AsValue().GetValue(), + Reason = json["reason"]!.AsValue().GetValue(), + Source = json["source"]!.AsValue().GetValue(), + Shares = json["shares"]!.AsValue().GetValue(), + ValuePerShare = json["value_per_share"]?.AsValue().GetValue(), + CurrencyCode = json["currency"]?.AsValue().GetValue(), + Comment = json["comment"]?.AsValue().GetValue(), + }; + } + public static JsonObject ToJson(this AreaComContract c) { return new JsonObject { ["fbnr"] = c.FbNr, diff --git a/Elwig/Models/Dtos/CreditNoteData.cs b/Elwig/Models/Dtos/CreditNoteData.cs index f52bdc0..27eb650 100644 --- a/Elwig/Models/Dtos/CreditNoteData.cs +++ b/Elwig/Models/Dtos/CreditNoteData.cs @@ -29,7 +29,7 @@ namespace Elwig.Models.Dtos { ("Gross", "Brutto", "€", 20), ("Penalties", "Pönalen FB", "€", 20), ("Penalty", "Unterl. GA", "€", 20), - ("AutoBs", "GA Nachz.", "€", 20), + ("AutoShares", "GA Nachz.", "€", 20), ("Custom", "Weitere", "€", 20), ("Others", "Sonstige", "€", 20), ("Considered", "Berückstgt.", "€", 20), @@ -56,9 +56,9 @@ namespace Elwig.Models.Dtos { p.plz, o.name AS ort, m.address, m.iban, c.tgnr, s.year, s.precision, p.amount - p.net_amount AS surcharge, c.net_amount, c.prev_net_amount, c.vat, c.vat_amount, c.gross_amount, c.modifiers, c.prev_modifiers, c.amount, - ROUND(b.total_penalty / POW(10, s.precision - 2)) AS bs_penalty, - ROUND(u.total_penalty / POW(10, 4 - 2)) AS fb_penalty, - ROUND(-a.total_amount / POW(10, s.precision - 2)) AS auto_bs, + ROUND(b.total_penalty / POW(10, s.precision - 2)) AS shares_penalty, + ROUND(u.total_penalty / POW(10, 4 - 2)) AS ac_penalty, + -a.total_amount AS auto_shares, x.amount AS custom_mod FROM credit c LEFT JOIN member m ON m.mgnr = c.mgnr @@ -94,7 +94,7 @@ namespace Elwig.Models.Dtos { public decimal Gross; public decimal? Penalties; public decimal? Penalty; - public decimal? AutoBs; + public decimal? AutoShares; public decimal? Custom; public decimal? Others; public decimal? Considered; @@ -122,14 +122,14 @@ namespace Elwig.Models.Dtos { } decimal mod = (row.Modifiers == null) ? 0 : Utils.DecFromDb((long)row.Modifiers, prec1); if (data.ConsiderContractPenalties) - Penalties = (row.FbPenalty == null) ? null : Utils.DecFromDb((long)row.FbPenalty, prec1); + Penalties = (row.AcPenalty == null) ? null : Utils.DecFromDb((long)row.AcPenalty, prec1); if (data.ConsiderTotalPenalty) - Penalty = (row.BsPenalty == null) ? null : Utils.DecFromDb((long)row.BsPenalty, prec1); + Penalty = (row.SharesPenalty == null) ? null : Utils.DecFromDb((long)row.SharesPenalty, prec1); if (data.ConsiderAutoBusinessShares) - AutoBs = (row.AutoBs == null) ? null : Utils.DecFromDb((long)row.AutoBs, prec1); + AutoShares = (row.AutoShares == null) ? null : Utils.DecFromDb((long)row.AutoShares, prec1); if (data.ConsiderCustomModifiers) Custom = (row.CustomMod == null) ? null : Utils.DecFromDb((long)row.CustomMod, prec1); - mod -= (Penalties ?? 0) + (Penalty ?? 0) + (AutoBs ?? 0) + (Custom ?? 0); + mod -= (Penalties ?? 0) + (Penalty ?? 0) + (AutoShares ?? 0) + (Custom ?? 0); Others = (mod == 0) ? null : mod; Gross = Utils.DecFromDb(row.GrossAmount, prec1); Considered = (row.PrevModifiers == null || row.PrevModifiers == 0) ? null : -Utils.DecFromDb((long)row.PrevModifiers, prec1); @@ -179,12 +179,12 @@ namespace Elwig.Models.Dtos { public long? PrevModifiers { get; set; } [Column("amount")] public long Amount { get; set; } - [Column("bs_penalty")] - public long? BsPenalty { get; set; } - [Column("fb_penalty")] - public long? FbPenalty { get; set; } - [Column("auto_bs")] - public long? AutoBs { get; set; } + [Column("shares_penalty")] + public long? SharesPenalty { get; set; } + [Column("ac_penalty")] + public long? AcPenalty { get; set; } + [Column("auto_shares")] + public long? AutoShares { get; set; } [Column("custom_mod")] public long? CustomMod { get; set; } } diff --git a/Elwig/Models/Dtos/CreditNoteDeliveryData.cs b/Elwig/Models/Dtos/CreditNoteDeliveryData.cs index e044987..bfb97d1 100644 --- a/Elwig/Models/Dtos/CreditNoteDeliveryData.cs +++ b/Elwig/Models/Dtos/CreditNoteDeliveryData.cs @@ -42,13 +42,10 @@ namespace Elwig.Models.Dtos { } private static async Task> FromDbSet(DbSet table, int? year = null, int? avnr = null, int? mgnr = null) { - var y = year?.ToString() ?? "NULL"; - var v = avnr?.ToString() ?? "NULL"; - var m = mgnr?.ToString() ?? "NULL"; - return await table.FromSqlRaw($""" + return await table.FromSql($""" SELECT d.year, c.tgnr, v.avnr, d.mgnr, d.did, d.lsnr, d.dpnr, d.weight, d.modifiers, b.bktnr, d.sortid, b.discr, b.value, pb.price, pb.amount, p.net_amount, p.amount AS total_amount, - s.name AS variety, a.name AS attribute, c.name AS cultivation, q.qualid AS qualid, q.name AS quality_level, d.oe, d.kmw, d.net_weight + s.name AS variety, s.type AS type, a.name AS attribute, c.name AS cultivation, q.qualid AS qualid, q.name AS quality_level, d.oe, d.kmw, d.net_weight FROM v_delivery d JOIN wine_variety s ON s.sortid = d.sortid LEFT JOIN wine_attribute a ON a.attrid = d.attrid @@ -59,7 +56,7 @@ namespace Elwig.Models.Dtos { LEFT JOIN payment_delivery_part p ON (p.year, p.did, p.dpnr, p.avnr) = (d.year, d.did, d.dpnr, v.avnr) LEFT JOIN payment_delivery_part_bucket pb ON (pb.year, pb.did, pb.dpnr, pb.bktnr, pb.avnr) = (b.year, b.did, b.dpnr, b.bktnr, v.avnr) LEFT JOIN credit c ON (c.year, c.avnr, c.mgnr) = (d.year, v.avnr, d.mgnr) - WHERE b.value > 0 AND (d.year = {y} OR {y} IS NULL) AND (v.avnr = {v} OR {v} IS NULL) AND (d.mgnr = {m} OR {m} IS NULL) + WHERE b.value > 0 AND (d.year = {year} OR {year} IS NULL) AND (v.avnr = {avnr} OR {avnr} IS NULL) AND (d.mgnr = {mgnr} OR {mgnr} IS NULL) ORDER BY d.year, v.avnr, d.mgnr, d.lsnr, d.dpnr """).ToListAsync(); } @@ -75,6 +72,7 @@ namespace Elwig.Models.Dtos { public string LsNr; public int DPNr; public string Variety; + public string Type; public string? Attribute; public string? Cultivation; public string[] Modifiers; @@ -97,24 +95,24 @@ namespace Elwig.Models.Dtos { LsNr = f.LsNr; DPNr = f.DPNr; Variety = f.Variety; + Type = f.Type; Attribute = f.Attribute; Cultivation = f.Cultivation; var modifiers = (IEnumerable)(f.Modifiers ?? "").Split(',') .Select(m => season?.Modifiers.FirstOrDefault(s => s.ModId == m)) .Where(m => m != null) - .OrderBy(m => m.Ordering) + .OrderBy(m => m!.Ordering) .ToList(); - Modifiers = modifiers.Select(m => m.Name).ToArray(); + Modifiers = [.. modifiers.Select(m => m.Name)]; QualId = f.QualId; QualityLevel = f.QualityLevel; Gradation = (f.Oe, f.Kmw); - Buckets = rows + Buckets = [.. rows .Where(b => b.Value > 0) .OrderByDescending(b => b.BktNr) .Select(b => (b.Discr == "_" ? "ungeb." : $"geb. {f.SortId}{b.Discr}", b.Value, b.Price != null ? season?.DecFromDb((long)b.Price) : null, - b.Amount != null ? season?.DecFromDb((long)b.Amount) : null)) - .ToArray(); + b.Amount != null ? season?.DecFromDb((long)b.Amount) : null))]; WeighingModifier = (varData == null || !varData.ConsiderDelieryModifiers) ? 0 : f.NetWeight ? varData.NetWeightModifier : varData.GrossWeightModifier; Amount = f.TotalAmount != null ? season?.DecFromDb((long)f.TotalAmount) : null; var netAmount = f.NetAmount != null ? season?.DecFromDb((long)f.NetAmount) : null; @@ -161,6 +159,8 @@ namespace Elwig.Models.Dtos { public long? TotalAmount { get; set; } [Column("variety")] public required string Variety { get; set; } + [Column("type")] + public required string Type { get; set; } [Column("attribute")] public string? Attribute { get; set; } [Column("cultivation")] diff --git a/Elwig/Models/Dtos/MemberDeliveryData.cs b/Elwig/Models/Dtos/MemberDeliveryData.cs index ca27407..6b0622e 100644 --- a/Elwig/Models/Dtos/MemberDeliveryData.cs +++ b/Elwig/Models/Dtos/MemberDeliveryData.cs @@ -8,7 +8,7 @@ namespace Elwig.Models.Dtos { public class MemberDeliveryData : DataTable { private static readonly (string, string, string?, int?)[] FieldNames = [ - ("MgNr", "MgNr.", null, 12), + ("MgNr", "MgNr.", null, 12), ("Name1", "Name", null, 40), ("Name2", "Vorname", null, 40), ("Address", "Adresse", null, 60), diff --git a/Elwig/Models/Dtos/MemberListData.cs b/Elwig/Models/Dtos/MemberListData.cs index f38f684..fa7b0cc 100644 --- a/Elwig/Models/Dtos/MemberListData.cs +++ b/Elwig/Models/Dtos/MemberListData.cs @@ -19,7 +19,7 @@ namespace Elwig.Models.Dtos { ("Locality", "Ort", null, 60), ("DefaultKg", "Stammgemeinde", null, 60), ("Branch", "Zweigstelle", null, 40), - ("BusinessShares", "GA", null, 10), + ("SharesTotal", "GA", null, 10), ("BillingName", "Rechnungsname", null, 60), ("BillingAddress", "Rechnungsadresse", null, 60), ("BillingPlz", "PLZ", null, 10), @@ -66,7 +66,11 @@ namespace Elwig.Models.Dtos { public string? Name2; public string? DefaultKg; public string? Branch; - public int BusinessShares; + public int Shares; + public int SharesRed; + public int SharesWhite; + public int SharesDormant; + public int SharesTotal; public string Address; public int Plz; public string Locality; @@ -98,7 +102,11 @@ namespace Elwig.Models.Dtos { Name2 = m.AdministrativeName2; DefaultKg = m.DefaultKg?.Name; Branch = m.Branch?.Name; - BusinessShares = m.BusinessShares; + Shares = m.Shares; + SharesRed = m.SharesRed; + SharesWhite = m.SharesWhite; + SharesDormant = m.SharesDormant; + SharesTotal = Shares + SharesRed + SharesWhite + SharesDormant; Address = m.Address; Plz = m.PostalDest.AtPlz!.Plz; Locality = m.PostalDest.AtPlz!.Ort.Name; diff --git a/Elwig/Models/Dtos/OverUnderDeliveryData.cs b/Elwig/Models/Dtos/OverUnderDeliveryData.cs index df7a5a4..47a7120 100644 --- a/Elwig/Models/Dtos/OverUnderDeliveryData.cs +++ b/Elwig/Models/Dtos/OverUnderDeliveryData.cs @@ -13,33 +13,71 @@ namespace Elwig.Models.Dtos { ("Address", "Adresse", null, 60), ("Plz", "PLZ", null, 10), ("Locality", "Ort", null, 60), - ("BusinessShares", "GA", null, 10), + ("Shares", "GA", null, 10), ("DeliveryObligation", "Lieferpflicht", "kg", 22), ("DeliveryRight", "Lieferrecht", "kg", 22), - ("Weight", "Geliefert", "kg", 22), + ("WeightTotal", "Geliefert", "kg", 22), ("OverUnderDelivery", "Über-/Unterliefert", "kg|%", 34), ]; + private static readonly (string, string, string?, int)[] FieldNamesRed = [ + ("MgNr", "MgNr.", null, 12), + ("Name1", "Name", null, 40), + ("Name2", "Vorname", null, 40), + ("Address", "Adresse", null, 60), + ("Plz", "PLZ", null, 10), + ("Locality", "Ort", null, 60), + ("SharesRed", "GA", null, 10), + ("DeliveryObligationRed", "Lieferpflicht", "kg", 22), + ("DeliveryRightRed", "Lieferrecht", "kg", 22), + ("WeightRed", "Geliefert", "kg", 22), + ("OverUnderDeliveryRed", "Über-/Unterliefert", "kg|%", 34), + ]; + + private static readonly (string, string, string?, int)[] FieldNamesWhite = [ + ("MgNr", "MgNr.", null, 12), + ("Name1", "Name", null, 40), + ("Name2", "Vorname", null, 40), + ("Address", "Adresse", null, 60), + ("Plz", "PLZ", null, 10), + ("Locality", "Ort", null, 60), + ("SharesWhite", "GA", null, 10), + ("DeliveryObligationWhite", "Lieferpflicht", "kg", 22), + ("DeliveryRightWhite", "Lieferrecht", "kg", 22), + ("WeightWhite", "Geliefert", "kg", 22), + ("OverUnderDeliveryWhite", "Über-/Unterliefert", "kg|%", 34), + ]; + public OverUnderDeliveryData(IEnumerable rows, int year) : base($"Über-Unterlieferungen", $"Über- und Unterlieferungen laut gezeichneten Geschäftsanteilen {year}", rows, FieldNames) { } + public OverUnderDeliveryData(IEnumerable rows, int year, string mode) : + base($"Über-Unterlieferungen", $"Über- und Unterlieferungen laut gezeichneten Geschäftsanteilen {(mode == "R" ? "rot" : "weiß")} {year}", rows, + mode == "R" ? FieldNamesRed : FieldNamesWhite) { + } + public static async Task ForSeason(DbSet table, int year) { var rows = 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, m.business_shares, - m.business_shares * s.min_kg_per_bs AS min_kg, - m.business_shares * s.max_kg_per_bs AS max_kg, - COALESCE(SUM(d.weight), 0) AS sum + p.plz, o.name AS ort, m.address, m.shares, m.shares_red, m.shares_white, + m.shares * COALESCE(s.min_kg_per_share, 0) AS min_kg, + m.shares * COALESCE(s.max_kg_per_share, 0) AS max_kg, + m.shares_red * COALESCE(s.min_kg_per_share_red, s.min_kg_per_share, 0) AS min_kg_red, + m.shares_red * COALESCE(s.max_kg_per_share_red, s.max_kg_per_share, 0) AS max_kg_red, + m.shares_white * COALESCE(s.min_kg_per_share_white, s.min_kg_per_share, 0) AS min_kg_white, + m.shares_white * COALESCE(s.max_kg_per_share_white, s.max_kg_per_share, 0) AS max_kg_white, + COALESCE(d.weight_total, 0) AS weight_total, + COALESCE(d.weight_red, 0) AS weight_red, + COALESCE(d.weight_white, 0) AS weight_white FROM season s, member m LEFT JOIN AT_plz_dest p ON p.id = m.postal_dest LEFT JOIN AT_ort o ON o.okz = p.okz - LEFT JOIN v_delivery d ON (d.year, d.mgnr) = (s.year, m.mgnr) - WHERE s.year = {year} AND (m.active = TRUE OR d.weight > 0) - GROUP BY s.year, m.mgnr - ORDER BY 100.0 * sum / max_kg, m.mgnr + LEFT JOIN v_stat_member d ON (d.year, d.mgnr) = (s.year, m.mgnr) + WHERE s.year = {year} AND (m.active = TRUE OR d.weight_total > 0) + ORDER BY 100.0 * weight_total / (max_kg + max_kg_red + max_kg_white), m.mgnr """).ToListAsync(); return new OverUnderDeliveryData(rows, year); } @@ -61,17 +99,41 @@ namespace Elwig.Models.Dtos { public required string LocalityFull { get; set; } [NotMapped] public string Locality => LocalityFull.Split(",")[0]; - [Column("business_shares")] - public int BusinessShares { get; set; } + [Column("shares")] + public int Shares { get; set; } + [Column("shares_red")] + public int SharesRed { get; set; } + [Column("shares_white")] + public int SharesWhite { get; set; } [Column("min_kg")] public int DeliveryObligation { get; set; } [Column("max_kg")] public int DeliveryRight { get; set; } - [Column("sum")] - public int Weight { get; set; } [NotMapped] public (int? Kg, double? Percent) OverUnderDelivery => - Weight < DeliveryObligation ? (Weight - DeliveryObligation, Weight * 100.0 / DeliveryObligation - 100.0) : - Weight > DeliveryRight ? (Weight - DeliveryRight, Weight * 100.0 / DeliveryRight - 100.0) : (null, null); + WeightTotal < DeliveryObligation ? (WeightTotal - DeliveryObligation, WeightTotal * 100.0 / DeliveryObligation - 100.0) : + WeightTotal > DeliveryRight ? (WeightTotal - DeliveryRight, WeightTotal * 100.0 / DeliveryRight - 100.0) : (null, null); + [Column("min_kg_red")] + public int DeliveryObligationRed { get; set; } + [Column("max_kg_red")] + public int DeliveryRightRed { get; set; } + [NotMapped] + public (int? Kg, double? Percent) OverUnderDeliveryRed => + WeightRed < DeliveryObligationRed ? (WeightRed - DeliveryObligationRed, WeightRed * 100.0 / DeliveryObligationRed - 100.0) : + WeightRed > DeliveryRightRed ? (WeightRed - DeliveryRightRed, WeightRed * 100.0 / DeliveryRightRed - 100.0) : (null, null); + [Column("min_kg_white")] + public int DeliveryObligationWhite { get; set; } + [Column("max_kg_white")] + public int DeliveryRightWhite { get; set; } + [NotMapped] + public (int? Kg, double? Percent) OverUnderDeliveryWhite => + WeightWhite < DeliveryObligationWhite ? (WeightWhite - DeliveryObligationWhite, WeightWhite * 100.0 / DeliveryObligationWhite - 100.0) : + WeightWhite > DeliveryRightWhite ? (WeightWhite - DeliveryRightWhite, WeightWhite * 100.0 / DeliveryRightWhite - 100.0) : (null, null); + [Column("weight_total")] + public int WeightTotal { get; set; } + [Column("weight_red")] + public int WeightRed { get; set; } + [Column("weight_white")] + public int WeightWhite { get; set; } } } diff --git a/Elwig/Models/Entities/Member.cs b/Elwig/Models/Entities/Member.cs index 3c5e7f7..343b83e 100644 --- a/Elwig/Models/Entities/Member.cs +++ b/Elwig/Models/Entities/Member.cs @@ -67,8 +67,17 @@ namespace Elwig.Models.Entities { set => ExitDateString = value?.ToString("yyyy-MM-dd"); } - [Column("business_shares")] - public int BusinessShares { get; set; } + [Column("shares")] + public int Shares { get; set; } + + [Column("shares_red")] + public int SharesRed { get; set; } + + [Column("shares_white")] + public int SharesWhite { get; set; } + + [Column("shares_dormant")] + public int SharesDormant { get; set; } [Column("accounting_nr")] public string? AccountingNr { get; set; } @@ -189,9 +198,14 @@ namespace Elwig.Models.Entities { [InverseProperty(nameof(BillingAddr.Member))] public virtual BillingAddr? BillingAddress { get; private set; } - [InverseProperty(nameof(Delivery.Member))] + [InverseProperty(nameof(DeliveryAncmt.Member))] public virtual ICollection Announcements { get; private set; } = null!; + [InverseProperty(nameof(MemberHistory.FromMember))] + public virtual ICollection HistoryFrom { get; private set; } = null!; + [InverseProperty(nameof(MemberHistory.ToMember))] + public virtual ICollection HistoryTo { get; private set; } = null!; + [InverseProperty(nameof(Delivery.Member))] public virtual ICollection Deliveries { get; private set; } = null!; diff --git a/Elwig/Models/Entities/MemberHistory.cs b/Elwig/Models/Entities/MemberHistory.cs index a023983..393ac92 100644 --- a/Elwig/Models/Entities/MemberHistory.cs +++ b/Elwig/Models/Entities/MemberHistory.cs @@ -1,12 +1,23 @@ +using Elwig.Helpers; using Microsoft.EntityFrameworkCore; using System; using System.ComponentModel.DataAnnotations.Schema; namespace Elwig.Models.Entities { - [Table("member_history"), PrimaryKey("MgNr", "DateString", "Type")] + [Table("member_history"), PrimaryKey("HistNr")] public class MemberHistory { - [Column("mgnr")] - public int MgNr { get; set; } + [Column("histnr")] + public int HistNr { get; set; } + + [Column("from_mgnr")] + public int? FromMgNr { get; set; } + [Column("from_type")] + public int? FromType { get; set; } + + [Column("to_mgnr")] + public int? ToMgNr { get; set; } + [Column("to_type")] + public int? ToType { get; set; } [Column("date")] public required string DateString { get; set; } @@ -16,16 +27,39 @@ namespace Elwig.Models.Entities { set => value.ToString("yyyy-MM-dd"); } - [Column("type")] - public required string Type { get; set; } + [Column("reason")] + public required string Reason { get; set; } - [Column("business_shares")] - public int BusinessShares { get; set; } + [Column("source")] + public required string Source { get; set; } + + [Column("shares")] + public int Shares { get; set; } + + [Column("value_per_share")] + public long? ValuePerShareValue { get; set; } + [NotMapped] + public decimal? ValuePerShare { + get => ValuePerShareValue != null ? Utils.DecFromDb(ValuePerShareValue.Value, 2) : null; + set => ValuePerShareValue = value != null ? Utils.DecToDb(value.Value, 2) : null; + } + + [NotMapped] + public decimal? TotalValue => Shares * ValuePerShare; + + [Column("currency")] + public string? CurrencyCode { get; set; } [Column("comment")] public string? Comment { get; set; } - [ForeignKey("MgNr")] - public virtual Member Member { get; private set; } = null!; + [ForeignKey("FromMgNr")] + public virtual Member FromMember { get; private set; } = null!; + + [ForeignKey("ToMgNr")] + public virtual Member ToMember { get; private set; } = null!; + + [ForeignKey("CurrencyCode")] + public virtual Currency Currency { get; private set; } = null!; } } diff --git a/Elwig/Models/Entities/Season.cs b/Elwig/Models/Entities/Season.cs index df61962..6f11369 100644 --- a/Elwig/Models/Entities/Season.cs +++ b/Elwig/Models/Entities/Season.cs @@ -25,11 +25,20 @@ namespace Elwig.Models.Entities { [Column("vat_flatrate")] public double VatFlatrate { get; set; } - [Column("min_kg_per_bs")] - public int MinKgPerBusinessShare { get; set; } + [Column("min_kg_per_share")] + public int? MinKgPerShare { get; set; } + [Column("max_kg_per_share")] + public int? MaxKgPerShare { get; set; } - [Column("max_kg_per_bs")] - public int MaxKgPerBusinessShare { get; set; } + [Column("min_kg_per_share_red")] + public int? MinKgPerShareRed { get; set; } + [Column("max_kg_per_share_red")] + public int? MaxKgPerShareRed { get; set; } + + [Column("min_kg_per_share_white")] + public int? MinKgPerShareWhite { get; set; } + [Column("max_kg_per_share_white")] + public int? MaxKgPerShareWhite { get; set; } [Column("penalty_per_kg")] public long? PenaltyPerKgValue { get; set; } @@ -55,23 +64,23 @@ namespace Elwig.Models.Entities { set => PenaltyNoneValue = value != null ? DecToDb(value.Value) : null; } - [Column("penalty_per_bs_amount")] - public long? PenaltyPerBsAmountValue { get; set; } + [Column("penalty_per_share_amount")] + public long? PenaltyPerShareAmountValue { get; set; } [NotMapped] - public decimal? PenaltyPerBsAmount { - get => PenaltyPerBsAmountValue != null ? DecFromDb(PenaltyPerBsAmountValue.Value) : null; - set => PenaltyPerBsAmountValue = value != null ? DecToDb(value.Value) : null; + public decimal? PenaltyPerShareAmount { + get => PenaltyPerShareAmountValue != null ? DecFromDb(PenaltyPerShareAmountValue.Value) : null; + set => PenaltyPerShareAmountValue = value != null ? DecToDb(value.Value) : null; } - [Column("penalty_per_bs_none")] - public long? PenaltyPerBsNoneValue { get; set; } + [Column("penalty_per_share_none")] + public long? PenaltyPerShareNoneValue { get; set; } [NotMapped] - public decimal? PenaltyPerBsNone { - get => PenaltyPerBsNoneValue != null ? DecFromDb(PenaltyPerBsNoneValue.Value) : null; - set => PenaltyPerBsNoneValue = value != null ? DecToDb(value.Value) : null; + public decimal? PenaltyPerShareNone { + get => PenaltyPerShareNoneValue != null ? DecFromDb(PenaltyPerShareNoneValue.Value) : null; + set => PenaltyPerShareNoneValue = value != null ? DecToDb(value.Value) : null; } - [Column("bs_value")] + [Column("share_value")] public long? BusinessShareValueValue { get; set; } [NotMapped] public decimal? BusinessShareValue { diff --git a/Elwig/Resources/Sql/39-40.sql b/Elwig/Resources/Sql/39-40.sql new file mode 100644 index 0000000..227ab16 --- /dev/null +++ b/Elwig/Resources/Sql/39-40.sql @@ -0,0 +1,298 @@ +-- schema version 39 to 40 + +PRAGMA writable_schema = ON; + +ALTER TABLE member RENAME COLUMN business_shares TO shares; +ALTER TABLE member ADD COLUMN shares_red INTEGER NOT NULL DEFAULT 0; +ALTER TABLE member ADD COLUMN shares_white INTEGER NOT NULL DEFAULT 0; +ALTER TABLE member ADD COLUMN shares_dormant INTEGER NOT NULL DEFAULT 0; + +CREATE TABLE member_history_new ( + histnr INTEGER NOT NULL, + from_mgnr INTEGER, + from_type INTEGER, + to_mgnr INTEGER, + to_type INTEGER, + date TEXT NOT NULL CHECK (date REGEXP '^[1-9][0-9]{3}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$') DEFAULT CURRENT_DATE, + reason TEXT NOT NULL CHECK (reason REGEXP '^[a-z_]+$'), + source TEXT NOT NULL CHECK (source REGEXP '^[a-z_]+$'), + + shares INTEGER NOT NULL, + value_per_share INTEGER DEFAULT NULL, + currency TEXT DEFAULT NULL, + comment TEXT DEFAULT NULL, + + CONSTRAINT pk_member_history PRIMARY KEY (histnr), + CONSTRAINT fk_member_history_member_from FOREIGN KEY (from_mgnr) REFERENCES member (mgnr) + ON UPDATE CASCADE + ON DELETE RESTRICT, + CONSTRAINT fk_member_history_member_to FOREIGN KEY (to_mgnr) REFERENCES member (mgnr) + ON UPDATE CASCADE + ON DELETE RESTRICT, + CONSTRAINT fk_member_history_currency FOREIGN KEY (currency) REFERENCES currency (code) + ON UPDATE CASCADE + ON DELETE RESTRICT, + CONSTRAINT c_member_history CHECK ((from_mgnr IS NOT NULL OR to_mgnr IS NOT NULL) AND + ((from_mgnr IS NULL AND from_type IS NULL) OR (from_mgnr IS NOT NULL AND from_type IS NOT NULL)) AND + ((to_mgnr IS NULL AND to_type IS NULL) OR (to_mgnr IS NOT NULL AND to_type IS NOT NULL))) +) STRICT; + +INSERT INTO member_history_new (histnr, from_mgnr, from_type, to_mgnr, to_type, date, reason, source, shares, value_per_share, currency, comment) +SELECT ROW_NUMBER() OVER(ORDER BY h.date, h.mgnr), NULL, NULL, h.mgnr, 1, h.date, h.type, 'elwig', h.business_shares, s.bs_value / POW(10, s.precision - 2), s.currency, comment +FROM member_history h +JOIN season s ON s.year = SUBSTR(h.date, 1, 4); + +PRAGMA foreign_keys = OFF; +DROP TABLE member_history; +ALTER TABLE member_history_new RENAME TO member_history; +PRAGMA foreign_keys = ON; + +CREATE TRIGGER t_member_history_i_member + AFTER INSERT ON member_history FOR EACH ROW + WHEN (SELECT value FROM client_parameter WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS') = 1 +BEGIN + UPDATE member SET shares = shares - NEW.shares WHERE mgnr = NEW.from_mgnr AND NEW.from_type = 1; + UPDATE member SET shares_red = shares_red - NEW.shares WHERE mgnr = NEW.from_mgnr AND NEW.from_type = 2; + UPDATE member SET shares_white = shares_white - NEW.shares WHERE mgnr = NEW.from_mgnr AND NEW.from_type = 3; + UPDATE member SET shares_dormant = shares_dormant - NEW.shares WHERE mgnr = NEW.from_mgnr AND NEW.from_type = 9; + UPDATE member SET shares = shares + NEW.shares WHERE mgnr = NEW.to_mgnr AND NEW.to_type = 1; + UPDATE member SET shares_red = shares_red + NEW.shares WHERE mgnr = NEW.to_mgnr AND NEW.to_type = 2; + UPDATE member SET shares_white = shares_white + NEW.shares WHERE mgnr = NEW.to_mgnr AND NEW.to_type = 3; + UPDATE member SET shares_dormant = shares_dormant + NEW.shares WHERE mgnr = NEW.to_mgnr AND NEW.to_type = 9; +END; + +CREATE TRIGGER t_member_history_u_member + AFTER UPDATE ON member_history FOR EACH ROW + WHEN (SELECT value FROM client_parameter WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS') = 1 +BEGIN + UPDATE member SET shares = shares + OLD.shares WHERE mgnr = OLD.from_mgnr AND OLD.from_type = 1; + UPDATE member SET shares_red = shares_red + OLD.shares WHERE mgnr = OLD.from_mgnr AND OLD.from_type = 2; + UPDATE member SET shares_white = shares_white + OLD.shares WHERE mgnr = OLD.from_mgnr AND OLD.from_type = 3; + UPDATE member SET shares_dormant = shares_dormant + OLD.shares WHERE mgnr = OLD.from_mgnr AND OLD.from_type = 9; + UPDATE member SET shares = shares - OLD.shares WHERE mgnr = OLD.to_mgnr AND OLD.to_type = 1; + UPDATE member SET shares_red = shares_red - OLD.shares WHERE mgnr = OLD.to_mgnr AND OLD.to_type = 2; + UPDATE member SET shares_white = shares_white - OLD.shares WHERE mgnr = OLD.to_mgnr AND OLD.to_type = 3; + UPDATE member SET shares_dormant = shares_dormant - OLD.shares WHERE mgnr = OLD.to_mgnr AND OLD.to_type = 9; + UPDATE member SET shares = shares - NEW.shares WHERE mgnr = NEW.from_mgnr AND NEW.from_type = 1; + UPDATE member SET shares_red = shares_red - NEW.shares WHERE mgnr = NEW.from_mgnr AND NEW.from_type = 2; + UPDATE member SET shares_white = shares_white - NEW.shares WHERE mgnr = NEW.from_mgnr AND NEW.from_type = 3; + UPDATE member SET shares_dormant = shares_dormant - NEW.shares WHERE mgnr = NEW.from_mgnr AND NEW.from_type = 9; + UPDATE member SET shares = shares + NEW.shares WHERE mgnr = NEW.to_mgnr AND NEW.to_type = 1; + UPDATE member SET shares_red = shares_red + NEW.shares WHERE mgnr = NEW.to_mgnr AND NEW.to_type = 2; + UPDATE member SET shares_white = shares_white + NEW.shares WHERE mgnr = NEW.to_mgnr AND NEW.to_type = 3; + UPDATE member SET shares_dormant = shares_dormant + NEW.shares WHERE mgnr = NEW.to_mgnr AND NEW.to_type = 9; +END; + +CREATE TRIGGER t_member_history_d_member + AFTER DELETE ON member_history FOR EACH ROW + WHEN (SELECT value FROM client_parameter WHERE param = 'ENABLE_MEMBER_HISTORY_TRIGGERS') = 1 +BEGIN + UPDATE member SET shares = shares + OLD.shares WHERE mgnr = OLD.from_mgnr AND OLD.from_type = 1; + UPDATE member SET shares_red = shares_red + OLD.shares WHERE mgnr = OLD.from_mgnr AND OLD.from_type = 2; + UPDATE member SET shares_white = shares_white + OLD.shares WHERE mgnr = OLD.from_mgnr AND OLD.from_type = 3; + UPDATE member SET shares_dormant = shares_dormant + OLD.shares WHERE mgnr = OLD.from_mgnr AND OLD.from_type = 9; + UPDATE member SET shares = shares - OLD.shares WHERE mgnr = OLD.to_mgnr AND OLD.to_type = 1; + UPDATE member SET shares_red = shares_red - OLD.shares WHERE mgnr = OLD.to_mgnr AND OLD.to_type = 2; + UPDATE member SET shares_white = shares_white - OLD.shares WHERE mgnr = OLD.to_mgnr AND OLD.to_type = 3; + UPDATE member SET shares_dormant = shares_dormant - OLD.shares WHERE mgnr = OLD.to_mgnr AND OLD.to_type = 9; +END; + +INSERT INTO client_parameter (param, value) VALUES ('ENABLE_MEMBER_HISTORY_TRIGGERS', '1'); + +CREATE TABLE season_new ( + year INTEGER NOT NULL CHECK (year >= 1000 AND year <= 9999), + currency TEXT NOT NULL, + precision INTEGER NOT NULL DEFAULT 4, + + max_kg_per_ha INTEGER NOT NULL DEFAULT 10000, + vat_normal REAL NOT NULL DEFAULT 0.10, + vat_flatrate REAL NOT NULL DEFAULT 0.13, + + min_kg_per_share INTEGER DEFAULT NULL, + max_kg_per_share INTEGER DEFAULT NULL, + min_kg_per_share_red INTEGER DEFAULT NULL, + max_kg_per_share_red INTEGER DEFAULT NULL, + min_kg_per_share_white INTEGER DEFAULT NULL, + max_kg_per_share_white INTEGER DEFAULT NULL, + penalty_per_kg INTEGER DEFAULT NULL, + penalty_amount INTEGER DEFAULT NULL, + penalty_none INTEGER DEFAULT NULL, + penalty_per_share_amount INTEGER DEFAULT NULL, + penalty_per_share_none INTEGER DEFAULT NULL, + share_value INTEGER, + + start_date TEXT CHECK (start_date REGEXP '^[1-9][0-9]{3}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$'), + end_date TEXT CHECK (end_date REGEXP '^[1-9][0-9]{3}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$'), + calc_mode INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT pk_season PRIMARY KEY (year), + CONSTRAINT fk_season_currency FOREIGN KEY (currency) REFERENCES currency (code) + ON UPDATE CASCADE + ON DELETE RESTRICT +) STRICT; + +INSERT INTO season_new (year, currency, precision, max_kg_per_ha, vat_normal, vat_flatrate, min_kg_per_share, max_kg_per_share, penalty_per_kg, penalty_amount, penalty_none, penalty_per_share_amount, penalty_per_share_none, share_value, start_date, end_date, calc_mode) +SELECT year, currency, precision, max_kg_per_ha, vat_normal, vat_flatrate, min_kg_per_bs, max_kg_per_bs, penalty_per_kg, penalty_amount, penalty_none, penalty_per_bs_amount, penalty_per_bs_none, bs_value, start_date, end_date, calc_mode +FROM season; + +PRAGMA foreign_keys = OFF; +DROP TABLE season; +ALTER TABLE season_new RENAME TO season; +PRAGMA foreign_keys = ON; + +DROP VIEW v_delivery; +CREATE VIEW v_delivery AS +SELECT p.year, p.did, p.dpnr, + d.date, d.time, d.zwstid, d.lnr, d.lsnr, + m.mgnr, m.name, m.given_name, + v.sortid, v.type, a.attrid, p.cultid, + p.weight, p.kmw, ROUND(p.kmw * (4.54 + 0.022 * p.kmw), 0) AS oe, p.qualid, + p.hkid, p.kgnr, p.rdnr, + p.net_weight, p.gebunden, + p.qualid IN (SELECT l.qualid FROM wine_quality_level l WHERE NOT l.predicate AND (p.kmw >= l.min_kmw OR l.min_kmw IS NULL) ORDER BY l.min_kmw DESC LIMIT 1,100) AS abgewertet, + p.qualid NOT IN ('WEI', 'RSW', 'LDW') AS min_quw, + IIF(a.strict, COALESCE(a.fill_lower, 0), 0) AS attribute_prio, + GROUP_CONCAT(o.modid) AS modifiers, + d.comment, p.comment AS part_comment +FROM delivery_part p + JOIN delivery d ON (d.year, d.did) = (p.year, p.did) + JOIN member m ON m.mgnr = d.mgnr + LEFT JOIN wine_variety v ON v.sortid = p.sortid + LEFT JOIN wine_attribute a ON a.attrid = p.attrid + LEFT JOIN delivery_part_modifier o ON (o.year, o.did, o.dpnr) = (p.year, p.did, p.dpnr) +GROUP BY p.year, p.did, p.dpnr +ORDER BY p.year, p.did, p.dpnr, o.modid; + +DROP VIEW v_stat_season; +CREATE VIEW v_stat_season AS +SELECT year, + SUM(weight) AS weight_total, + SUM(IIF(type = 'R', weight, 0)) AS weight_red, + SUM(IIF(type = 'W', weight, 0)) AS weight_white, + ROUND(SUM(kmw * weight) / SUM(weight), 2) AS kmw, + ROUND(SUM(oe * weight) / SUM(weight), 1) AS oe, + COUNT(DISTINCT did) AS lieferungen, + COUNT(DISTINCT mgnr) AS mitglieder +FROM v_delivery +GROUP BY year +ORDER BY year; + +DROP VIEW v_stat_sort; +CREATE VIEW v_stat_variety AS +SELECT year, sortid, + SUM(weight) AS weight_total, + SUM(IIF(type = 'R', weight, 0)) AS weight_red, + SUM(IIF(type = 'W', weight, 0)) AS weight_white, + ROUND(SUM(kmw * weight) / SUM(weight), 2) AS kmw, + ROUND(SUM(oe * weight) / SUM(weight), 1) AS oe, + COUNT(DISTINCT did) AS lieferungen, + COUNT(DISTINCT mgnr) AS mitglieder +FROM v_delivery +GROUP BY year, sortid +ORDER BY year, sortid; + +DROP VIEW v_stat_attr; +CREATE VIEW v_stat_attr AS +SELECT year, attrid, + SUM(weight) AS weight_total, + SUM(IIF(type = 'R', weight, 0)) AS weight_red, + SUM(IIF(type = 'W', weight, 0)) AS weight_white, + ROUND(SUM(kmw * weight) / SUM(weight), 2) AS kmw, + ROUND(SUM(oe * weight) / SUM(weight), 1) AS oe, + COUNT(DISTINCT did) AS lieferungen, + COUNT(DISTINCT mgnr) AS mitglieder +FROM v_delivery +GROUP BY year, attrid +ORDER BY year, attrid; + +DROP VIEW v_stat_sort_attr; +CREATE VIEW v_stat_variety_attr AS +SELECT year, sortid, attrid, + SUM(weight) AS weight_total, + SUM(IIF(type = 'R', weight, 0)) AS weight_red, + SUM(IIF(type = 'W', weight, 0)) AS weight_white, + ROUND(SUM(kmw * weight) / SUM(weight), 2) AS kmw, + ROUND(SUM(oe * weight) / SUM(weight), 1) AS oe, + COUNT(DISTINCT did) AS lieferungen, + COUNT(DISTINCT mgnr) AS mitglieder +FROM v_delivery +GROUP BY year, sortid, attrid +ORDER BY year, sortid, attrid; + +DROP VIEW v_stat_member; +CREATE VIEW v_stat_member AS +SELECT year, mgnr, + SUM(weight) AS weight_total, + SUM(IIF(type = 'R', weight, 0)) AS weight_red, + SUM(IIF(type = 'W', weight, 0)) AS weight_white, + ROUND(SUM(kmw * weight) / SUM(weight), 2) AS kmw, + ROUND(SUM(oe * weight) / SUM(weight), 1) AS oe, + COUNT(DISTINCT did) AS lieferungen +FROM v_delivery +GROUP BY year, mgnr +ORDER BY year, mgnr; + +DROP VIEW v_total_under_delivery; +CREATE VIEW v_total_under_delivery AS +SELECT s.year, m.mgnr, + m.shares, + m.shares * COALESCE(s.min_kg_per_share, 0) AS min_kg, + m.shares * COALESCE(s.max_kg_per_share, 0) AS max_kg, + COALESCE(d.weight_total, 0) AS weight_total, + IIF(COALESCE(d.weight_total, 0) < m.shares * COALESCE(s.min_kg_per_share, 0), + COALESCE(d.weight_total, 0) - m.shares * COALESCE(s.min_kg_per_share, 0), + IIF(COALESCE(d.weight_total, 0) > m.shares * COALESCE(s.max_kg_per_share, 0), + COALESCE(d.weight_total, 0) - m.shares * COALESCE(s.max_kg_per_share, 0), + 0)) AS diff, + m.shares_red, + m.shares_red * COALESCE(s.min_kg_per_share_red, s.min_kg_per_share, 0) AS min_kg_red, + m.shares_red * COALESCE(s.max_kg_per_share_red, s.max_kg_per_share, 0) AS max_kg_red, + COALESCE(d.weight_red, 0) AS weight_red, + IIF(COALESCE(d.weight_red, 0) < m.shares_red * COALESCE(s.min_kg_per_share_red, s.min_kg_per_share, 0), + COALESCE(d.weight_red, 0) - m.shares_red * COALESCE(s.min_kg_per_share_red, s.min_kg_per_share, 0), + IIF(COALESCE(d.weight_red, 0) > m.shares_red * COALESCE(s.max_kg_per_share_red, s.max_kg_per_share, 0), + COALESCE(d.weight_red, 0) - m.shares_red * COALESCE(s.max_kg_per_share_red, s.max_kg_per_share, 0), + 0)) AS diff_red, + m.shares_white, + m.shares_white * COALESCE(s.min_kg_per_share_white, s.min_kg_per_share, 0) AS min_kg_white, + m.shares_white * COALESCE(s.max_kg_per_share_white, s.max_kg_per_share, 0) AS max_kg_white, + COALESCE(d.weight_white, 0) AS weight_white, + IIF(COALESCE(d.weight_white, 0) < m.shares_white * COALESCE(s.min_kg_per_share_white, s.min_kg_per_share, 0), + COALESCE(d.weight_white, 0) - m.shares_white * COALESCE(s.min_kg_per_share_white, s.min_kg_per_share, 0), + IIF(COALESCE(d.weight_white, 0) > m.shares_white * COALESCE(s.max_kg_per_share_white, s.max_kg_per_share, 0), + COALESCE(d.weight_white, 0) - m.shares_white * COALESCE(s.max_kg_per_share_white, s.max_kg_per_share, 0), + 0)) AS diff_white +FROM member m, season s + LEFT JOIN v_stat_member d ON (d.year, d.mgnr) = (s.year, m.mgnr) +ORDER BY s.year, m.mgnr; + +DROP VIEW v_penalty_business_shares; +CREATE VIEW v_penalty_business_shares AS +SELECT u.year, u.mgnr, + SUM(IIF(u.weight_total = 0, COALESCE(-s.penalty_none, 0) + COALESCE(-(u.shares + u.shares_red + u.shares_white) * s.penalty_per_share_none, 0), 0) + + IIF(u.diff < 0 OR u.diff_red < 0 OR u.diff_white < 0, COALESCE(-s.penalty_amount, 0), 0) + + COALESCE((u.diff + u.diff_red + u.diff_white) * s.penalty_per_kg, 0) + + COALESCE(CEIL(CAST(u.diff AS REAL) / s.min_kg_per_share) * s.penalty_per_share_amount, 0) + + COALESCE(CEIL(CAST(u.diff_red AS REAL) / s.min_kg_per_share_red) * s.penalty_per_share_amount, 0) + + COALESCE(CEIL(CAST(u.diff_white AS REAL) / s.min_kg_per_share_white) * s.penalty_per_share_amount, 0) + ) AS total_penalty +FROM v_total_under_delivery u + JOIN season s ON u.year = s.year + JOIN member m ON m.mgnr = u.mgnr +WHERE m.active +GROUP BY u.year, u.mgnr +HAVING total_penalty < 0 +ORDER BY u.year, u.mgnr; + +DROP VIEW v_auto_business_shares; +CREATE VIEW v_auto_business_shares AS +SELECT (SUBSTR(h.date, 1, 4) + 0) AS year, + h.to_mgnr AS mgnr, + SUM(h.shares) AS shares, + SUM(h.shares * COALESCE(h.value_per_share, 0)) AS total_amount +FROM member_history h +WHERE h.reason = 'auto' AND h.source = 'elwig' +GROUP BY year, h.to_mgnr +ORDER BY year, h.to_mgnr; + +PRAGMA writable_schema = OFF; diff --git a/Elwig/Services/MemberService.cs b/Elwig/Services/MemberService.cs index fc195cc..cca978b 100644 --- a/Elwig/Services/MemberService.cs +++ b/Elwig/Services/MemberService.cs @@ -126,7 +126,7 @@ namespace Elwig.Services { vm.EntryDate = (m.EntryDateString != null) ? string.Join(".", m.EntryDateString.Split("-").Reverse()) : null; vm.ExitDate = (m.ExitDateString != null) ? string.Join(".", m.ExitDateString.Split("-").Reverse()) : null; - vm.BusinessShares = m.BusinessShares; + vm.BusinessShares = m.Shares; vm.AccountingNr = m.AccountingNr; vm.Branch = (Branch?)ControlUtils.GetItemFromSourceWithPk(vm.BranchSource, m.ZwstId); vm.DefaultKg = (AT_Kg?)ControlUtils.GetItemFromSourceWithPk(vm.DefaultKgSource, m.DefaultKgNr); @@ -487,6 +487,13 @@ namespace Elwig.Services { .Include(m => m.TelephoneNumbers) .Include(m => m.EmailAddresses) .ToListAsync(); + var history1 = await query.IgnoreAutoIncludes() + .SelectMany(m => m.HistoryFrom) + .ToListAsync(); + var history2 = await query.IgnoreAutoIncludes() + .SelectMany(m => m.HistoryTo) + .ToListAsync(); + var history = history1.Union(history2).DistinctBy(h => h.HistNr).OrderBy(h => h.HistNr).ToList(); var areaComs = await query .SelectMany(m => m.AreaCommitments) .Select(c => c.Contract).Distinct() @@ -499,7 +506,7 @@ namespace Elwig.Services { .Distinct() .OrderBy(k => k.KgNr) .ToList(); - await ElwigData.Export(filename, members, areaComs, wbKgs, filterNames); + await ElwigData.Export(filename, members, history, areaComs, wbKgs, filterNames); }); } } else if (mode == ExportMode.Upload && App.Config.SyncUrl != null) { @@ -542,7 +549,7 @@ namespace Elwig.Services { EntryDateString = string.IsNullOrEmpty(vm.EntryDate) ? null : string.Join("-", vm.EntryDate.Split(".").Reverse()), ExitDateString = string.IsNullOrEmpty(vm.ExitDate) ? null : string.Join("-", vm.ExitDate.Split(".").Reverse()), - BusinessShares = (int)vm.BusinessShares!, + Shares = (int)vm.BusinessShares!, AccountingNr = string.IsNullOrEmpty(vm.AccountingNr) ? null : vm.AccountingNr, IsActive = vm.IsActive, IsVollLieferant = vm.IsVollLieferant, diff --git a/Elwig/Services/SyncService.cs b/Elwig/Services/SyncService.cs index 978e913..98d4471 100644 --- a/Elwig/Services/SyncService.cs +++ b/Elwig/Services/SyncService.cs @@ -27,6 +27,13 @@ namespace Elwig.Services { .Include(m => m.TelephoneNumbers) .Include(m => m.EmailAddresses) .ToListAsync(); + var history1 = await query.IgnoreAutoIncludes() + .SelectMany(m => m.HistoryFrom) + .ToListAsync(); + var history2 = await query.IgnoreAutoIncludes() + .SelectMany(m => m.HistoryTo) + .ToListAsync(); + var history = history1.Union(history2).DistinctBy(h => h.HistNr).OrderBy(h => h.HistNr).ToList(); var areaComs = await query .SelectMany(m => m.AreaCommitments) .Select(c => c.Contract).Distinct() @@ -44,7 +51,7 @@ namespace Elwig.Services { InteractionService.ShowError("Mitglieder hochladen", "Es wurden keine Mitglieder zum Hochladen ausgewählt!"); } else { var exportedAt = DateTime.Now; - await ElwigData.Export(path, members, areaComs, wbKgs, filterNames); + await ElwigData.Export(path, members, history, areaComs, wbKgs, filterNames); await Utils.UploadExportData(path, url, username, password); await UpdateExportedAt(members, areaComs, [], exportedAt); InteractionService.ShowInformation("Mitglieder hochgeladen", $"Hochladen von {members.Count:N0} Mitgliedern erfolgreich!"); diff --git a/Elwig/ViewModels/MemberAdminViewModel.cs b/Elwig/ViewModels/MemberAdminViewModel.cs index bd453b0..4fffc85 100644 --- a/Elwig/ViewModels/MemberAdminViewModel.cs +++ b/Elwig/ViewModels/MemberAdminViewModel.cs @@ -126,7 +126,7 @@ namespace Elwig.ViewModels { [ObservableProperty] private string? _businessSharesString; public int? BusinessShares { - get => int.TryParse(BusinessSharesString, out var bs) ? bs : null; + get => int.TryParse(BusinessSharesString, out var shares) ? shares : null; set => BusinessSharesString = $"{value}"; } [ObservableProperty] diff --git a/Elwig/Windows/BaseDataWindow.xaml b/Elwig/Windows/BaseDataWindow.xaml index 0da1831..057dea6 100644 --- a/Elwig/Windows/BaseDataWindow.xaml +++ b/Elwig/Windows/BaseDataWindow.xaml @@ -453,13 +453,13 @@ HorizontalAlignment="Left"/>