diff --git a/Elwig/Documents/CreditNote.cs b/Elwig/Documents/CreditNote.cs index 31160bd..544d2e0 100644 --- a/Elwig/Documents/CreditNote.cs +++ b/Elwig/Documents/CreditNote.cs @@ -1,6 +1,7 @@ using Elwig.Helpers; using Elwig.Models.Dtos; using Elwig.Models.Entities; +using System.Collections.Generic; using System.Linq; namespace Elwig.Documents { @@ -13,8 +14,9 @@ namespace Elwig.Documents { public string CurrencySymbol; public int Precision; public string MemberModifier; + public IEnumerable<(string Name, int Kg, decimal Amount)>? MemberUnderDeliveries; - public CreditNote(AppDbContext ctx, PaymentMember p, CreditNoteData data) : + public CreditNote(AppDbContext ctx, PaymentMember p, CreditNoteData data, Dictionary<string, UnderDelivery>? underDeliveries = null) : base($"Traubengutschrift {(p.Credit != null ? $"Nr. {p.Credit.Year}/{p.Credit.TgNr:000}" : p.Member.Name)} – {p.Variant.Name}", p.Member) { UseBillingAddress = true; ShowDateAndLocation = true; @@ -38,5 +40,19 @@ namespace Elwig.Documents { DocumentId = $"Tr.-Gutschr. " + (p.Credit != null ? $"{p.Credit.Year}/{p.Credit.TgNr:000}" : p.MgNr); CurrencySymbol = season.Currency.Symbol ?? season.Currency.Code; Precision = season.Precision; + + var variants = ctx.WineVarieties.ToDictionary(v => v.SortId, v => v); + var attributes = ctx.WineAttributes.ToDictionary(a => a.AttrId, a => a); + var comTypes = ctx.AreaCommitmentTypes.ToDictionary(t => t.VtrgId, t => t); + MemberUnderDeliveries = underDeliveries? + .OrderBy(u => u.Key) + .Select(u => ( + variants[u.Key[..2]].Name + (u.Key.Length > 2 ? " " + attributes[u.Key[2..]].Name : ""), + u.Value.Diff, + u.Value.Diff * (comTypes[u.Key].PenaltyPerKg ?? 0) + - (comTypes[u.Key].PenaltyAmount ?? 0) + - ((u.Value.Weight == 0 ? comTypes[u.Key].PenaltyNone : null) ?? 0))) + .Where(u => u.Item3 != 0) + .ToList(); } }} diff --git a/Elwig/Documents/CreditNote.cshtml b/Elwig/Documents/CreditNote.cshtml index 4e78032..3d98f80 100644 --- a/Elwig/Documents/CreditNote.cshtml +++ b/Elwig/Documents/CreditNote.cshtml @@ -79,12 +79,12 @@ } </tbody> @{ - string FormatRow(string name, decimal? value, bool add = false, bool halfLine = true, bool bold = false) { + string FormatRow(string name, decimal? value, bool add = false, bool halfLine = true, bool bold = false, bool subCat = false) { return $"<tr class=\"{(halfLine || add ? "" : "sum")} {(bold ? "large bold" : "")}\">" + $"<td colspan=\"4\"></td>" - + $"<td class=\"{(halfLine && !add ? "sum" : "")}\" colspan=\"4\">{name}:</td>" - + $"<td class=\"number large {(halfLine && !add ? "sum" : "")}\">{(value < 0 ? "–" : (add ? "+" : ""))}</td>" - + $"<td colspan=\"2\" class=\"number large {(halfLine && !add ? "sum" : "")}\">" + + $"<td class=\"{(halfLine && !add ? "sum" : "")} {(subCat ? "small" : "")}\" colspan=\"4\" style=\"overflow: visible;\">{name}:</td>" + + $"<td class=\"number {(subCat ? "small" : "large")} {(halfLine && !add ? "sum" : "")}\">{(value < 0 ? "–" : (add ? "+" : ""))}</td>" + + $"<td colspan=\"2\" class=\"number {(subCat ? "small" : "large")} {(halfLine && !add ? "sum" : "")}\">" + $"<span class=\"fleft\">{Model.CurrencySymbol}</span>{Math.Abs(value ?? 0):N2}</td>" + $"</tr>\n"; } @@ -116,5 +116,33 @@ } } </tbody> + <tbody style="break-inside: avoid;"> + @{ decimal penalty = 0; } + + @if (Model.MemberUnderDeliveries != null && Model.MemberUnderDeliveries.Count() > 0) { + <tr class="small"> + <td colspan="4"></td> + <td colspan="5" style="padding-top: 5mm;">Anfallende Pönalen durch Unterlieferungen:</td> + <td colspan="2"></td> + </tr> + foreach (var u in Model.MemberUnderDeliveries) { + @Raw(FormatRow($"{u.Name} ({u.Kg:N0} kg)", u.Amount, add: true, subCat: true)) + penalty += u.Amount; + } + penalty = Math.Round(penalty, 2, MidpointRounding.AwayFromZero); + } + + @if (Model.Credit == null) { + @Raw(FormatRow("Auszahlungsbetrag", (Model.Payment?.Amount + penalty) ?? (sum + penalty), bold: true)) + } else { + if (Model.Credit.Modifiers - penalty != 0) { + @Raw(FormatRow("Weitere Abzüge", Model.Credit.Modifiers - penalty, add: true)) + } + if (Model.Credit.PrevModifiers != null && Model.Credit.PrevModifiers != 0) { + @Raw(FormatRow("Bereits berücksichtigte Abzüge", -Model.Credit.PrevModifiers, add: true)) + } + @Raw(FormatRow("Auszahlungsbetrag", Model.Credit.Amount, bold: true)) + } + </tbody> </table> </main> diff --git a/Elwig/Helpers/AppDbContext.cs b/Elwig/Helpers/AppDbContext.cs index 87b2a95..06ed0b7 100644 --- a/Elwig/Helpers/AppDbContext.cs +++ b/Elwig/Helpers/AppDbContext.cs @@ -14,6 +14,7 @@ using Elwig.Models.Dtos; namespace Elwig.Helpers { public record struct AreaComBucket(int Area, int Obligation, int Right); + public record struct UnderDelivery(int Weight, int Diff); public record struct MemberBucket(string Name, int Area, int Obligation, int Right, int Delivery, int DeliveryStrict, int Payment); public class AppDbContext : DbContext { @@ -64,10 +65,11 @@ namespace Elwig.Helpers { public static string ConnectionString => $"Data Source=\"{App.Config.DatabaseFile}\"; Foreign Keys=True; Mode=ReadWrite; Cache=Default"; - private readonly Dictionary<int, Dictionary<int, Dictionary<string, AreaComBucket>>> _memberAreaCommitmentBuckets = new(); - private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBuckets = new(); - private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBucketsStrict = new(); - private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberPaymentBuckets = new(); + private readonly Dictionary<int, Dictionary<int, Dictionary<string, AreaComBucket>>> _memberAreaCommitmentBuckets = []; + private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBuckets = []; + private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBucketsStrict = []; + private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberPaymentBuckets = []; + private readonly Dictionary<int, Dictionary<int, Dictionary<string, UnderDelivery>>> _memberUnderDelivery = []; public AppDbContext() { if (App.Config.DatabaseLog != null) { @@ -217,7 +219,7 @@ namespace Elwig.Helpers { while (await reader.ReadAsync()) { var mgnr = reader.GetInt32(0); var vtrgid = reader.GetString(1); - if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = new(); + if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = []; buckets[mgnr][vtrgid] = new(reader.GetInt32(2), reader.GetInt32(3), reader.GetInt32(4)); } } @@ -235,7 +237,7 @@ namespace Elwig.Helpers { while (await reader.ReadAsync()) { var mgnr = reader.GetInt32(0); var bucket = reader.GetString(1); - if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = new(); + if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = []; buckets[mgnr][bucket] = reader.GetInt32(2); } } @@ -253,7 +255,7 @@ namespace Elwig.Helpers { while (await reader.ReadAsync()) { var mgnr = reader.GetInt32(0); var bucket = reader.GetString(1); - if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = new(); + if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = []; buckets[mgnr][bucket] = reader.GetInt32(2); } } @@ -271,7 +273,7 @@ namespace Elwig.Helpers { while (await reader.ReadAsync()) { var mgnr = reader.GetInt32(0); var bucket = reader.GetString(1); - if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = new(); + if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = []; buckets[mgnr][bucket] = reader.GetInt32(2); } } @@ -279,28 +281,52 @@ namespace Elwig.Helpers { _memberPaymentBuckets[year] = buckets; } + private async Task FetchMemberUnderDelivery(int year, SqliteConnection? cnx = null) { + var ownCnx = cnx == null; + cnx ??= await ConnectAsync(); + var buckets = new Dictionary<int, Dictionary<string, UnderDelivery>>(); + using (var cmd = cnx.CreateCommand()) { + cmd.CommandText = $"SELECT mgnr, bucket, weight, diff FROM v_under_delivery WHERE year = {year}"; + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) { + var mgnr = reader.GetInt32(0); + var bucket = reader.GetString(1); + if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = []; + buckets[mgnr][bucket] = new(reader.GetInt32(2), reader.GetInt32(3)); + } + } + if (ownCnx) await cnx.DisposeAsync(); + _memberUnderDelivery[year] = buckets; + } + public async Task<Dictionary<string, AreaComBucket>> GetMemberAreaCommitmentBuckets(int year, int mgnr, SqliteConnection? cnx = null) { if (!_memberAreaCommitmentBuckets.ContainsKey(year)) await FetchMemberAreaCommitmentBuckets(year, cnx); - return _memberAreaCommitmentBuckets[year].GetValueOrDefault(mgnr, new()); + return _memberAreaCommitmentBuckets[year].GetValueOrDefault(mgnr, []); } public async Task<Dictionary<string, int>> GetMemberDeliveryBuckets(int year, int mgnr, SqliteConnection? cnx = null) { if (!_memberDeliveryBuckets.ContainsKey(year)) await FetchMemberDeliveryBuckets(year, cnx); - return _memberDeliveryBuckets[year].GetValueOrDefault(mgnr, new()); + return _memberDeliveryBuckets[year].GetValueOrDefault(mgnr, []); } public async Task<Dictionary<string, int>> GetMemberDeliveryBucketsStrict(int year, int mgnr, SqliteConnection? cnx = null) { if (!_memberDeliveryBucketsStrict.ContainsKey(year)) await FetchMemberDeliveryBucketsStrict(year, cnx); - return _memberDeliveryBucketsStrict[year].GetValueOrDefault(mgnr, new()); + return _memberDeliveryBucketsStrict[year].GetValueOrDefault(mgnr, []); } public async Task<Dictionary<string, int>> GetMemberPaymentBuckets(int year, int mgnr, SqliteConnection? cnx = null) { if (!_memberPaymentBuckets.ContainsKey(year)) await FetchMemberPaymentBuckets(year, cnx); - return _memberPaymentBuckets[year].GetValueOrDefault(mgnr, new()); + return _memberPaymentBuckets[year].GetValueOrDefault(mgnr, []); + } + + public async Task<Dictionary<string, UnderDelivery>> GetMemberUnderDelivery(int year, int mgnr, SqliteConnection? cnx = null) { + if (!_memberUnderDelivery.ContainsKey(year)) + await FetchMemberUnderDelivery(year, cnx); + return _memberUnderDelivery[year].GetValueOrDefault(mgnr, []); } public async Task<Dictionary<string, MemberBucket>> GetMemberBuckets(int year, int mgnr, SqliteConnection? cnx = null) { diff --git a/Elwig/Helpers/Billing/BillingVariant.cs b/Elwig/Helpers/Billing/BillingVariant.cs index 7f86782..79a1797 100644 --- a/Elwig/Helpers/Billing/BillingVariant.cs +++ b/Elwig/Helpers/Billing/BillingVariant.cs @@ -23,18 +23,17 @@ namespace Elwig.Helpers.Billing { await Revert(); using var cnx = await AppDbContext.ConnectAsync(); using (var cmd = cnx.CreateCommand()) { - // TODO modifiers and prev_modifiers cmd.CommandText = $""" INSERT INTO credit (year, tgnr, mgnr, avnr, net_amount, prev_net_amount, vat, modifiers, prev_modifiers) SELECT s.year, COALESCE(t.tgnr, 0) + ROW_NUMBER() OVER(ORDER BY m.mgnr) AS tgnr, m.mgnr, v.avnr, - ROUND(p.amount / POW(10, s.precision - 2)), + ROUND(p.amount / POW(10, s.precision - 2)) AS net_amount, ROUND(lp.amount / POW(10, s.precision - 2)) AS prev_amount, IIF(m.buchführend, s.vat_normal, s.vat_flatrate) AS vat, - NULL, - NULL + ROUND(COALESCE(u.total_penalty, 0) / POW(10, 4 - 2)) AS modifiers, + lc.modifiers AS prev_modifiers FROM season s JOIN payment_variant v ON v.year = s.year LEFT JOIN payment_variant l ON l.year = s.year @@ -48,6 +47,14 @@ namespace Elwig.Helpers.Billing { JOIN member m ON m.mgnr = d.mgnr LEFT JOIN payment_member lp ON (lp.year, lp.avnr, lp.mgnr) = (l.year, l.avnr, m.mgnr) LEFT JOIN payment_member p ON (p.year, p.avnr, p.mgnr) = (v.year, v.avnr, m.mgnr) + LEFT JOIN credit lc ON (lc.year, lc.avnr, lc.mgnr) = (l.year, l.avnr, m.mgnr) + LEFT JOIN (SELECT year, mgnr, + SUM(COALESCE(IIF(u.weight = 0, -t.penalty_none, 0), 0) + + COALESCE(IIF(u.diff < 0, -t.penalty_amount, 0), 0) + + COALESCE(u.diff * t.penalty_per_kg, 0)) AS total_penalty + FROM v_under_delivery u + JOIN area_commitment_type t ON t.vtrgid = u.bucket + GROUP BY year, mgnr) u ON (u.year, u.mgnr) = (s.year, m.mgnr) WHERE s.year = {Year} AND v.avnr = {AvNr} """; await cmd.ExecuteNonQueryAsync(); diff --git a/Elwig/Windows/PaymentVariantsWindow.xaml.cs b/Elwig/Windows/PaymentVariantsWindow.xaml.cs index 8b3d9bf..c3cf985 100644 --- a/Elwig/Windows/PaymentVariantsWindow.xaml.cs +++ b/Elwig/Windows/PaymentVariantsWindow.xaml.cs @@ -140,8 +140,9 @@ namespace Elwig.Windows { IEnumerable<Member> list = await members.ToListAsync(); var data = await CreditNoteData.ForPaymentVariant(Context.CreditNoteRows, Context.Seasons, v.Year, v.AvNr); var payments = await Context.MemberPayments.Where(p => p.Year == v.Year && p.AvNr == v.AvNr).ToDictionaryAsync(c => c.MgNr); + await Context.GetMemberAreaCommitmentBuckets(Year, 0); using var doc = Document.Merge(list.Select(m => - new CreditNote(Context, payments[m.MgNr], data[m.MgNr]) + new CreditNote(Context, payments[m.MgNr], data[m.MgNr], Context.GetMemberUnderDelivery(Year, m.MgNr).GetAwaiter().GetResult()) )); await doc.Generate(new Progress<double>(v => { ProgressBar.Value = v;