using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Elwig.Helpers.Billing { public class Billing { protected readonly int Year; protected readonly AppDbContext Context; protected readonly Dictionary Attributes; protected readonly Dictionary Modifiers; protected readonly Dictionary AreaComTypes; public Billing(int year) { Year = year; Context = new AppDbContext(); Attributes = Context.WineAttributes.ToDictionary(a => a.AttrId, a => a.Name); Modifiers = Context.Modifiers.Where(m => m.Year == Year).ToDictionary(m => m.ModId, m => (m.Abs, m.Rel)); AreaComTypes = Context.AreaCommitmentTypes.ToDictionary(v => v.VtrgId, v => (v.SortId, v.AttrId, v.Discriminator, v.MinKgPerHa, v.PenaltyAmount)); } public async Task FinishSeason() { using var cnx = await AppDbContext.ConnectAsync(); await AppDbContext.ExecuteBatch(cnx, $""" UPDATE season SET (start_date, end_date) = (SELECT MIN(date), MAX(date) FROM delivery WHERE year = {Year}) WHERE year = {Year}; DELETE FROM delivery_part_bucket WHERE year = {Year}; """); } public async Task AutoAdjustBusinessShare() { using var cnx = await AppDbContext.ConnectAsync(); await AppDbContext.ExecuteBatch(cnx, $""" INSERT INTO member_history (mgnr, date, business_shares, type) SELECT u.mgnr, '{Utils.Today:yyyy-MM-dd}', u.diff / s.max_kg_per_bs AS bs, 'auto' FROM v_total_under_delivery u JOIN season s ON s.year = u.year WHERE s.year = {Year} AND bs > 0 ON CONFLICT DO NOTHING """); } public async Task CalculateBuckets(bool allowAttrsIntoLower, bool avoidUnderDeliveries, bool honorGebunden) { var attrVals = Context.WineAttributes.ToDictionary(a => a.AttrId, a => (a.IsStrict, a.FillLower)); var attrForced = attrVals.Where(a => a.Value.IsStrict && a.Value.FillLower == 0).Select(a => a.Key).ToArray(); using var cnx = await AppDbContext.ConnectAsync(); await Context.GetMemberAreaCommitmentBuckets(Year, 0, cnx); var inserts = new List<(int, int, int, string, int)>(); var deliveries = new List<(int, int, int, string, int, double, string, string?, string[], bool?)>(); using (var cmd = cnx.CreateCommand()) { cmd.CommandText = $""" SELECT mgnr, did, dpnr, sortid, weight, kmw, qualid, attrid, modifiers, gebunden FROM v_delivery WHERE year = {Year} ORDER BY mgnr, sortid, abgewertet ASC, {(honorGebunden ? "gebunden IS NOT NULL DESC, gebunden DESC," : "")} attribute_prio DESC, COALESCE(attrid, '~'), kmw DESC, lsnr, dpnr """; using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { deliveries.Add(( reader.GetInt32(0), reader.GetInt32(1), reader.GetInt32(2), reader.GetString(3), reader.GetInt32(4), reader.GetDouble(5), reader.GetString(6), reader.IsDBNull(7) ? null : reader.GetString(7), reader.IsDBNull(8) ? Array.Empty() : reader.GetString(8).Split(",").Order().ToArray(), reader.IsDBNull(9) ? null : reader.GetBoolean(9) )); } } int lastMgNr = 0; Dictionary? rightsAndObligations = null; Dictionary used = new(); foreach (var (mgnr, did, dpnr, sortid, weight, kmw, qualid, attrid, modifiers, gebunden) in deliveries) { if (lastMgNr != mgnr) { rightsAndObligations = await Context.GetMemberAreaCommitmentBuckets(Year, mgnr); used = new(); } if ((honorGebunden && gebunden == false) || rightsAndObligations == null || rightsAndObligations.Count == 0 || qualid == "WEI" || qualid == "RSW" || qualid == "LDW") { // Explizit als ungebunden markiert, // Mitglied hat keine Flächenbindungen, oder // Nicht mindestens Qualitätswein (QUW) // -> ungebunden inserts.Add((did, dpnr, 0, "_", weight)); continue; } int w = weight; var attributes = attrid == null ? Array.Empty() : new string[] { attrid }; var isStrict = attrid != null && attrVals[attrid].IsStrict; foreach (var p in Utils.Permutate(attributes, attributes.Intersect(attrForced))) { var c = p.Count(); var key = sortid + string.Join("", p); if (rightsAndObligations.ContainsKey(key)) { int i = (c == 0) ? 1 : 2; var u = used.GetValueOrDefault(key, 0); var vr = Math.Max(0, Math.Min(rightsAndObligations[key].Right - u, w)); var vo = Math.Max(0, Math.Min(rightsAndObligations[key].Obligation - u, w)); var v = (attributes.Length == c || attributes.Select(a => !attrVals[a].IsStrict ? 2 : attrVals[a].FillLower).Min() == 2) ? vr : vo; used[key] = u + v; if (key.Length > 2 && !isStrict) used[key[..2]] = used.GetValueOrDefault(key[..2], 0) + v; inserts.Add((did, dpnr, i, key[2..], v)); w -= v; } if (w == 0 || (!allowAttrsIntoLower && isStrict)) break; } inserts.Add((did, dpnr, 0, "_", w)); lastMgNr = mgnr; } await AppDbContext.ExecuteBatch(cnx, $""" INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value) VALUES {string.Join(",\n ", inserts.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '{i.Item4}', {i.Item5})"))} ON CONFLICT DO UPDATE SET discr = excluded.discr, value = value + excluded.value; """); if (!avoidUnderDeliveries) return; // FIXME avoidUnderDelivery-calculations not always right! var underDeliveries = new Dictionary<(int, string), int>(); using (var cmd = cnx.CreateCommand()) { cmd.CommandText = $""" SELECT c.mgnr, c.bucket, COALESCE(p.weight, 0) - c.min_kg AS diff FROM v_area_commitment_bucket c LEFT JOIN v_payment_bucket p ON (p.year, p.mgnr, p.bucket) = (c.year, c.mgnr, c.bucket) WHERE c.year = {Year} AND LENGTH(c.bucket) = 2 AND diff < 0 """; using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { underDeliveries[(reader.GetInt32(0), reader.GetString(1))] = reader.GetInt32(2); } } var fittingBuckets = new Dictionary<(int, string), int>(); using (var cmd = cnx.CreateCommand()) { cmd.CommandText = $""" SELECT c.mgnr, c.bucket, COALESCE(p.weight, 0) - c.min_kg AS diff FROM v_area_commitment_bucket c LEFT JOIN v_payment_bucket p ON (p.year, p.mgnr, p.bucket) = (c.year, c.mgnr, c.bucket) WHERE c.year = {Year} AND LENGTH(c.bucket) = 3 AND diff > 0 """; using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { fittingBuckets[(reader.GetInt32(0), reader.GetString(1))] = reader.GetInt32(2); } } foreach (var item in fittingBuckets) { var mgnr = item.Key.Item1; var id = item.Key.Item2[..2]; var attr = item.Key.Item2[2..]; var available = item.Value; var needed = -underDeliveries.GetValueOrDefault((mgnr, id), 0); if (needed <= 0) continue; var fittingDeliveries = new List<(int, int, int, int)>(); using (var cmd = cnx.CreateCommand()) { cmd.CommandText = $""" SELECT d.did, d.dpnr, d.weight, b.value FROM v_delivery d JOIN delivery_part_bucket b ON (b.year, b.did, b.dpnr) = (d.year, d.did, d.dpnr) AND b.discr = '{attr}' WHERE d.year = {Year} AND mgnr = {mgnr} AND sortid = '{id}' AND attrid = '{attr}' ORDER BY kmw ASC, lsnr DESC, d.dpnr DESC """; using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { fittingDeliveries.Add(( reader.GetInt32(0), reader.GetInt32(1), reader.GetInt32(2), reader.GetInt32(3) )); } } var negChanges = new List<(int, int, int, int)>(); var posChanges = new List<(int, int, int, int)>(); foreach (var (did, dpnr, _, w) in fittingDeliveries) { int v = Math.Min(needed, w); needed -= v; posChanges.Add((did, dpnr, 1, v)); negChanges.Add((did, dpnr, 2, w - v)); if (needed == 0) break; } await AppDbContext.ExecuteBatch(cnx, $""" INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value) VALUES {string.Join(",\n ", posChanges.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '', {i.Item4})"))} ON CONFLICT DO UPDATE SET value = value + excluded.value; INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value) VALUES {string.Join(",\n ", negChanges.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '', {i.Item4})"))} ON CONFLICT DO UPDATE SET value = excluded.value; """); } } } }