DeliveryConfirmation: Add Statistics table

This commit is contained in:
2023-10-16 22:23:42 +02:00
parent 8c49cc8ef2
commit ff2968c989
8 changed files with 268 additions and 147 deletions

View File

@ -93,7 +93,60 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<!-- TODO add Gesamtlieferung tabelle --> <table class="delivery-confirmation-stats">
<colgroup>
<col style="width: 45mm;"/>
<col style="width: 17mm;"/>
<col style="width: 17mm;"/>
<col style="width: 17mm;"/>
<col style="width: 19mm;"/>
<col style="width: 16mm;"/>
<col style="width: 17mm;"/>
<col style="width: 17mm;"/>
</colgroup>
<thead>
<tr>
<th><b>Lese @Model.Year</b> per @($"{Model.Date:dd.MM.yyyy}") [kg]</th>
<th>Lieferpflicht</th>
<th>Lieferrecht</th>
<th>Unterliefert<br/>(bzgl. Zuget.)</th>
<th>Noch zu liefern<br/>(bzgl. Gelft.)</th>
<th>Überliefert<br/>(bzgl. Gelft.)</th>
<th>Zugeteilt</th>
<th>Geliefert</th>
</tr>
</thead>
<tbody>
@{
string FormatRow(int obligation, int right, int sum, int? payment = null) {
var isGa = payment == null;
payment ??= sum;
return $"<td>{obligation:N0}</td>" +
$"<td>{right:N0}</td>" +
$"<td>{(payment < obligation ? $"<b>{obligation - payment:N0}\x3c/b>" : "-")}</td>" +
$"<td>{(sum >= obligation && sum <= right ? $"{right - sum:N0}" : "-")}</td>" +
$"<td>{(obligation == 0 && right == 0 ? "-" : (sum > right ? ((isGa ? "<b>" : "") + $"{sum - right:N0}" + (isGa ? "</b>" : "")) : "-"))}</td>" +
$"<td>{(obligation == 0 && right == 0 ? "-" : $"{payment:N0}")}</td>" +
$"<td>{sum:N0}</td>";
}
}
<tr>
<th>Gesamtlieferung lt. gez. GA</th>
@Raw(FormatRow(Model.Member.DeliveryObligation, Model.Member.DeliveryRight, Model.Member.Deliveries.Where(d => d.Year == Model.Year).Sum(d => d.Weight)))
</tr>
<tr class="subheading">
<th>Flächenbindungen:</th>
</tr>
@foreach (var (id, (name, right, obligation, sum, payment)) in Model.MemberBins.OrderBy(b => b.Key)) {
if (right > 0 || obligation > 0 || sum > 0) {
<tr>
<th>@name</th>
@Raw(FormatRow(obligation, right, sum, payment))
</tr>
}
}
</tbody>
</table>
<div class="text" style="margin-top: 2em;"> <div class="text" style="margin-top: 2em;">
@if (Model.Text != null) { @if (Model.Text != null) {
<p class="comment" style="white-space: pre-wrap; break-inside: avoid;">@Model.Text</p> <p class="comment" style="white-space: pre-wrap; break-inside: avoid;">@Model.Text</p>

View File

@ -10,6 +10,7 @@ namespace Elwig.Documents {
public int Year; public int Year;
public IEnumerable<DeliveryPart> Deliveries; public IEnumerable<DeliveryPart> Deliveries;
public string? Text = App.Client.TextDeliveryConfirmation; public string? Text = App.Client.TextDeliveryConfirmation;
public Dictionary<string, (string, int, int, int, int)> MemberBins;
public DeliveryConfirmation(AppDbContext ctx, int year, Member m) : public DeliveryConfirmation(AppDbContext ctx, int year, Member m) :
base($"Anlieferungsbestätigung {year} {((IAddress?)m.BillingAddress ?? m).Name}", m) { base($"Anlieferungsbestätigung {year} {((IAddress?)m.BillingAddress ?? m).Name}", m) {
@ -29,6 +30,7 @@ namespace Elwig.Documents {
v.kmw DESC, v.lsnr, v.dpnr v.kmw DESC, v.lsnr, v.dpnr
""") """)
.ToList(); .ToList();
MemberBins = ctx.GetMemberBins(Year, m.MgNr).GetAwaiter().GetResult();
} }
} }
} }

View File

@ -76,8 +76,7 @@
<p class="comment">Amerkung zur Lieferung: @Model.Delivery.Comment</p> <p class="comment">Amerkung zur Lieferung: @Model.Delivery.Comment</p>
} }
@if (Model.DisplayStats > 0) { @if (Model.DisplayStats > 0) {
<div id="delivery-stats"> <table class="delivery-note-stats @(Model.DisplayStats > 2 ? "expanded" : "")">
<table class="delivery-stats @(Model.DisplayStats > 2 ? "expanded" : "")">
<colgroup> <colgroup>
<col style="width: 45mm;"/> <col style="width: 45mm;"/>
<col style="width: 20mm;"/> <col style="width: 20mm;"/>
@ -109,7 +108,7 @@
$"<td>{sum:N0}</td>"; $"<td>{sum:N0}</td>";
} }
var sortids = Model.Delivery.Parts.Select(p => p.SortId).ToList(); var sortids = Model.Delivery.Parts.Select(p => p.SortId).ToList();
var bins = Model.MemberBins.GroupBy(b => b.Item1[..2]).ToDictionary(g => g.Key, g => g.Count()); var bins = Model.MemberBins.GroupBy(b => b.Key[..2]).ToDictionary(g => g.Key, g => g.Count());
} }
<tr> <tr>
<th>Gesamtlieferung lt. gez. GA</th> <th>Gesamtlieferung lt. gez. GA</th>
@ -119,7 +118,7 @@
<tr class="subheading"> <tr class="subheading">
<th>Flächenbindungen:</th> <th>Flächenbindungen:</th>
</tr> </tr>
@foreach (var (id, name, right, obligation, sum) in Model.MemberBins.OrderBy(b => b.Item1)) { @foreach (var (id, (name, right, obligation, sum, _)) in Model.MemberBins.OrderBy(b => b.Key)) {
if (right > 0 || obligation > 0 || (sum > 0 && bins[id[..2]] > 1 && !id.EndsWith('_'))) { if (right > 0 || obligation > 0 || (sum > 0 && bins[id[..2]] > 1 && !id.EndsWith('_'))) {
<tr class="@(sortids.Contains(id[..2]) ? "" : "optional")"> <tr class="@(sortids.Contains(id[..2]) ? "" : "optional")">
<th>@name</th> <th>@name</th>
@ -130,7 +129,6 @@
} }
</tbody> </tbody>
</table> </table>
</div>
} }
</main> </main>
@for (int i = 0; i < 2; i++) { @for (int i = 0; i < 2; i++) {

View File

@ -7,7 +7,7 @@ namespace Elwig.Documents {
public Delivery Delivery; public Delivery Delivery;
public string? Text; public string? Text;
public IEnumerable<(string, string, int, int, int)> MemberBins; public Dictionary<string, (string, int, int, int, int)> MemberBins;
// 0 - none // 0 - none
// 1 - GA only // 1 - GA only
@ -27,7 +27,7 @@ namespace Elwig.Documents {
$"</tbody></table>"; $"</tbody></table>";
Text = App.Client.TextDeliveryNote; Text = App.Client.TextDeliveryNote;
DocumentId = d.LsNr; DocumentId = d.LsNr;
MemberBins = ctx.GetMemberBins(d.Member, d.Year).GetAwaiter().GetResult(); MemberBins = ctx.GetMemberBins(d.Year, d.Member.MgNr).GetAwaiter().GetResult();
} }
} }
} }

View File

@ -1,6 +1,7 @@
table.delivery-confirmation { table.delivery-confirmation {
font-size: 10pt; font-size: 10pt;
margin-bottom: 5mm;
} }
table.delivery-confirmation thead { table.delivery-confirmation thead {
@ -68,3 +69,38 @@ table.delivery-confirmation tr.sum {
table.delivery-confirmation tr.sum td { table.delivery-confirmation tr.sum td {
padding-top: 1mm; padding-top: 1mm;
} }
table.delivery-confirmation-stats {
font-size: 10pt;
break-inside: avoid;
}
table.delivery-confirmation-stats th,
table.delivery-confirmation-stats td {
padding: 0.125mm 0;
}
table.delivery-confirmation-stats tr.subheading th {
text-align: left;
}
table.delivery-confirmation-stats thead th {
font-weight: normal;
font-style: italic;
text-align: right;
font-size: 8pt;
}
table.delivery-confirmation-stats thead th:first-child {
text-align: left;
}
table.delivery-confirmation-stats td {
text-align: right;
}
table.delivery-confirmation-stats tbody th {
font-weight: normal;
font-style: italic;
text-align: left;
}

View File

@ -52,45 +52,45 @@ table.delivery tr.sum td {
padding-top: 1mm; padding-top: 1mm;
} }
table.delivery-stats { table.delivery-note-stats {
font-size: 8pt; font-size: 8pt;
break-inside: avoid; break-inside: avoid;
break-after: avoid; break-after: avoid;
} }
table.delivery-stats th, table.delivery-note-stats th,
table.delivery-stats td { table.delivery-note-stats td {
padding: 0.125mm 0; padding: 0.125mm 0;
} }
table.delivery-stats:not(.expanded) tr.optional { table.delivery-note-stats:not(.expanded) tr.optional {
display: none; display: none;
} }
table.delivery-stats tr.subheading th { table.delivery-note-stats tr.subheading th {
text-align: left; text-align: left;
} }
table.delivery-stats.expanded tr.subheading:not(:has(~ tr)), table.delivery-note-stats.expanded tr.subheading:not(:has(~ tr)),
table.delivery-stats tr.subheading:not(:has(~ tr:not(.optional))) { table.delivery-note-stats tr.subheading:not(:has(~ tr:not(.optional))) {
display: none; display: none;
} }
table.delivery-stats thead th { table.delivery-note-stats thead th {
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
text-align: right; text-align: right;
} }
table.delivery-stats thead th:first-child { table.delivery-note-stats thead th:first-child {
text-align: left; text-align: left;
} }
table.delivery-stats td { table.delivery-note-stats td {
text-align: right; text-align: right;
} }
table.delivery-stats tbody th { table.delivery-note-stats tbody th {
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
text-align: left; text-align: left;

View File

@ -53,6 +53,10 @@ namespace Elwig.Helpers {
public static string ConnectionString => $"Data Source=\"{App.Config.DatabaseFile}\"; Foreign Keys=True; Mode=ReadWrite; Cache=Default"; public static string ConnectionString => $"Data Source=\"{App.Config.DatabaseFile}\"; Foreign Keys=True; Mode=ReadWrite; Cache=Default";
private readonly Dictionary<int, Dictionary<int, Dictionary<string, (int, int)>>> _memberRightsAndObligations = new();
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBins = new();
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberPaymentBins = new();
public AppDbContext() { public AppDbContext() {
if (App.Config.DatabaseLog != null) { if (App.Config.DatabaseLog != null) {
try { try {
@ -206,26 +210,111 @@ namespace Elwig.Helpers {
} }
} }
public async Task<IEnumerable<(string, string, int, int, int)>> GetMemberBins(Member m, int year) { private async Task FetchMemberRightsAndObligations(int year, SqliteConnection? cnx = null) {
using var cnx = await ConnectAsync(); var ownCnx = cnx == null;
var (rights, obligations) = await Billing.Billing.GetMemberRightsObligations(cnx, year, m.MgNr); cnx ??= await ConnectAsync();
var bins = await Billing.Billing.GetMemberBinWeights(m.MgNr, year, cnx); var bins = new Dictionary<int, Dictionary<string, (int, int)>>();
using (var cmd = cnx.CreateCommand()) {
var list = new List<(string, string, int, int, int)>(); cmd.CommandText = $"""
foreach (var id in rights.Keys.Union(obligations.Keys).Union(bins.Keys)) { SELECT mgnr, t.vtrgid,
var s = await WineVarieties.FindAsync(id[..2]); ROUND(SUM(COALESCE(area * min_kg_per_ha, 0)) / 10000.0) AS min_kg,
var attrIds = id[2..]; ROUND(SUM(COALESCE(area * max_kg_per_ha, 0)) / 10000.0) AS max_kg
var a = await WineAttributes.Where(a => attrIds.Contains(a.AttrId)).ToListAsync(); FROM area_commitment c
var name = (s?.Name ?? "") + (a.Count > 0 ? $" ({string.Join(" / ", a.Select(a => a.Name))})" : ""); JOIN area_commitment_type t ON t.vtrgid = c.vtrgid
list.Add(( WHERE (year_from IS NULL OR year_from <= {year}) AND
id, name, (year_to IS NULL OR year_to >= {year})
rights.TryGetValue(id, out var v1) ? v1 : 0, GROUP BY mgnr, t.vtrgid
obligations.TryGetValue(id, out var v2) ? v2 : 0, ORDER BY LENGTH(t.vtrgid) DESC, t.vtrgid
bins.TryGetValue(id, out var v3) ? v3 : 0 """;
)); using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
var vtrgid = reader.GetString(1);
if (!bins.ContainsKey(mgnr)) bins[mgnr] = new();
bins[mgnr][vtrgid] = (reader.GetInt32(3), reader.GetInt32(2));
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberRightsAndObligations[year] = bins;
} }
return list; private async Task FetchMemberDeliveryBins(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var bins = new Dictionary<int, Dictionary<string, int>>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, bin, weight FROM v_delivery_bin WHERE year = {year}";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
var bin = reader.GetString(1);
if (!bins.ContainsKey(mgnr)) bins[mgnr] = new();
bins[mgnr][bin] = reader.GetInt32(2);
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberDeliveryBins[year] = bins;
}
private async Task FetchMemberPaymentBins(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var bins = new Dictionary<int, Dictionary<string, int>>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, bin, weight FROM v_payment_bin WHERE year = {year}";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
var bin = reader.GetString(1);
if (!bins.ContainsKey(mgnr)) bins[mgnr] = new();
bins[mgnr][bin] = reader.GetInt32(2);
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberPaymentBins[year] = bins;
}
public async Task<Dictionary<string, (int, int)>> GetMemberRightsAndObligations(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberRightsAndObligations.ContainsKey(year))
await FetchMemberRightsAndObligations(year, cnx);
return _memberRightsAndObligations[year].GetValueOrDefault(mgnr, new());
}
public async Task<Dictionary<string, int>> GetMemberDeliveryBins(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberDeliveryBins.ContainsKey(year))
await FetchMemberDeliveryBins(year, cnx);
return _memberDeliveryBins[year].GetValueOrDefault(mgnr, new());
}
public async Task<Dictionary<string, int>> GetMemberPaymentBins(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberPaymentBins.ContainsKey(year))
await FetchMemberPaymentBins(year, cnx);
return _memberPaymentBins[year].GetValueOrDefault(mgnr, new());
}
public async Task<Dictionary<string, (string, int, int, int, int)>> GetMemberBins(int year, int mgnr, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var rightsAndObligations = await GetMemberRightsAndObligations(year, mgnr, cnx);
var deliveryBins = await GetMemberDeliveryBins(year, mgnr, cnx);
var paymentBins = await GetMemberPaymentBins(year, mgnr, cnx);
if (ownCnx) await cnx.DisposeAsync();
var bins = new Dictionary<string, (string, int, int, int, int)>();
foreach (var id in rightsAndObligations.Keys.Union(deliveryBins.Keys).Union(paymentBins.Keys)) {
var variety = await WineVarieties.FindAsync(id[..2]);
var attrIds = id[2..];
var attrs = await WineAttributes.Where(a => attrIds.Contains(a.AttrId)).ToListAsync();
var name = (variety?.Name ?? "") + (attrs.Count > 0 ? $" ({string.Join(" / ", attrs.Select(a => a.Name))})" : "");
bins[id] = (
name,
rightsAndObligations.GetValueOrDefault(id).Item1,
rightsAndObligations.GetValueOrDefault(id).Item2,
deliveryBins.GetValueOrDefault(id),
paymentBins.GetValueOrDefault(id)
);
}
return bins;
} }
} }
} }

View File

@ -42,7 +42,7 @@ namespace Elwig.Helpers.Billing {
var attrVals = Context.WineAttributes.ToDictionary(a => a.AttrId, a => a.FillLowerBins); var attrVals = Context.WineAttributes.ToDictionary(a => a.AttrId, a => a.FillLowerBins);
var attrForced = attrVals.Where(a => a.Value == 0).Select(a => a.Key).ToArray(); var attrForced = attrVals.Where(a => a.Value == 0).Select(a => a.Key).ToArray();
using var cnx = await AppDbContext.ConnectAsync(); using var cnx = await AppDbContext.ConnectAsync();
var memberOblRig = await GetMemberRightsObligations(cnx, Year); await Context.GetMemberRightsAndObligations(Year, 0, cnx);
var inserts = new List<(int, int, int, string, int)>(); var inserts = new List<(int, int, int, string, int)>();
var deliveries = new List<(int, int, int, string, int, double, string, string[], string[], bool?)>(); var deliveries = new List<(int, int, int, string, int, double, string, string[], string[], bool?)>();
@ -68,19 +68,15 @@ namespace Elwig.Helpers.Billing {
} }
int lastMgNr = 0; int lastMgNr = 0;
Dictionary<string, int>? rights = null; Dictionary<string, (int, int)>? rightsAndObligations = null;
Dictionary<string, int>? obligations = null;
Dictionary<string, int> used = new(); Dictionary<string, int> used = new();
foreach (var (mgnr, did, dpnr, sortid, weight, kmw, qualid, attributes, modifiers, gebunden) in deliveries) { foreach (var (mgnr, did, dpnr, sortid, weight, kmw, qualid, attributes, modifiers, gebunden) in deliveries) {
if (lastMgNr != mgnr) { if (lastMgNr != mgnr) {
var or = memberOblRig.GetValueOrDefault(mgnr, (new(), new())); rightsAndObligations = await Context.GetMemberRightsAndObligations(Year, mgnr);
rights = or.Item2;
obligations = or.Item1;
used = new(); used = new();
} }
if ((honorGebunden && gebunden == false) || if ((honorGebunden && gebunden == false) ||
obligations == null || rights == null || rightsAndObligations == null || rightsAndObligations.Count == 0 ||
obligations.Count == 0 || rights.Count == 0 ||
qualid == "WEI" || qualid == "RSW" || qualid == "LDW") qualid == "WEI" || qualid == "RSW" || qualid == "LDW")
{ {
// Explizit als ungebunden markiert, // Explizit als ungebunden markiert,
@ -98,7 +94,7 @@ namespace Elwig.Helpers.Billing {
foreach (var p in Utils.Permutate(attributes, attributes.Intersect(attrForced))) { foreach (var p in Utils.Permutate(attributes, attributes.Intersect(attrForced))) {
var c = p.Count(); var c = p.Count();
var key = sortid + string.Join("", p); var key = sortid + string.Join("", p);
if (rights.ContainsKey(key) && obligations.ContainsKey(key)) { if (rightsAndObligations.ContainsKey(key)) {
int i = 4; int i = 4;
if (c == 1) { if (c == 1) {
i = (p.ElementAt(0) == attributes[0]) ? 2 : 3; i = (p.ElementAt(0) == attributes[0]) ? 2 : 3;
@ -106,8 +102,8 @@ namespace Elwig.Helpers.Billing {
i = 1; i = 1;
} }
var u = used.GetValueOrDefault(key, 0); var u = used.GetValueOrDefault(key, 0);
var vr = Math.Max(0, Math.Min(rights[key] - u, w)); var vr = Math.Max(0, Math.Min(rightsAndObligations[key].Item1 - u, w));
var vo = Math.Max(0, Math.Min(obligations[key] - u, w)); var vo = Math.Max(0, Math.Min(rightsAndObligations[key].Item2 - u, w));
var v = (c == 0 || p.Select(a => attrVals[a]).Min() == 2) ? vr : vo; var v = (c == 0 || p.Select(a => attrVals[a]).Min() == 2) ? vr : vo;
used[key] = u + v; used[key] = u + v;
inserts.Add((did, dpnr, i, key[2..], v)); inserts.Add((did, dpnr, i, key[2..], v));
@ -131,58 +127,5 @@ namespace Elwig.Helpers.Billing {
// TODO add second round to avoid under deliveries // TODO add second round to avoid under deliveries
} }
public static async Task<Dictionary<int, (Dictionary<string, int>, Dictionary<string, int>)>> GetMemberRightsObligations(SqliteConnection cnx, int year, int? mgnr = null) {
var members = new Dictionary<int, (Dictionary<string, int>, Dictionary<string, int>)>();
using var cmd = cnx.CreateCommand();
cmd.CommandText = $"""
SELECT mgnr, t.vtrgid,
SUM(COALESCE(area * min_kg_per_ha, 0)) / 10000 AS min_kg,
SUM(COALESCE(area * max_kg_per_ha, 0)) / 10000 AS max_kg
FROM area_commitment c
JOIN area_commitment_type t ON t.vtrgid = c.vtrgid
WHERE ({(mgnr == null ? "NULL" : mgnr)} IS NULL OR mgnr = {(mgnr == null ? "NULL" : mgnr)}) AND
(year_from IS NULL OR year_from <= {year}) AND
(year_to IS NULL OR year_to >= {year})
GROUP BY mgnr, t.vtrgid
ORDER BY LENGTH(t.vtrgid) DESC, t.vtrgid
""";
var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var m = reader.GetInt32(0);
var vtrgid = reader.GetString(1);
if (!members.ContainsKey(m)) members[m] = (new(), new());
members[m].Item1[vtrgid] = reader.GetInt32(2);
members[m].Item2[vtrgid] = reader.GetInt32(3);
}
return members;
}
public static async Task<(Dictionary<string, int>, Dictionary<string, int>)> GetMemberRightsObligations(SqliteConnection cnx, int year, int mgnr) {
var members = await GetMemberRightsObligations(cnx, year, (int?)mgnr);
return members.GetValueOrDefault(mgnr, (new(), new()));
}
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 bin, weight
FROM v_delivery_bin
WHERE (year, mgnr) = ({year}, {mgnr})
""";
var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var bin = reader.GetString(0);
bins[bin] = reader.GetInt32(1);
}
return bins;
}
} }
} }