CreditNote: Display under deliveries of area commitments

This commit is contained in:
2023-12-23 21:27:26 +01:00
parent 4f07d9b129
commit 8e71e82efc
5 changed files with 100 additions and 22 deletions

View File

@ -1,6 +1,7 @@
using Elwig.Helpers; using Elwig.Helpers;
using Elwig.Models.Dtos; using Elwig.Models.Dtos;
using Elwig.Models.Entities; using Elwig.Models.Entities;
using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Elwig.Documents { namespace Elwig.Documents {
@ -13,8 +14,9 @@ namespace Elwig.Documents {
public string CurrencySymbol; public string CurrencySymbol;
public int Precision; public int Precision;
public string MemberModifier; 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) { base($"Traubengutschrift {(p.Credit != null ? $"Nr. {p.Credit.Year}/{p.Credit.TgNr:000}" : p.Member.Name)} {p.Variant.Name}", p.Member) {
UseBillingAddress = true; UseBillingAddress = true;
ShowDateAndLocation = 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); DocumentId = $"Tr.-Gutschr. " + (p.Credit != null ? $"{p.Credit.Year}/{p.Credit.TgNr:000}" : p.MgNr);
CurrencySymbol = season.Currency.Symbol ?? season.Currency.Code; CurrencySymbol = season.Currency.Symbol ?? season.Currency.Code;
Precision = season.Precision; 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();
} }
}} }}

View File

@ -79,12 +79,12 @@
} }
</tbody> </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" : "")}\">" return $"<tr class=\"{(halfLine || add ? "" : "sum")} {(bold ? "large bold" : "")}\">"
+ $"<td colspan=\"4\"></td>" + $"<td colspan=\"4\"></td>"
+ $"<td class=\"{(halfLine && !add ? "sum" : "")}\" colspan=\"4\">{name}:</td>" + $"<td class=\"{(halfLine && !add ? "sum" : "")} {(subCat ? "small" : "")}\" colspan=\"4\" style=\"overflow: visible;\">{name}:</td>"
+ $"<td class=\"number large {(halfLine && !add ? "sum" : "")}\">{(value < 0 ? "" : (add ? "+" : ""))}</td>" + $"<td class=\"number {(subCat ? "small" : "large")} {(halfLine && !add ? "sum" : "")}\">{(value < 0 ? "" : (add ? "+" : ""))}</td>"
+ $"<td colspan=\"2\" class=\"number large {(halfLine && !add ? "sum" : "")}\">" + $"<td colspan=\"2\" class=\"number {(subCat ? "small" : "large")} {(halfLine && !add ? "sum" : "")}\">"
+ $"<span class=\"fleft\">{Model.CurrencySymbol}</span>{Math.Abs(value ?? 0):N2}</td>" + $"<span class=\"fleft\">{Model.CurrencySymbol}</span>{Math.Abs(value ?? 0):N2}</td>"
+ $"</tr>\n"; + $"</tr>\n";
} }
@ -116,5 +116,33 @@
} }
} }
</tbody> </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> </table>
</main> </main>

View File

@ -14,6 +14,7 @@ using Elwig.Models.Dtos;
namespace Elwig.Helpers { namespace Elwig.Helpers {
public record struct AreaComBucket(int Area, int Obligation, int Right); 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 record struct MemberBucket(string Name, int Area, int Obligation, int Right, int Delivery, int DeliveryStrict, int Payment);
public class AppDbContext : DbContext { 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"; 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, AreaComBucket>>> _memberAreaCommitmentBuckets = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBuckets = new(); private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBuckets = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBucketsStrict = new(); private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBucketsStrict = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberPaymentBuckets = new(); private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberPaymentBuckets = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, UnderDelivery>>> _memberUnderDelivery = [];
public AppDbContext() { public AppDbContext() {
if (App.Config.DatabaseLog != null) { if (App.Config.DatabaseLog != null) {
@ -217,7 +219,7 @@ namespace Elwig.Helpers {
while (await reader.ReadAsync()) { while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0); var mgnr = reader.GetInt32(0);
var vtrgid = reader.GetString(1); 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)); buckets[mgnr][vtrgid] = new(reader.GetInt32(2), reader.GetInt32(3), reader.GetInt32(4));
} }
} }
@ -235,7 +237,7 @@ namespace Elwig.Helpers {
while (await reader.ReadAsync()) { while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0); var mgnr = reader.GetInt32(0);
var bucket = reader.GetString(1); var bucket = reader.GetString(1);
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = new(); if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
buckets[mgnr][bucket] = reader.GetInt32(2); buckets[mgnr][bucket] = reader.GetInt32(2);
} }
} }
@ -253,7 +255,7 @@ namespace Elwig.Helpers {
while (await reader.ReadAsync()) { while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0); var mgnr = reader.GetInt32(0);
var bucket = reader.GetString(1); var bucket = reader.GetString(1);
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = new(); if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
buckets[mgnr][bucket] = reader.GetInt32(2); buckets[mgnr][bucket] = reader.GetInt32(2);
} }
} }
@ -271,7 +273,7 @@ namespace Elwig.Helpers {
while (await reader.ReadAsync()) { while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0); var mgnr = reader.GetInt32(0);
var bucket = reader.GetString(1); var bucket = reader.GetString(1);
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = new(); if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
buckets[mgnr][bucket] = reader.GetInt32(2); buckets[mgnr][bucket] = reader.GetInt32(2);
} }
} }
@ -279,28 +281,52 @@ namespace Elwig.Helpers {
_memberPaymentBuckets[year] = buckets; _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) { public async Task<Dictionary<string, AreaComBucket>> GetMemberAreaCommitmentBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberAreaCommitmentBuckets.ContainsKey(year)) if (!_memberAreaCommitmentBuckets.ContainsKey(year))
await FetchMemberAreaCommitmentBuckets(year, cnx); 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) { public async Task<Dictionary<string, int>> GetMemberDeliveryBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberDeliveryBuckets.ContainsKey(year)) if (!_memberDeliveryBuckets.ContainsKey(year))
await FetchMemberDeliveryBuckets(year, cnx); 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) { public async Task<Dictionary<string, int>> GetMemberDeliveryBucketsStrict(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberDeliveryBucketsStrict.ContainsKey(year)) if (!_memberDeliveryBucketsStrict.ContainsKey(year))
await FetchMemberDeliveryBucketsStrict(year, cnx); 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) { public async Task<Dictionary<string, int>> GetMemberPaymentBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberPaymentBuckets.ContainsKey(year)) if (!_memberPaymentBuckets.ContainsKey(year))
await FetchMemberPaymentBuckets(year, cnx); 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) { public async Task<Dictionary<string, MemberBucket>> GetMemberBuckets(int year, int mgnr, SqliteConnection? cnx = null) {

View File

@ -23,18 +23,17 @@ namespace Elwig.Helpers.Billing {
await Revert(); await Revert();
using var cnx = await AppDbContext.ConnectAsync(); using var cnx = await AppDbContext.ConnectAsync();
using (var cmd = cnx.CreateCommand()) { using (var cmd = cnx.CreateCommand()) {
// TODO modifiers and prev_modifiers
cmd.CommandText = $""" cmd.CommandText = $"""
INSERT INTO credit (year, tgnr, mgnr, avnr, net_amount, prev_net_amount, vat, modifiers, prev_modifiers) INSERT INTO credit (year, tgnr, mgnr, avnr, net_amount, prev_net_amount, vat, modifiers, prev_modifiers)
SELECT s.year, SELECT s.year,
COALESCE(t.tgnr, 0) + ROW_NUMBER() OVER(ORDER BY m.mgnr) AS tgnr, COALESCE(t.tgnr, 0) + ROW_NUMBER() OVER(ORDER BY m.mgnr) AS tgnr,
m.mgnr, m.mgnr,
v.avnr, 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, ROUND(lp.amount / POW(10, s.precision - 2)) AS prev_amount,
IIF(m.buchführend, s.vat_normal, s.vat_flatrate) AS vat, IIF(m.buchführend, s.vat_normal, s.vat_flatrate) AS vat,
NULL, ROUND(COALESCE(u.total_penalty, 0) / POW(10, 4 - 2)) AS modifiers,
NULL lc.modifiers AS prev_modifiers
FROM season s FROM season s
JOIN payment_variant v ON v.year = s.year JOIN payment_variant v ON v.year = s.year
LEFT JOIN payment_variant l ON l.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 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 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 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} WHERE s.year = {Year} AND v.avnr = {AvNr}
"""; """;
await cmd.ExecuteNonQueryAsync(); await cmd.ExecuteNonQueryAsync();

View File

@ -140,8 +140,9 @@ namespace Elwig.Windows {
IEnumerable<Member> list = await members.ToListAsync(); IEnumerable<Member> list = await members.ToListAsync();
var data = await CreditNoteData.ForPaymentVariant(Context.CreditNoteRows, Context.Seasons, v.Year, v.AvNr); 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); 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 => 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 => { await doc.Generate(new Progress<double>(v => {
ProgressBar.Value = v; ProgressBar.Value = v;