Billing: Add feature to calculate member/delivery bins

This commit is contained in:
2023-10-11 23:46:38 +02:00
parent badf4ce955
commit 4d950b2597
16 changed files with 351 additions and 131 deletions

View File

@ -206,13 +206,13 @@ namespace Elwig.Helpers {
}
}
public async Task<IEnumerable<(string, string, int, int, int)>> GetMemberBuckets(Member m, int year) {
public async Task<IEnumerable<(string, string, int, int, int)>> GetMemberBins(Member m, int year) {
using var cnx = await ConnectAsync();
var (rights, obligations) = await Billing.Billing.GetMemberRightsObligations(m.MgNr, year, cnx);
var buckets = await Billing.Billing.GetMemberBucketWeights(m.MgNr, year, cnx);
var bins = await Billing.Billing.GetMemberBinWeights(m.MgNr, year, cnx);
var list = new List<(string, string, int, int, int)>();
foreach (var id in rights.Keys.Union(obligations.Keys).Union(buckets.Keys)) {
foreach (var id in rights.Keys.Union(obligations.Keys).Union(bins.Keys)) {
var s = await WineVarieties.FindAsync(id[..2]);
var attrIds = id[2..];
var a = await WineAttributes.Where(a => attrIds.Contains(a.AttrId)).ToListAsync();
@ -221,7 +221,7 @@ namespace Elwig.Helpers {
id, name,
rights.TryGetValue(id, out var v1) ? v1 : 0,
obligations.TryGetValue(id, out var v2) ? v2 : 0,
buckets.TryGetValue(id, out var v3) ? v3 : 0
bins.TryGetValue(id, out var v3) ? v3 : 0
));
}

View File

@ -4,7 +4,7 @@ using System;
namespace Elwig.Helpers {
public static class AppDbUpdater {
public static readonly int RequiredSchemaVersion = 2;
public static readonly int RequiredSchemaVersion = 3;
private static int _versionOffset = 0;
private static readonly Action<SqliteConnection>[] _updaters = new[] {
@ -80,6 +80,99 @@ namespace Elwig.Helpers {
ExecuteNonQuery(cnx, "ALTER TABLE delivery_part ADD COLUMN weighing_reason TEXT CHECK(NOT (manual_weighing = FALSE AND weighing_reason IS NOT NULL))");
}
private static void UpdateDbSchema_2_To_3(SqliteConnection cnx) { }
private static void UpdateDbSchema_2_To_3(SqliteConnection cnx) {
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN bin_1_name TEXT DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN bin_2_name TEXT DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN bin_3_name TEXT DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN bin_4_name TEXT DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN bin_5_name TEXT DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN bin_6_name TEXT DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN bin_7_name TEXT DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN bin_8_name TEXT DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN bin_9_name TEXT DEFAULT NULL");
ExecuteNonQuery(cnx, """
UPDATE season
SET bin_1_name = n.bucket_1_name,
bin_2_name = n.bucket_2_name,
bin_3_name = n.bucket_3_name,
bin_4_name = n.bucket_4_name,
bin_5_name = n.bucket_5_name,
bin_6_name = n.bucket_6_name,
bin_7_name = n.bucket_7_name,
bin_8_name = n.bucket_8_name,
bin_9_name = n.bucket_9_name
FROM (SELECT year, bucket_1_name, bucket_2_name, bucket_3_name, bucket_4_name, bucket_5_name, bucket_6_name, bucket_7_name, bucket_8_name, bucket_9_name
FROM payment_variant GROUP BY year HAVING avnr = MAX(avnr)) AS n
WHERE season.year = n.year
""");
ExecuteNonQuery(cnx, """
CREATE TABLE delivery_part_bin (
year INTEGER NOT NULL,
did INTEGER NOT NULL,
dpnr INTEGER NOT NULL,
bin_1 INTEGER DEFAULT NULL,
bin_2 INTEGER DEFAULT NULL,
bin_3 INTEGER DEFAULT NULL,
bin_4 INTEGER DEFAULT NULL,
bin_5 INTEGER DEFAULT NULL,
bin_6 INTEGER DEFAULT NULL,
bin_7 INTEGER DEFAULT NULL,
bin_8 INTEGER DEFAULT NULL,
bin_9 INTEGER DEFAULT NULL,
CONSTRAINT pk_delivery_part_bin PRIMARY KEY (year, did, dpnr),
CONSTRAINT fk_delivery_part_bin_delivery_part FOREIGN KEY (year, did, dpnr) REFERENCES delivery_part (year, did, dpnr)
ON UPDATE CASCADE
ON DELETE CASCADE
) STRICT;
""");
ExecuteNonQuery(cnx, """
INSERT INTO delivery_part_bin
(year, did, dpnr, bin_1, bin_2, bin_3, bin_4, bin_5, bin_6, bin_7, bin_8, bin_9)
SELECT year, did, dpnr, bucket_1, bucket_2, bucket_3, bucket_4, bucket_5, bucket_6, bucket_7, bucket_8, bucket_9
FROM payment_delivery_part
WHERE COALESCE(bucket_1, bucket_2, bucket_3, bucket_4, bucket_5, bucket_6, bucket_7, bucket_8, bucket_9) IS NOT NULL
ON CONFLICT DO NOTHING;
""");
ExecuteNonQuery(cnx, "ALTER TABLE payment_delivery_part DROP COLUMN bucket_1");
ExecuteNonQuery(cnx, "ALTER TABLE payment_delivery_part DROP COLUMN bucket_2");
ExecuteNonQuery(cnx, "ALTER TABLE payment_delivery_part DROP COLUMN bucket_3");
ExecuteNonQuery(cnx, "ALTER TABLE payment_delivery_part DROP COLUMN bucket_4");
ExecuteNonQuery(cnx, "ALTER TABLE payment_delivery_part DROP COLUMN bucket_5");
ExecuteNonQuery(cnx, "ALTER TABLE payment_delivery_part DROP COLUMN bucket_6");
ExecuteNonQuery(cnx, "ALTER TABLE payment_delivery_part DROP COLUMN bucket_7");
ExecuteNonQuery(cnx, "ALTER TABLE payment_delivery_part DROP COLUMN bucket_8");
ExecuteNonQuery(cnx, "ALTER TABLE payment_delivery_part DROP COLUMN bucket_9");
ExecuteNonQuery(cnx, "ALTER TABLE payment_variant DROP COLUMN bucket_1_name");
ExecuteNonQuery(cnx, "ALTER TABLE payment_variant DROP COLUMN bucket_2_name");
ExecuteNonQuery(cnx, "ALTER TABLE payment_variant DROP COLUMN bucket_3_name");
ExecuteNonQuery(cnx, "ALTER TABLE payment_variant DROP COLUMN bucket_4_name");
ExecuteNonQuery(cnx, "ALTER TABLE payment_variant DROP COLUMN bucket_5_name");
ExecuteNonQuery(cnx, "ALTER TABLE payment_variant DROP COLUMN bucket_6_name");
ExecuteNonQuery(cnx, "ALTER TABLE payment_variant DROP COLUMN bucket_7_name");
ExecuteNonQuery(cnx, "ALTER TABLE payment_variant DROP COLUMN bucket_8_name");
ExecuteNonQuery(cnx, "ALTER TABLE payment_variant DROP COLUMN bucket_9_name");
ExecuteNonQuery(cnx, "DROP VIEW v_delivery");
ExecuteNonQuery(cnx, """
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.family_name, m.given_name,
p.sortid, 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.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,
GROUP_CONCAT(DISTINCT a.attrid) as attributes, GROUP_CONCAT(DISTINCT 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 delivery_part_attribute a ON (a.year, a.did, a.dpnr) = (p.year, p.did, p.dpnr)
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;
""");
}
}
}

View File

@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -6,41 +7,59 @@ using System.Threading.Tasks;
namespace Elwig.Helpers.Billing {
public class Billing {
private readonly int Year;
private readonly int AvNr;
private readonly AppDbContext Context;
private readonly Dictionary<string, string> Attributes;
private readonly Dictionary<string, (decimal?, decimal?)> Modifiers;
private readonly Dictionary<string, (string, string?, string?, string?, int?, int?, decimal?)> AreaComTypes;
protected readonly int Year;
protected readonly AppDbContext Context;
protected readonly Dictionary<string, string> Attributes;
protected readonly Dictionary<string, (decimal?, decimal?)> Modifiers;
protected readonly Dictionary<string, (string, string?, string?, string?, int?, int?, decimal?)> AreaComTypes;
public Billing(int year, int avnr) {
public Billing(int year) {
Year = year;
AvNr = avnr;
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.AttrId1, v.AttrId2, v.Discriminator, v.MinKgPerHa, v.MaxKgPerHa, v.PenaltyAmount));
}
protected async Task DeleteInDb() {
public async Task FinishSeason() {
using var cnx = await AppDbContext.ConnectAsync();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"DELETE FROM payment_delivery_part WHERE (year, avnr) = ({Year}, {AvNr})";
await cmd.ExecuteNonQueryAsync();
}
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"DELETE FROM payment_member WHERE (year, avnr) = ({Year}, {AvNr})";
cmd.CommandText = $"""
UPDATE season
SET (start_date, end_date) = (SELECT MIN(date), MAX(date) FROM delivery WHERE year = {Year}),
bin_1_name = 'gebunden mit zwei Attributen',
bin_2_name = 'gebunden mit (erstem) Attribut',
bin_3_name = 'gebunden mit zweitem Attribut',
bin_4_name = 'gebunden ohne Attribut',
bin_5_name = 'ungebunden'
WHERE year = {Year}
""";
await cmd.ExecuteNonQueryAsync();
}
}
public async Task Calculate() {
await DeleteInDb();
var tasks = new List<Task>();
foreach (var mgnr in Context.Members.Select(m => m.MgNr)) {
tasks.Add(Task.Run(() => CalculateMember(mgnr)));
public async Task CalculateBins() {
var inserts = new List<(int, int, int, int, int, int, int)>();
foreach (var mgnr in Context.Members.Where(m => m.IsActive).OrderBy(m => m.MgNr).Select(m => m.MgNr)) {
inserts.AddRange(await CalculateMemberBins(mgnr));
}
await Task.WhenAll(tasks);
using var cnx = await AppDbContext.ConnectAsync();
using var cmd = cnx.CreateCommand();
cmd.CommandText = $"""
INSERT INTO delivery_part_bin (year, did, dpnr, bin_1, bin_2, bin_3, bin_4, bin_5)
VALUES {string.Join(",\n ", inserts.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, {i.Item4}, {i.Item5}, {i.Item6}, {i.Item7})"))}
ON CONFLICT DO UPDATE
SET bin_1 = excluded.bin_1,
bin_2 = excluded.bin_2,
bin_3 = excluded.bin_3,
bin_4 = excluded.bin_4,
bin_5 = excluded.bin_5,
bin_6 = NULL,
bin_7 = NULL,
bin_8 = NULL,
bin_9 = NULL;
""";
await cmd.ExecuteNonQueryAsync();
}
public static async Task<(Dictionary<string, int>, Dictionary<string, int>)> GetMemberRightsObligations(int mgnr, int year, SqliteConnection cnx) {
@ -69,26 +88,26 @@ namespace Elwig.Helpers.Billing {
return (rights, obligations);
}
public static async Task<Dictionary<string, int>> GetMemberBucketWeights(int mgnr, int year, SqliteConnection cnx) {
var buckets = new Dictionary<string, int>();
public static async Task<Dictionary<string, int>> GetMemberBinWeights(int mgnr, int year, SqliteConnection cnx) {
var bins = new Dictionary<string, int>();
using var cmd = cnx.CreateCommand();
cmd.CommandText = $"""
SELECT bucket, weight
FROM v_bucket
SELECT bin, weight
FROM v_bin
WHERE (year, mgnr) = ({year}, {mgnr})
""";
var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var bucket = reader.GetString(0);
buckets[bucket] = reader.GetInt32(1);
var bin = reader.GetString(0);
bins[bin] = reader.GetInt32(1);
}
return buckets;
return bins;
}
protected async Task CalculateMember(int mgnr) {
protected async Task<List<(int, int, int, int, int, int, int)>> CalculateMemberBins(int mgnr) {
using var cnx = await AppDbContext.ConnectAsync();
var (rights, obligations) = await GetMemberRightsObligations(mgnr, Year, cnx);
@ -98,33 +117,51 @@ namespace Elwig.Helpers.Billing {
SELECT did, dpnr, sortid, weight, kmw, qualid, attributes, modifiers
FROM v_delivery
WHERE (year, mgnr) = ({Year}, {mgnr})
ORDER BY kmw DESC, weight DESC, did, dpnr
ORDER BY sortid, abgewertet ASC, LENGTH(attributes) DESC, COALESCE(attributes, '~'), kmw DESC, lsnr, dpnr
""";
var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
deliveries.Add((
reader.GetInt32(0), reader.GetInt32(1), reader.GetString(2), reader.GetInt32(3),
reader.GetDouble(4), reader.GetString(5), reader.GetString(6).Split(","), reader.GetString(7).Split(",")
reader.GetDouble(4), reader.GetString(5),
reader.IsDBNull(6) ? Array.Empty<string>() : reader.GetString(6).Split(",").Order().ToArray(),
reader.IsDBNull(7) ? Array.Empty<string>() : reader.GetString(7).Split(",").Order().ToArray()
));
}
}
List<(int, int, int, int, int, int, int)> inserts = new();
foreach (var (did, dpnr, sortid, weight, kmw, qualid, attributes, modifiers) in deliveries) {
if (qualid == "WEI" || qualid == "RSW" || qualid == "LDW") {
// Nicht mindestens Qualitätswein (QUW)
using var cmd = cnx.CreateCommand();
// TODO
cmd.CommandText = $"""
INSERT INTO payment_delivery_part (year, did, dpnr, avnr, mod_abs, mod_rel, )
VALUES ({Year}, {did}, {dpnr}, {AvNr}, )
""";
await cmd.ExecuteNonQueryAsync();
// Nicht mindestens Qualitätswein (QUW) -> ungebunden
inserts.Add((did, dpnr, 0, 0, 0, 0, weight));
continue;
}
// TODO
}
if (attributes.Length > 2)
throw new NotSupportedException();
int w = weight;
int[] b = new int[4];
foreach (var p in Utils.Permutate(attributes)) {
var c = p.Count();
var key = sortid + string.Join("", p);
if (rights.ContainsKey(key)) {
int i = 0;
if (c == 1) {
i = (p.ElementAt(0) == attributes[0]) ? 1 : 2;
} else if (c == 0) {
i = b.Length - 1;
}
var v = Math.Max(0, Math.Min(rights[key], w));
b[i] += v;
rights[key] -= v;
w -= v;
}
}
inserts.Add((did, dpnr, b[0], b[1], b[2], b[3], weight - b[0] - b[1] - b[2] - b[3]));
}
return inserts;
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Elwig.Helpers.Billing {
public class BillingVariant : Billing {
private readonly int AvNr;
public BillingVariant(int year, int avnr) : base(year) {
AvNr = avnr;
}
protected async Task DeleteInDb() {
using var cnx = await AppDbContext.ConnectAsync();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"DELETE FROM payment_delivery_part WHERE (year, avnr) = ({Year}, {AvNr})";
await cmd.ExecuteNonQueryAsync();
}
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"DELETE FROM payment_member WHERE (year, avnr) = ({Year}, {AvNr})";
await cmd.ExecuteNonQueryAsync();
}
}
public async Task CalculatePrices() {
await DeleteInDb();
var tasks = new List<Task>();
foreach (var mgnr in Context.Members.Select(m => m.MgNr)) {
tasks.Add(Task.Run(() => CalculateMemberPrices(mgnr)));
}
await Task.WhenAll(tasks);
}
protected async Task CalculateMemberPrices(int mgnr) {
}
}
}

View File

@ -326,5 +326,19 @@ namespace Elwig.Helpers {
return (familyName, fullName.Replace(familyName, "").Replace(" ", " ").Trim());
}
}
public static IEnumerable<IEnumerable<T>> Permutate<T>(IEnumerable<T> input) {
List<IEnumerable<T>> output = new();
for (int i = 0; i < Math.Pow(2, input.Count()); i++) {
List<T> t = new();
for (int j = 0; j < 30; j++) {
if ((i & (1 << j)) != 0) {
t.Add(input.ElementAt(j));
}
}
output.Add(t);
}
return output.OrderByDescending(l => l.Count());
}
}
}