Compare commits

...

5 Commits

14 changed files with 222 additions and 62 deletions

View File

@@ -2,7 +2,6 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.IO; using System.IO;
using Elwig.Helpers; using Elwig.Helpers;
using System.Text;
namespace Elwig.Documents { namespace Elwig.Documents {
public abstract class Document : IDisposable { public abstract class Document : IDisposable {
@@ -63,7 +62,7 @@ namespace Elwig.Documents {
public async Task Generate() { public async Task Generate() {
var pdf = new TempFile("pdf"); var pdf = new TempFile("pdf");
using (var tmpHtml = new TempFile("html")) { using (var tmpHtml = new TempFile("html")) {
await File.WriteAllTextAsync(tmpHtml.FilePath, await Render(), Encoding.UTF8); await File.WriteAllTextAsync(tmpHtml.FilePath, await Render(), Utils.UTF8);
await Pdf.Convert(tmpHtml.FilePath, pdf.FilePath); await Pdf.Convert(tmpHtml.FilePath, pdf.FilePath);
} }
PdfFile = pdf; PdfFile = pdf;

View File

@@ -1,14 +1,35 @@
using Elwig.Models; using Elwig.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Helpers.Billing { namespace Elwig.Helpers.Billing {
public class Transaction { public class Transaction {
public readonly Member Member; 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; Member = m;
AmountCent = (int)(amount * 100); AmountCent = (long)Math.Round(amount * 100);
Currency = currency;
Nr = nr;
} }
public static IEnumerable<Transaction> 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}";
} }
} }

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using IniParser; using IniParser;
using IniParser.Model; using IniParser.Model;
@@ -25,7 +24,7 @@ namespace Elwig.Helpers {
var parser = new FileIniDataParser(); var parser = new FileIniDataParser();
IniData? ini = null; IniData? ini = null;
try { try {
ini = parser.ReadFile(FileName, Encoding.UTF8); ini = parser.ReadFile(FileName, Utils.UTF8);
} catch {} } catch {}
if (ini == null || !ini.TryGetKey("database.file", out string db)) { if (ini == null || !ini.TryGetKey("database.file", out string db)) {
@@ -70,7 +69,7 @@ namespace Elwig.Helpers {
} }
public void Write() { public void Write() {
using var file = new StreamWriter(FileName, false, Encoding.UTF8); using var file = new StreamWriter(FileName, false, Utils.UTF8);
file.Write($"\r\n[general]\r\n"); file.Write($"\r\n[general]\r\n");
if (Branch != null) file.Write($"branch = {Branch}\r\n"); if (Branch != null) file.Write($"branch = {Branch}\r\n");
file.Write($"\r\n[database]\r\nfile = {DatabaseFile}\r\n"); file.Write($"\r\n[database]\r\nfile = {DatabaseFile}\r\n");

View File

@@ -1,11 +1,14 @@
using Elwig.Models; using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System;
namespace Elwig.Helpers.Export { namespace Elwig.Helpers.Export {
public class Bki : Csv<DeliveryPart> {
using Row = Tuple<(string, string?, string?, string?, string, int, string, int), (string, int, string, string, string, int, string, double, double)>;
public class Bki : Csv<Row> {
private readonly string _clientData; private readonly string _clientData;
@@ -22,29 +25,42 @@ namespace Elwig.Helpers.Export {
_clientData = $"{c.LfbisNr};{c.NameFull};;{a1};{a2};{c.Plz};{c.Ort}"; _clientData = $"{c.LfbisNr};{c.NameFull};;{a1};{a2};{c.Plz};{c.Ort}";
} }
public async Task ExportAsync(AppDbContext ctx, int year) { public async Task ExportAsync(int year) {
await ExportAsync(await ctx.DeliveryParts.Where(p => p.Year == year).ToListAsync()); using var cnx = await AppDbContext.ConnectAsync();
} using var cmd = cnx.CreateCommand();
cmd.CommandText = $"""
public void Export(AppDbContext ctx, int year) { SELECT lfbis_nr, family_name, name, billing_name, address, plz, ort, area,
ExportAsync(ctx, year).GetAwaiter().GetResult(); date, weight, type, sortid, qualid, year, hkid, kmw, oe
} FROM v_bki_delivery
WHERE year = {year}
public override string FormatRow(DeliveryPart p) { """;
var d = p.Delivery; var r = await cmd.ExecuteReaderAsync();
var m = d.Member; List<Row> rows = new();
string memberData; while (await r.ReadAsync()) {
if (m.BillingAddress is BillingAddr a) { rows.Add(new(
var (n1, n2) = Utils.SplitName(a.Name, m.FamilyName); (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)),
var (a1, a2) = Utils.SplitAddress(a.Address); (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))
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}";
} }
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};{p.Oe}"; await ExportAsync(rows);
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}"; }
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}"; return $"{_clientData};{memberData};{deliveryData};{vollData}";
} }
} }

View File

@@ -2,43 +2,54 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Elwig.Helpers.Export { namespace Elwig.Helpers.Export {
public abstract class Csv<T> : IExporter<T> { public abstract class Csv<T> : IExporter<T> {
public static string FileExtension => "csv"; public static string FileExtension => "csv";
protected readonly StreamWriter Writer; private readonly StreamWriter _writer;
protected readonly char Separator; protected readonly char Separator;
protected string? Header; 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) { public Csv(string filename, char separator, Encoding encoding) {
Writer = new StreamWriter(filename, false, encoding); _writer = new StreamWriter(filename, false, encoding);
Separator = separator; Separator = separator;
} }
public void Dispose() { public void Dispose() {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
Writer.Dispose(); _writer.Dispose();
} }
public ValueTask DisposeAsync() { public ValueTask DisposeAsync() {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
return Writer.DisposeAsync(); return _writer.DisposeAsync();
} }
public async Task ExportAsync(IEnumerable<T> data) { public async Task ExportAsync(IEnumerable<T> data, IProgress<double>? progress = null) {
if (Header != null) await Writer.WriteLineAsync(Header); 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) { 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<T> data) { public void Export(IEnumerable<T> data, IProgress<double>? progress = null) {
ExportAsync(data).GetAwaiter().GetResult(); ExportAsync(data, progress).GetAwaiter().GetResult();
} }
public string FormatRow(IEnumerable row) { public string FormatRow(IEnumerable row) {

View File

@@ -1,6 +1,9 @@
using Elwig.Helpers.Billing; using Elwig.Helpers.Billing;
using Elwig.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Elwig.Helpers.Export { namespace Elwig.Helpers.Export {
@@ -8,20 +11,99 @@ namespace Elwig.Helpers.Export {
public static string FileExtension => "xml"; 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() { public void Dispose() {
throw new NotImplementedException(); GC.SuppressFinalize(this);
_writer.Dispose();
} }
public ValueTask DisposeAsync() { public ValueTask DisposeAsync() {
throw new NotImplementedException(); GC.SuppressFinalize(this);
return _writer.DisposeAsync();
} }
public void Export(IEnumerable<Transaction> data) { public void Export(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) {
throw new NotImplementedException(); ExportAsync(transactions, progress).GetAwaiter().GetResult();
} }
public Task ExportAsync(IEnumerable<Transaction> data) { public async Task ExportAsync(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) {
throw new NotImplementedException(); 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($"""
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.xsd">
<CstmrCdtTrfInitn>
<GrpHdr>
<MsgId>{msgId}</MsgId>
<CreDtTm>{DateTime.UtcNow:o}</CreDtTm>
<NbOfTxs>{nbOfTxs}</NbOfTxs>
<CtrlSum>{ctrlSum}</CtrlSum>
<InitgPty><Nm>{App.Client.NameFull}</Nm></InitgPty>
</GrpHdr>
<PmtInf>
<PmtInfId>{pmtInfId}</PmtInfId>
<PmtMtd>TRF</PmtMtd>
<NbOfTxs>{nbOfTxs}</NbOfTxs>
<CtrlSum>{ctrlSum}</CtrlSum>
<ReqdExctnDt><Dt>{_date:yyyy-MM-dd}</Dt></ReqdExctnDt>
<Dbtr><Nm>{App.Client.NameFull}</Nm></Dbtr>
<DbtrAcct><Id><IBAN>{App.Client.Iban?.Replace(" ", "")}</IBAN></Id></DbtrAcct>
<DbtrAgt><FinInstnId><BICFI>{App.Client.Bic ?? "NOTPROVIDED"}</BICFI></FinInstnId></DbtrAgt>
""");
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($"""
<CdtTrfTxInf>
<PmtId><EndToEndId>{id}</EndToEndId></PmtId>
<Amt><InstdAmt Ccy="{tx.Currency}">{Transaction.FormatAmountCent(tx.AmountCent)}</InstdAmt></Amt>
<Cdtr>
<Nm>{a.Name}</Nm>
<PstlAdr>
<StrtNm>{a1}</StrtNm><BldgNb>{a2}</BldgNb>
<PstCd>{a.PostalDest.AtPlz?.Plz}</PstCd><TwnNm>{a.PostalDest.AtPlz?.Ort.Name}</TwnNm>
<Ctry>{a.PostalDest.Country.Alpha2}</Ctry>
</PstlAdr>
</Cdtr>
<CdtrAcct><Id><IBAN>{tx.Member.Iban}</IBAN></Id><CdtrAcct>
<CdtrAgt><FinInstnId><BICFI>{tx.Member.Bic ?? "NOTPROVIDED"}</BICFI></FinInstnId></CdtrAgt>
<RmtInf><Ustrd>{info}</Ustrd></RmtInf>
</CdtTrfTxInf>
""");
progress?.Report(100.0 * ++i / count);
}
await _writer.WriteLineAsync("""
</PmtInf>
</CstmrCdtTrfInitn>
</Document>
""");
await _writer.FlushAsync();
progress?.Report(100.0);
} }
} }
} }

View File

@@ -7,18 +7,20 @@ namespace Elwig.Helpers.Export {
/// <summary> /// <summary>
/// The default file extension of the exported files to be used (whithout a preceding ".") /// The default file extension of the exported files to be used (whithout a preceding ".")
/// </summary> /// </summary>
static string FileExtension { get; } static abstract string FileExtension { get; }
/// <summary> /// <summary>
/// Export the given data to the given file. /// Export the given data to the given file.
/// </summary> /// </summary>
/// <param name="data">The data to be exported</param> /// <param name="data">The data to be exported</param>
void Export(IEnumerable<T> data); /// <param name="progress">The progress object to report to</param>
void Export(IEnumerable<T> data, IProgress<double>? progress = null);
/// <summary> /// <summary>
/// Export the given data to the given file. /// Asynchronosly export the given data to the given file.
/// </summary> /// </summary>
/// <param name="data">The data to be exported</param> /// <param name="data">The data to be exported</param>
Task ExportAsync(IEnumerable<T> data); /// <param name="progress">The progress object to report to</param>
Task ExportAsync(IEnumerable<T> data, IProgress<double>? progress = null);
} }
} }

View File

@@ -0,0 +1,9 @@
using Elwig.Models;
namespace Elwig.Helpers {
public interface IAddress {
string Name { get; }
string Address { get; }
PostalDest PostalDest { get; }
}
}

View File

@@ -15,6 +15,8 @@ using Elwig.Models;
namespace Elwig.Helpers { namespace Elwig.Helpers {
public static partial class Utils { public static partial class Utils {
public static readonly Encoding UTF8 = new UTF8Encoding(false, true);
public static int CurrentYear => DateTime.Now.Year; public static int CurrentYear => DateTime.Now.Year;
public static int CurrentNextSeason => DateTime.Now.Year - (DateTime.Now.Month <= 3 ? 1 : 0); public static int CurrentNextSeason => DateTime.Now.Year - (DateTime.Now.Month <= 3 ? 1 : 0);
public static int CurrentLastSeason => DateTime.Now.Year - (DateTime.Now.Month <= 7 ? 1 : 0); public static int CurrentLastSeason => DateTime.Now.Year - (DateTime.Now.Month <= 7 ? 1 : 0);

View File

@@ -1,9 +1,10 @@
using Elwig.Helpers;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
namespace Elwig.Models { namespace Elwig.Models {
[Table("member_billing_address"), PrimaryKey("MgNr")] [Table("member_billing_address"), PrimaryKey("MgNr")]
public class BillingAddr { public class BillingAddr : IAddress {
[Column("mgnr")] [Column("mgnr")]
public int MgNr { get; set; } public int MgNr { get; set; }

View File

@@ -7,7 +7,7 @@ using System.Linq;
namespace Elwig.Models { namespace Elwig.Models {
[Table("member"), PrimaryKey("MgNr")] [Table("member"), PrimaryKey("MgNr")]
public class Member { public class Member : IAddress {
[Column("mgnr")] [Column("mgnr")]
public int MgNr { get; set; } public int MgNr { get; set; }

View File

@@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
namespace Elwig.Models { namespace Elwig.Models {
[Table("payment_member"), PrimaryKey("Year", "AvNr", "MgNr")] [Table("payment_member"), PrimaryKey("Year", "AvNr", "MgNr"), Index("Year", "TgNr", IsUnique = true)]
public class PaymentMember { public class PaymentMember {
[Column("year")] [Column("year")]
public int Year { get; set; } public int Year { get; set; }
@@ -16,6 +16,9 @@ namespace Elwig.Models {
[Column("amount")] [Column("amount")]
public long AmountValue { get; set; } public long AmountValue { get; set; }
[Column("tgnr")]
public int? TgNr { get; set; }
[NotMapped] [NotMapped]
public decimal Amount { public decimal Amount {
get => Variant.Season.DecFromDb(AmountValue); get => Variant.Season.DecFromDb(AmountValue);

View File

@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
namespace Elwig.Models { namespace Elwig.Models {
@@ -19,12 +20,17 @@ namespace Elwig.Models {
[NotMapped] [NotMapped]
public DateOnly Date { public DateOnly Date {
get { get => DateOnly.ParseExact(DateString, "yyyy-MM-dd");
return DateOnly.ParseExact(DateString, "yyyy-MM-dd"); set => DateString = value.ToString("yyyy-MM-dd");
} }
set {
DateString = value.ToString("yyyy-MM-dd"); [Column("transfer_date")]
} public string? TransferDateString { get; set; }
[NotMapped]
public DateOnly? TransferDate {
get => TransferDateString != null ? DateOnly.ParseExact(TransferDateString, "yyyy-MM-dd") : null;
set => TransferDateString = value?.ToString("yyyy-MM-dd");
} }
[Column("test_variant")] [Column("test_variant")]
@@ -68,5 +74,8 @@ namespace Elwig.Models {
[ForeignKey("Year")] [ForeignKey("Year")]
public virtual Season Season { get; private set; } public virtual Season Season { get; private set; }
[InverseProperty("Variant")]
public virtual ISet<PaymentMember> MemberPayments { get; private set; }
} }
} }

View File

@@ -48,6 +48,12 @@ namespace Elwig.Models {
[InverseProperty("Season")] [InverseProperty("Season")]
public virtual ISet<Modifier> Modifiers { get; private set; } public virtual ISet<Modifier> Modifiers { get; private set; }
[InverseProperty("Season")]
public virtual ISet<PaymentVar> PaymentVariants { get; private set; }
[InverseProperty("Season")]
public virtual ISet<Delivery> Deliveries { get; private set; }
public decimal DecFromDb(long value) { public decimal DecFromDb(long value) {
return Utils.DecFromDb(value, Precision); return Utils.DecFromDb(value, Precision);
} }