Compare commits

..

60 Commits

Author SHA1 Message Date
lorenz.stechauner 09a7889044 AppDbContext: Add NextAvNr() 2024-01-08 14:18:15 +01:00
lorenz.stechauner d77aac43ec App: Add FocusBaseDataSeason() 2024-01-08 14:17:56 +01:00
lorenz.stechauner 062c7bed5e AppDbUpdater: Fix migration 10 to 11 by creating a new table 2024-01-08 11:21:01 +01:00
lorenz.stechauner b723161fa5 BillingVariant: Fix CalculateModifiers() by restricting modifier year 2024-01-08 02:40:32 +01:00
lorenz.stechauner debee3b4bf PaymentVariantsWindow: Fix once wrong, every time wrong error 2024-01-08 02:24:19 +01:00
lorenz.stechauner f15733f827 CreditNote: Fix styling 2024-01-07 23:43:56 +01:00
lorenz.stechauner 05b6e8ddd6 CreditNote: Add hint regarding calculations 2024-01-07 22:24:06 +01:00
lorenz.stechauner 5ae326074f Document: Fix GenerationProportion 2024-01-07 22:08:21 +01:00
lorenz.stechauner 60b99bf95b CreditNote: Split table into two tables 2024-01-07 21:47:25 +01:00
lorenz.stechauner 3562d304de Document: Remove break-before: avoid for tr.sum 2024-01-07 21:39:42 +01:00
lorenz.stechauner 9f73d13dbf Document: Add GenerationProportion and set to 1/8 2024-01-07 15:26:51 +01:00
lorenz.stechauner 5e665ffb50 PaymentVariantsWindow: Impelment BillingVariant 2024-01-07 15:21:32 +01:00
lorenz.stechauner 1e751c473a BillingData: Implement elwig json format parsing 2024-01-06 14:45:18 +01:00
lorenz.stechauner d67e434fed Use InvariantCulture instead of replacing ',' with '.' 2024-01-06 14:25:42 +01:00
lorenz.stechauner 3a89e16db3 Tests: Move tests into package 2024-01-05 18:03:28 +01:00
lorenz.stechauner 2556033a07 Tests: Add in-memory database for testing 2024-01-05 18:03:21 +01:00
lorenz.stechauner 3f6a94e773 AppDbContext: Add ExecuteEmbeddedScript() 2024-01-05 14:49:28 +01:00
lorenz.stechauner e75e2ddbda BillingData: Move schema to Resources/ 2024-01-05 13:45:37 +01:00
lorenz.stechauner 121ca10261 AppDbContext: Add ExecuteBatch() and ExecuteScalar() 2024-01-05 13:41:27 +01:00
lorenz.stechauner f28a1a2db9 AppDbUpdater: Extract sql commands as embedded resources 2024-01-05 13:17:29 +01:00
lorenz.stechauner ab61edc402 AppDbUpdater: Using async/await 2024-01-05 10:20:53 +01:00
lorenz.stechauner ba55692cbe Models: Update Payment models 2024-01-05 09:53:05 +01:00
lorenz.stechauner eb46955b3b Billing: Add BillingData and JSON schema validation 2024-01-05 00:41:20 +01:00
lorenz.stechauner 37bf8d0855 Documents: Add Property Name 2024-01-04 15:05:40 +01:00
lorenz.stechauner be87f31211 ChartWindow: Small fixes 2024-01-04 13:12:48 +01:00
lorenz.stechauner 4738cde9e4 Billing: Update EBICS exporter 2023-12-29 13:14:32 +01:00
lorenz.stechauner 6cf5e0d45e Documents: Change wording in documents 2023-12-23 23:05:25 +01:00
lorenz.stechauner 8e71e82efc CreditNote: Display under deliveries of area commitments 2023-12-23 21:27:26 +01:00
lorenz.stechauner 4f07d9b129 AreaComUnderDeliveryData: Fix typo 2023-12-23 14:49:43 +01:00
lorenz.stechauner 8555748202 CreditNote: Add FormatRow() 2023-12-23 14:07:10 +01:00
lorenz.stechauner a0914f4d54 BillingVariant: Fix retrieval of last used payment variant 2023-12-23 14:07:01 +01:00
lorenz.stechauner 2301251420 CreditNote: Small fixes 2023-12-23 14:06:57 +01:00
lorenz.stechauner 83f9b58d4d CreditNoteData: Fix SQL query for multiple payment variants in one year 2023-12-23 14:06:57 +01:00
lorenz.stechauner f8aef20b0d CreditNote: Add VAT display 2023-12-23 14:06:57 +01:00
lorenz.stechauner 0dcffc8677 PaymentVariantsWindow: Add commit and revert buttons 2023-12-23 14:06:57 +01:00
lorenz.stechauner bc578b212e BillingVariant: Add Commit() and Revert() functions 2023-12-23 14:06:52 +01:00
lorenz.stechauner 8368caf58a CreditNote: Add member modifier display 2023-12-22 20:18:48 +01:00
lorenz.stechauner c836b45920 Documents: Add .bold for tables 2023-12-22 19:48:13 +01:00
lorenz.stechauner b9a2893d80 [#17] CreditNote: Overhaul CreditNote 2023-12-22 15:34:03 +01:00
lorenz.stechauner 82f93746ab MemberDataSheet: Fix typo 2023-12-22 10:11:35 +01:00
lorenz.stechauner b79fcfb1ed MemberDataSheet: Make more compact 2023-12-22 00:56:57 +01:00
lorenz.stechauner ba241c98a9 MemberDataSheet: Make Bankverbindung section smaller 2023-12-22 00:15:42 +01:00
lorenz.stechauner f7cdd7a4c1 MemberDataSheet: Minor improvements 2023-12-21 23:24:14 +01:00
lorenz.stechauner 1bdb7183ed [#5][#17][#23] MemberDataSheet: Overhaul entire MemberDataSheet 2023-12-21 22:24:04 +01:00
lorenz.stechauner 4d5ad13e0c Utils: Add PhoneNrTypes 2023-12-21 22:21:38 +01:00
lorenz.stechauner 69e6b6a713 Member: Use current calendar year to retrieve ActiveAreaCommitments 2023-12-21 22:20:24 +01:00
lorenz.stechauner 781077e5e3 [#25] Documents: Fix page breaking 2023-12-20 01:39:26 +01:00
lorenz.stechauner 16d429e9e4 DeliveryConfirmation: Keep last two tables together on page 2023-12-19 22:04:34 +01:00
lorenz.stechauner 18600a44da Document: Add mailto: and http: hrefs to footer 2023-12-19 20:48:01 +01:00
lorenz.stechauner 9c46974bd7 [#23] DeliveryConfirmation: Add Sortenaufteilung table 2023-12-19 20:48:01 +01:00
lorenz.stechauner 480f99234c AppDbContext: Remove support for multiple attributes in buckets 2023-12-19 19:31:35 +01:00
lorenz.stechauner 8811ca25ce [#17][#23] BusinessDocument: Remove enum RowMode 2023-12-19 18:11:02 +01:00
lorenz.stechauner 161bf31a62 [#17][#23] Documents: Unify table stylings 2023-12-19 18:10:31 +01:00
lorenz.stechauner ae00fd2c31 [#22] DeliveryConfirmation: Fix wrong border 2023-12-18 21:01:15 +01:00
lorenz.stechauner 2c48c89cfa Installer/config.ini: Comment out branch 2023-12-18 12:20:05 +01:00
lorenz.stechauner de5e62de50 MemberAdminWindow: Rename ...Memberdata... to ...MemberDataSheet... 2023-12-02 17:30:04 +01:00
lorenz.stechauner 0eed426559 [#21] AdministrationWindow: Temporarily fix UnitTextBox/TextBox casting error 2023-12-02 14:00:22 +01:00
lorenz.stechauner 03a9a3793a Config: Use Microsoft's INI implementation instead of a 3rd party's 2023-12-02 13:04:50 +01:00
lorenz.stechauner 7528764ff3 Update project to .NET 8 2023-12-02 12:28:16 +01:00
lorenz.stechauner 3576a066fe AppDbContext: Use records instead of unnamed tuples for buckets 2023-11-30 17:29:01 +01:00
72 changed files with 3009 additions and 1851 deletions
+1
View File
@@ -3,3 +3,4 @@ bin/
*.user
.vs
.idea
Tests/Resources/Create.sql
+12 -2
View File
@@ -16,6 +16,8 @@ using Elwig.Helpers.Printing;
using Elwig.Windows;
using Elwig.Dialogs;
using System.Threading.Tasks;
using Elwig.Helpers.Billing;
using Elwig.Models.Entities;
namespace Elwig {
public partial class App : Application {
@@ -65,7 +67,7 @@ namespace Elwig {
CurrentApp = this;
}
protected override void OnStartup(StartupEventArgs evt) {
protected override async void OnStartup(StartupEventArgs evt) {
var locale = new CultureInfo("de-AT");
locale.NumberFormat.CurrencyGroupSeparator = "\u202f";
locale.NumberFormat.NumberGroupSeparator = "\u202f";
@@ -82,7 +84,7 @@ namespace Elwig {
Version = typeof(App).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion.Split("+")[0] ?? "0.0.0";
try {
AppDbUpdater.CheckDb();
await AppDbUpdater.CheckDb();
} catch (Exception e) {
MessageBox.Show($"Invalid Database:\n\n{e.Message}", "Invalid Database", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown();
@@ -104,6 +106,7 @@ namespace Elwig {
Utils.RunBackground("HTML Initialization", () => Html.Init(PrintingReadyChanged));
Utils.RunBackground("PDF Initialization", () => Pdf.Init(PrintingReadyChanged));
Utils.RunBackground("JSON Schema Initialization", BillingData.Init);
var list = new List<IScale>();
foreach (var s in Config.Scales) {
@@ -219,6 +222,13 @@ namespace Elwig {
return w;
}
public static BaseDataWindow FocusBaseDataSeason(int year) {
var w = FocusBaseData();
w.Seasons.Focus();
ControlUtils.SelectListBoxItem(w.SeasonList, s => (s as Season)?.Year, year);
return w;
}
public static SeasonFinishWindow FocusSeasonFinish() {
return FocusWindow<SeasonFinishWindow>(() => new());
}
+147
View File
@@ -1,5 +1,8 @@
using Elwig.Helpers;
using Elwig.Models.Entities;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Elwig.Documents {
public abstract class BusinessDocument : Document {
@@ -32,5 +35,149 @@ namespace Elwig.Documents {
return (addr is BillingAddr ? $"{addr.Name}\n" : "") + $"{Member.AdministrativeName}\n{addr.Address}\n{plz?.Plz} {plz?.Ort.Name.Split(",")[0]}\n{addr.PostalDest.Country.Name}";
}
}
private static string GetColGroup(IEnumerable<double> cols) {
return "<colgroup>\n" + string.Join("\n", cols.Select(g => $"<col style=\"width: {g.ToString(CultureInfo.InvariantCulture)}mm;\"/>")) + "\n</colgroup>\n";
}
public static string PrintSortenaufteilung(Dictionary<string, MemberBucket> buckets) {
List<string> attributes = ["_", ""];
List<string> names = ["kein Qual.Wein", "ohne Attribut"];
List<(string, string)> bucketAttrs = [
.. buckets
.Where(b => b.Key.Length > 2 && b.Key[2] != '_' && b.Value.DeliveryStrict > 0)
.Select(b => (b.Key[2..], b.Value.Name.Split("(")[1][..^1]))
.Distinct()
.OrderBy(v => v.Item1)
];
names.AddRange(bucketAttrs.Select(b => b.Item2));
names.Add("Gesamt");
attributes.AddRange(bucketAttrs.Select(b => b.Item1));
List<double> cols = [40];
cols.AddRange(names.Select(_ => 125.0 / names.Count));
string tbl = GetColGroup(cols);
tbl += "<thead><tr>" +
$"<th><b>Sortenaufteilung</b> [kg]</th>" +
string.Join("", names.Select(c => $"<th>{c}</th>")) +
"</tr></thead>";
tbl += string.Join("\n", buckets
.GroupBy(b => (b.Key[..2], b.Value.Name.Split("(")[0].Trim()))
.Where(g => g.Sum(a => a.Value.DeliveryStrict) > 0)
.OrderBy(g => g.Key.Item1)
.Select(g => {
var dict = g.ToDictionary(a => a.Key[2..], a => a.Value);
var vals = attributes.Select(a => dict.TryGetValue(a, out MemberBucket value) ? value.DeliveryStrict : 0).ToList();
return $"<tr><th>{g.Key.Item2}</th>" + string.Join("", vals.Select(v => "<td class=\"number\">" + (v == 0 ? "-" : $"{v:N0}") + "</td>")) +
$"<td class=\"number\">{dict.Values.Select(v => v.DeliveryStrict).Sum():N0}</td></tr>";
})
);
var totalDict = buckets.GroupBy(b => b.Key[2..]).ToDictionary(g => g.Key, g => g.Sum(a => a.Value.DeliveryStrict));
var totals = attributes.Select(a => totalDict.TryGetValue(a, out int value) ? value : 0);
tbl += "<tr class=\"sum bold\"><td></td>" + string.Join("", totals.Select(v => $"<td class=\"number\">{v:N0}</td>")) +
$"<td class=\"number\">{totalDict.Values.Sum():N0}</td></tr>";
return "<table class=\"sortenaufteilung small number cohere\">" + tbl + "</table>";
}
private static string FormatRow(
int obligation, int right, int delivery, int? payment = null, int? area = null,
bool isGa = false, bool showPayment = false, bool showArea = false
) {
payment ??= delivery;
var baseline = showPayment ? payment : delivery;
if (showArea) {
return $"<td>{(area == null ? "" : $"{area:N0}")}</td>" +
$"<td>{obligation:N0}</td>" +
$"<td>{right:N0}</td>";
}
return $"<td>{(obligation == 0 ? "-" : $"{obligation:N0}")}</td>" +
$"<td>{(right == 0 ? "-" : $"{right:N0}")}</td>" +
$"<td>{(baseline < obligation ? $"<b>{obligation - baseline:N0}</b>" : "-")}</td>" +
$"<td>{(baseline >= obligation && delivery <= right ? $"{right - delivery:N0}" : "-")}</td>" +
$"<td>{(obligation == 0 && right == 0 ? "-" : (delivery > right ? ((isGa ? "<b>" : "") + $"{delivery - right:N0}" + (isGa ? "</b>" : "")) : "-"))}</td>" +
(showPayment ? $"<td>{(isGa ? "" : obligation == 0 && right == 0 ? "-" : $"{payment:N0}")}</td>" : "") +
$"<td>{delivery:N0}</td>";
}
private static string FormatRow(MemberBucket bucket, bool isGa = false, bool showPayment = false, bool showArea = false) {
return FormatRow(bucket.Obligation, bucket.Right, bucket.Delivery, bucket.Payment, bucket.Area, isGa, showPayment, showArea);
}
public string PrintBucketTable(
Season season, Dictionary<string, MemberBucket> buckets,
bool includeDelivery = true, bool includePayment = false,
bool isTiny = false, IEnumerable<string>? filter = null
) {
includePayment = includePayment && includeDelivery;
string tbl = GetColGroup(!includeDelivery ? [105, 20, 20, 20] : includePayment ? [45, 17, 17, 17, 19, 16, 17, 17] : [45, 20, 20, 20, 20, 20, 20]);
tbl += $"""
<thead>
<tr>
<th{(!includeDelivery ? " rowspan=\"2\"" : "")}>
<b>{(includeDelivery ? "Lese " + season.Year : "Zusammengefasste Flächenbindungen")}</b>
per {Date:dd.MM.yyyy} {(includeDelivery ? "[kg]" : "")}
</th>
{(!includeDelivery ? "<th>Fläche</th>" : "")}
<th>Lieferpflicht</th>
<th>Lieferrecht</th>
{(includeDelivery ? "<th>Unterliefert</th>" : "")}
{(includeDelivery ? "<th>Noch lieferbar</th>" : "")}
{(includeDelivery ? "<th>Überliefert</th>" : "")}
{(includePayment ? "<th>Zugeteilt</th>" : "")}
{(includeDelivery ? "<th>Geliefert</th>" : "")}
</tr>
{(!includeDelivery ? "<tr><th class=\"unit\">[m²]</th><th class=\"unit\">[kg]</th><th class=\"unit\">[kg]</th></tr>" : "")}
</thead>
""";
var mBuckets = buckets
.Where(b => ((!includeDelivery && b.Value.Area > 0) ||
(includeDelivery && (b.Value.Right > 0 || b.Value.Obligation > 0 || b.Value.Delivery > 0))) &&
(filter == null || filter.Contains(b.Key[..2])))
.ToList();
var fbVars = mBuckets
.Where(b => b.Value.Right > 0 || b.Value.Obligation > 0)
.Select(b => b.Key.Replace("_", ""))
.Order()
.ToArray();
var fbs = mBuckets
.Where(b => fbVars.Contains(b.Key) && b.Key.Length == 2)
.OrderBy(b => b.Value.Name);
var vtr = mBuckets
.Where(b => fbVars.Contains(b.Key) && b.Key.Length > 2)
.OrderBy(b => b.Value.Name);
var rem = mBuckets
.Where(b => !fbVars.Contains(b.Key))
.OrderBy(b => b.Value.Name);
tbl += "\n<tbody>\n";
tbl += $"<tr><th>Gesamtlieferung lt. gez. GA</th>{FormatRow(
Member.BusinessShares * season.MinKgPerBusinessShare,
Member.BusinessShares * season.MaxKgPerBusinessShare,
Member.Deliveries.Where(d => d.Year == season.Year).Sum(d => d.Weight),
isGa: true, showPayment: includePayment, showArea: !includeDelivery
)}</tr>";
if (fbs.Any()) {
tbl += $"<tr class=\"subheading{(filter == null ? " border" : "")}\"><th colspan=\"{(includePayment ? 8 : 7)}\">" +
$"Flächenbindungen{(vtr.Any() ? " (inkl. Verträge)" : "")}:</th></tr>";
foreach (var (id, b) in fbs) {
tbl += $"<tr><th>{b.Name}</th>{FormatRow(b, showPayment: includePayment, showArea: !includeDelivery)}</tr>";
}
}
if (vtr.Any()) {
tbl += $"<tr class=\"subheading{(filter == null ? " border" : "")}\"><th colspan=\"{(includePayment ? 8 : 7)}\">" +
"Verträge:</th></tr>";
foreach (var (id, b) in vtr) {
tbl += $"<tr><th>{b.Name}</th>{FormatRow(b, showPayment: includePayment, showArea: !includeDelivery)}</tr>";
}
}
tbl += "\n</tbody>\n";
return $"<table class=\"buckets {(isTiny ? "tiny" : "small")} number cohere\">\n" + tbl + "\n</table>";
}
}
}
+6 -3
View File
@@ -1,5 +1,5 @@
.address-wrapper, aside, main {
.address-wrapper, aside {
overflow: hidden;
}
@@ -64,7 +64,7 @@ aside table thead:not(:first-child) tr {
border-top: 0.5pt solid #808080;
}
aside table thead th {
aside table thead tr {
background-color: #E0E0E0;
font-size: 10pt;
}
@@ -95,7 +95,10 @@ main > *:first-child {
margin-top: 0;
}
main h1, .main-wrapper p {
main h1,
main h2,
main h3,
.main-wrapper p {
font-size: 12pt;
margin: 1em 0;
text-align: justify;
+35 -6
View File
@@ -1,31 +1,60 @@
using Elwig.Helpers;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class CreditNote : BusinessDocument {
public new static string Name => "Traubengutschrift";
public PaymentMember? Payment;
public Credit? Credit;
public CreditNoteData Data;
public string? Text;
public string CurrencySymbol;
public int Precision;
public string MemberModifier;
public IEnumerable<(string Name, int Kg, decimal Amount)>? MemberUnderDeliveries;
public CreditNote(AppDbContext ctx, PaymentMember p, CreditNoteData data) :
base($"Traubengutschrift {(p.Credit != null ? $"Nr. {p.Credit.Year}/{p.Credit.TgNr}" : p.Member.Name)} {p.Variant.Name}", p.Member) {
public CreditNote(AppDbContext ctx, PaymentMember p, CreditNoteData data, Dictionary<string, UnderDelivery>? underDeliveries = null) :
base($"{Name} {(p.Credit != null ? $"Nr. {p.Credit.Year}/{p.Credit.TgNr:000}" : p.Member.Name)} {p.Variant.Name}", p.Member) {
UseBillingAddress = true;
ShowDateAndLocation = true;
Data = data;
Payment = p;
Credit = p.Credit;
var season = p.Variant.Season;
var mod = App.Client.IsMatzen ? ctx.Modifiers.Where(m => m.Year == season.Year && m.Name.StartsWith("Treue")).FirstOrDefault() : null;
if (mod != null) {
MemberModifier = $"{mod.Name} ({mod.ValueStr})";
} else {
MemberModifier = "Sonstige Zu-/Abschläge";
}
Aside = Aside.Replace("</table>", "") +
$"<thead><tr><th colspan='2'>Gutschrift</th></tr></thead><tbody>" +
$"<tr><th>TG-Nr.</th><td>{(p.Credit != null ? $"{p.Credit.Year}/{p.Credit.TgNr}" : "-")}</td></tr>" +
$"<tr><th>TG-Nr.</th><td>{(p.Credit != null ? $"{p.Credit.Year}/{p.Credit.TgNr:000}" : "-")}</td></tr>" +
$"<tr><th>Überw. am</th><td>{p.Variant.TransferDate:dd.MM.yyyy}</td></tr>" +
$"<tr><th>Datum/Zeit</th><td>{p.Credit?.ModifiedTimestamp:dd.MM.yyyy} / {p.Credit?.ModifiedTimestamp:HH:mm}</td></tr>" +
$"</tbody></table>";
Text = App.Client.TextDeliveryNote;
DocumentId = $"Tr.-Gutschr. " + (p.Credit != null ? $"{p.Credit.Year}/{p.Credit.TgNr}" : p.MgNr);
CurrencySymbol = p.Variant.Season.Currency.Symbol ?? p.Variant.Season.Currency.Code;
Precision = p.Variant.Season.Precision;
DocumentId = $"Tr.-Gutschr. " + (p.Credit != null ? $"{p.Credit.Year}/{p.Credit.TgNr:000}" : p.MgNr);
CurrencySymbol = season.Currency.Symbol ?? season.Currency.Code;
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();
}
}}
+110 -48
View File
@@ -1,3 +1,4 @@
@using Elwig.Helpers
@using RazorLight
@inherits TemplatePage<Elwig.Documents.CreditNote>
@model Elwig.Documents.CreditNote
@@ -8,84 +9,145 @@
<table class="credit">
<colgroup>
<col style="width: 25mm;"/>
<col style="width: 5mm;"/>
<col style="width: 5mm;"/>
<col style="width: 20mm;"/>
<col style="width: 20mm;"/>
<col style="width: 18mm;"/>
<col style="width: 10mm;"/>
<col style="width: 10mm;"/>
<col style="width: 15mm;"/>
<col style="width: 12mm;"/>
<col style="width: 15mm;"/>
<col style="width: 10mm;"/>
<col style="width: 10mm;"/>
<col style="width: 15mm;"/>
<col style="width: 17mm;"/>
<col style="width: 16mm;"/>
</colgroup>
<thead>
<tr>
<th rowspan="3" style="text-align: left;">Lieferschein-Nr.</th>
<th rowspan="3">Pos.</th>
<th rowspan="3" style="text-align: left;">Sorte</th>
<th rowspan="3" style="text-align: left;">Attribut</th>
<th rowspan="2" colspan="2">Gradation</th>
<th rowspan="2" colspan="2">Flächenbindung</th>
<th rowspan="2">Preis</th>
<th colspan="2">Zu-/Abschläge</th>
<th rowspan="2">Betrag</th>
<th rowspan="2" style="text-align: left;">Lieferschein-Nr.</th>
<th rowspan="2" class="narrow">Pos.</th>
<th rowspan="2" style="text-align: left;">Sorte</th>
<th rowspan="2" style="text-align: left;">Attribut</th>
<th colspan="2">Gradation</th>
<th colspan="2">Flächenbindung</th>
<th>Preis</th>
<th class="narrow">Zu-/Abschläge</th>
<th>Betrag</th>
</tr>
<tr>
<th>Rel.</th>
<th>Abs.</th>
</tr>
<tr>
<th>[°Oe]</th>
<th>[°KMW]</th>
<th colspan="2">[kg]</th>
<th>[@Model.CurrencySymbol/kg]</th>
<th>[%]</th>
<th>[@Model.CurrencySymbol/kg]</th>
<th>[@Model.CurrencySymbol]</th>
<th class="unit">[°Oe]</th>
<th class="unit narrow">[°KMW]</th>
<th class="unit" colspan="2">[kg]</th>
<th class="unit">[@Model.CurrencySymbol/kg]</th>
<th class="unit">[@Model.CurrencySymbol]</th>
<th class="unit">[@Model.CurrencySymbol]</th>
</tr>
</thead>
<tbody>
<tbody class="sum">
@foreach (var p in Model.Data.Rows) {
var rows = Math.Max(p.Buckets.Length, p.Modifiers.Length + 1);
var first = true;
//var pmt = p.Payment;
var abs = 0; // pmt?.ModAbs == null || pmt?.ModAbs == 0 ? "-" : pmt?.ModAbs.ToString("0." + string.Concat(Enumerable.Repeat('0', Model.Precision)));
var rel = 0; // pmt?.ModRel == null || pmt?.ModRel == 0 ? "-" : $"{pmt?.ModRel * 100:0.00##}";
@for (int i = 0; i < rows; i++) {
<tr class="@(first ? "first" : "") @(rows > i + 1 ? "trailing" : "")">
@if (first) {
<td rowspan="@rows" class="lsnr">@p.LsNr</td>
<td rowspan="@rows" class="dpnr">@p.DPNr</td>
<td class="variant small">@p.Variant</td>
<td class="attribute small">@p.Attribute</td>
<td rowspan="@rows" class="oe">@($"{p.Gradation.Oe:N0}")</td>
<td rowspan="@rows" class="kmw">@($"{p.Gradation.Kmw:N1}")</td>
<tr class="@(i == 0 ? "first" : "") @(rows == i + 1 ? "last" : "")">
@if (i == 0) {
<td rowspan="@rows">@p.LsNr</td>
<td rowspan="@rows">@p.DPNr</td>
<td class="small">@p.Variant</td>
<td class="small">@p.Attribute</td>
<td rowspan="@rows" class="center">@($"{p.Gradation.Oe:N0}")</td>
<td rowspan="@rows" class="center">@($"{p.Gradation.Kmw:N1}")</td>
}
@if (i > 0 && i <= p.Modifiers.Length) {
<td colspan="2" class="mod">@(p.Modifiers[i - 1])</td>
<td colspan="2" class="small mod">@p.Modifiers[i - 1]</td>
} else if (i > 0) {
<td colspan="2"></td>
}
@if (i < p.Buckets.Length) {
var bucket = p.Buckets[i];
<td class="geb small">@bucket.Name:</td>
<td class="weight">@($"{bucket.Value:N0}")</td>
<td class="price">@($"{bucket.Price:N4}")</td>
<td class="small">@bucket.Name:</td>
<td class="number">@($"{bucket.Value:N0}")</td>
<td class="number">@($"{bucket.Price:N4}")</td>
} else {
<td colspan="3"></td>
}
@if (first) {
<td rowspan="@rows" class="rel">@rel</td>
<td rowspan="@rows" class="abs">@abs</td>
<!-- FIXME rel/abs mods -->
<td rowspan="@rows" class="amount">@($"{p.Buckets.Sum(b => b.Amount):N2}")</td>
first = false;
@if (i == p.Buckets.Length - 1) {
var totalMod = p.TotalModifiers ?? 0;
<td class="number@(totalMod == 0 ? " center" : "")">@(totalMod == 0 ? "-" : Utils.GetSign(totalMod) + $"{Math.Abs(totalMod):N2}")</td>
<td class="number">@($"{p.Amount:N2}")</td>
} else {
<td colspan="2"></td>
}
</tr>
}
}
</tbody>
</table>
<div class="hint">
Hinweis:<br/>
Die Summe der Lieferungen und die Summe der anfallenden Pönalen werden mit
@Model.Payment?.Variant.Season.Precision Nachkommastellen berechnent,
erst das Ergebnis wird kaufmännisch auf 2 Nachkommastellen gerundet.
</div>
<table class="credit-sum">
@{
string FormatRow(string name, decimal? value, bool add = false, bool bold = false, bool subCat = false, bool noTopBorder = false) {
return $"<tr class=\"{(!add && !noTopBorder ? "sum" : !add ? "large" : "")} {(bold ? "large bold" : "")}\">"
+ $"<td class=\"{(subCat ? "small" : "")}\" style=\"overflow: visible;\">{name}:</td>"
+ $"<td class=\"number {(subCat ? "small" : "large")}\">{(value < 0 ? "" : (add ? "+" : ""))}</td>"
+ $"<td class=\"number {(subCat ? "small" : "large")}\">"
+ $"<span class=\"fleft\">{Model.CurrencySymbol}</span>{Math.Abs(value ?? 0):N2}</td>"
+ $"</tr>\n";
}
}
<tbody style="break-inside: avoid;">
@{ var sum = Model.Data.Rows.Sum(p => p.Amount); }
@if (Model.Payment == null) {
@Raw(FormatRow("Gesamt", sum, bold: true, noTopBorder: true))
} else {
var noBorder = true;
if (Model.Payment.NetAmount != Model.Payment.Amount) {
@Raw(FormatRow("Zwischensumme", Model.Payment.NetAmount, noTopBorder: noBorder))
noBorder = false;
@Raw(FormatRow(Model.MemberModifier, Model.Payment.Amount - Model.Payment.NetAmount, add: true))
}
if (Model.Credit == null) {
@Raw(FormatRow("Gesamtbetrag", Model.Payment.Amount, bold: true, noTopBorder: noBorder))
// TODO Mock VAT
} else {
var hasPrev = Model.Credit.PrevNetAmount != null;
@Raw(FormatRow(hasPrev ? "Gesamtbetrag" : "Nettobetrag", Model.Credit.NetAmount, bold: true))
if (hasPrev) {
@Raw(FormatRow("Bisher berücksichtigt", -Model.Credit.PrevNetAmount, add: true))
@Raw(FormatRow("Nettobetrag", Model.Credit.NetAmount - (Model.Credit.PrevNetAmount ?? 0)))
}
@Raw(FormatRow($"Mehrwertsteuer ({Model.Credit.Vat * 100} %)", Model.Credit.VatAmount, add: true))
@Raw(FormatRow("Bruttobetrag", Model.Credit.GrossAmount, bold: true))
}
}
</tbody>
<tbody style="break-inside: avoid;">
@{ decimal penalty = 0; }
@if (Model.MemberUnderDeliveries != null && Model.MemberUnderDeliveries.Count() > 0) {
<tr class="small">
<td colspan="2" style="padding-top: 5mm;">Anfallende Pönalen durch Unterlieferungen:</td>
<td></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>
</main>
+30 -51
View File
@@ -1,69 +1,48 @@
table.credit {
font-size: 10pt;
margin-bottom: 0;
}
table.credit th,
table.credit td {
padding: 0 0.25mm;
table.credit .mod {
padding-left: 5mm;
}
table.credit thead {
font-size: 8pt
}
table.credit thead th {
font-weight: normal;
font-style: italic;
vertical-align: bottom;
}
table.credit td {
vertical-align: top;
}
table.credit .oe,
table.credit .kmw {
text-align: center;
}
table.credit .dpnr {
text-align: center;
}
table.credit .amount,
table.credit .weight,
table.credit .price {
text-align: right;
}
table.credit .rel,
table.credit .abs {
text-align: center;
}
table.credit .amount.sum {
vertical-align: bottom;
padding-bottom: 1mm;
}
table.credit tbody tr:not(.first):not(.last) {
table.credit tbody tr:not(.first) {
break-before: avoid;
}
table.credit tbody tr:not(.last) {
break-after: avoid;
}
table.credit tbody tr.first td {
padding-top: 1mm;
table.credit tr:not(.first) td {
padding-top: 0;
}
table.credit tbody tr.last td {
padding-bottom: 1mm;
table.credit tr.last td {
padding-bottom: 0;
}
table.credit tbody tr.new {
border-top: 0.5pt solid black;
table.credit-sum {
width: 50%;
margin-left: 50%;
}
table.credit .small {
table.credit-sum tr.sum,
table.credit-sum tr .sum {
font-size: 12pt;
}
table.credit-sum tr.sum td,
table.credit-sum td.sum {
padding-top: 1mm !important;
}
.hint {
font-style: italic;
font-size: 8pt;
width: 74mm;
position: absolute;
left: 0;
margin: 2mm 4mm;
}
+4 -2
View File
@@ -7,13 +7,15 @@ using System.Collections.Generic;
namespace Elwig.Documents {
public class DeliveryConfirmation : BusinessDocument {
public new static string Name => "Anlieferungsbestätigung";
public Season Season;
public DeliveryConfirmationData Data;
public string? Text = App.Client.TextDeliveryConfirmation;
public Dictionary<string, (string, int, int, int, int)> MemberBuckets;
public Dictionary<string, MemberBucket> MemberBuckets;
public DeliveryConfirmation(AppDbContext ctx, int year, Member m, DeliveryConfirmationData data) :
base($"Anlieferungsbestätigung {year}", m) {
base($"{Name} {year}", m) {
Season = ctx.Seasons.Find(year) ?? throw new ArgumentException("invalid season");
ShowDateAndLocation = true;
UseBillingAddress = true;
+21 -98
View File
@@ -1,3 +1,4 @@
@using Elwig.Documents
@using RazorLight
@inherits TemplatePage<Elwig.Documents.DeliveryConfirmation>
@model Elwig.Documents.DeliveryConfirmation
@@ -8,7 +9,7 @@
<table class="delivery-confirmation">
<colgroup>
<col style="width: 25mm;"/>
<col style="width: 5mm;"/>
<col style="width: 5mm;"/>
<col style="width: 20mm;"/>
<col style="width: 21mm;"/>
<col style="width: 19mm;"/>
@@ -22,7 +23,7 @@
<thead>
<tr>
<th rowspan="2" style="text-align: left;">Lieferschein-Nr.</th>
<th rowspan="2">Pos.</th>
<th rowspan="2" class="narrow">Pos.</th>
<th rowspan="2" style="text-align: left;">Sorte</th>
<th rowspan="2" style="text-align: left;">Attribut</th>
<th rowspan="2" style="text-align: left;">Qualitätsstufe</th>
@@ -32,11 +33,11 @@
<th>Davon<br/>abzuwerten</th>
</tr>
<tr>
<th>[°Oe]</th>
<th>[°KMW]</th>
<th colspan="2">[kg]</th>
<th>[kg]</th>
<th>[kg]</th>
<th class="unit">[°Oe]</th>
<th class="unit narrow">[°KMW]</th>
<th class="unit" colspan="2">[kg]</th>
<th class="unit">[kg]</th>
<th class="unit">[kg]</th>
</tr>
</thead>
<tbody>
@@ -47,128 +48,50 @@
var rows = Math.Max(p.Buckets.Length, p.Modifiers.Length + 1);
var first = true;
@for (int i = 0; i < rows; i++) {
<tr class="@(first ? "first" : "") @(p.Variant != lastVariant && lastVariant != "" ? "new": "") @(rows > i + 1 ? "trailing" : "")">
<tr class="@(first ? "first" : "") @(p.Variant != lastVariant && lastVariant != "" ? "new": "") @(rows > i + 1 ? "last" : "")">
@if (first) {
<td rowspan="@rows">@p.LsNr</td>
<td rowspan="@rows">@p.DPNr</td>
<td class="small">@p.Variant</td>
<td class="small">@p.Attribute</td>
<td class="small">@p.QualityLevel</td>
<td rowspan="@rows" class="grad">@($"{p.Gradation.Oe:N0}")</td>
<td rowspan="@rows" class="grad">@($"{p.Gradation.Kmw:N1}")</td>
<td rowspan="@rows" class="center">@($"{p.Gradation.Oe:N0}")</td>
<td rowspan="@rows" class="center">@($"{p.Gradation.Kmw:N1}")</td>
}
@if (i > 0 && i <= p.Modifiers.Length) {
<td colspan="3" class="mod">@(p.Modifiers[i - 1])</td>
<td colspan="3" class="small mod">@(p.Modifiers[i - 1])</td>
} else if (i > 0) {
<td colspan="3"></td>
}
@if (i < p.Buckets.Length) {
var bucket = p.Buckets[i];
<td class="geb">@bucket.Name:</td>
<td class="weight">@($"{bucket.Value:N0}")</td>
<td class="small">@bucket.Name:</td>
<td class="number">@($"{bucket.Value:N0}")</td>
} else {
<td colspan="2"></td>
}
@if (i == p.Buckets.Length - 1) {
<td class="weight">@($"{p.Weight:N0}")</td>
<td class="number">@($"{p.Weight:N0}")</td>
} else {
<td></td>
}
@if (first) {
<td rowspan="@rows" class="weight"></td>
<td rowspan="@rows" class="number"></td>
first = false;
}
</tr>
lastVariant = p.Variant;
}
lastVariant = p.Variant;
}
<tr class="sum">
<tr class="sum bold">
<td colspan="8">Gesamt:</td>
<td colspan="2" class="weight">@($"{Model.Data.Rows.Sum(p => p.Weight):N0}")</td>
<td colspan="2" class="number">@($"{Model.Data.Rows.Sum(p => p.Weight):N0}")</td>
<td></td>
</tr>
</tbody>
</table>
<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.Season.Year</b> per @($"{Model.Date:dd.MM.yyyy}") [kg]</th>
<th>Lieferpflicht</th>
<th>Lieferrecht</th>
<th>Unterliefert</th>
<th>Noch lieferbar</th>
<th>Überliefert</th>
<th>Zugeteilt</th>
<th>Geliefert</th>
</tr>
</thead>
<tbody>
@{
string FormatRow(int mode, int obligation, int right, int sum, int? payment = null) {
var isGa = mode == 0;
payment ??= sum;
return $"<td>{(mode == 1 ? "" : obligation == 0 ? "-" : $"{obligation:N0}")}</td>" +
$"<td>{(mode == 1 ? "" : right == 0 ? "-" : $"{right:N0}")}</td>" +
$"<td>{(mode == 1 ? "" : payment < obligation ? $"<b>{obligation - payment:N0}\x3c/b>" : "-")}</td>" +
$"<td>{(mode == 1 ? "" : payment >= obligation && sum <= right ? $"{right - sum:N0}" : "-")}</td>" +
$"<td>{(mode == 1 ? "" : obligation == 0 && right == 0 ? "-" : (sum > right ? ((isGa ? "<b>" : "") + $"{sum - right:N0}" + (isGa ? "</b>" : "")) : "-"))}</td>" +
$"<td>{(mode != 2 ? "" : obligation == 0 && right == 0 ? "-" : $"{payment:N0}")}</td>" +
$"<td>{sum:N0}</td>";
}
var mBuckets = Model.MemberBuckets.Where(b => b.Value.Item2 > 0 || b.Value.Item3 > 0 || b.Value.Item4 > 0).ToList();
var fbVars = mBuckets.Where(b => b.Value.Item2 > 0 || b.Value.Item3 > 0).Select(b => b.Key.Replace("_", "")).Order().ToArray();
var fbs = mBuckets.Where(b => fbVars.Contains(b.Key) && b.Key.Length == 2).OrderBy(b => b.Value.Item1);
var vtr = mBuckets.Where(b => fbVars.Contains(b.Key) && b.Key.Length > 2).OrderBy(b => b.Value.Item1);
var rem = mBuckets.Where(b => !fbVars.Contains(b.Key)).OrderBy(b => b.Value.Item1);
}
<tr>
<th>Gesamtlieferung lt. gez. GA</th>
@Raw(FormatRow(
0,
Model.Member.BusinessShares * Model.Season.MinKgPerBusinessShare,
Model.Member.BusinessShares * Model.Season.MaxKgPerBusinessShare,
Model.Member.Deliveries.Where(d => d.Year == Model.Season.Year).Sum(d => d.Weight)
))
</tr>
@if (rem.Any()) {
<tr class="subheading"><th colspan="8">Sortenaufteilung:</th></tr>
}
@foreach (var (id, (name, right, obligation, sum, payment)) in rem) {
<tr>
<th>@name</th>
@Raw(FormatRow(1, obligation, right, sum, payment))
</tr>
}
@if (fbs.Any()){
<tr class="subheading"><th colspan="8">Flächenbindungen:</th></tr>
}
@foreach (var (id, (name, right, obligation, sum, payment)) in fbs) {
<tr>
<th>@name</th>
@Raw(FormatRow(2, obligation, right, sum, payment))
</tr>
}
@if (vtr.Any()) {
<tr class="subheading"><th colspan="8">Verträge:</th></tr>
}
@foreach (var (id, (name, right, obligation, sum, payment)) in vtr) {
<tr>
<th>@name</th>
@Raw(FormatRow(2, obligation, right, sum, payment))
</tr>
}
</tbody>
</table>
@Raw(BusinessDocument.PrintSortenaufteilung(Model.MemberBuckets))
@Raw(Model.PrintBucketTable(Model.Season, Model.MemberBuckets, includePayment: true))
<div class="text" style="margin-top: 2em;">
@if (Model.Text != null) {
<p class="comment" style="white-space: pre-wrap; break-inside: avoid;">@Model.Text</p>
+5 -86
View File
@@ -1,52 +1,8 @@
table.delivery-confirmation {
font-size: 10pt;
margin-bottom: 5mm;
}
table.delivery-confirmation thead {
font-size: 8pt;
}
table.delivery-confirmation thead th {
font-weight: normal;
font-style: italic;
}
table.delivery-confirmation td {
overflow: hidden;
white-space: nowrap;
}
table.delivery-confirmation td[rowspan] {
vertical-align: top;
}
table.delivery-confirmation .weight {
text-align: right;
}
table.delivery-confirmation .grad {
text-align: center;
}
table.delivery-confirmation .geb {
font-size: 8pt;
}
table.delivery-confirmation .mod {
font-size: 8pt;
padding-left: 5mm;
}
table.delivery-confirmation .small {
font-size: 8pt;
}
table.delivery-confirmation tr.new td {
border-top: 0.5pt solid black;
}
table.delivery-confirmation tr:not(.first) {
break-before: avoid;
}
@@ -55,14 +11,11 @@ table.delivery-confirmation tr:not(.first) td {
padding-top: 0;
}
table.delivery-confirmation tr.trailing td {
table.delivery-confirmation tr.last td {
padding-bottom: 0;
}
table.delivery-confirmation tr.sum {
border-top: 0.5pt solid black;
break-before: avoid;
font-weight: bold;
font-size: 12pt;
}
@@ -70,44 +23,10 @@ table.delivery-confirmation tr.sum td {
padding-top: 1mm;
}
table.delivery-confirmation-stats {
font-size: 10pt;
break-inside: avoid;
table.sortenaufteilung {
break-after: avoid;
}
table.delivery-confirmation-stats th,
table.delivery-confirmation-stats td {
padding: 0.125mm 0;
overflow: hidden;
white-space: nowrap;
}
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;
}
table.delivery-confirmation-stats tr.subheading th {
font-weight: bold;
border-top: 0.5pt solid black;
table.buckets {
break-before: avoid;
}
+18 -18
View File
@@ -10,9 +10,9 @@
<colgroup>
<col style="width: 25mm;"/>
<col style="width: 5mm;"/>
<col style="width: 17mm;"/>
<col style="width: 10mm;"/>
<col style="width: 15mm;"/>
<col style="width: 8mm;"/>
<col style="width: 12mm;"/>
<col style="width: 38mm;"/>
<col style="width: 28mm;"/>
<col style="width: 10mm;"/>
@@ -22,7 +22,7 @@
<thead>
<tr>
<th rowspan="2" style="text-align: left;">Lieferschein-Nr.</th>
<th rowspan="2">Pos.</th>
<th rowspan="2" class="narrow">Pos.</th>
<th rowspan="2">Datum</th>
<th rowspan="2">Zeit</th>
<th colspan="2" rowspan="2" style="text-align: left;">Mitglied</th>
@@ -31,9 +31,9 @@
<th>Gewicht</th>
</tr>
<tr>
<th>[°Oe]</th>
<th>[°KMW]</th>
<th>[kg]</th>
<th class="unit">[°Oe]</th>
<th class="unit narrow">[°KMW]</th>
<th class="unit">[kg]</th>
</tr>
</thead>
<tbody>
@@ -41,26 +41,26 @@
<tr>
<td>@p.Delivery.LsNr</td>
<td>@p.DPNr</td>
<td>@($"{p.Delivery.Date:dd.MM.yyyy}")</td>
<td>@($"{p.Delivery.Time:HH:mm}")</td>
<td class="mgnr">@p.Delivery.Member.MgNr</td>
<td>@p.Delivery.Member.AdministrativeName</td>
<td>@p.Variant.Name</td>
<td class="grad">@($"{p.Oe:N0}")</td>
<td class="grad">@($"{p.Kmw:N1}")</td>
<td class="weight">@($"{p.Weight:N0}")</td>
<td class="small">@($"{p.Delivery.Date:dd.MM.yyyy}")</td>
<td class="small">@($"{p.Delivery.Time:HH:mm}")</td>
<td class="number">@p.Delivery.Member.MgNr</td>
<td class="small">@p.Delivery.Member.AdministrativeName</td>
<td class="small">@p.Variant.Name</td>
<td class="center">@($"{p.Oe:N0}")</td>
<td class="center">@($"{p.Kmw:N1}")</td>
<td class="number">@($"{p.Weight:N0}")</td>
</tr>
}
<tr class="sum">
<tr class="sum bold">
@{
var kmw = Elwig.Helpers.Utils.AggregateDeliveryPartsKmw(Model.Deliveries);
var oe = Elwig.Helpers.Utils.KmwToOe(kmw);
}
<td colspan="2">Gesamt:</td>
<td colspan="5">(Teil-)Lieferungen: @($"{Model.Deliveries.DistinctBy(p => p.Delivery).Count():N0}") (@($"{Model.Deliveries.Count():N0}"))</td>
<td class="grad">@($"{oe:N0}")</td>
<td class="grad">@($"{kmw:N1}")</td>
<td class="weight">@($"{Model.Deliveries.Sum(p => p.Weight):N0}")</td>
<td class="center">@($"{oe:N0}")</td>
<td class="center">@($"{kmw:N1}")</td>
<td class="number">@($"{Model.Deliveries.Sum(p => p.Weight):N0}")</td>
</tr>
</tbody>
</table>
-35
View File
@@ -10,38 +10,3 @@ h2 {
font-size: 14pt;
margin-top: 2mm;
}
table.journal {
font-size: 10pt;
}
table.journal thead {
font-size: 8pt;
}
table.journal th {
font-weight: normal;
font-style: italic;
}
table.journal td {
overflow: hidden;
white-space: nowrap;
}
table.journal .mgnr,
table.journal .weight {
text-align: right;
}
table.journal .grad {
text-align: center;
}
table.journal tr.sum {
font-weight: bold;
}
table.journal tr.sum td {
border-top: 0.5pt solid black;
}
+1 -1
View File
@@ -7,7 +7,7 @@ namespace Elwig.Documents {
public Delivery Delivery;
public string? Text;
public Dictionary<string, (string, int, int, int, int)> MemberBuckets;
public Dictionary<string, MemberBucket> MemberBuckets;
// 0 - none
// 1 - GA only
+19 -70
View File
@@ -5,7 +5,7 @@
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\DeliveryNote.css" />
<main>
<h1>@Model.Title</h1>
<table class="delivery">
<table class="delivery large">
<colgroup>
<col style="width: 10.00mm;"/>
<col style="width: 21.25mm;"/>
@@ -19,7 +19,7 @@
</colgroup>
<thead>
<tr>
<th class="main" rowspan="2" style="text-align: center;">Pos.</th>
<th class="main center narrow" rowspan="2">Pos.</th>
<th class="main" rowspan="2" colspan="2">Sorte</th>
<th class="main" rowspan="2" colspan="2">Attribut</th>
<th class="main" rowspan="2">Qualitätsstufe</th>
@@ -27,21 +27,21 @@
<th>Gewicht</th>
</tr>
<tr>
<th style="font-size: 8pt;">[°Oe]</th>
<th style="font-size: 8pt;">[°KMW]</th>
<th style="font-size: 8pt;">[kg]</th>
<th class="unit">[°Oe]</th>
<th class="unit narrow">[°KMW]</th>
<th class="unit">[kg]</th>
</tr>
</thead>
<tbody>
@foreach (var part in Model.Delivery.Parts.OrderBy(p => p.DPNr)) {
<tr class="main">
<td style="text-align: center;">@part.DPNr</td>
<td class="center">@part.DPNr</td>
<td colspan="2">@part.Variant.Name</td>
<td colspan="2">@part.Attribute?.Name</td>
<td>@part.Quality.Name</td>
<td class="narrow" style="text-align: center;">@($"{part.Oe:N0}")</td>
<td class="narrow" style="text-align: center;">@($"{part.Kmw:N1}")</td>
<td class="narrow" style="text-align: right;">@($"{part.Weight:N0}")</td>
<td class="center">@($"{part.Oe:N0}")</td>
<td class="center">@($"{part.Kmw:N1}")</td>
<td class="number">@($"{part.Weight:N0}")</td>
</tr>
<tr><td></td><td colspan="5" style="white-space: pre;"><i>Herkunft:</i> @part.OriginString</td></tr>
@if (part.Modifiers.Count() > 0) {
@@ -63,11 +63,11 @@
}
}
@if (Model.Delivery.Parts.Count() > 1) {
<tr class="main sum">
<tr class="main sum bold">
<td colspan="6">Gesamt:</td>
<td style="text-align: center;">@($"{Model.Delivery.Oe:N0}")</td>
<td style="text-align: center;">@($"{Model.Delivery.Kmw:N1}")</td>
<td style="text-align: right;">@($"{Model.Delivery.Weight:N0}")</td>
<td class="center">@($"{Model.Delivery.Oe:N0}")</td>
<td class="center">@($"{Model.Delivery.Kmw:N1}")</td>
<td class="number">@($"{Model.Delivery.Weight:N0}")</td>
</tr>
}
</tbody>
@@ -76,63 +76,12 @@
<p class="comment">Amerkung zur Lieferung: @Model.Delivery.Comment</p>
}
@if (Model.DisplayStats > 0) {
<table class="delivery-note-stats @(Model.DisplayStats > 2 ? "expanded" : "")">
<colgroup>
<col style="width: 45mm;"/>
<col style="width: 20mm;"/>
<col style="width: 20mm;"/>
<col style="width: 20mm;"/>
<col style="width: 20mm;"/>
<col style="width: 20mm;"/>
<col style="width: 20mm;"/>
</colgroup>
<thead>
<tr>
<th><b>Lese @Model.Delivery.Year</b> per @($"{Model.Date:dd.MM.yyyy}") [kg]</th>
<th>Lieferpflicht</th>
<th>Lieferrecht</th>
<th>Unterliefert</th>
<th>Noch lieferbar</th>
<th>Überliefert</th>
<th>Geliefert</th>
</tr>
</thead>
<tbody>
@{
string FormatRow(int obligation, int right, int sum) {
return $"<td>{obligation:N0}</td>" +
$"<td>{right:N0}</td>" +
$"<td>{(sum < obligation ? $"{obligation - sum:N0}" : "-")}</td>" +
$"<td>{(sum >= obligation && sum <= right ? $"{right - sum:N0}" : "-")}</td>" +
$"<td>{(sum > right ? $"{sum - right:N0}" : "-")}</td>" +
$"<td>{sum:N0}</td>";
}
var sortids = Model.Delivery.Parts.Select(p => p.SortId).ToList();
var buckets = Model.MemberBuckets.GroupBy(b => b.Key[..2]).ToDictionary(g => g.Key, g => g.Count());
}
<tr>
<th>Gesamtlieferung lt. gez. GA</th>
@Raw(FormatRow(
Model.Member.BusinessShares * Model.Delivery.Season.MinKgPerBusinessShare,
Model.Member.BusinessShares * Model.Delivery.Season.MaxKgPerBusinessShare,
Model.Member.Deliveries.Where(d => d.Year == Model.Delivery.Year).Sum(d => d.Weight)
))
</tr>
@if (Model.DisplayStats > 1) {
<tr class="subheading">
<th>Flächenbindungen:</th>
</tr>
@foreach (var (id, (name, right, obligation, sum, _)) in Model.MemberBuckets.OrderBy(b => b.Key)) {
if (right > 0 || obligation > 0 || (sum > 0 && buckets[id[..2]] > 1 && !id.EndsWith('_'))) {
<tr class="@(sortids.Contains(id[..2]) ? "" : "optional")">
<th>@name</th>
@Raw(FormatRow(obligation, right, sum))
</tr>
}
}
}
</tbody>
</table>
@Raw(Model.PrintBucketTable(
Model.Delivery.Season, Model.MemberBuckets, isTiny: true,
filter: Model.DisplayStats > 2 ? null :
Model.DisplayStats == 1 ? new List<string>() :
Model.Delivery.Parts.Select(p => p.SortId).Distinct().ToList()
))
}
</main>
@for (int i = 0; i < 2; i++) {
-51
View File
@@ -11,12 +11,6 @@ table.delivery tr:not(.main) {
break-before: avoid;
}
table.delivery th {
font-weight: normal;
font-style: italic;
font-size: 10pt;
}
table.delivery th.main {
text-align: left;
}
@@ -43,55 +37,10 @@ table.delivery tr.tight:has(+ tr:not(.tight)) td {
padding-bottom: 0.5mm !important;
}
table.delivery tr.sum {
border-top: 0.5pt solid black;
break-before: avoid;
}
table.delivery tr.sum td {
padding-top: 1mm;
}
table.delivery-note-stats {
font-size: 8pt;
break-inside: avoid;
break-after: avoid;
}
table.delivery-note-stats th,
table.delivery-note-stats td {
padding: 0.125mm 0;
}
table.delivery-note-stats:not(.expanded) tr.optional {
display: none;
}
table.delivery-note-stats tr.subheading th {
text-align: left;
}
table.delivery-note-stats.expanded tr.subheading:not(:has(~ tr)),
table.delivery-note-stats tr.subheading:not(:has(~ tr:not(.optional))) {
display: none;
}
table.delivery-note-stats thead th {
font-weight: normal;
font-style: italic;
text-align: right;
}
table.delivery-note-stats thead th:first-child {
text-align: left;
}
table.delivery-note-stats td {
text-align: right;
}
table.delivery-note-stats tbody th {
font-weight: normal;
font-style: italic;
text-align: left;
}
+5
View File
@@ -68,4 +68,9 @@ hr.page-break {
.page::after {
content: "Seite " counter(page) " von " counter(pages) !important;
}
a {
text-decoration: inherit;
color: inherit;
}
}
+153
View File
@@ -0,0 +1,153 @@
main table {
border-collapse: collapse;
margin-bottom: 10mm;
font-size: 10pt;
}
main table.large {font-size: 12pt;}
main table.tiny {
font-size: 8pt;
margin-bottom: 5mm;
}
main table.border {
border: 0.5pt solid black;
}
main table tr {
break-inside: avoid;
}
main table th,
main table td {
overflow: hidden;
white-space: nowrap;
vertical-align: middle;
padding: 0.5mm 1mm;
font-weight: normal;
}
main table.small th,
main table.small td,
main table.tiny th,
main table.tiny td {
padding: 0.125mm 0.125mm;
}
main table td[rowspan] {
vertical-align: top;
}
main table thead {
font-size: 8pt;
}
main table.large thead {
font-size: 10pt;
}
main table th {
font-style: italic;
}
main table tbody {
}
main table .small {
font-size: 8pt;
}
main table .large {
font-size: 12pt;
}
main table.number td,
main table.number th {
padding-left: 0;
padding-right: 0;
}
main table.number thead th,
main table.number td,
main table tbody td.number {
text-align: right;
}
main table.center tbody td,
main table tbody td.center {
text-align: center;
}
main table tbody th {
text-align: left;
}
main table.cohere {
break-inside: avoid;
}
main table tr.subheading th,
main table tr.subheading td {
font-weight: bold;
}
main table tr.subheading th {
text-align: left;
font-size: 10pt;
}
main table.small tr.subheading th,
main table.small tr.subheading td,
main table.tiny tr.subheading th,
main table.tiny tr.subheading td {
font-size: 8pt;
}
main table tr.sectionheading {
background-color: #E0E0E0;
}
main table tr.sectionheading th {
padding-top: 0.5mm;
padding-bottom: 0.5mm;
font-weight: bold;
text-align: center;
font-size: 10pt;
border-top: 0.5pt solid black;
}
main table.number thead tr:first-child th:first-child {
text-align: left;
}
main table tr.bold td {
font-weight: bold;
}
main table tr.sum,
main table td.sum,
main table tr.new,
main table tr.border {
border-top: 0.5pt solid black;
}
main table th.unit {
font-size: 8pt;
}
main table.number th.unit {
padding-right: 2mm;
}
main table th.narrow {
padding-left: 0;
padding-right: 0;
}
main table .lborder {border-left: 0.5pt solid black;}
main table .rborder {border-right: 0.5pt solid black;}
main table .fleft {
float: left;
}
main tbody.sum tr:last-child {
border-bottom: 0.5pt solid black;
}
+10 -4
View File
@@ -9,6 +9,10 @@ using Elwig.Helpers.Printing;
namespace Elwig.Documents {
public abstract partial class Document : IDisposable {
public static string Name => "Dokument";
private static readonly double GenerationProportion = 0.125;
private TempFile? _pdfFile = null;
public bool ShowFoldMarks = App.Config.Debug;
@@ -33,7 +37,9 @@ namespace Elwig.Documents {
Footer = Utils.GenerateFooter("<br/>", " \u00b7 ")
.Item(c.NameFull).NextLine()
.Item(c.Address).Item($"{c.Plz} {c.Ort}").Item("Österreich").Item("Tel.", c.PhoneNr).Item("Fax", c.FaxNr).NextLine()
.Item(c.EmailAddress).Item(c.Website).Item("Betriebs-Nr.", c.LfbisNr).Item("UID", c.UstIdNr).NextLine()
.Item(c.EmailAddress != null ? $"<a href=\"mailto:{c.Name} {c.NameSuffix} <{c.EmailAddress}>\">{c.EmailAddress}</a>" : null)
.Item(c.Website != null ? $"<a href=\"http://{c.Website}/\">{c.Website}</a>" : null)
.Item("Betriebs-Nr.", c.LfbisNr).Item("UID", c.UstIdNr).NextLine()
.Item("BIC", c.Bic).Item("IBAN", c.Iban)
.ToString();
Date = DateTime.Today;
@@ -91,10 +97,10 @@ namespace Elwig.Documents {
await File.WriteAllTextAsync(tmpHtml.FilePath, await doc.Render(), Utils.UTF8);
tmpHtmls.Add(tmpHtml);
i++;
progress?.Report(25.0 * i / n);
progress?.Report(GenerationProportion * 100 * i / n);
}
progress?.Report(25.0);
await Pdf.Convert(tmpHtmls.Select(f => f.FileName), pdf.FileName, DoubleSided, new Progress<double>(v => progress?.Report(25.0 + v * 0.75)));
progress?.Report(GenerationProportion * 100);
await Pdf.Convert(tmpHtmls.Select(f => f.FileName), pdf.FileName, DoubleSided, new Progress<double>(v => progress?.Report(GenerationProportion * 100 + v * (1 - GenerationProportion))));
foreach (var tmp in tmpHtmls) {
tmp.Dispose();
}
+1
View File
@@ -9,6 +9,7 @@
<meta charset="UTF-8"/>
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\Document.css"/>
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\Document.Page.css"/>
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\Document.Table.css"/>
@if (Model.DoubleSided) {
<style>
@@page :left {
+5 -5
View File
@@ -6,16 +6,16 @@ using System.Collections.Generic;
namespace Elwig.Documents {
public class MemberDataSheet : BusinessDocument {
public new static string Name => "Stammdatenblatt";
public Season Season;
public int Year = Utils.CurrentYear;
public Dictionary<string, (string, int, int, int, int)> MemberBuckets;
public Dictionary<string, int> BucketAreas;
public Dictionary<string, MemberBucket> MemberBuckets;
public MemberDataSheet(Member m, AppDbContext ctx) : base($"Stammdatenblatt {m.AdministrativeName}", m) {
DocumentId = $"Stammdatenblatt {m.MgNr}";
public MemberDataSheet(Member m, AppDbContext ctx) : base($"{Name} {m.AdministrativeName}", m) {
DocumentId = $"{Name} {m.MgNr}";
Season = ctx.Seasons.Find(Year) ?? throw new ArgumentException("invalid season");
MemberBuckets = ctx.GetMemberBuckets(Year, m.MgNr).GetAwaiter().GetResult();
BucketAreas = ctx.GetMemberBucketAreas(Year, m.MgNr).GetAwaiter().GetResult();
}
}
}
+136 -247
View File
@@ -1,4 +1,5 @@
@using RazorLight
@using Elwig.Helpers
@inherits TemplatePage<Elwig.Documents.MemberDataSheet>
@model Elwig.Documents.MemberDataSheet
@{
@@ -7,166 +8,146 @@
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\MemberDataSheet.css" />
<main>
<h1>@Model.Title</h1>
<table class="member">
<thead>
<tr><th colspan="4">Persönliche Daten</th></tr>
</thead>
<table class="member border">
<colgroup>
<col style="width: 30.0mm;"/>
<col style="width: 51.5mm;"/>
<col style="width: 20.0mm;"/>
<col style="width: 12.0mm;"/>
<col style="width: 18.0mm;"/>
<col style="width: 31.5mm;"/>
</colgroup>
<tbody>
<tr class="sectionheading"><th colspan="6">Persönliche Daten</th></tr>
<tr>
<th>Mitglieds-Nr.</th>
<th class="small">Titel (vorangestellt)</th>
<th class="small">Vorname</th>
<th colspan="3" class="small">Nachname</th>
<th class="small">Titel (nachgestellt)</th>
</tr>
<tr>
<td class="large">@Model.Member.Prefix</td>
<td class="large">@Model.Member.GivenName @Model.Member.MiddleName</td>
<td class="large" colspan="3">@Model.Member.FamilyName</td>
<td class="large">@Model.Member.Suffix</td>
</tr>
<tr>
<th>Mitglieds-Nr.:</th>
<td>@Model.Member.MgNr</td>
<th>Vorgänger MgNr.</th>
<td>@Model.Member.PredecessorMgNr</td>
<th colspan="2">Geburtsjahr/-tag:</th>
<td colspan="2">@(string.Join('.', Model.Member.Birthday?.Split('-')?.Reverse() ?? Array.Empty<string>()))</td>
</tr>
<tr>
<th>Titel (vorangestellt)</th>
<td>@Model.Member.Prefix</td>
<th>Titel (nachgestellt)</th>
<td>@Model.Member.Suffix</td>
<th>Adresse:</th>
<td colspan="5">@Model.Member.Address</td>
</tr>
<tr>
<th>Vorname</th>
<td>@Model.Member.GivenName</td>
<th>Nachname</th>
<td>@Model.Member.FamilyName</td>
<th>PLZ/Ort:</th>
<td colspan="5">
@Model.Member.PostalDest.AtPlz?.Plz
@Model.Member.PostalDest.AtPlz?.Dest
(@Model.Member.PostalDest.AtPlz?.Ort.Name)
</td>
</tr>
<tr class="sectionheading"><th colspan="6">Rechnungsadresse (optional)</th></tr>
<tr>
<th>Name:</th>
<td colspan="5">@Model.Member.BillingAddress?.Name</td>
</tr>
<tr>
<th>Weitere Vornamen</th>
<td colspan="3">@Model.Member.MiddleName</td>
<th>Adresse:</th>
<td colspan="5">@Model.Member.BillingAddress?.Address</td>
</tr>
<tr>
<th>Geburtsjahr/-tag</th>
<td colspan="3">@(string.Join('.', Model.Member.Birthday?.Split('-')?.Reverse()??Array.Empty<string>()))</td>
</tr>
</tbody>
<thead>
<tr><th colspan="4">Anschrift</th></tr>
</thead>
<tbody>
<tr>
<th>Adresse</th>
<td colspan="3">@Model.Member.Address</td>
</tr>
<tr>
<th>PLZ/Ort</th>
<td colspan="3">@Model.Member.PostalDest.AtPlz?.Plz @Model.Member.PostalDest.AtPlz?.Dest (@Model.Member.PostalDest.AtPlz?.Ort.Name)</td>
</tr>
</tbody>
<thead>
<tr><th colspan="4">Rechnungsadresse</th></tr>
</thead>
<tbody>
<tr>
<th>Name</th>
<td colspan="3">@Model.Member.BillingAddress?.Name</td>
</tr>
<tr>
<th>Adresse</th>
<td colspan="3">@Model.Member.BillingAddress?.Address</td>
</tr>
<tr>
<th>PLZ/Ort</th>
<td colspan="3">@Model.Member.BillingAddress?.PostalDest.AtPlz?.Plz @Model.Member.BillingAddress?.PostalDest.AtPlz?.Dest @((Model.Member.BillingAddress != null && Model.Member.BillingAddress.PostalDest.AtPlz != null) ? "(" + Model.Member.BillingAddress?.PostalDest.AtPlz?.Ort.Name + ")" : "")</td>
</tr>
</tbody>
<thead>
<tr><th colspan="4">Kontaktdaten</th></tr>
</thead>
<tbody>
@foreach (var e in Model.Member.EmailAddresses) {
var emailNumber = Model.Member.EmailAddresses.Count > 1 ? 1 : 0;
<tr>
<th>E-Mail-Adresse @(emailNumber != 0 ? $"({emailNumber})" : "")</th>
<td colspan="3">@e.Address</td>
</tr>
emailNumber++;
}
@foreach (var k in Model.Member.TelephoneNumbers) {
<tr>
@if (k.Type.Equals("landline")) {
<th>Tel.-Nr. (Festnetz)</th>
} else if (k.Type.Equals("mobile")) {
<th>Tel.-Nr. (mobil)</th>
} else if (k.Type.Equals("fax")) {
<th>Fax-Nr.</th>
<th>PLZ/Ort:</th>
<td colspan="5">
@if (Model.Member.BillingAddress != null) {
@Model.Member.BillingAddress.PostalDest.AtPlz?.Plz
@Model.Member.BillingAddress.PostalDest.AtPlz?.Dest
@("(")@Model.Member.BillingAddress.PostalDest.AtPlz?.Ort.Name@(")")
}
<td colspan="3">@k.Number</td>
</td>
</tr>
<tr class="sectionheading">
<th colspan="3">Kontaktdaten</th>
<th colspan="3" class="lborder">Bankverbindung</th>
</tr>
@{
List<string?[]> subTbl1 = new();
subTbl1.AddRange(Model.Member.EmailAddresses.Select(a => new[] { "E-Mail-Adresse", a.Address }));
subTbl1.AddRange(Model.Member.TelephoneNumbers.Select(n => new[] { Utils.PhoneNrTypeToString(n.Type), n.Number, n.Comment }));
subTbl1.Add(new[] { "Tel.-Nr./E-Mail-Adr.", null });
List<string?[]> subTbl2 = new();
subTbl2.Add(new[] { "IBAN", Model.Member.Iban != null ? Elwig.Helpers.Utils.FormatIban(Model.Member.Iban) : null });
subTbl2.Add(new[] { "BIC", Model.Member.Bic });
}
@for (int i = 0; i < Math.Max(subTbl1.Count, subTbl2.Count); i++) {
<tr>
<th>@(i < subTbl1.Count ? subTbl1[i][0] + ":" : "")</th>
@if (i < subTbl1.Count && subTbl1[i].Length >= 3 && subTbl1[i][2] != null) {
<td>@subTbl1[i][1]</td>
<td>(@subTbl1[i][2])</td>
} else {
<td colspan="2">@(i < subTbl1.Count ? subTbl1[i][1] : "")</td>
}
<th class="lborder">@(i < subTbl2.Count ? subTbl2[i][0] + ":" : "")</th>
<td colspan="2">@(i < subTbl2.Count ? subTbl2[i][1] : "")</td>
</tr>
}
</tbody>
<thead>
<tr><th colspan="4">Bankverbindung</th></tr>
</thead>
<tbody>
<tr class="sectionheading"><th colspan="6">Betrieb</th></tr>
<tr>
<th>IBAN</th>
<td colspan="3">@(Model.Member.Iban != null ? Elwig.Helpers.Utils.FormatIban(Model.Member.Iban) : "")</td>
</tr>
<tr>
<th>BIC</th>
<td colspan="3">@Model.Member.Bic</td>
</tr>
</tbody>
<thead>
<tr><th colspan="4">Betrieb</th></tr>
</thead>
<tbody>
<tr>
<th>UID</th>
<td>@Model.Member.UstIdNr</td>
<th>Betriebs-Nr.</th>
<th>Betriebs-Nr.:</th>
<td>@Model.Member.LfbisNr</td>
<th colspan="2">UID:</th>
<td colspan="2">@Model.Member.UstIdNr</td>
</tr>
<tr>
<th>Buchführend</th>
<td>@(Model.Member.IsBuchführend ? "Ja" : "Nein")</td>
<th>Bio</th>
<td>@(Model.Member.IsOrganic ? "Ja" : "Nein")</td>
</tr>
</tbody>
<thead>
<tr><th colspan="4">Genossenschaft</th></tr>
</thead>
<tbody>
<tr>
<th>Eintritt</th>
<td>@Model.Member.EntryDate</td>
<th>Austritt</th>
<td>@Model.Member.ExitDate</td>
</tr>
<tr>
<th>Aktiv</th>
<td>@(Model.Member.IsActive ? "Ja" : "Nein")</td>
<th>Geschäftsanteile</th>
<td>@Model.Member.BusinessShares</td>
</tr>
<tr>
<th>Volllierferant</th>
<td>@(Model.Member.IsVollLieferant ? "Ja" : "Nein")</td>
<th>Funktionär</th>
<td>@(Model.Member.IsFunktionär ? "Ja" : "Nein")</td>
</tr>
<tr>
<th>Stamm-Zweigstelle</th>
<td>@Model.Member.Branch?.Name</td>
<th>Stammgemeinde</th>
<th>Stammgemeinde:</th>
<td>@Model.Member.DefaultKg?.Name</td>
<th colspan="2">Buchführend:</th>
<td colspan="2">@(Model.Member.IsBuchführend ? "Ja" : "Nein")</td>
</tr>
<tr>
<th>Kontakt via Post</th>
<td>@(Model.Member.ContactViaPost ? "Ja" : "Nein")</td>
<th>Kontakt via E-Mail</th>
<td>@(Model.Member.ContactViaEmail ? "Ja" : "Nein")</td>
<th colspan="2" class="small">(Katastralgemeinde mit dem größten Anteil an Weinbauflächen)</th>
<th colspan="2">Bio:</th>
<td colspan="2">@(Model.Member.IsOrganic ? "Ja" : "Nein")</td>
</tr>
<tr class="sectionheading"><th colspan="6">Genossenschaft</th></tr>
<tr>
<th>Status:</th>
<td>
@(Model.Member.IsActive ? "Aktiv" : "Nicht aktiv")
<span class="small">
(@(Model.Member.ExitDate != null ?
$"{Model.Member.EntryDate:dd.MM.yyyy}{Model.Member.ExitDate:dd.MM.yyyy}" :
$"seit {Model.Member.EntryDate:dd.MM.yyyy}"))
</span>
</td>
<th colspan="2">Geschäftsanteile:</th>
<td colspan="2">@Model.Member.BusinessShares</td>
</tr>
<tr>
<th>Stamm-Zweigstelle:</th>
<td>@Model.Member.Branch?.Name</td>
<th colspan="2">Volllierferant:</th>
<td colspan="2">@(Model.Member.IsVollLieferant ? "Ja" : "Nein")</td>
</tr>
<tr>
<th>Zusendungen via...</th>
<td>
<i>Post:</i> @(Model.Member.ContactViaPost ? "Ja" : "Nein")
<i>E-Mail:</i> @(Model.Member.ContactViaEmail ? "Ja" : "Nein")
</td>
<th colspan="2">Funktionär:</th>
<td colspan="2">@(Model.Member.IsFunktionär ? "Ja" : "Nein")</td>
</tr>
</tbody>
</table>
@Raw(Model.PrintBucketTable(Model.Season, Model.MemberBuckets, includeDelivery: false))
@{
var areaComs = Model.Member.ActiveAreaCommitments.GroupBy(a => a.AreaComType).Select(group => new {
AreaComType = group.Key,
@@ -177,15 +158,15 @@
}
@if (areaComs.Count != 0) {
<h1>Flächenbindungen</h1>
<h2>Flächenbindungen per @($"{Model.Date:dd.MM.yyyy}")</h2>
<table class="area-commitements">
<colgroup>
<col style="width: 40mm;" />
<col style="width: 30mm;" />
<col style="width: 35mm;" />
<col style="width: 15mm;" />
<col style="width: 25mm;" />
<col style="width: 20mm;" />
<col style="width: 40mm;"/>
<col style="width: 30mm;"/>
<col style="width: 35mm;"/>
<col style="width: 15mm;"/>
<col style="width: 25mm;"/>
<col style="width: 20mm;"/>
</colgroup>
<thead>
<tr>
@@ -201,124 +182,32 @@
</tr>
</thead>
<tbody>
@foreach (var contractType in areaComs) {
<tr class="contract-type @(contractType.AreaComType.DisplayName != lastContract && lastContract != "" ? "new" : "")">
<th colspan="3" style="text-align: left">
<tr class="subheading @(contractType.AreaComType.DisplayName != lastContract && lastContract != "" ? "new" : "")">
<th colspan="3">
@($"{contractType.AreaComType.WineVar.Name} {(contractType.AreaComType.WineAttr != null ? "(" + contractType.AreaComType.WineAttr + ")" : "")}")
</th>
<th style="text-align: right">@($"{contractType.Size:N0}")</th>
<th colspan="2"></th>
<td class="number">@($"{contractType.Size:N0}")</td>
<td colspan="2"></td>
</tr>
@foreach (var areaCom in contractType.AreaComs) {
<tr class="area-commitment">
<td>@areaCom.Kg.AtKg.Name <span style="font-size: 8pt;">(@areaCom.Kg.AtKg.KgNr)</span></td>
<td>@areaCom.Kg.AtKg.Name <span style="font-size: 8pt;">(@($"{areaCom.Kg.AtKg.KgNr:00000}"))</span></td>
<td>@areaCom.Rd?.Name</td>
<td>@areaCom.GstNr.Replace(",", ", ")</td>
<td style="text-align: right;">@($"{areaCom.Area:N0}")</td>
<td style="text-align: center;">@areaCom.WineCult.Name</td>
<td style="text-align: center;">Ab @areaCom.YearFrom</td>
<td class="text">@areaCom.GstNr.Replace(",", ", ").Replace("-", "")</td>
<td class="number">@($"{areaCom.Area:N0}")</td>
<td class="center">@areaCom.WineCult.Name</td>
<td class="center">@(areaCom.YearTo == null ? $"ab {areaCom.YearFrom}" : $"{areaCom.YearFrom}{areaCom.YearTo}")</td>
</tr>
lastContract = contractType.AreaComType.DisplayName;
}
}
<tr class="new">
<th colspan="3" style="text-align: left">Gesamt</th>
<th style="text-align: right">@($"{Model.Member.ActiveAreaCommitments.Select(a => a.Area).Sum():N0}")</th>
<th colspan="2"></th>
</tr>
</tbody>
</table>
<table class="delivery-confirmation-stats">
<colgroup>
<col style="width: 102mm;" />
<col style="width: 21mm;" />
<col style="width: 21mm;" />
<col style="width: 21mm;" />
</colgroup>
<thead>
<tr>
<th rowspan="2"></th>
<th style="text-align: center;">Fläche</th>
<th style="text-align: center;">Lieferpflicht</th>
<th style="text-align: center;">Lieferrecht</th>
</tr>
<tr>
<th style="text-align: center;">[m²]</th>
<th style="text-align: center;">[kg]</th>
<th style="text-align: center;">[kg]</th>
</tr>
</thead>
<tbody>
@{
string FormatRow(int mode, int area, int obligation, int right) {
return $"<td>{(mode == 0 || mode == 1 ? "" : area == 0 ? "-" : $"{area:N0}")}</td>" +
$"<td>{(mode == 1 ? "" : obligation == 0 ? "-" : $"{obligation:N0}")}</td>" +
$"<td>{(mode == 1 ? "" : right == 0 ? "-" : $"{right:N0}")}</td>";
}
var mBuckets = Model.MemberBuckets.Where(b => b.Value.Item2 > 0 || b.Value.Item3 > 0 || b.Value.Item4 > 0).ToList();
var fbVars = mBuckets.Where(b => b.Value.Item2 > 0 || b.Value.Item3 > 0).Select(b => b.Key.Replace("_", "")).Order().ToArray();
var fbs = mBuckets.Where(b => fbVars.Contains(b.Key) && b.Key.Length == 2).OrderBy(b => b.Value.Item1);
var vtr = mBuckets.Where(b => fbVars.Contains(b.Key) && b.Key.Length > 2).OrderBy(b => b.Value.Item1);
}
<tr>
<th>Laut gezeichneten GA</th>
@Raw(FormatRow(
0,
0,
Model.Member.BusinessShares * Model.Season.MinKgPerBusinessShare,
Model.Member.BusinessShares * Model.Season.MaxKgPerBusinessShare
))
</tr>
@if (fbs.Any()) {
<tr class="subheading"><th colspan="8">Flächenbindungen:</th></tr>
}
@foreach (var (id, (name, right, obligation, _, _)) in fbs) {
<tr>
<th>@name</th>
@Raw(FormatRow(2, Model.BucketAreas[id], obligation, right))
</tr>
}
@if (vtr.Any()) {
<tr class="subheading"><th colspan="8">Verträge:</th></tr>
}
@foreach (var (id, (name, right, obligation, _, _)) in vtr) {
<tr>
<th>@name</th>
@Raw(FormatRow(2, Model.BucketAreas[id], obligation, right))
</tr>
}
</tbody>
</table>
} else {
<table class="delivery-confirmation-stats" style="margin-top: 5mm;">
<colgroup>
<col style="width: 123mm;" />
<col style="width: 21mm;" />
<col style="width: 21mm;" />
</colgroup>
<thead>
<tr>
<th rowspan="2"></th>
<th style="text-align: center;">Lieferpflicht</th>
<th style="text-align: center;">Lieferrecht</th>
</tr>
<tr>
<th style="text-align: center;">[kg]</th>
<th style="text-align: center;">[kg]</th>
</tr>
</thead>
<tbody>
<tr>
<th>Laut gezeichneten GA</th>
<td>@($"{Model.Member.BusinessShares * Model.Season.MinKgPerBusinessShare:N0}")</td>
<td>@($"{Model.Member.BusinessShares * Model.Season.MaxKgPerBusinessShare:N0}")</td>
<tr class="sum bold">
<td colspan="3">Gesamt:</td>
<td class="number">@($"{Model.Member.ActiveAreaCommitments.Select(a => a.Area).Sum():N0}")</td>
<td colspan="2"></td>
</tr>
</tbody>
</table>
}
</main>
+11 -69
View File
@@ -1,82 +1,24 @@
table.member {
border: 0.5pt solid black;
h2 {
margin-bottom: 0.5em !important;
}
table.member, table.area-commitements {
font-size: 10pt;
table.member {
margin-bottom: 5mm;
}
table.member tbody th {
font-weight: normal;
font-style: italic;
text-align: left;
}
table.member thead,
table.member tbody {
border: 0.5pt solid black;
table.area-commitements {
margin-top: 0;
}
table.area-commitements td {
overflow: hidden;
}
table.area-commitements thead tr th {
font-weight: normal;
font-style: italic;
}
table.area-commitements tbody tr.contract-type {
margin-bottom: 10px;
}
table.area-commitements tbody td {
vertical-align: top;
}
table.area-commitements tr.new th {
border-top: 0.5pt solid black;
table.area-commitements td.text {
white-space: normal;
}
table.delivery-confirmation-stats {
font-size: 10pt;
break-inside: avoid;
table.area-commitements tr.sum {
font-size: 12pt;
}
table.delivery-confirmation-stats th,
table.delivery-confirmation-stats td {
padding: 0.125mm 0;
overflow: hidden;
white-space: nowrap;
}
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: 10pt;
}
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;
}
table.delivery-confirmation-stats tr.subheading th {
font-weight: bold;
border-top: 0.5pt solid black;
}
+12 -9
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<PreserveCompilationContext>true</PreserveCompilationContext>
@@ -14,6 +14,8 @@
<ItemGroup>
<Resource Include="Resources\Images\Elwig.png" />
<Content Include="Resources\Images\Elwig.ico" />
<EmbeddedResource Include="Resources\Schemas\PaymentVariantData.json" />
<EmbeddedResource Include="Resources\Sql\*" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
@@ -22,16 +24,17 @@
<ItemGroup>
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.1" />
<PackageReference Include="ini-parser" Version="2.5.2" />
<PackageReference Include="LinqKit" Version="1.2.4" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.24" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2088.41" />
<PackageReference Include="LinqKit" Version="1.2.5" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.25" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2151.40" />
<PackageReference Include="NJsonSchema" Version="11.0.0" />
<PackageReference Include="RazorLight" Version="2.3.1" />
<PackageReference Include="ScottPlot.WPF" Version="4.1.68" />
<PackageReference Include="System.IO.Ports" Version="7.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
</ItemGroup>
</Project>
+97 -42
View File
@@ -10,8 +10,14 @@ using Microsoft.Data.Sqlite;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using Elwig.Models.Dtos;
using System.Reflection;
namespace Elwig.Helpers {
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 class AppDbContext : DbContext {
public DbSet<Country> Countries { get; private set; }
@@ -58,12 +64,14 @@ namespace Elwig.Helpers {
public DateTime SavedLastWriteTime { get; private set; }
public bool HasBackendChanged => SavedLastWriteTime != LastWriteTime;
public static string ConnectionString => $"Data Source=\"{App.Config.DatabaseFile}\"; Foreign Keys=True; Mode=ReadWrite; Cache=Default";
public static string? ConnectionStringOverride { get; set; } = null;
public static string ConnectionString => ConnectionStringOverride ?? $"Data Source=\"{App.Config.DatabaseFile}\"; Mode=ReadWrite; Foreign Keys=True; Cache=Default";
private readonly Dictionary<int, Dictionary<int, Dictionary<string, (int, int)>>> _memberRightsAndObligations = new();
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBuckets = new();
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberPaymentBuckets = new();
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberBucketAreas = new();
private readonly Dictionary<int, Dictionary<int, Dictionary<string, AreaComBucket>>> _memberAreaCommitmentBuckets = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBuckets = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBucketsStrict = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberPaymentBuckets = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, UnderDelivery>>> _memberUnderDelivery = [];
public AppDbContext() {
if (App.Config.DatabaseLog != null) {
@@ -94,6 +102,24 @@ namespace Elwig.Helpers {
return cnx;
}
public static async Task ExecuteBatch(SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
await (await cmd.ExecuteReaderAsync()).CloseAsync();
}
public static async Task ExecuteEmbeddedScript(SqliteConnection cnx, Assembly asm, string name) {
using var stream = asm.GetManifestResourceStream(name) ?? throw new FileNotFoundException("Unable to load embedded resource");
using var reader = new StreamReader(stream);
await ExecuteBatch(cnx, await reader.ReadToEndAsync());
}
public static async Task<object?> ExecuteScalar(SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
return await cmd.ExecuteScalarAsync();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
optionsBuilder.UseSqlite(ConnectionString);
optionsBuilder.UseLazyLoadingProxies();
@@ -174,6 +200,13 @@ namespace Elwig.Helpers {
return c + 1;
}
public async Task<int> NextAvNr(int year) {
int c = 0;
(await PaymentVariants.Where(v => v.Year == year).Select(v => v.AvNr).ToListAsync())
.ForEach(a => { if (a <= c + 100) c = a; });
return c + 1;
}
public async Task<WineQualLevel> GetWineQualityLevel(double kmw) {
return await WineQualityLevels
.Where(q => !q.IsPredicate && (q.MinKmw == null || q.MinKmw <= kmw))
@@ -203,22 +236,22 @@ namespace Elwig.Helpers {
}
}
private async Task FetchMemberRightsAndObligations(int year, SqliteConnection? cnx = null) {
private async Task FetchMemberAreaCommitmentBuckets(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var buckets = new Dictionary<int, Dictionary<string, (int, int)>>();
var buckets = new Dictionary<int, Dictionary<string, AreaComBucket>>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, bucket, min_kg, max_kg FROM v_area_commitment_bucket WHERE year = {year}";
cmd.CommandText = $"SELECT mgnr, bucket, area, min_kg, max_kg FROM v_area_commitment_bucket WHERE year = {year}";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
var vtrgid = reader.GetString(1);
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = new();
buckets[mgnr][vtrgid] = (reader.GetInt32(3), reader.GetInt32(2));
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
buckets[mgnr][vtrgid] = new(reader.GetInt32(2), reader.GetInt32(3), reader.GetInt32(4));
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberRightsAndObligations[year] = buckets;
_memberAreaCommitmentBuckets[year] = buckets;
}
private async Task FetchMemberDeliveryBuckets(int year, SqliteConnection? cnx = null) {
@@ -231,7 +264,7 @@ namespace Elwig.Helpers {
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
var bucket = reader.GetString(1);
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = new();
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
buckets[mgnr][bucket] = reader.GetInt32(2);
}
}
@@ -239,6 +272,24 @@ namespace Elwig.Helpers {
_memberDeliveryBuckets[year] = buckets;
}
private async Task FetchMemberDeliveryBucketsStrict(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var buckets = new Dictionary<int, Dictionary<string, int>>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, bucket, weight FROM v_delivery_bucket_strict 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] = reader.GetInt32(2);
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberDeliveryBucketsStrict[year] = buckets;
}
private async Task FetchMemberPaymentBuckets(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
@@ -249,7 +300,7 @@ namespace Elwig.Helpers {
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
var bucket = reader.GetString(1);
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = new();
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
buckets[mgnr][bucket] = reader.GetInt32(2);
}
}
@@ -257,71 +308,75 @@ namespace Elwig.Helpers {
_memberPaymentBuckets[year] = buckets;
}
private async Task FetchMemberBucketAreas(int year, SqliteConnection? cnx = null) {
private async Task FetchMemberUnderDelivery(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var buckets = new Dictionary<int, Dictionary<string, int>>();
var buckets = new Dictionary<int, Dictionary<string, UnderDelivery>>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, bucket, area FROM v_area_commitment_bucket_strict WHERE year = {year}";
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);
var v = reader.GetInt32(2);
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = new();
buckets[mgnr][bucket] = v;
if (bucket.Length > 2) {
buckets[mgnr][bucket[..2]] = buckets[mgnr].GetValueOrDefault(bucket[..2], 0) + v;
}
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
buckets[mgnr][bucket] = new(reader.GetInt32(2), reader.GetInt32(3));
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberBucketAreas[year] = buckets;
_memberUnderDelivery[year] = buckets;
}
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, AreaComBucket>> GetMemberAreaCommitmentBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberAreaCommitmentBuckets.ContainsKey(year))
await FetchMemberAreaCommitmentBuckets(year, cnx);
return _memberAreaCommitmentBuckets[year].GetValueOrDefault(mgnr, []);
}
public async Task<Dictionary<string, int>> GetMemberDeliveryBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberDeliveryBuckets.ContainsKey(year))
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) {
if (!_memberDeliveryBucketsStrict.ContainsKey(year))
await FetchMemberDeliveryBucketsStrict(year, cnx);
return _memberDeliveryBucketsStrict[year].GetValueOrDefault(mgnr, []);
}
public async Task<Dictionary<string, int>> GetMemberPaymentBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberPaymentBuckets.ContainsKey(year))
await FetchMemberPaymentBuckets(year, cnx);
return _memberPaymentBuckets[year].GetValueOrDefault(mgnr, new());
return _memberPaymentBuckets[year].GetValueOrDefault(mgnr, []);
}
public async Task<Dictionary<string, int>> GetMemberBucketAreas(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberBucketAreas.ContainsKey(year))
await FetchMemberBucketAreas(year, cnx);
return _memberBucketAreas[year].GetValueOrDefault(mgnr, new());
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, (string, int, int, int, int)>> GetMemberBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
public async Task<Dictionary<string, MemberBucket>> GetMemberBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var rightsAndObligations = await GetMemberRightsAndObligations(year, mgnr, cnx);
var rightsAndObligations = await GetMemberAreaCommitmentBuckets(year, mgnr, cnx);
var deliveryBuckets = await GetMemberDeliveryBuckets(year, mgnr, cnx);
var deliveryBucketsStrict = await GetMemberDeliveryBucketsStrict(year, mgnr, cnx);
var paymentBuckets = await GetMemberPaymentBuckets(year, mgnr, cnx);
if (ownCnx) await cnx.DisposeAsync();
var buckets = new Dictionary<string, (string, int, int, int, int)>();
var buckets = new Dictionary<string, MemberBucket>();
foreach (var id in rightsAndObligations.Keys.Union(deliveryBuckets.Keys).Union(paymentBuckets.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 ?? "") + (attrIds == "_" ? " (kein Qual.Wein)" : attrs.Count > 0 ? $" ({string.Join(" / ", attrs.Select(a => a.Name))})" : "");
buckets[id] = (
var attribute = await WineAttributes.FindAsync(id[2..]);
var name = (variety?.Name ?? "") + (id[2..] == "_" ? " (kein Qual.Wein)" : attribute != null ? $" ({attribute})" : "");
buckets[id] = new(
name,
rightsAndObligations.GetValueOrDefault(id).Item1,
rightsAndObligations.GetValueOrDefault(id).Item2,
rightsAndObligations.GetValueOrDefault(id).Area,
rightsAndObligations.GetValueOrDefault(id).Obligation,
rightsAndObligations.GetValueOrDefault(id).Right,
deliveryBuckets.GetValueOrDefault(id),
deliveryBucketsStrict.GetValueOrDefault(id),
paymentBuckets.GetValueOrDefault(id)
);
}
+48 -675
View File
@@ -1,717 +1,90 @@
using Microsoft.Data.Sqlite;
using System;
using System.Windows;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Elwig.Helpers {
public static class AppDbUpdater {
public static readonly int RequiredSchemaVersion = 9;
// Don't forget to update value in Tests/fetch-resources.bat!
public static readonly int RequiredSchemaVersion = 12;
private static int _versionOffset = 0;
private static readonly Action<SqliteConnection>[] _updaters = new[] {
UpdateDbSchema_1_To_2, UpdateDbSchema_2_To_3, UpdateDbSchema_3_To_4, UpdateDbSchema_4_To_5,
UpdateDbSchema_5_To_6, UpdateDBSchema_6_To_7, UpdateDbSchema_7_To_8, UpdateDbSchema_8_To_9,
};
private static int VersionOffset = 0;
private static void ExecuteNonQuery(SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
}
private static object? ExecuteScalar(SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
return cmd.ExecuteScalar();
}
public static string CheckDb() {
public static async Task<string> CheckDb() {
using var cnx = AppDbContext.Connect();
var applId = (long?)ExecuteScalar(cnx, "PRAGMA application_id") ?? 0;
var applId = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA application_id") ?? 0;
if (applId != 0x454C5747) throw new Exception("Invalid application_id of database");
var schemaVers = (long?)ExecuteScalar(cnx, "PRAGMA schema_version") ?? 0;
_versionOffset = (int)(schemaVers % 100);
if (_versionOffset != 0) {
var schemaVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA schema_version") ?? 0;
VersionOffset = (int)(schemaVers % 100);
if (VersionOffset != 0) {
// schema was modified manually/externally
// TODO issue warning
}
UpdateDbSchema(cnx, (int)(schemaVers / 100), RequiredSchemaVersion);
await UpdateDbSchema(cnx, (int)(schemaVers / 100), RequiredSchemaVersion);
var userVers = (long?)ExecuteScalar(cnx, "PRAGMA user_version") ?? 0;
var userVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA user_version") ?? 0;
var major = userVers >> 24;
var minor = (userVers >> 16) & 0xFF;
var patch = userVers & 0xFFFF;
if (App.VersionMajor > major ||
(App.VersionMajor == major && App.VersionMinor > minor) ||
(App.VersionMajor == major && App.VersionMinor == minor && App.VersionPatch > patch))
{
(App.VersionMajor == major && App.VersionMinor == minor && App.VersionPatch > patch)) {
long vers = (App.VersionMajor << 24) | (App.VersionMinor << 16) | App.VersionPatch;
ExecuteNonQuery(cnx, $"PRAGMA user_version = {vers}");
await AppDbContext.ExecuteBatch(cnx, $"PRAGMA user_version = {vers}");
}
return $"{major}.{minor}.{patch}";
}
private static void UpdateDbSchema(SqliteConnection cnx, int fromVersion, int toVersion) {
private static async Task UpdateDbSchema(SqliteConnection cnx, int fromVersion, int toVersion) {
if (fromVersion == toVersion) {
return;
} else if (fromVersion > toVersion) {
throw new Exception("schema_version of database is too new");
} else if (toVersion - 1 > _updaters.Length) {
throw new Exception("Unable to update database schema: Updater not implemented");
} else if (fromVersion <= 0) {
throw new Exception("schema_version of database is invalid");
}
ExecuteNonQuery(cnx, "PRAGMA locking_mode = EXCLUSIVE");
ExecuteNonQuery(cnx, "PRAGMA foreign_keys = OFF");
ExecuteNonQuery(cnx, "BEGIN EXCLUSIVE");
for (int i = fromVersion; i < toVersion; i++) {
_updaters[i - 1](cnx);
var asm = Assembly.GetExecutingAssembly();
(int From, int To, string Name)[] scripts = asm.GetManifestResourceNames()
.Where(n => n.StartsWith("Elwig.Resources.Sql."))
.Select(n => {
var p = n.Split(".")[^2].Split("-");
return (int.Parse(p[0]), int.Parse(p[1]), n);
})
.OrderBy(s => s.Item1).ThenBy(s => s.Item2)
.ToArray();
List<string> toExecute = [];
var vers = fromVersion;
while (vers < toVersion) {
var (_, to, name) = scripts.Where(s => s.From == vers).Last();
toExecute.Add(name);
vers = to;
}
ExecuteNonQuery(cnx, "PRAGMA foreign_key_check");
ExecuteNonQuery(cnx, "COMMIT");
ExecuteNonQuery(cnx, "PRAGMA foreign_keys = ON");
ExecuteNonQuery(cnx, "VACUUM");
ExecuteNonQuery(cnx, $"PRAGMA schema_version = {toVersion * 100 + _versionOffset}");
}
if (toExecute.Count == 0)
return;
private static void UpdateDbSchema_1_To_2(SqliteConnection cnx) {
ExecuteNonQuery(cnx, "DROP VIEW v_area_commitment");
ExecuteNonQuery(cnx, "ALTER TABLE delivery_part DROP COLUMN weighing_reason");
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) {
ExecuteNonQuery(cnx, """
CREATE TABLE delivery_part_bin (
year INTEGER NOT NULL,
did INTEGER NOT NULL,
dpnr INTEGER NOT NULL,
binnr INTEGER NOT NULL,
discr TEXT NOT NULL,
value INTEGER NOT NULL,
CONSTRAINT pk_delivery_part_bin PRIMARY KEY (year, did, dpnr, binnr),
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;
await AppDbContext.ExecuteBatch(cnx, """
PRAGMA locking_mode = EXCLUSIVE;
PRAGMA foreign_keys = OFF;
BEGIN EXCLUSIVE;
""");
ExecuteNonQuery(cnx, """
INSERT INTO delivery_part_bin (year, did, dpnr, binnr, discr, value)
SELECT year, did, dpnr, 0, '_', bucket_2 + bucket_3
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, """
INSERT INTO delivery_part_bin (year, did, dpnr, binnr, discr, value)
SELECT d.year, d.did, d.dpnr, 1, COALESCE(attributes, ''), bucket_1
FROM payment_delivery_part p
JOIN v_delivery d ON (d.year, d.did, d.dpnr) = (p.year, p.did, p.dpnr)
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, "ALTER TABLE delivery_part ADD COLUMN gebunden INTEGER CHECK (gebunden IN (TRUE, FALSE)) DEFAULT NULL");
ExecuteNonQuery(cnx, "DROP VIEW v_delivery");
ExecuteNonQuery(cnx, """
CREATE VIEW v_delivery AS
SELECT s.*, GROUP_CONCAT(o.modid) AS modifiers
FROM (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.gerebelt, p.gebunden,
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(a.attrid) AS attributes,
COALESCE(SUM(a.fill_lower_bins), 0) AS attribute_prio,
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 pa ON (pa.year, pa.did, pa.dpnr) = (p.year, p.did, p.dpnr)
LEFT JOIN wine_attribute a ON a.attrid = pa.attrid
GROUP BY p.year, p.did, p.dpnr
ORDER BY p.year, p.did, p.dpnr, a.attrid) s
LEFT JOIN delivery_part_modifier o ON (o.year, o.did, o.dpnr) = (s.year, s.did, s.dpnr)
GROUP BY s.year, s.lsnr, s.dpnr
ORDER BY s.year, s.lsnr, s.dpnr, o.modid;
""");
ExecuteNonQuery(cnx, "DROP VIEW v_bucket");
ExecuteNonQuery(cnx, """
CREATE VIEW v_delivery_bin AS
SELECT year, mgnr,
sortid || IIF(min_quw, REPLACE(COALESCE(attributes, ''), ',', ''), '_') AS bin,
SUM(weight) AS weight
FROM v_delivery
GROUP BY year, mgnr, bin
ORDER BY year, mgnr, LENGTH(bin) DESC, bin;
""");
ExecuteNonQuery(cnx, """
CREATE VIEW v_payment_bin AS
SELECT d.year, d.mgnr,
sortid || discr AS bin,
SUM(value) AS weight
FROM v_delivery d
JOIN delivery_part_bin b ON (b.year, b.did, b.dpnr) = (d.year, d.did, d.dpnr)
GROUP BY d.year, d.mgnr, bin
HAVING SUM(value) > 0
ORDER BY d.year, d.mgnr, bin;
""");
ExecuteNonQuery(cnx, "ALTER TABLE wine_attribute ADD COLUMN fill_lower_bins INTEGER NOT NULL CHECK (fill_lower_bins IN (0, 1, 2)) DEFAULT 0");
}
private static void UpdateDbSchema_3_To_4(SqliteConnection cnx) {
ExecuteNonQuery(cnx, "DROP VIEW v_payment_bin");
ExecuteNonQuery(cnx, """
CREATE VIEW v_payment_bin AS
SELECT d.year, d.mgnr,
sortid || discr AS bin,
SUM(value) AS weight
FROM v_delivery d
JOIN delivery_part_bin b ON (b.year, b.did, b.dpnr) = (d.year, d.did, d.dpnr)
GROUP BY d.year, d.mgnr, bin
HAVING SUM(value) > 0
ORDER BY d.year, d.mgnr, LENGTH(bin) DESC, bin;
""");
ExecuteNonQuery(cnx, """
CREATE VIEW v_area_commitment_bin AS
SELECT s.year, c.mgnr,
c.vtrgid AS bin,
CAST(ROUND(SUM(COALESCE(area * min_kg_per_ha, 0)) / 10000.0, 0) AS INTEGER) AS min_kg,
CAST(ROUND(SUM(COALESCE(area * max_kg_per_ha, 0)) / 10000.0, 0) AS INTEGER) AS max_kg
FROM area_commitment c, season s
JOIN area_commitment_type t ON t.vtrgid = c.vtrgid
WHERE (year_from IS NULL OR year_from <= s.year) AND
(year_to IS NULL OR year_to >= s.year)
GROUP BY s.year, c.mgnr, c.vtrgid
ORDER BY s.year, c.mgnr, LENGTH(c.vtrgid) DESC, c.vtrgid;
""");
}
private static void UpdateDbSchema_4_To_5(SqliteConnection cnx) {
ExecuteNonQuery(cnx, """
CREATE TABLE _area_commitment_type (
vtrgid TEXT NOT NULL CHECK (vtrgid = sortid || COALESCE(attrid, '') || disc),
sortid TEXT NOT NULL,
attrid TEXT,
disc TEXT DEFAULT NULL CHECK (disc REGEXP '^[A-Z0-9]+$'),
min_kg_per_ha INTEGER,
max_kg_per_ha INTEGER,
penalty_amount INTEGER,
CONSTRAINT pk_area_commitment_type PRIMARY KEY (vtrgid),
CONSTRAINT sk_area_commitment_type_sort_attr UNIQUE (sortid, attrid, disc),
CONSTRAINT fk_area_commitment_type_wine_variety FOREIGN KEY (sortid) REFERENCES wine_variety (sortid)
ON UPDATE CASCADE
ON DELETE RESTRICT,
CONSTRAINT fk_area_commitment_type_wine_attribute FOREIGN KEY (attrid) REFERENCES wine_attribute (attrid)
ON UPDATE CASCADE
ON DELETE RESTRICT
) STRICT;
""");
ExecuteNonQuery(cnx, """
INSERT INTO _area_commitment_type (vtrgid, sortid, attrid, disc, min_kg_per_ha, max_kg_per_ha, penalty_amount)
SELECT vtrgid, sortid, attrid_1, disc, min_kg_per_ha, max_kg_per_ha, penalty_amount FROM area_commitment_type
""");
ExecuteNonQuery(cnx, "PRAGMA writable_schema = ON");
ExecuteNonQuery(cnx, "DROP TABLE area_commitment_type");
ExecuteNonQuery(cnx, "ALTER TABLE _area_commitment_type RENAME TO area_commitment_type");
ExecuteNonQuery(cnx, "PRAGMA writable_schema = OFF");
ExecuteNonQuery(cnx, """
ALTER TABLE delivery_part ADD COLUMN attrid TEXT DEFAULT NULL
REFERENCES wine_attribute (attrid)
ON UPDATE CASCADE
ON DELETE RESTRICT
""");
ExecuteNonQuery(cnx, """
UPDATE delivery_part
SET attrid = (SELECT attrid
FROM delivery_part_attribute a
WHERE (delivery_part.year, delivery_part.did, delivery_part.dpnr) = (a.year, a.did, a.dpnr)
ORDER BY attrid DESC
LIMIT 1)
""");
ExecuteNonQuery(cnx, "DROP TRIGGER t_delivery_part_attribute_i_mtime_delivery_part");
ExecuteNonQuery(cnx, "DROP TRIGGER t_delivery_part_attribute_u_mtime_delivery_part");
ExecuteNonQuery(cnx, "DROP TRIGGER t_delivery_part_attribute_d_mtime_delivery_part");
ExecuteNonQuery(cnx, "DROP TABLE delivery_part_attribute");
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, a.attrid,
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.gerebelt, p.gebunden,
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,
COALESCE(a.fill_lower_bins, 0) AS attribute_prio,
GROUP_CONCAT(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 wine_attribute a ON a.attrid = p.attrid
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, o.modid;
""");
ExecuteNonQuery(cnx, "DROP VIEW v_delivery_bin");
ExecuteNonQuery(cnx, """
CREATE VIEW v_delivery_bin AS
SELECT year, mgnr,
sortid || IIF(min_quw, COALESCE(attrid, ''), '_') AS bin,
SUM(weight) AS weight
FROM v_delivery
GROUP BY year, mgnr, bin
ORDER BY year, mgnr, LENGTH(bin) DESC, bin;
""");
ExecuteNonQuery(cnx, "DROP VIEW v_stat_attr");
ExecuteNonQuery(cnx, """
CREATE VIEW v_stat_attr AS
SELECT year, attrid,
SUM(weight) as sum,
ROUND(SUM(kmw * weight) / SUM(weight), 2) AS kmw,
ROUND(SUM(oe * weight) / SUM(weight), 1) AS oe,
COUNT(DISTINCT did) AS lieferungen,
COUNT(DISTINCT mgnr) AS mitglieder
FROM v_delivery
GROUP BY year, attrid
ORDER BY year, attrid;
""");
ExecuteNonQuery(cnx, "DROP VIEW v_stat_sort_attr");
ExecuteNonQuery(cnx, """
CREATE VIEW v_stat_sort_attr AS
SELECT year, sortid, attrid,
SUM(weight) as sum,
ROUND(SUM(kmw * weight) / SUM(weight), 2) AS kmw,
ROUND(SUM(oe * weight) / SUM(weight), 1) AS oe,
COUNT(DISTINCT did) AS lieferungen,
COUNT(DISTINCT mgnr) AS mitglieder
FROM v_delivery
GROUP BY year, sortid, attrid
ORDER BY year, sortid, attrid;
""");
}
private static void UpdateDbSchema_5_To_6(SqliteConnection cnx) {
ExecuteNonQuery(cnx, "DROP VIEW IF EXISTS v_area_commitment");
ExecuteNonQuery(cnx, "PRAGMA writable_schema = ON");
ExecuteNonQuery(cnx, "ALTER TABLE wine_attribute DROP COLUMN fill_lower_bins");
ExecuteNonQuery(cnx, "ALTER TABLE wine_attribute ADD COLUMN strict INTEGER NOT NULL CHECK (strict IN (TRUE, FALSE)) DEFAULT FALSE");
ExecuteNonQuery(cnx, "ALTER TABLE wine_attribute ADD COLUMN fill_lower INTEGER NOT NULL CHECK (fill_lower IN (0, 1, 2)) DEFAULT 0");
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, a.attrid,
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.gerebelt, p.gebunden,
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,
IIF(a.strict, COALESCE(a.fill_lower, 0), 0) AS attribute_prio,
GROUP_CONCAT(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 wine_attribute a ON a.attrid = p.attrid
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, o.modid;
""");
ExecuteNonQuery(cnx, "PRAGMA writable_schema = OFF");
ExecuteNonQuery(cnx, "DROP VIEW v_area_commitment_bin");
ExecuteNonQuery(cnx, "DROP VIEW v_delivery_bin");
ExecuteNonQuery(cnx, "DROP VIEW v_payment_bin");
ExecuteNonQuery(cnx, "ALTER TABLE area_commitment_type DROP COLUMN max_kg_per_ha");
ExecuteNonQuery(cnx, "ALTER TABLE area_commitment_type ADD COLUMN penalty_per_kg INTEGER DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE area_commitment_type ADD COLUMN penalty_none INTEGER DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE wine_cultivation ADD COLUMN description TEXT DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE member ADD COLUMN organic INTEGER NOT NULL CHECK (organic IN (TRUE, FALSE)) DEFAULT FALSE");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN max_kg_per_ha INTEGER NOT NULL DEFAULT 10000");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN vat_normal REAL NOT NULL DEFAULT 0.10");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN vat_flatrate REAL NOT NULL DEFAULT 0.13");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN min_kg_per_bs INTEGER NOT NULL DEFAULT 750");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN max_kg_per_bs INTEGER NOT NULL DEFAULT 3000");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN penalty_per_kg INTEGER DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN penalty_amount INTEGER DEFAULT NULL");
ExecuteNonQuery(cnx, "ALTER TABLE season ADD COLUMN penalty_none INTEGER DEFAULT NULL");
ExecuteNonQuery(cnx, "DELETE FROM client_parameter WHERE param IN ('DELIVERY_RIGHT', 'DELIVERY_OBLIGATION', 'VAT_NORMAL', 'VAT_REDUCED', 'VAT_FLATRATE')");
ExecuteNonQuery(cnx, """
CREATE TABLE delivery_part_bucket (
year INTEGER NOT NULL,
did INTEGER NOT NULL,
dpnr INTEGER NOT NULL,
bktnr INTEGER NOT NULL,
discr TEXT NOT NULL,
value INTEGER NOT NULL,
CONSTRAINT pk_delivery_part_bucket PRIMARY KEY (year, did, dpnr, bktnr),
CONSTRAINT fk_delivery_part_bucket_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_bucket (year, did, dpnr, bktnr, discr, value)
SELECT year, did, dpnr, binnr, discr, value
FROM delivery_part_bin
""");
ExecuteNonQuery(cnx, "DROP TABLE delivery_part_bin");
ExecuteNonQuery(cnx, """
CREATE VIEW v_area_commitment_bucket_strict AS
SELECT s.year, c.mgnr,
t.sortid || COALESCE(a.attrid, '') AS bucket,
t.sortid, a.attrid,
CAST(ROUND(SUM(area) * COALESCE(t.min_kg_per_ha, 0) / 10000.0, 0) AS INTEGER) AS min_kg,
CAST(ROUND(SUM(area) * MIN(COALESCE(a.max_kg_per_ha, s.max_kg_per_ha), s.max_kg_per_ha) / 10000.0, 0) AS INTEGER) AS max_kg,
CAST(ROUND(SUM(area) * s.max_kg_per_ha / 10000.0, 0) AS INTEGER) AS upper_max_kg
FROM season s, area_commitment c
JOIN area_commitment_type t ON t.vtrgid = c.vtrgid
LEFT JOIN wine_attribute a ON a.attrid = t.attrid
WHERE (year_from IS NULL OR year_from <= s.year) AND
(year_to IS NULL OR year_to >= s.year)
GROUP BY s.year, c.mgnr, bucket
ORDER BY s.year, c.mgnr, bucket;
""");
ExecuteNonQuery(cnx, """
CREATE VIEW v_area_commitment_bucket AS
SELECT year, mgnr, bucket, min_kg, max_kg
FROM v_area_commitment_bucket_strict
WHERE attrid IS NOT NULL
UNION ALL
SELECT b.year, b.mgnr, b.sortid,
SUM(b.min_kg) AS min_kg,
SUM(b.upper_max_kg) AS max_kg
FROM v_area_commitment_bucket_strict b
LEFT JOIN wine_attribute a ON a.attrid = b.attrid
WHERE a.strict IS NULL OR a.strict = FALSE
GROUP BY b.year, b.mgnr, b.sortid
ORDER BY year, mgnr, bucket;
""");
ExecuteNonQuery(cnx, """
CREATE VIEW v_delivery_bucket_strict AS
SELECT year, mgnr,
sortid || IIF(min_quw, COALESCE(attrid, ''), '_') AS bucket,
sortid, IIF(min_quw, attrid, NULL) AS attrid,
SUM(weight) AS weight,
min_quw
FROM v_delivery
GROUP BY year, mgnr, bucket
ORDER BY year, mgnr, bucket;
""");
ExecuteNonQuery(cnx, """
CREATE VIEW v_delivery_bucket AS
SELECT year, mgnr, bucket, weight
FROM v_delivery_bucket_strict
WHERE attrid IS NOT NULL OR NOT min_quw
UNION ALL
SELECT b.year, b.mgnr, b.sortid,
SUM(b.weight) AS weight
FROM v_delivery_bucket_strict b
LEFT JOIN wine_attribute a ON a.attrid = b.attrid
WHERE min_quw AND (a.strict IS NULL OR a.strict = FALSE)
GROUP BY b.year, b.mgnr, b.sortid
ORDER BY year, mgnr, bucket;
""");
ExecuteNonQuery(cnx, """
CREATE VIEW v_payment_bucket_strict AS
SELECT d.year, d.mgnr,
d.sortid || b.discr AS bucket,
d.sortid, IIF(b.discr IN ('', '_'), NULL, b.discr) AS attrid,
SUM(b.value) AS weight,
b.discr != '_' AS gebunden
FROM v_delivery d
LEFT JOIN delivery_part_bucket b ON (b.year, b.did, b.dpnr) = (d.year, d.did, d.dpnr)
GROUP BY d.year, d.mgnr, bucket
HAVING SUM(b.value) > 0
ORDER BY d.year, d.mgnr, bucket;
""");
ExecuteNonQuery(cnx, """
CREATE VIEW v_payment_bucket AS
SELECT year, mgnr, bucket, weight
FROM v_payment_bucket_strict
WHERE attrid IS NOT NULL OR NOT gebunden
UNION ALL
SELECT b.year, b.mgnr, b.sortid,
SUM(b.weight) AS weight
FROM v_payment_bucket_strict b
LEFT JOIN wine_attribute a ON a.attrid = b.attrid
WHERE gebunden AND (a.strict IS NULL OR a.strict = FALSE)
GROUP BY b.year, b.mgnr, b.sortid
ORDER BY year, mgnr, bucket;
""");
}
private static void UpdateDBSchema_6_To_7(SqliteConnection cnx) {
ExecuteNonQuery(cnx, "DROP VIEW v_area_commitment_bucket_strict");
ExecuteNonQuery(cnx, """
CREATE VIEW v_area_commitment_bucket_strict AS
SELECT s.year, c.mgnr,
t.sortid || COALESCE(a.attrid, '') AS bucket,
t.sortid, a.attrid,
SUM(area) AS area,
CAST(ROUND(SUM(area) * COALESCE(t.min_kg_per_ha, 0) / 10000.0, 0) AS INTEGER) AS min_kg,
CAST(ROUND(SUM(area) * MIN(COALESCE(a.max_kg_per_ha, s.max_kg_per_ha), s.max_kg_per_ha) / 10000.0, 0) AS INTEGER) AS max_kg,
CAST(ROUND(SUM(area) * s.max_kg_per_ha / 10000.0, 0) AS INTEGER) AS upper_max_kg
FROM season s, area_commitment c
JOIN area_commitment_type t ON t.vtrgid = c.vtrgid
LEFT JOIN wine_attribute a ON a.attrid = t.attrid
WHERE (year_from IS NULL OR year_from <= s.year) AND
(year_to IS NULL OR year_to >= s.year)
GROUP BY s.year, c.mgnr, bucket
ORDER BY s.year, c.mgnr, bucket;
""");
ExecuteNonQuery(cnx, """
CREATE VIEW v_under_delivery_bucket_strict AS
SELECT c.year, c.mgnr, c.bucket, c.min_kg, COALESCE(p.weight, 0) AS weight
FROM v_area_commitment_bucket_strict c
LEFT JOIN v_payment_bucket_strict p ON (p.year, p.mgnr, p.bucket) = (c.year, c.mgnr, c.bucket)
ORDER BY c.year, c.mgnr, c.bucket;
""");
ExecuteNonQuery(cnx, """
CREATE VIEW v_under_delivery_bucket AS
SELECT u.year, u.mgnr, u.bucket, u.min_kg,
u.weight + SUM(MAX(COALESCE(p.weight - s.min_kg, 0), 0)) AS weight
FROM v_under_delivery_bucket_strict u
LEFT JOIN v_payment_bucket_strict p ON (p.year, p.mgnr, p.sortid) = (u.year, u.mgnr, u.bucket) AND p.attrid IS NOT NULL
LEFT JOIN wine_attribute a ON a.attrid = p.attrid
LEFT JOIN v_area_commitment_bucket_strict s ON (s.year, s.mgnr, s.bucket) = (p.year, p.mgnr, p.bucket)
WHERE (p.gebunden IS NULL OR p.gebunden) AND (a.strict IS NULL OR a.strict = FALSE)
GROUP BY u.year, u.mgnr, u.bucket
ORDER BY u.year, u.mgnr, u.bucket;
""");
ExecuteNonQuery(cnx, """
CREATE VIEW v_under_delivery AS
SELECT year, mgnr, bucket, min_kg, weight, weight - min_kg AS diff
FROM v_under_delivery_bucket
WHERE diff < 0;
""");
}
private static void UpdateDbSchema_7_To_8(SqliteConnection cnx) {
ExecuteNonQuery(cnx, """
INSERT INTO wb_gem
SELECT a.gkz, 'WLNO'
FROM AT_gem a
LEFT JOIN wb_gem w ON w.gkz = a.gkz
WHERE a.gkz / 10000 = 3 AND w.hkid IS NULL;
""");
ExecuteNonQuery(cnx, """
INSERT INTO wb_gem
SELECT gkz, 'SLVL'
FROM AT_gem
WHERE gkz / 100 IN (617, 622, 623);
""");
ExecuteNonQuery(cnx, """
INSERT INTO wb_gem
SELECT gkz, 'SLSS'
FROM AT_gem
WHERE gkz / 100 = 610;
""");
ExecuteNonQuery(cnx, """
UPDATE wb_gem
SET hkid = 'SLVL'
WHERE gkz IN (61007, 61052, 61001, 61055, 61027, 61057, 61008, 61057);
""");
ExecuteNonQuery(cnx, """
INSERT INTO wb_gem
SELECT gkz, 'SLWS'
FROM AT_gem
WHERE gkz / 100 IN (603, 616) OR gkz IN (60101, 60663, 60651, 60659, 60664, 60647, 60641, 60639, 60665, 60669, 60618, 60629, 60608, 60670, 60624, 60660, 60656, 60655);
""");
ExecuteNonQuery(cnx, """
INSERT INTO wb_gem
SELECT g.gkz, 'SLVL'
FROM AT_gem g
LEFT JOIN wb_gem w ON w.gkz = g.gkz
WHERE g.gkz / 100 = 606 AND w.hkid IS NULL;
""");
ExecuteNonQuery(cnx, """
INSERT INTO wb_gem
SELECT g.gkz, 'SLST'
FROM AT_gem g
LEFT JOIN wb_gem w ON w.gkz = g.gkz
WHERE g.gkz / 10000 = 6 AND w.hkid IS NULL;
""");
ExecuteNonQuery(cnx, """
INSERT INTO wb_gem
SELECT gkz, 'BLOO'
FROM AT_gem
WHERE gkz / 10000 = 4;
""");
ExecuteNonQuery(cnx, """
INSERT INTO wb_gem
SELECT gkz, 'BLKA'
FROM AT_gem
WHERE gkz / 10000 = 2;
""");
ExecuteNonQuery(cnx, """
INSERT INTO wb_gem
SELECT gkz, 'BLSB'
FROM AT_gem
WHERE gkz / 10000 = 5;
""");
ExecuteNonQuery(cnx, """
INSERT INTO wb_gem
SELECT gkz, 'BLTI'
FROM AT_gem
WHERE gkz / 10000 = 7;
""");
ExecuteNonQuery(cnx, """
INSERT INTO wb_gem
SELECT gkz, 'BLVO'
FROM AT_gem
WHERE gkz / 10000 = 8;
""");
}
private static void UpdateDbSchema_8_To_9(SqliteConnection cnx) {
ExecuteNonQuery(cnx, """
CREATE TABLE payment_delivery_part_bucket (
year INTEGER NOT NULL,
did INTEGER NOT NULL,
dpnr INTEGER NOT NULL,
bktnr INTEGER NOT NULL,
avnr INTEGER NOT NULL,
price INTEGER NOT NULL,
amount INTEGER NOT NULL,
CONSTRAINT pk_payment_delivery_part_bucket PRIMARY KEY (year, did, dpnr, bktnr, avnr),
CONSTRAINT fk_payment_delivery_part_bucket_delivery_part_bucket FOREIGN KEY (year, did, dpnr, bktnr) REFERENCES delivery_part_bucket (year, did, dpnr, bktnr)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT fk_payment_delivery_part_bucket_payment_variant FOREIGN KEY (year, avnr) REFERENCES payment_variant (year, avnr)
ON UPDATE CASCADE
ON DELETE CASCADE
) STRICT;
""");
ExecuteNonQuery(cnx, "DROP TRIGGER IF EXISTS t_payment_delivery_part_i");
ExecuteNonQuery(cnx, "DROP TRIGGER IF EXISTS t_payment_delivery_part_u");
ExecuteNonQuery(cnx, "DROP TRIGGER IF EXISTS t_payment_delivery_part_d");
ExecuteNonQuery(cnx, "ALTER TABLE payment_delivery_part RENAME COLUMN amount TO net_amount");
ExecuteNonQuery(cnx, "ALTER TABLE payment_delivery_part ADD COLUMN amount INTEGER NOT NULL GENERATED ALWAYS AS (ROUND(net_amount * (1 + mod_rel) + mod_abs)) VIRTUAL");
ExecuteNonQuery(cnx, """
CREATE TRIGGER t_payment_delivery_part_bucket_i
AFTER INSERT ON payment_delivery_part_bucket FOR EACH ROW
BEGIN
INSERT INTO payment_delivery_part (year, did, dpnr, avnr, net_amount)
VALUES (NEW.year, NEW.did, NEW.dpnr, NEW.avnr, NEW.amount)
ON CONFLICT DO UPDATE SET net_amount = net_amount + NEW.amount;
END;
""");
ExecuteNonQuery(cnx, """
CREATE TRIGGER t_payment_delivery_part_bucket_u
AFTER UPDATE OF amount ON payment_delivery_part_bucket FOR EACH ROW
BEGIN
UPDATE payment_delivery_part
SET net_amount = net_amount - OLD.amount
WHERE (year, did, dpnr, avnr) = (NEW.year, NEW.did, NEW.dpnr, NEW.avnr);
UPDATE payment_delivery_part
SET net_amount = net_amount + NEW.amount
WHERE (year, did, dpnr, avnr) = (NEW.year, NEW.did, NEW.dpnr, NEW.avnr);
END;
""");
ExecuteNonQuery(cnx, """
CREATE TRIGGER t_payment_delivery_part_bucket_d
AFTER DELETE ON payment_delivery_part_bucket FOR EACH ROW
BEGIN
UPDATE payment_delivery_part
SET net_amount = net_amount - OLD.amount
WHERE (year, did, dpnr, avnr) = (OLD.year, OLD.did, OLD.dpnr, OLD.avnr);
END;
""");
ExecuteNonQuery(cnx, "ALTER TABLE payment_member RENAME COLUMN amount TO net_amount");
ExecuteNonQuery(cnx, "ALTER TABLE payment_member ADD COLUMN mod_abs INTEGER NOT NULL DEFAULT 0");
ExecuteNonQuery(cnx, "ALTER TABLE payment_member ADD COLUMN mod_rel REAL NOT NULL DEFAULT 0");
ExecuteNonQuery(cnx, "ALTER TABLE payment_member ADD COLUMN amount INTEGER NOT NULL GENERATED ALWAYS AS (ROUND(net_amount * (1 + mod_rel) + mod_rel)) VIRTUAL");
ExecuteNonQuery(cnx, """
CREATE TRIGGER t_payment_delivery_part_i
AFTER INSERT ON payment_delivery_part FOR EACH ROW
BEGIN
INSERT INTO payment_member (year, avnr, mgnr, net_amount)
VALUES (NEW.year, NEW.avnr, (SELECT mgnr FROM delivery WHERE (year, did) = (NEW.year, NEW.did)), NEW.amount)
ON CONFLICT DO UPDATE SET net_amount = net_amount + excluded.net_amount;
END;
""");
ExecuteNonQuery(cnx, """
CREATE TRIGGER t_payment_delivery_part_u
AFTER UPDATE OF amount ON payment_delivery_part FOR EACH ROW
BEGIN
UPDATE payment_member
SET net_amount = net_amount - OLD.amount
WHERE (year, avnr, mgnr) = (NEW.year, NEW.avnr, (SELECT mgnr FROM delivery WHERE (year, did) = (NEW.year, NEW.did)));
UPDATE payment_member
SET net_amount = net_amount + NEW.amount
WHERE (year, avnr, mgnr) = (NEW.year, NEW.avnr, (SELECT mgnr FROM delivery WHERE (year, did) = (NEW.year, NEW.did)));
END;
""");
ExecuteNonQuery(cnx, """
CREATE TRIGGER t_payment_delivery_part_d
AFTER DELETE ON payment_delivery_part FOR EACH ROW
BEGIN
UPDATE payment_member
SET net_amount = net_amount - OLD.amount
WHERE (year, avnr, mgnr) = (OLD.year, OLD.avnr, (SELECT mgnr FROM delivery WHERE (year, did) = (OLD.year, OLD.did)));
END;
foreach (var script in toExecute) {
await AppDbContext.ExecuteEmbeddedScript(cnx, asm, script);
}
await AppDbContext.ExecuteBatch(cnx, $"""
PRAGMA foreign_key_check;
COMMIT;
PRAGMA foreign_keys = ON;
VACUUM;
PRAGMA schema_version = {toVersion * 100 + VersionOffset};
""");
}
}
+27 -44
View File
@@ -22,26 +22,20 @@ namespace Elwig.Helpers.Billing {
public async Task FinishSeason() {
using var cnx = await AppDbContext.ConnectAsync();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
UPDATE season
SET (start_date, end_date) = (SELECT MIN(date), MAX(date) FROM delivery WHERE year = {Year})
WHERE year = {Year}
""";
await cmd.ExecuteNonQueryAsync();
}
await AppDbContext.ExecuteBatch(cnx, $"""
UPDATE season
SET (start_date, end_date) = (SELECT MIN(date), MAX(date) FROM delivery WHERE year = {Year})
WHERE year = {Year};
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"DELETE FROM delivery_part_bucket WHERE year = {Year}";
await cmd.ExecuteNonQueryAsync();
}
DELETE FROM delivery_part_bucket WHERE year = {Year};
""");
}
public async Task CalculateBuckets(bool allowAttrsIntoLower, bool avoidUnderDeliveries, bool honorGebunden) {
var attrVals = Context.WineAttributes.ToDictionary(a => a.AttrId, a => (a.IsStrict, a.FillLower));
var attrForced = attrVals.Where(a => a.Value.IsStrict && a.Value.FillLower == 0).Select(a => a.Key).ToArray();
using var cnx = await AppDbContext.ConnectAsync();
await Context.GetMemberRightsAndObligations(Year, 0, cnx);
await Context.GetMemberAreaCommitmentBuckets(Year, 0, cnx);
var inserts = new List<(int, int, int, string, int)>();
var deliveries = new List<(int, int, int, string, int, double, string, string?, string[], bool?)>();
@@ -66,11 +60,11 @@ namespace Elwig.Helpers.Billing {
}
int lastMgNr = 0;
Dictionary<string, (int, int)>? rightsAndObligations = null;
Dictionary<string, AreaComBucket>? rightsAndObligations = null;
Dictionary<string, int> used = new();
foreach (var (mgnr, did, dpnr, sortid, weight, kmw, qualid, attrid, modifiers, gebunden) in deliveries) {
if (lastMgNr != mgnr) {
rightsAndObligations = await Context.GetMemberRightsAndObligations(Year, mgnr);
rightsAndObligations = await Context.GetMemberAreaCommitmentBuckets(Year, mgnr);
used = new();
}
if ((honorGebunden && gebunden == false) ||
@@ -94,8 +88,8 @@ namespace Elwig.Helpers.Billing {
if (rightsAndObligations.ContainsKey(key)) {
int i = (c == 0) ? 1 : 2;
var u = used.GetValueOrDefault(key, 0);
var vr = Math.Max(0, Math.Min(rightsAndObligations[key].Item1 - u, w));
var vo = Math.Max(0, Math.Min(rightsAndObligations[key].Item2 - u, w));
var vr = Math.Max(0, Math.Min(rightsAndObligations[key].Right - u, w));
var vo = Math.Max(0, Math.Min(rightsAndObligations[key].Obligation - u, w));
var v = (attributes.Length == c || attributes.Select(a => !attrVals[a].IsStrict ? 2 : attrVals[a].FillLower).Min() == 2) ? vr : vo;
used[key] = u + v;
if (key.Length > 2 && !isStrict) used[key[..2]] = used.GetValueOrDefault(key[..2], 0) + v;
@@ -108,15 +102,12 @@ namespace Elwig.Helpers.Billing {
lastMgNr = mgnr;
}
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
VALUES {string.Join(",\n ", inserts.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '{i.Item4}', {i.Item5})"))}
ON CONFLICT DO UPDATE
SET discr = excluded.discr, value = value + excluded.value
""";
await cmd.ExecuteNonQueryAsync();
}
await AppDbContext.ExecuteBatch(cnx, $"""
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
VALUES {string.Join(",\n ", inserts.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '{i.Item4}', {i.Item5})"))}
ON CONFLICT DO UPDATE
SET discr = excluded.discr, value = value + excluded.value;
""");
if (!avoidUnderDeliveries)
return;
@@ -186,25 +177,17 @@ namespace Elwig.Helpers.Billing {
if (needed == 0) break;
}
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
VALUES {string.Join(",\n ", posChanges.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '', {i.Item4})"))}
ON CONFLICT DO UPDATE
SET value = value + excluded.value
""";
await cmd.ExecuteNonQueryAsync();
}
await AppDbContext.ExecuteBatch(cnx, $"""
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
VALUES {string.Join(",\n ", posChanges.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '', {i.Item4})"))}
ON CONFLICT DO UPDATE
SET value = value + excluded.value;
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
VALUES {string.Join(",\n ", negChanges.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '', {i.Item4})"))}
ON CONFLICT DO UPDATE
SET value = excluded.value
""";
await cmd.ExecuteNonQueryAsync();
}
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
VALUES {string.Join(",\n ", negChanges.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '', {i.Item4})"))}
ON CONFLICT DO UPDATE
SET value = excluded.value;
""");
}
}
}
+220
View File
@@ -0,0 +1,220 @@
using Newtonsoft.Json;
using NJsonSchema;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace Elwig.Helpers.Billing {
public class BillingData {
public enum CalculationMode { Elwig, WgMaster }
public enum CurveMode { Oe, Kmw }
public record struct Curve(CurveMode Mode, Dictionary<double, decimal> Normal, Dictionary<double, decimal>? Gebunden);
public static JsonSchema? Schema { get; private set; }
public static async Task Init() {
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Elwig.Resources.Schemas.PaymentVariantData.json");
Schema = await JsonSchema.FromJsonAsync(stream ?? throw new ArgumentException("JSON schema not found"));
}
private readonly CalculationMode Mode;
private readonly JsonObject Data;
private readonly Dictionary<int, Curve> Curves;
private readonly Dictionary<string, Curve> PaymentData;
private readonly Dictionary<string, Curve> QualityData;
public BillingData(JsonObject data, IEnumerable<string> attributeVariants) {
if (attributeVariants.Any(e => e.Any(c => c < 'A' || c > 'Z')))
throw new ArgumentException("Invalid attributeVariants");
Data = data;
var mode = Data["mode"]?.GetValue<string>();
Mode = (mode == "elwig") ? CalculationMode.Elwig : CalculationMode.WgMaster;
Curves = GetCurves(data);
PaymentData = GetPaymentData(attributeVariants);
QualityData = GetQualityData(attributeVariants);
}
public static JsonObject ParseJson(string json) {
if (Schema == null) throw new InvalidOperationException("Schema has to be initialized first");
try {
var errors = Schema.Validate(json);
if (errors.Count != 0) throw new ArgumentException("Invalid JSON data");
return JsonNode.Parse(json)?.AsObject() ?? throw new ArgumentException("Invalid JSON data");
} catch (JsonReaderException) {
throw new ArgumentException("Invalid JSON data");
}
}
public static BillingData FromJson(string json, IEnumerable<string> attributeVariants) {
return new(ParseJson(json), attributeVariants);
}
private static Dictionary<double, decimal> GetCurveData(JsonObject data, CurveMode mode) {
var dict = new Dictionary<double, decimal>();
foreach (var (index, price) in data) {
double idx;
bool? gtlt = index.StartsWith('>') ? true : index.StartsWith('<') ? false : null;
if (index.EndsWith("kmw")) {
idx = double.Parse(index[(gtlt != null ? 1 : 0)..^3], CultureInfo.InvariantCulture);
if (mode == CurveMode.Oe) {
idx = Utils.KmwToOe(idx);
}
} else if (index.EndsWith("oe")) {
idx = double.Parse(index[(gtlt != null ? 1 : 0)..^2], CultureInfo.InvariantCulture);
if (mode == CurveMode.Kmw) {
idx = Utils.OeToKmw(idx);
}
} else {
throw new InvalidOperationException();
}
if (gtlt == true) {
idx = Math.BitIncrement(idx);
} else if (gtlt == false) {
idx = Math.BitDecrement(idx);
}
dict[idx] = price?.AsValue().GetValue<decimal>() ?? throw new InvalidOperationException();
}
return dict;
}
public static Dictionary<int, Curve> GetCurves(JsonObject data) {
var dict = new Dictionary<int, Curve>();
var curves = data["curves"]?.AsArray() ?? throw new InvalidOperationException();
foreach (var c in curves) {
var obj = c?.AsObject() ?? throw new InvalidOperationException();
var id = obj["id"]?.GetValue<int>() ?? throw new InvalidOperationException();
var cMode = (obj["mode"]?.GetValue<string>() == "kmw") ? CurveMode.Kmw : CurveMode.Oe;
Dictionary<double, decimal> c1;
Dictionary<double, decimal>? c2 = null;
var norm = obj["data"];
if (norm is JsonObject) {
c1 = GetCurveData(norm.AsObject(), cMode);
} else if (norm?.AsValue().TryGetValue(out decimal v) == true) {
c1 = new() { { cMode == CurveMode.Oe ? 73 : 15, v } };
} else {
throw new InvalidOperationException();
}
var geb = obj["geb"];
if (geb is JsonObject) {
c2 = GetCurveData(geb.AsObject(), cMode);
} else if (geb?.AsValue().TryGetValue(out decimal v) == true) {
c2 = c1.ToDictionary(e => e.Key, e => e.Value + v);
}
dict.Add(id, new(cMode, c1, c2));
}
return dict;
}
private Dictionary<string, Curve> GetData(JsonObject data, IEnumerable<string> attributeVariants) {
Dictionary<string, Curve> dict;
if (data["default"] is JsonValue def) {
var c = LookupCurve(def);
dict = attributeVariants.ToDictionary(e => e, _ => c);
} else {
dict = [];
}
var variants = data.Where(p => !p.Key.StartsWith('/') && p.Key.Length == 2);
var attributes = data.Where(p => p.Key.StartsWith('/'));
var others = data.Where(p => !p.Key.StartsWith('/') && p.Key.Length > 2);
foreach (var (idx, v) in variants) {
var curve = LookupCurve(v?.AsValue() ?? throw new InvalidOperationException());
foreach (var i in attributeVariants.Where(e => e.StartsWith(idx[..^1]))) {
dict[i] = curve;
}
}
foreach (var (idx, v) in attributes) {
var curve = LookupCurve(v?.AsValue() ?? throw new InvalidOperationException());
foreach (var i in attributeVariants.Where(e => e[2..] == idx[1..])) {
dict[i] = curve;
}
}
foreach (var (idx, v) in others) {
var curve = LookupCurve(v?.AsValue() ?? throw new InvalidOperationException());
dict[idx.Replace("/", "")] = curve;
}
return dict;
}
public Dictionary<string, Curve> GetPaymentData(IEnumerable<string> attributeVariants) {
// TODO parse wgmaster
var p = Data["payment"];
if (p is JsonValue val) {
var c = LookupCurve(val);
return attributeVariants.ToDictionary(e => e, _ => c);
}
return GetData(p?.AsObject() ?? throw new InvalidOperationException(), attributeVariants);
}
public Dictionary<string, Curve> GetQualityData(IEnumerable<string> attributeVariants) {
// TODO parse wgmaster
var q = Data["quality"]?.AsObject();
Dictionary<string, Curve> dict = [];
if (q == null) return dict;
foreach (var (qualid, data) in q) {
Dictionary<string, Curve> qualDict;
if (data is JsonValue val) {
var c = LookupCurve(val);
qualDict = attributeVariants.ToDictionary(e => e, _ => c);
} else {
qualDict = GetData(data?.AsObject() ?? throw new InvalidOperationException(), attributeVariants);
}
foreach (var (idx, d) in qualDict) {
dict[$"{qualid}/{idx}"] = d;
}
}
return dict;
}
public decimal CalculatePrice(string sortid, string? attrid, string qualid, bool gebunden, double oe, double kmw) {
var curve = GetQualityCurve(qualid, sortid, attrid) ?? GetCurve(sortid, attrid);
var d = (gebunden ? curve.Gebunden : null) ?? curve.Normal;
if (d.Count == 1) return d.First().Value;
var r = curve.Mode == CurveMode.Oe ? oe : kmw;
var lt = d.Keys.Where(v => v <= r);
var gt = d.Keys.Where(v => v >= r);
if (!lt.Any()) {
return d[gt.Min()];
} else if (!gt.Any()) {
return d[lt.Max()];
}
var max = lt.Max();
var min = gt.Min();
if (max == min) return d[r];
var p1 = ((decimal)r - (decimal)min) / ((decimal)max - (decimal)min);
var p2 = 1 - p1;
return d[min] * p2 + d[max] * p1;
}
private Curve LookupCurve(JsonValue val) {
if (val.TryGetValue(out string? curve)) {
var curveId = int.Parse(curve.Split(":")[1]);
return Curves[curveId];
} else if (val.TryGetValue(out decimal value)) {
return new(CurveMode.Oe, new() { { 73, value } }, null);
}
throw new InvalidOperationException();
}
public Curve GetCurve(string sortid, string? attrid) {
return PaymentData[$"{sortid}{attrid ?? ""}"];
}
public Curve? GetQualityCurve(string qualid, string sortid, string? attrid) {
return QualityData.TryGetValue($"{qualid}/{sortid}{attrid ?? ""}", out var curve) ? curve : null;
}
}
}
+140 -30
View File
@@ -1,3 +1,6 @@
using Elwig.Models.Entities;
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -5,36 +8,132 @@ using System.Threading.Tasks;
namespace Elwig.Helpers.Billing {
public class BillingVariant : Billing {
private readonly int AvNr;
protected readonly int AvNr;
protected readonly PaymentVar PaymentVariant;
public BillingVariant(int year, int avnr) : base(year) {
AvNr = avnr;
PaymentVariant = Context.PaymentVariants.Find(Year, AvNr) ?? throw new ArgumentException("PaymentVar not found");
}
protected async Task DeleteInDb() {
public async Task Calculate() {
using var cnx = await AppDbContext.ConnectAsync();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"DELETE FROM payment_delivery_part_bucket WHERE (year, avnr) = ({Year}, {AvNr})";
await cmd.ExecuteNonQueryAsync();
}
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();
using var tx = await cnx.BeginTransactionAsync();
await DeleteInDb(cnx);
await SetCalcTime(cnx);
await CalculatePrices(cnx);
await CalculateModifiers(cnx);
await CalculateMemberModifiers(cnx);
await tx.CommitAsync();
}
public async Task Commit() {
await Revert();
using var cnx = await AppDbContext.ConnectAsync();
await AppDbContext.ExecuteBatch(cnx, $"""
INSERT INTO credit (year, tgnr, mgnr, avnr, net_amount, prev_net_amount, vat, modifiers, prev_modifiers)
SELECT s.year,
COALESCE(t.tgnr, 0) + ROW_NUMBER() OVER(ORDER BY m.mgnr) AS tgnr,
m.mgnr,
v.avnr,
ROUND(p.amount / POW(10, s.precision - 2)) AS net_amount,
ROUND(lp.amount / POW(10, s.precision - 2)) AS prev_amount,
IIF(m.buchführend, s.vat_normal, s.vat_flatrate) AS vat,
ROUND(COALESCE(u.total_penalty, 0) / POW(10, 4 - 2)) AS modifiers,
lc.modifiers AS prev_modifiers
FROM season s
JOIN payment_variant v ON v.year = s.year
LEFT JOIN payment_variant l ON l.year = s.year
AND l.avnr = (SELECT avnr
FROM payment_variant
WHERE year = s.year AND NOT test_variant
ORDER BY COALESCE(transfer_date, date) DESC, avnr DESC
LIMIT 1)
LEFT JOIN (SELECT year, MAX(tgnr) AS tgnr FROM credit GROUP BY year) t ON t.year = s.year
JOIN (SELECT DISTINCT year, mgnr FROM delivery) d ON d.year = s.year
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 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};
UPDATE payment_variant SET test_variant = FALSE WHERE (year, avnr) = ({Year}, {AvNr});
""");
}
public async Task Revert() {
using var cnx = await AppDbContext.ConnectAsync();
await AppDbContext.ExecuteBatch(cnx, $"""
DELETE FROM credit WHERE (year, avnr) = ({Year}, {AvNr});
UPDATE payment_variant SET test_variant = TRUE WHERE (year, avnr) = ({Year}, {AvNr});
""");
}
protected async Task SetCalcTime(SqliteConnection cnx) {
await AppDbContext.ExecuteBatch(cnx, $"""
UPDATE payment_variant SET calc_time = UNIXEPOCH() WHERE (year, avnr) = ({Year}, {AvNr})
""");
}
protected async Task DeleteInDb(SqliteConnection cnx) {
await AppDbContext.ExecuteBatch(cnx, $"""
DELETE FROM payment_delivery_part_bucket WHERE (year, avnr) = ({Year}, {AvNr});
DELETE FROM payment_delivery_part WHERE (year, avnr) = ({Year}, {AvNr});
DELETE FROM payment_member WHERE (year, avnr) = ({Year}, {AvNr});
UPDATE payment_variant SET calc_time = NULL WHERE (year, avnr) = ({Year}, {AvNr});
""");
}
protected async Task CalculateMemberModifiers(SqliteConnection cnx) {
if (App.Client.IsMatzen) {
var lastYears = 3;
var multiplier = 0.50;
var modName = "Treue%";
await AppDbContext.ExecuteBatch(cnx, $"""
INSERT INTO payment_member (year, avnr, mgnr, net_amount, mod_abs, mod_rel)
SELECT c.year, {AvNr}, s.mgnr, 0,
ROUND(s.sum * COALESCE(m.abs, 0)),
COALESCE(m.rel, 0)
FROM (SELECT {Year} AS year, mgnr,
ROUND(AVG(sum) * {multiplier}) AS baseline,
COUNT(*) = {lastYears} AND MIN(sum) > 0 AS allowed
FROM v_stat_member
WHERE year > {Year} - {lastYears}
GROUP BY mgnr
HAVING allowed) c
JOIN v_stat_member s ON (s.year, s.mgnr) = (c.year, c.mgnr)
LEFT JOIN modifier m ON m.year = c.year AND m.name LIKE '{modName}'
WHERE sum >= baseline
ON CONFLICT DO UPDATE
SET mod_abs = mod_abs + excluded.mod_abs,
mod_rel = mod_rel + excluded.mod_rel
""");
}
}
public async Task CalculatePrices() {
await DeleteInDb();
using var cnx = await AppDbContext.ConnectAsync();
protected async Task CalculatePrices(SqliteConnection cnx) {
var jsonData = PaymentVariant.Data;
var attrVariants = Context.DeliveryParts
.Where(d => d.Year == Year)
.Select(d => $"{d.SortId}{d.AttrId}")
.Distinct()
.ToList()
.Union(Context.WineVarieties.Select(v => v.SortId))
.ToList();
var parts = new List<(int Year, int DId, int DPNr, int BktNr, string SortId, string Discr, int Value, bool MinQuw, double Oe, double Kmw)>();
var data = BillingData.FromJson(jsonData, attrVariants);
var parts = new List<(int Year, int DId, int DPNr, int BktNr, string SortId, string Discr, int Value, double Oe, double Kmw, string QualId)>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
SELECT d.year, d.did, d.dpnr, b.bktnr, d.sortid, b.discr, b.value, d.min_quw, d.oe, d.kmw
SELECT d.year, d.did, d.dpnr, b.bktnr, d.sortid, b.discr, b.value, d.oe, d.kmw, d.qualid
FROM delivery_part_bucket b
JOIN v_delivery d ON (d.year, d.did, d.dpnr) = (b.year, b.did, b.dpnr)
WHERE b.year = {Year}
@@ -44,26 +143,37 @@ namespace Elwig.Helpers.Billing {
parts.Add((
reader.GetInt32(0), reader.GetInt32(1), reader.GetInt32(2), reader.GetInt32(3),
reader.GetString(4), reader.GetString(5), reader.GetInt32(6),
reader.GetBoolean(7), reader.GetDouble(8), reader.GetDouble(9)
reader.GetDouble(7), reader.GetDouble(8), reader.GetString(9)
));
}
}
var inserts = new List<(int Year, int DId, int DPNr, int BktNr, long Price, long Amount)>();
foreach (var part in parts) {
var price = !part.MinQuw ? 0.5m : ((part.BktNr == 2 ? 0.8m : (part.BktNr == 1 ? 0.7m : 0.6m)) + ((decimal)(part.Oe - 73) * 0.005m)); // TODO
var priceL = Utils.DecToDb(price, 4);
var amount = Utils.DecToDb(price * part.Value, 4);
inserts.Add((part.Year, part.DId, part.DPNr, part.BktNr, priceL, amount));
var attrId = (part.Discr == "_" || part.Discr == "") ? null : part.Discr;
var price = data.CalculatePrice(part.SortId, attrId, part.QualId, part.Discr != "_", part.Oe, part.Kmw);
var priceL = PaymentVariant.Season.DecToDb(price);
inserts.Add((part.Year, part.DId, part.DPNr, part.BktNr, priceL, priceL * part.Value));
}
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
INSERT INTO payment_delivery_part_bucket (year, did, dpnr, bktnr, avnr, price, amount)
VALUES {string.Join(",\n ", inserts.Select(i => $"({i.Year}, {i.DId}, {i.DPNr}, {i.BktNr}, {AvNr}, {i.Price}, {i.Amount})"))}
""";
await cmd.ExecuteNonQueryAsync();
}
await AppDbContext.ExecuteBatch(cnx, $"""
INSERT INTO payment_delivery_part_bucket (year, did, dpnr, bktnr, avnr, price, amount)
VALUES {string.Join(",\n ", inserts.Select(i => $"({i.Year}, {i.DId}, {i.DPNr}, {i.BktNr}, {AvNr}, {i.Price}, {i.Amount})"))};
""");
}
protected async Task CalculateModifiers(SqliteConnection cnx) {
await AppDbContext.ExecuteBatch(cnx, $"""
INSERT INTO payment_delivery_part (year, did, dpnr, avnr, net_amount, mod_abs, mod_rel)
SELECT d.year, d.did, d.dpnr, {AvNr}, 0, COALESCE(m.abs, 0), COALESCE(m.rel, 0)
FROM delivery_part d
LEFT JOIN delivery_part_modifier p ON (p.year, p.did, p.dpnr) = (d.year, d.did, d.dpnr)
LEFT JOIN modifier m ON (m.year, m.modid) = (d.year, p.modid)
WHERE d.year = {Year}
ON CONFLICT DO UPDATE
SET mod_abs = mod_abs + excluded.mod_abs,
mod_rel = mod_rel + excluded.mod_rel
""");
}
}
}
-32
View File
@@ -1,32 +0,0 @@
using Elwig.Models.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Helpers.Billing {
public class Transaction {
public readonly Member Member;
public readonly long AmountCent;
public readonly string Currency;
public readonly int Nr;
public Transaction(Member m, decimal amount, string currency, int nr) {
Member = m;
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.Credits
.OrderBy(c => c.MgNr)
.Select(c => new Transaction(c.Member, c.Amount, variant.Season.CurrencyCode, c.TgNr))
.ToList();
}
public static string FormatAmountCent(long cents) => $"{cents / 100}.{cents % 100:00}";
}
}
+17 -50
View File
@@ -1,8 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using IniParser;
using IniParser.Model;
using Microsoft.Extensions.Configuration;
namespace Elwig.Helpers {
public class Config {
@@ -13,7 +12,8 @@ namespace Elwig.Helpers {
public string? DatabaseLog = null;
public string? Branch = null;
public IList<string?[]> Scales;
private readonly List<string?[]> ScaleList = new();
private readonly List<string?[]> ScaleList = [];
private static readonly string[] trueValues = ["1", "true", "yes", "on"];
public Config(string filename) {
FileName = filename;
@@ -22,57 +22,24 @@ namespace Elwig.Helpers {
}
public void Read() {
var parser = new FileIniDataParser();
IniData? ini = null;
try {
ini = parser.ReadFile(FileName, Utils.UTF8);
} catch {}
var config = new ConfigurationBuilder().AddIniFile(FileName).Build();
if (ini == null || !ini.TryGetKey("database.file", out string db)) {
DatabaseFile = App.DataPath + "database.sqlite3";
} else if (db.Length > 1 && (db[1] == ':' || db[0] == '/' || db[0] == '\\')) {
DatabaseFile = db;
} else {
DatabaseFile = App.DataPath + db;
}
if (ini == null || !ini.TryGetKey("database.log", out string log)) {
DatabaseLog = null;
} else if (log.Length > 1 && (log[1] == ':' || log[0] == '/' || log[0] == '\\')) {
DatabaseLog = log;
} else {
DatabaseLog = App.DataPath + log;
}
if (ini == null || !ini.TryGetKey("general.branch", out string branch)) {
Branch = null;
} else {
Branch = branch;
}
if (ini == null || !ini.TryGetKey("general.debug", out string debug)) {
Debug = false;
} else {
debug = debug.ToLower();
Debug = debug == "1" || debug == "true" || debug == "yes" || debug == "on";
}
DatabaseFile = Utils.GetAbsolutePath(config["database:file"] ?? "database.sqlite3", App.DataPath);
var log = config["database:log"];
DatabaseLog = log != null ? Utils.GetAbsolutePath(log, App.DataPath) : null;
Branch = config["general:branch"];
Debug = trueValues.Contains(config["general:debug"]?.ToLower());
var scales = config.AsEnumerable().Where(i => i.Key.StartsWith("scale.")).GroupBy(i => i.Key.Split(':')[0][6..]).Select(i => i.Key);
ScaleList.Clear();
Scales = ScaleList;
if (ini != null) {
foreach (var s in ini.Sections.Where(s => s.SectionName.StartsWith("scale."))) {
string? scaleLog = null;
if (s.Keys["log"] != null) {
scaleLog = s.Keys["log"];
if (scaleLog.Length <= 1 || (scaleLog[1] != ':' && scaleLog[0] != '/' && scaleLog[0] != '\\')) {
scaleLog = App.DataPath + scaleLog;
}
}
ScaleList.Add(new string?[] {
s.SectionName[6..], s.Keys["type"], s.Keys["model"], s.Keys["connection"],
s.Keys["empty"], s.Keys["filling"], s.Keys["limit"], scaleLog
});
}
foreach (var s in scales) {
string? scaleLog = config[$"scale.{s}:log"];
if (scaleLog != null) scaleLog = Utils.GetAbsolutePath(scaleLog, App.DataPath);
ScaleList.Add([
s, config[$"scale.{s}:type"], config[$"scale.{s}:model"], config[$"scale.{s}:connection"],
config[$"scale.{s}:empty"], config[$"scale.{s}:filling"], config[$"scale.{s}:limit"], scaleLog
]);
}
}
+51 -59
View File
@@ -1,4 +1,4 @@
using Elwig.Helpers.Billing;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using System;
using System.Collections.Generic;
@@ -7,32 +7,24 @@ using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Helpers.Export {
public class Ebics : IBankingExporter {
public class Ebics(PaymentVar variant, string filename) : IBankingExporter {
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;
}
private readonly StreamWriter Writer = new(filename, false, Utils.UTF8);
private readonly DateOnly Date = variant.TransferDate ?? throw new ArgumentException("TransferDate has to be set in PaymentVar");
private readonly int Year = variant.Year;
private readonly string Name = variant.Name;
private readonly int AvNr = variant.AvNr;
public void Dispose() {
GC.SuppressFinalize(this);
_writer.Dispose();
Writer.Dispose();
}
public ValueTask DisposeAsync() {
GC.SuppressFinalize(this);
return _writer.DisposeAsync();
return Writer.DisposeAsync();
}
public void Export(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) {
@@ -43,66 +35,66 @@ namespace Elwig.Helpers.Export {
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 ctrlSum = transactions.Sum(tx => tx.Amount);
var msgId = $"ELWIG-{App.Client.NameToken}-{Year}-AV{AvNr:00}";
var pmtInfId = $"{msgId}-1";
await _writer.WriteLineAsync($"""
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>
<CstmrCdtTrfInitn>
<GrpHdr>
<MsgId>{msgId}</MsgId>
<CreDtTm>{DateTime.UtcNow:o}</CreDtTm>
<NbOfTxs>{nbOfTxs}</NbOfTxs>
<CtrlSum>{Transaction.FormatAmount(ctrlSum)}</CtrlSum>
<InitgPty><Nm>{App.Client.NameFull}</Nm></InitgPty>
</GrpHdr>
<PmtInf>
<PmtInfId>{pmtInfId}</PmtInfId>
<PmtMtd>TRF</PmtMtd>
<NbOfTxs>{nbOfTxs}</NbOfTxs>
<CtrlSum>{Transaction.FormatAmount(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>
var id = $"ELWIG-{App.Client.NameToken}-{Year}-TG{tx.Nr:0000}";
var info = $"{Name} - Traubengutschrift Nr. {Year}/{tx.Nr:000}";
await Writer.WriteLineAsync($"""
<CdtTrfTxInf>
<PmtId><EndToEndId>{id}</EndToEndId></PmtId>
<Amt><InstdAmt Ccy="{tx.Currency}">{Transaction.FormatAmount(tx.Amount)}</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>
await Writer.WriteLineAsync("""
</PmtInf>
</CstmrCdtTrfInitn>
</Document>
""");
await _writer.FlushAsync();
await Writer.FlushAsync();
progress?.Report(100.0);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
using Elwig.Helpers.Billing;
using Elwig.Models.Dtos;
using System;
namespace Elwig.Helpers.Export {
+1 -1
View File
@@ -1,4 +1,4 @@
using Elwig.Helpers.Billing;
using Elwig.Models.Dtos;
namespace Elwig.Helpers.Export {
/// <summary>
+3 -2
View File
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Data.Entity.Core.Common.CommandTrees.ExpressionBuilder;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
@@ -264,7 +265,7 @@ namespace Elwig.Helpers.Export {
} else if (data is float || data is double || data is byte || data is char ||
data is short || data is ushort || data is int || data is uint || data is long || data is ulong) {
double v = double.Parse(data?.ToString() ?? "0");
double v = double.Parse(data?.ToString() ?? "0", CultureInfo.InvariantCulture);
if (units != null && units.Length > 0) {
int n = -1;
switch (units[0]) {
@@ -274,7 +275,7 @@ namespace Elwig.Helpers.Export {
}
if (n >= 0) add = string.Join(" ", add.Split(" ").Select(p => p.StartsWith("table:style-name=") ? $"table:style-name=\"N{n}\"" : p));
}
c = $"<{ct} office:value-type=\"float\" calcext:value-type=\"float\" office:value=\"{v.ToString()?.Replace(",", ".")}\"{add}><text:p>{data}</text:p></{ct}>";
c = $"<{ct} office:value-type=\"float\" calcext:value-type=\"float\" office:value=\"{v.ToString(CultureInfo.InvariantCulture)}\"{add}><text:p>{data}</text:p></{ct}>";
} else {
c = $"<{ct} office:value-type=\"string\" calcext:value-type=\"string\"{add}><text:p>{data}</text:p></{ct}>";
}
+15
View File
@@ -11,6 +11,7 @@ using Elwig.Dialogs;
using System.Text;
using System.Numerics;
using Elwig.Models.Entities;
using System.IO;
namespace Elwig.Helpers {
public static partial class Utils {
@@ -47,6 +48,16 @@ namespace Elwig.Helpers {
[GeneratedRegex(@"^(.*?) +([0-9].*)$", RegexOptions.Compiled)]
private static partial Regex GeneratedAddressRegex();
public static readonly KeyValuePair<string, string>[] PhoneNrTypes = [
new("landline", "Tel.-Nr. (Festnetz)"),
new("mobile", "Tel.-Nr. (mobil)"),
new("fax", "Fax-Nr."),
];
public static string PhoneNrTypeToString(string type) {
return PhoneNrTypes.Where(t => t.Key == type).Select(t => t.Value).FirstOrDefault(type);
}
private static readonly ushort[] Crc16ModbusTable = {
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
@@ -345,5 +356,9 @@ namespace Elwig.Helpers {
}
return output.OrderByDescending(l => l.Count());
}
public static string GetAbsolutePath(string path, string basePath) {
return (path.Length > 1 && (path[1] == ':' || path[0] == '/' || path[0] == '\\')) ? Path.Combine(basePath, path) : path;
}
}
}
+1 -1
View File
@@ -37,7 +37,7 @@ namespace Elwig.Models.Dtos {
private static async Task<IEnumerable<AreaComUnderDeliveryRowSingle>> FromDbSet(DbSet<AreaComUnderDeliveryRowSingle> table, int year) {
return await table.FromSqlRaw($"""
SELECT m.mgnr, m.family_name, m.given_name, p.plz, o.name AS ort, m.address,
c.bucket, c.area, u.min_kg, u.weight
c.bucket, c.area, u.min_kg, u.weight
FROM member m
LEFT JOIN AT_plz_dest p ON p.id = m.postal_dest
LEFT JOIN AT_ort o ON o.okz = p.okz
+36 -14
View File
@@ -1,4 +1,5 @@
using Elwig.Helpers;
using Elwig.Helpers.Billing;
using Elwig.Models.Entities;
using Microsoft.EntityFrameworkCore;
using System;
@@ -27,11 +28,11 @@ namespace Elwig.Models.Dtos {
MgNr = mgnr;
}
public static async Task<IDictionary<int, CreditNoteData>> ForPaymentVariant(DbSet<CreditNoteRowSingle> table, int year, int avnr) {
public static async Task<IDictionary<int, CreditNoteData>> ForPaymentVariant(DbSet<CreditNoteRowSingle> table, DbSet<Season> seasons, int year, int avnr) {
return (await FromDbSet(table, year, avnr))
.GroupBy(
r => new { r.Year, r.AvNr, r.MgNr, r.TgNr, r.DId, r.DPNr },
(k, g) => new CreditNoteRow(g))
(k, g) => new CreditNoteRow(g, seasons))
.GroupBy(
r => new { r.Year, r.AvNr, r.MgNr, r.TgNr },
(k, g) => new CreditNoteData(g, k.Year, k.TgNr, mgnr: k.MgNr))
@@ -43,17 +44,20 @@ namespace Elwig.Models.Dtos {
var v = avnr?.ToString() ?? "NULL";
var m = mgnr?.ToString() ?? "NULL";
return await table.FromSqlRaw($"""
SELECT d.year, c.tgnr, p.avnr, d.mgnr, d.did, d.lsnr, d.dpnr, b.bktnr, d.sortid, b.discr, b.value, p.price, p.amount,
v.name AS variant, a.name AS attribute, q.name AS quality_level, d.oe, d.kmw
SELECT d.year, c.tgnr, v.avnr, d.mgnr, d.did, d.lsnr, d.dpnr, d.weight, d.modifiers,
b.bktnr, d.sortid, b.discr, b.value, pb.price, pb.amount, p.net_amount, p.amount AS total_amount,
s.name AS variant, a.name AS attribute, q.name AS quality_level, d.oe, d.kmw
FROM v_delivery d
JOIN wine_variety v ON d.sortid = v.sortid
JOIN wine_variety s ON s.sortid = d.sortid
LEFT JOIN wine_attribute a ON a.attrid = d.attrid
JOIN wine_quality_level q ON q.qualid = d.qualid
LEFT JOIN delivery_part_bucket b ON (b.year, b.did, b.dpnr) = (d.year, d.did, d.dpnr)
LEFT JOIN payment_delivery_part_bucket p ON (p.year, p.did, p.dpnr, p.bktnr) = (b.year, b.did, b.dpnr, b.bktnr)
LEFT JOIN credit c ON (c.year, c.avnr, c.mgnr) = (d.year, p.avnr, d.mgnr)
WHERE b.value > 0 AND (d.year = {y} OR {y} IS NULL) AND (p.avnr = {v} OR {v} IS NULL OR p.avnr IS NULL) AND (d.mgnr = {m} OR {m} IS NULL)
ORDER BY d.year, p.avnr, d.mgnr, d.lsnr, d.dpnr
LEFT JOIN payment_variant v ON v.year = d.year
LEFT JOIN payment_delivery_part p ON (p.year, p.did, p.dpnr, p.avnr) = (d.year, d.did, d.dpnr, v.avnr)
LEFT JOIN payment_delivery_part_bucket pb ON (pb.year, pb.did, pb.dpnr, pb.bktnr, pb.avnr) = (b.year, b.did, b.dpnr, b.bktnr, v.avnr)
LEFT JOIN credit c ON (c.year, c.avnr, c.mgnr) = (d.year, v.avnr, d.mgnr)
WHERE b.value > 0 AND (d.year = {y} OR {y} IS NULL) AND (v.avnr = {v} OR {v} IS NULL) AND (d.mgnr = {m} OR {m} IS NULL)
ORDER BY d.year, v.avnr, d.mgnr, d.lsnr, d.dpnr
""").ToListAsync();
}
}
@@ -73,28 +77,38 @@ namespace Elwig.Models.Dtos {
public string QualityLevel;
public (double Oe, double Kmw) Gradation;
public (string Name, int Value, decimal? Price, decimal? Amount)[] Buckets;
public decimal? TotalModifiers;
public decimal? Amount;
public CreditNoteRow(IEnumerable<CreditNoteRowSingle> rows) {
public CreditNoteRow(IEnumerable<CreditNoteRowSingle> rows, DbSet<Season> seasons) {
var f = rows.First();
Year = f.Year;
TgNr = f.TgNr;
MgNr = f.MgNr;
var season = seasons.Find(Year);
LsNr = f.LsNr;
DPNr = f.DPNr;
Variant = f.Variant;
Attribute = f.Attribute;
Modifiers = Array.Empty<string>(); // TODO
var modifiers = (IEnumerable<Modifier>)(f.Modifiers ?? "").Split(',')
.Select(m => season?.Modifiers.FirstOrDefault(s => s.ModId == m))
.Where(m => m != null)
.OrderBy(m => m.Ordering)
.ToList();
Modifiers = modifiers.Select(m => m.Name).ToArray();
QualityLevel = f.QualityLevel;
Gradation = (f.Oe, f.Kmw);
Buckets = rows
.Where(b => b.Value > 0)
.OrderByDescending(b => b.BktNr)
// FIXME precision
.Select(b => (b.Discr == "_" ? "ungeb." : $"geb. {f.SortId}{b.Discr}", b.Value,
b.Price != null ? (decimal?)Utils.DecFromDb((long)b.Price, 4) : null,
b.Amount != null ? (decimal?)Utils.DecFromDb((long)b.Amount, 4) : null))
b.Price != null ? season?.DecFromDb((long)b.Price) : null,
b.Amount != null ? season?.DecFromDb((long)b.Amount) : null))
.ToArray();
Amount = f.TotalAmount != null ? season?.DecFromDb((long)f.TotalAmount) : null;
var netAmount = f.NetAmount != null ? season?.DecFromDb((long)f.NetAmount) : null;
TotalModifiers = Amount - netAmount;
}
}
@@ -114,6 +128,10 @@ namespace Elwig.Models.Dtos {
public string LsNr { get; set; }
[Column("dpnr")]
public int DPNr { get; set; }
[Column("weight")]
public int Weight { get; set; }
[Column("modifiers")]
public string? Modifiers { get; set; }
[Column("bktnr")]
public int BktNr { get; set; }
[Column("sortid")]
@@ -126,6 +144,10 @@ namespace Elwig.Models.Dtos {
public long? Price { get; set; }
[Column("amount")]
public long? Amount { get; set; }
[Column("net_amount")]
public long? NetAmount { get; set; }
[Column("total_amount")]
public long? TotalAmount { get; set; }
[Column("variant")]
public string Variant { get; set; }
[Column("attribute")]
+26
View File
@@ -0,0 +1,26 @@
using Elwig.Models.Entities;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Models.Dtos {
public class Transaction(Member member, decimal amount, string currency, int nr) {
public readonly Member Member = member;
public readonly decimal Amount = amount;
public readonly string Currency = currency;
public readonly int Nr = nr;
public Transaction(Credit c) : this(c.Member, c.Amount, c.Variant.Season.CurrencyCode, c.TgNr) { }
public static IEnumerable<Transaction> FromPaymentVariant(PaymentVar variant) {
return variant.Credits
.OrderBy(c => c.TgNr)
.Select(c => new Transaction(c))
.ToList();
}
public static string FormatAmountCent(long cents) => $"{cents / 100}.{cents % 100:00}";
public static string FormatAmount(decimal amount) => FormatAmountCent((int)(amount * 100));
}
}
+1 -1
View File
@@ -163,7 +163,7 @@ namespace Elwig.Models.Entities {
[NotMapped]
public IEnumerable<AreaCom> ActiveAreaCommitments => AreaCommitments
.Where(c => c.YearFrom <= Utils.CurrentNextSeason && (c.YearTo ?? int.MaxValue) >= Utils.CurrentNextSeason);
.Where(c => c.YearFrom <= Utils.CurrentYear && (c.YearTo ?? int.MaxValue) >= Utils.CurrentYear);
[InverseProperty("Member")]
public virtual BillingAddr? BillingAddress { get; private set; }
+10 -5
View File
@@ -16,6 +16,14 @@ namespace Elwig.Models.Entities {
[Column("avnr")]
public int AvNr { get; set; }
[Column("net_amount")]
public long NetAmountValue { get; set; }
[NotMapped]
public decimal NetAmount {
get => Variant.Season.DecFromDb(NetAmountValue);
set => NetAmountValue = Variant.Season.DecToDb(value);
}
[Column("mod_abs")]
public long ModAbsValue { get; set; }
[NotMapped]
@@ -33,12 +41,9 @@ namespace Elwig.Models.Entities {
}
[Column("amount")]
public long? AmountValue { get; set; }
public long AmountValue { get; private set; }
[NotMapped]
public decimal? Amount {
get => AmountValue != null ? Variant.Season.DecFromDb(AmountValue.Value) : null;
set => AmountValue = value != null ? Variant.Season.DecToDb(value.Value) : null;
}
public decimal Amount => Variant.Season.DecFromDb(AmountValue);
[ForeignKey("Year, AvNr")]
public virtual PaymentVar Variant { get; private set; }
+26 -5
View File
@@ -13,15 +13,36 @@ namespace Elwig.Models.Entities {
[Column("mgnr")]
public int MgNr { get; set; }
[Column("amount")]
public long AmountValue { get; set; }
[Column("net_amount")]
public long NetAmountValue { get; set; }
[NotMapped]
public decimal Amount {
get => Variant.Season.DecFromDb(AmountValue);
set => AmountValue = Variant.Season.DecToDb(value);
public decimal NetAmount {
get => Variant.Season.DecFromDb(NetAmountValue);
set => NetAmountValue = Variant.Season.DecToDb(value);
}
[Column("mod_abs")]
public long ModAbsValue { get; set; }
[NotMapped]
public decimal ModAbs {
get => Variant.Season.DecFromDb(ModAbsValue);
set => ModAbsValue = Variant.Season.DecToDb(value);
}
[Column("mod_rel")]
public double ModRelValue { get; set; }
[NotMapped]
public decimal ModRel {
get => (decimal)ModRelValue;
set => ModRelValue = (double)value;
}
[Column("amount")]
public long AmountValue { get; private set; }
[NotMapped]
public decimal Amount => Variant.Season.DecFromDb(AmountValue);
[ForeignKey("Year, AvNr")]
public virtual PaymentVar Variant { get; private set; }
+3
View File
@@ -51,6 +51,9 @@ namespace Elwig.Models.Entities {
[InverseProperty("Variant")]
public virtual ISet<PaymentMember> MemberPayments { get; private set; }
[InverseProperty("Variant")]
public virtual ISet<PaymentDeliveryPart> DeliveryPartPayments { get; private set; }
[InverseProperty("Variant")]
public virtual ISet<Credit> Credits { get; private set; }
}
@@ -0,0 +1,135 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["mode"],
"anyOf": [{
"required": ["version", "payment", "curves"],
"additionalProperties": false,
"properties": {
"mode": {"enum": ["elwig"]},
"version": {"enum": [1]},
"payment": {
"type": ["number", "string", "object"],
"pattern": "^curve:[0-9]+$",
"additionalProperties": false,
"properties": {
"default": {
"type": ["number", "string"],
"pattern": "^curve:[0-9]+$"
}
},
"patternProperties": {
"^([A-Z]{2})?(\/[A-Z]*)?$": {
"type": ["number", "string"],
"pattern": "^curve:[0-9]+$"
}
}
},
"quality": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[A-Z]{3}$": {
"type": ["number", "string", "object"],
"pattern": "^curve:[0-9]+$",
"additionalProperties": false,
"properties": {
"default": {
"type": ["number", "string"],
"pattern": "^curve:[0-9]+$"
}
},
"patternProperties": {
"^([A-Z]{2})?(\/[A-Z]*)?$": {
"type": ["number", "string"],
"pattern": "^curve:[0-9]+$"
}
}
}
}
},
"curves": {
"type": "array",
"items": {"$ref": "#/definitions/curve"}
}
}
}, {
"properties": {
"mode": {"enum": ["wgmaster"]},
"AuszahlungSorten": {
"type": "object",
"additionalProperties": false,
"required": ["Kurven"],
"properties": {
"Kurven": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"patternProperties": {
"^[0-9]+(\\.[0-9]+)?oe$": {"type": "number"}
}
}
}
},
"patternProperties": {
"^[A-Z]{2}$": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[A-Z]*$": {
"type": "object",
"additionalProperties": false,
"properties": {
"Gebunden": {
"type": "integer",
"minimum": 0
},
"NichtGebunden": {
"type": "integer",
"minimum": 0
}
}
}
}
}
}
}
}
}],
"definitions": {
"curve": {
"type": "object",
"required": ["id", "mode", "data"],
"additionalProperties": false,
"properties": {
"id": {
"type": "integer",
"minimum": 0
},
"mode": {"enum": ["oe", "kmw"]},
"data": {
"anyOf": [
{"type": "number" },
{"$ref": "#/definitions/curve_data"}
]
},
"geb": {
"anyOf": [
{"type": "number"},
{"$ref": "#/definitions/curve_data"}
]
}
}
},
"curve_data": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"patternProperties": {
"^[<>]?([0-9]+(\\.[0-9]+)?)(oe|kmw)$": {"type": "number"}
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
-- schema version 1 to 2
DROP VIEW v_area_commitment;
ALTER TABLE delivery_part DROP COLUMN weighing_reason;
ALTER TABLE delivery_part ADD COLUMN weighing_reason TEXT CHECK(NOT (manual_weighing = FALSE AND weighing_reason IS NOT NULL));
+96
View File
@@ -0,0 +1,96 @@
-- schema version 2 to 3
CREATE TABLE delivery_part_bin (
year INTEGER NOT NULL,
did INTEGER NOT NULL,
dpnr INTEGER NOT NULL,
binnr INTEGER NOT NULL,
discr TEXT NOT NULL,
value INTEGER NOT NULL,
CONSTRAINT pk_delivery_part_bin PRIMARY KEY (year, did, dpnr, binnr),
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;
INSERT INTO delivery_part_bin (year, did, dpnr, binnr, discr, value)
SELECT year, did, dpnr, 0, '_', bucket_2 + bucket_3
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;
INSERT INTO delivery_part_bin (year, did, dpnr, binnr, discr, value)
SELECT d.year, d.did, d.dpnr, 1, COALESCE(attributes, ''), bucket_1
FROM payment_delivery_part p
JOIN v_delivery d ON (d.year, d.did, d.dpnr) = (p.year, p.did, p.dpnr)
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;
ALTER TABLE payment_delivery_part DROP COLUMN bucket_1;
ALTER TABLE payment_delivery_part DROP COLUMN bucket_2;
ALTER TABLE payment_delivery_part DROP COLUMN bucket_3;
ALTER TABLE payment_delivery_part DROP COLUMN bucket_4;
ALTER TABLE payment_delivery_part DROP COLUMN bucket_5;
ALTER TABLE payment_delivery_part DROP COLUMN bucket_6;
ALTER TABLE payment_delivery_part DROP COLUMN bucket_7;
ALTER TABLE payment_delivery_part DROP COLUMN bucket_8;
ALTER TABLE payment_delivery_part DROP COLUMN bucket_9;
ALTER TABLE payment_variant DROP COLUMN bucket_1_name;
ALTER TABLE payment_variant DROP COLUMN bucket_2_name;
ALTER TABLE payment_variant DROP COLUMN bucket_3_name;
ALTER TABLE payment_variant DROP COLUMN bucket_4_name;
ALTER TABLE payment_variant DROP COLUMN bucket_5_name;
ALTER TABLE payment_variant DROP COLUMN bucket_6_name;
ALTER TABLE payment_variant DROP COLUMN bucket_7_name;
ALTER TABLE payment_variant DROP COLUMN bucket_8_name;
ALTER TABLE payment_variant DROP COLUMN bucket_9_name;
ALTER TABLE delivery_part ADD COLUMN gebunden INTEGER CHECK (gebunden IN (TRUE, FALSE)) DEFAULT NULL;
DROP VIEW v_delivery;
CREATE VIEW v_delivery AS
SELECT s.*, GROUP_CONCAT(o.modid) AS modifiers
FROM (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.gerebelt, p.gebunden,
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(a.attrid) AS attributes,
COALESCE(SUM(a.fill_lower_bins), 0) AS attribute_prio,
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 pa ON (pa.year, pa.did, pa.dpnr) = (p.year, p.did, p.dpnr)
LEFT JOIN wine_attribute a ON a.attrid = pa.attrid
GROUP BY p.year, p.did, p.dpnr
ORDER BY p.year, p.did, p.dpnr, a.attrid) s
LEFT JOIN delivery_part_modifier o ON (o.year, o.did, o.dpnr) = (s.year, s.did, s.dpnr)
GROUP BY s.year, s.lsnr, s.dpnr
ORDER BY s.year, s.lsnr, s.dpnr, o.modid;
DROP VIEW v_bucket;
CREATE VIEW v_delivery_bin AS
SELECT year, mgnr,
sortid || IIF(min_quw, REPLACE(COALESCE(attributes, ''), ',', ''), '_') AS bin,
SUM(weight) AS weight
FROM v_delivery
GROUP BY year, mgnr, bin
ORDER BY year, mgnr, LENGTH(bin) DESC, bin;
CREATE VIEW v_payment_bin AS
SELECT d.year, d.mgnr,
sortid || discr AS bin,
SUM(value) AS weight
FROM v_delivery d
JOIN delivery_part_bin b ON (b.year, b.did, b.dpnr) = (d.year, d.did, d.dpnr)
GROUP BY d.year, d.mgnr, bin
HAVING SUM(value) > 0
ORDER BY d.year, d.mgnr, bin;
ALTER TABLE wine_attribute ADD COLUMN fill_lower_bins INTEGER NOT NULL CHECK (fill_lower_bins IN (0, 1, 2)) DEFAULT 0;
+24
View File
@@ -0,0 +1,24 @@
-- schema version 3 to 4
DROP VIEW v_payment_bin;
CREATE VIEW v_payment_bin AS
SELECT d.year, d.mgnr,
sortid || discr AS bin,
SUM(value) AS weight
FROM v_delivery d
JOIN delivery_part_bin b ON (b.year, b.did, b.dpnr) = (d.year, d.did, d.dpnr)
GROUP BY d.year, d.mgnr, bin
HAVING SUM(value) > 0
ORDER BY d.year, d.mgnr, LENGTH(bin) DESC, bin;
CREATE VIEW v_area_commitment_bin AS
SELECT s.year, c.mgnr,
c.vtrgid AS bin,
CAST(ROUND(SUM(COALESCE(area * min_kg_per_ha, 0)) / 10000.0, 0) AS INTEGER) AS min_kg,
CAST(ROUND(SUM(COALESCE(area * max_kg_per_ha, 0)) / 10000.0, 0) AS INTEGER) AS max_kg
FROM area_commitment c, season s
JOIN area_commitment_type t ON t.vtrgid = c.vtrgid
WHERE (year_from IS NULL OR year_from <= s.year) AND
(year_to IS NULL OR year_to >= s.year)
GROUP BY s.year, c.mgnr, c.vtrgid
ORDER BY s.year, c.mgnr, LENGTH(c.vtrgid) DESC, c.vtrgid;
+100
View File
@@ -0,0 +1,100 @@
-- schema version 4 to 5
CREATE TABLE _area_commitment_type (
vtrgid TEXT NOT NULL CHECK (vtrgid = sortid || COALESCE(attrid, '') || disc),
sortid TEXT NOT NULL,
attrid TEXT,
disc TEXT DEFAULT NULL CHECK (disc REGEXP '^[A-Z0-9]+$'),
min_kg_per_ha INTEGER,
max_kg_per_ha INTEGER,
penalty_amount INTEGER,
CONSTRAINT pk_area_commitment_type PRIMARY KEY (vtrgid),
CONSTRAINT sk_area_commitment_type_sort_attr UNIQUE (sortid, attrid, disc),
CONSTRAINT fk_area_commitment_type_wine_variety FOREIGN KEY (sortid) REFERENCES wine_variety (sortid)
ON UPDATE CASCADE
ON DELETE RESTRICT,
CONSTRAINT fk_area_commitment_type_wine_attribute FOREIGN KEY (attrid) REFERENCES wine_attribute (attrid)
ON UPDATE CASCADE
ON DELETE RESTRICT
) STRICT;
INSERT INTO _area_commitment_type (vtrgid, sortid, attrid, disc, min_kg_per_ha, max_kg_per_ha, penalty_amount)
SELECT vtrgid, sortid, attrid_1, disc, min_kg_per_ha, max_kg_per_ha, penalty_amount FROM area_commitment_type;
PRAGMA writable_schema = ON;
DROP TABLE area_commitment_type;
ALTER TABLE _area_commitment_type RENAME TO area_commitment_type;
PRAGMA writable_schema = OFF;
ALTER TABLE delivery_part ADD COLUMN attrid TEXT DEFAULT NULL REFERENCES wine_attribute (attrid)
ON UPDATE CASCADE
ON DELETE RESTRICT;
UPDATE delivery_part
SET attrid = (SELECT attrid
FROM delivery_part_attribute a
WHERE (delivery_part.year, delivery_part.did, delivery_part.dpnr) = (a.year, a.did, a.dpnr)
ORDER BY attrid DESC
LIMIT 1);
DROP TRIGGER t_delivery_part_attribute_i_mtime_delivery_part;
DROP TRIGGER t_delivery_part_attribute_u_mtime_delivery_part;
DROP TRIGGER t_delivery_part_attribute_d_mtime_delivery_part;
DROP TABLE delivery_part_attribute;
DROP VIEW v_delivery;
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, a.attrid,
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.gerebelt, p.gebunden,
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,
COALESCE(a.fill_lower_bins, 0) AS attribute_prio,
GROUP_CONCAT(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 wine_attribute a ON a.attrid = p.attrid
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, o.modid;
DROP VIEW v_delivery_bin;
CREATE VIEW v_delivery_bin AS
SELECT year, mgnr,
sortid || IIF(min_quw, COALESCE(attrid, ''), '_') AS bin,
SUM(weight) AS weight
FROM v_delivery
GROUP BY year, mgnr, bin
ORDER BY year, mgnr, LENGTH(bin) DESC, bin;
DROP VIEW v_stat_attr;
CREATE VIEW v_stat_attr AS
SELECT year, attrid,
SUM(weight) as sum,
ROUND(SUM(kmw * weight) / SUM(weight), 2) AS kmw,
ROUND(SUM(oe * weight) / SUM(weight), 1) AS oe,
COUNT(DISTINCT did) AS lieferungen,
COUNT(DISTINCT mgnr) AS mitglieder
FROM v_delivery
GROUP BY year, attrid
ORDER BY year, attrid;
DROP VIEW v_stat_sort_attr;
CREATE VIEW v_stat_sort_attr AS
SELECT year, sortid, attrid,
SUM(weight) as sum,
ROUND(SUM(kmw * weight) / SUM(weight), 2) AS kmw,
ROUND(SUM(oe * weight) / SUM(weight), 1) AS oe,
COUNT(DISTINCT did) AS lieferungen,
COUNT(DISTINCT mgnr) AS mitglieder
FROM v_delivery
GROUP BY year, sortid, attrid
ORDER BY year, sortid, attrid;
+150
View File
@@ -0,0 +1,150 @@
-- schema version 5 to 6
DROP VIEW IF EXISTS v_area_commitment;
PRAGMA writable_schema = ON;
ALTER TABLE wine_attribute DROP COLUMN fill_lower_bins;
ALTER TABLE wine_attribute ADD COLUMN strict INTEGER NOT NULL CHECK (strict IN (TRUE, FALSE)) DEFAULT FALSE;
ALTER TABLE wine_attribute ADD COLUMN fill_lower INTEGER NOT NULL CHECK (fill_lower IN (0, 1, 2)) DEFAULT 0;
DROP VIEW v_delivery;
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, a.attrid,
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.gerebelt, p.gebunden,
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,
IIF(a.strict, COALESCE(a.fill_lower, 0), 0) AS attribute_prio,
GROUP_CONCAT(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 wine_attribute a ON a.attrid = p.attrid
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, o.modid;
PRAGMA writable_schema = OFF;
DROP VIEW v_area_commitment_bin;
DROP VIEW v_delivery_bin;
DROP VIEW v_payment_bin;
ALTER TABLE area_commitment_type DROP COLUMN max_kg_per_ha;
ALTER TABLE area_commitment_type ADD COLUMN penalty_per_kg INTEGER DEFAULT NULL;
ALTER TABLE area_commitment_type ADD COLUMN penalty_none INTEGER DEFAULT NULL;
ALTER TABLE wine_cultivation ADD COLUMN description TEXT DEFAULT NULL;
ALTER TABLE member ADD COLUMN organic INTEGER NOT NULL CHECK (organic IN (TRUE, FALSE)) DEFAULT FALSE;
ALTER TABLE season ADD COLUMN max_kg_per_ha INTEGER NOT NULL DEFAULT 10000;
ALTER TABLE season ADD COLUMN vat_normal REAL NOT NULL DEFAULT 0.10;
ALTER TABLE season ADD COLUMN vat_flatrate REAL NOT NULL DEFAULT 0.13;
ALTER TABLE season ADD COLUMN min_kg_per_bs INTEGER NOT NULL DEFAULT 750;
ALTER TABLE season ADD COLUMN max_kg_per_bs INTEGER NOT NULL DEFAULT 3000;
ALTER TABLE season ADD COLUMN penalty_per_kg INTEGER DEFAULT NULL;
ALTER TABLE season ADD COLUMN penalty_amount INTEGER DEFAULT NULL;
ALTER TABLE season ADD COLUMN penalty_none INTEGER DEFAULT NULL;
DELETE FROM client_parameter WHERE param IN ('DELIVERY_RIGHT', 'DELIVERY_OBLIGATION', 'VAT_NORMAL', 'VAT_REDUCED', 'VAT_FLATRATE');
CREATE TABLE delivery_part_bucket (
year INTEGER NOT NULL,
did INTEGER NOT NULL,
dpnr INTEGER NOT NULL,
bktnr INTEGER NOT NULL,
discr TEXT NOT NULL,
value INTEGER NOT NULL,
CONSTRAINT pk_delivery_part_bucket PRIMARY KEY (year, did, dpnr, bktnr),
CONSTRAINT fk_delivery_part_bucket_delivery_part FOREIGN KEY (year, did, dpnr) REFERENCES delivery_part (year, did, dpnr)
ON UPDATE CASCADE
ON DELETE CASCADE
) STRICT;
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
SELECT year, did, dpnr, binnr, discr, value
FROM delivery_part_bin;
DROP TABLE delivery_part_bin;
CREATE VIEW v_area_commitment_bucket_strict AS
SELECT s.year, c.mgnr,
t.sortid || COALESCE(a.attrid, '') AS bucket,
t.sortid, a.attrid,
CAST(ROUND(SUM(area) * COALESCE(t.min_kg_per_ha, 0) / 10000.0, 0) AS INTEGER) AS min_kg,
CAST(ROUND(SUM(area) * MIN(COALESCE(a.max_kg_per_ha, s.max_kg_per_ha), s.max_kg_per_ha) / 10000.0, 0) AS INTEGER) AS max_kg,
CAST(ROUND(SUM(area) * s.max_kg_per_ha / 10000.0, 0) AS INTEGER) AS upper_max_kg
FROM season s, area_commitment c
JOIN area_commitment_type t ON t.vtrgid = c.vtrgid
LEFT JOIN wine_attribute a ON a.attrid = t.attrid
WHERE (year_from IS NULL OR year_from <= s.year) AND
(year_to IS NULL OR year_to >= s.year)
GROUP BY s.year, c.mgnr, bucket
ORDER BY s.year, c.mgnr, bucket;
CREATE VIEW v_area_commitment_bucket AS
SELECT year, mgnr, bucket, min_kg, max_kg
FROM v_area_commitment_bucket_strict
WHERE attrid IS NOT NULL
UNION ALL
SELECT b.year, b.mgnr, b.sortid,
SUM(b.min_kg) AS min_kg,
SUM(b.upper_max_kg) AS max_kg
FROM v_area_commitment_bucket_strict b
LEFT JOIN wine_attribute a ON a.attrid = b.attrid
WHERE a.strict IS NULL OR a.strict = FALSE
GROUP BY b.year, b.mgnr, b.sortid
ORDER BY year, mgnr, bucket;
CREATE VIEW v_delivery_bucket_strict AS
SELECT year, mgnr,
sortid || IIF(min_quw, COALESCE(attrid, ''), '_') AS bucket,
sortid, IIF(min_quw, attrid, NULL) AS attrid,
SUM(weight) AS weight,
min_quw
FROM v_delivery
GROUP BY year, mgnr, bucket
ORDER BY year, mgnr, bucket;
CREATE VIEW v_delivery_bucket AS
SELECT year, mgnr, bucket, weight
FROM v_delivery_bucket_strict
WHERE attrid IS NOT NULL OR NOT min_quw
UNION ALL
SELECT b.year, b.mgnr, b.sortid,
SUM(b.weight) AS weight
FROM v_delivery_bucket_strict b
LEFT JOIN wine_attribute a ON a.attrid = b.attrid
WHERE min_quw AND (a.strict IS NULL OR a.strict = FALSE)
GROUP BY b.year, b.mgnr, b.sortid
ORDER BY year, mgnr, bucket;
CREATE VIEW v_payment_bucket_strict AS
SELECT d.year, d.mgnr,
d.sortid || b.discr AS bucket,
d.sortid, IIF(b.discr IN ('', '_'), NULL, b.discr) AS attrid,
SUM(b.value) AS weight,
b.discr != '_' AS gebunden
FROM v_delivery d
LEFT JOIN delivery_part_bucket b ON (b.year, b.did, b.dpnr) = (d.year, d.did, d.dpnr)
GROUP BY d.year, d.mgnr, bucket
HAVING SUM(b.value) > 0
ORDER BY d.year, d.mgnr, bucket;
CREATE VIEW v_payment_bucket AS
SELECT year, mgnr, bucket, weight
FROM v_payment_bucket_strict
WHERE attrid IS NOT NULL OR NOT gebunden
UNION ALL
SELECT b.year, b.mgnr, b.sortid,
SUM(b.weight) AS weight
FROM v_payment_bucket_strict b
LEFT JOIN wine_attribute a ON a.attrid = b.attrid
WHERE gebunden AND (a.strict IS NULL OR a.strict = FALSE)
GROUP BY b.year, b.mgnr, b.sortid
ORDER BY year, mgnr, bucket;
+40
View File
@@ -0,0 +1,40 @@
-- schema version 6 to 7
DROP VIEW v_area_commitment_bucket_strict;
CREATE VIEW v_area_commitment_bucket_strict AS
SELECT s.year, c.mgnr,
t.sortid || COALESCE(a.attrid, '') AS bucket,
t.sortid, a.attrid,
SUM(area) AS area,
CAST(ROUND(SUM(area) * COALESCE(t.min_kg_per_ha, 0) / 10000.0, 0) AS INTEGER) AS min_kg,
CAST(ROUND(SUM(area) * MIN(COALESCE(a.max_kg_per_ha, s.max_kg_per_ha), s.max_kg_per_ha) / 10000.0, 0) AS INTEGER) AS max_kg,
CAST(ROUND(SUM(area) * s.max_kg_per_ha / 10000.0, 0) AS INTEGER) AS upper_max_kg
FROM season s, area_commitment c
JOIN area_commitment_type t ON t.vtrgid = c.vtrgid
LEFT JOIN wine_attribute a ON a.attrid = t.attrid
WHERE (year_from IS NULL OR year_from <= s.year) AND
(year_to IS NULL OR year_to >= s.year)
GROUP BY s.year, c.mgnr, bucket
ORDER BY s.year, c.mgnr, bucket;
CREATE VIEW v_under_delivery_bucket_strict AS
SELECT c.year, c.mgnr, c.bucket, c.min_kg, COALESCE(p.weight, 0) AS weight
FROM v_area_commitment_bucket_strict c
LEFT JOIN v_payment_bucket_strict p ON (p.year, p.mgnr, p.bucket) = (c.year, c.mgnr, c.bucket)
ORDER BY c.year, c.mgnr, c.bucket;
CREATE VIEW v_under_delivery_bucket AS
SELECT u.year, u.mgnr, u.bucket, u.min_kg,
u.weight + SUM(MAX(COALESCE(p.weight - s.min_kg, 0), 0)) AS weight
FROM v_under_delivery_bucket_strict u
LEFT JOIN v_payment_bucket_strict p ON (p.year, p.mgnr, p.sortid) = (u.year, u.mgnr, u.bucket) AND p.attrid IS NOT NULL
LEFT JOIN wine_attribute a ON a.attrid = p.attrid
LEFT JOIN v_area_commitment_bucket_strict s ON (s.year, s.mgnr, s.bucket) = (p.year, p.mgnr, p.bucket)
WHERE (p.gebunden IS NULL OR p.gebunden) AND (a.strict IS NULL OR a.strict = FALSE)
GROUP BY u.year, u.mgnr, u.bucket
ORDER BY u.year, u.mgnr, u.bucket;
CREATE VIEW v_under_delivery AS
SELECT year, mgnr, bucket, min_kg, weight, weight - min_kg AS diff
FROM v_under_delivery_bucket
WHERE diff < 0;
+63
View File
@@ -0,0 +1,63 @@
-- schema version 7 to 8
INSERT INTO wb_gem
SELECT a.gkz, 'WLNO'
FROM AT_gem a
LEFT JOIN wb_gem w ON w.gkz = a.gkz
WHERE a.gkz / 10000 = 3 AND w.hkid IS NULL;
INSERT INTO wb_gem
SELECT gkz, 'SLVL'
FROM AT_gem
WHERE gkz / 100 IN (617, 622, 623);
INSERT INTO wb_gem
SELECT gkz, 'SLSS'
FROM AT_gem
WHERE gkz / 100 = 610;
UPDATE wb_gem
SET hkid = 'SLVL'
WHERE gkz IN (61007, 61052, 61001, 61055, 61027, 61057, 61008, 61057);
INSERT INTO wb_gem
SELECT gkz, 'SLWS'
FROM AT_gem
WHERE gkz / 100 IN (603, 616) OR gkz IN (60101, 60663, 60651, 60659, 60664, 60647, 60641, 60639, 60665, 60669, 60618, 60629, 60608, 60670, 60624, 60660, 60656, 60655);
INSERT INTO wb_gem
SELECT g.gkz, 'SLVL'
FROM AT_gem g
LEFT JOIN wb_gem w ON w.gkz = g.gkz
WHERE g.gkz / 100 = 606 AND w.hkid IS NULL;
INSERT INTO wb_gem
SELECT g.gkz, 'SLST'
FROM AT_gem g
LEFT JOIN wb_gem w ON w.gkz = g.gkz
WHERE g.gkz / 10000 = 6 AND w.hkid IS NULL;
INSERT INTO wb_gem
SELECT gkz, 'BLOO'
FROM AT_gem
WHERE gkz / 10000 = 4;
INSERT INTO wb_gem
SELECT gkz, 'BLKA'
FROM AT_gem
WHERE gkz / 10000 = 2;
INSERT INTO wb_gem
SELECT gkz, 'BLSB'
FROM AT_gem
WHERE gkz / 10000 = 5;
INSERT INTO wb_gem
SELECT gkz, 'BLTI'
FROM AT_gem
WHERE gkz / 10000 = 7;
INSERT INTO wb_gem
SELECT gkz, 'BLVO'
FROM AT_gem
WHERE gkz / 10000 = 8;
+85
View File
@@ -0,0 +1,85 @@
-- schema version 8 to 9
CREATE TABLE payment_delivery_part_bucket (
year INTEGER NOT NULL,
did INTEGER NOT NULL,
dpnr INTEGER NOT NULL,
bktnr INTEGER NOT NULL,
avnr INTEGER NOT NULL,
price INTEGER NOT NULL,
amount INTEGER NOT NULL,
CONSTRAINT pk_payment_delivery_part_bucket PRIMARY KEY (year, did, dpnr, bktnr, avnr),
CONSTRAINT fk_payment_delivery_part_bucket_delivery_part_bucket FOREIGN KEY (year, did, dpnr, bktnr) REFERENCES delivery_part_bucket (year, did, dpnr, bktnr)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT fk_payment_delivery_part_bucket_payment_variant FOREIGN KEY (year, avnr) REFERENCES payment_variant (year, avnr)
ON UPDATE CASCADE
ON DELETE CASCADE
) STRICT;
DROP TRIGGER IF EXISTS t_payment_delivery_part_i;
DROP TRIGGER IF EXISTS t_payment_delivery_part_u;
DROP TRIGGER IF EXISTS t_payment_delivery_part_d;
ALTER TABLE payment_delivery_part RENAME COLUMN amount TO net_amount;
ALTER TABLE payment_delivery_part ADD COLUMN amount INTEGER NOT NULL GENERATED ALWAYS AS (ROUND(net_amount * (1 + mod_rel) + mod_abs)) VIRTUAL;
CREATE TRIGGER t_payment_delivery_part_bucket_i
AFTER INSERT ON payment_delivery_part_bucket FOR EACH ROW
BEGIN
INSERT INTO payment_delivery_part (year, did, dpnr, avnr, net_amount)
VALUES (NEW.year, NEW.did, NEW.dpnr, NEW.avnr, NEW.amount)
ON CONFLICT DO UPDATE SET net_amount = net_amount + NEW.amount;
END;
CREATE TRIGGER t_payment_delivery_part_bucket_u
AFTER UPDATE OF amount ON payment_delivery_part_bucket FOR EACH ROW
BEGIN
UPDATE payment_delivery_part
SET net_amount = net_amount - OLD.amount
WHERE (year, did, dpnr, avnr) = (NEW.year, NEW.did, NEW.dpnr, NEW.avnr);
UPDATE payment_delivery_part
SET net_amount = net_amount + NEW.amount
WHERE (year, did, dpnr, avnr) = (NEW.year, NEW.did, NEW.dpnr, NEW.avnr);
END;
CREATE TRIGGER t_payment_delivery_part_bucket_d
AFTER DELETE ON payment_delivery_part_bucket FOR EACH ROW
BEGIN
UPDATE payment_delivery_part
SET net_amount = net_amount - OLD.amount
WHERE (year, did, dpnr, avnr) = (OLD.year, OLD.did, OLD.dpnr, OLD.avnr);
END;
ALTER TABLE payment_member RENAME COLUMN amount TO net_amount;
ALTER TABLE payment_member ADD COLUMN mod_abs INTEGER NOT NULL DEFAULT 0;
ALTER TABLE payment_member ADD COLUMN mod_rel REAL NOT NULL DEFAULT 0;
ALTER TABLE payment_member ADD COLUMN amount INTEGER NOT NULL GENERATED ALWAYS AS (ROUND(net_amount * (1 + mod_rel) + mod_rel)) VIRTUAL;
CREATE TRIGGER t_payment_delivery_part_i
AFTER INSERT ON payment_delivery_part FOR EACH ROW
BEGIN
INSERT INTO payment_member (year, avnr, mgnr, net_amount)
VALUES (NEW.year, NEW.avnr, (SELECT mgnr FROM delivery WHERE (year, did) = (NEW.year, NEW.did)), NEW.amount)
ON CONFLICT DO UPDATE SET net_amount = net_amount + excluded.net_amount;
END;
CREATE TRIGGER t_payment_delivery_part_u
AFTER UPDATE OF amount ON payment_delivery_part FOR EACH ROW
BEGIN
UPDATE payment_member
SET net_amount = net_amount - OLD.amount
WHERE (year, avnr, mgnr) = (NEW.year, NEW.avnr, (SELECT mgnr FROM delivery WHERE (year, did) = (NEW.year, NEW.did)));
UPDATE payment_member
SET net_amount = net_amount + NEW.amount
WHERE (year, avnr, mgnr) = (NEW.year, NEW.avnr, (SELECT mgnr FROM delivery WHERE (year, did) = (NEW.year, NEW.did)));
END;
CREATE TRIGGER t_payment_delivery_part_d
AFTER DELETE ON payment_delivery_part FOR EACH ROW
BEGIN
UPDATE payment_member
SET net_amount = net_amount - OLD.amount
WHERE (year, avnr, mgnr) = (OLD.year, OLD.avnr, (SELECT mgnr FROM delivery WHERE (year, did) = (OLD.year, OLD.did)));
END;
+19
View File
@@ -0,0 +1,19 @@
-- schema version 9 to 10
UPDATE wine_quality_level SET min_kmw = 10.6 WHERE qualid = 'RSW';
DROP VIEW v_area_commitment_bucket;
CREATE VIEW v_area_commitment_bucket AS
SELECT year, mgnr, bucket, area, min_kg, max_kg
FROM v_area_commitment_bucket_strict
WHERE attrid IS NOT NULL
UNION ALL
SELECT b.year, b.mgnr, b.sortid,
SUM(b.area) AS area,
SUM(b.min_kg) AS min_kg,
SUM(b.upper_max_kg) AS max_kg
FROM v_area_commitment_bucket_strict b
LEFT JOIN wine_attribute a ON a.attrid = b.attrid
WHERE a.strict IS NULL OR a.strict = FALSE
GROUP BY b.year, b.mgnr, b.sortid
ORDER BY year, mgnr, bucket;
+73
View File
@@ -0,0 +1,73 @@
-- schema version 10 to 11
CREATE TABLE payment_delivery_part_new (
year INTEGER NOT NULL,
did INTEGER NOT NULL,
dpnr INTEGER NOT NULL,
avnr INTEGER NOT NULL,
net_amount INTEGER NOT NULL,
mod_abs INTEGER NOT NULL DEFAULT 0,
mod_rel REAL NOT NULL DEFAULT 0,
amount INTEGER NOT NULL GENERATED ALWAYS AS (ROUND(net_amount * (1 + mod_rel) + mod_abs)) STORED,
CONSTRAINT pk_payment_delivery_part PRIMARY KEY (year, did, dpnr, avnr),
CONSTRAINT fk_payment_delivery_part_delivery_part FOREIGN KEY (year, did, dpnr) REFERENCES delivery_part (year, did, dpnr)
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT fk_payment_delivery_part_payment_variant FOREIGN KEY (year, avnr) REFERENCES payment_variant (year, avnr)
ON UPDATE CASCADE
ON DELETE CASCADE
) STRICT;
INSERT INTO payment_delivery_part_new (year, did, dpnr, avnr, net_amount, mod_abs, mod_rel)
SELECT year, did, dpnr, avnr, net_amount, mod_abs, mod_rel
FROM payment_delivery_part;
PRAGMA writable_schema = ON;
DROP TABLE payment_delivery_part;
ALTER TABLE payment_delivery_part_new RENAME TO payment_delivery_part;
PRAGMA writable_schema = OFF;
DROP TRIGGER IF EXISTS t_payment_delivery_part_i;
CREATE TRIGGER t_payment_delivery_part_i
AFTER INSERT ON payment_delivery_part FOR EACH ROW
BEGIN
INSERT INTO payment_member (year, avnr, mgnr, net_amount)
SELECT year, NEW.avnr, mgnr, NEW.amount FROM delivery WHERE (year, did) = (NEW.year, NEW.did)
ON CONFLICT DO UPDATE SET net_amount = net_amount + excluded.net_amount;
END;
DROP TRIGGER IF EXISTS t_payment_delivery_part_u;
CREATE TRIGGER t_payment_delivery_part_u
AFTER UPDATE ON payment_delivery_part FOR EACH ROW
BEGIN
UPDATE payment_member
SET net_amount = net_amount - OLD.amount
WHERE (year, avnr, mgnr) IN (SELECT year, OLD.avnr, mgnr FROM delivery WHERE (year, did) = (OLD.year, OLD.did));
INSERT INTO payment_member (year, avnr, mgnr, net_amount)
SELECT year, NEW.avnr, mgnr, NEW.amount FROM delivery WHERE (year, did) = (NEW.year, NEW.did)
ON CONFLICT DO UPDATE SET net_amount = net_amount + excluded.net_amount;
END;
DROP TRIGGER IF EXISTS t_payment_delivery_part_d;
CREATE TRIGGER t_payment_delivery_part_d
AFTER DELETE ON payment_delivery_part FOR EACH ROW
BEGIN
UPDATE payment_member
SET net_amount = net_amount - OLD.amount
WHERE (year, avnr, mgnr) IN (SELECT year, OLD.avnr, mgnr FROM delivery WHERE (year, did) = (OLD.year, OLD.did));
END;
DROP TRIGGER IF EXISTS t_payment_delivery_part_bucket_u;
CREATE TRIGGER t_payment_delivery_part_bucket_u
AFTER UPDATE ON payment_delivery_part_bucket FOR EACH ROW
BEGIN
UPDATE payment_delivery_part
SET net_amount = net_amount - OLD.amount
WHERE (year, did, dpnr, avnr) = (OLD.year, OLD.did, OLD.dpnr, OLD.avnr);
UPDATE payment_delivery_part
SET net_amount = net_amount + NEW.amount
WHERE (year, did, dpnr, avnr) = (NEW.year, NEW.did, NEW.dpnr, NEW.avnr);
END;
ALTER TABLE payment_member DROP COLUMN amount;
ALTER TABLE payment_member ADD COLUMN amount INTEGER NOT NULL GENERATED ALWAYS AS (ROUND(net_amount * (1 + mod_rel) + mod_abs)) VIRTUAL;
+11
View File
@@ -0,0 +1,11 @@
-- schema version 11 to 12
CREATE VIEW v_stat_member AS
SELECT year, mgnr,
SUM(weight) AS sum,
ROUND(SUM(kmw * weight) / SUM(weight), 2) AS kmw,
ROUND(SUM(oe * weight) / SUM(weight), 1) AS oe,
COUNT(DISTINCT did) AS lieferungen
FROM v_delivery
GROUP BY year, mgnr
ORDER BY year, mgnr;
+7 -4
View File
@@ -427,8 +427,9 @@ namespace Elwig.Windows {
}
protected void TextBox_TextChanged(object sender, RoutedEventArgs? evt) {
var input = (TextBox)sender;
if (SenderIsRequired(input) && input.Text.Length == 0) {
var input = (Control)sender;
var tb = input as TextBox ?? (input as UnitTextBox)?.TextBox;
if (SenderIsRequired(input) && tb?.Text.Length == 0) {
ValidateInput(input, false);
ControlUtils.SetInputInvalid(input);
} else {
@@ -472,11 +473,13 @@ namespace Elwig.Windows {
}
protected void IntegerInput_TextChanged(object sender, TextChangedEventArgs evt) {
InputTextChanged((TextBox)sender, Validator.CheckInteger);
// FIXME
InputTextChanged((sender as UnitTextBox)?.TextBox ?? (TextBox)sender, Validator.CheckInteger);
}
protected void DecimalInput_TextChanged(object sender, TextChangedEventArgs evt) {
InputTextChanged((TextBox)sender, Validator.CheckDecimal);
// FIXME
InputTextChanged((sender as UnitTextBox)?.TextBox ?? (TextBox)sender, Validator.CheckDecimal);
}
protected void PartialDateInput_TextChanged(object sender, TextChangedEventArgs evt) {
+1 -1
View File
@@ -375,7 +375,7 @@
</Grid>
</TabItem>
<TabItem Header="Saisons">
<TabItem Header="Saisons" x:Name="Seasons">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180"/>
+22 -21
View File
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using System.Windows;
@@ -45,9 +46,9 @@ namespace Elwig.Windows {
AvNr = avnr;
var v = Context.PaymentVariants.Find(year, avnr);
Title = $"{v?.Name} - Lese {year} - Elwig";
ExemptInputs = new Control[] {
ExemptInputs = [
GraphList, OechsleInput, PriceInput, FreeZoomInput, GradationLinesInput, TooltipInput
};
];
}
private void Window_Loaded(object sender, RoutedEventArgs evt) {
@@ -60,15 +61,19 @@ namespace Elwig.Windows {
await RefreshGraphListQuery();
}
private async Task RefreshGraphListQuery(bool updateSort = false) {
List<PaymentVar> paymentVars = await Context.PaymentVariants.Where(p => p.Year == Year && p.AvNr == AvNr).ToListAsync();
if (paymentVars.Count != 1) {
return;
private static JsonObject? ParseData(PaymentVar variant) {
try {
return BillingData.ParseJson(variant.Data);
} catch (ArgumentException) {
return null;
}
}
PaymentVar paymentVar = paymentVars[0];
var data = JsonNode.Parse(paymentVar.Data).AsObject();
private async Task RefreshGraphListQuery(bool updateSort = false) {
var paymentVar = await Context.PaymentVariants.FindAsync(Year, AvNr);
if (paymentVar == null) return;
var data = ParseData(paymentVar);
if (data == null) return;
var auszahlungsSorten = data["AuszahlungSorten"]?.AsObject();
if (auszahlungsSorten == null) {
@@ -81,7 +86,7 @@ namespace Elwig.Windows {
return;
}
List<Graph> GraphsList = new();
List<Graph> GraphsList = [];
int i = 1;
foreach (var graph in Graphs) {
@@ -97,8 +102,8 @@ namespace Elwig.Windows {
RefreshInputs();
}
private String ParseContracts(JsonObject auszahlungsSorten, int num) {
List<string> contracts = new();
private string ParseContracts(JsonObject auszahlungsSorten, int num) {
List<string> contracts = [];
foreach (var sorte in auszahlungsSorten) {
if (sorte.Key == "Kurven") continue;
@@ -111,18 +116,14 @@ namespace Elwig.Windows {
}
}
return string.Join("\n", contracts.ToArray());
return string.Join("\n", contracts);
}
private async Task<bool> RemoveGraph(int num) {
List<PaymentVar> paymentVars = await Context.PaymentVariants.Where(p => p.Year == Year && p.AvNr == AvNr).ToListAsync();
if (paymentVars.Count != 1) {
return false;
}
PaymentVar paymentVar = paymentVars[0];
var data = JsonNode.Parse(paymentVar.Data).AsObject();
var paymentVar = await Context.PaymentVariants.FindAsync(Year, AvNr);
if (paymentVar == null) return false;
var data = ParseData(paymentVar);
if (data == null) return false;
var auszahlungsSorten = data["AuszahlungSorten"]?.AsObject();
if (auszahlungsSorten == null) {
+3 -2
View File
@@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
@@ -391,8 +392,8 @@ namespace Elwig.Windows {
if (e.Length == 1) filter.RemoveAt(i--);
} else if (e.Length > 1 && Utils.FromToRegex.IsMatch(e)) {
var parts = e.Split("-");
double? from = (parts[0].Length > 0) ? double.Parse(parts[0].Replace(".", ",")) : null;
double? to = (parts[1].Length > 0) ? double.Parse(parts[1].Replace(".", ",")) : null;
double? from = (parts[0].Length > 0) ? double.Parse(parts[0], CultureInfo.InvariantCulture) : null;
double? to = (parts[1].Length > 0) ? double.Parse(parts[1], CultureInfo.InvariantCulture) : null;
switch ((from, to)) {
case ( <= 30, <= 30):
case ( <= 30, null):
+6 -3
View File
@@ -1,4 +1,5 @@
<local:AdministrationWindow x:Class="Elwig.Windows.MemberAdminWindow"
<local:AdministrationWindow
x:Class="Elwig.Windows.MemberAdminWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -53,8 +54,10 @@
<MenuItem Header="Drucken">
<MenuItem x:Name="Menu_Print_Letterhead" Header="Briefkopf drucken"
Click="Menu_Print_Letterhead_Click"/>
<MenuItem x:Name="Menu_Show_Memberdata" Header="Stammdatenblatt anzeigen" Click="Menu_Show_Memberdata_Click"/>
<MenuItem x:Name="Menu_Print_Memberdata" Header="Stammdatenblatt drucken" Click="Menu_Print_Memberdata_Click"/>
<MenuItem x:Name="Menu_Show_MemberDataSheet" Header="Stammdatenblatt anzeigen" IsEnabled="False" Tag="Print"
Click="Menu_Show_MemberDataSheet_Click"/>
<MenuItem x:Name="Menu_Print_MemberDataSheet" Header="Stammdatenblatt drucken" IsEnabled="False" Tag="Print"
Click="Menu_Print_MemberDataSheet_Click"/>
<MenuItem Header="Briefköpfe drucken">
<MenuItem x:Name="Menu_Print_Letterheads_MgNr" Header="nach MgNr. sortiert" IsEnabled="False" Tag="Print"
Click="Menu_Print_Letterheads_MgNr_Click"/>
+17 -20
View File
@@ -15,29 +15,25 @@ using Elwig.Documents;
namespace Elwig.Windows {
public partial class MemberAdminWindow : AdministrationWindow {
private List<string> TextFilter = new();
private List<string> TextFilter = [];
private readonly RoutedCommand CtrlF = new();
private readonly (ComboBox, TextBox, TextBox)[] PhoneNrInputs;
private static ObservableCollection<KeyValuePair<string, string>> PhoneNrTypes { get; set; } = new() {
new("landline", "Tel.-Nr. (Festnetz)"),
new("mobile", "Tel.-Nr. (mobil)"),
new("fax", "Fax-Nr."),
};
private static ObservableCollection<KeyValuePair<string, string>> PhoneNrTypes { get; set; } = new(Utils.PhoneNrTypes);
public MemberAdminWindow() {
InitializeComponent();
CtrlF.InputGestures.Add(new KeyGesture(Key.F, ModifierKeys.Control));
CommandBindings.Add(new CommandBinding(CtrlF, FocusSearchInput));
ExemptInputs = new Control[] {
ExemptInputs = [
SearchInput, ActiveMemberInput, MemberList,
};
RequiredInputs = new Control[] {
];
RequiredInputs = [
MgNrInput, GivenNameInput, FamilyNameInput,
AddressInput, PlzInput, OrtInput, BillingOrtInput,
BusinessSharesInput, BranchInput, DefaultKgInput
};
PhoneNrInputs = new (ComboBox, TextBox, TextBox)[] {
];
PhoneNrInputs = [
(PhoneNr1TypeInput, PhoneNr1Input, PhoneNr1CommentInput),
(PhoneNr2TypeInput, PhoneNr2Input, PhoneNr2CommentInput),
(PhoneNr3TypeInput, PhoneNr3Input, PhoneNr3CommentInput),
@@ -47,7 +43,7 @@ namespace Elwig.Windows {
(PhoneNr7TypeInput, PhoneNr7Input, PhoneNr7CommentInput),
(PhoneNr8TypeInput, PhoneNr8Input, PhoneNr8CommentInput),
(PhoneNr9TypeInput, PhoneNr9Input, PhoneNr9CommentInput),
};
];
foreach (var input in PhoneNrInputs) input.Item1.ItemsSource = PhoneNrTypes;
InitializeDelayTimer(SearchInput, SearchInput_TextChanged);
@@ -57,8 +53,9 @@ namespace Elwig.Windows {
private void Window_Loaded(object sender, RoutedEventArgs evt) {
Menu_Print_Letterheads_MgNr.IsEnabled = App.IsPrintingReady;
Menu_Print_Letterheads_Name.IsEnabled = App.IsPrintingReady;
Menu_Show_Memberdata.IsEnabled = App.IsPrintingReady;
Menu_Print_Memberdata.IsEnabled = App.IsPrintingReady;
Menu_Print_Letterheads_Plz.IsEnabled = App.IsPrintingReady;
Menu_Show_MemberDataSheet.IsEnabled = App.IsPrintingReady;
Menu_Print_MemberDataSheet.IsEnabled = App.IsPrintingReady;
ActiveMemberInput.IsChecked = true;
UpdatePhoneNrInputVisibility();
@@ -339,7 +336,7 @@ namespace Elwig.Windows {
await PrintLetterheads(2);
}
private async void Menu_Print_Memberdata_Click(object sender, RoutedEventArgs evt) {
private async void Menu_Print_MemberDataSheet_Click(object sender, RoutedEventArgs evt) {
if (MemberList.SelectedItem is not Member m)
return;
Mouse.OverrideCursor = Cursors.AppStarting;
@@ -353,7 +350,7 @@ namespace Elwig.Windows {
}
}
private async void Menu_Show_Memberdata_Click(object sender, RoutedEventArgs evt) {
private async void Menu_Show_MemberDataSheet_Click(object sender, RoutedEventArgs evt) {
if (MemberList.SelectedItem is not Member m)
return;
Mouse.OverrideCursor = Cursors.AppStarting;
@@ -649,8 +646,8 @@ namespace Elwig.Windows {
Menu_Member_SendEmail.IsEnabled = m.EmailAddresses.Count > 0;
Menu_Print_Letterhead.IsEnabled = true;
Menu_Show_Memberdata.IsEnabled = true;
Menu_Print_Memberdata.IsEnabled = true;
Menu_Show_MemberDataSheet.IsEnabled = true;
Menu_Print_MemberDataSheet.IsEnabled = true;
FinishInputFilling();
}
@@ -658,8 +655,8 @@ namespace Elwig.Windows {
new protected void ClearInputs(bool validate = false) {
Menu_Member_SendEmail.IsEnabled = false;
Menu_Print_Letterhead.IsEnabled = false;
Menu_Show_Memberdata.IsEnabled = false;
Menu_Print_Memberdata.IsEnabled = false;
Menu_Show_MemberDataSheet.IsEnabled = false;
Menu_Print_MemberDataSheet.IsEnabled = false;
StatusDeliveriesLastSeason.Text = $"Lieferungen ({Utils.CurrentLastSeason - 1}): -";
StatusDeliveriesThisSeason.Text = $"Lieferungen ({Utils.CurrentLastSeason}): -";
StatusAreaCommitment.Text = "Gebundene Fläche: -";
+82 -20
View File
@@ -6,7 +6,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Windows"
mc:Ignorable="d"
Title="Auszahlungsvarianten - Elwig" Height="450" Width="800" MinHeight="400" MinWidth="700">
Title="Auszahlungsvarianten - Elwig" Height="500" Width="820" MinHeight="500" MinWidth="820">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
@@ -41,8 +41,8 @@
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="200"/>
<RowDefinition Height="*"/>
<RowDefinition Height="200"/>
</Grid.RowDefinitions>
<ListBox x:Name="PaymentVariantList" Margin="10,10,35,10" Grid.RowSpan="2" SelectionChanged="PaymentVariantList_SelectionChanged">
@@ -66,34 +66,96 @@
VerticalAlignment="Center" HorizontalAlignment="Right" Width="25" Height="25" Margin="5,60,5,0" Grid.RowSpan="2"/>
<Label Content="Name:" Margin="10,10,0,0" Grid.Column="1"/>
<TextBox x:Name="NameInput" Width="200" Grid.Column="2" HorizontalAlignment="Left" Margin="0,10,0,0"/>
<TextBox x:Name="NameInput" Width="200" Grid.Column="2" HorizontalAlignment="Left" Margin="0,10,0,0"
TextChanged="NameInput_TextChanged"/>
<Label Content="Beschreibung:" Margin="10,40,0,0" Grid.Column="1"/>
<TextBox x:Name="CommentInput" Grid.Column="2" HorizontalAlignment="Stretch" Margin="0,40,10,0"/>
<TextBox x:Name="CommentInput" Grid.Column="2" HorizontalAlignment="Stretch" Margin="0,40,10,0"
TextChanged="CommentInput_TextChanged"/>
<Label Content="Erstellt am:" Margin="10,70,0,0" Grid.Column="1"/>
<TextBox x:Name="DateInput" Grid.Column="2" Width="77" HorizontalAlignment="Left" Margin="0,70,10,0"/>
<TextBox x:Name="DateInput" Grid.Column="2" Width="77" HorizontalAlignment="Left" Margin="0,70,10,0" IsReadOnly="True"/>
<Label Content="Überwiesen am:" Margin="10,100,0,0" Grid.Column="1"/>
<TextBox x:Name="TransferDateInput" Grid.Column="2" Width="77" HorizontalAlignment="Left" Margin="0,100,10,0"/>
<TextBox x:Name="TransferDateInput" Grid.Column="2" Width="77" HorizontalAlignment="Left" Margin="0,100,10,0"
TextChanged="TransferDateInput_TextChanged"/>
<TextBox x:Name="DataInput" Margin="82,70,10,42" Grid.Column="2"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="auto"
AcceptsReturn="True" VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Auto"
FontFamily="Cascadia Code Light" FontSize="13"
TextChanged="DataInput_TextChanged"/>
<Button x:Name="SaveButton" Content="Speichern" Grid.Column="1" Grid.ColumnSpan="2"
Click="SaveButton_Click"
VerticalAlignment="Bottom" HorizontalAlignment="Center" Width="100" Margin="10,10,325,10"/>
<Button x:Name="CommitButton" Content="Festsetzen" Grid.Column="1" Grid.ColumnSpan="2"
Click="CommitButton_Click"
VerticalAlignment="Bottom" HorizontalAlignment="Center" Width="100" Margin="10,10,115,10"/>
<Button x:Name="RevertButton" Content="Freigeben" Grid.Column="1" Grid.ColumnSpan="2"
Click="RevertButton_Click"
VerticalAlignment="Bottom" HorizontalAlignment="Center" Width="100" Margin="10,10,115,10"/>
<Button x:Name="CalculateButton" Content="Berechnen" Grid.Column="1" Grid.ColumnSpan="2"
Click="CalculateButton_Click"
VerticalAlignment="Bottom" HorizontalAlignment="Right" Width="100" Margin="10,10,115,10"/>
VerticalAlignment="Bottom" HorizontalAlignment="Center" Width="100" Margin="115,10,10,10"/>
<Button x:Name="EditButton" Content="Bearbeiten" Grid.Column="1" Grid.ColumnSpan="2"
Click="EditButton_Click"
VerticalAlignment="Bottom" HorizontalAlignment="Right" Width="100" Margin="10,10,10,10"/>
VerticalAlignment="Bottom" HorizontalAlignment="Center" Width="100" Margin="325,10,10,10"/>
<GroupBox Header="Abschluss" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="10,0,10,10">
<Grid>
<ProgressBar x:Name="ProgressBar" Margin="10,0,0,74" Height="27" Width="180"
VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
<Button x:Name="ShowButton" Content="Vorschau" FontSize="14" Width="180" Margin="10,10,10,42" Height="27" Tag="Print" IsEnabled="False"
Click="ShowButton_Click"
VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
<Button x:Name="PrintButton" Content="Drucken" FontSize="14" Width="180" Margin="10,10,10,10" Height="27" Tag="Print" IsEnabled="False"
Click="PrintButton_Click"
VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
</Grid>
</GroupBox>
<Grid Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="250"/>
</Grid.ColumnDefinitions>
<GroupBox Header="Abschluss" Margin="10,0,5,10">
<Grid>
<Button x:Name="ExportButton" Content="Exportieren" FontSize="14" Width="180" Margin="10,10,10,10" Height="27" IsEnabled="False"
Click="ExportButton_Click"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
<ProgressBar x:Name="ProgressBar" Margin="10,10,10,74" Height="27" Width="180"
VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
<Button x:Name="ShowButton" Content="Vorschau" FontSize="14" Width="180" Margin="10,10,10,42" Height="27" Tag="Print" IsEnabled="False"
Click="ShowButton_Click"
VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
<Button x:Name="PrintButton" Content="Drucken" FontSize="14" Width="180" Margin="10,10,10,10" Height="27" Tag="Print" IsEnabled="False"
Click="PrintButton_Click"
VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
</Grid>
</GroupBox>
<GroupBox Header="Ergebnis" Margin="5,0,10,10" Grid.Column="1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<Style TargetType="TextBlock">
<Setter Property="Grid.Column" Value="1"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="TextAlignment" Value="Right"/>
</Style>
</Grid.Resources>
<Label Content="Zu-/Abschläge:" Margin="10,10,0,0"/>
<TextBlock x:Name="ModifierSum" Margin="0,12,10,0"/>
<Label Content="Gesamtbeträge:" Margin="10,40,0,0"/>
<TextBlock x:Name="TotalSum" Margin="0,42,10,0"/>
<Label Content="Mehrwertsteuer:" Margin="10,70,0,0"/>
<TextBlock x:Name="VatSum" Margin="0,72,10,0"/>
<Label Content="Abzüge:" Margin="10,100,0,0"/>
<TextBlock x:Name="DeductionSum" Margin="0,102,10,0"/>
<Label Content="Auszuzahlen:" Margin="10,130,0,0"/>
<TextBlock x:Name="PaymentSum" Margin="0,132,10,0"/>
</Grid>
</GroupBox>
</Grid>
</Grid>
</local:ContextWindow>
+224 -7
View File
@@ -11,16 +11,25 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Elwig.Helpers.Billing;
using Elwig.Helpers.Export;
using Microsoft.Win32;
using System.Text.Json;
namespace Elwig.Windows {
public partial class PaymentVariantsWindow : ContextWindow {
public readonly int Year;
private bool DataValid, DataChanged, NameChanged, CommentChanged, TransferDateValid, TransferDateChanged;
private static readonly JsonSerializerOptions JsonOpt = new() { WriteIndented = true };
public PaymentVariantsWindow(int year) {
InitializeComponent();
Year = year;
Title = $"Auszahlungsvarianten - Lese {Year} - Elwig";
if (!App.Config.Debug) {
DataInput.Visibility = Visibility.Hidden;
}
}
protected override async Task OnRenewContext() {
@@ -30,26 +39,80 @@ namespace Elwig.Windows {
private void Update() {
if (PaymentVariantList.SelectedItem is PaymentVar v) {
var locked = v.TransferDate != null || !v.TestVariant;
var locked = !v.TestVariant;
DeleteButton.IsEnabled = !locked;
CalculateButton.IsEnabled = !locked;
CommitButton.IsEnabled = !locked;
CommitButton.Visibility = !locked ? Visibility.Visible : Visibility.Hidden;
RevertButton.IsEnabled = locked;
RevertButton.Visibility = locked ? Visibility.Visible : Visibility.Hidden;
CopyButton.IsEnabled = true;
EditButton.IsEnabled = true;
EditButton.Content = locked ? "Ansehen" : "Bearbeiten";
ShowButton.IsEnabled = true;
PrintButton.IsEnabled = true;
ExportButton.IsEnabled = locked;
NameInput.Text = v.Name;
CommentInput.Text = v.Comment;
DateInput.Text = $"{v.Date:dd.MM.yyyy}";
TransferDateInput.Text = $"{v.TransferDate:dd.MM.yyyy}";
if (App.Config.Debug) {
try {
var json = BillingData.ParseJson(v.Data);
DataInput.Text = JsonSerializer.Serialize(json, JsonOpt);
} catch {
DataInput.Text = v.Data;
}
}
} else {
EditButton.IsEnabled = false;
EditButton.Content = "Bearbeiten";
CopyButton.IsEnabled = false;
CalculateButton.IsEnabled = false;
CommitButton.IsEnabled = false;
CommitButton.Visibility = Visibility.Visible;
RevertButton.IsEnabled = false;
RevertButton.Visibility = Visibility.Hidden;
DeleteButton.IsEnabled = false;
ShowButton.IsEnabled = false;
PrintButton.IsEnabled = false;
ExportButton.IsEnabled = false;
NameInput.Text = "";
CommentInput.Text = "";
DateInput.Text = "";
TransferDateInput.Text = "";
DataInput.Text = "";
}
UpdateSums();
UpdateSaveButton();
}
private void UpdateSaveButton() {
SaveButton.IsEnabled = PaymentVariantList.SelectedItem != null &&
((DataChanged && DataValid) || NameChanged || CommentChanged ||
(TransferDateChanged && TransferDateValid));
}
private void UpdateSums() {
if (PaymentVariantList.SelectedItem is PaymentVar v) {
var sym = v.Season.Currency.Symbol;
ModifierSum.Text = $"{v.DeliveryPartPayments.Sum(p => p.Amount - p.NetAmount):N2} {sym}";
TotalSum.Text = $"{v.MemberPayments.Sum(p => p.Amount):N2} {sym}";
if (v.Credits.Count == 0) {
VatSum.Text = $"- {sym}";
DeductionSum.Text = $"- {sym}";
PaymentSum.Text = $"- {sym}";
} else {
VatSum.Text = $"{v.Credits.Sum(c => c.VatAmount):N2} {sym}";
DeductionSum.Text = $"{-v.Credits.Sum(c => c.Modifiers ?? 0):N2} {sym}";
PaymentSum.Text = $"{v.Credits.Sum(c => c.Amount):N2} {sym}";
}
} else {
ModifierSum.Text = "-";
TotalSum.Text = "-";
VatSum.Text = "-";
DeductionSum.Text = "-";
PaymentSum.Text = "-";
}
}
@@ -74,8 +137,13 @@ namespace Elwig.Windows {
return;
CalculateButton.IsEnabled = false;
Mouse.OverrideCursor = Cursors.AppStarting;
var b = new BillingVariant(v.Year, v.AvNr);
await b.CalculatePrices();
try {
var b = new BillingVariant(v.Year, v.AvNr);
await b.Calculate();
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Berechnungsfehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
await App.HintContextChange();
Mouse.OverrideCursor = null;
CalculateButton.IsEnabled = true;
}
@@ -94,6 +162,154 @@ namespace Elwig.Windows {
await Generate(2);
}
private async void CommitButton_Click(object sender, RoutedEventArgs evt) {
if (PaymentVariantList.SelectedValue is not PaymentVar v)
return;
CommitButton.IsEnabled = false;
Mouse.OverrideCursor = Cursors.AppStarting;
var b = new BillingVariant(v.Year, v.AvNr);
await b.Commit();
Mouse.OverrideCursor = null;
RevertButton.IsEnabled = true;
await App.HintContextChange();
}
private async void RevertButton_Click(object sender, RoutedEventArgs evt) {
if (PaymentVariantList.SelectedValue is not PaymentVar v)
return;
var res = MessageBox.Show(
"Sollen wirklich alle festgesetzten Traubengutschriften der ausgewählten Auszahlungsvariante unwiderruflich gelöscht werden?\n\n" +
"Dies ist im Allgemeinen nie empfohlen. Handelt es sich um die aktuellste Auszahlungsvariante könnte das eine Ausnahme sein.",
"Traubengutschriften löschen", MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
if (res != MessageBoxResult.Yes)
return;
RevertButton.IsEnabled = false;
Mouse.OverrideCursor = Cursors.AppStarting;
var b = new BillingVariant(v.Year, v.AvNr);
await b.Revert();
Mouse.OverrideCursor = null;
CommitButton.IsEnabled = true;
await App.HintContextChange();
}
private async void ExportButton_Click(object sender, RoutedEventArgs evt) {
if (PaymentVariantList.SelectedValue is not PaymentVar v) {
return;
} else if (v.TransferDate == null) {
MessageBox.Show("Überweisungsdatum muss gesetzt sein!", "Exportieren nicht möglich", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
var d = new SaveFileDialog() {
FileName = $"{App.Client.NameToken}-Überweisungsdaten-{v.Year}-{v.Name.Trim().Replace(' ', '-')}.{Ebics.FileExtension}",
DefaultExt = Ebics.FileExtension,
Filter = "EBICS-Datei (*.xml)|*.xml",
Title = $"Überweisungsdaten speichern unter - Elwig",
};
if (d.ShowDialog() == true) {
ExportButton.IsEnabled = false;
Mouse.OverrideCursor = Cursors.AppStarting;
using var e = new Ebics(v, d.FileName);
await e.ExportAsync(Transaction.FromPaymentVariant(v));
Mouse.OverrideCursor = null;
ExportButton.IsEnabled = true;
}
}
private async void SaveButton_Click(object sender, RoutedEventArgs evt) {
if (PaymentVariantList.SelectedItem is not PaymentVar v) return;
try {
v.Name = NameInput.Text;
v.Comment = (CommentInput.Text != "") ? CommentInput.Text : null;
v.TransferDateString = (TransferDateInput.Text != "") ? string.Join("-", TransferDateInput.Text.Split(".").Reverse()) : null;
if (App.Config.Debug) v.Data = JsonSerializer.Serialize(BillingData.ParseJson(DataInput.Text));
Context.Update(v);
await Context.SaveChangesAsync();
await App.HintContextChange();
} catch (Exception exc) {
await HintContextChange();
var str = "Der Eintrag konnte nicht in der Datenbank aktualisiert werden!\n\n" + exc.Message;
if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;
MessageBox.Show(str, "Auszahlungsvariante aktualisieren", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void NameInput_TextChanged(object sender, TextChangedEventArgs evt) {
if (PaymentVariantList.SelectedItem is not PaymentVar v) {
ControlUtils.ClearInputState(NameInput);
return;
}
if (NameInput.Text != v.Name) {
ControlUtils.SetInputChanged(NameInput);
NameChanged = true;
} else {
ControlUtils.ClearInputState(NameInput);
NameChanged = false;
}
UpdateSaveButton();
}
private void CommentInput_TextChanged(object sender, TextChangedEventArgs evt) {
if (PaymentVariantList.SelectedItem is not PaymentVar v) {
ControlUtils.ClearInputState(CommentInput);
return;
}
if (CommentInput.Text != (v.Comment ?? "")) {
ControlUtils.SetInputChanged(CommentInput);
CommentChanged = true;
} else {
ControlUtils.ClearInputState(CommentInput);
CommentChanged = false;
}
UpdateSaveButton();
}
private void TransferDateInput_TextChanged(object sender, TextChangedEventArgs evt) {
if (PaymentVariantList.SelectedItem is not PaymentVar v) {
ControlUtils.ClearInputState(TransferDateInput);
return;
}
var res = Validator.CheckDate(TransferDateInput, false);
if (!res.IsValid) {
ControlUtils.SetInputInvalid(TransferDateInput);
TransferDateValid = false;
} else if (TransferDateInput.Text != $"{v.TransferDate:dd.MM.yyyy}") {
ControlUtils.SetInputChanged(TransferDateInput);
TransferDateValid = true;
TransferDateChanged = true;
} else {
ControlUtils.ClearInputState(TransferDateInput);
TransferDateValid = true;
TransferDateChanged = false;
}
UpdateSaveButton();
}
private void DataInput_TextChanged(object sender, TextChangedEventArgs evt) {
if (PaymentVariantList.SelectedItem is not PaymentVar v) {
ControlUtils.ClearInputState(DataInput);
return;
}
try {
var json = BillingData.ParseJson(DataInput.Text);
var origJson = v.Data;
try {
origJson = JsonSerializer.Serialize(BillingData.ParseJson(v.Data));
} catch { }
DataValid = true;
if (JsonSerializer.Serialize(json) != origJson) {
ControlUtils.SetInputChanged(DataInput);
DataChanged = true;
} else {
ControlUtils.ClearInputState(DataInput);
DataChanged = false;
}
} catch {
ControlUtils.SetInputInvalid(DataInput);
DataValid = false;
}
UpdateSaveButton();
}
private async Task Generate(int mode) {
if (PaymentVariantList.SelectedItem is not PaymentVar v)
return;
@@ -110,10 +326,11 @@ namespace Elwig.Windows {
members = members.OrderBy(m => m.MgNr);
IEnumerable<Member> list = await members.ToListAsync();
var data = await CreditNoteData.ForPaymentVariant(Context.CreditNoteRows, 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);
await Context.GetMemberAreaCommitmentBuckets(Year, 0);
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 => {
ProgressBar.Value = v;
+1 -1
View File
@@ -1,7 +1,7 @@
[general]
; Only needed, if more than one branch is stored in database
branch = Zweigstelle
;branch = Zweigstelle
;debug = true
[database]
+27
View File
@@ -0,0 +1,27 @@
using Elwig.Helpers;
using Microsoft.Data.Sqlite;
using System.Reflection;
namespace Tests {
[SetUpFixture]
public class DatabaseSetup {
private SqliteConnection? Connection;
[OneTimeSetUp]
public async Task SetupDatabase() {
AppDbContext.ConnectionStringOverride = $"Data Source=ElwigTestDB; Mode=Memory; Foreign Keys=True; Cache=Default";
Connection = await AppDbContext.ConnectAsync();
await AppDbContext.ExecuteEmbeddedScript(Connection, Assembly.GetExecutingAssembly(), "Tests.Resources.Create.sql");
}
[OneTimeTearDown]
public async Task TeardownDatabase() {
AppDbContext.ConnectionStringOverride = null;
if (Connection == null) return;
await Connection.DisposeAsync();
Connection = null;
// The in-memory database will be dropped if all connections to it are closed
}
}
}
+305
View File
@@ -0,0 +1,305 @@
using Elwig.Helpers;
using Elwig.Helpers.Billing;
namespace Tests.Helpers {
[TestFixture]
public class BillingDataTest {
private static readonly string[] AttributeVariants = ["GV", "GVD", "GVK", "GVS", "GVZ", "WR", "WRS", "ZW", "ZWS", "ZWZ"];
[OneTimeSetUp]
public async Task SetupBilling() {
await BillingData.Init();
}
private static (string, string?) GetSortIdAttrId(string bucket) {
return (bucket[..2], bucket.Length > 2 ? bucket[2..] : null);
}
private static string GetQualId(double kmw) {
return kmw switch {
>= 17.0 => "KAB",
>= 15.0 => "QUW",
>= 14.0 => "LDW",
>= 10.6 => "RSW",
_ => "WEI",
};
}
private static void TestCalcOe(BillingData data, string bucket, double oe, decimal expected, string? qualid = null, bool geb = false) {
var (sortid, attrid) = GetSortIdAttrId(bucket);
var kmw = Utils.OeToKmw(oe);
var v = data.CalculatePrice(sortid, attrid, qualid ?? GetQualId(kmw), geb, oe, kmw);
Assert.That(Math.Round(v, 6), Is.EqualTo(expected));
}
private static void TestCalcKmw(BillingData data, string bucket, double kmw, decimal expected, string? qualid = null, bool geb = false) {
var (sortid, attrid) = GetSortIdAttrId(bucket);
var oe = Utils.KmwToOe(kmw);
var v = data.CalculatePrice(sortid, attrid, qualid ?? GetQualId(kmw), geb, oe, kmw);
Assert.That(Math.Round(v, 6), Is.EqualTo(expected));
}
[Test]
public void Test_01_Flatrate() {
var data = BillingData.FromJson("""
{
"mode": "elwig",
"version": 1,
"payment": 0.5,
"curves": []
}
""", AttributeVariants);
Assert.Multiple(() => {
TestCalcOe(data, "GV", 73, 0.5m);
TestCalcOe(data, "WRS", 74, 0.5m);
});
}
[Test]
public void Test_02_Simple() {
var data = BillingData.FromJson("""
{
"mode": "elwig",
"version": 1,
"payment": "curve:1",
"curves": [{
"id": 1,
"mode": "oe",
"data": {
"72.0oe": 0.25,
"15.0kmw": 0.5,
"83oe": 1
},
"geb": 0.10
}]
}
""", AttributeVariants);
Assert.Multiple(() => {
TestCalcOe(data, "GV", 70, 0.25m);
TestCalcOe(data, "GV", 72, 0.25m);
TestCalcOe(data, "GV", 73, 0.50m);
TestCalcOe(data, "GV", 74, 0.55m);
TestCalcOe(data, "GV", 80, 0.85m);
TestCalcOe(data, "GV", 83, 1.00m);
TestCalcOe(data, "GV", 90, 1.00m);
TestCalcOe(data, "GV", 73, 0.60m, geb: true);
TestCalcOe(data, "GV", 74, 0.65m, geb: true);
TestCalcOe(data, "GV", 80, 0.95m, geb: true);
TestCalcOe(data, "GV", 83, 1.10m, geb: true);
TestCalcOe(data, "GV", 90, 1.10m, geb: true);
});
}
[Test]
public void Test_03_GreaterThanAndLessThan() {
var data = BillingData.FromJson("""
{
"mode": "elwig",
"version": 1,
"payment": "curve:1",
"curves": [{
"id": 1,
"mode": "kmw",
"data": {
"<14kmw": 0.1,
"14kmw": 0.2,
"<15kmw": 0.25,
"15kmw": 0.5,
"17kmw": 1,
">17kmw": 1.25
}
}]
}
""", AttributeVariants);
Assert.Multiple(() => {
TestCalcKmw(data, "GV", 13.00, 0.10m);
TestCalcKmw(data, "GV", 13.50, 0.10m);
TestCalcKmw(data, "GV", 13.99, 0.10m);
TestCalcKmw(data, "GV", 14.00, 0.20m);
TestCalcKmw(data, "GV", 14.50, 0.225m);
TestCalcKmw(data, "GV", 15.00, 0.50m);
TestCalcKmw(data, "GV", 15.50, 0.625m);
TestCalcKmw(data, "GV", 16.00, 0.75m);
TestCalcKmw(data, "GV", 16.50, 0.875m);
TestCalcKmw(data, "GV", 17.00, 1.00m);
TestCalcKmw(data, "GV", 17.01, 1.25m);
TestCalcKmw(data, "GV", 17.50, 1.25m);
TestCalcKmw(data, "GV", 18.00, 1.25m);
TestCalcKmw(data, "GV", 18.50, 1.25m);
});
}
[Test]
public void Test_04_VariantsAndAttributes() {
var data = BillingData.FromJson("""
{
"mode": "elwig",
"version": 1,
"payment": {
"default": 0.10,
"GV/": 0.20,
"ZW": 0.25,
"/S": 0.15,
"GV/K": 0.30
},
"curves": []
}
""", AttributeVariants);
Assert.Multiple(() => {
TestCalcOe(data, "WR", 73, 0.10m);
TestCalcOe(data, "WRS", 73, 0.15m);
TestCalcOe(data, "GV", 73, 0.20m);
TestCalcOe(data, "GVD", 73, 0.10m);
TestCalcOe(data, "GVK", 73, 0.30m);
TestCalcOe(data, "GVS", 73, 0.15m);
TestCalcOe(data, "GVZ", 73, 0.10m);
TestCalcOe(data, "ZW", 73, 0.25m);
TestCalcOe(data, "ZWS", 73, 0.15m);
TestCalcOe(data, "ZWZ", 73, 0.25m);
});
}
[Test]
public void Test_05_QualityLevel() {
var data = BillingData.FromJson("""
{
"mode": "elwig",
"version": 1,
"payment": 0.5,
"quality": {
"WEI": {
"default": 0.25,
"GV": 0.3,
"/S": 0.2
}
},
"curves": []
}
""", AttributeVariants);
Assert.Multiple(() => {
TestCalcOe(data, "GV", 75, 0.30m, qualid: "WEI");
TestCalcOe(data, "ZW", 76, 0.25m, qualid: "WEI");
TestCalcOe(data, "GVS", 75, 0.20m, qualid: "WEI");
TestCalcOe(data, "GVK", 74, 0.30m, qualid: "WEI");
TestCalcOe(data, "ZWS", 73, 0.20m, qualid: "WEI");
TestCalcOe(data, "GV", 70, 0.5m);
TestCalcOe(data, "GV", 72, 0.5m);
TestCalcOe(data, "GV", 73, 0.5m);
TestCalcOe(data, "ZWS", 74, 0.5m);
TestCalcOe(data, "GVK", 80, 0.5m);
});
}
[Test]
public void Test_06_ModeOeAndKmw() {
var data = BillingData.FromJson("""
{
"mode": "elwig",
"version": 1,
"payment": {
"default": 1.0,
"GV": "curve:1",
"ZW": "curve:2"
},
"curves": [{
"id": 1,
"mode": "oe",
"data": {
"73oe": 2.0,
"17kmw": 3.0
}
}, {
"id": 2,
"mode": "kmw",
"data": {
"73oe": 2.0,
"17kmw": 3.0
}
}]
}
""", AttributeVariants);
Assert.Multiple(() => {
TestCalcKmw(data, "GV", 15.0, 2.0m);
TestCalcKmw(data, "GV", 15.5, 2.272727m);
TestCalcKmw(data, "GV", 16.0, 2.454545m);
TestCalcKmw(data, "GV", 16.5, 2.727273m);
TestCalcKmw(data, "GV", 17.0, 3.0m);
TestCalcKmw(data, "ZW", 15.0, 2.0m);
TestCalcKmw(data, "ZW", 15.5, 2.25m);
TestCalcKmw(data, "ZW", 16.0, 2.50m);
TestCalcKmw(data, "ZW", 16.5, 2.75m);
TestCalcKmw(data, "ZW", 17.0, 3.0m);
});
}
[Test]
public void Test_07_MultipleCurves() {
var data = BillingData.FromJson("""
{
"mode": "elwig",
"version": 1,
"payment": {
"default": 0.25,
"/S": "curve:1",
"GV/": 0.75,
"GV/K": "curve:2",
"WR/S": "curve:3"
},
"curves": [{
"id": 3,
"mode": "kmw",
"data": {
"73oe": 0.7,
"17kmw": 0.8
},
"geb": {
"15kmw": 0.8,
"17kmw": 0.95
}
}, {
"id": 1,
"mode": "kmw",
"data": {
"73oe": 0.5,
"17kmw": 0.6
},
"geb": 0.1
}, {
"id": 2,
"mode": "kmw",
"data": {
"15kmw": 0.6,
"17kmw": 0.7
},
"geb": {
"73oe": 0.65,
"17kmw": 0.80
}
}]
}
""", AttributeVariants);
Assert.Multiple(() => {
TestCalcKmw(data, "GV", 15.0, 0.75m);
TestCalcKmw(data, "GVS", 15.0, 0.50m);
TestCalcKmw(data, "GVS", 16.0, 0.55m);
TestCalcKmw(data, "GVS", 17.0, 0.60m);
TestCalcKmw(data, "GVS", 15.0, 0.60m, geb: true);
TestCalcKmw(data, "GVS", 16.0, 0.65m, geb: true);
TestCalcKmw(data, "GVS", 17.0, 0.70m, geb: true);
TestCalcKmw(data, "GVK", 15.0, 0.60m);
TestCalcKmw(data, "GVK", 16.0, 0.65m);
TestCalcKmw(data, "GVK", 17.0, 0.70m);
TestCalcKmw(data, "GVK", 15.0, 0.65m, geb: true);
TestCalcKmw(data, "GVK", 16.0, 0.725m, geb: true);
TestCalcKmw(data, "GVK", 17.0, 0.80m, geb: true);
TestCalcKmw(data, "WRS", 15.0, 0.70m);
TestCalcKmw(data, "WRS", 16.0, 0.75m);
TestCalcKmw(data, "WRS", 17.0, 0.80m);
TestCalcKmw(data, "WRS", 15.0, 0.80m, geb: true);
TestCalcKmw(data, "WRS", 16.0, 0.875m, geb: true);
TestCalcKmw(data, "WRS", 17.0, 0.95m, geb: true);
});
}
}
}
+30
View File
@@ -0,0 +1,30 @@
using Elwig.Helpers;
using Microsoft.Data.Sqlite;
using System.Reflection;
namespace Tests.Helpers {
[TestFixture]
public class BillingTest {
private SqliteConnection? Connection;
[OneTimeSetUp]
public async Task SetupDatabase() {
Connection = await AppDbContext.ConnectAsync();
await AppDbContext.ExecuteEmbeddedScript(Connection, Assembly.GetExecutingAssembly(), "Tests.Resources.BillingInsert.sql");
}
[OneTimeTearDown]
public async Task TeardownDatabase() {
if (Connection == null) return;
await AppDbContext.ExecuteEmbeddedScript(Connection, Assembly.GetExecutingAssembly(), "Tests.Resources.BillingDelete.sql");
await Connection.DisposeAsync();
Connection = null;
}
[Test]
public void Test() {
// TODO
}
}
}
@@ -1,8 +1,8 @@
using Elwig.Helpers;
namespace Tests {
namespace Tests.Helpers {
[TestFixture]
public class HelpersUtilsTest {
public class UtilsTest {
private static readonly double[,] Gradation = new double[,] {
{ 14.0, 68.0 },
@@ -1,10 +1,10 @@
using Elwig.Helpers;
using System.Windows.Controls;
namespace Tests {
namespace Tests.Helpers {
[TestFixture]
[Apartment(ApartmentState.STA)]
public class HelpersValidatorTest {
public class ValidatorTest {
private static TextBox TB(string value, int caret = 0) {
return new() {
+1
View File
@@ -0,0 +1 @@
-- deletes for HelpersBillingTest
+1
View File
@@ -0,0 +1 @@
-- inserts for HelpersBillingTest
+9 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -9,6 +9,14 @@
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\*.sql" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="call fetch-resources.bat" />
</Target>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
+1
View File
@@ -0,0 +1 @@
curl -s "https://www.necronda.net/elwig/files/create.sql?v=12" -u "elwig:ganzGeheim123!" -o "Resources\Create.sql"