From f5f00a77394009d88ff6824933561bd0710ab09f Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Fri, 8 Sep 2023 00:43:53 +0200 Subject: [PATCH] Export: Add Ebics and improve Bki export --- Elwig/Helpers/Billing/Transaction.cs | 27 +++++++- Elwig/Helpers/Export/Bki.cs | 66 +++++++++++-------- Elwig/Helpers/Export/Csv.cs | 31 ++++++--- Elwig/Helpers/Export/Ebics.cs | 94 ++++++++++++++++++++++++++-- Elwig/Helpers/Export/IExporter.cs | 10 +-- 5 files changed, 180 insertions(+), 48 deletions(-) diff --git a/Elwig/Helpers/Billing/Transaction.cs b/Elwig/Helpers/Billing/Transaction.cs index 0c79498..2e0160c 100644 --- a/Elwig/Helpers/Billing/Transaction.cs +++ b/Elwig/Helpers/Billing/Transaction.cs @@ -1,14 +1,35 @@ using Elwig.Models; +using System; +using System.Collections.Generic; +using System.Linq; namespace Elwig.Helpers.Billing { public class Transaction { public readonly Member Member; - public readonly int AmountCent; + public readonly long AmountCent; + public readonly string Currency; + public readonly int Nr; - public Transaction(Member m, decimal amount) { + public Transaction(Member m, decimal amount, string currency, int nr) { Member = m; - AmountCent = (int)(amount * 100); + AmountCent = (long)Math.Round(amount * 100); + Currency = currency; + Nr = nr; } + + public static IEnumerable FromPaymentVariant(PaymentVar variant) { + var last = variant.Season.PaymentVariants.Where(v => v.TransferDate != null).OrderBy(v => v.TransferDate).LastOrDefault(); + var dict = last?.MemberPayments.ToDictionary(m => m.MgNr, m => m.Amount) ?? new(); + return variant.MemberPayments + .OrderBy(m => m.MgNr) + .Select(m => { + var amt = Math.Round(dict.GetValueOrDefault(m.MgNr, 0), 2); + return new Transaction(m.Member, m.Amount - amt, m.Variant.Season.CurrencyCode, m.TgNr ?? 0); + }) + .ToList(); + } + + public static string FormatAmountCent(long cents) => $"{cents / 100}.{cents % 100:00}"; } } diff --git a/Elwig/Helpers/Export/Bki.cs b/Elwig/Helpers/Export/Bki.cs index 7d50ef7..cba5080 100644 --- a/Elwig/Helpers/Export/Bki.cs +++ b/Elwig/Helpers/Export/Bki.cs @@ -1,11 +1,14 @@ -using Elwig.Models; -using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using System; namespace Elwig.Helpers.Export { - public class Bki : Csv { + + using Row = Tuple<(string, string?, string?, string?, string, int, string, int), (string, int, string, string, string, int, string, double, double)>; + + public class Bki : Csv { private readonly string _clientData; @@ -22,29 +25,42 @@ namespace Elwig.Helpers.Export { _clientData = $"{c.LfbisNr};{c.NameFull};;{a1};{a2};{c.Plz};{c.Ort}"; } - public async Task ExportAsync(AppDbContext ctx, int year) { - await ExportAsync(await ctx.DeliveryParts.Where(p => p.Year == year).ToListAsync()); - } - - public void Export(AppDbContext ctx, int year) { - ExportAsync(ctx, year).GetAwaiter().GetResult(); - } - - public override string FormatRow(DeliveryPart p) { - var d = p.Delivery; - var m = d.Member; - string memberData; - if (m.BillingAddress is BillingAddr a) { - var (n1, n2) = Utils.SplitName(a.Name, m.FamilyName); - var (a1, a2) = Utils.SplitAddress(a.Address); - memberData = $"{m.LfbisNr};{n1};{n2};{a1};{a2};{a.PostalDest.AtPlz?.Plz};{a.PostalDest.AtPlz?.Ort.Name}"; - } else { - var (a1, a2) = Utils.SplitAddress(m.Address); - memberData = $"{m.LfbisNr};{m.FamilyName};{m.AdministrativeName2};{a1};{a2};{m.PostalDest.AtPlz?.Plz};{m.PostalDest.AtPlz?.Ort.Name}"; + public async Task ExportAsync(int year) { + using var cnx = await AppDbContext.ConnectAsync(); + using var cmd = cnx.CreateCommand(); + cmd.CommandText = $""" + SELECT lfbis_nr, family_name, name, billing_name, address, plz, ort, area, + date, weight, type, sortid, qualid, year, hkid, kmw, oe + FROM v_bki_delivery + WHERE year = {year} + """; + var r = await cmd.ExecuteReaderAsync(); + List rows = new(); + while (await r.ReadAsync()) { + rows.Add(new( + (r.GetString(0), r.IsDBNull(1) ? null : r.GetString(1), r.IsDBNull(2) ? null : r.GetString(2), r.IsDBNull(3) ? null : r.GetString(3), r.GetString(4), r.GetInt32(5), r.GetString(6), r.GetInt32(7)), + (r.GetString(8), r.GetInt32(9), r.GetString(10), r.GetString(11), r.GetString(12), r.GetInt32(13), r.GetString(14), r.GetDouble(15), r.GetDouble(16)) + )); } - var s = p.Variant; - var deliveryData = $"{d.Date:dd.MM.yyyy};{p.Weight};TB;{(s.IsWhite ? "J" : "")};{(s.IsRed ? "J" : "")};{s.SortId};;;{p.QualId};{d.Year};{p.HkId};{p.Kmw:0.0};{p.Oe:0}"; - var vollData = $"N;;;{m.AreaCommitments.Where(a => a.YearFrom <= d.Year && (a.YearTo == null || a.YearTo >= d.Year)).Sum(a => a.Area) / 10_000.0}"; + + await ExportAsync(rows); + } + + public void Export(int year) { + ExportAsync(year).GetAwaiter().GetResult(); + } + + public override string FormatRow(Row row) { + var (member, delivery) = row; + var (lfBisNr, familyName, name, billingName, address, plz, ort, area) = member; + var (date, weight, type, sortid, qualid, year, hkid, kmw, oe) = delivery; + + var (n1, n2) = billingName == null ? (familyName, name) : Utils.SplitName(billingName, familyName); + var (a1, a2) = Utils.SplitAddress(address); + var memberData = $"{lfBisNr};{n1};{n2};{a1};{a2};{plz};{ort}"; + var deliveryData = $"{string.Join(".", date.Split("-").Reverse())};{weight};TB;{(type == "W" ? "J" : "")};{(type == "R" ? "J" : "")};{sortid};;;{qualid};{year};{hkid};{kmw:0.0};{oe:0}"; + var vollData = $"N;;;{area / 10_000.0}"; + return $"{_clientData};{memberData};{deliveryData};{vollData}"; } } diff --git a/Elwig/Helpers/Export/Csv.cs b/Elwig/Helpers/Export/Csv.cs index e34691c..fae38e6 100644 --- a/Elwig/Helpers/Export/Csv.cs +++ b/Elwig/Helpers/Export/Csv.cs @@ -2,43 +2,54 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Threading.Tasks; namespace Elwig.Helpers.Export { public abstract class Csv : IExporter { + public static string FileExtension => "csv"; - protected readonly StreamWriter Writer; + private readonly StreamWriter _writer; protected readonly char Separator; protected string? Header; - public Csv(string filename, char separator = ';') : this(filename, separator, Encoding.UTF8) { } + public Csv(string filename, char separator = ';') : this(filename, separator, Utils.UTF8) { } public Csv(string filename, char separator, Encoding encoding) { - Writer = new StreamWriter(filename, false, encoding); + _writer = new StreamWriter(filename, false, encoding); Separator = separator; } public void Dispose() { GC.SuppressFinalize(this); - Writer.Dispose(); + _writer.Dispose(); } public ValueTask DisposeAsync() { GC.SuppressFinalize(this); - return Writer.DisposeAsync(); + return _writer.DisposeAsync(); } - public async Task ExportAsync(IEnumerable data) { - if (Header != null) await Writer.WriteLineAsync(Header); + public async Task ExportAsync(IEnumerable data, IProgress? progress = null) { + progress?.Report(0.0); + int count = data.Count() + 2, i = 0; + + if (Header != null) await _writer.WriteLineAsync(Header); + progress?.Report(100.0 * ++i / count); + foreach (var row in data) { - await Writer.WriteLineAsync(FormatRow(row)); + await _writer.WriteLineAsync(FormatRow(row)); + progress?.Report(100.0 * ++i / count); } + + await _writer.FlushAsync(); + progress?.Report(100.0); } - public void Export(IEnumerable data) { - ExportAsync(data).GetAwaiter().GetResult(); + public void Export(IEnumerable data, IProgress? progress = null) { + ExportAsync(data, progress).GetAwaiter().GetResult(); } public string FormatRow(IEnumerable row) { diff --git a/Elwig/Helpers/Export/Ebics.cs b/Elwig/Helpers/Export/Ebics.cs index f454ccb..9bbc27f 100644 --- a/Elwig/Helpers/Export/Ebics.cs +++ b/Elwig/Helpers/Export/Ebics.cs @@ -1,6 +1,9 @@ using Elwig.Helpers.Billing; +using Elwig.Models; using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Threading.Tasks; namespace Elwig.Helpers.Export { @@ -8,20 +11,99 @@ namespace Elwig.Helpers.Export { public static string FileExtension => "xml"; + private readonly StreamWriter _writer; + private readonly DateOnly _date; + private readonly int _year; + private readonly string _name; + private readonly int _nr; + + public Ebics(PaymentVar variant, string filename) { + _writer = new(filename, false, Utils.UTF8); + _date = variant.TransferDate ?? DateOnly.Parse("2021-01-10"); //throw new ArgumentException("TransferDate has to be set in PaymentVar"); + _year = variant.Year; + _name = variant.Name; + _nr = variant.AvNr; + } + public void Dispose() { - throw new NotImplementedException(); + GC.SuppressFinalize(this); + _writer.Dispose(); } public ValueTask DisposeAsync() { - throw new NotImplementedException(); + GC.SuppressFinalize(this); + return _writer.DisposeAsync(); } - public void Export(IEnumerable data) { - throw new NotImplementedException(); + public void Export(IEnumerable transactions, IProgress? progress = null) { + ExportAsync(transactions, progress).GetAwaiter().GetResult(); } - public Task ExportAsync(IEnumerable data) { - throw new NotImplementedException(); + public async Task ExportAsync(IEnumerable transactions, IProgress? progress = null) { + progress?.Report(0.0); + var nbOfTxs = transactions.Count(); + int count = nbOfTxs + 2, i = 0; + var ctrlSum = Transaction.FormatAmountCent(transactions.Sum(tx => tx.AmountCent)); + var msgId = $"ELWIG-{App.Client.NameToken}-{_year}-AV{_nr:00}"; + var pmtInfId = $"{msgId}-1"; + + await _writer.WriteLineAsync($""" + + + + + {msgId} + {DateTime.UtcNow:o} + {nbOfTxs} + {ctrlSum} + {App.Client.NameFull} + + + {pmtInfId} + TRF + {nbOfTxs} + {ctrlSum} +
{_date:yyyy-MM-dd}
+ {App.Client.NameFull} + {App.Client.Iban?.Replace(" ", "")} + {App.Client.Bic ?? "NOTPROVIDED"} + """); + progress?.Report(100.0 * ++i / count); + + foreach (var tx in transactions) { + var a = (IAddress?)tx.Member.BillingAddress ?? tx.Member; + var (a1, a2) = Utils.SplitAddress(a.Address); + var id = $"ELWIG-{App.Client.NameToken}-{_year}-TG{tx.Nr:0000}"; + var info = $"{_name} - Traubengutschrift {_year}/{tx.Nr:000}"; + await _writer.WriteLineAsync($""" + + {id} + {Transaction.FormatAmountCent(tx.AmountCent)} + + {a.Name} + + {a1}{a2} + {a.PostalDest.AtPlz?.Plz}{a.PostalDest.AtPlz?.Ort.Name} + {a.PostalDest.Country.Alpha2} + + + {tx.Member.Iban} + {tx.Member.Bic ?? "NOTPROVIDED"} + {info} + + """); + progress?.Report(100.0 * ++i / count); + } + + await _writer.WriteLineAsync(""" +
+
+
+ """); + await _writer.FlushAsync(); + progress?.Report(100.0); } } } diff --git a/Elwig/Helpers/Export/IExporter.cs b/Elwig/Helpers/Export/IExporter.cs index 8f9b92e..d7b689d 100644 --- a/Elwig/Helpers/Export/IExporter.cs +++ b/Elwig/Helpers/Export/IExporter.cs @@ -7,18 +7,20 @@ namespace Elwig.Helpers.Export { /// /// The default file extension of the exported files to be used (whithout a preceding ".") /// - static string FileExtension { get; } + static abstract string FileExtension { get; } /// /// Export the given data to the given file. /// /// The data to be exported - void Export(IEnumerable data); + /// The progress object to report to + void Export(IEnumerable data, IProgress? progress = null); /// - /// Export the given data to the given file. + /// Asynchronosly export the given data to the given file. /// /// The data to be exported - Task ExportAsync(IEnumerable data); + /// The progress object to report to + Task ExportAsync(IEnumerable data, IProgress? progress = null); } }