Compare commits

..
2 Commits
Author SHA1 Message Date
lorenz.stechauner e816979efb ChartWindow: Automatically calculate variant when saved
Test / Run tests (push) Successful in 1m49s
2025-07-03 11:48:03 +02:00
lorenz.stechauner 132864b4fc PaymentVariantsWindow: Add estimates for uncommited variants
Test / Run tests (push) Successful in 2m18s
2025-07-03 11:33:56 +02:00
269 changed files with 8619 additions and 20168 deletions

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
name: Deploy
on:
push:
tags: ["v[0-9]+.[0-9]+.[0-9]+.[0-9]+"]
tags: ["v[0-9]+.[0-9]+.[0-9]+"]
jobs:
deploy:
name: Build and Deploy
+1 -11
View File
@@ -2,7 +2,7 @@ name: Test
on:
push:
branches: ["**"]
paths: ["Elwig/**", "Tests/**", ".gitea/workflows/test.yaml"]
paths: ["Elwig/**", "Tests/**", "Installer/Files/*.exe", ".gitea/workflows/test.yaml"]
jobs:
test:
name: Run tests
@@ -23,16 +23,6 @@ jobs:
echo "No files with BOM found"
exit 0
}
- name: Check for code smells
shell: powershell
run: |
git grep -IEn "\.(Single|First|Min|Max|Any)(OrDefault)?Async\([^)]|^using System.Data.Entity;"
if ( $lastexitcode -ne 1 ) {
exit 1
} else {
echo "No files with code smells found"
exit 0
}
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1.1
- name: Setup NuGet
-1
View File
@@ -7,4 +7,3 @@ Tests/Resources/Sql/Create.sql
*.exe
!WinziPrint.exe
*.sqlite3
*.zip
+245 -947
View File
File diff suppressed because it is too large. Load diff
+11 -18
View File
@@ -2,11 +2,18 @@
x:Class="Elwig.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrl="clr-namespace:Elwig.Controls">
xmlns:ctrl="clr-namespace:Elwig.Controls"
Exit="Application_Exit">
<Application.Resources>
<ctrl:BoolToStringConverter x:Key="BoolToStarConverter" FalseValue="" TrueValue="*"/>
<ctrl:WidthToMarginConverter x:Key="WidthToMarginConverter"/>
<DataTemplate x:Key="PostalDestTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Dest}"/>
<TextBlock Text="{Binding Ort.Name, StringFormat='{} ({0})'}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="KgNrTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
@@ -23,37 +30,23 @@
<DataTemplate x:Key="WineVarietyTemplateCollapsed">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Foreground="{Binding Color}"/>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="WineVarietyTemplateExpanded">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding SortIdFormat}" Foreground="{Binding Color}" MinWidth="36" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Name}" Foreground="{Binding Color}"/>
<TextBlock Text="{Binding SortId}" MinWidth="36" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding CommentFormat}" FontSize="10" VerticalAlignment="Bottom" Margin="0,0,0,2"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="AreaCommitmentTypeTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding WineVar.Name}" Foreground="{Binding WineVar.Color}"/>
<TextBlock Text="{Binding WineAttr.Name}" Margin="5,0,0,0"/>
<TextBlock Text="{Binding Discriminator}" Margin="5,0,0,0"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="ModifierTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" MinWidth="250" Margin="0,0,10,0"/>
<TextBlock Text="{Binding ValueStr}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="PublicModifierTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" MinWidth="250" Margin="0,0,10,0"/>
<TextBlock Text="{Binding PublicValueStr}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="WineAttributeTemplate">
<StackPanel Orientation="Horizontal">
+55 -190
View File
@@ -1,41 +1,37 @@
using Elwig.Dialogs;
using Elwig.Helpers;
using Elwig.Helpers.Billing;
using Elwig.Helpers.Export;
using Elwig.Helpers.Printing;
using Elwig.Helpers.Weighing;
using Elwig.Models.Entities;
using Elwig.Services;
using Elwig.Windows;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.IO;
using Elwig.Helpers;
using Elwig.Helpers.Weighing;
using System.Collections.Generic;
using System.Windows.Threading;
using System.Reflection;
using Elwig.Helpers.Printing;
using Elwig.Windows;
using Elwig.Dialogs;
using System.Threading.Tasks;
using Elwig.Helpers.Billing;
using Elwig.Models.Entities;
using System.Text;
namespace Elwig {
public partial class App : Application {
public static App CurrentApp;
protected static App CurrentApp;
public static int NumWindows => CurrentApp.Windows.Count;
public static bool ForceShutdown { get; set; } = false;
public static bool ForceShutdown { get; private set; } = false;
private readonly DispatcherTimer _autoUpdateTimer = new() { Interval = TimeSpan.FromHours(1) };
private readonly DispatcherTimer _autoBackupTimer = new() { Interval = TimeSpan.FromMinutes(10) };
public readonly SerialPortWatcher SerialPortWatcher = new();
public static readonly string DataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Elwig");
public static readonly string MailsPath = Path.Combine(DataPath, "mails");
public static readonly string ConfigPath = Path.Combine(DataPath, "config.ini");
public static readonly string InstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Elwig");
public static readonly string DocumentsPath = (Assembly.GetEntryAssembly()?.Location.Contains(@"\bin\") ?? false) ?
Path.Combine(Assembly.GetEntryAssembly()!.Location.Split(@"\bin\")[0], "../Elwig/Documents") :
Path.Combine(InstallPath, "resources/Documents");
public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "Elwig");
public static Config Config { get; private set; } = new(ConfigPath);
@@ -50,10 +46,9 @@ namespace Elwig {
public static string? BranchPhoneNr { get; private set; }
public static string? BranchFaxNr { get; private set; }
public static string? BranchMobileNr { get; private set; }
public static IList<IScale> Scales { get; private set; } = [];
public static IList<ICommandScale> CommandScales => [.. Scales.Where(s => s is ICommandScale).Cast<ICommandScale>()];
public static IList<IEventScale> EventScales => [.. Scales.Where(s => s is IEventScale).Cast<IEventScale>()];
public static IList<IScale> Scales { get; private set; }
public static IList<ICommandScale> CommandScales => Scales.Where(s => s is ICommandScale).Cast<ICommandScale>().ToList();
public static IList<IEventScale> EventScales => Scales.Where(s => s is IEventScale).Cast<IEventScale>().ToList();
public static ClientParameters Client { get; set; }
public static Dispatcher MainDispatcher { get; private set; }
@@ -76,7 +71,7 @@ namespace Elwig {
Config = new(Path.GetFullPath(args[1]));
}
ContextTimer.Tick += (sender, evt) => {
ContextTimer.Tick += (object? sender, EventArgs evt) => {
var ch = CurrentLastWrite;
if (ch > LastChanged) {
LastChanged = ch;
@@ -90,20 +85,16 @@ namespace Elwig {
}
protected override async void OnStartup(StartupEventArgs evt) {
Version = new Version(typeof(App).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion.Split('+')[0] ?? "0.0.0.0");
Version = new Version(typeof(App).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion.Split('+')[0] ?? "0.0.0");
try {
await AppDbUpdater.CheckDb();
} catch (FileNotFoundException exc) {
InteractionService.ShowException("Datenbank nicht gefunden", "Die Datenbank konnte nicht gefunden werden", exc);
} catch (Exception exc) {
} catch (Exception e) {
if (Config.UpdateUrl != null && Utils.HasInternetConnectivity()) {
await CheckForUpdates();
}
InteractionService.ShowException("Fehlerhafte Datenbank", "Schwerwiegender Fehler in der Datenbank, unbedingt den Programmierern melden!", exc);
if (InteractionService.AskQuestion("Backup wiederherstellen", "Soll die Datenbank aus einem Backup wiederhergestellt werden?\n\nAchtung: Das kann zu Datenverlust führen!", false))
await BackupService.RestoreDatabase();
ShutdownNow();
MessageBox.Show($"Invalid Database:\n\n{e.Message}", "Invalid Database", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown();
return;
}
@@ -112,15 +103,12 @@ namespace Elwig {
Dictionary<string, (string, string, int?, string?, string?, string?, string?, string?)> branches = [];
using (var ctx = new AppDbContext()) {
branches = await ctx.FetchBranches()
.ToDictionaryAsync(b => b.Name.ToLower(), b => (b.ZwstId, b.Name, b.PostalDest?.AtPlz?.Plz, b.PostalDest?.AtPlz?.Ort.Name, b.Address, b.PhoneNr, b.FaxNr, b.MobileNr));
branches = ctx.Branches.ToDictionary(b => b.Name.ToLower(), b => (b.ZwstId, b.Name, b.PostalDest?.AtPlz?.Plz, b.PostalDest?.AtPlz?.Ort.Name, b.Address, b.PhoneNr, b.FaxNr, b.MobileNr));
try {
Client = new(ctx);
} catch (Exception exc) {
InteractionService.ShowException("Fehler", "Fehler beim Laden der Mandantendaten, unbedingt den Programmierern melden!", exc);
if (InteractionService.AskQuestion("Backup wiederherstellen", "Soll die Datenbank aus einem Backup wiederhergestellt werden?\n\nAchtung: Das kann zu Datenverlust führen!", false))
await BackupService.RestoreDatabase();
ShutdownNow();
} catch (Exception e) {
MessageBox.Show($"Fehler beim Laden der Mandantendaten:\n\n{e.Message}", "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown();
return;
}
BranchNum = branches.Count;
@@ -131,78 +119,46 @@ namespace Elwig {
return Task.CompletedTask;
});
Utils.RunBackground("PDF Initialization", async () => Pdf.Init());
Utils.RunBackground("HTML Initialization", () => Html.Init());
Utils.RunBackground("PDF Initialization", () => Pdf.Init());
Utils.RunBackground("JSON Schema Initialization", BillingData.Init);
if (Config.BackupAuto && !Config.Debug) {
Utils.RunBackground("Automatisches Backup", async () => {
Directory.CreateDirectory(Config.BackupPath);
await Task.Delay(500);
await BackupService.TryDatabaseBackup();
});
_autoBackupTimer.Tick += new EventHandler((sender, evt) => {
Utils.RunBackground("Automatisches Backup", async () => {
await BackupService.TryDatabaseBackup();
});
});
_autoBackupTimer.Start();
}
if (Config.UpdateAuto && Config.UpdateUrl != null) {
if (Utils.HasInternetConnectivity()) {
Utils.RunBackground("Auto Updater", async () => {
await Task.Delay(1000);
await Task.Delay(500);
await CheckForUpdates();
});
}
NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
_autoUpdateTimer.Tick += new EventHandler(OnAutoUpdateTimer);
_autoUpdateTimer.Start();
}
SerialPortWatcher.SerialPortConnected += OnSerialPortConnected;
SerialPortWatcher.SerialPortDisconnected += OnSerialPortDisconnected;
var list = new List<IScale>();
foreach (var s in Config.Scales) {
try {
var scale = Scale.FromConfig(s);
if (s.SyncTime && scale is ICommandScale cmd) {
try {
await cmd.SetDateAndTime(DateTime.Now);
} catch { }
}
list.Add(scale);
} catch (Exception exc) {
list.Add(Scale.FromConfig(s));
} catch (Exception e) {
list.Add(new InvalidScale(s.Id));
if (s.Required)
InteractionService.ShowException("Waagenfehler", $"Verbindung zu Waage {s.Id} konnte nicht hergestellt werden", exc);
MessageBox.Show($"Unable to create scale {s.Id}:\n\n{e.Message}", "Scale Error",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
Scales = list;
if (Config.Branch != null) {
if (!branches.ContainsKey(Config.Branch.ToLower())) {
InteractionService.ShowError("Ungültige Zweigstelle", "Ungültige Zweigstelle in Konfigurationsdatei!");
ShutdownNow();
MessageBox.Show("Invalid branch name in config!", "Invalid Branch Config", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown();
} else {
SetBranch(branches[Config.Branch.ToLower()]);
}
} else if (branches.Count == 1) {
SetBranch(branches.First().Value);
} else {
InteractionService.ShowError("Ungültige Zweigstelle", "Erkennen der lokalen Zweigstelle nicht möglich!");
ShutdownNow();
}
if (Config.WeighingMode == null) {
if (Client.IsMatzen || Client.IsWolkersdorf) {
Config.WeighingMode = WeighingMode.Net;
} else if (Client.IsHaugsdorf || Client.IsSitzendorf) {
Config.WeighingMode = WeighingMode.Box;
} else if (Client.IsBaden || Client.IsGrInzersdorf || Client.IsPamhagen) {
Config.WeighingMode = WeighingMode.Gross;
}
MessageBox.Show("Unable to determine local branch!", "Invalid Branch Config", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown();
}
base.OnStartup(evt);
@@ -211,29 +167,11 @@ namespace Elwig {
window.Show();
}
public static void ShutdownNow() {
ForceShutdown = true;
Current.Shutdown();
}
public async void ShutdownGracefully() {
Mouse.OverrideCursor = Cursors.AppStarting;
if (Config.BackupAuto && !Config.Debug) {
try {
await BackupService.TryDatabaseBackup();
} catch (Exception exc) {
InteractionService.ShowException("Automatisches Backup", exc);
}
}
SerialPortWatcher.Dispose();
private async void Application_Exit(object sender, ExitEventArgs evt) {
foreach (var s in EventScales) {
s.Dispose();
}
Pdf.Cleanup();
Shutdown();
await Pdf.Cleanup();
}
public static void SetBranch(Branch b) {
@@ -245,7 +183,6 @@ namespace Elwig {
BranchName = entry.Item2;
BranchPlz = entry.Item3;
BranchLocation = entry.Item4?
.Split(",")[0]
.Split(" in ")[0]
.Split(" im ")[0]
.Split(" an ")[0]
@@ -263,13 +200,10 @@ namespace Elwig {
var ch = CurrentLastWrite;
if (ch > CurrentApp.LastChanged)
CurrentApp.LastChanged = ch;
MainDispatcher.Invoke(() => {
foreach (Window w in CurrentApp.Windows) {
if (w is not ContextWindow c) continue;
MainDispatcher.Invoke(c.HintContextChange);
MainDispatcher.BeginInvoke(c.TryContextReload);
}
});
foreach (Window w in CurrentApp.Windows) {
if (w is not ContextWindow c) continue;
MainDispatcher.BeginInvoke(c.HintContextChange);
}
}
private void OnAutoUpdateTimer(object? sender, EventArgs? evt) {
@@ -281,62 +215,6 @@ namespace Elwig {
}
}
private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs evt) {
if (!evt.IsAvailable) return;
if (Utils.HasInternetConnectivity()) {
Utils.RunBackground("Auto Updater", async () => {
await Task.Delay(2000);
await CheckForUpdates();
});
}
}
private void OnSerialPortConnected(object? sender, string name) {
for (var i = 0; i < Config.Scales.Count; i++) {
var s = Config.Scales[i];
if (s.Connection?.StartsWith($"serial://{name}:") ?? false) {
if (Scales[i] is InvalidScale) {
try {
Scales[i] = Scale.FromConfig(s);
InteractionService.ShowInformation($"Waage {s.Id}", $"Verbindung zu Waage {s.Id} wieder hergestellt!");
} catch (Exception exc) {
Scales[i] = new InvalidScale(s.Id);
InteractionService.ShowException("Waagenfehler", $"Verbindung zu Waage {s.Id} konnte nicht hergestellt werden", exc);
}
} else if (Scales[i] is IEventScale) {
InteractionService.ShowInformation($"Waage {s.Id}", $"Verbindung zu Waage {s.Id} wieder hergestellt!");
}
}
}
UpdateScales();
}
private void OnSerialPortDisconnected(object? sender, string name) {
for (var i = 0; i < Config.Scales.Count; i++) {
var s = Config.Scales[i];
if ((s.Connection?.StartsWith($"serial://{name}:") ?? false) && Scales[i] is not InvalidScale) {
InteractionService.ShowWarning($"Waagen {s.Id}", $"Verbindung zu Waage {s.Id} unterbrochen!");
if (Scales[i] is ICommandScale) {
try {
Scales[i].Dispose();
} catch {
// ignore
}
Scales[i] = new InvalidScale(s.Id);
}
}
}
UpdateScales();
}
public static void UpdateScales() {
foreach (Window w in CurrentApp.Windows) {
if (w is DeliveryAdminWindow t && t.ViewModel.IsReceipt) {
t.UpdateScales();
}
}
}
public static async Task CheckForUpdates(bool showAlert = false) {
if (Config.UpdateUrl == null) return;
var latest = await Utils.GetLatestInstallerUrl(Config.UpdateUrl);
@@ -344,30 +222,21 @@ namespace Elwig {
await MainDispatcher.BeginInvoke(() => {
var d = new UpdateDialog(latest.Value.Version, latest.Value.Url, latest.Value.Size);
if (d.ShowDialog() == true) {
ShutdownNow();
ForceShutdown = true;
Current.Shutdown();
}
});
} else if (showAlert) {
if (latest == null) {
InteractionService.ShowError("Nach Updates suchen", "Informationen konnten nicht abgerufen werden!");
MessageBox.Show("Informationen konnten nicht abgerufen werden!", "Nach Updates suchen",
MessageBoxButton.OK, MessageBoxImage.Error);
} else {
InteractionService.ShowInformation("Nach Updates suchen", $"Elwig ist auf dem aktuellsten Stand! (Version: {latest.Value.Version})");
MessageBox.Show($"Elwig ist auf dem aktuellsten Stand! (Version: {latest.Value.Version})", "Nach Updates suchen",
MessageBoxButton.OK, MessageBoxImage.Information);
}
}
}
public static async Task ReplaceDatabase(string filename) {
try {
await Task.Run(async () => {
await Database.Import(filename);
});
InteractionService.ShowInformation("Datenbank ersetzen", "Das Ersetzen war erfolgreich!\n\nBitte starten Sie Elwig neu!");
ShutdownNow();
} catch (Exception exc) {
InteractionService.ShowException("Datenbank ersetzen", "Fehler beim Ersetzen", exc);
}
}
private static T FocusWindow<T>(Func<T> constructor, Predicate<T>? selector = null) where T : Window {
foreach (Window w in CurrentApp.Windows) {
if (w is T t && (selector == null || selector(t))) {
@@ -386,23 +255,19 @@ namespace Elwig {
return FocusWindow<DeliveryAdminWindow>(() => new(true), w => w.ViewModel.IsReceipt);
}
public static MemberBusinessSharesAdminWindow FocusMemberBusinessSharesWindow(int mgnr) {
return FocusWindow<MemberBusinessSharesAdminWindow>(() => new(mgnr), w => w.ViewModel.FilterMember?.MgNr == mgnr);
}
public static DeliveryAdminWindow FocusMemberDeliveries(int mgnr) {
return FocusWindow<DeliveryAdminWindow>(() => new(mgnr), w => w.ViewModel.FilterMember?.MgNr == mgnr);
}
public static AreaCmtAdminWindow FocusMemberAreaCmts(int mgnr) {
return FocusWindow<AreaCmtAdminWindow>(() => new(mgnr), w => w.ViewModel.FilterMember.MgNr == mgnr);
public static AreaComAdminWindow FocusMemberAreaComs(int mgnr) {
return FocusWindow<AreaComAdminWindow>(() => new(mgnr), w => w.ViewModel.FilterMember.MgNr == mgnr);
}
public static BaseDataWindow FocusBaseData() {
return FocusWindow<BaseDataWindow>(() => new());
}
public static BaseDataWindow FocusBaseDataAreaCmtType() {
public static BaseDataWindow FocusBaseDataAreaComType() {
var w = FocusBaseData();
w.AreaCommitmentTypes.Focus();
return w;
+4 -9
View File
@@ -122,26 +122,21 @@ namespace Elwig.Controls {
private void OnSelectionChanged(object sender, SelectionChangedEventArgs evt) {
SelectItemsReverse();
var dmp = !string.IsNullOrWhiteSpace(ListDisplayMemberPath) ? ListDisplayMemberPath : !string.IsNullOrWhiteSpace(DisplayMemberPath) ? DisplayMemberPath : null;
var dmp = !string.IsNullOrEmpty(ListDisplayMemberPath) ? ListDisplayMemberPath : !string.IsNullOrEmpty(DisplayMemberPath) ? DisplayMemberPath : null;
if (SelectedItems.Count == ItemsSource.Cast<object>().Count() && AllItemsSelectedContent != null) {
_textBox.Text = AllItemsSelectedContent;
AllItemsSelected = true;
} else if (SelectedItems.Count == 0) {
AllItemsSelected = false;
} else {
AllItemsSelected = null;
}
if (SelectedItems.Count > 1 && SelectedItems.Count == ItemsSource.Cast<object>().Count() && AllItemsSelectedContent != null) {
_textBox.Text = AllItemsSelectedContent;
} else if (SelectedItems.Count == 0) {
_textBox.Text = "";
AllItemsSelected = false;
} else {
_textBox.Text = string.Join(Delimiter,
dmp == null ? SelectedItems.Cast<object>() :
SelectedItems.Cast<object>()
.Select(i => i.GetType().GetProperty(dmp)?.GetValue(i))
);
AllItemsSelected = null;
}
RaiseEvent(new SelectionChangedEventArgs(SelectionChangedEvent, evt.RemovedItems, evt.AddedItems));
}
+2 -11
View File
@@ -41,21 +41,13 @@ namespace Elwig.Controls {
incButton!.Click += IncrementButton_Click;
decButton!.Click += DecrementButton_Click;
base.OnApplyTemplate();
UpdateButtons();
}
private void UpdateButtons() {
var incButton = GetTemplateChild("IncrementButton") as RepeatButton;
var decButton = GetTemplateChild("DecrementButton") as RepeatButton;
incButton?.IsEnabled = Maximum == null || Value < Maximum;
decButton?.IsEnabled = Minimum == null || Value > Minimum;
}
private void IntegerUpDown_TextChanged(object sender, TextChangedEventArgs evt) {
var idx = CaretIndex;
Text = new string([.. Text.Where(char.IsAsciiDigit).Take(4)]);
Text = new string(Text.Where(char.IsAsciiDigit).Take(4).ToArray());
CaretIndex = idx;
evt.Handled = !((!Minimum.HasValue || Value >= Minimum) && (!Maximum.HasValue || Value <= Maximum));
evt.Handled = !(Value >= Minimum && Value <= Maximum);
if (idx >= 4) {
if (Value < Minimum) {
Value = Minimum;
@@ -64,7 +56,6 @@ namespace Elwig.Controls {
}
CaretIndex = 4;
}
UpdateButtons();
}
private void IntegerUpDown_LostFocus(object sender, RoutedEventArgs evt) {
-71
View File
@@ -1,71 +0,0 @@
<Window x:Class="Elwig.Dialogs.AreaCmtModifyDialog"
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"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
xmlns:ctrl="clr-namespace:Elwig.Controls"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Flächenbindungen bearbeiten" Height="220" Width="450">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Padding" Value="2,4,2,4"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Height" Value="25"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
</Style>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<TextBlock x:Name="QuestionBlock1" TextAlignment="Center" Margin="0,10,0,0"
HorizontalAlignment="Center" VerticalAlignment="Top">
Soll die Flächenbindung (<Run Text="{Binding Area}"/> m², <Run Text="{Binding OrigYearFrom}"/>–<Run Text="{Binding OrigYearTo}"/>) rückwirkend bearbeitet werden,<LineBreak/>
oder erst ab der angegebenen Saison?
</TextBlock>
<TextBlock x:Name="QuestionBlock2" TextAlignment="Center" Margin="0,10,0,0" Visibility="Hidden"
HorizontalAlignment="Center" VerticalAlignment="Top">
Soll die Flächenbindung (<Run Text="{Binding Area}"/> m², <Run Text="{Binding OrigYearFrom}"/>–<Run Text="{Binding OrigYearTo}"/>) rückwirkend gelöscht werden,<LineBreak/>
oder erst ab der angegebenen Saison?
</TextBlock>
<Label x:Name="SeasonLabel" Content="Saison:" Margin="0,50,100,0"
HorizontalAlignment="Center" VerticalAlignment="Top"/>
<ctrl:IntegerUpDown x:Name="SeasonInput" Width="56" Height="25" Margin="0,50,0,0" FontSize="14"
Minimum="1900" Maximum="9999"
HorizontalAlignment="Center" VerticalAlignment="Top"
TextChanged="SeasonInput_TextChanged"/>
<CheckBox x:Name="RetroactiveInput" Content="Rückwirkend bearbeiten" Margin="0,80,0,0"
HorizontalAlignment="Center" VerticalAlignment="Top"
Checked="RetroactiveInput_Changed" Unchecked="RetroactiveInput_Changed"
IsChecked="{Binding ModifyRetroactively}"/>
<TextBlock x:Name="DescBlock1" Margin="0,105,0,0" TextAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Top">
Die Flächenbindung bleibt bis inkl. Saison <Bold><Run x:Name="CancelSeason1"/></Bold> unverändert,<LineBreak/>
und wird ab inkl. Saison <Bold><Run x:Name="NewSeason1"/></Bold> in geänderter Form übernommen.
</TextBlock>
<TextBlock x:Name="DescBlock2" Margin="0,105,0,0" TextAlignment="Center" Visibility="Hidden"
HorizontalAlignment="Center" VerticalAlignment="Top">
Die Flächenbindung bleibt bis inklusive Saison <Bold><Run x:Name="CancelSeason2"/></Bold> gültig.
</TextBlock>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" Grid.Column="1"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" Grid.Column="1" IsCancel="True"/>
</Grid>
</Window>
-67
View File
@@ -1,67 +0,0 @@
using Elwig.Helpers;
using System;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class AreaCmtModifyDialog : Window {
public int? YearTo { get; set; }
public bool ModifyRetroactively { get; set; }
public string OrigYearFrom { get; set; }
public string OrigYearTo { get; set; }
public string Area { get; set; }
public AreaCmtModifyDialog(int? yearFrom, int? yearTo, int area, bool delete, bool forceRetroactive = false) {
Area = $"{area:N0}";
OrigYearFrom = $"{yearFrom}";
OrigYearTo = $"{yearTo}";
InitializeComponent();
Title = delete ? "Flächenbindung löschen" : "Flächenbindung bearbeiten";
RetroactiveInput.Content = delete ? "Rückwirkend löschen" : "Rückwirkend bearbeiten";
forceRetroactive = forceRetroactive || yearFrom.HasValue && yearTo.HasValue && yearFrom.Value == yearTo.Value;
RetroactiveInput.IsEnabled = !forceRetroactive;
if (delete) {
QuestionBlock1.Visibility = Visibility.Hidden;
QuestionBlock2.Visibility = Visibility.Visible;
DescBlock1.Visibility = Visibility.Hidden;
DescBlock2.Visibility = Visibility.Visible;
}
SeasonInput.Minimum = yearFrom.HasValue ? yearFrom + 1 : null;
SeasonInput.Maximum = yearTo.HasValue ? yearTo : null;
if (forceRetroactive || (yearTo.HasValue && yearTo < Utils.CurrentYear) || (yearFrom.HasValue && yearFrom >= Utils.CurrentYear)) {
RetroactiveInput.IsChecked = true;
} else {
SeasonInput.Text = $"{Utils.CurrentYear}";
}
}
private void SeasonInput_TextChanged(object sender, TextChangedEventArgs evt) {
YearTo = SeasonInput.Value! - 1;
CancelSeason1.Text = $"{YearTo}";
CancelSeason2.Text = $"{YearTo}";
NewSeason1.Text = $"{YearTo + 1}";
}
private void RetroactiveInput_Changed(object sender, RoutedEventArgs evt) {
if (ModifyRetroactively) {
SeasonInput.IsEnabled = false;
SeasonInput.Text = "";
YearTo = null;
DescBlock1.Visibility = Visibility.Hidden;
DescBlock2.Visibility = Visibility.Hidden;
} else {
SeasonInput.IsEnabled = true;
SeasonInput.Text = $"{Math.Max(SeasonInput.Minimum ?? Utils.CurrentYear, Utils.CurrentYear)}";
DescBlock1.Visibility = QuestionBlock1.Visibility;
DescBlock2.Visibility = QuestionBlock2.Visibility;
}
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
Close();
}
}
}
@@ -1,4 +1,4 @@
<Window x:Class="Elwig.Dialogs.AreaCmtTransferDialog"
<Window x:Class="Elwig.Dialogs.AreaComDialog"
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"
@@ -8,7 +8,7 @@
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Flächenbindungen übertragen" Height="240" Width="450">
Title="Flächenbindungen übertragen" Height="260" Width="450">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
@@ -35,11 +35,11 @@
<TextBlock x:Name="QuestionBlock1" TextAlignment="Center" Margin="0,10,0,0"
HorizontalAlignment="Center" VerticalAlignment="Top">
Sollen die aktiven Flächenbindungen des angegebenen Vorgängers<LineBreak/>
übernommen werden? (<Run Text="{Binding AreaCmtNum}"/> FB, <Run Text="{Binding Area}"/> m²)
übernommen werden? (<Run Text="{Binding AreaComNum}"/> FB, <Run Text="{Binding Area}"/> m²)
</TextBlock>
<TextBlock x:Name="QuestionBlock2" TextAlignment="Center" Margin="0,10,0,0" Visibility="Hidden"
HorizontalAlignment="Center" VerticalAlignment="Top">
Sollen die aktiven Flächenbindungen gekündigt werden? (<Run Text="{Binding AreaCmtNum}"/> FB, <Run Text="{Binding Area}"/> m²)
Sollen die aktiven Flächenbindungen gekündigt werden? (<Run Text="{Binding AreaComNum}"/> FB, <Run Text="{Binding Area}"/> m²)
</TextBlock>
<Label x:Name="SeasonLabel" Content="Saison:" Margin="0,50,100,0"
@@ -48,8 +48,12 @@
Minimum="1900" Maximum="9999"
HorizontalAlignment="Center" VerticalAlignment="Top"
TextChanged="SeasonInput_TextChanged"/>
<CheckBox x:Name="CopyYearToInput" Content="Beginn der Laufzeit von Vorgänger übernehmen" Margin="0,80,0,0"
HorizontalAlignment="Center" VerticalAlignment="Top"
Checked="CopyYearToInput_Changed" Unchecked="CopyYearToInput_Changed"
IsChecked="{Binding MaintainYearFrom}"/>
<TextBlock x:Name="DescBlock1" Margin="0,90,0,0" TextAlignment="Center"
<TextBlock x:Name="DescBlock1" Margin="0,105,0,0" TextAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Top">
Die Flächenbindungen beim <Bold>Vorgänger</Bold> sind bis inkl. Saison <Bold><Run x:Name="CancelSeason1"/></Bold> gültig,<LineBreak/>
und werden beim <Bold>Nachfolger</Bold> ab <Run x:Name="DescBlock1Season" Text="inkl. Saison "/><Bold><Run x:Name="TransferSeason"/></Bold> übernommen.
@@ -3,53 +3,53 @@ using System.Windows;
using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class AreaCmtTransferDialog : Window {
public partial class AreaComDialog : Window {
public int CancelSeason => SuccessorSeason - 1;
public int SuccessorSeason { get; set; }
public int CancelSeason { get; set; }
public int SuccessorSeason => CancelSeason + 1;
public bool MaintainYearFrom { get; set; }
public string AreaCmtNum { get; set; }
public string AreaComNum { get; set; }
public string Area { get; set; }
public AreaCmtTransferDialog(string name, int areaCmtNum, int area) {
SuccessorSeason = Utils.FollowingSeason;
AreaCmtNum = $"{areaCmtNum:N0}";
public AreaComDialog(string name, int areaComNum, int area) {
CancelSeason = Utils.FollowingSeason - 1;
AreaComNum = $"{areaComNum:N0}";
Area = $"{area:N0}";
InitializeComponent();
SeasonInput.Text = $"{SuccessorSeason}";
SeasonInput.Minimum = Utils.CurrentLastSeason;
SeasonInput.Text = $"{CancelSeason}";
Title = $"Aktive Flächenbindungen kündigen - {name}";
QuestionBlock1.Visibility = Visibility.Hidden;
QuestionBlock2.Visibility = Visibility.Visible;
DescBlock1.Visibility = Visibility.Hidden;
DescBlock2.Visibility = Visibility.Visible;
CopyYearToInput.Visibility = Visibility.Hidden;
Height = 240;
SeasonInput.Margin = new(0, 40, 0, 0);
SeasonLabel.Margin = new(0, 40, 100, 0);
}
public AreaCmtTransferDialog(string name, string successorName, int areaCmtNum, int area) {
SuccessorSeason = Utils.FollowingSeason;
AreaCmtNum = $"{areaCmtNum:N0}";
public AreaComDialog(string name, string successorName, int areaComNum, int area) {
CancelSeason = Utils.FollowingSeason - 1;
AreaComNum = $"{areaComNum:N0}";
Area = $"{area:N0}";
InitializeComponent();
SeasonInput.Text = $"{SuccessorSeason}";
SeasonInput.Minimum = Utils.CurrentLastSeason;
SeasonInput.Text = $"{CancelSeason}";
Title = $"Aktive Flächenbindungen übertragen - {name} - {successorName}";
InfoBlock.Visibility = Visibility.Hidden;
}
private void SeasonInput_TextChanged(object sender, TextChangedEventArgs evt) {
SuccessorSeason = (int)SeasonInput.Value!;
CancelSeason = (int)SeasonInput.Value!;
CancelSeason1.Text = $"{CancelSeason}";
CancelSeason2.Text = $"{CancelSeason}";
TransferSeason.Text = $"{SuccessorSeason}";
DescBlock1Season.Text = "inkl. Saison ";
TransferSeason.Text = MaintainYearFrom ? "" : $"{SuccessorSeason}";
DescBlock1Season.Text = MaintainYearFrom ? "dem originalen Beginn der FB" : "inkl. Saison ";
}
private void CopyYearToInput_Changed(object sender, RoutedEventArgs evt) {
TransferSeason.Text = $"{SuccessorSeason}";
DescBlock1Season.Text = "inkl. Saison ";
TransferSeason.Text = MaintainYearFrom ? "" : $"{SuccessorSeason}";
DescBlock1Season.Text = MaintainYearFrom ? "dem originalen Beginn der FB" : "inkl. Saison ";
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
+2 -2
View File
@@ -47,9 +47,9 @@
TextChanged="NameInput_TextChanged"/>
<Label Content="Beim Löschen müssen folgende Daten auch gelöscht werden:" Margin="10,120,10,10"/>
<CheckBox x:Name="AreaCmtInput" Content="Flächenbindungen" Margin="40,145,0,0"
<CheckBox x:Name="AreaComInput" Content="Flächenbindungen" Margin="40,145,0,0"
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
IsChecked="{Binding DeleteAreaCmts}"/>
IsChecked="{Binding DeleteAreaComs}"/>
<CheckBox x:Name="DeliveryInput" Content="Lieferungen" Margin="40,165,0,0"
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
IsChecked="{Binding DeleteDeliveries}"/>
+6 -6
View File
@@ -8,16 +8,16 @@ namespace Elwig.Dialogs {
protected string[] NameParts;
public bool DeleteAreaCmts { get; set; }
public bool DeleteAreaComs { get; set; }
public bool DeleteDeliveries { get; set; }
public bool DeletePaymentData { get; set; }
public DeleteMemberDialog(int mgnr, string name, int numAreaCmts, int numDeliveries, int numCredits) {
public DeleteMemberDialog(int mgnr, string name, int numAreaComs, int numDeliveries, int numCredits) {
NameParts = name.ToLower().Split(' ').Where(p => p.Length > 0).Append($"{mgnr}").ToArray();
InitializeComponent();
Title += " - " + name;
AreaCmtInput.IsEnabled = numAreaCmts != 0;
AreaCmtInput.Content += $" ({numAreaCmts:N0})";
AreaComInput.IsEnabled = numAreaComs != 0;
AreaComInput.Content += $" ({numAreaComs:N0})";
DeliveryInput.IsEnabled = numDeliveries != 0;
DeliveryInput.Content += $" ({numDeliveries:N0})";
PaymentInput.IsEnabled = numCredits != 0;
@@ -43,7 +43,7 @@ namespace Elwig.Dialogs {
private void Update() {
var t = NameInput.Text.ToLower().Split(' ');
var nameValid = NameParts.All(t.Contains);
UpdateCheckBox(AreaCmtInput);
UpdateCheckBox(AreaComInput);
UpdateCheckBox(DeliveryInput);
UpdateCheckBox(PaymentInput);
if (!nameValid) {
@@ -52,7 +52,7 @@ namespace Elwig.Dialogs {
ControlUtils.ClearInputState(NameInput);
}
ConfirmButton.IsEnabled =
(!AreaCmtInput.IsEnabled || DeleteAreaCmts) &&
(!AreaComInput.IsEnabled || DeleteAreaComs) &&
(!DeliveryInput.IsEnabled || DeleteDeliveries) &&
(!PaymentInput.IsEnabled || DeletePaymentData) &&
nameValid;
+1 -1
View File
@@ -74,7 +74,7 @@
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Part.DPNr}" Width="20" TextAlignment="Right"
VerticalAlignment="Center" Margin="0,0,5,0" FontSize="14"/>
<TextBlock Text="{Binding Part.SortId}" Foreground="{Binding Part.Variety.Color}" Width="40" TextAlignment="Center"
<TextBlock Text="{Binding Part.SortId}" Width="40" TextAlignment="Center"
VerticalAlignment="Center" Margin="0,0,0,0" FontSize="14"/>
<TextBlock Text="{Binding Part.Kmw, StringFormat='{}{0:N1}°'}" Width="40" TextAlignment="Right" Padding="0,0,10,0"
VerticalAlignment="Center"/>
@@ -2,6 +2,7 @@ using Elwig.Helpers;
using Elwig.Models.Entities;
using Elwig.Windows;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
@@ -43,10 +44,15 @@ namespace Elwig.Dialogs {
}
protected override async Task OnRenewContext(AppDbContext ctx) {
ControlUtils.RenewItemsSource(MemberInput, await ctx.FetchMembers().ToListAsync());
ControlUtils.RenewItemsSource(MemberInput, await ctx.Members
.Where(m => m.IsActive)
.OrderBy(m => m.Name)
.ThenBy(m => m.GivenName)
.ToListAsync());
ControlUtils.RenewItemsSource(DeliveryInput, await ctx.Deliveries
.Where(d => d.DateString == $"{_delivery.Date:yyyy-MM-dd}" && d.ZwstId == _delivery.ZwstId)
.OrderBy(d => d.LsNr)
.Include(d => d.Member)
.Include(d => d.Parts)
.ToListAsync());
if (DeliveryInput.SelectedItem == null)
+5 -5
View File
@@ -1,11 +1,9 @@
<Window x:Class="Elwig.Dialogs.ManualWeighingDialog"
AutomationProperties.AutomationId="ManualWeighingDialog"
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"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
xmlns:ctrl="clr-namespace:Elwig.Controls"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=WeightInput}"
@@ -39,9 +37,11 @@
</Grid.ColumnDefinitions>
<Label Content="Gewicht:" Margin="10,20,10,10"/>
<ctrl:UnitTextBox x:Name="WeightInput" Grid.Column="1" Width="62" Height="25" Unit="kg"
Margin="0,20,10,10" HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="14" Padding="2"
TextChanged="WeightInput_TextChanged"/>
<Grid Grid.Column="1" Width="70" Height="25" Margin="0,20,10,10" HorizontalAlignment="Left" VerticalAlignment="Top">
<TextBox x:Name="WeightInput" TextAlignment="Right" Padding="2,2,17,2"
TextChanged="WeightInput_TextChanged"/>
<Label Content="kg" Margin="0,4,3,0" HorizontalAlignment="Right" FontSize="10" Padding="2,4,2,4"/>
</Grid>
<Label Content="Grund:" Margin="10,50,10,10"/>
<TextBox x:Name="ReasonInput" Grid.Column="1" Margin="0,50,10,10"
+2 -2
View File
@@ -6,8 +6,8 @@ using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class ManualWeighingDialog : Window {
public int Weight { get; private set; }
public string? Reason { get; private set; }
public int Weight = 0;
public string? Reason = null;
public ManualWeighingDialog(string? reason = null) {
InitializeComponent();
-55
View File
@@ -1,55 +0,0 @@
<Window x:Class="Elwig.Dialogs.TareCorrectionDialog"
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"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
xmlns:ctrl="clr-namespace:Elwig.Controls"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=TargetTareInput}"
Title="Tara korrigieren" Height="170" Width="300">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Padding" Value="2,4,2,4"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Height" Value="25"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
</Style>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<Label Content="Ist-Tara:" Margin="10,20,0,10" Grid.Column="1"/>
<ctrl:UnitTextBox x:Name="ActualTareInput" Grid.Column="2" Width="56" Height="25" Unit="kg" IsReadOnly="True"
Margin="0,20,10,10" HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="14" Padding="2"
TextChanged="TareInput_TextChanged"/>
<Label Content="Soll-Tara:" Margin="10,50,0,10" Grid.Column="1"/>
<ctrl:UnitTextBox x:Name="TargetTareInput" Grid.Column="2" Width="56" Height="25" Unit="kg"
Margin="0,50,10,10" HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="14" Padding="2"
TextChanged="TareInput_TextChanged"/>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" Grid.ColumnSpan="3" IsEnabled="False" IsDefault="True"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" Grid.ColumnSpan="3" IsCancel="True"/>
</Grid>
</Window>
@@ -1,38 +0,0 @@
using Elwig.Helpers;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class TareCorrectionDialog : Window {
public int ActualTare { get; private set; }
public int TargetTare { get; private set; }
public TareCorrectionDialog(int? actualTare, int? targetTare = null) {
InitializeComponent();
ActualTare = actualTare ?? 0;
TargetTare = targetTare ?? actualTare ?? 0;
ActualTareInput.IsReadOnly = actualTare != null;
ActualTareInput.Text = ActualTare.ToString();
TargetTareInput.Text = TargetTare.ToString();
TargetTareInput.SelectAll();
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
ActualTare = int.Parse(ActualTareInput.Text);
TargetTare = int.Parse(TargetTareInput.Text);
Close();
}
private void UpdateButtons() {
ConfirmButton.IsEnabled = ActualTareInput.Text.Length > 0 && TargetTareInput.Text.Length > 0;
}
private void TareInput_TextChanged(object sender, TextChangedEventArgs evt) {
if (sender is not TextBox b) return;
Validator.CheckInteger(b, true, 4);
UpdateButtons();
}
}
}
+1 -2
View File
@@ -1,5 +1,4 @@
using Elwig.Helpers;
using Elwig.Services;
using System;
using System.Diagnostics;
using System.IO;
@@ -52,7 +51,7 @@ namespace Elwig.Dialogs {
File.Delete(fileName);
return;
} catch (Exception exc) {
InteractionService.ShowException(exc);
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Process.Start(fileName);
DialogResult = true;
+90 -280
View File
@@ -1,176 +1,46 @@
using Elwig.Helpers;
using Elwig.Models;
using Elwig.Models.Entities;
using iText.Kernel.Colors;
using iText.Kernel.Geom;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Action;
using iText.Kernel.Pdf.Canvas;
using iText.Layout;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Elwig.Documents {
public class BusinessDocument : Document {
public abstract class BusinessDocument : Document {
public Member Member;
public MemberHistoryPoint MemberShares;
public string? Location;
public bool IncludeSender = false;
public bool UseBillingAddress = false;
public bool ShowDateAndLocation = false;
public DateOnly? DateFrom;
protected Table? Aside;
public string Aside;
public BusinessDocument(string title, Member m, bool includeSender = false) : base(title) {
Member = m;
Location = App.BranchLocation;
IncludeSender = includeSender;
var uid = (m.UstIdNr ?? "-") + (m.IsBuchführend ? "" : " <i>(pauschaliert)</i>");
Aside = $"<table><colgroup><col span='1' style='width: 22.5mm;'/><col span='1' style='width: 42.5mm;'/></colgroup>" +
$"<thead><tr><th colspan='2'>Mitglied</th></tr></thead><tbody>" +
$"<tr><th>Mitglieds-Nr.:</th><td>{m.MgNr}</td></tr>" +
$"<tr><th>Betriebs-Nr.:</th><td>{m.LfbisNr}</td></tr>" +
$"<tr><th>UID:</th><td>{uid}</td></tr>" +
$"</tbody></table>";
}
public string Address {
get {
IAddress addr = (Member.BillingAddress != null && UseBillingAddress) ? Member.BillingAddress : Member;
var plz = addr.PostalDest.AtPlz;
return string.Join("\n", ((string?[])[addr is BillingAddr ? addr.FullName : null, Member.AdministrativeName, Member.ForTheAttentionOf, addr.Address, $"{plz?.Plz} {plz?.Ort.Name.Split(",")[0]}", addr.PostalDest.Country.Name]).Where(s => !string.IsNullOrWhiteSpace(s)));
return string.Join("\n", ((string?[])[Member.BillingAddress?.FullName, Member.AdministrativeName, Member.ForTheAttentionOf, addr.Address, $"{plz?.Plz} {plz?.Ort.Name.Split(",")[0]}", addr.PostalDest.Country.Name]).Where(s => !string.IsNullOrWhiteSpace(s)));
}
}
protected Cell NewAsideCell(Paragraph text, int colspan = 1, bool isName = false, bool right = false, bool center = false) {
var cell = NewCell(text, colspan: colspan).SetPaddingsMM(0.25f, center ? 0 : right ? 1 : 0.5f, 0.25f, isName ? 1 : 0);
if (colspan == 26) {
cell.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetBackgroundColor(new DeviceRgb(0xe0, 0xe0, 0xe0))
.SetBorderTop(new SolidBorder(new DeviceRgb(0x80, 0x80, 0x80), BorderThickness))
.SetPaddingsMM(0.5f, 1, 0.5f, 1);
} else if (isName) {
cell.SetBorderLeft(new SolidBorder(new DeviceRgb(0x80, 0x80, 0x80), BorderThickness));
}
if (right) cell.SetTextAlignment(TextAlignment.RIGHT);
if (center) cell.SetTextAlignment(TextAlignment.CENTER);
return cell;
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";
}
protected Cell NewAsideCell(string text, int colspan = 1, bool isName = false, bool right = false, bool center = false) {
return NewAsideCell(new KernedParagraph(text, 10), colspan, isName, right, center);
}
public BusinessDocument(string title, Member m, DateOnly? dateFrom, bool includeSender = false) :
base(title) {
Member = m;
MemberShares = new(m.Shares, m.SharesRed, m.SharesWhite, m.SharesDormant);
Location = App.BranchLocation;
IncludeSender = includeSender;
DateFrom = dateFrom;
}
protected override void BeforeRenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.BeforeRenderBody(doc, pdf);
Aside = new Table(ColsMM(2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5))
.SetWidth(UnitValue.CreatePointValue(65 * PtInMM)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetFont(NF).SetFontSize(10)
.SetBorder(new SolidBorder(new DeviceRgb(0x80, 0x80, 0x80), BorderThickness))
.AddCell(NewAsideCell("Mitglied", 26));
if (Member.IsOrganic) {
Aside
.AddCell(NewAsideCell("Mitgl.-Nr.:", 8, isName: true)).AddCell(NewAsideCell($"{Member.MgNr}", 4, right: true))
.AddCell(NewAsideCell("Bio-KSt.:", 6, isName: true)).AddCell(NewAsideCell(Member.OrganicAuthorityCode ?? "-", 8, right: true, center: Member.OrganicAuthorityCode == null))
.AddCell(NewAsideCell("Betr.-Nr.:", 6, isName: true)).AddCell(NewAsideCell(Member.LfbisNr ?? "-", 6, right: true, center: Member.LfbisNr == null))
.AddCell(NewAsideCell("OOC-ID:", 6, isName: true)).AddCell(NewAsideCell(Member.OrganicOperatorId ?? "-", 8, right: true, center: Member.OrganicOperatorId == null))
.AddCell(NewAsideCell("UID:", 6, isName: true)).AddCell(NewAsideCell(Member.UstIdNr ?? "-", 10, right: true, center: Member.UstIdNr == null))
.AddCell(NewAsideCell(new KernedParagraph("", 10).Add(Italic(Member.IsAccounting ? "(buchführend)" : "(pauschaliert)")), 10, center: true));
} else {
Aside
.AddCell(NewAsideCell("Mitglieds-Nr.:", 12, isName: true)).AddCell(NewAsideCell($"{Member.MgNr}", 4, right: true)).AddCell(NewAsideCell("", 10))
.AddCell(NewAsideCell("Betriebs-Nr.:", 10, isName: true)).AddCell(NewAsideCell(Member.LfbisNr ?? "-", 6, right: true, center: Member.LfbisNr == null)).AddCell(NewAsideCell("", 10))
.AddCell(NewAsideCell("UID:", 6, isName: true)).AddCell(NewAsideCell(Member.UstIdNr ?? "-", 10, right: true, center: Member.UstIdNr == null))
.AddCell(NewAsideCell(new KernedParagraph("", 10).Add(Italic(Member.IsAccounting ? "(buchführend)" : "(pauschaliert)")), 10, center: true));
}
}
protected void RenderAddress(Canvas canvas, Rectangle pageSize) {
canvas.Add(new Div()
.SetFixedPositionMM(25, 50, 80, 45, pageSize)
.SetFont(NF)
.Add(new KernedParagraph(IncludeSender ? $"{App.Client.Sender1}\n{App.Client.Sender2}" : "", 8).SetMargins(8, 0, 8, 0).SetHeight(16.0625f))
.Add(new KernedParagraph(Address, 12).SetMargin(0).SetHeight(12 * 5)));
}
protected static void RenderAside(Table aside, Canvas canvas, Rectangle pageSize) {
canvas.Add(new Div()
.SetFixedPositionMM(125, 50, 65, 50, pageSize)
.Add(aside));
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
var page = pdf.AddNewPage();
var pageSize = page.GetPageSize();
var pdfCanvas = new PdfCanvas(page.NewContentStreamBefore(), page.GetResources(), pdf);
using (var canvas = new Canvas(pdfCanvas, pageSize)) {
// header
var header = new Div()
.SetFixedPositionMM(25, 10, pageSize.GetWidth() / PtInMM - 45, 45, pageSize)
.SetTextAlignment(TextAlignment.CENTER);
header.Add(new KernedParagraph(App.Client.Name, 18).SetFont(BF).SetMarginsMM(8, 0, 0, 0));
if (App.Client.NameSuffix != null) header.Add(new KernedParagraph(App.Client.NameSuffix, 14).SetFont(BF).SetMargin(0));
header.Add(new KernedParagraph(App.Client.NameTypeFull, 12).SetFont(NF).SetMargin(0));
canvas.Add(header);
// address
RenderAddress(canvas, pageSize);
// aside
if (Aside != null) {
RenderAside(Aside, canvas, pageSize);
}
}
doc.Add(new KernedParagraph(ShowDateAndLocation ? $"{Location}, am {DateFrom ?? Date:dd.MM.yyyy}" : "", 12).SetTextAlignment(TextAlignment.RIGHT).SetVerticalAlignment(VerticalAlignment.MIDDLE).SetHeight(24).SetMargin(0));
doc.Add(new KernedParagraph(Title, 12).SetFont(BF).SetMargins(0, 0, 12, 0));
}
protected override Paragraph GetFooter() {
var c = App.Client;
var link1 = new Link(c.EmailAddress ?? "", PdfAction.CreateURI($"mailto:{Uri.EscapeDataString(c.Name)}%20<{c.EmailAddress}>"));
var link2 = new Link(c.Website ?? "", PdfAction.CreateURI($"http://{c.Website}/"));
link1.GetLinkAnnotation().SetBorder(new PdfAnnotationBorder(0, 0, 0));
link2.GetLinkAnnotation().SetBorder(new PdfAnnotationBorder(0, 0, 0));
return new KernedParagraph(10)
.AddAll(Utils.GenerateFooter("\n", " \u00b7 ")
.Item(c.NameFull).NextLine()
.Item(c.Address).Item($"{c.Plz} {c.Ort}").Item("Österreich").NextGroup()
.Item("Tel.", c.PhoneNr).Item("Fax", c.FaxNr).NextGroup()
.Item(c.EmailAddress != null ? link1 : null).NextLine()
.Item(c.Website != null ? link2 : null).NextGroup()
.Item("Betriebs-Nr.", c.LfbisNr).NextGroup()
.Item("Bio-KSt.", c.OrganicAuthorityCode).NextGroup()
.Item("OOC-ID", c.OrganicOperatorId).NextLine()
.Item("UID", c.UstIdNr).Item("BIC", c.Bic).Item("IBAN", c.Iban)
.RebalanceLines()
.ToLeafElements());
}
protected Cell NewWeightsHdr(Paragraph p) {
return NewCell(p)
.SetPaddingsMM(0.125f, 0, 0.125f, 0);
}
protected Cell NewWeightsHdr(string text) {
return NewCell(new KernedParagraph(text, 8).SetFont(IF).SetTextAlignment(TextAlignment.RIGHT))
.SetPaddingsMM(0.125f, 0, 0.125f, 0);
}
protected Cell NewWeightsTh(string name) {
return NewCell(new KernedParagraph(name, 10).SetFont(IF))
.SetPaddingsMM(0.125f, 0, 0.125f, 0);
}
protected Cell NewWeightsTd(string text, bool sum = false) {
return NewCell(new KernedParagraph(text, 10).SetFont(sum ? BF : NF).SetTextAlignment(TextAlignment.RIGHT))
.SetPaddingsMM(sum ? 0.25f : 0.125f, 0, sum ? 0.25f : 0.125f, 0)
.SetBorderTop(sum ? new SolidBorder(BorderThickness) : Border.NO_BORDER);
}
protected Table NewWeightsTable(List<MemberStat> stats) {
public static string PrintSortenaufteilung(List<MemberStat> stats) {
List<string> discrs = [""];
List<string> names = ["ohne Attr./Bewirt."];
List<string> bucketAttrs = [
@@ -186,136 +56,82 @@ namespace Elwig.Documents {
List<double> cols = [40];
cols.AddRange(names.Select(_ => 125.0 / names.Count));
var tbl = new Table(ColsMM([.. cols]))
.AddHeaderCell(NewWeightsHdr(new KernedParagraph(8).Add(BoldItalic("Sortenaufteilung ")).Add(Italic("[kg]"))));
foreach (var name in names)
tbl.AddHeaderCell(NewWeightsHdr(name));
foreach (var g in stats.GroupBy(b => b.Variety).OrderBy(b => b.Key)) {
var dict = g.ToDictionary(a => a.Discr, a => a.Weight);
var vals = discrs.Select(a => dict.GetValueOrDefault(a, 0)).ToList();
tbl.AddCell(NewWeightsTh(g.Key));
foreach (var v in vals)
tbl.AddCell(NewWeightsTd(v == 0 ? "-" : $"{v:N0}"));
tbl.AddCell(NewWeightsTd($"{dict.Values.Sum():N0}"));
}
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", stats
.GroupBy(b => b.Variety)
.OrderBy(b => b.Key)
.Select(g => {
var dict = g.ToDictionary(a => a.Discr, a => a.Weight);
var vals = discrs.Select(a => dict.GetValueOrDefault(a, 0)).ToList();
return $"<tr><th>{g.Key}</th>" + string.Join("", vals.Select(v => "<td class=\"number\">" + (v == 0 ? "-" : $"{v:N0}") + "</td>")) +
$"<td class=\"number\">{dict.Values.Sum():N0}</td></tr>";
})
);
var totalDict = stats.GroupBy(s => s.Discr).ToDictionary(g => g.Key, g => g.Sum(a => a.Weight));
var totals = discrs.Select(a => totalDict.TryGetValue(a, out int value) ? value : 0);
tbl.AddCell(NewWeightsTd("", true));
foreach (var v in totals)
tbl.AddCell(NewWeightsTd($"{v:N0}", true));
tbl.AddCell(NewWeightsTd($"{totalDict.Values.Sum():N0}", true));
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 tbl;
return "<table class=\"sortenaufteilung small number cohere\">" + tbl + "</table>";
}
protected Cell NewBucketHdr(Paragraph p, int rowspan = 1, bool left = false, bool unit = false) {
p.SetProperty(Property.NO_SOFT_WRAP_INLINE, true);
p.SetProperty(Property.OVERFLOW_X, OverflowPropertyValue.HIDDEN);
return NewCell(p, rowspan: rowspan)
.SetPaddingsMM(0.125f, unit ? 2 : 0, 0.125f, 0)
.SetTextAlignment(left ? TextAlignment.LEFT : TextAlignment.RIGHT).SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetFont(IF);
}
protected Cell NewBucketHdr(string text, int rowspan = 1, bool left = false, bool unit = false) {
return NewBucketHdr(new KernedParagraph(text, 8), rowspan, left, unit);
}
protected Cell NewBucketSubHdr(string text, int colspan, bool isTiny = false) {
var p = new KernedParagraph(text, 8);
p.SetProperty(Property.NO_SOFT_WRAP_INLINE, true);
p.SetProperty(Property.OVERFLOW_X, OverflowPropertyValue.HIDDEN);
return NewCell(p, colspan: colspan)
.SetBorderTop(!isTiny ? new SolidBorder(BorderThickness) : Border.NO_BORDER)
.SetPaddingsMM(isTiny ? 0 : 0.125f, 0, isTiny ? 0 : 0.125f, 0).SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetTextAlignment(TextAlignment.LEFT).SetFont(BI);
}
protected Cell NewBucketTh(string text, bool isTiny = false) {
var p = new KernedParagraph(text, isTiny ? 8 : 10);
p.SetProperty(Property.NO_SOFT_WRAP_INLINE, true);
p.SetProperty(Property.OVERFLOW_X, OverflowPropertyValue.HIDDEN);
return NewCell(p)
.SetPaddingsMM(isTiny ? 0 : 0.125f, 0, isTiny ? 0 : 0.125f, 0)
.SetTextAlignment(TextAlignment.LEFT).SetFont(IF);
}
protected Cell NewBucketTd(string text, bool bold = false, bool isTiny = false) {
var p = new KernedParagraph(text, isTiny ? 8 : 10);
p.SetProperty(Property.NO_SOFT_WRAP_INLINE, true);
p.SetProperty(Property.OVERFLOW_X, OverflowPropertyValue.HIDDEN);
return NewCell(p)
.SetPaddingsMM(isTiny ? 0 : 0.125f, 0, isTiny ? 0 : 0.125f, 0)
.SetTextAlignment(TextAlignment.RIGHT).SetFont(bold ? BF : NF);
}
protected Cell[] FormatRow(
int obligation, int right, int delivery, int? totalDelivery = null, int? payment = null, int? area = null,
bool isGa = false, bool showPayment = false, bool showArea = false, bool isTiny = false
private static string FormatRow(
int obligation, int right, int delivery, int? totalDelivery = null, int? payment = null, int? area = null,
bool isGa = false, bool showPayment = false, bool showArea = false
) {
totalDelivery ??= delivery;
payment ??= delivery;
if (showArea) {
return [
NewBucketTd(area == null ? "" : $"{area:N0}", isTiny: isTiny),
NewBucketTd($"{obligation:N0}", isTiny: isTiny),
NewBucketTd($"{right:N0}", isTiny: isTiny),
];
return $"<td>{(area == null ? "" : $"{area:N0}")}</td>" +
$"<td>{obligation:N0}</td>" +
$"<td>{right:N0}</td>";
}
return [
NewBucketTd(obligation == 0 ? "-" : $"{obligation:N0}", isTiny: isTiny),
NewBucketTd(right == 0 ? "-" : $"{right:N0}", isTiny: isTiny),
NewBucketTd(totalDelivery < obligation ? $"{obligation - totalDelivery:N0}" : "-", totalDelivery < obligation, isTiny: isTiny),
NewBucketTd(delivery <= right ? $"{right - delivery:N0}" : "-", isTiny: isTiny),
NewBucketTd(obligation == 0 && right == 0 ? "-" : (delivery > right ? $"{delivery - right:N0}" : "-"), delivery > right && isGa, isTiny: isTiny),
..(showPayment ? new List<Cell>() {
NewBucketTd(isGa ? "" : obligation == 0 && right == 0 ? "-" : $"{payment:N0}", isTiny: isTiny)
} : []),
NewBucketTd($"{totalDelivery:N0}", isTiny: isTiny),
];
return $"<td>{(obligation == 0 ? "-" : $"{obligation:N0}")}</td>" +
$"<td>{(right == 0 ? "-" : $"{right:N0}")}</td>" +
$"<td>{(totalDelivery < obligation ? $"<b>{obligation - totalDelivery:N0}</b>" : "-")}</td>" +
$"<td>{(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>{totalDelivery:N0}</td>";
}
protected Cell[] FormatRow(MemberBucket bucket, bool isGa = false, bool showPayment = false, bool showArea = false, bool isTiny = false) {
return FormatRow(bucket.Obligation, bucket.Right, bucket.Delivery, bucket.DeliveryTotal, bucket.Payment, bucket.Area, isGa, showPayment, showArea, isTiny);
private static string FormatRow(MemberBucket bucket, bool isGa = false, bool showPayment = false, bool showArea = false) {
return FormatRow(bucket.Obligation, bucket.Right, bucket.Delivery, bucket.DeliveryTotal, bucket.Payment, bucket.Area, isGa, showPayment, showArea);
}
protected Table NewBucketTable(
Season season, Dictionary<string, MemberBucket> buckets, int deliveredWeightRed, int deliveredWeightWhite,
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;
var tbl = new Table(ColsMM(!includeDelivery ? [105, 20, 20, 20] : includePayment ? [45, 17, 17, 17, 19, 16, 17, 17] : [45, 20, 20, 20, 20, 20, 20]))
.SetBorder(Border.NO_BORDER).SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetFont(NF).SetFontSize(isTiny ? 8 : 10);
if (includeDelivery) {
tbl.AddHeaderCell(NewBucketHdr(new KernedParagraph(8)
.Add(BoldItalic($"Lese {season.Year}"))
.Add(Italic($" per {Date:dd.MM.yyyy} [kg]")), left: true))
.AddHeaderCell(NewBucketHdr("Lieferpflicht"))
.AddHeaderCell(NewBucketHdr("Lieferrecht"))
.AddHeaderCell(NewBucketHdr("Unterliefert"))
.AddHeaderCell(NewBucketHdr("Noch lieferbar"))
.AddHeaderCell(NewBucketHdr("Überliefert"));
if (includePayment) tbl.AddHeaderCell(NewBucketHdr("Zugeteilt"));
tbl.AddHeaderCell(NewBucketHdr("Geliefert"));
} else {
tbl.AddHeaderCell(NewBucketHdr(new KernedParagraph(8)
.Add(BoldItalic("Zusammengefasste Flächenbindungen"))
.Add(Italic($" per {Date:dd.MM.yyyy}")), 2, left: true))
.AddHeaderCell(NewBucketHdr("Fläche"))
.AddHeaderCell(NewBucketHdr("Lieferpflicht"))
.AddHeaderCell(NewBucketHdr("Lieferrecht"))
.AddHeaderCell(NewBucketHdr("[m²]", unit: true))
.AddHeaderCell(NewBucketHdr("[kg]", unit: true))
.AddHeaderCell(NewBucketHdr("[kg]", unit: true));
}
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) ||
@@ -337,36 +153,30 @@ namespace Elwig.Documents {
.Where(b => !fbVars.Contains(b.Key))
.OrderBy(b => b.Value.Name);
if (MemberShares.Shares != 0 || (MemberShares.SharesRed == 0 && MemberShares.SharesWhite == 0)) {
tbl.AddCell(NewBucketTh("Gesamtlieferung lt. gez. GA", isTiny: isTiny));
tbl.AddCells(FormatRow(MemberShares.Shares * (season.MinKgPerShare ?? 0), MemberShares.Shares * (season.MaxKgPerShare ?? 0),
deliveredWeightRed + deliveredWeightWhite, isGa: true, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny));
}
if (MemberShares.SharesRed != 0 || MemberShares.SharesWhite != 0) {
tbl.AddCell(NewBucketTh("Gesamtlieferung lt. gez. GA (rot)", isTiny: isTiny));
tbl.AddCells(FormatRow(MemberShares.SharesRed * (season.MinKgPerShareRed ?? season.MinKgPerShare ?? 0), MemberShares.SharesRed * (season.MaxKgPerShareRed ?? season.MaxKgPerShare ?? 0),
deliveredWeightRed, isGa: true, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny));
tbl.AddCell(NewBucketTh("Gesamtlieferung lt. gez. GA (weiß)", isTiny: isTiny));
tbl.AddCells(FormatRow(MemberShares.SharesWhite * (season.MinKgPerShareWhite ?? season.MinKgPerShare ?? 0), MemberShares.SharesWhite * (season.MaxKgPerShareWhite ?? season.MaxKgPerShare ?? 0),
deliveredWeightWhite, isGa: true, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny));
}
tbl += "\n<tbody>\n";
tbl += $"<tr><th>Gesamtlieferung lt. gez. GA</th>{FormatRow(
Member.BusinessShares * season.MinKgPerBusinessShare,
Member.BusinessShares * season.MaxKgPerBusinessShare,
season.Deliveries.Where(d => d.MgNr == Member.MgNr).Sum(d => d.Weight),
isGa: true, showPayment: includePayment, showArea: !includeDelivery
)}</tr>";
if (fbs.Any()) {
tbl.AddCell(NewBucketSubHdr("Flächenbindungen" + (vtr.Any() ? " (inkl. Verträge)" : "") + ":", includePayment ? 8 : 7, isTiny: isTiny));
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.AddCell(NewBucketTh(b.Name, isTiny: isTiny)).AddCells(FormatRow(b, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny));
tbl += $"<tr><th>{b.Name}</th>{FormatRow(b, showPayment: includePayment, showArea: !includeDelivery)}</tr>";
}
}
if (vtr.Any()) {
tbl.AddCell(NewBucketSubHdr("Verträge:", includePayment ? 8 : 7, isTiny: isTiny));
tbl += $"<tr class=\"subheading{(filter == null ? " border" : "")}\"><th colspan=\"{(includePayment ? 8 : 7)}\">" +
"Verträge:</th></tr>";
foreach (var (id, b) in vtr) {
tbl.AddCell(NewBucketTh(b.Name, isTiny: isTiny)).AddCells(FormatRow(b, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny));
tbl += $"<tr><th>{b.Name}</th>{FormatRow(b, showPayment: includePayment, showArea: !includeDelivery)}</tr>";
}
}
tbl += "\n</tbody>\n";
return tbl;
return $"<table class=\"buckets {(isTiny ? "tiny" : "small")} number cohere\">\n" + tbl + "\n</table>";
}
}
}
+21
View File
@@ -0,0 +1,21 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.BusinessDocument>
@model Elwig.Documents.BusinessDocument
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\BusinessDocument.css"/>
<div class="info-wrapper">
<div class="address-wrapper">
<div class="sender">
@if (Model.IncludeSender) {
<div>@Elwig.App.Client.Sender1</div>
<div>@Elwig.App.Client.Sender2</div>
}
</div>
<address>@Model.Address</address>
</div>
<aside>@Raw(Model.Aside)</aside>
@if (Model.ShowDateAndLocation) {
<div class="date">@Model.Location, am @($"{Model.Date:dd.MM.yyyy}")</div>
}
</div>
@RenderBody()
+150
View File
@@ -0,0 +1,150 @@
.address-wrapper, aside {
overflow: hidden;
}
.spacing {
height: 20mm;
}
.info-wrapper {
width: 100%;
height: 45mm;
margin: 0 0 2mm 0;
position: relative;
}
.info-wrapper .date {
text-align: right;
position: absolute;
right: 0;
bottom: -1.5em;
}
.address-wrapper {
height: 45mm;
width: 85mm;
margin: 0;
padding: 5mm;
position: absolute;
left: -5mm;
top: 0;
}
.address-wrapper .sender {
height: 4em;
font-size: 8pt;
padding: 1em 0;
}
address {
height: 5em;
white-space: pre-line;
font-size: 12pt;
font-style: normal;
}
aside {
height: 40mm;
width: 75mm;
margin: 0;
position: absolute;
left: 100mm;
top: 5mm;
}
aside table {
border-collapse: collapse;
border: var(--border-thickness) solid #808080;
width: 65mm;
margin-right: 10mm;
}
aside table thead:not(:first-child) tr {
border-top: var(--border-thickness) solid #808080;
}
aside table thead tr {
background-color: #E0E0E0;
font-size: 10pt;
}
aside table tbody th,
aside table tbody td {
text-align: left;
font-size: 10pt;
}
aside table tbody th {
padding: 0.25mm 0.5mm 0.25mm 1mm;
}
aside table tbody td {
padding: 0.25mm 0;
}
aside table tbody th {
font-weight: normal;
}
main {
margin: 2em 0 1em 0;
}
main > *:first-child {
margin-top: 0;
}
main h1,
main h2,
main h3,
.main-wrapper p {
font-size: 12pt;
margin: 1em 0;
text-align: justify;
}
.main-wrapper p {
widows: 3;
orphans: 3;
hyphens: manual;
}
main h1 {
margin-top: 0;
margin-bottom: 2em;
}
.main-wrapper p.comment {
font-size: 10pt;
}
.main-wrapper p.custom {
white-space: pre-wrap;
break-inside: avoid;
}
.main-wrapper .hidden {
break-before: avoid;
}
.main-wrapper .bottom {
bottom: 0;
position: absolute;
width: 165mm;
}
.main-wrapper .signatures {
width: 100%;
display: flex;
justify-content: space-around;
margin: 20mm 0 2mm 0;
}
.main-wrapper .signatures > * {
width: 50mm;
border-top: var(--border-thickness) solid black;
padding-top: 1mm;
text-align: center;
font-size: 10pt;
}
+7
View File
@@ -0,0 +1,7 @@
using Elwig.Models.Entities;
namespace Elwig.Documents {
public class BusinessLetter : BusinessDocument {
public BusinessLetter(string title, Member m) : base(title, m) { }
}
}
+9
View File
@@ -0,0 +1,9 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.BusinessLetter>
@model Elwig.Documents.BusinessLetter
@{ Layout = "BusinessDocument"; }
<main>
<p>Sehr geehrtes Mitglied,</p>
<p>nein.</p>
<p>Mit freundlichen Grüßen<br/>Ihre Winzergenossenschaft</p>
</main>
+49 -298
View File
@@ -1,116 +1,89 @@
using Elwig.Helpers;
using Elwig.Helpers.Billing;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Documents {
public class CreditNote : BusinessDocument {
public new static string Name => "Traubengutschrift";
public PaymentMember Payment;
public PaymentMember? Payment;
public Credit? Credit;
public CreditNoteDeliveryData Data;
public string? Text;
public string CurrencySymbol;
public int Precision;
public string? MemberModifier;
public List<(string Name, int Kg, decimal Amount)>? MemberUnderDeliveries;
public string MemberModifier;
public IEnumerable<(string Name, int Kg, decimal Amount)>? MemberUnderDeliveries;
public decimal MemberTotalUnderDelivery;
public int MemberAutoBusinessShares;
public decimal MemberAutoBusinessSharesAmount;
public PaymentCustom? CustomPayment;
protected bool ConsiderContractPenalties;
protected bool ConsiderTotalPenalty;
protected bool ConsiderAutoBusinessShares;
protected bool ConsiderCustomModifiers;
private CreditNoteDeliveryData? _data;
private Dictionary<string, UnderDelivery>? _underDeliveries;
public CreditNote(PaymentMember p, DateOnly? dateFrom, BillingData? billingData = null, CreditNoteDeliveryData? data = null, Dictionary<string, UnderDelivery>? underDeliveries = null) :
base($"{Name} {(p.Credit != null ? $"Nr. {p.Credit.Year}/{p.Credit.TgNr:000}" : p.Member.FullName)} – {p.Variant.Name}", p.Member, dateFrom) {
public CreditNote(
AppDbContext ctx,
PaymentMember p,
CreditNoteDeliveryData data,
bool considerContractPenalties,
bool considerTotalPenalty,
bool considerAutoBusinessShares,
bool considerCustomModifiers,
Dictionary<string, UnderDelivery>? underDeliveries = null
) :
base($"{Name} {(p.Credit != null ? $"Nr. {p.Credit.Year}/{p.Credit.TgNr:000}" : p.Member.FullName)} – {p.Variant.Name}", p.Member) {
UseBillingAddress = true;
ShowDateAndLocation = true;
Data = data;
Payment = p;
Credit = p.Credit;
Text = App.Client.TextCreditNote;
DocumentId = $"Tr.-Gutschr. " + (Credit != null ? $"{Credit.Year}/{Credit.TgNr:000}" : Payment.MgNr);
IsPreview = Credit == null;
_data = data;
_underDeliveries = underDeliveries;
CurrencySymbol = Payment.Variant.Season.Currency.Symbol ?? Payment.Variant.Season.Currency.Code;
Precision = Payment.Variant.Season.Precision;
billingData ??= BillingData.FromJson(Payment.Variant.Data);
ConsiderContractPenalties = billingData.ConsiderContractPenalties;
ConsiderTotalPenalty = billingData.ConsiderTotalPenalty;
ConsiderAutoBusinessShares = billingData.ConsiderAutoBusinessShares;
ConsiderCustomModifiers = billingData.ConsiderCustomModifiers;
}
public static async Task<CreditNote> Initialize(int year, int avnr, int mgnr, DateOnly? dateFrom, BillingData? billingData = null, CreditNoteDeliveryData? data = null, Dictionary<string, UnderDelivery>? underDeliveries = null) {
using var ctx = new AppDbContext();
var p = await ctx.MemberPayments
.Where(p => p.Year == year && p.AvNr == avnr && p.MgNr == mgnr)
.SingleAsync();
return new CreditNote(p, dateFrom, billingData, data, underDeliveries);
}
protected override async Task LoadData(AppDbContext ctx) {
await base.LoadData(ctx);
var season = Payment.Variant.Season;
if (ConsiderCustomModifiers) {
CustomPayment = await ctx.CustomPayments.FindAsync(Payment.Year, Payment.MgNr);
var season = p.Variant.Season;
if (considerCustomModifiers) {
CustomPayment = ctx.CustomPayments.Find(p.Year, p.MgNr);
}
_data ??= (await CreditNoteDeliveryData.ForPaymentVariant(ctx.CreditNoteDeliveryRows, ctx.PaymentVariants, Payment.Year, Payment.AvNr))[Member.MgNr];
_underDeliveries ??= await ctx.GetMemberUnderDelivery(Payment.Year, Member.MgNr);
var mod = App.Client.IsMatzen ? await ctx.FetchModifiers(season.Year).Where(m => m.Name.StartsWith("Treue")).FirstOrDefaultAsync() : null;
var mod = App.Client.IsMatzen ? ctx.Modifiers.Where(m => m.Year == season.Year && m.Name.StartsWith("Treue")).FirstOrDefault() : null;
if (CustomPayment?.ModComment != null) {
MemberModifier = CustomPayment.ModComment;
} else if (mod != null) {
MemberModifier = $"{mod.Name} ({mod.PublicValueStr})";
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:000}" : "-")}</td></tr>" +
$"<tr><th>Datum:</th><td>{p.Variant.Date:dd.MM.yyyy}</td></tr>" +
$"<tr><th>Überw. am:</th><td>{p.Variant.TransferDate:dd.MM.yyyy}</td></tr>" +
$"</tbody></table>";
Text = App.Client.TextCreditNote;
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;
if (ConsiderTotalPenalty) {
var weights = _data.Rows.Select(r => new {r.Type, Weight = r.Buckets.Sum(b => b.Value)}).GroupBy(r => r.Type).ToDictionary(r => r.Key, g => g.Sum(r => r.Weight));
var red = weights.GetValueOrDefault("R", 0);
var white = weights.GetValueOrDefault("W", 0);
var total = red + white;
var underDeliveryTotal = total - Member.Shares * (season.MinKgPerShare ?? 0);
var underDeliveryRed = red - Member.SharesRed * (season.MinKgPerShareRed ?? season.MinKgPerShare ?? 0);
var underDeliveryWhite = white - Member.SharesWhite * (season.MinKgPerShareWhite ?? season.MinKgPerShare ?? 0);
MemberTotalUnderDelivery =
(underDeliveryTotal < 0 ? underDeliveryTotal * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) - (season.PenaltyPerShareAmount * Math.Floor(-(decimal)underDeliveryTotal / (season.MinKgPerShare ?? 0)) ?? 0) : 0) +
(underDeliveryRed < 0 ? underDeliveryRed * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) - (season.PenaltyPerShareAmount * Math.Floor(-(decimal)underDeliveryRed / (season.MinKgPerShareRed ?? season.MinKgPerShare ?? 0)) ?? 0) : 0) +
(underDeliveryWhite < 0 ? underDeliveryWhite * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) - (season.PenaltyPerShareAmount * Math.Floor(-(decimal)underDeliveryWhite / (season.MinKgPerShareWhite ?? season.MinKgPerShare ?? 0)) ?? 0) : 0);
if (considerTotalPenalty) {
var total = data.Rows.SelectMany(r => r.Buckets).Sum(b => b.Value);
var totalUnderDelivery = total - p.Member.BusinessShares * season.MinKgPerBusinessShare;
MemberTotalUnderDelivery = totalUnderDelivery < 0 ? totalUnderDelivery * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) - (season.PenaltyPerBsAmount * Math.Floor(-(decimal)totalUnderDelivery / season.MinKgPerBusinessShare) ?? 0) : 0;
if (total == 0)
MemberTotalUnderDelivery -= (season.PenaltyNone ?? 0) + (season.PenaltyPerShareNone * (Member.Shares + Member.SharesRed + Member.SharesWhite) ?? 0);
MemberTotalUnderDelivery -= (season.PenaltyNone ?? 0) + (season.PenaltyPerBsNone * p.Member.BusinessShares ?? 0);
}
if (ConsiderAutoBusinessShares) {
MemberAutoBusinessShares = await ctx.MemberHistory
.Where(h => h.ToMgNr == Member.MgNr && h.DeductYear == season.Year)
.SumAsync(h => h.Shares);
if (considerAutoBusinessShares) {
var fromDate = $"{season.Year}-01-01";
var toDate = $"{season.Year}-12-31";
MemberAutoBusinessShares = ctx.MemberHistory
.Where(h => h.MgNr == p.Member.MgNr && h.Type == "auto")
.Where(h => h.DateString.CompareTo(fromDate) >= 0 && h.DateString.CompareTo(toDate) <= 0)
.Sum(h => h.BusinessShares);
MemberAutoBusinessSharesAmount = MemberAutoBusinessShares * (-season.BusinessShareValue ?? 0);
}
if (ConsiderContractPenalties) {
var varieties = await ctx.FetchWineVarieties().ToDictionaryAsync(v => v.SortId, v => v);
var attributes = await ctx.FetchWineAttributes().ToDictionaryAsync(a => a.AttrId, a => a);
var comTypes = await ctx.AreaCommitmentTypes.ToDictionaryAsync(t => t.VtrgId, t => t);
MemberUnderDeliveries = _underDeliveries?
if (considerContractPenalties) {
var varieties = 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 => (
varieties[u.Key[..2]].Name + (u.Key.Length > 2 ? " " + attributes[u.Key[2..]].Name : ""),
@@ -122,226 +95,4 @@ namespace Elwig.Documents {
.ToList();
}
}
protected override void BeforeRenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.BeforeRenderBody(doc, pdf);
Aside?.AddCell(NewAsideCell("Gutschrift", 26))
.AddCell(NewAsideCell("Tr.-Gutschr.-Nr.:", 12, isName: true)).AddCell(NewAsideCell(Payment?.Credit != null ? $"{Payment.Credit.Year}/{Payment.Credit.TgNr:000}" : "-", 14))
.AddCell(NewAsideCell("Datum:", 12, isName: true)).AddCell(NewAsideCell($"{Payment?.Variant.Date:dd.MM.yyyy}", 14))
.AddCell(NewAsideCell("Überwiesen am:", 12, isName: true)).AddCell(NewAsideCell($"{Payment?.Variant.TransferDate:dd.MM.yyyy}", 14));
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
if (_data == null) throw new Exception("Call LoadData before RenderBody");
base.RenderBody(doc, pdf);
doc.Add(NewCreditTable(_data));
var div = new Table(ColsMM(60, 105))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
var hint = new KernedParagraph(8)
.Add(Italic("Hinweis:\n" +
$"Die Summe der Lieferungen und die Summe der anfal\u00adlenden Pönalen werden mit " +
$"{Payment?.Variant.Season.Precision} Nach\u00adkomma-stellen berechnent, " +
$"erst das Ergebnis wird kauf-männisch auf 2 Nach\u00adkomma\u00adstellen gerundet."))
.SetWidth(56 * PtInMM).SetMarginsMM(4, 2, 4, 2);
div.AddCell(new Cell(1, 2).SetPadding(0).SetBorder(Border.NO_BORDER).SetBorderTop(new SolidBorder(BorderThickness)));
div.AddCell(new Cell(3, 1).SetPadding(0).SetBorder(Border.NO_BORDER).Add(hint).SetKeepTogether(true));
var tbl1 = new Table(ColsMM(70, 5, 5, 25))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetKeepTogether(true);
var tbl2 = new Table(ColsMM(70, 5, 5, 25))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetKeepTogether(true);
var tbl3 = new Table(ColsMM(70, 5, 5, 25))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetKeepTogether(true);
var sum = _data.Rows.Sum(p => p.Amount);
if (Payment == null) {
tbl1.AddCells(FormatRow("Gesamt", sum, bold: true, noTopBorder: true));
} else {
var noBorder = true;
if (Payment.NetAmount != Payment.Amount) {
tbl1.AddCells(FormatRow("Zwischensumme", Payment.NetAmount, noTopBorder: noBorder));
noBorder = false;
tbl1.AddCells(FormatRow(MemberModifier ?? "", Payment.Amount - Payment.NetAmount, add: true));
}
if (Credit == null) {
tbl1.AddCells(FormatRow("Gesamtbetrag", Payment.Amount, bold: true, noTopBorder: noBorder));
// TODO Mock VAT
} else {
var hasPrev = Credit.PrevNetAmount != null;
tbl1.AddCells(FormatRow(hasPrev ? "Gesamtbetrag" : "Nettobetrag", Credit.NetAmount, bold: true, noTopBorder: noBorder));
if (hasPrev) {
tbl1.AddCells(FormatRow("Bisher berücksichtigt", -Credit.PrevNetAmount, add: true));
tbl1.AddCells(FormatRow("Nettobetrag", Credit.NetAmount - (Credit.PrevNetAmount ?? 0)));
}
tbl1.AddCells(FormatRow($"Mehrwertsteuer ({Credit.Vat * 100} %)", Credit.VatAmount, add: true));
tbl1.AddCells(FormatRow("Bruttobetrag", Credit.GrossAmount, bold: true));
}
}
decimal penalty = 0;
string? comment = null;
if (MemberUnderDeliveries != null && MemberUnderDeliveries.Count > 0) {
tbl2.AddCell(NewTd("Anfallende Pönalen durch Unterlieferungen:", colspan: 2).SetPaddingTopMM(5))
.AddCell(NewCell(colspan: 2));
foreach (var u in MemberUnderDeliveries) {
tbl2.AddCells(FormatRow($"{u.Name} ({u.Kg:N0} kg)", u.Amount, add: true, subCat: true));
penalty += u.Amount;
}
penalty = Math.Round(penalty, 2, MidpointRounding.AwayFromZero);
}
if (MemberTotalUnderDelivery != 0) {
tbl2.AddCells(FormatRow("Unterlieferung (GA)", MemberTotalUnderDelivery, add: true));
penalty += MemberTotalUnderDelivery;
}
if (MemberAutoBusinessSharesAmount != 0) {
tbl2.AddCells(FormatRow($"Nachzeichnung von GA ({MemberAutoBusinessShares})", MemberAutoBusinessSharesAmount, add: true));
penalty += MemberAutoBusinessSharesAmount;
}
if (CustomPayment?.Amount != null) {
comment = CustomPayment.Comment;
string text = (CustomPayment.Amount.Value < 0 ? "Weitere Abzüge" : "Weitere Zuschläge") + (comment != null ? "*" : "");
if (comment != null && comment!.Length <= 30) {
text = comment;
comment = null;
}
tbl2.AddCells(FormatRow(text, CustomPayment.Amount.Value, add: true));
penalty += CustomPayment.Amount.Value;
}
if (Credit == null) {
tbl3.AddCells(FormatRow("Auszahlungsbetrag", (Payment?.Amount + penalty) ?? (sum + penalty), bold: true));
} else {
var diff = Credit.Modifiers - penalty;
if (diff != 0) {
tbl3.AddCells(FormatRow(diff < 0 ? "Sonstige Abzüge" : "Sonstige Zuschläge", diff, add: true));
}
if (Credit.PrevModifiers != null && Credit.PrevModifiers != 0) {
tbl3.AddCells(FormatRow("Bereits berücksichtigte Abzüge", -Credit.PrevModifiers, add: true));
}
tbl3.AddCells(FormatRow("Auszahlungsbetrag", Credit.Amount, bold: true));
}
div.AddCell(new Cell().SetPadding(0).SetBorder(Border.NO_BORDER).Add(tbl1));
div.AddCell(new Cell().SetPadding(0).SetBorder(Border.NO_BORDER).Add(tbl2));
div.AddCell(new Cell().SetPadding(0).SetBorder(Border.NO_BORDER).Add(tbl3));
doc.Add(div);
if (comment != null) {
doc.Add(new KernedParagraph($"*{comment}", 12).SetMarginTopMM(10));
}
doc.Add(new KernedParagraph($"Überweisung erfolgt auf Konto {Utils.FormatIban(Member.Iban ?? "-")}.", 12).SetPaddingTopMM(10));
if (Text != null) {
doc.Add(new KernedParagraph(Text, 12).SetMarginTop(12).SetKeepTogether(true));
}
}
protected Cell[] FormatRow(string name, decimal? value, bool add = false, bool bold = false, bool subCat = false, bool noTopBorder = false) {
float textSize = subCat ? 8 : !add ? 12 : 10;
float numSize = subCat ? 8 : 12;
var border = !add && !noTopBorder;
var pad = !add && !noTopBorder ? 1 : 0.5f;
return [
NewTd($"{name}:", textSize, bold: bold, borderTop: border).SetPaddingTopMM(pad),
NewTd(value < 0 ? "–" : (add ? "+" : ""), numSize, right: true, bold: bold, borderTop: border).SetPaddingTopMM(pad),
NewTd(CurrencySymbol, numSize, bold: bold, borderTop: border).SetPaddingTopMM(pad),
NewTd($"{Math.Abs(value ?? 0):N2}", numSize, right: true, bold: bold, borderTop: border).SetPaddingTopMM(pad),
];
}
protected Table NewCreditTable(CreditNoteDeliveryData data) {
var tbl = new Table(ColsMM(25, 6, 36, 10, 10, 15, 12, 13, 5, 17, 16), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Lieferschein-Nr.", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Pos.", rowspan: 2).SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("Sorte/Attribut/Bewirtschaftg.\nZu-/Abschlag", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Gradation", colspan: 2))
.AddHeaderCell(NewTh("Flächenbindung", colspan: 2))
.AddHeaderCell(NewTh("Preis"))
.AddHeaderCell(NewTh("Rbl.").SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("Zu-/Abschläge").SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("Betrag"))
.AddHeaderCell(NewTh("[°Oe]"))
.AddHeaderCell(NewTh("[°KMW]").SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("[kg]", colspan: 2))
.AddHeaderCell(NewTh($"[{CurrencySymbol}/kg]"))
.AddHeaderCell(NewTh("[%]").SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh($"[{CurrencySymbol}]"))
.AddHeaderCell(NewTh($"[{CurrencySymbol}]"));
foreach (var p in data.Rows) {
var sub = new Table(ColsMM(25, 6, 36, 10, 10, 15, 12, 13, 5, 17, 16))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetKeepTogether(true);
var attr = p.Attribute != null || p.Cultivation != null || p.QualId == "WEI";
var rows = Math.Max(p.Buckets.Length, 1 + (attr ? 1 : 0) + p.Modifiers.Length);
for (int i = 0; i < rows; i++) {
if (i == 0) {
sub.AddCell(NewTd(p.LsNr))
.AddCell(NewTd($"{p.DPNr:N0}", center: true).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd(p.Variety))
.AddCell(NewTd($"{p.Gradation.Oe:N0}", center: true))
.AddCell(NewTd($"{p.Gradation.Kmw:N1}", center: true));
} else if (i == 1 && attr) {
var varibute = new KernedParagraph(8);
if (p.Attribute != null) varibute.Add(Normal(p.Attribute));
if (p.Attribute != null && p.Cultivation != null) varibute.Add(Normal(" / "));
if (p.Cultivation != null) varibute.Add(Normal(p.Cultivation));
if ((p.Attribute != null || p.Cultivation != null) && p.QualId == "WEI") varibute.Add(Normal(" / "));
if (p.QualId == "WEI") varibute.Add(Italic("abgew."));
sub.AddCell(NewCell(colspan: 2))
.AddCell(NewTd(varibute, colspan: 3).SetPaddingTop(0));
} else {
var idx = i - (rows - p.Modifiers.Length);
if (idx >= 0 && idx < p.Modifiers.Length) {
sub.AddCell(NewCell(colspan: 2))
.AddCell(NewTd(p.Modifiers[idx], 8, colspan: 3).SetPaddingTop(0).SetPaddingLeftMM(5));
} else {
sub.AddCell(NewCell(colspan: 5));
}
}
if (i < p.Buckets.Length) {
var bucket = p.Buckets[i];
var pad = i == 0 ? 0.5f : 0;
sub.AddCell(NewTd($"{bucket.Name}:", 8)
.SetPaddingTopMM(pad))
.AddCell(NewTd($"{bucket.Value:N0}", right: true)
.SetPaddingTopMM(pad))
.AddCell(NewTd($"{bucket.Price:N4}", right: true)
.SetPaddingTopMM(pad));
} else {
sub.AddCell(NewCell(colspan: 3));
}
if (i == p.Buckets.Length - 1) {
var rebelMod = p.WeighingModifier * 100;
var totalMod = p.TotalModifiers ?? 0;
var pad = i == 0 ? 0.5f : 0;
sub.AddCell(NewTd(rebelMod == 0 ? "-" : (Utils.GetSign(rebelMod) + $"{Math.Abs(rebelMod):0.0##}"), 6, center: true)
.SetPaddingTopMM(pad).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd(totalMod == 0 ? "-" : Utils.GetSign(totalMod) + $"{Math.Abs(totalMod):N2}", center: totalMod == 0, right: true)
.SetPaddingTopMM(pad))
.AddCell(NewTd($"{p.Amount:N2}", right: true)
.SetPaddingTopMM(pad));
} else {
sub.AddCell(NewCell(colspan: 3));
}
}
tbl.AddCell(new Cell(1, 12).SetPadding(0).SetBorder(Border.NO_BORDER).Add(sub));
}
return tbl;
}
}
}
}}
+185
View File
@@ -0,0 +1,185 @@
@using Elwig.Helpers
@using RazorLight
@inherits TemplatePage<Elwig.Documents.CreditNote>
@model Elwig.Documents.CreditNote
@{ Layout = "BusinessDocument"; }
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\CreditNote.css" />
<main>
<h1>@Model.Title</h1>
<table class="credit">
<colgroup>
<col style="width: 25mm;"/>
<col style="width: 6mm;"/>
<col style="width: 21mm;"/>
<col style="width: 15mm;"/>
<col style="width: 10mm;"/>
<col style="width: 10mm;"/>
<col style="width: 15mm;"/>
<col style="width: 12mm;"/>
<col style="width: 13mm;"/>
<col style="width: 5mm;"/>
<col style="width: 17mm;"/>
<col style="width: 16mm;"/>
</colgroup>
<thead>
<tr>
<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;">Attr./Bewirt.</th>
<th colspan="2">Gradation</th>
<th colspan="2">Flächenbindung</th>
<th>Preis</th>
<th class="narrow">Rbl.</th>
<th class="narrow">Zu-/Abschläge</th>
<th>Betrag</th>
</tr>
<tr>
<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="narrow unit">[%]</th>
<th class="unit">[@Model.CurrencySymbol]</th>
<th class="unit">[@Model.CurrencySymbol]</th>
</tr>
</thead>
<tbody class="sum">
@foreach (var p in Model.Data.Rows) {
var rows = Math.Max(p.Buckets.Length, p.Modifiers.Length + 1);
@for (int i = 0; i < rows; i++) {
<tr class="@(i == 0 ? "first" : "") @(rows == i + 1 ? "last" : "")">
@if (i == 0) {
<td rowspan="@rows">@p.LsNr</td>
<td rowspan="@rows" class="center narrow">@p.DPNr</td>
<td class="small">@p.Variety</td>
<td class="small">
@p.Attribute@(p.Attribute != null && p.Cultivation != null ? " / " : "")@p.Cultivation
@((p.Attribute != null || p.Cultivation != null) && p.QualId == "WEI" ? " / " : "")@Raw(p.QualId == "WEI" ? "<i>abgew.</i>" : "")
</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="4" class="small mod">@p.Modifiers[i - 1]</td>
} else if (i > 0) {
<td colspan="4"></td>
}
@if (i < p.Buckets.Length) {
var bucket = p.Buckets[i];
<td class="small">@bucket.Name:</td>
<td class="number">@($"{bucket.Value:N0}")</td>
<td class="number">@($"{bucket.Price:N4}")</td>
} else {
<td></td>
}
@if (i == p.Buckets.Length - 1) {
var rebelMod = p.WeighingModifier * 100;
var totalMod = p.TotalModifiers ?? 0;
<td class="tiny center">@(rebelMod == 0 ? "-" : (Utils.GetSign(rebelMod) + $"{Math.Abs(rebelMod):0.0##}"))</td>
<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 anfal&shy;lenden Pönalen werden mit
@Model.Payment?.Variant.Season.Precision Nach&shy;komma&shy;stellen berechnent,
erst das Ergebnis wird kauf&shy;männisch auf 2 Nach&shy;komma&shy;stellen gerundet.
</div>
<table class="credit-sum">
<colgroup>
<col style="width: auto;"/>
<col style="width: 5mm;"/>
<col style="width: 30mm;"/>
</colgroup>
@{
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" : "")}\">{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, noTopBorder: noBorder))
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.MemberTotalUnderDelivery != 0) {
@Raw(FormatRow("Unterlieferung (GA)", Model.MemberTotalUnderDelivery, add: true));
penalty += Model.MemberTotalUnderDelivery;
}
@if (Model.MemberAutoBusinessSharesAmount != 0) {
@Raw(FormatRow($"Autom. Nachz. von GA ({Model.MemberAutoBusinessShares})", Model.MemberAutoBusinessSharesAmount, add: true));
penalty += Model.MemberAutoBusinessSharesAmount;
}
@if (Model.CustomPayment?.Amount != null) {
@Raw(FormatRow(Model.CustomPayment.Comment ?? ((Model.CustomPayment.Amount.Value) < 0 ? "Weitere Abzüge" : "Weitere Zuschläge"), Model.CustomPayment.Amount.Value, add: true));
penalty += Model.CustomPayment.Amount.Value;
}
@if (Model.Credit == null) {
@Raw(FormatRow("Auszahlungsbetrag", (Model.Payment?.Amount + penalty) ?? (sum + penalty), bold: true))
} else {
var diff = Model.Credit.Modifiers - penalty;
if (diff != 0) {
@Raw(FormatRow(diff < 0 ? "Sonstige Abzüge" : "Sonstige Zuschläge", diff, 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>
<p>Überweisung erfolgt auf Konto @(Elwig.Helpers.Utils.FormatIban(Model.Member.Iban ?? "-")).</p>
<div style="margin-top: 1em;">
@if (Model.Text != null) {
<p class="custom">@Model.Text</p>
}
</div>
</main>
+48
View File
@@ -0,0 +1,48 @@
table.credit {
margin-bottom: 0;
}
table.credit .mod {
padding-left: 5mm;
}
table.credit tbody tr:not(.first) {
break-before: avoid;
}
table.credit tbody tr:not(.last) {
break-after: avoid;
}
table.credit tr:not(.first) td {
padding-top: 0;
}
table.credit tr.last td {
padding-bottom: 0;
}
table.credit-sum {
width: 60%;
margin-left: 40%;
}
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: 56mm;
position: absolute;
left: 0;
margin: 2mm 4mm;
}
+3 -50
View File
@@ -1,9 +1,5 @@
using Elwig.Models.Dtos;
using iText.Kernel.Pdf;
using iText.Layout.Element;
using iText.Layout.Properties;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class DeliveryAncmtList : Document {
@@ -11,58 +7,15 @@ namespace Elwig.Documents {
public new static string Name => "Anmeldeliste";
public string Filter;
public List<DeliveryAncmtListRow> Announcements;
public IEnumerable<DeliveryAncmtListRow> Announcements;
public DeliveryAncmtList(string filter, IEnumerable<DeliveryAncmtListRow> announcements) :
base($"{Name} {filter}") {
public DeliveryAncmtList(string filter, IEnumerable<DeliveryAncmtListRow> announcements) : base($"{Name} {filter}") {
Filter = filter;
Announcements = [.. announcements];
Announcements = announcements;
}
public DeliveryAncmtList(string filter, DeliveryAncmtListData data) :
this(filter, data.Rows) {
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
doc.Add(new KernedParagraph(Name, 24)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(new KernedParagraph(Filter, 14)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(NewAncmtTable(Announcements));
}
protected Table NewAncmtTable(List<DeliveryAncmtListRow> ancmts) {
var tbl = new Table(ColsMM(15, 12, 50, 25, 38, 11, 14), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Datum", rowspan: 2))
.AddHeaderCell(NewTh("MgNr.", rowspan: 2))
.AddHeaderCell(NewTh("Mitglied", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Ort", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Sorte", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Anmldg.", rowspan: 2))
.AddHeaderCell(NewTh("Menge"))
.AddHeaderCell(NewTh("[kg]"));
foreach (var a in ancmts) {
tbl.AddCell(NewTd($"{a.Date:dd.MM.yyyy}", 8))
.AddCell(NewTd($"{a.MgNr}", right: true))
.AddCell(NewTd(a.AdministrativeName))
.AddCell(NewTd(a.DefaultKg, 8))
.AddCell(NewTd(a.Variety))
.AddCell(NewTd(a.Status ?? "-", 8, center: true))
.AddCell(NewTd($"{a.Weight:N0}", right: true));
}
tbl.AddCell(NewTd("Gesamt:", colspan: 2, bold: true, borderTop: true))
.AddCell(NewTd($"Anmeldungen: {ancmts.Count:N0}", colspan: 3, bold: true, borderTop: true))
.AddCell(NewTd($"{ancmts.Sum(a => a.Weight):N0}", colspan: 2, right: true, bold: true, borderTop: true));
return tbl;
}
}
}
+52
View File
@@ -0,0 +1,52 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.DeliveryAncmtList>
@model Elwig.Documents.DeliveryAncmtList
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\DeliveryAncmtList.css" />
<main>
<h1>Anmeldeliste</h1>
<h2>@Model.Filter</h2>
<table class="announcement-list">
<colgroup>
<col style="width: 15mm;"/>
<col style="width: 12mm;"/>
<col style="width: 50mm;"/>
<col style="width: 25mm;"/>
<col style="width: 38mm;"/>
<col style="width: 11mm;"/>
<col style="width: 14mm;"/>
</colgroup>
<thead>
<tr>
<th rowspan="2">Datum</th>
<th rowspan="2">MgNr.</th>
<th rowspan="2" style="text-align: left;">Mitglied</th>
<th rowspan="2" style="text-align: left;">Ort</th>
<th rowspan="2" style="text-align: left;">Sorte</th>
<th rowspan="2">Anmldg.</th>
<th>Menge</th>
</tr>
<tr>
<th class="unit">[kg]</th>
</tr>
</thead>
<tbody>
@foreach (var a in Model.Announcements) {
<tr>
<td class="small">@($"{a.Date:dd.MM.yyyy}")</td>
<td class="number">@a.MgNr</td>
<td>@a.AdministrativeName</td>
<td class="small">@a.DefaultKg</td>
<td>@a.Variety</td>
<td class="small center">@(a.Status ?? "-")</td>
<td class="number">@($"{a.Weight:N0}")</td>
</tr>
}
<tr class="sum bold">
<td colspan="2">Gesamt:</td>
<td colspan="3">Anmeldungen: @($"{Model.Announcements.Count():N0}")</td>
<td colspan="2" class="number">@($"{Model.Announcements.Sum(a => a.Weight):N0}")</td>
</tr>
</tbody>
</table>
</main>
+13
View File
@@ -0,0 +1,13 @@
h1 {
text-align: center;
font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}
+10 -157
View File
@@ -1,176 +1,29 @@
using Elwig.Helpers;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Documents {
public class DeliveryConfirmation : BusinessDocument {
public new static string Name => "Anlieferungsbestätigung";
private readonly int _year;
public Season? Season;
public int MemberDeliveredWeightRed;
public int MemberDeliveredWeightWhite;
public DeliveryConfirmationDeliveryData? Data;
public Season Season;
public DeliveryConfirmationDeliveryData Data;
public string? Text = App.Client.TextDeliveryConfirmation;
public Dictionary<string, MemberBucket> MemberBuckets = [];
public List<MemberStat> MemberStats = [];
public Dictionary<string, MemberBucket> MemberBuckets;
public List<MemberStat> MemberStats;
public DeliveryConfirmation(int year, Member m, DateOnly? dateFrom, DeliveryConfirmationDeliveryData? data = null) :
base($"{Name} {year}", m, dateFrom) {
_year = year;
public DeliveryConfirmation(AppDbContext ctx, int year, Member m, DeliveryConfirmationDeliveryData data) :
base($"{Name} {year}", m) {
Season = ctx.Seasons.Find(year) ?? throw new ArgumentException("invalid season");
ShowDateAndLocation = true;
UseBillingAddress = true;
DocumentId = $"Anlfrg.-Bstng. {_year}/{m.MgNr}";
DocumentId = $"Anl.-Best. {Season.Year}/{m.MgNr}";
Data = data;
}
protected override async Task LoadData(AppDbContext ctx) {
await base.LoadData(ctx);
Season = await ctx.FetchSeasons(_year).SingleOrDefaultAsync() ?? throw new ArgumentException("Invalid season");
var weights = await ctx.Deliveries
.Where(d => d.Year == Season.Year && d.MgNr == Member.MgNr)
.SelectMany(d => d.Parts)
.GroupBy(p => p.Variety.Type)
.ToDictionaryAsync(g => g.Key, g => g.Sum(p => p.Weight));
MemberDeliveredWeightRed = weights.GetValueOrDefault("R", 0);
MemberDeliveredWeightWhite = weights.GetValueOrDefault("W", 0);
MemberShares = await ctx.GetMemberHistory(Season.Year, Member.MgNr);
MemberBuckets = await ctx.GetMemberBuckets(Season.Year, Member.MgNr);
MemberStats = await AppDbContext.GetMemberStats(Season.Year, Member.MgNr);
Data ??= await DeliveryConfirmationDeliveryData.ForMember(ctx.DeliveryParts, Season.Year, Member);
}
protected override void BeforeRenderBody(iText.Layout.Document doc, PdfDocument pdf) {
if (Data == null) throw new Exception("Call LoadData before BeforeRenderBody");
base.BeforeRenderBody(doc, pdf);
var firstDay = Data.Rows.MinBy(r => r.Date)?.Date;
var lastDay = Data.Rows.MaxBy(r => r.Date)?.Date;
Aside?.AddCell(NewAsideCell("Saison", 26))
.AddCell(NewAsideCell("Lieferungen:", 12, isName: true)).AddCell(NewAsideCell($"{Data.Rows.DistinctBy(r => r.LsNr).Count():N0} (Teil-Lfrg.: {Data.RowNum:N0})", 14))
.AddCell(NewAsideCell("Zeitraum:", 8, isName: true)).AddCell(NewAsideCell(firstDay == null || lastDay == null ? "-" : firstDay == lastDay ? $"{firstDay:dd.MM.} (1 Tag)" : $"{firstDay:dd.MM.}\u2013{lastDay:dd.MM.} ({lastDay?.DayNumber - firstDay?.DayNumber + 1:N0} Tage)", 18, center: true))
.AddCell(NewAsideCell("Menge:", 12, isName: true)).AddCell(NewAsideCell($"{MemberStats.Sum(s => s.Weight):N0} kg", 14));
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
if (Season == null || Data == null) throw new Exception("Call LoadData before RenderBody");
base.RenderBody(doc, pdf);
doc.Add(NewDeliveryListTable(Data));
doc.Add(NewWeightsTable(MemberStats)
.SetMarginTopMM(10).SetKeepTogether(true));
doc.Add(NewBucketTable(Season, MemberBuckets, MemberDeliveredWeightRed, MemberDeliveredWeightWhite, includePayment: true)
.SetMarginTopMM(10).SetKeepTogether(true));
if (Text != null) {
doc.Add(new KernedParagraph(Text, 10)
.SetMarginTop(20).SetKeepTogether(true));
}
}
protected Table NewDeliveryListTable(DeliveryConfirmationDeliveryData data) {
var tbl = new Table(ColsMM(25, 7, 39, 14, 11, 11, 15, 12, 14, 3, 14), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Lieferschein-Nr.", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Pos.", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Sorte/Attribut/Bewirtschaftung\nZu-/Abschlag", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Qual.", rowspan: 2))
.AddHeaderCell(NewTh("Gradation", colspan: 2))
.AddHeaderCell(NewTh("Flächenbindung", colspan: 2))
.AddHeaderCell(NewTh("Menge"))
.AddHeaderCell(NewTh("gerebelt", rowspan: 3, rotated: true))
.AddHeaderCell(NewTh("Davon\nabzuwerten"))
.AddHeaderCell(NewTh("[°Oe]"))
.AddHeaderCell(NewTh("[°KMW]"))
.AddHeaderCell(NewTh("[kg]", colspan: 2))
.AddHeaderCell(NewTh("[kg]"))
.AddHeaderCell(NewTh("[kg]"));
var lastVariety = "";
foreach (var p in data.Rows) {
var sub = new Table(ColsMM(25, 7, 39, 14, 11, 11, 15, 12, 14, 3, 14))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetKeepTogether(true);
if (lastVariety != "" && lastVariety != p.Variety) {
sub.SetBorderTop(new SolidBorder(BorderThickness));
}
var attr = p.Attribute != null || p.Cultivation != null;
var rows = Math.Max(p.Buckets.Length, 1 + (attr ? 1 : 0) + p.Modifiers.Length);
for (int i = 0; i < rows; i++) {
if (i == 0) {
sub.AddCell(NewTd(p.LsNr))
.AddCell(NewTd($"{p.DPNr:N0}", center: true))
.AddCell(NewTd(p.Variety))
.AddCell(NewTd(p.QualId, center: true))
.AddCell(NewTd($"{p.Gradation.Oe:N0}", center: true))
.AddCell(NewTd($"{p.Gradation.Kmw:N1}", center: true));
} else if (i == 1 && attr) {
sub.AddCell(NewCell(colspan: 2))
.AddCell(NewTd($"{p.Attribute}{(p.Attribute != null && p.Cultivation != null ? " / " : "")}{p.Cultivation}", 8, colspan: 2)
.SetPaddingTop(0))
.AddCell(NewCell(colspan: 2));
} else {
sub.AddCell(NewCell(colspan: 2));
var idx = i - (rows - p.Modifiers.Length);
if (idx >= 0 && idx < p.Modifiers.Length) {
sub.AddCell(NewTd(p.Modifiers[idx], 8, colspan: 2)
.SetPaddingTop(0).SetPaddingLeftMM(5));
} else {
sub.AddCell(NewCell(colspan: 2));
}
sub.AddCell(NewCell(colspan: 2));
}
if (i < p.Buckets.Length) {
var bucket = p.Buckets[i];
var pad = i == 0 ? 0.5f : 0;
sub.AddCell(NewTd($"{bucket.Name}:", 8).SetHeight(10).SetPaddingTopMM(pad));
sub.AddCell(NewTd($"{bucket.Value:N0}", right: true).SetPaddingTopMM(pad));
} else {
sub.AddCell(NewCell(colspan: 2));
}
if (i == p.Buckets.Length - 1) {
sub.AddCell(NewTd($"{p.Weight:N0}", right: true).SetPaddingTop(0));
sub.AddCell(NewTd(p.IsNetWeight ? "\u2611" : "\u2610", 7, right: true).SetFont(SF).SetPadding(0));
} else {
sub.AddCell(NewCell(colspan: 2));
}
if (i == 0) {
sub.AddCell(NewCell(rowspan: rows));
}
lastVariety = p.Variety;
}
tbl.AddCell(new Cell(1, 11).SetPadding(0).SetBorder(Border.NO_BORDER).Add(sub));
}
tbl.AddCell(NewTd("Gesamt:", 12, colspan: 7, bold: true, borderTop: true)
.SetPaddingsMM(1, 1, 0, 1))
.AddCell(NewTd($"{data.Rows.Sum(p => p.Weight):N0}", 12, colspan: 2, right: true, bold: true, borderTop: true)
.SetPaddingsMM(1, 1, 0, 1))
.AddCell(NewTd(colspan: 2, borderTop: true)
.SetPaddingsMM(1, 1, 0, 1))
.AddCell(NewTd("Davon abgewertet:", 10, colspan: 7)
.SetPaddingsMM(0, 1, 0.5f, 1))
.AddCell(NewTd($"{data.Rows.Where(p => p.IsDepreciated).Sum(p => p.Weight):N0}", 10, colspan: 2, right: true)
.SetPaddingsMM(0, 1, 0.5f, 1))
.AddCell(NewCell(colspan: 2)
.SetPaddingsMM(0, 1, 0.5f, 1));
return tbl;
MemberBuckets = ctx.GetMemberBuckets(Season.Year, m.MgNr).GetAwaiter().GetResult();
MemberStats = AppDbContext.GetMemberStats(Season.Year, m.MgNr).GetAwaiter().GetResult();
}
}
}
+112
View File
@@ -0,0 +1,112 @@
@using Elwig.Documents
@using RazorLight
@inherits TemplatePage<Elwig.Documents.DeliveryConfirmation>
@model Elwig.Documents.DeliveryConfirmation
@{ Layout = "BusinessDocument"; }
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\DeliveryConfirmation.css" />
<main>
<h1>@Model.Title</h1>
<table class="delivery-confirmation">
<colgroup>
<col style="width: 25mm;"/>
<col style="width: 6mm;"/>
<col style="width: 23mm;"/>
<col style="width: 16mm;"/>
<col style="width: 17mm;"/>
<col style="width: 10mm;"/>
<col style="width: 10mm;"/>
<col style="width: 15mm;"/>
<col style="width: 12mm;"/>
<col style="width: 14mm;"/>
<col style="width: 3mm;"/>
<col style="width: 14mm;"/>
</colgroup>
<thead>
<tr>
<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;">Attr./Bewirt.</th>
<th rowspan="2" style="text-align: left;">Qualitätsstufe</th>
<th colspan="2">Gradation</th>
<th colspan="2">Flächenbindung</th>
<th>Menge</th>
<th rowspan="3" style="padding: 0;">
<svg width="10" height="40" xmlns="http://www.w3.org/2000/svg">
<text x="-40" y="4" transform="rotate(270)" font-size="8pt" font-style="italic" font-family="Times New Roman"
style="text-anchor: start; alignment-baseline: middle;">
gerebelt
</text>
</svg>
</th>
<th>Davon<br/>abzuwerten</th>
</tr>
<tr>
<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>
@{
var lastVariety = "";
}
@foreach (var p in Model.Data.Rows) {
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.Variety != lastVariety && lastVariety != "" ? "new": "") @(rows > i + 1 ? "last" : "")">
@if (first) {
<td rowspan="@rows">@p.LsNr</td>
<td rowspan="@rows" class="center narrow">@p.DPNr</td>
<td class="small">@p.Variety</td>
<td class="small">@p.Attribute@(p.Attribute != null && p.Cultivation != null ? " / " : "")@p.Cultivation</td>
<td class="small">@p.QualityLevel</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="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="small">@bucket.Name:</td>
<td class="number">@($"{bucket.Value:N0}")</td>
} else {
<td colspan="2"></td>
}
@if (i == p.Buckets.Length - 1) {
<td class="number">@($"{p.Weight:N0}")</td>
<td style="font-size: 7pt;">@(p.IsNetWeight ? "\u2611" : "\u2610")</td>
} else {
<td></td>
<td></td>
}
@if (first) {
<td rowspan="@rows" class="number"></td>
first = false;
}
</tr>
lastVariety = p.Variety;
}
}
<tr class="sum bold">
<td colspan="8">Gesamt:</td>
<td colspan="2" class="number">@($"{Model.Data.Rows.Sum(p => p.Weight):N0}")</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
@Raw(BusinessDocument.PrintSortenaufteilung(Model.MemberStats))
@Raw(Model.PrintBucketTable(Model.Season, Model.MemberBuckets, includePayment: true))
<div style="margin-top: 2em;">
@if (Model.Text != null) {
<p class="custom comment">@Model.Text</p>
}
</div>
</main>
+32
View File
@@ -0,0 +1,32 @@
table.delivery-confirmation .mod {
padding-left: 5mm;
}
table.delivery-confirmation tr:not(.first) {
break-before: avoid;
}
table.delivery-confirmation tr:not(.first) td {
padding-top: 0;
}
table.delivery-confirmation tr.last td {
padding-bottom: 0;
}
table.delivery-confirmation tr.sum {
font-size: 12pt;
}
table.delivery-confirmation tr.sum td {
padding-top: 1mm;
}
table.sortenaufteilung {
break-after: avoid;
}
table.buckets {
break-before: avoid;
}
+2 -88
View File
@@ -1,9 +1,5 @@
using Elwig.Models.Dtos;
using iText.Kernel.Pdf;
using iText.Layout.Element;
using iText.Layout.Properties;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class DeliveryDepreciationList : Document {
@@ -11,98 +7,16 @@ namespace Elwig.Documents {
public new static string Name => "Abwertungsliste";
public string Filter;
public List<DeliveryJournalRow> Deliveries;
public IEnumerable<DeliveryJournalRow> Deliveries;
public DeliveryDepreciationList(string filter, IEnumerable<DeliveryJournalRow> deliveries) :
base($"{Name} {filter}") {
Filter = filter;
Deliveries = [.. deliveries];
Deliveries = deliveries;
}
public DeliveryDepreciationList(string filter, DeliveryJournalData data) :
this(filter, data.Rows) {
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
doc.Add(new KernedParagraph(Name, 24)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(new KernedParagraph(Filter, 14)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 5, 0));
doc.Add(NewJournalTable(Deliveries));
}
protected Table NewJournalTable(List<DeliveryJournalRow> deliveries) {
var tbl = new Table(ColsMM(25, 6, 20, 12, 38, 18, 12, 10, 10, 14), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Lieferschein-Nr.", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Pos.", rowspan: 2).SetPaddingRight(0))
.AddHeaderCell(NewTh("Datum", rowspan: 2))
.AddHeaderCell(NewTh("Zeit", rowspan: 2))
.AddHeaderCell(NewTh("Sorte", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Attr./Bewirt.", rowspan: 2, colspan: 2, left: true))
.AddHeaderCell(NewTh("Gradation", colspan: 2))
.AddHeaderCell(NewTh("Menge"))
.AddHeaderCell(NewTh("[°Oe]"))
.AddHeaderCell(NewTh("[°KMW]").SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("[kg]"));
int? lastMember = null;
foreach (var p in deliveries) {
if (lastMember != p.MgNr) {
var border = lastMember != null;
var memberDeliveries = deliveries.Where(d => d.MgNr == p.MgNr).ToList();
var memberKmw = Helpers.Utils.AggregateDeliveryPartsKmw(memberDeliveries);
var memberOe = Helpers.Utils.KmwToOe(memberKmw);
tbl.AddCell(NewTd($"{p.MgNr}, {p.AdministrativeName}", colspan: 5, borderTop: border)
.SetFont(BI))
.AddCell(NewTd("Teil-Lfrg.:", borderTop: border, bold: true))
.AddCell(NewTd($"{memberDeliveries.Count:N0}", right: true, borderTop: border, bold: true))
.AddCell(NewTd($"{memberOe:N0}", center: true, borderTop: border, bold: true))
.AddCell(NewTd($"{memberKmw:N1}", center: true, borderTop: border, bold: true))
.AddCell(NewTd($"{memberDeliveries.Sum(p => p.Weight):N0}", right: true, borderTop: border, bold: true));
}
tbl.AddCell(NewTd(p.LsNr))
.AddCell(NewTd($"{p.Pos:N0}", center: true).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd($"{p.Date:dd.MM.yyyy}", center: true))
.AddCell(NewTd($"{p.Time:HH:mm}", center: true).SetPadding(0))
.AddCell(NewTd(p.Variety))
.AddCell(NewTd($"{p.Attribute}{(p.Attribute != null && p.Cultivation != null ? " / " : "")}{p.Cultivation}", colspan: 2))
.AddCell(NewTd($"{p.Oe:N0}", center: true))
.AddCell(NewTd($"{p.Kmw:N1}", center: true).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd($"{p.Weight:N0}", right: true));
lastMember = p.MgNr;
}
var branches = deliveries.Select(d => d.DeliveryBranch).Distinct().Order().ToArray();
if (branches.Length > 1) {
foreach (var b in branches) {
var border = branches[0] == b;
var branchDeliveries = deliveries.Where(d => d.DeliveryBranch == b).ToList();
var branchKmw = Helpers.Utils.AggregateDeliveryPartsKmw(branchDeliveries);
var branchOe = Helpers.Utils.KmwToOe(branchKmw);
tbl.AddCell(NewTd($"{b}:", colspan: 2, bold: true, borderTop: border))
.AddCell(NewTd($"(Teil-)Lieferungen: {branchDeliveries.DistinctBy(p => p.LsNr).Count():N0} ({branchDeliveries.Count:N0})", colspan: 5, bold: true, borderTop: border))
.AddCell(NewTd($"{branchOe:N0}", center: true, bold: true, borderTop: border))
.AddCell(NewTd($"{branchOe:N1}", center: true, bold: true, borderTop: border))
.AddCell(NewTd($"{branchDeliveries.Sum(p => p.Weight):N0}", right: true, bold: true, borderTop: border));
}
}
var kmw = Helpers.Utils.AggregateDeliveryPartsKmw(deliveries);
var oe = Helpers.Utils.KmwToOe(kmw);
tbl.AddCell(NewTd("Gesamt:", colspan: 2, bold: true, borderTop: true))
.AddCell(NewTd($"(Teil-)Lieferungen: {deliveries.DistinctBy(p => p.LsNr).Count():N0} ({deliveries.Count:N0})", colspan: 5, bold: true, borderTop: true))
.AddCell(NewTd($"{oe:N0}", center: true, bold: true, borderTop: true))
.AddCell(NewTd($"{kmw:N1}", center: true, bold: true, borderTop: true))
.AddCell(NewTd($"{deliveries.Sum(p => p.Weight):N0}", right: true, bold: true, borderTop: true));
return tbl;
}
}
}
@@ -0,0 +1,104 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.DeliveryDepreciationList>
@model Elwig.Documents.DeliveryDepreciationList
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\DeliveryDepreciationList.css" />
<main>
<h1>Abwertungsliste</h1>
<h2>@Model.Filter</h2>
<table class="journal">
<colgroup>
<col style="width: 25mm;"/>
<col style="width: 6mm;"/>
<col style="width: 20mm;"/>
<col style="width: 15mm;"/>
<col style="width: 35mm;"/>
<col style="width: 30mm;"/>
<col style="width: 10mm;"/>
<col style="width: 10mm;"/>
<col style="width: 14mm;"/>
</colgroup>
<thead>
<tr>
<th rowspan="2" style="text-align: left;">Lieferschein-Nr.</th>
<th rowspan="2" class="narrow">Pos.</th>
<th rowspan="2">Datum</th>
<th rowspan="2">Zeit</th>
<th rowspan="2" style="text-align: left;">Sorte</th>
<th rowspan="2" style="text-align: left;">Attr./Bewirt.</th>
<th colspan="2">Gradation</th>
<th>Menge</th>
</tr>
<tr>
<th class="unit">[°Oe]</th>
<th class="unit narrow">[°KMW]</th>
<th class="unit">[kg]</th>
</tr>
</thead>
<tbody>
@{
int? lastMember = null;
}
@foreach (var p in Model.Deliveries) {
if (lastMember != p.MgNr) {
<tr class="subheading @(lastMember != null ? "new" : "")">
@{
var memberDeliveries = Model.Deliveries.Where(d => d.MgNr == p.MgNr).ToList();
var memberKmw = Elwig.Helpers.Utils.AggregateDeliveryPartsKmw(memberDeliveries);
var memberOe = Elwig.Helpers.Utils.KmwToOe(memberKmw);
}
<th colspan="5">
@($"{p.MgNr}, {p.AdministrativeName}")
</th>
<td>Teil-Lfrg.: <span style="float: right;">@($"{memberDeliveries.Count():N0}")</span></td>
<td class="center">@($"{memberOe:N0}")</td>
<td class="center">@($"{memberKmw:N1}")</td>
<td class="number">@($"{memberDeliveries.Sum(p => p.Weight):N0}")</td>
</tr>
}
<tr>
<td>@p.LsNr</td>
<td class="center narrow">@p.Pos</td>
<td>@($"{p.Date:dd.MM.yyyy}")</td>
<td>@($"{p.Time:HH:mm}")</td>
<td>@p.Variety</td>
<td>@p.Attribute@(p.Attribute != null && p.Cultivation != null ? " / " : "")@p.Cultivation</td>
<td class="center">@($"{p.Oe:N0}")</td>
<td class="center">@($"{p.Kmw:N1}")</td>
<td class="number">@($"{p.Weight:N0}")</td>
</tr>
lastMember = p.MgNr;
}
@{
var branches = Model.Deliveries.Select(d => d.DeliveryBranch).Distinct().Order().ToArray();
if (branches.Length > 1) {
foreach (var b in branches) {
<tr class="@(branches[0] == b ? "sum" : "") bold">
@{
var branchDeliveries = Model.Deliveries.Where(d => d.DeliveryBranch == b).ToList();
var branchKmw = Elwig.Helpers.Utils.AggregateDeliveryPartsKmw(branchDeliveries);
var branchOe = Elwig.Helpers.Utils.KmwToOe(branchKmw);
}
<td colspan="2">@b:</td>
<td colspan="4">(Teil-)Lieferungen: @($"{branchDeliveries.DistinctBy(p => p.LsNr).Count():N0}") (@($"{branchDeliveries.Count():N0}"))</td>
<td class="center">@($"{branchOe:N0}")</td>
<td class="center">@($"{branchKmw:N1}")</td>
<td class="number">@($"{branchDeliveries.Sum(p => p.Weight):N0}")</td>
</tr>
}
}
}
<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="4">(Teil-)Lieferungen: @($"{Model.Deliveries.DistinctBy(p => p.LsNr).Count():N0}") (@($"{Model.Deliveries.Count():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>
</main>
@@ -0,0 +1,13 @@
h1 {
text-align: center;
font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}
+2 -74
View File
@@ -1,9 +1,5 @@
using Elwig.Models.Dtos;
using iText.Kernel.Pdf;
using iText.Layout.Element;
using iText.Layout.Properties;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class DeliveryJournal : Document {
@@ -11,84 +7,16 @@ namespace Elwig.Documents {
public new static string Name => "Lieferjournal";
public string Filter;
public List<DeliveryJournalRow> Deliveries;
public IEnumerable<DeliveryJournalRow> Deliveries;
public DeliveryJournal(string filter, IEnumerable<DeliveryJournalRow> deliveries) :
base($"{Name} {filter}") {
Filter = filter;
Deliveries = [.. deliveries];
Deliveries = deliveries;
}
public DeliveryJournal(string filter, DeliveryJournalData data) :
this(filter, data.Rows) {
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
doc.Add(new KernedParagraph(Name, 24)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(new KernedParagraph(Filter, 14)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 5, 0));
doc.Add(NewJournalTable(Deliveries));
}
protected Table NewJournalTable(List<DeliveryJournalRow> deliveries) {
var tbl = new Table(ColsMM(25, 6, 15, 8, 11, 38, 28, 10, 10, 14), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Lieferschein-Nr.", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Pos.", rowspan: 2).SetPaddingRight(0))
.AddHeaderCell(NewTh("Datum", rowspan: 2))
.AddHeaderCell(NewTh("Zeit", rowspan: 2))
.AddHeaderCell(NewTh("MgNr.", rowspan: 2))
.AddHeaderCell(NewTh("Mitglied", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Sorte", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Gradation", colspan: 2))
.AddHeaderCell(NewTh("Menge"))
.AddHeaderCell(NewTh("[°Oe]"))
.AddHeaderCell(NewTh("[°KMW]").SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("[kg]"));
foreach (var p in deliveries) {
tbl.AddCell(NewTd(p.LsNr))
.AddCell(NewTd($"{p.Pos:N0}", center: true).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd($"{p.Date:dd.MM.yyyy}", 8, center: true))
.AddCell(NewTd($"{p.Time:HH:mm}", 8, center: true).SetPadding(0))
.AddCell(NewTd($"{p.MgNr}", right: true))
.AddCell(NewTd(p.AdministrativeName, 8))
.AddCell(NewTd(p.Variety, 8))
.AddCell(NewTd($"{p.Oe:N0}", center: true))
.AddCell(NewTd($"{p.Kmw:N1}", center: true).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd($"{p.Weight:N0}", right: true));
}
var branches = deliveries.Select(d => d.DeliveryBranch).Distinct().Order().ToArray();
if (branches.Length > 1) {
foreach (var b in branches) {
var border = branches[0] == b;
var branchDeliveries = deliveries.Where(d => d.DeliveryBranch == b).ToList();
var branchKmw = Helpers.Utils.AggregateDeliveryPartsKmw(branchDeliveries);
var branchOe = Helpers.Utils.KmwToOe(branchKmw);
tbl.AddCell(NewTd($"{b}:", colspan: 2, bold: true, borderTop: border))
.AddCell(NewTd($"(Teil-)Lieferungen: {branchDeliveries.DistinctBy(p => p.LsNr).Count():N0} ({branchDeliveries.Count:N0})", colspan: 5, bold: true, borderTop: border))
.AddCell(NewTd($"{branchOe:N0}", center: true, bold: true, borderTop: border))
.AddCell(NewTd($"{branchOe:N1}", center: true, bold: true, borderTop: border))
.AddCell(NewTd($"{branchDeliveries.Sum(p => p.Weight):N0}", right: true, bold: true, borderTop: border));
}
}
var kmw = Helpers.Utils.AggregateDeliveryPartsKmw(deliveries);
var oe = Helpers.Utils.KmwToOe(kmw);
tbl.AddCell(NewTd("Gesamt:", colspan: 2, bold: true, borderTop: true))
.AddCell(NewTd($"(Teil-)Lieferungen: {deliveries.DistinctBy(p => p.LsNr).Count():N0} ({deliveries.Count:N0})", colspan: 5, bold: true, borderTop: true))
.AddCell(NewTd($"{oe:N0}", center: true, bold: true, borderTop: true))
.AddCell(NewTd($"{kmw:N1}", center: true, bold: true, borderTop: true))
.AddCell(NewTd($"{deliveries.Sum(p => p.Weight):N0}", right: true, bold: true, borderTop: true));
return tbl;
}
}
}
+87
View File
@@ -0,0 +1,87 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.DeliveryJournal>
@model Elwig.Documents.DeliveryJournal
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\DeliveryJournal.css"/>
<main>
<h1>Lieferjournal</h1>
<h2>@Model.Filter</h2>
<table class="journal">
<colgroup>
<col style="width: 25mm;"/>
<col style="width: 6mm;"/>
<col style="width: 15mm;"/>
<col style="width: 8mm;"/>
<col style="width: 11mm;"/>
<col style="width: 38mm;"/>
<col style="width: 28mm;"/>
<col style="width: 10mm;"/>
<col style="width: 10mm;"/>
<col style="width: 14mm;"/>
</colgroup>
<thead>
<tr>
<th rowspan="2" style="text-align: left;">Lieferschein-Nr.</th>
<th rowspan="2" class="narrow">Pos.</th>
<th rowspan="2">Datum</th>
<th rowspan="2">Zeit</th>
<th rowspan="2">MgNr.</th>
<th rowspan="2" style="text-align: left;">Mitglied</th>
<th rowspan="2" style="text-align: left;">Sorte</th>
<th colspan="2">Gradation</th>
<th>Menge</th>
</tr>
<tr>
<th class="unit">[°Oe]</th>
<th class="unit narrow">[°KMW]</th>
<th class="unit">[kg]</th>
</tr>
</thead>
<tbody>
@foreach (var p in Model.Deliveries) {
<tr>
<td>@p.LsNr</td>
<td class="center narrow">@p.Pos</td>
<td class="small">@($"{p.Date:dd.MM.yyyy}")</td>
<td class="small">@($"{p.Time:HH:mm}")</td>
<td class="number">@p.MgNr</td>
<td class="small">@p.AdministrativeName</td>
<td class="small">@p.Variety</td>
<td class="center">@($"{p.Oe:N0}")</td>
<td class="center">@($"{p.Kmw:N1}")</td>
<td class="number">@($"{p.Weight:N0}")</td>
</tr>
}
@{
var branches = Model.Deliveries.Select(d => d.DeliveryBranch).Distinct().Order().ToArray();
if (branches.Length > 1) {
foreach (var b in branches) {
<tr class="@(branches[0] == b ? "sum" : "") bold">
@{
var branchDeliveries = Model.Deliveries.Where(d => d.DeliveryBranch == b).ToList();
var branchKmw = Elwig.Helpers.Utils.AggregateDeliveryPartsKmw(branchDeliveries);
var branchOe = Elwig.Helpers.Utils.KmwToOe(branchKmw);
}
<td colspan="2">@b:</td>
<td colspan="5">(Teil-)Lieferungen: @($"{branchDeliveries.DistinctBy(p => p.LsNr).Count():N0}") (@($"{branchDeliveries.Count():N0}"))</td>
<td class="center">@($"{branchOe:N0}")</td>
<td class="center">@($"{branchKmw:N1}")</td>
<td class="number">@($"{branchDeliveries.Sum(p => p.Weight):N0}")</td>
</tr>
}
}
}
<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.LsNr).Count():N0}") (@($"{Model.Deliveries.Count():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>
</main>
+13
View File
@@ -0,0 +1,13 @@
h1 {
text-align: center;
font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}
+9 -227
View File
@@ -1,16 +1,6 @@
using Elwig.Helpers;
using Elwig.Models.Entities;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Layout;
using iText.Layout.Properties;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Elwig.Documents {
public class DeliveryNote : BusinessDocument {
@@ -19,9 +9,7 @@ namespace Elwig.Documents {
public Delivery Delivery;
public string? Text;
public int MemberDeliveredWeightRed;
public int MemberDeliveredWeightWhite;
public Dictionary<string, MemberBucket> MemberBuckets = [];
public Dictionary<string, MemberBucket> MemberBuckets;
// 0 - none
// 1 - GA only
@@ -29,225 +17,19 @@ namespace Elwig.Documents {
// 3 - full
public int DisplayStats = App.Client.ModeDeliveryNoteStats;
public DeliveryNote(Delivery d) :
base($"{Name} Nr. {d.LsNr}", d.Member, DateOnly.FromDateTime(d.ModifiedAt)) {
public DeliveryNote(Delivery d, AppDbContext? ctx = null) : base($"{Name} Nr. {d.LsNr}", d.Member) {
UseBillingAddress = true;
ShowDateAndLocation = true;
Delivery = d;
Aside = Aside.Replace("</table>", "") +
$"<thead><tr><th colspan='2'>Lieferung</th></tr></thead><tbody>" +
$"<tr><th>LS-Nr.:</th><td>{d.LsNr}</td></tr>" +
$"<tr><th>Datum/Zeit:</th><td>{d.Date:dd.MM.yyyy} / {d.Time:HH:mm}</td></tr>" +
$"<tr><th>Zweigstelle:</th><td>{d.Branch.Name}</td></tr>" +
$"</tbody></table>";
Text = App.Client.TextDeliveryNote;
DocumentId = d.LsNr;
}
public static async Task<DeliveryNote> Initialize(int year, int did) {
using var ctx = new AppDbContext();
await ctx.WineOrigins.LoadAsync();
var d = await ctx.Deliveries
.Where(d => d.Year == year && d.DId == did)
.Include(d => d.Parts).ThenInclude(p => p.PartModifiers)
.SingleAsync();
return new DeliveryNote(d);
}
public static async Task<DeliveryNote> Initialize(string lsnr) {
using var ctx = new AppDbContext();
await ctx.WineOrigins.LoadAsync();
var d = await ctx.Deliveries
.Where(d => d.LsNr == lsnr)
.Include(d => d.Parts).ThenInclude(p => p.PartModifiers)
.SingleAsync();
return new DeliveryNote(d);
}
protected override async Task LoadData(AppDbContext ctx) {
await base.LoadData(ctx);
var weights = await ctx.DeliveryParts
.Where(d => d.Year == Delivery.Year && d.Delivery.MgNr == Member.MgNr)
.GroupBy(p => p.Variety.Type)
.ToDictionaryAsync(g => g.Key, g => g.Sum(p => p.Weight));
MemberDeliveredWeightRed = weights.GetValueOrDefault("R", 0);
MemberDeliveredWeightWhite = weights.GetValueOrDefault("W", 0);
MemberShares = await ctx.GetMemberHistory(Delivery.Year, Member.MgNr);
MemberBuckets = await ctx.GetMemberBuckets(Delivery.Year, Member.MgNr) ?? [];
}
protected override void BeforeRenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.BeforeRenderBody(doc, pdf);
Aside?.AddCell(NewAsideCell("Lieferung", 26))
.AddCell(NewAsideCell("Lieferschein-Nr.:", 12, isName: true)).AddCell(NewAsideCell(Delivery.LsNr, 14))
.AddCell(NewAsideCell("Datum/Zeit:", 12, isName: true)).AddCell(NewAsideCell($"{Delivery.Date:dd.MM.yyyy} / {Delivery.Time:HH:mm}", 14))
.AddCell(NewAsideCell("Zweigstelle:", 12, isName: true)).AddCell(NewAsideCell(Delivery.Branch.Name, 14));
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
doc.Add(NewDeliveryTable());
if (Delivery.Comment != null) {
doc.Add(new KernedParagraph($"Anmerkung zur Lieferung: {Delivery.Comment}", 10).SetMarginsMM(5, 0, 0, 0));
}
if (DisplayStats > 0) {
doc.Add(NewBucketTable(Delivery.Season, MemberBuckets, MemberDeliveredWeightRed, MemberDeliveredWeightWhite, isTiny: true,
filter: DisplayStats > 2 ? null : DisplayStats == 1 ? [] : Delivery.Parts.Select(p => p.SortId).Distinct().ToList())
.SetKeepTogether(true)
.SetMarginsMM(5, 0, 0, 0));
}
var sig = new Div().SetWidth(165 * PtInMM);
if (Text != null)
sig.Add(new KernedParagraph(Regex.Replace(Text, @"\s+", " "), 10).SetTextAlignment(TextAlignment.JUSTIFIED).SetSpacingRatio(1));
sig.Add(new Table(ColsMM(15, 50, 35, 50, 15)).SetMarginsMM(18, 0, 2, 0)
.AddCell(NewTd())
.AddCell(NewTd("Genossenschaft", center: true, borderTop: true).SetPaddingTopMM(1))
.AddCell(NewTd())
.AddCell(NewTd("Mitglied", center: true, borderTop: true).SetPaddingTopMM(1))
.AddCell(NewTd()));
var renderer = sig.CreateRendererSubTree();
renderer.SetParent(doc.GetRenderer());
var layoutResult = renderer.Layout(new LayoutContext(new LayoutArea(1, pdf.GetDefaultPageSize())));
float sigHeight = layoutResult.GetOccupiedArea().GetBBox().GetHeight();
doc.Add(new Div().SetWidth(165 * PtInMM).SetHeight(sigHeight));
var size = pdf.GetDefaultPageSize();
doc.Add(sig.SetFixedPosition(
size.GetLeft() + doc.GetLeftMargin(),
size.GetBottom() + doc.GetBottomMargin(),
size.GetWidth() - doc.GetLeftMargin() - doc.GetRightMargin())); ;
}
protected Cell NewDeliveryMainTd(string? text, int rowspan = 1, int colspan = 1, bool center = false, bool right = false) {
return NewTd(text, 12, rowspan: rowspan, colspan: colspan, bold: true, center: center, right: right)
.SetPaddingTopMM(2);
}
protected Table NewDeliveryTable() {
var tbl = new Table(ColsMM(10, 21, 25, 20, 20, 30, 12.5, 12.5, 14), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorder(Border.NO_BORDER).SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Pos.", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Sorte", colspan: 2, rowspan: 2, left: true))
.AddHeaderCell(NewTh("Attribut", colspan: 2, rowspan: 2, left: true))
.AddHeaderCell(NewTh("Qualitätsstufe", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Gradation", colspan: 2))
.AddHeaderCell(NewTh("Menge").SetPaddingRight(0))
.AddHeaderCell(NewTh("[°Oe]", 8))
.AddHeaderCell(NewTh("[°KMW]", 8))
.AddHeaderCell(NewTh("[kg]", 8).SetPaddingRight(0));
foreach (var part in Delivery.Parts.OrderBy(p => p.DPNr)) {
var sub = new Table(ColsMM(10, 21, 25, 20, 20, 30, 12.5, 12.5, 14), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetKeepTogether(true);
int rowspan = 1 + (part.Cultivation != null ? 1 : 0) + 1 + part.Modifiers.Count() + 1 + (part.Comment != null ? 1 : 0) + (part.Temperature != null || part.Acid != null ? 1 : 0);
sub.AddCell(NewDeliveryMainTd($"{part.DPNr:N0}", center: true))
.AddCell(NewDeliveryMainTd(part.Variety.Name, colspan: 2))
.AddCell(NewDeliveryMainTd(part.Attribute?.Name, colspan: 2))
.AddCell(NewDeliveryMainTd(part.Quality.Name))
.AddCell(NewDeliveryMainTd($"{part.Oe:N0}", center: true))
.AddCell(NewDeliveryMainTd($"{part.Kmw:N1}", center: true))
.AddCell(NewDeliveryMainTd($"{part.Weight:N0}", right: true).SetPaddingRight(0));
if (part.Cultivation != null) {
var cult = new KernedParagraph(8);
cult.Add(Italic("Bewirtschaftung:")).Add(Normal(" " + part.Cultivation.Name + (part.Cultivation.Description != null ? $" ({part.Cultivation.Description})" : "")));
if (part.Cultivation.IsOrganic) {
cult.Add(" \u2013 ").Add(Italic("Kontrollstelle:")).Add(Normal($" {Member.OrganicAuthorityCode ?? "?"}"));
}
sub.AddCell(NewTd())
.AddCell(NewTd(cult, colspan: 5))
.AddCell(NewTd(colspan: 3));
}
sub.AddCell(NewTd())
.AddCell(NewTd(new KernedParagraph(8).Add(Italic("Herkunft:")).Add(Normal(" " + part.OriginString.Replace("\n ", "\n\u00a0"))), colspan: 5))
.AddCell(NewTd(colspan: 3));
if (part.Modifiers.Any()) {
sub.AddCell(NewTd())
.AddCell(NewTd("Zu-/Abschläge:", 8, italic: true));
int i = 0, last = part.Modifiers.Count() - 1;
foreach (var mod in part.Modifiers) {
if (i > 0) sub.AddCell(NewCell(colspan: 2));
sub.AddCell(NewTd(mod.Name, 8, bold: true, colspan: 3).SetPaddingsMM(i == 0 ? 0.5f : 0, 1, i == last ? 0.5f : 0, 1))
.AddCell(NewTd(mod.PublicValueStr, 8).SetPaddingsMM(i == 0 ? 0.5f : 0, 1, i == last ? 0.5f : 0, 1))
.AddCell(NewTd(colspan: 3));
i++;
}
}
var weighing = new KernedParagraph(8);
if (part.IsManualWeighing) {
weighing.Add(Italic("Handwiegung " + (part.IsNetWeight ? "(gerebelt gewogen)" : "(nicht gerebelt gewogen)")));
if (part.WeighingReason != null) {
weighing.Add(Normal(", "))
.Add(Italic("Begründung:"))
.Add(Normal(" " + part.WeighingReason));
}
} else {
var info = part.WeighingInfo;
weighing.Add(Italic("Waage/Terminal:"))
.Add(Normal(" " + (info.ScaleId ?? "?") + "/" + (part.ScaleTerminalId ?? "?") + ", "))
.Add(Italic("ID:"))
.Add(Normal(" " + (info.Id ?? "?")));
if (info.Date != null || info.Time != null)
weighing.Add(Normal(" \u2013 "));
if (info.Time != null)
weighing.Add(Normal($"{info.Time:HH:mm}"));
if (info.Date != null)
weighing.Add(Normal($", {info.Date:dd.MM.yyyy}"));
if (info.Gross != null && info.Tare != null && info.Net != null) {
weighing.Add("\n")
.Add(Italic("Brutto:"))
.Add(Normal($" {info.Gross:N0} kg \u2013 "))
.Add(Italic("Tara:"))
.Add(Normal($" {info.Tare:N0} kg "));
if (info.TareCorrection != null && info.TareCorrection != 0)
weighing.Add(Bold($"(Korr.: {Utils.GetSign(info.TareCorrection.Value)}{Math.Abs(info.TareCorrection.Value)} kg)")).Add(" ");
weighing
.Add(Normal("\u2013 "))
.Add(Italic("Netto:"))
.Add(Normal($" {info.Net:N0} kg "));
if (info.TareCorrection != null && info.TareCorrection != 0)
weighing.Add(Normal($"({Utils.GetSign(-info.TareCorrection.Value)}{Math.Abs(info.TareCorrection.Value)} kg) "));
weighing
.Add(Normal("\u2013 "))
.Add(Italic(part.IsNetWeight ? "gerebelt gewogen" : "nicht gerebelt gewogen"));
} else {
weighing.Add(" ")
.Add(Italic(part.IsNetWeight ? "(gerebelt gewogen)" : "(nicht gerebelt gewogen)"));
}
}
sub.AddCell(NewCell())
.AddCell(NewCell(weighing, colspan: 5))
.AddCell(NewCell(colspan: 3));
if (part.Comment != null) {
sub.AddCell(NewCell())
.AddCell(NewCell(new KernedParagraph(8).Add(Italic("Anmerkung:")).Add(Normal(" " + part.Comment)), colspan: 5))
.AddCell(NewCell(colspan: 3));
}
if (part.Temperature != null || part.Acid != null) {
var p = new KernedParagraph(8);
if (part.Temperature != null)
p.Add(Italic("Temperatur:")).Add(Normal($" {part.Temperature:N1} °C"));
if (part.Temperature != null && part.Acid != null)
p.Add(Normal(", "));
if (part.Acid != null)
p.Add(Italic("Säure:")).Add(Normal($" {part.Acid:N1} g/l"));
sub.AddCell(NewCell())
.AddCell(NewCell(p, colspan: 5))
.AddCell(NewCell(colspan: 3));
}
tbl.AddCell(new Cell(1, 9).SetPadding(0).SetBorder(Border.NO_BORDER).Add(sub));
}
if (Delivery.Parts.Count > 1) {
tbl.AddCell(NewTd("Gesamt:", 12, bold: true, borderTop: true, colspan: 6).SetPaddingsMM(1, 1, 1, 0))
.AddCell(NewTd($"{Delivery.Oe:N0}", 12, bold: true, center: true, borderTop: true).SetPaddingsMM(1, 1, 1, 1))
.AddCell(NewTd($"{Delivery.Kmw:N1}", 12, bold: true, center: true, borderTop: true).SetPaddingsMM(1, 1, 1, 1))
.AddCell(NewTd($"{Delivery.Weight:N0}", 12, bold: true, right: true, borderTop: true).SetPaddingsMM(1, 0, 1, 1));
}
return tbl;
MemberBuckets = ctx?.GetMemberBuckets(d.Year, d.Member.MgNr).GetAwaiter().GetResult() ?? [];
}
}
}
+115
View File
@@ -0,0 +1,115 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.DeliveryNote>
@model Elwig.Documents.DeliveryNote
@{ Layout = "BusinessDocument"; }
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\DeliveryNote.css" />
<main>
<h1>@Model.Title</h1>
<table class="delivery large">
<colgroup>
<col style="width: 10.00mm;"/>
<col style="width: 21.00mm;"/>
<col style="width: 25.00mm;"/>
<col style="width: 19.50mm;"/>
<col style="width: 19.50mm;"/>
<col style="width: 30.00mm;"/>
<col style="width: 12.50mm;"/>
<col style="width: 12.50mm;"/>
<col style="width: 15.00mm;"/>
</colgroup>
<thead>
<tr>
<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>
<th colspan="2">Gradation</th>
<th>Menge</th>
</tr>
<tr>
<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 class="center">@part.DPNr</td>
<td colspan="2">@part.Variety.Name</td>
<td colspan="2">@part.Attribute?.Name</td>
<td>@part.Quality.Name</td>
<td class="center">@($"{part.Oe:N0}")</td>
<td class="center">@($"{part.Kmw:N1}")</td>
<td class="number">@($"{part.Weight:N0}")</td>
</tr>
@if (part.Cultivation != null) {
<tr><td></td><td><i>Bewirtschaftung:</i></td><td colspan="4"><b>
@part.Cultivation.Name
@if(part.Cultivation.Description != null) {
@("(")@part.Cultivation.Description@(")")
}
</b></td></tr>
}
<tr><td></td><td colspan="5" style="white-space: pre;"><i>Herkunft:</i> @part.OriginString</td></tr>
@if (part.Modifiers.Count() > 0) {
var first = true;
foreach (var mod in part.Modifiers) {
<tr class="tight @(first ? "first" : "")"><td></td><td>@Raw(first ? "<i>Zu-/Abschläge:</i>" : "")</td><td colspan="3"><b>@mod.Name</b></td><td style="white-space: pre;">@mod.ValueStr</td></tr>
first = false;
}
}
<tr><td></td><td colspan="5">
@if (part.IsManualWeighing) {
<i>Handwiegung @(part.IsNetWeight ? " (gerebelt gewogen)" : " (nicht gerebelt gewogen)")</i>@Raw(part.WeighingReason != null ? ", <i>Begründung:</i> " : "") @part.WeighingReason
} else {
var info = part.WeighingInfo;
<i>Waage:</i> @(part.ScaleId ?? "?")@(", ") <i>ID:</i> @(info.Id ?? "?")
@(info.Date != null || info.Time != null ? " – " : "")@(info.Time != null ? $"{info.Time:HH:mm}" : "")@(info.Date != null ? $", {info.Date:dd.MM.yyyy}" : "")
@if (info.Gross != null && info.Tare != null && info.Net != null) {
<br/><i>Brutto:</i> @($"{info.Gross:N0} kg")@(" – ") <i>Tara:</i> @($"{info.Tare:N0} kg")@(" – ") <i>Netto:</i> @($"{info.Net:N0} kg")@(" – ")@Raw(part.IsNetWeight ? "<i>gerebelt gewogen</i>" : "<i>nicht gerebelt gewogen</i>")
} else {
@Raw($" <i>({(part.IsNetWeight ? "gerebelt gewogen" : "nicht gerebelt gewogen")})</i>")
}
}
</td></tr>
@if (part.Comment != null) {
<tr><td></td><td colspan="5"><i>Anmerkung:</i> @part.Comment</td></tr>
}
@if (part.Temperature != null || part.Acid != null) {
<tr><td></td><td colspan="5">@Raw(part.Temperature != null ? $"<i>Temperatur:</i> {part.Temperature:N1} °C" : "")@(part.Temperature != null && part.Acid != null ? ", " : "")@Raw(part.Acid != null ? $"<i>Säure:</i> {part.Acid:N1} g/l" : "")</td></tr>
}
}
@if (Model.Delivery.Parts.Count() > 1) {
<tr class="main sum bold">
<td colspan="6">Gesamt:</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>
</table>
@if (Model.Delivery.Comment != null) {
<p class="comment">Amerkung zur Lieferung: @Model.Delivery.Comment</p>
}
@if (Model.DisplayStats > 0) {
@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++) {
<div class="text @(i == 0 ? "hidden" : "bottom")">
@if (Model.Text != null) {
<p class="comment">@Model.Text</p>
}
<div class="signatures">
<div>Genossenschaft</div>
<div>Mitglied</div>
</div>
</div>
}
+46
View File
@@ -0,0 +1,46 @@
main h1 {
margin-bottom: 1.5em !important;
}
table.delivery {
margin-bottom: 5mm;
}
table.delivery tr:not(.main) {
break-before: avoid;
}
table.delivery th.main {
text-align: left;
}
table.delivery tr.main td {
font-weight: bold;
padding-top: 2mm;
}
table.delivery tbody tr:not(.main) td {
font-size: 8pt;
}
table.delivery tr.tight:not(.first) td {
padding-top: 0;
padding-bottom: 0;
}
table.delivery tr.tight.first td {
padding-bottom: 0;
}
table.delivery tr.tight:has(+ tr:not(.tight)) td {
padding-bottom: 0.5mm !important;
}
table.delivery tr.sum td {
padding-top: 1mm;
}
table.delivery-note-stats {
break-after: avoid;
}
+76
View File
@@ -0,0 +1,76 @@
.m1, .m2, .m3 {
height: 0;
width: 10mm;
position: fixed;
left: -25mm;
border-top: var(--border-thickness) solid black;
}
.m1.r, .m2.r, .m3.r {
left: initial;
right: -20mm;
}
.m1 {top: 80mm;}
.m2 {top: 123.5mm;}
.m3 {top: 185mm;}
.page-break {
break-before: page;
}
hr.page-break {
display: none;
}
@page {
size: A4;
margin: 25mm 20mm 35mm 25mm;
@bottom-center {
content: element(page-footer);
}
}
@media screen {
body, header, .footer-wrapper {
width: 210mm;
}
header, .address-wrapper, aside, main {
border: 1px solid lightgray;
}
.m1, .m2, .m3 {
display: none;
}
header {
top: 0;
}
.spacing {
height: 45mm;
}
.main-wrapper {
margin: 0 20mm 40mm 25mm;
}
.footer-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
}
}
@media print {
.page::after {
content: "Seite " counter(page) " von " counter(pages) !important;
}
a {
text-decoration: inherit;
color: inherit;
}
}
+178
View File
@@ -0,0 +1,178 @@
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: var(--border-thickness) 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 .tiny {
font-size: 6pt;
}
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: var(--border-thickness) solid black;
}
main table tr.header {
border: var(--border-thickness) solid black;
background-color: #E0E0E0;
}
main table tr.header th {
font-weight: bold;
font-style: normal;
font-size: 16pt;
padding: 1mm 2mm;
}
main table tr.spacing td,
main table tr.spacing th {
height: 5mm;
}
main table tr.spacing ~ tr.header {
break-before: avoid;
}
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: var(--border-thickness) 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 .tborder {border-top: var(--border-thickness) solid black;}
main table .lborder {border-left: var(--border-thickness) solid black;}
main table .rborder {border-right: var(--border-thickness) solid black;}
main table .fleft {
float: left;
}
main tbody.sum tr:last-child {
border-bottom: var(--border-thickness) solid black;
}
+94 -418
View File
@@ -1,63 +1,53 @@
using Elwig.Helpers;
using Elwig.Helpers.Printing;
using iText.IO.Font;
using iText.Kernel.Font;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
using iText.Kernel.Pdf.Event;
using iText.Kernel.Pdf.Xobject;
using iText.Kernel.Utils;
using iText.Layout;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using MimeKit;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.IO;
using Elwig.Helpers;
using System.Collections.Generic;
using System.Linq;
using Elwig.Helpers.Printing;
using MimeKit;
namespace Elwig.Documents {
public class Document : IDisposable {
public abstract partial class Document : IDisposable {
public static string Name => "Dokument";
public const float PtInMM = 2.8346456693f;
public const float BorderThickness = 0.5f;
protected PdfFont NF = null!;
protected PdfFont BF = null!;
protected PdfFont IF = null!;
protected PdfFont BI = null!;
protected PdfFont SF = null!;
//protected readonly PdfFont NF = PdfFontFactory.CreateFont(StandardFonts.TIMES_ROMAN);
//protected readonly PdfFont BF = PdfFontFactory.CreateFont(StandardFonts.TIMES_BOLD);
//protected readonly PdfFont IF = PdfFontFactory.CreateFont(StandardFonts.TIMES_ITALIC);
//protected readonly PdfFont BI = PdfFontFactory.CreateFont(StandardFonts.TIMES_BOLDITALIC);
protected static readonly double GenerationProportion = 0.125;
protected TempFile? _pdfFile = null;
protected string? _pdfPath;
protected string? PdfPath => _pdfPath ?? _pdfFile?.FilePath;
public int? TotalPages { get; private set; }
public int? Pages => TotalPages / (IsDoublePaged ? 2 : 1);
public int? Pages => TotalPages / (DoublePaged ? 2 : 1);
public bool ShowFoldMarks = App.Config.Debug;
public bool IsDoublePaged = false;
public bool IsPreview = false;
private iText.Layout.Document? _doc;
public bool DoublePaged = false;
public string DocumentsPath;
public int CurrentNextSeason;
public string? DocumentId;
public string Title;
public string Author;
public string Header;
public string Footer;
public DateOnly Date;
public Document(string title) {
var c = App.Client;
DocumentsPath = App.DocumentsPath;
CurrentNextSeason = Utils.CurrentNextSeason;
Title = title;
Author = App.Client.NameFull;
Date = DateOnly.FromDateTime(DateTime.Today);
Author = c.NameFull;
Header = $"<div class='name'>{c.Name}</div><div class='suffix'>{c.NameSuffix}</div><div class='type'>{c.NameTypeFull}</div>";
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 != 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 = DateOnly.FromDateTime(Utils.Today);
}
~Document() {
@@ -75,170 +65,83 @@ namespace Elwig.Documents {
}
public static Document FromPdf(string path) {
return new RawPdfDocument(path);
return new PdfDocument(path);
}
private class RawPdfDocument : Document {
public RawPdfDocument(string pdfPath) :
base(Path.GetFileNameWithoutExtension(pdfPath)) {
_pdfPath = pdfPath;
private async Task<string> Render() {
string name;
if (this is BusinessLetter) {
name = "BusinessLetter";
} else if (this is DeliveryNote) {
name = "DeliveryNote";
} else if (this is CreditNote) {
name = "CreditNote";
} else if (this is DeliveryJournal) {
name = "DeliveryJournal";
} else if (this is DeliveryDepreciationList) {
name = "DeliveryDepreciationList";
} else if (this is Letterhead) {
name = "Letterhead";
} else if (this is DeliveryConfirmation) {
name = "DeliveryConfirmation";
} else if (this is MemberDataSheet) {
name = "MemberDataSheet";
} else if (this is MemberList) {
name = "MemberList";
} else if (this is WineQualityStatistics) {
name = "WineQualityStatistics";
} else if (this is PaymentVariantSummary) {
name = "PaymentVariantSummary";
} else if (this is DeliveryAncmtList) {
name = "DeliveryAncmtList";
} else {
throw new InvalidOperationException("Invalid document object");
}
return await Render(name);
}
public int Render(string path) {
using (var writer = new PdfWriter(path)) {
Render(writer);
}
using var reader = new PdfReader(path);
using var pdf = new PdfDocument(reader);
return pdf.GetNumberOfPages();
private async Task<string> Render(string name) {
return await Html.CompileRenderAsync(name, this); ;
}
private void Render(PdfWriter writer) {
NF = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\times.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
BF = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\timesbd.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
IF = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\timesi.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
BI = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\timesbi.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
SF = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\seguisym.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
NF.SetSubset(true);
BF.SetSubset(true);
IF.SetSubset(true);
BI.SetSubset(true);
SF.SetSubset(true);
writer.SetCompressionLevel(CompressionConstants.BEST_COMPRESSION);
writer.SetSmartMode(true);
using var pdf = new PdfDocument(writer);
pdf.GetDocumentInfo()
.SetTitle(Title)
.SetAuthor(Author)
.SetCreator($"Elwig {App.Version}");
var handler = new EventHandler(this);
pdf.AddEventHandler(PdfDocumentEvent.START_PAGE, handler);
pdf.AddEventHandler(PdfDocumentEvent.END_PAGE, handler);
pdf.AddEventHandler(PdfDocumentEvent.START_DOCUMENT_CLOSING, handler);
_doc = new iText.Layout.Document(pdf, iText.Kernel.Geom.PageSize.A4);
try {
_doc.SetFont(NF).SetFontSize(12);
BeforeRenderBody(_doc, pdf);
RenderBody(_doc, pdf);
} finally {
_doc.Close();
}
}
protected virtual async Task LoadData(AppDbContext ctx) { }
protected virtual void BeforeRenderBody(iText.Layout.Document doc, PdfDocument pdf) { }
protected virtual void RenderBody(iText.Layout.Document doc, PdfDocument pdf) { }
protected virtual Paragraph GetFooter() {
return new KernedParagraph(App.Client.NameFull, 10);
}
public async Task Generate(AppDbContext ctx, CancellationToken? cancelToken = null, IProgress<double>? progress = null) {
public async Task Generate(IProgress<double>? progress = null) {
if (_pdfFile != null)
return;
progress?.Report(0.0);
if (this is RawPdfDocument) {
if (this is PdfDocument) {
// nothing to do
} else if (this is MergedDocument m) {
using var tmpPdf = new TempFile("pdf");
var pdf = new TempFile("pdf");
try {
var pageNums = new List<int>();
using var writer = new PdfWriter(pdf.FilePath);
using var mergedPdf = new PdfDocument(writer);
var merger = new PdfMerger(mergedPdf);
(PdfPage Page, int InsertIndex, int DocIndex)? letterhead = null;
int p = mergedPdf.GetNumberOfPages();
for (int i = 0; i < m.Documents.Count; i++) {
if (cancelToken?.IsCancellationRequested ?? false)
throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!");
var doc = m.Documents[i];
int p0 = p;
if (letterhead != null && doc is Letterhead) {
if (p0 <= letterhead.Value.InsertIndex) {
mergedPdf.AddPage(letterhead.Value.Page);
mergedPdf.AddNewPage();
} else {
mergedPdf.AddPage(letterhead.Value.InsertIndex + 1, letterhead.Value.Page);
mergedPdf.AddNewPage(letterhead.Value.InsertIndex + 2);
}
pageNums[letterhead.Value.DocIndex] = 1;
letterhead = null;
p += 2;
}
if (doc is RawPdfDocument) {
using var reader = new PdfReader(doc.PdfPath);
using var src = new PdfDocument(reader);
merger.Merge(src, 1, src.GetNumberOfPages());
p += src.GetNumberOfPages();
} else {
await doc.LoadData(ctx);
int pageNum = doc.Render(tmpPdf.FilePath);
if (IsDoublePaged && doc is Letterhead) {
using var reader = new PdfReader(tmpPdf.FilePath);
using var src = new PdfDocument(reader);
letterhead = (src.GetPage(1).CopyTo(mergedPdf), p0, i);
} else {
using var reader = new PdfReader(tmpPdf.FilePath);
using var src = new PdfDocument(reader);
merger.Merge(src, 1, pageNum);
p += pageNum;
}
}
int p1 = p;
pageNums.Add(p1 - p0);
if (IsDoublePaged && doc is not Letterhead && p % 2 != 0) {
if (letterhead != null) {
mergedPdf.AddPage(letterhead.Value.Page);
letterhead = null;
} else {
mergedPdf.AddNewPage();
}
p++;
}
progress?.Report(100.0 * (i + 1) / (m.Documents.Count + 1));
var tmpHtmls = new List<TempFile>();
var tmpFiles = new List<string>();
var n = m.Documents.Count();
int i = 0;
foreach (var doc in m.Documents) {
if (doc is PdfDocument) {
tmpFiles.Add(doc.PdfPath!);
continue;
}
if (letterhead != null) {
if (p <= letterhead.Value.InsertIndex) {
mergedPdf.AddPage(letterhead.Value.Page);
mergedPdf.AddNewPage();
} else {
mergedPdf.AddPage(letterhead.Value.InsertIndex + 1, letterhead.Value.Page);
mergedPdf.AddNewPage(letterhead.Value.InsertIndex + 2);
}
pageNums[letterhead.Value.DocIndex] = 1;
p += 2;
}
TotalPages = p;
} catch {
pdf.Dispose();
throw;
var tmpHtml = new TempFile("html");
await File.WriteAllTextAsync(tmpHtml.FilePath, await doc.Render(), Utils.UTF8);
tmpHtmls.Add(tmpHtml);
tmpFiles.Add((doc is Letterhead ? "#" : "") + tmpHtml.FileName);
i++;
progress?.Report(GenerationProportion * 100 * i / n);
}
progress?.Report(GenerationProportion * 100);
var pages = await Pdf.Convert(tmpFiles, pdf.FileName, DoublePaged, new Progress<double>(v => progress?.Report(GenerationProportion * 100 + v * (1 - GenerationProportion))));
TotalPages = pages.Pages;
foreach (var tmp in tmpHtmls) {
tmp.Dispose();
}
_pdfFile = pdf;
} else {
if (cancelToken?.IsCancellationRequested ?? false)
throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!");
var pdf = new TempFile("pdf");
try {
await LoadData(ctx);
TotalPages = Render(pdf.FilePath);
} catch {
pdf.Dispose();
throw;
using (var tmpHtml = new TempFile("html")) {
await File.WriteAllTextAsync(tmpHtml.FilePath, await Render(), Utils.UTF8);
progress?.Report(50.0);
var pages = await Pdf.Convert(tmpHtml.FilePath, pdf.FilePath, DoublePaged);
TotalPages = pages.Pages;
}
_pdfFile = pdf;
}
@@ -252,7 +155,7 @@ namespace Elwig.Documents {
public async Task Print(int copies = 1) {
if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet");
Pdf.Print(PdfPath, copies, IsDoublePaged);
await Pdf.Print(PdfPath, copies, DoublePaged);
}
public void Show() {
@@ -270,241 +173,14 @@ namespace Elwig.Documents {
};
}
private class MergedDocument : Document {
public List<Document> Documents;
public MergedDocument(IEnumerable<Document> docs) :
base("Mehrere Dokumente") {
Documents = [.. docs];
IsDoublePaged = docs.Any(x => x.IsDoublePaged);
}
private class MergedDocument(IEnumerable<Document> docs) : Document("Mehrere Dokumente") {
public IEnumerable<Document> Documents = docs;
}
protected static Cell NewCell(Paragraph? p = null, int rowspan = 1, int colspan = 1, bool overflow = false) {
var cell = new Cell(rowspan, colspan)
.SetBorder(Border.NO_BORDER)
.SetPaddingsMM(0.5f, 1, 0.5f, 1);
if (p != null) {
p.SetProperty(Property.NO_SOFT_WRAP_INLINE, true);
if (!overflow) p.SetProperty(Property.OVERFLOW_X, OverflowPropertyValue.HIDDEN);
cell.Add(p);
}
return cell;
}
protected Cell NewTh(string? text, float fontSize = 8, int rowspan = 1, int colspan = 1, bool left = false, bool rotated = false) {
var p = new KernedParagraph(text ?? "", fontSize);
if (rotated) p.SetRotationAngle(0.5 * Math.PI);
var cell = NewCell(p, rowspan: rowspan, colspan: colspan)
.SetTextAlignment(left ? TextAlignment.LEFT : TextAlignment.CENTER)
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetFont(IF);
if (rotated || !left) cell.SetPadding(0);
return cell;
}
protected Cell NewTd(string? text = null, float fontSize = 10, int rowspan = 1, int colspan = 1, bool center = false, bool right = false, bool bold = false, bool italic = false, bool borderTop = false, bool overflow = false) {
return NewTd(new KernedParagraph(text ?? "", fontSize), rowspan: rowspan, colspan: colspan, center: center, right: right, bold: bold, italic: italic, borderTop: borderTop, overflow: overflow);
}
protected Cell NewTd(KernedParagraph p, int rowspan = 1, int colspan = 1, bool center = false, bool right = false, bool bold = false, bool italic = false, bool borderTop = false, bool overflow = false) {
return NewCell(p, rowspan: rowspan, colspan: colspan, overflow: overflow)
.SetTextAlignment(center ? TextAlignment.CENTER : right ? TextAlignment.RIGHT : TextAlignment.LEFT)
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetBorderTop(borderTop ? new SolidBorder(BorderThickness) : Border.NO_BORDER)
.SetFont(bold ? (italic ? BI : BF) : (italic ? IF : NF));
}
public static float[] ColsMM(params double[] widths) {
return [.. widths.Select(w => (float)w * PtInMM)];
}
public Text Normal(string? text, float? fontSize = null) {
var t = new Text(text ?? "").SetFont(NF);
if (fontSize != null) t.SetFontSize(fontSize.Value);
return t;
}
public Text Bold(string? text, float? fontSize = null) {
var t = new Text(text ?? "").SetFont(BF);
if (fontSize != null) t.SetFontSize(fontSize.Value);
return t;
}
public Text Italic(string? text, float? fontSize = null) {
var t = new Text(text ?? "").SetFont(IF);
if (fontSize != null) t.SetFontSize(fontSize.Value);
return t;
}
public Text BoldItalic(string? text, float? fontSize = null) {
var t = new Text(text ?? "").SetFont(BI);
if (fontSize != null) t.SetFontSize(fontSize.Value);
return t;
}
private class EventHandler : AbstractPdfDocumentEventHandler {
private const float _fontSize = 10;
private const float _placeholderWidth = 50 * PtInMM;
private readonly Document _doc;
private readonly List<PdfFormXObject> _pageNumPlaceholders;
public int NumberOfPages { get; private set; }
public EventHandler(Document doc) {
_doc = doc;
_pageNumPlaceholders = [];
}
protected override void OnAcceptedEvent(AbstractPdfDocumentEvent evt) {
if (evt.GetType() == PdfDocumentEvent.START_PAGE) {
OnPageStart((PdfDocumentEvent)evt);
} else if (evt.GetType() == PdfDocumentEvent.END_PAGE) {
OnPageEnd((PdfDocumentEvent)evt);
} else if (evt.GetType() == PdfDocumentEvent.START_DOCUMENT_CLOSING) {
OnDocumentClose((PdfDocumentEvent)evt);
}
}
private void OnPageStart(PdfDocumentEvent evt) {
var pdf = evt.GetDocument();
var page = evt.GetPage();
var pageNum = pdf.GetPageNumber(page);
if (pageNum == 1) {
// first page
if (_doc is BusinessDocument) {
_doc._doc?.SetMarginsMM(90, 20, 35, 25);
} else {
_doc._doc?.SetMarginsMM(20, 20, 35, 25);
}
} else if (_doc.IsDoublePaged && (pageNum % 2) == 0) {
// left page (= swapped)
_doc._doc?.SetMarginsMM(20, 25, 25, 20);
} else {
// right page
_doc._doc?.SetMarginsMM(20, 20, 25, 25);
}
}
private void OnPageEnd(PdfDocumentEvent evt) {
if (_doc is Letterhead) return;
var pdf = evt.GetDocument();
var page = evt.GetPage();
var pageNum = pdf.GetPageNumber(page);
var pageSize = page.GetPageSize();
float leftX1 = pageSize.GetLeft() + 25 * PtInMM;
float leftX2 = pageSize.GetLeft() + 20 * PtInMM;
float rightX1 = pageSize.GetRight() - 20 * PtInMM;
float rightX2 = pageSize.GetRight() - 25 * PtInMM;
float footerY = pageSize.GetBottom() + 25 * PtInMM;
float y1 = footerY + _fontSize;
float y2 = footerY - _fontSize;
var pdfCanvas = new PdfCanvas(page.NewContentStreamAfter(), page.GetResources(), pdf);
using var canvas = new Canvas(pdfCanvas, pageSize);
var placeholder = new PdfFormXObject(new iText.Kernel.Geom.Rectangle(0, 0, _placeholderWidth, _fontSize));
_pageNumPlaceholders.Add(placeholder);
var c = App.Client;
var dateP = new KernedParagraph($"{_doc.Date:dddd, d. MMMM yyyy}", _fontSize).SetFont(_doc.NF);
var centerP = new KernedParagraph(_doc.DocumentId ?? "", _fontSize).SetFont(_doc.IF);
var pageNumP = new KernedParagraph(_fontSize).Add(new Image(placeholder)).SetFont(_doc.NF);
if (pageNum == 1) {
// first page
canvas.ShowTextAligned(dateP, leftX1, y1, TextAlignment.LEFT, VerticalAlignment.BOTTOM);
canvas.ShowTextAligned(centerP, (leftX1 + rightX1) / 2, y1, TextAlignment.CENTER, VerticalAlignment.BOTTOM);
canvas.ShowTextAligned(pageNumP, rightX1, y1, TextAlignment.RIGHT, VerticalAlignment.BOTTOM);
var footer = _doc.GetFooter();
using var footerCanvas = new Canvas(page, pageSize);
footerCanvas.Add(new Table(1).AddCell(new Cell().Add(footer).SetBorder(Border.NO_BORDER).SetPaddingsMM(1, 0, 0, 0))
.SetFixedPositionMM(25, 0, 165).SetHeightMM(25)
.SetFont(_doc.NF).SetFontSize(10)
.SetTextAlignment(TextAlignment.CENTER)
.SetBorder(Border.NO_BORDER)
.SetBorderTop(new SolidBorder(BorderThickness)));
} else if (_doc.IsDoublePaged && (pageNum % 2 == 0)) {
// left page (= swapped)
canvas.ShowTextAligned(pageNumP, leftX2, y2, TextAlignment.LEFT, VerticalAlignment.TOP);
canvas.ShowTextAligned(centerP, (leftX2 + rightX2) / 2, y2, TextAlignment.CENTER, VerticalAlignment.TOP);
canvas.ShowTextAligned(dateP, rightX2, y2, TextAlignment.RIGHT, VerticalAlignment.TOP);
} else {
// right page
canvas.ShowTextAligned(dateP, leftX1, y2, TextAlignment.LEFT, VerticalAlignment.TOP);
canvas.ShowTextAligned(centerP, (leftX1 + rightX1) / 2, y2, TextAlignment.CENTER, VerticalAlignment.TOP);
canvas.ShowTextAligned(pageNumP, rightX1, y2, TextAlignment.RIGHT, VerticalAlignment.TOP);
}
if (_doc.ShowFoldMarks) {
var m1 = pageSize.GetTop() - 105 * PtInMM;
var m2 = pageSize.GetTop() - 148.5 * PtInMM;
var m3 = pageSize.GetTop() - 210 * PtInMM;
pdfCanvas.SetLineWidth(BorderThickness);
pdfCanvas.MoveTo(0, m1);
pdfCanvas.LineTo(10 * PtInMM, m1);
pdfCanvas.MoveTo(pageSize.GetRight(), m1);
pdfCanvas.LineTo(pageSize.GetRight() - 10 * PtInMM, m1);
pdfCanvas.MoveTo(0, m2);
pdfCanvas.LineTo(7 * PtInMM, m2);
pdfCanvas.MoveTo(pageSize.GetRight(), m2);
pdfCanvas.LineTo(pageSize.GetRight() - 7 * PtInMM, m2);
pdfCanvas.MoveTo(0, m3);
pdfCanvas.LineTo(10 * PtInMM, m3);
pdfCanvas.MoveTo(pageSize.GetRight(), m3);
pdfCanvas.LineTo(pageSize.GetRight() - 10 * PtInMM, m3);
pdfCanvas.ClosePathStroke();
}
if (NumberOfPages > 0) {
// FillPlaceholders() was already called
FillPlaceholder(pdf, pageNum);
}
}
private void OnDocumentClose(PdfDocumentEvent evt) {
var pdf = evt.GetDocument();
var page = evt.GetPage();
var pageNum = pdf.GetPageNumber(page);
// ...
FillPlaceholders(pdf);
}
private void FillPlaceholders(PdfDocument pdf) {
NumberOfPages = pdf.GetNumberOfPages();
for (int i = 0; i < _pageNumPlaceholders.Count; i++)
FillPlaceholder(pdf, i + 1);
}
private void FillPlaceholder(PdfDocument pdf, int pageNum) {
var placeholder = _pageNumPlaceholders[pageNum - 1];
using var canvas = new Canvas(placeholder, pdf);
if (_doc.IsDoublePaged && (pageNum % 2 == 0)) {
// left page (= swapped)
var p = new KernedParagraph(_fontSize).SetFont(_doc.NF);
if (_doc.IsPreview) p.Add(_doc.Bold("(vorläufig) "));
p.Add(_doc.Normal($"Seite {pageNum:N0} von {NumberOfPages:N0} "));
canvas.ShowTextAligned(p, 0, 0, TextAlignment.LEFT);
} else {
// right page
var p = new KernedParagraph(_fontSize).SetFont(_doc.NF)
.Add(_doc.Normal($"Seite {pageNum:N0} von {NumberOfPages:N0}"));
if (_doc.IsPreview) p.Add(_doc.Bold(" (vorläufig)"));
canvas.ShowTextAligned(p, _placeholderWidth, 0, TextAlignment.RIGHT);
}
private class PdfDocument : Document {
public PdfDocument(string pdfPath) :
base(Path.GetFileNameWithoutExtension(pdfPath)) {
_pdfPath = pdfPath;
}
}
}
+57
View File
@@ -0,0 +1,57 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.Document>
@model Elwig.Documents.Document
<!DOCTYPE html>
<html lang="de-AT">
<head>
<title>@Model.Title</title>
<meta name="author" value="@Model.Author"/>
<meta charset="UTF-8"/>
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\Document.css" />
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\Document.Page.css" />
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\Document.Table.css" />
@if (Model.DoublePaged) {
<style>
@@page :left {
margin: 25mm 25mm 35mm 20mm;
@@bottom-center {
content: element(page-footer-left);
}
}
</style>
}
</head>
<body>
@if (Model.ShowFoldMarks) {
<div class="m1"></div>
<div class="m2"></div>
<div class="m3"></div>
<div class="m1 r"></div>
<div class="m2 r"></div>
<div class="m3 r"></div>
}
<div class="footer-wrapper">
<div class="pre-footer">
<span class="date">@($"{Model.Date:dddd, d. MMMM yyyy}")</span>
<span class="doc-id">@Model.DocumentId</span>
<span class="page"></span>
</div>
<footer>@Raw(Model.Footer)</footer>
</div>
@if (Model.DoublePaged) {
<div class="footer-wrapper left">
<div class="pre-footer">
<span class="page"></span>
<span class="doc-id">@Model.DocumentId</span>
<span class="date">@($"{Model.Date:dddd, d. MMMM yyyy}")</span>
</div>
<footer>@Raw(Model.Footer)</footer>
</div>
}
<header>@Raw(Model.Header)</header>
<div class="spacing"></div>
<div class="main-wrapper">
@RenderBody()
</div>
</body>
</html>
+113
View File
@@ -0,0 +1,113 @@
:root {
font-family: "Times New Roman", serif;
line-height: 1;
--border-thickness: 0.5pt;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
table td,
table th {
padding: 0.5mm 1mm;
}
table th {
text-align: center;
}
header {
height: 45mm;
padding: 10mm 0 0 0;
position: absolute;
top: -25mm;
left: 0;
right: 0;
text-align: center;
overflow: hidden;
}
header .name {
font-size: 18pt;
margin-top: 8mm;
font-weight: bold;
}
header .suffix {
font-size: 14pt;
font-weight: bold;
}
header .type {
font-size: 12pt;
font-weight: normal;
}
.footer-wrapper {
position: running(page-footer);
width: 165mm;
/* for some reason the position without the following statement changes on the second page */
border: var(--border-thickness) solid #00000000;
}
.footer-wrapper.left {
position: running(page-footer-left);
}
.pre-footer {
margin: 1em 0;
font-size: 10pt;
}
.pre-footer > * {
display: inline-block;
width: 33%;
}
.pre-footer > *:first-child {
text-align: left;
}
.pre-footer > *:nth-child(2) {
text-align: center;
font-style: italic;
}
.pre-footer > *:last-child {
text-align: right;
float: right;
}
.pre-footer .page::after {
content: "Seite 1 von 1";
}
footer {
font-size: 10pt;
border-top: var(--border-thickness) solid black;
height: 25mm;
padding-top: 1mm;
text-align: center;
}
.hidden {
visibility: hidden;
}
hr {
border: none;
border-top: var(--border-thickness) solid black;
margin: 5mm 0;
}
-93
View File
@@ -1,93 +0,0 @@
using iText.Kernel.Geom;
using iText.Layout;
using iText.Layout.Element;
using iText.Layout.Properties;
namespace Elwig.Documents {
public static class Extensions {
public static T SetFixedPositionMM<T>(this BlockElement<T> element, float left, float top, float width, Rectangle pageSize) where T : IElement {
element.SetVerticalAlignment(VerticalAlignment.TOP);
return element.SetFixedPosition(left * Document.PtInMM, pageSize.GetTop() - top * Document.PtInMM, width * Document.PtInMM);
}
public static T SetFixedPositionMM<T>(this BlockElement<T> element, float left, float top, float width, float height, Rectangle pageSize) where T : IElement {
element.SetHeight(height * Document.PtInMM);
return element.SetFixedPosition(left * Document.PtInMM, pageSize.GetTop() - (top + height) * Document.PtInMM, width * Document.PtInMM);
}
public static T SetFixedPositionMM<T>(this ElementPropertyContainer<T> element, float left, float bottom, float width) where T : IPropertyContainer {
return element.SetFixedPosition(left * Document.PtInMM, bottom * Document.PtInMM, width * Document.PtInMM);
}
public static T SetHeightMM<T>(this BlockElement<T> element, float height) where T : IElement {
return element.SetHeight(height * Document.PtInMM);
}
public static T SetPaddingTopMM<T>(this BlockElement<T> element, float top) where T : IElement {
return element.SetPaddingTop(top * Document.PtInMM);
}
public static T SetPaddingRightMM<T>(this BlockElement<T> element, float right) where T : IElement {
return element.SetPaddingRight(right * Document.PtInMM);
}
public static T SetPaddingBottomMM<T>(this BlockElement<T> element, float bottom) where T : IElement {
return element.SetPaddingBottom(bottom * Document.PtInMM);
}
public static T SetPaddingLeftMM<T>(this BlockElement<T> element, float left) where T : IElement {
return element.SetPaddingLeft(left * Document.PtInMM);
}
public static T SetPaddingsMM<T>(this BlockElement<T> element, float top, float right, float bottom, float left) where T : IElement {
return element.SetPaddings(top * Document.PtInMM, right * Document.PtInMM, bottom * Document.PtInMM, left * Document.PtInMM);
}
public static T SetMarginTopMM<T>(this BlockElement<T> element, float top) where T : IElement {
return element.SetMarginTop(top * Document.PtInMM);
}
public static T SetMarginRightMM<T>(this BlockElement<T> element, float right) where T : IElement {
return element.SetMarginRight(right * Document.PtInMM);
}
public static T SetMarginBottomMM<T>(this BlockElement<T> element, float bottom) where T : IElement {
return element.SetMarginBottom(bottom * Document.PtInMM);
}
public static T SetMarginLeftMM<T>(this BlockElement<T> element, float left) where T : IElement {
return element.SetMarginLeft(left * Document.PtInMM);
}
public static T SetMarginsMM<T>(this BlockElement<T> element, float top, float right, float bottom, float left) where T : IElement {
return element.SetMargins(top * Document.PtInMM, right * Document.PtInMM, bottom * Document.PtInMM, left * Document.PtInMM);
}
public static void SetTopMarginMM(this iText.Layout.Document element, float top) {
element.SetTopMargin(top * Document.PtInMM);
}
public static void SetRightMarginMM(this iText.Layout.Document element, float right) {
element.SetRightMargin(right * Document.PtInMM);
}
public static void SetBottomMarginMM(this iText.Layout.Document element, float bottom) {
element.SetBottomMargin(bottom * Document.PtInMM);
}
public static void SetLeftMarginMM(this iText.Layout.Document element, float left) {
element.SetLeftMargin(left * Document.PtInMM);
}
public static void SetMarginsMM(this iText.Layout.Document element, float top, float right, float bottom, float left) {
element.SetMargins(top * Document.PtInMM, right * Document.PtInMM, bottom * Document.PtInMM, left * Document.PtInMM);
}
public static Table AddCells(this Table table, params Cell[] cells) {
foreach (var cell in cells)
table.AddCell(cell);
return table;
}
}
}
-82
View File
@@ -1,82 +0,0 @@
using iText.IO.Font;
using iText.IO.Font.Otf;
using iText.Kernel.Font;
using iText.Layout.Element;
using iText.Layout.Properties;
using iText.Layout.Renderer;
using System;
namespace Elwig.Documents {
public class KernedParagraph : Paragraph {
public KernedParagraph(float fontSize) :
base() {
SetFontKerning(FontKerning.YES);
SetFixedLeading(fontSize);
SetFontSize(fontSize);
}
public KernedParagraph(string text, float fontSize) :
base(text) {
SetFontKerning(FontKerning.YES);
SetFixedLeading(fontSize);
SetFontSize(fontSize);
}
public KernedParagraph(Text text, float fontSize) :
base(text) {
SetFontKerning(FontKerning.YES);
SetFixedLeading(fontSize);
SetFontSize(fontSize);
}
public override KernedParagraph Add(ILeafElement element) {
if (element is Text t) {
t.SetFontKerning(FontKerning.YES);
t.SetNextRenderer(new KerningTextRenderer(t));
}
base.Add(element);
return this;
}
public override Paragraph Add(IBlockElement element) {
base.Add(element);
return this;
}
public class KerningTextRenderer(Text textElement) : TextRenderer(textElement) {
public override IRenderer GetNextRenderer() {
return new KerningTextRenderer((Text)modelElement);
}
public override void ApplyOtf() {
PdfFont font;
try {
font = GetPropertyAsFont(Property.FONT);
} catch (InvalidCastException) {
return;
}
if (strToBeConverted != null) {
SetProcessedGlyphLineAndFont(TextPreprocessingUtil.ReplaceSpecialWhitespaceGlyphs(font.CreateGlyphLine(strToBeConverted), font), font);
}
if (otfFeaturesApplied || text.GetStart() >= text.GetEnd()) {
return;
}
if (GetProperty(Property.FONT_KERNING, (FontKerning?)FontKerning.NO) == FontKerning.YES) {
ApplyKerning(font.GetFontProgram(), text);
}
otfFeaturesApplied = true;
}
private static void ApplyKerning(FontProgram font, GlyphLine text) {
for (int i = 1; i < text.Size(); i++) {
var kerning = font.GetKerning(text.Get(i - 1), text.Get(i));
if (kerning != 0) {
text.Set(i - 1, new Glyph(text.Get(i - 1), 0, 0, kerning, 0, 0));
}
}
}
}
}
}
+2 -14
View File
@@ -1,21 +1,9 @@
using Elwig.Models.Entities;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
using iText.Layout;
namespace Elwig.Documents {
public class Letterhead : BusinessDocument {
public Letterhead(Member m) :
base($"Briefkopf {m.FullName}", m, null, includeSender: true) {
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
// do not render anything except this
var page = pdf.AddNewPage();
var pageSize = page.GetPageSize();
var pdfCanvas = new PdfCanvas(page.NewContentStreamBefore(), page.GetResources(), pdf);
using var canvas = new Canvas(pdfCanvas, pageSize);
RenderAddress(canvas, pageSize);
public Letterhead(Member m) : base($"Briefkopf {m.FullName}", m, true) {
Aside = "";
}
}
}
+9
View File
@@ -0,0 +1,9 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.Letterhead>
@model Elwig.Documents.Letterhead
@{ Layout = "BusinessDocument"; }
<style>
header, .footer-wrapper {
visibility: hidden;
}
</style>
+7 -350
View File
@@ -1,366 +1,23 @@
using Elwig.Helpers;
using Elwig.Models;
using Elwig.Models.Entities;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Elwig.Documents {
public class MemberDataSheet : BusinessDocument {
public new static string Name => "Stammdatenblatt";
public Season? Season;
public int MemberDeliveredWeightRed;
public int MemberDeliveredWeightWhite;
public List<MemberHistory> MemberHistory = [];
public Dictionary<string, MemberBucket> MemberBuckets = [];
public List<AreaCmt> ActiveAreaCommitments = [];
public Season Season;
public Dictionary<string, MemberBucket> MemberBuckets;
public IEnumerable<AreaCom> ActiveAreaCommitments;
public MemberDataSheet(Member m) :
base($"{Name} {m.AdministrativeName}", m, DateOnly.FromDateTime(m.ModifiedAt)) {
ShowDateAndLocation = true;
public MemberDataSheet(Member m, AppDbContext ctx) : base($"{Name} {m.AdministrativeName}", m) {
DocumentId = $"{Name} {m.MgNr}";
}
public static async Task<MemberDataSheet> Initialize(int mgnr) {
using var ctx = new AppDbContext();
return new MemberDataSheet(await ctx.FetchMembers(mgnr, includeContactInfo: true).SingleAsync());
}
protected override async Task LoadData(AppDbContext ctx) {
await base.LoadData(ctx);
Season = await ctx.FetchSeasons().FirstOrDefaultAsync() ?? throw new ArgumentException("Invalid season");
MemberHistory = await ctx.MemberHistory
.Where(h => h.FromMgNr == Member.MgNr || h.ToMgNr == Member.MgNr)
.OrderBy(h => h.DateNoticeString).ThenBy(h => h.HistNr)
.ToListAsync();
MemberBuckets = await ctx.GetMemberBuckets(Utils.CurrentYear, Member.MgNr);
ActiveAreaCommitments = await Member.ActiveAreaCommitments(ctx)
.Include(c => c.Contract).ThenInclude(c => c.Revisions)
.ToListAsync();
var weights = await ctx.Deliveries
.Where(d => d.Year == Season.Year && d.MgNr == Member.MgNr)
.SelectMany(d => d.Parts)
.GroupBy(p => p.Variety.Type)
.ToDictionaryAsync(g => g.Key, g => g.Sum(p => p.Weight));
MemberDeliveredWeightRed = weights.GetValueOrDefault("R", 0);
MemberDeliveredWeightWhite = weights.GetValueOrDefault("W", 0);
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
if (Season == null) throw new Exception("Call LoadData before RenderBody");
base.RenderBody(doc, pdf);
doc.Add(NewMemberData(Season).SetMarginBottomMM(5));
doc.Add(NewBucketTable(Season, MemberBuckets, MemberDeliveredWeightRed, MemberDeliveredWeightWhite, includeDelivery: false));
if (ActiveAreaCommitments.Count != 0) {
bool firstOnPage = false;
if (pdf.GetNumberOfPages() == 1) {
doc.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
firstOnPage = true;
}
doc.Add(new KernedParagraph(12).Add(Bold($"Flächenbindungen per {Date:dd.MM.yyyy}")).SetMargins(firstOnPage ? 0 : 24, 0, 6, 0));
doc.Add(NewAreaCmtTable(ActiveAreaCommitments));
}
if (App.Client.EnableMemberHistory && App.Client.ModeMemberDataSheetShares > 0) {
bool firstOnPage = false;
if (pdf.GetNumberOfPages() == 1) {
doc.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
firstOnPage = true;
}
doc.Add(new KernedParagraph(12).Add(Bold($"Verlauf der Geschäftsanteile per {Date:dd.MM.yyyy}")).SetMargins(firstOnPage ? 0 : 24, 0, 6, 0));
doc.Add(NewMemberBusinessSharesTable(MemberHistory));
}
}
protected Cell NewDataHdr(string title, int colspan) {
return NewTd(title, 10, colspan: colspan, bold: true, italic: true, center: true, borderTop: true)
.SetBackgroundColor(new DeviceRgb(0xe0, 0xe0, 0xe0));
}
protected Cell NewDataTh(string text, float fontSize = 10, int colspan = 1) {
return NewTd(text, fontSize, colspan: colspan, italic: true)
.SetPaddingRightMM(0);
}
protected Table NewMemberData(Season season) {
var tbl = new Table(ColsMM(30.0, 51.5, 20.0, 12.0, 18.0, 31.5))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetBorder(new SolidBorder(BorderThickness));
tbl.AddCell(NewDataHdr("Persönliche Daten", 6));
if (Member.IsJuridicalPerson) {
tbl
.AddCell(NewDataTh("Name", 8, colspan: 3))
.AddCell(NewDataTh("Zu Handen", 8, colspan: 3))
.AddCell(NewTd(Member.Name, 12, colspan: 3))
.AddCell(NewTd(Member.ForTheAttentionOf, 12, colspan: 3));
} else {
tbl
.AddCell(NewDataTh("Titel (vorangestellt)", 8))
.AddCell(NewDataTh("Vorname", 8))
.AddCell(NewDataTh("Nachname", 8, colspan: 3))
.AddCell(NewDataTh("Titel (nachgestellt)", 8))
.AddCell(NewTd(Member.Prefix, 12))
.AddCell(NewTd($"{Member.GivenName} {Member.MiddleName}", 12))
.AddCell(NewTd(Member.Name, 12, colspan: 3))
.AddCell(NewTd(Member.Suffix, 12));
}
tbl
.AddCell(NewDataTh("Mitglieds-Nr.:")).AddCell(NewTd($"{Member.MgNr}"))
.AddCell(NewDataTh(Member.IsJuridicalPerson ? "Gründungsjahr/-tag:" : "Geburtsjahr/-tag:", colspan: 2))
.AddCell(NewTd(string.Join('.', Member.Birthday?.Split('-')?.Reverse() ?? []), colspan: 2))
.AddCell(NewDataTh("Adresse:")).AddCell(NewTd(Member.Address, colspan: 5))
.AddCell(NewDataTh("PLZ/Ort:"))
.AddCell(NewTd($"{Member.PostalDest.AtPlz?.Plz} {Member.PostalDest.AtPlz?.Dest} ({Member.PostalDest.AtPlz?.Ort.Name})", colspan: 5))
.AddCell(NewDataHdr("Rechnungsadresse (optional)", colspan: 6))
.AddCell(NewDataTh("Name:")).AddCell(NewTd(Member.BillingAddress?.FullName, colspan: 5))
.AddCell(NewDataTh("Adresse:")).AddCell(NewTd(Member.BillingAddress?.Address, colspan: 5))
.AddCell(NewDataTh("PLZ/Ort:"))
.AddCell(NewTd(Member.BillingAddress != null ? $"{Member.BillingAddress.PostalDest.AtPlz?.Plz} {Member.BillingAddress.PostalDest.AtPlz?.Dest} ({Member.BillingAddress.PostalDest.AtPlz?.Ort.Name})" : "", colspan: 5));
tbl.AddCell(NewDataHdr("Kontaktdaten", colspan: 3))
.AddCell(NewDataHdr("Bankverbindung", colspan: 3).SetBorderLeft(new SolidBorder(BorderThickness)));
List<string?[]> subTbl1 = [
.. Member.EmailAddresses.Select(a => new[] { "E-Mail-Adresse", a.Address }),
.. Member.TelephoneNumbers.Select(n => new[] { Utils.PhoneNrTypeToString(n.Type), n.Number, n.Comment }),
["Tel.-Nr./E-Mail-Adr.", null],
];
List<string?[]> subTbl2 = [
["IBAN", Member.Iban != null ? Utils.FormatIban(Member.Iban) : null],
["BIC", Member.Bic],
];
for (int i = 0; i < Math.Max(subTbl1.Count, subTbl2.Count); i++) {
tbl.AddCell(NewDataTh(i < subTbl1.Count ? subTbl1[i][0] + ":" : ""));
if (i < subTbl1.Count && subTbl1[i].Length >= 3 && subTbl1[i][2] != null) {
tbl.AddCell(NewTd(subTbl1[i][1])).AddCell(NewTd($"({subTbl1[i][2]})"));
} else {
tbl.AddCell(NewTd(i < subTbl1.Count ? subTbl1[i][1] : "", colspan: 2));
}
tbl.AddCell(NewDataTh(i < subTbl2.Count ? subTbl2[i][0] + ":" : "").SetBorderLeft(new SolidBorder(BorderThickness)))
.AddCell(NewTd(i < subTbl2.Count ? subTbl2[i][1] : "", colspan: 2));
}
var shares = (Member.Shares != 0 || (Member.SharesRed == 0 && Member.SharesWhite == 0) ? $"{Member.Shares:N0}" : "") +
(Member.SharesRed != 0 || Member.SharesWhite != 0 ? (Member.Shares != 0 ? " / " : "") + $"{Member.SharesRed:N0} (rot) / {Member.SharesWhite:N0} (weiß)" : "") +
(Member.SharesDormant != 0 ? $" / {Member.SharesDormant:N0} (ruhend)" : "");
tbl.AddCell(NewDataHdr("Betrieb", colspan: 6))
.AddCell(NewDataTh("Betriebs-Nr.:")).AddCell(NewTd(Member.LfbisNr))
.AddCell(NewDataTh("UID:", colspan: 2)).AddCell(NewTd(Member.UstIdNr, colspan: 2))
.AddCell(NewDataTh("Stammgemeinde:")).AddCell(NewTd(Member.DefaultKg?.Name))
.AddCell(NewDataTh("Buchführend:", colspan: 2)).AddCell(NewTd(new KernedParagraph(Member.IsAccounting ? "Ja " : "Nein ", 10)
.Add(Normal($"({(Member.IsAccounting ? season.VatNormal : season.VatFlatrate) * 100:N0}% USt.)", 8)), colspan: 2))
.AddCell(NewDataTh("(Katastralgemeinde mit dem größten Anteil an Weinbauflächen)", 8, colspan: 2))
.AddCell(NewDataTh("Bio:", colspan: 2)).AddCell(NewTd(new KernedParagraph(Member.IsOrganic ? "Ja" : "Nein", 10)
.Add(Normal(Member.OrganicAuthorityCode != null || Member.OrganicOperatorId != null ? $" ({Member.OrganicAuthorityCode}{(Member.OrganicAuthorityCode != null && Member.OrganicOperatorId != null ? ", " : "")}{Member.OrganicOperatorId})" : "", 8)), colspan: 2))
.AddCell(NewDataHdr("Genossenschaft", colspan: 6))
.AddCell(NewDataTh("Status:")).AddCell(NewTd(new KernedParagraph(Member.IsActive ? "Aktiv " : "Nicht aktiv ", 10)
.Add(Normal("(" + (Member.ExitDate != null ? $"{Member.EntryDate:dd.MM.yyyy}\u2013{Member.ExitDate:dd.MM.yyyy}" : $"seit {Member.EntryDate:dd.MM.yyyy}") + ")", 8))))
.AddCell(NewDataTh("Geschäftsanteile:", colspan: 2)).AddCell(NewTd(shares, colspan: 2))
.AddCell(NewDataTh("Stamm-Zweigstelle:")).AddCell(NewTd(Member.Branch?.Name))
.AddCell(NewDataTh("Vollablieferer:", colspan: 2)).AddCell(NewTd(Member.IsFullDelivery ? "Ja" : "Nein", colspan: 2))
.AddCell(NewDataTh("Zusendungen per\u2026")).AddCell(NewTd(new KernedParagraph(10)
.Add(Italic("Post:")).Add(Normal(Member.ContactViaPost ? " Ja \u2013 " : " Nein \u2013 "))
.Add(Italic("E-Mail:")).Add(Normal(Member.ContactViaEmail ? " Ja" : " Nein"))))
.AddCell(NewDataTh("Funktionär:", colspan: 2)).AddCell(NewTd(Member.IsOfficer ? "Ja" : "Nein", colspan: 2));
return tbl;
}
protected Table NewAreaCmtTable(IEnumerable<AreaCmt> activeAreaCmts) {
var areaCmts = activeAreaCmts.GroupBy(a => a.AreaCmtType).Select(group => new {
Type = group.Key,
AreaCmts = group.OrderBy(c => c.Contract.Kg.AtKg.Name).ToList(),
Size = group.Sum(c => c.Area)
}).OrderByDescending(a => a.Size).ToList();
var tbl = new Table(ColsMM(40, 30, 35, 15, 25, 20), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetBorder(Border.NO_BORDER)
.SetFontSize(10);
tbl.AddHeaderCell(NewTh("Katastralgemeinde", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Ried", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Parzelle(n)", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Fläche"))
.AddHeaderCell(NewTh("Bewirt.", rowspan: 2))
.AddHeaderCell(NewTh("Laufzeit", rowspan: 2))
.AddHeaderCell(NewTh("[m²]"));
var lastContract = "";
foreach (var contractType in areaCmts) {
tbl.AddCell(NewCell(new KernedParagraph(10).Add(BoldItalic($"{contractType.Type.WineVar.Name} {(contractType.Type.WineAttr != null ? "(" + contractType.Type.WineAttr + ")" : "")}")), colspan: 3)
.SetBorderTop(contractType.Type.DisplayName != lastContract && lastContract != "" ? new SolidBorder(BorderThickness) : Border.NO_BORDER))
.AddCell(NewCell(new KernedParagraph(10).Add(Bold($"{contractType.Size:N0}")).SetTextAlignment(TextAlignment.RIGHT))
.SetBorderTop(contractType.Type.DisplayName != lastContract && lastContract != "" ? new SolidBorder(BorderThickness) : Border.NO_BORDER))
.AddCell(NewCell(colspan: 2)
.SetBorderTop(contractType.Type.DisplayName != lastContract && lastContract != "" ? new SolidBorder(BorderThickness) : Border.NO_BORDER));
foreach (var areaCmt in contractType.AreaCmts) {
var c = areaCmt.Contract;
tbl.AddCell(NewTd(new KernedParagraph(10).Add(Normal($"{c.Kg.AtKg.Name} ")).Add(Normal($"({c.Kg.AtKg.KgNr:00000})", 8))))
.AddCell(NewTd(c.Rd?.Name))
.AddCell(NewTd(Regex.Replace(areaCmt.GstNr.Replace(",", ", ").Replace("-", "\u2013"), @"\s+", " "), 10))
.AddCell(NewTd($"{areaCmt.Area:N0}", right: true))
.AddCell(NewTd(areaCmt.WineCult?.Name, center: true))
.AddCell(NewTd(c.YearTo == null ? (c.YearFrom == null ? "unbefristet" : $"ab {c.YearFrom}") : (c.YearFrom == null ? $"bis {c.YearTo}" : $"{c.YearFrom}–{c.YearTo}"), center: true));
lastContract = contractType.Type.DisplayName;
}
}
tbl.AddCell(NewTd("Gesamt:", 12, colspan: 2, bold: true, borderTop: true).SetPaddingsMM(1, 1, 1, 1));
tbl.AddCell(NewTd($"{activeAreaCmts.Sum(a => a.Area):N0}", 12, colspan: 2, right: true, bold: true, borderTop: true).SetPaddingsMM(1, 1, 1, 1));
tbl.AddCell(NewTd(colspan: 2, borderTop: true).SetPaddingsMM(1, 1, 1, 1));
return tbl;
}
protected Cell[] NewBusinessSharesCells(MemberHistoryPoint p, bool isDiff = true) {
if (App.Client.HasRedWhite) {
return [
NewTd(isDiff ? (p.SharesRed == 0 ? "" : p.SharesRed < 0 ? $"–{-p.SharesRed:N0}" : $"+{p.SharesRed:N0}") : $"{p.SharesRed:N0}",
right: true, bold: !isDiff, borderTop: !isDiff),
NewTd(isDiff ? (p.SharesWhite == 0 ? "" : p.SharesWhite < 0 ? $"–{-p.SharesWhite:N0}" : $"+{p.SharesWhite:N0}") : $"{p.SharesWhite:N0}",
right: true, bold: !isDiff, borderTop: !isDiff),
NewTd(isDiff ? (p.SharesDormant == 0 ? "" : p.SharesDormant < 0 ? $"–{-p.SharesDormant:N0}" : $"+{p.SharesDormant:N0}") : $"{p.SharesDormant:N0}",
right: true, bold: !isDiff, borderTop: !isDiff),
];
} else {
return [
NewTd(isDiff ? (p.Shares == 0 ? "" : p.Shares < 0 ? $"–{-p.Shares:N0}" : $"+{p.Shares:N0}") : $"{p.Shares:N0}",
right: true, bold: !isDiff, borderTop: !isDiff),
NewTd(isDiff ? (p.SharesDormant == 0 ? "" : p.SharesDormant < 0 ? $"–{-p.SharesDormant:N0}" : $"+{p.SharesDormant:N0}") : $"{p.SharesDormant:N0}",
right : true, bold: !isDiff, borderTop: !isDiff),
];
}
}
protected Table NewMemberBusinessSharesTable(IEnumerable<MemberHistory> history) {
var tbl = new Table(ColsMM(18, 93, App.Client.HasRedWhite ? 18 : 12, App.Client.HasRedWhite ? 12 : 18, 12, 12), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetBorder(Border.NO_BORDER)
.SetFontSize(10);
var tw = App.Client.HasRedWhite ? 1 : 2;
tbl.AddHeaderCell(NewTh("Meldung am", rowspan: 2))
.AddHeaderCell(NewTh("Text", rowspan: 2, colspan: tw, left: true))
.AddHeaderCell(NewTh("Wirksam ab", rowspan: 2))
.AddHeaderCell(NewTh("Geschäftsanteile", colspan: App.Client.HasRedWhite ? 3 : 2));
if (App.Client.HasRedWhite) {
tbl.AddHeaderCell(NewTh("rot"))
.AddHeaderCell(NewTh("weiß"))
.AddHeaderCell(NewTh("ruhend"));
} else {
tbl.AddHeaderCell(NewTh("normal"))
.AddHeaderCell(NewTh("ruhend"));
}
if (!history.Any() || Member.EntryDate <= history.First().DateNotice) {
tbl.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}"))
.AddCell(NewTd("Eintritt", colspan: tw, bold: true))
.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}"))
.AddCells(NewBusinessSharesCells(new()));
}
var recorded = history.Aggregate(new MemberHistoryPoint(), (s, h) => new(
s.Shares + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.Normal ? h.Shares : 0) - (h.ToMgNr == h.FromMgNr && h.ToType == BusinessShareType.Normal && h.FromType == BusinessShareType.Normal ? 0 : h.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.Normal ? h.Shares : 0),
s.SharesRed + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.Red ? h.Shares : 0) - (h.ToMgNr == h.FromMgNr && h.ToType == BusinessShareType.Red && h.FromType == BusinessShareType.Red ? 0 : h.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.Red ? h.Shares : 0),
s.SharesWhite + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.White ? h.Shares : 0) - (h.ToMgNr == h.FromMgNr && h.ToType == BusinessShareType.White && h.FromType == BusinessShareType.White ? 0 : h.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.White ? h.Shares : 0),
s.SharesDormant + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.Dormant ? h.Shares : 0) - (h.ToMgNr == h.FromMgNr && h.ToType == BusinessShareType.Dormant && h.FromType == BusinessShareType.Dormant ? 0 : h.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.Dormant ? h.Shares : 0)));
var cur = new MemberHistoryPoint(Member.Shares - recorded.Shares, Member.SharesRed - recorded.SharesRed, Member.SharesWhite - recorded.SharesWhite, Member.SharesDormant - recorded.SharesDormant);
if (cur.Shares != 0 || cur.SharesRed != 0 || cur.SharesWhite != 0 || cur.SharesDormant != 0) {
tbl.AddCell(NewTd())
.AddCell(NewTd("Nicht in Software erfasst", colspan: tw, italic: true))
.AddCell(NewTd())
.AddCells(NewBusinessSharesCells(cur));
if (history.Any()) {
tbl.AddCell(NewTd(borderTop: true))
.AddCell(NewTd($"Bis Lese {history.First().DateNotice.Year - 1}", borderTop: true, colspan: tw, bold: true))
.AddCell(NewTd(borderTop: true))
.AddCells(NewBusinessSharesCells(cur, isDiff: false));
}
}
DateOnly? lastDate = null;
foreach (var row in history) {
int s = 0, r = 0, w = 0, d = 0;
if (row.FromMgNr == Member.MgNr && !(row.FromMgNr == row.ToMgNr && row.FromType == row.ToType)) {
switch (row.FromType) {
case BusinessShareType.Normal: s -= row.Shares; break;
case BusinessShareType.Red: r -= row.Shares; break;
case BusinessShareType.White: w -= row.Shares; break;
case BusinessShareType.Dormant: d -= row.Shares; break;
}
}
if (row.ToMgNr == Member.MgNr) {
switch (row.ToType) {
case BusinessShareType.Normal: s += row.Shares; break;
case BusinessShareType.Red: r += row.Shares; break;
case BusinessShareType.White: w += row.Shares; break;
case BusinessShareType.Dormant: d += row.Shares; break;
}
}
if (lastDate < Member.EntryDate && row.DateNotice <= Member.EntryDate) {
tbl.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}"))
.AddCell(NewTd("Eintritt", colspan: tw, bold: true))
.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}"))
.AddCells(NewBusinessSharesCells(new()));
}
if (lastDate != null && row.DateNotice.Year != lastDate?.Year) {
tbl.AddCell(NewTd(borderTop: true))
.AddCell(NewTd(lastDate?.Year == row.DateNotice.Year - 1 ? $"Lese {lastDate?.Year}" : $"Lese {lastDate?.Year}–{row.DateNotice.Year - 1}", borderTop: true, colspan: tw, bold: true))
.AddCell(NewTd(borderTop: true))
.AddCells(NewBusinessSharesCells(cur, isDiff: false));
}
if (lastDate < Member.ExitDate && row.DateNotice <= Member.ExitDate) {
tbl.AddCell(NewTd($"{Member.ExitDate:dd.MM.yyyy}"))
.AddCell(NewTd("Austritt", colspan: tw, bold: true))
.AddCell(NewTd($"31.12.{Member.ExitDate.Value.Year + 1}")) // FIXME
.AddCells(NewBusinessSharesCells(new()));
}
cur = new(cur.Shares + s, cur.SharesRed + r, cur.SharesWhite + w, cur.SharesDormant + d);
tbl.AddCell(NewTd($"{row.DateNotice:dd.MM.yyyy}"))
.AddCell(NewTd(row.FormatReasonPhrase(Member.MgNr), colspan: tw))
.AddCell(NewTd($"{row.DateEffective:dd.MM.yyyy}"))
.AddCells(NewBusinessSharesCells(new(s, r, w, d)));
lastDate = row.DateNotice;
}
if (Member.EntryDate != null && history.Any() && Member.EntryDate > lastDate) {
tbl.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}"))
.AddCell(NewTd("Eintritt", colspan: tw, bold: true))
.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}"))
.AddCells(NewBusinessSharesCells(new()));
}
if (Member.ExitDate != null && (!history.Any() || Member.ExitDate > lastDate)) {
tbl.AddCell(NewTd($"{Member.ExitDate:dd.MM.yyyy}"))
.AddCell(NewTd("Austritt", colspan: tw, bold: true))
.AddCell(NewTd($"31.12.{Member.ExitDate.Value.Year + 1}")) // FIXME
.AddCells(NewBusinessSharesCells(new()));
}
tbl.AddCell(NewTd(borderTop: true))
.AddCell(NewTd(lastDate == null ? "Aktuell" : $"Ab Lese {lastDate?.Year}", borderTop: true, colspan: tw, bold: true))
.AddCell(NewTd(borderTop: true))
.AddCells(NewBusinessSharesCells(cur, isDiff: false));
return tbl;
Season = ctx.Seasons.ToList().MaxBy(s => s.Year) ?? throw new ArgumentException("invalid season");
MemberBuckets = ctx.GetMemberBuckets(Utils.CurrentYear, m.MgNr).GetAwaiter().GetResult();
ActiveAreaCommitments = m.ActiveAreaCommitments(ctx);
}
}
}
+222
View File
@@ -0,0 +1,222 @@
@using RazorLight
@using Elwig.Helpers
@inherits TemplatePage<Elwig.Documents.MemberDataSheet>
@model Elwig.Documents.MemberDataSheet
@{ Layout = "BusinessDocument"; }
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\MemberDataSheet.css" />
<main>
<h1>@Model.Title</h1>
<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>
@if (Model.Member.IsJuridicalPerson) {
<th colspan="3" class="small">Name</th>
<th colspan="3" class="small">Zu Handen</th>
} else {
<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>
@if (Model.Member.IsJuridicalPerson) {
<td colspan="3" class="large">@Model.Member.Name</td>
<td colspan="3" class="large">@Model.Member.ForTheAttentionOf</td>
} else {
<td class="large">@Model.Member.Prefix</td>
<td class="large">@Model.Member.GivenName @Model.Member.MiddleName</td>
<td class="large" colspan="3">@Model.Member.Name</td>
<td class="large">@Model.Member.Suffix</td>
}
</tr>
<tr>
<th>Mitglieds-Nr.:</th>
<td>@Model.Member.MgNr</td>
<th colspan="2">@(Model.Member.IsJuridicalPerson ? "Gründungsjahr/-tag" : "Geburtsjahr/-tag"):</th>
<td colspan="2">@(string.Join('.', Model.Member.Birthday?.Split('-')?.Reverse() ?? Array.Empty<string>()))</td>
</tr>
<tr>
<th>Adresse:</th>
<td colspan="5">@Model.Member.Address</td>
</tr>
<tr>
<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?.FullName</td>
</tr>
<tr>
<th>Adresse:</th>
<td colspan="5">@Model.Member.BillingAddress?.Address</td>
</tr>
<tr>
<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>
</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>
}
<tr class="sectionheading"><th colspan="6">Betrieb</th></tr>
<tr>
<th>Betriebs-Nr.:</th>
<td>@Model.Member.LfbisNr</td>
<th colspan="2">UID:</th>
<td colspan="2">@Model.Member.UstIdNr</td>
</tr>
<tr>
<th>Stammgemeinde:</th>
<td>@Model.Member.DefaultKg?.Name</td>
<th colspan="2">Buchführend:</th>
<td colspan="2">@(Model.Member.IsBuchführend ? "Ja" : "Nein") <span class="small">(@((Model.Member.IsBuchführend ? Model.Season.VatNormal : Model.Season.VatFlatrate) * 100)% USt.)</span></td>
</tr>
<tr>
<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.ActiveAreaCommitments.GroupBy(a => a.AreaComType).Select(group => new {
AreaComType = group.Key,
AreaComs = group.OrderBy(c => c.Kg.AtKg.Name),
Size = group.Sum(c => c.Area)
}).OrderByDescending(a => a.Size).ToList();
var lastContract = "";
}
@if (areaComs.Count != 0) {
<br class="area-commitements"/>
<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;"/>
</colgroup>
<thead>
<tr>
<th rowspan="2" style="text-align: left;">Katastralgemeinde</th>
<th rowspan="2" style="text-align: left;">Ried</th>
<th rowspan="2" style="text-align: left;">Parzelle(n)</th>
<th>Fläche</th>
<th rowspan="2" style="text-align: center;">Bewirt.</th>
<th rowspan="2" style="text-align: center;">Laufzeit</th>
</tr>
<tr>
<th>[m²]</th>
</tr>
</thead>
<tbody>
@foreach (var contractType in areaComs) {
<tr class="subheading @(contractType.AreaComType.DisplayName != lastContract && lastContract != "" ? "new" : "")">
<th colspan="3">
@($"{contractType.AreaComType.WineVar.Name} {(contractType.AreaComType.WineAttr != null ? "(" + contractType.AreaComType.WineAttr + ")" : "")}")
</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:00000}"))</span></td>
<td>@areaCom.Rd?.Name</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 ? (areaCom.YearFrom == null ? "unbefristet" : $"ab {areaCom.YearFrom}") : (areaCom.YearFrom == null ? $"bis {areaCom.YearTo}" : $"{areaCom.YearFrom}–{areaCom.YearTo}"))</td>
</tr>
lastContract = contractType.AreaComType.DisplayName;
}
}
<tr class="sum bold">
<td colspan="3">Gesamt:</td>
<td class="number">@($"{Model.ActiveAreaCommitments.Sum(a => a.Area):N0}")</td>
<td colspan="2"></td>
</tr>
</tbody>
</table>
}
</main>
+30
View File
@@ -0,0 +1,30 @@
h2 {
margin-bottom: 0.5em !important;
}
table.member {
margin-bottom: 5mm;
}
table.area-commitements {
margin-top: 0;
}
table.area-commitements td {
vertical-align: top;
}
table.area-commitements td.text {
white-space: normal;
}
table.area-commitements tr.sum {
font-size: 12pt;
}
@page :not(:first) {
br.area-commitements {
display: none;
}
}
+6 -87
View File
@@ -1,10 +1,4 @@
using Elwig.Models.Dtos;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -14,16 +8,15 @@ namespace Elwig.Documents {
public new static string Name => "Mitgliederliste";
public string Filter;
public List<MemberListRow> Members;
public IEnumerable<MemberListRow> Members;
public string[] AreaCmtFilters;
public bool FilterAreaCmts => AreaCmtFilters.Length > 0;
public string[] AreaComFilters;
public bool FilterAreaComs => AreaComFilters.Length > 0;
public MemberList(string filter, IEnumerable<MemberListRow> members) :
base(Name) {
public MemberList(string filter, IEnumerable<MemberListRow> members) : base(Name) {
Filter = filter;
Members = [.. members];
AreaCmtFilters = [..members
Members = members;
AreaComFilters = [..members
.SelectMany(m => m.AreaCommitmentsFiltered)
.Select(c => c.VtrgId)
.Distinct()
@@ -33,79 +26,5 @@ namespace Elwig.Documents {
public MemberList(string filter, MemberListData data) :
this(filter, data.Rows) {
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
doc.Add(new KernedParagraph(Name, 24)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(new KernedParagraph(Filter, 14)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(NewMemberTable(Members));
}
protected Table NewMemberTable(List<MemberListRow> members) {
var tbl = new Table(AreaCmtFilters.Length > 1 ? ColsMM(8, 38, 36, 8, 18, 12, 5, 16, 12, 12) : ColsMM(8, 42, 40, 8, 20, 12, 5, 18, 12), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
var headerSpan = FilterAreaCmts ? 3 : 2;
tbl.AddHeaderCell(NewTh("Nr.", rowspan: headerSpan))
.AddHeaderCell(NewTh("Name", rowspan: headerSpan, left: true))
.AddHeaderCell(NewTh("Adresse", rowspan: headerSpan, left: true))
.AddHeaderCell(NewTh("PLZ", rowspan: headerSpan))
.AddHeaderCell(NewTh("Ort", rowspan: headerSpan, left: true))
.AddHeaderCell(NewTh("Betr.-Nr.", rowspan: headerSpan))
.AddHeaderCell(NewTh("GA", rowspan: headerSpan).SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("Stamm-KG", rowspan: headerSpan, left: true))
.AddHeaderCell(NewTh("Geb. Fl.", colspan: FilterAreaCmts ? AreaCmtFilters.Length : 1));
if (FilterAreaCmts) {
foreach (var vtrgId in AreaCmtFilters) {
tbl.AddHeaderCell(NewTh(vtrgId));
}
}
for (int i = 0; i < (FilterAreaCmts ? AreaCmtFilters.Length : 1); i++) {
tbl.AddHeaderCell(NewTh("[m²]"));
}
string? lastBranch = members.Select(m => m.Branch).Distinct().Count() == 1 ? null : "";
foreach (var m in members) {
if (lastBranch != null && m.Branch != lastBranch) {
tbl.AddCell(NewCell(colspan: 8 + Math.Max(AreaCmtFilters.Length, 1)).SetHeightMM(5).SetKeepWithNext(true));
tbl.AddCell(NewCell(new KernedParagraph(m.Branch ?? "", 16).SetFont(BF), colspan: 8 + Math.Max(AreaCmtFilters.Length, 1))
.SetPaddingsMM(1, 2, 1, 2)
.SetBorder(new SolidBorder(BorderThickness))
.SetBackgroundColor(new DeviceRgb(0xe0, 0xe0, 0xe0)));
lastBranch = m.Branch;
}
tbl.AddCell(NewTd($"{m.MgNr}", 8, rowspan: m.BillingName != null ? 2 : 1, right: true).SetVerticalAlignment(VerticalAlignment.TOP))
.AddCell(NewTd($"{m.AdminName1} {m.Name2}", 8))
.AddCell(NewTd(m.Address, 8))
.AddCell(NewTd($"{m.Plz}", 8))
.AddCell(NewTd(m.Locality, 6))
.AddCell(NewTd(m.LfbisNr ?? "", 8))
.AddCell(NewTd($"{m.SharesActive:N0}", 8, right: true).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd(m.DefaultKg ?? "", 6));
if (AreaCmtFilters.Length > 0) {
foreach (var v in AreaCmtFilters) {
tbl.AddCell(NewTd($"{m.AreaCommitmentsFiltered.FirstOrDefault(c => c.VtrgId == v).Area:N0}", 8, right: true));
}
} else {
tbl.AddCell(NewTd($"{m.AreaCommitment:N0}", 8, right: true));
}
if (m.BillingName != null) {
tbl.AddCell(NewTd(m.BillingName, 8))
.AddCell(NewTd(m.BillingAddress ?? "", 8))
.AddCell(NewTd($"{m.BillingPlz}", 8))
.AddCell(NewTd(m.BillingLocality ?? "", 6))
.AddCell(NewTd(colspan: 3 + Math.Max(AreaCmtFilters.Length, 1)));
}
}
return tbl;
}
}
}
+109
View File
@@ -0,0 +1,109 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.MemberList>
@model Elwig.Documents.MemberList
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\MemberList.css" />
<main>
<h1>Mitgliederliste</h1>
<h2>@Model.Filter</h2>
<table class="members">
<colgroup>
<col style="width: 8mm;"/>
@if (Model.AreaComFilters.Length > 1) {
<col style="width: 38mm;"/>
} else {
<col style="width: 42mm;"/>
}
@if (Model.AreaComFilters.Length > 1) {
<col style="width: 36mm;"/>
} else {
<col style="width: 40mm;"/>
}
<col style="width: 8mm;"/>
@if (Model.AreaComFilters.Length > 1) {
<col style="width: 18mm;"/>
} else {
<col style="width: 20mm;"/>
}
<col style="width: 12mm;"/>
<col style="width: 5mm;"/>
@if (Model.AreaComFilters.Length > 1) {
<col style="width: 16mm;"/>
} else {
<col style="width: 18mm;"/>
}
<col style="width: 12mm;"/>
@if (Model.AreaComFilters.Length > 1) {
<col style="width: 12mm;"/>
}
</colgroup>
<thead>
<tr>
@{
var headerSpan = Model.FilterAreaComs ? 3 : 2;
}
<th rowspan="@headerSpan">Nr.</th>
<th rowspan="@headerSpan" style="text-align: left;">Name</th>
<th rowspan="@headerSpan" style="text-align: left;">Adresse</th>
<th rowspan="@headerSpan">PLZ</th>
<th rowspan="@headerSpan" style="text-align: left;">Ort</th>
<th rowspan="@headerSpan">Betr.-Nr.</th>
<th rowspan="@headerSpan">GA</th>
<th rowspan="@headerSpan" style="text-align: left;">Stamm-KG</th>
<th colspan="@(Model.FilterAreaComs ? Model.AreaComFilters.Length : 1)">Geb. Fl.</th>
</tr>
@if (Model.FilterAreaComs) {
<tr>
@foreach (var vtrgId in Model.AreaComFilters) {
<th>@vtrgId</th>
}
</tr>
}
<tr>
@for (int i = 0; i < Math.Max(Model.AreaComFilters.Length, 1); i++) {
<th class="unit">[m²]</th>
}
</tr>
</thead>
<tbody class="small">
@{
string? lastBranch = Model.Members.Select(m => m.Branch).Distinct().Count() == 1 ? null : "";
}
@foreach (var m in Model.Members) {
if (lastBranch != null && m.Branch != lastBranch) {
<tr class="spacing"><td colspan="@(8 + Math.Max(Model.AreaComFilters.Length, 1))"></td></tr>
<tr class="header">
<th colspan="@(8 + Math.Max(Model.AreaComFilters.Length, 1))">@m.Branch</th>
</tr>
lastBranch = m.Branch;
}
<tr>
<td class="number" rowspan="@(m.BillingName != null ? 2 : 1)">@m.MgNr</td>
<td>@m.AdminName1 @m.Name2</td>
<td>@m.Address</td>
<td>@m.Plz</td>
<td class="tiny">@m.Locality</td>
<td>@m.LfbisNr</td>
<td class="number">@m.BusinessShares</td>
<td class="tiny">@m.DefaultKg</td>
@if (Model.AreaComFilters.Length > 0) {
foreach (var v in Model.AreaComFilters) {
<td class="number">@($"{m.AreaCommitmentsFiltered.FirstOrDefault(c => c.VtrgId == v).Area:N0}")</td>
}
} else {
<td class="number">@($"{m.AreaCommitment:N0}")</td>
}
</tr>
if (m.BillingName != null) {
<tr>
<td>@m.BillingName</td>
<td>@m.BillingAddress</td>
<td>@m.BillingPlz</td>
<td class="tiny">@m.BillingLocality</td>
<td colspan="4"></td>
</tr>
}
}
</tbody>
</table>
</main>
+13
View File
@@ -0,0 +1,13 @@
h1 {
text-align: center;
font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}
+9 -289
View File
@@ -2,315 +2,35 @@ using Elwig.Helpers;
using Elwig.Helpers.Billing;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Documents {
public class PaymentVariantSummary : Document {
public new static string Name => "Auszahlungsvariante";
public PaymentVariantSummaryData? Data;
public PaymentVariantSummaryData Data;
public PaymentVar Variant;
public BillingData BillingData;
public string CurrencySymbol;
public int MemberNum;
public int DeliveryNum;
public int DeliveryPartNum;
public List<ModifierStat>? ModifierStat;
public Dictionary<string, Modifier>? Modifiers;
public List<ModifierStat> ModifierStat;
public Dictionary<string, Modifier> Modifiers;
private List<Credit> _credits = [];
private List<PaymentDeliveryPart> _parts = [];
public PaymentVariantSummary(PaymentVar v, PaymentVariantSummaryData? data = null) :
public PaymentVariantSummary(PaymentVar v, PaymentVariantSummaryData data) :
base($"{Name} {v.Year} - {v.Name}") {
Variant = v;
BillingData = BillingData.FromJson(v.Data);
Data = data;
CurrencySymbol = v.Season.Currency.Symbol ?? v.Season.Currency.Code;
}
public static async Task<PaymentVariantSummary> Initialize(int year, int avnr, PaymentVariantSummaryData? data = null) {
using var ctx = new AppDbContext();
var v = await ctx.PaymentVariants
.Where(v => v.Year == year && v.AvNr == avnr)
.SingleAsync();
return new PaymentVariantSummary(v, data);
}
protected override async Task LoadData(AppDbContext ctx) {
_credits = await ctx.Credits.Where(c => c.Year == Variant.Year && c.AvNr == Variant.AvNr).ToListAsync();
_parts = await ctx.PaymentDeliveryParts.Where(p => p.Year == Variant.Year && p.AvNr == Variant.AvNr).ToListAsync();
MemberNum = _credits.Count;
IsPreview = MemberNum == 0;
DeliveryNum = await ctx.Deliveries.Where(d => d.Year == Variant.Year).CountAsync();
DeliveryPartNum = await ctx.DeliveryParts.Where(d => d.Year == Variant.Year).CountAsync();
Data ??= await PaymentVariantSummaryData.ForPaymentVariant(Variant, ctx.PaymentVariantSummaryRows);
ModifierStat = await AppDbContext.GetModifierStats(Variant.Year, Variant.AvNr);
Modifiers = await ctx.FetchModifiers(Variant.Year).ToDictionaryAsync(m => m.ModId);
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
if (Data == null || Modifiers == null || ModifierStat == null) throw new Exception("Call LoadData before RenderBody");
base.RenderBody(doc, pdf);
doc.Add(new KernedParagraph($"{Name} Lese {Variant.Year}", 24)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(new KernedParagraph(Variant.Name, 14)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 10, 0));
doc.Add(NewVariantStatTable(Data).SetMarginBottomMM(10));
doc.Add(NewModifierStatTable(Modifiers, ModifierStat));
doc.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
doc.Add(NewPriceTable(Data));
}
protected Cell NewSectionHdr(string text, int colspan = 1, bool borderLeft = false) {
return NewTd(text, 10, colspan: colspan, bold: true, italic: true, center: true, borderTop: true)
.SetBackgroundColor(new DeviceRgb(0xe0, 0xe0, 0xe0))
.SetPaddingsMM(0.5f, 1, 0.5f, 1)
.SetBorderLeft(borderLeft ? new SolidBorder(BorderThickness) : Border.NO_BORDER);
}
protected Cell NewSectionTh(string? text = null, float fontSize = 10, int colspan = 1, bool borderTop = false, bool borderLeft = false, bool overflow = false) {
return NewTd(text, fontSize, colspan: colspan, italic: true, borderTop: borderTop, overflow: overflow)
.SetPaddingRightMM(0)
.SetBorderLeft(borderLeft ? new SolidBorder(BorderThickness) : Border.NO_BORDER);
}
protected Table NewVariantStatTable(PaymentVariantSummaryData data) {
var tbl = new Table(ColsMM(20, 30, 4.5, 4.5, 23.5, 47.5, 15, 20))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetBorder(new SolidBorder(BorderThickness));
//var sum1 = _parts.Sum(p => p.NetAmount);
//var sum2 = _credits.Sum(p => p.); //Variant.MemberPayments.Sum(p => p.Amount);
var deliveryModifiers = _parts.Sum(p => p.Amount - p.NetAmount);
var memberModifiers = _credits.Sum(c => c.Payment.Amount - c.Payment.NetAmount);
var sum2 = _credits.Sum(p => p.NetAmount);
var sum1 = sum2 - deliveryModifiers - memberModifiers;
var payed = -_credits.Sum(p => p.PrevNetAmount ?? 0m);
var netSum = _credits.Sum(p => p.NetAmount) - _credits.Sum(p => p.PrevNetAmount ?? 0m);
var vat = _credits.Sum(p => p.VatAmount);
var grossSum = _credits.Sum(p => p.GrossAmount);
var totalMods = _credits.Sum(p => p.Modifiers ?? 0m);
var considered = -_credits.Sum(p => p.PrevModifiers ?? 0m);
var totalSum = _credits.Sum(p => p.Amount);
var weiRows = data.Rows.Where(r => r.QualityLevel == "Wein");
var minWei = weiRows.Min(r => r.UnCtrd.MinPrice);
var maxWei = weiRows.Max(r => r.UnCtrd.MaxPrice);
var quwRows = data.Rows.Where(r => r.QualityLevel != "Wein");
var minPrice = quwRows.Min(r => r.UnCtrd.MinPrice);
var maxPrice = quwRows.Max(r => r.UnCtrd.MaxPrice);
var ctrdRows = data.Rows
.Where(r => r.Ctrd.MaxPrice != null && r.UnCtrd.MinPrice != null)
.Select(r => r.Ctrd.MaxPrice - r.UnCtrd.MinPrice);
var minCtrd = ctrdRows.Min();
var maxCtrd = ctrdRows.Max();
tbl.AddCell(NewSectionHdr("Allgemein", colspan: 5))
.AddCell(NewSectionHdr("Berücksichtigt", colspan: 3, borderLeft: true))
.AddCell(NewSectionTh("Name:"))
.AddCell(NewTd(Variant.Name, colspan: 4))
.AddCell(NewSectionTh("Zu-/Abschläge bei Lieferungen:", colspan: 2, borderLeft: true))
.AddCell(NewTd(BillingData.ConsiderDelieryModifiers ? "Ja" : "Nein", center: true))
.AddCell(NewSectionTh("Beschr.:"))
.AddCell(NewTd(Variant.Comment, colspan: 4))
.AddCell(NewSectionTh("Pönalen bei Unterlieferungen (FB):", colspan: 2, borderLeft: true))
.AddCell(NewTd(BillingData.ConsiderContractPenalties ? "Ja" : "Nein", center: true))
.AddCell(NewSectionTh("Rebel-Zuschl.:", overflow: true))
.AddCell(NewTd($"{Utils.GetSign(BillingData.NetWeightModifier)}{Math.Abs(BillingData.NetWeightModifier) * 100:N2} % / {Utils.GetSign(BillingData.GrossWeightModifier)}{Math.Abs(BillingData.GrossWeightModifier) * 100:N2} %", colspan: 4, center: true))
.AddCell(NewSectionTh("Strafen bei Unterlieferungen (GA):", colspan: 2, borderLeft: true))
.AddCell(NewTd(BillingData.ConsiderTotalPenalty ? "Ja" : "Nein", center: true))
.AddCell(NewSectionTh("Datum/Überw.:", overflow: true))
.AddCell(NewTd($"{Variant.Date:dd.MM.yyyy} / {Variant.TransferDate:dd.MM.yyyy}", colspan: 4, center: true))
.AddCell(NewSectionTh("(Automatische) Nachzeichnung von GA:", colspan: 2, borderLeft: true))
.AddCell(NewTd(BillingData.ConsiderAutoBusinessShares ? "Ja" : "Nein", center: true))
.AddCell(NewSectionTh("Berechnung:"))
.AddCell(NewTd($"{Variant.CalcTime:dd.MM.yyyy, HH:mm:ss}", colspan: 4, center: true))
.AddCell(NewSectionTh("Benutzerdef. Zu-/Abschläge pro Mitglied:", colspan: 2, borderLeft: true))
.AddCell(NewTd(BillingData.ConsiderCustomModifiers ? "Ja" : "Nein", center: true))
.AddCell(NewSectionHdr("Beträge", colspan: 5))
.AddCell(NewSectionHdr("Statistik", colspan: 3, borderLeft: true))
.AddCell(NewSectionTh("Zwischensumme:", colspan: 2))
.AddCell(NewTd())
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{sum1:N2}", right: true))
.AddCell(NewSectionTh("Lieferanten:", borderLeft: true))
.AddCell(NewTd($"{MemberNum:N0}", colspan: 2, right: true))
.AddCell(NewSectionTh("Zu-/Abschläge (Mitglieder):", colspan: 2))
.AddCell(NewTd(Utils.GetSign(memberModifiers), right: true))
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{Math.Abs(memberModifiers):N2}", right: true))
.AddCell(NewSectionTh("Lieferungen:", borderLeft: true))
.AddCell(NewTd($"{DeliveryNum:N0}", colspan: 2, right: true))
.AddCell(NewSectionTh("Zu-/Abschläge (Lieferungen):", colspan: 2))
.AddCell(NewTd(Utils.GetSign(deliveryModifiers), right: true))
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{Math.Abs(deliveryModifiers):N2}", right: true))
.AddCell(NewSectionTh("Teillieferungen:", borderLeft: true))
.AddCell(NewTd($"{DeliveryPartNum:N0}", colspan: 2, right: true))
.AddCell(NewSectionTh("Gesamtsumme:", colspan: 2))
.AddCell(NewTd(borderTop: true))
.AddCell(NewTd(CurrencySymbol, borderTop: true))
.AddCell(NewTd($"{sum2:N2}", right: true, borderTop: true))
.AddCell(NewSectionTh(borderLeft: true))
.AddCell(NewTd(colspan: 2))
.AddCell(NewSectionTh("Bisher ausgezahlt:", colspan: 2))
.AddCell(NewTd(Utils.GetSign(payed), right: true))
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{Math.Abs(payed):N2}", right: true))
.AddCell(NewSectionTh("Preis (abgewertet):", borderTop: true, borderLeft: true))
.AddCell(NewTd(minWei == null && maxWei == null ? "-" : (minWei != maxWei ? $"{minWei:N4}\u2013{maxWei:N4}" : $"{minWei:N4}") + $" {CurrencySymbol}/kg", colspan: 2, center: true, borderTop: true))
.AddCell(NewSectionTh("Nettosumme:", colspan: 2))
.AddCell(NewTd(borderTop: true))
.AddCell(NewTd(CurrencySymbol, borderTop: true))
.AddCell(NewTd($"{netSum:N2}", right: true, borderTop: true))
.AddCell(NewSectionTh("Preis (ungeb., nicht abgew.):", borderLeft: true))
.AddCell(NewTd(minPrice == null && maxPrice == null ? "-" : (minPrice != maxPrice ? $"{minPrice:N4}–{maxPrice:N4}" : $"{minPrice:N4}") + $" {CurrencySymbol}/kg", colspan: 2, center: true))
.AddCell(NewSectionTh("Mehrwertsteuer:", colspan: 2))
.AddCell(NewTd(Utils.GetSign(vat), right: true))
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{Math.Abs(vat):N2}", right: true))
.AddCell(NewSectionTh("Gebunden-Zuschlag:", borderLeft: true))
.AddCell(NewTd(minCtrd == null && maxCtrd == null ? "-" : minCtrd != maxCtrd ? $"{minCtrd:N4}\u2013{maxCtrd:N4} {CurrencySymbol}/kg" : minCtrd == 0 ? "-" : $"{minCtrd:N4} {CurrencySymbol}/kg", colspan: 2, center: true))
.AddCell(NewSectionTh("Bruttosumme:", colspan: 2))
.AddCell(NewTd(borderTop: true))
.AddCell(NewTd(CurrencySymbol, borderTop: true))
.AddCell(NewTd($"{grossSum:N2}", right: true, borderTop: true))
.AddCell(NewSectionTh(borderLeft: true))
.AddCell(NewTd(colspan: 2))
.AddCell(NewSectionTh("Abzüge (Strafen/Pönalen, GA, \u2026):", colspan: 2))
.AddCell(NewTd(Utils.GetSign(totalMods)))
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{Math.Abs(totalMods):N2}", right: true))
.AddCell(NewSectionTh("Menge (ungebunden):", borderLeft: true, borderTop: true))
.AddCell(NewTd($"{data.Rows.Sum(r => r.UnCtrd.Weight):N0} kg", colspan: 2, right: true, borderTop: true))
.AddCell(NewSectionTh("Bereits berücksichtigte Abzüge:", colspan: 2))
.AddCell(NewTd(Utils.GetSign(considered)))
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{Math.Abs(considered):N2}", right: true))
.AddCell(NewSectionTh("Menge (gebunden):", borderLeft: true))
.AddCell(NewTd($"{data.Rows.Sum(r => r.Ctrd.Weight + r.LowCtrd.Weight):N0} kg", colspan: 2, right: true))
.AddCell(NewSectionTh("Auszahlungsbetrag:", colspan: 2))
.AddCell(NewTd(borderTop: true))
.AddCell(NewTd(CurrencySymbol, borderTop: true))
.AddCell(NewTd($"{totalSum:N2}", right: true, borderTop: true))
.AddCell(NewSectionTh("Gesamtmenge:", borderLeft: true))
.AddCell(NewTd($"{data.Rows.Sum(r => r.UnCtrd.Weight + r.LowCtrd.Weight + r.Ctrd.Weight):N0} kg", colspan: 2, right: true, borderTop: true));
return tbl;
}
protected Table NewModifierStatTable(Dictionary<string, Modifier> modifiers, IEnumerable<ModifierStat> modStat) {
var tbl = new Table(ColsMM(35, 30, 25, 25, 25, 25))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetBorder(new SolidBorder(BorderThickness));
tbl.AddCell(NewSectionHdr("Statistik Zu-/Abschläge", colspan: 6))
.AddCell(NewTh("Name", rowspan: 2))
.AddCell(NewTh("Zu-/Abschlag", rowspan: 2))
.AddCell(NewTh("Lieferungen"))
.AddCell(NewTh("Minimum"))
.AddCell(NewTh("Maximum"))
.AddCell(NewTh("Betrag"))
.AddCell(NewTh("[#]"))
.AddCell(NewTh($"[{CurrencySymbol}]"))
.AddCell(NewTh($"[{CurrencySymbol}]"))
.AddCell(NewTh($"[{CurrencySymbol}]"));
foreach (var m in modStat) {
var mod = modifiers[m.ModId];
tbl.AddCell(NewTd(mod.Name, italic: true))
.AddCell(NewTd(mod.ValueStr, right: true))
.AddCell(NewTd($"{m.Count:N0}", right: true))
.AddCell(NewTd($"{m.Min:N2}", right: true))
.AddCell(NewTd($"{m.Max:N2}", right: true))
.AddCell(NewTd($"{m.Sum:N2}", right: true));
}
return tbl;
}
protected Table NewPriceTable(PaymentVariantSummaryData data) {
var tbl = new Table(ColsMM(25, 19, 18, 15, 18, 15, 18, 15, 22))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Sorte/Attr./Bewirt.\nQualitätsstufe", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Gradation"))
.AddHeaderCell(NewTh("ungebunden", colspan: 2))
.AddHeaderCell(NewTh("attributlos gebunden", colspan: 2))
.AddHeaderCell(NewTh("gebunden", colspan: 2))
.AddHeaderCell(NewTh("Gesamt"))
.AddHeaderCell(NewTh(true ? "[°Oe]" : "[°KMW]"))
.AddHeaderCell(NewTh("[kg]"))
.AddHeaderCell(NewTh($"[{CurrencySymbol}/kg]"))
.AddHeaderCell(NewTh("[kg]"))
.AddHeaderCell(NewTh($"[{CurrencySymbol}/kg]"))
.AddHeaderCell(NewTh("[kg]"))
.AddHeaderCell(NewTh($"[{CurrencySymbol}/kg]"))
.AddHeaderCell(NewTh($"[{CurrencySymbol}]"));
string? lastHdr = null;
foreach (var row in data.Rows) {
var hdr = $"{row.Variety}{(row.Attribute != null ? " / " : "")}{row.Attribute}{(row.Cultivation != null ? " / " : "")}{row.Cultivation}";
if (lastHdr != hdr) {
var rows = data.Rows
.Where(r => r.Variety == row.Variety && r.Attribute == row.Attribute && r.Cultivation == row.Cultivation)
.ToList();
var border = lastHdr != null;
tbl.AddCell(NewTd(hdr, colspan: 2, bold: true, italic: true, borderTop: border))
.AddCell(NewTd($"{rows.Sum(r => r.UnCtrd.Weight):N0}", right: true, bold: true, borderTop: border))
.AddCell(NewTd(borderTop: border))
.AddCell(NewTd($"{rows.Sum(r => r.LowCtrd.Weight):N0}", right: true, bold: true, borderTop: border))
.AddCell(NewTd(borderTop: border))
.AddCell(NewTd($"{rows.Sum(r => r.Ctrd.Weight):N0}", right: true, bold: true, borderTop: border))
.AddCell(NewTd(borderTop: border))
.AddCell(NewTd($"{rows.Sum(r => r.Amount):N2}", right: true, bold: true, borderTop: border));
}
tbl.AddCell(NewTd(row.QualityLevel))
.AddCell(NewTd($"{row.Oe:N0}", center: true))
.AddCell(NewTd(row.UnCtrd.Weight != 0 ? $"{row.UnCtrd.Weight:N0}" : "-", right: true))
.AddCell(NewTd(row.UnCtrd.MaxPrice != null ? $"{row.UnCtrd.MaxPrice:N4}" : "-", right: true))
.AddCell(NewTd(row.LowCtrd.Weight != 0 ? $"{row.LowCtrd.Weight:N0}" : "-", right: true))
.AddCell(NewTd(row.LowCtrd.MaxPrice != null ? $"{row.LowCtrd.MaxPrice:N4}" : "-", right: true))
.AddCell(NewTd(row.Ctrd.Weight != 0 ? $"{row.Ctrd.Weight:N0}" : "-", right: true))
.AddCell(NewTd(row.Ctrd.MaxPrice != null ? $"{row.Ctrd.MaxPrice:N4}" : "-", right: true))
.AddCell(NewTd($"{row.Amount:N2}", right: true));
lastHdr = hdr;
}
return tbl;
MemberNum = v.Credits.Count;
DeliveryNum = v.DeliveryPartPayments.DistinctBy(p => p.DeliveryPart.Delivery).Count();
DeliveryPartNum = v.DeliveryPartPayments.Count;
ModifierStat = AppDbContext.GetModifierStats(v.Year, v.AvNr).GetAwaiter().GetResult();
Modifiers = v.Season.Modifiers.ToDictionary(m => m.ModId);
}
}
}
@@ -0,0 +1,288 @@
@using RazorLight
@using Elwig.Helpers
@inherits TemplatePage<Elwig.Documents.PaymentVariantSummary>
@model Elwig.Documents.PaymentVariantSummary
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\PaymentVariantSummary.css" />
<main>
<h1>Auszahlungsvariante Lese @Model.Variant.Year</h1>
<h2>@Model.Variant.Name</h2>
<table class="payment-variant border">
<colgroup>
<col style="width: 20.0mm;"/>
<col style="width: 30.0mm;"/>
<col style="width: 4.5mm;"/>
<col style="width: 28.0mm;"/>
<col style="width: 47.5mm;"/>
<col style="width: 15.0mm;"/>
<col style="width: 20.0mm;"/>
</colgroup>
@{
//var sum1 = Model.Variant.DeliveryPartPayments.Sum(p => p.NetAmount);
//var sum2 = Model.Variant.Credits.Sum(p => p.); //Model.Variant.MemberPayments.Sum(p => p.Amount);
var deliveryModifiers = Model.Variant.DeliveryPartPayments.Sum(p => p.Amount - p.NetAmount);
var memberModifiers = Model.Variant.Credits.Sum(c => c.Payment.Amount - c.Payment.NetAmount);
var sum2 = Model.Variant.Credits.Sum(p => p.NetAmount);
var sum1 = sum2 - deliveryModifiers - memberModifiers;
var payed = -Model.Variant.Credits.Sum(p => p.PrevNetAmount ?? 0m);
var netSum = Model.Variant.Credits.Sum(p => p.NetAmount) - Model.Variant.Credits.Sum(p => p.PrevNetAmount ?? 0m);
var vat = Model.Variant.Credits.Sum(p => p.VatAmount);
var grossSum = Model.Variant.Credits.Sum(p => p.GrossAmount);
var totalMods = Model.Variant.Credits.Sum(p => p.Modifiers ?? 0m);
var considered = -Model.Variant.Credits.Sum(p => p.PrevModifiers ?? 0m);
var totalSum = Model.Variant.Credits.Sum(p => p.Amount);
}
<tbody>
<tr class="sectionheading">
<th colspan="4">Allgemein</th>
<th colspan="3" class="lborder">Berücksichtigt</th>
</tr>
<tr>
<th>Name:</th>
<td colspan="3">@Model.Variant.Name</td>
<th colspan="2" class="lborder">Zu-/Abschläge bei Lieferungen:</th>
<td class="center">@(Model.BillingData.ConsiderDelieryModifiers ? "Ja" : "Nein")</td>
</tr>
<tr>
<th>Beschr.:</th>
<td colspan="3">@Model.Variant.Comment</td>
<th colspan="2" class="lborder">Pönalen bei Unterlieferungen (FB):</th>
<td class="center">@(Model.BillingData.ConsiderContractPenalties ? "Ja" : "Nein")</td>
</tr>
<tr>
<th style="overflow: visible;">Rebel-Zuschl.:</th>
<td colspan="3" class="center">
@($"{Utils.GetSign(Model.BillingData.NetWeightModifier)}{Math.Abs(Model.BillingData.NetWeightModifier) * 100:N2}") % /
@($"{Utils.GetSign(Model.BillingData.GrossWeightModifier)}{Math.Abs(Model.BillingData.GrossWeightModifier) * 100:N2}") %
</td>
<th colspan="2" class="lborder">Strafen bei Unterlieferungen (GA):</th>
<td class="center">@(Model.BillingData.ConsiderTotalPenalty ? "Ja" : "Nein")</td>
</tr>
<tr>
<th style="overflow: visible;">Datum/Überw.:</th>
<td colspan="3" class="center">
@($"{Model.Variant.Date:dd.MM.yyyy}") /
@($"{Model.Variant.TransferDate:dd.MM.yyyy}")
</td>
<th colspan="2" class="lborder">Automatische Nachzeichnung der GA:</th>
<td class="center">@(Model.BillingData.ConsiderAutoBusinessShares ? "Ja" : "Nein")</td>
</tr>
<tr>
<th>Berechnung:</th>
<td colspan="3" class="center">@($"{Model.Variant.CalcTime:dd.MM.yyyy, HH:mm:ss}")</td>
<th colspan="2" class="lborder">Benutzerdef. Zu-/Abschläge pro Mitglied:</th>
<td class="center">@(Model.BillingData.ConsiderCustomModifiers ? "Ja" : "Nein")</td>
</tr>
<tr class="sectionheading">
<th colspan="4">Beträge</th>
<th colspan="3" class="lborder">Statistik</th>
</tr>
<tr>
<th colspan="2">Zwischensumme:</th>
<td></td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{sum1:N2}")</td>
<th class="lborder">Lieferanten:</th>
<td colspan="2" class="number">@($"{Model.MemberNum:N0}")</td>
</tr>
<tr>
<th colspan="2">Zu-/Abschläge (Mitglieder):</th>
<td class="number">@Utils.GetSign(memberModifiers)</td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(memberModifiers):N2}")</td>
<th class="lborder">Lieferungen:</th>
<td colspan="2" class="number">@($"{Model.DeliveryNum:N0}")</td>
</tr>
<tr>
<th colspan="2">Zu-/Abschläge (Lieferungen):</th>
<td class="number">@Utils.GetSign(deliveryModifiers)</td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(deliveryModifiers):N2}")</td>
<th class="lborder">Teillieferungen:</th>
<td colspan="2" class="number">@($"{Model.DeliveryPartNum:N0}")</td>
</tr>
<tr>
<th colspan="2">Gesamtsumme:</th>
<td class="number tborder"></td>
<td class="number tborder"><span class="fleft">@Model.CurrencySymbol</span>@($"{sum2:N2}")</td>
<th class="lborder"></th>
<td colspan="2"></td>
</tr>
<tr>
<th colspan="2">Bisher ausgezahlt:</th>
<td class="number">@Utils.GetSign(payed)</td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(payed):N2}")</td>
@{
var weiRows = Model.Data.Rows.Where(r => r.QualityLevel == "Wein");
var minWei = weiRows.Min(r => r.Ungeb.MinPrice);
var maxWei = weiRows.Max(r => r.Ungeb.MaxPrice);
}
<th class="lborder tborder">Preis (abgewertet):</th>
<td colspan="2" class="center tborder">@(minWei != maxWei ? $"{minWei:N4}–{maxWei:N4}" : $"{minWei:N4}") @Model.CurrencySymbol/kg</td>
</tr>
<tr>
<th colspan="2">Nettosumme:</th>
<td class="number tborder"></td>
<td class="number tborder"><span class="fleft">@Model.CurrencySymbol</span>@($"{netSum:N2}")</td>
@{
var quwRows = Model.Data.Rows.Where(r => r.QualityLevel != "Wein");
var minPrice = quwRows.Min(r => r.Ungeb.MinPrice);
var maxPrice = quwRows.Max(r => r.Ungeb.MaxPrice);
}
<th class="lborder">Preis (ungeb., nicht abgew.):</th>
<td colspan="2" class="center">@(minPrice != maxPrice ? $"{minPrice:N4}–{maxPrice:N4}" : $"{minPrice:N4}") @Model.CurrencySymbol/kg</td>
</tr>
<tr>
<th colspan="2">Mehrwertsteuer:</th>
<td class="number">@Utils.GetSign(vat)</td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(vat):N2}")</td>
@{
var gebRows = Model.Data.Rows
.Where(r => r.Geb.MaxPrice != null && r.Ungeb.MinPrice != null)
.Select(r => r.Geb.MaxPrice - r.Ungeb.MinPrice);
var minGeb = gebRows.Min();
var maxGeb = gebRows.Max();
}
<th class="lborder">Gebunden-Zuschlag:</th>
<td colspan="2" class="center">
@(minGeb != maxGeb ? $"{minGeb:N4}–{maxGeb:N4} {Model.CurrencySymbol}/kg" : minGeb == 0 ? "-" : $"{minGeb:N4} {Model.CurrencySymbol}/kg")
</td>
</tr>
<tr>
<th colspan="2">Bruttosumme:</th>
<td class="number tborder"></td>
<td class="number tborder"><span class="fleft">@Model.CurrencySymbol</span>@($"{grossSum:N2}")</td>
<th class="lborder"></th>
<td colspan="2"></td>
</tr>
<tr>
<th colspan="2">Abzüge (Strafen/Pönalen, GA, ...):</th>
<td class="number">@Utils.GetSign(totalMods)</td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(totalMods):N2}")</td>
<th class="lborder tborder">Menge (ungebunden):</th>
<td colspan="2" class="number tborder">@($"{Model.Data.Rows.Sum(r => r.Ungeb.Weight):N0}") kg</td>
</tr>
<tr>
<th colspan="2">Bereits berücksichtigte Abzüge:</th>
<td class="number">@Utils.GetSign(considered)</td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(considered):N2}")</td>
<th class="lborder">Menge (gebunden):</th>
<td colspan="2" class="number">@($"{Model.Data.Rows.Sum(r => r.Geb.Weight):N0}") kg</td>
</tr>
<tr>
<th colspan="2">Auszahlungsbetrag:</th>
<td class="number tborder"></td>
<td class="number tborder"><span class="fleft">@Model.CurrencySymbol</span>@($"{totalSum:N2}")</td>
<th class="lborder">Gesamtmenge:</th>
<td colspan="2" class="number tborder">@($"{Model.Data.Rows.Sum(r => r.Ungeb.Weight + r.Geb.Weight):N0}") kg</td>
</tr>
</tbody>
</table>
<table class="payment-variant border">
<colgroup>
<col style="width: 35mm;"/>
<col style="width: 30mm;"/>
<col style="width: 25mm;"/>
<col style="width: 25mm;"/>
<col style="width: 25mm;"/>
<col style="width: 25mm;"/>
</colgroup>
<thead>
<tr class="sectionheading">
<th colspan="6">Statistik Zu-/Abschläge</th>
</tr>
<tr>
<th rowspan="2">Name</th>
<th rowspan="2">Zu-/Abschlag</th>
<th>Lieferungen</th>
<th>Minimum</th>
<th>Maximum</th>
<th>Betrag</th>
</tr>
<tr>
<th>[#]</th>
<th>[@Model.CurrencySymbol]</th>
<th>[@Model.CurrencySymbol]</th>
<th>[@Model.CurrencySymbol]</th>
</tr>
</thead>
<tbody>
@foreach (var m in Model.ModifierStat) {
var mod = Model.Modifiers[m.ModId];
<tr>
<th>@mod.Name</th>
<td class="number">@mod.ValueStr</td>
<td class="number">@($"{m.Count:N0}")</td>
<td class="number">@($"{m.Min:N2}")</td>
<td class="number">@($"{m.Max:N2}")</td>
<td class="number">@($"{m.Sum:N2}")</td>
</tr>
}
</tbody>
</table>
<table class="payment-variant-data">
<colgroup>
<col style="width: 25mm;"/>
<col style="width: 19mm;"/>
<col style="width: 18mm;"/>
<col style="width: 15mm;"/>
<col style="width: 18mm;"/>
<col style="width: 15mm;"/>
<col style="width: 18mm;"/>
<col style="width: 15mm;"/>
<col style="width: 22mm;"/>
</colgroup>
<thead>
<tr>
<th rowspan="2" style="text-align: left;">Qualitätsstufe</th>
<th>Gradation</th>
<th colspan="2">ungebunden</th>
<th colspan="2">attributlos gebunden</th>
<th colspan="2">gebunden</th>
<th>Gesamt</th>
</tr>
<tr>
<th>[@(true ? "°Oe" : "°KMW")]</th>
<th>[kg]</th>
<th>[@(Model.CurrencySymbol)/kg]</th>
<th>[kg]</th>
<th>[@(Model.CurrencySymbol)/kg]</th>
<th>[kg]</th>
<th>[@(Model.CurrencySymbol)/kg]</th>
<th>[@(Model.CurrencySymbol)]</th>
</tr>
</thead>
<tbody>
@{
string? lastHdr = null;
}
@foreach (var row in Model.Data.Rows) {
var hdr = $"{row.Variety}{(row.Attribute != null ? " / " : "")}{row.Attribute}{(row.Cultivation != null ? " / " : "")}{row.Cultivation}";
if (lastHdr != hdr) {
var rows = Model.Data.Rows
.Where(r => r.Variety == row.Variety && r.Attribute == row.Attribute && r.Cultivation == row.Cultivation)
.ToList();
<tr class="subheading @(lastHdr != null ? "new" : "")">
<th colspan="2">@hdr</th>
<td class="number">@($"{rows.Sum(r => r.Ungeb.Weight):N0}")</td>
<td></td>
<td class="number">@($"{rows.Sum(r => r.LowGeb.Weight):N0}")</td>
<td></td>
<td class="number">@($"{rows.Sum(r => r.Geb.Weight):N0}")</td>
<td></td>
<td class="number">@($"{rows.Sum(r => r.Amount):N2}")</td>
</tr>
}
<tr>
<td>@(row.QualityLevel)</td>
<td class="center">@($"{row.Oe:N0}")</td>
<td class="number">@(row.Ungeb.Weight != 0 ? $"{row.Ungeb.Weight:N0}" : "-")</td>
<td class="number">@(row.Ungeb.MaxPrice != null ? $"{row.Ungeb.MaxPrice:N4}" : "-")</td>
<td class="number">@(row.LowGeb.Weight != 0 ? $"{row.LowGeb.Weight:N0}" : "-")</td>
<td class="number">@(row.LowGeb.MaxPrice != null ? $"{row.LowGeb.MaxPrice:N4}" : "-")</td>
<td class="number">@(row.Geb.Weight != 0 ? $"{row.Geb.Weight:N0}" : "-")</td>
<td class="number">@(row.Geb.MaxPrice != null ? $"{row.Geb.MaxPrice:N4}" : "-")</td>
<td class="number">@($"{row.Amount:N2}")</td>
</tr>
lastHdr = hdr;
}
</tbody>
</table>
</main>
+21
View File
@@ -0,0 +1,21 @@
h1 {
text-align: center;
font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}
table.payment-variant {
margin-top: 10mm;
}
table.payment-variant-data {
break-before: page;
}
+1 -99
View File
@@ -1,13 +1,5 @@
using Elwig.Helpers;
using Elwig.Models.Dtos;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class WineQualityStatistics : Document {
@@ -27,99 +19,9 @@ namespace Elwig.Documents {
public WineQualityStatisticsData Data;
public bool UseOe => Data.UseOe;
public WineQualityStatistics(string filter, WineQualityStatisticsData data) :
base($"{Name} {filter}") {
public WineQualityStatistics(string filter, WineQualityStatisticsData data) : base($"{Name} {filter}") {
Filter = filter;
Data = data;
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
doc.Add(new KernedParagraph(Name, 24)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginBottomMM(2));
doc.Add(new KernedParagraph(Filter, 14)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginBottomMM(10));
foreach (var sec in Data.Sections) {
doc.Add(NewQualitySectionTable(sec).SetMarginBottomMM(5));
}
}
protected Table NewQualityColumnTable(string[] qualIds, WineQualityStatisticsData.QualitySection sec) {
var tbl = new Table(ColsMM(9.5, 10, 19.5))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetMarginsMM(1, 0, 1, 0)
.AddCell(NewCell(new KernedParagraph(UseOe ? "[°Oe]" : "[°KMW]", 8)
.SetTextAlignment(TextAlignment.CENTER).SetFont(IF)).SetPaddingsMM(1, 1, 1, 2))
.AddCell(NewCell(new KernedParagraph("[#]", 8)
.SetTextAlignment(TextAlignment.CENTER).SetFont(IF)).SetPaddingsMM(1, 1, 1, 1))
.AddCell(NewCell(new KernedParagraph("[kg]", 8)
.SetTextAlignment(TextAlignment.CENTER).SetFont(IF)).SetPaddingsMM(1, 2, 1, 1));
foreach (var qualId in qualIds) {
tbl.AddCell(NewCell(new KernedParagraph(QualityLevels.GetValueOrDefault(qualId, qualId), 10)
.SetFont(BI).SetTextAlignment(TextAlignment.CENTER), colspan: 3)
.SetPaddingsMM(2, 0, 2, 0));
foreach (var (grad, avgKmw, num, weight) in sec.Data.GetValueOrDefault(qualId, Array.Empty<(double, double, int, int)>())) {
tbl.AddCell(NewCell(new KernedParagraph(UseOe ? $"{grad:N0}" : $"{grad:N1}", 10)
.SetTextAlignment(TextAlignment.CENTER)).SetPaddingsMM(0, 0, 0, 2))
.AddCell(NewCell(new KernedParagraph($"{num:N0}", 10)
.SetTextAlignment(TextAlignment.RIGHT)).SetPaddingsMM(0, 0, 0, 0))
.AddCell(NewCell(new KernedParagraph($"{weight:N0}", 10)
.SetTextAlignment(TextAlignment.RIGHT)).SetPaddingsMM(0, 2, 0, 0));
}
}
return tbl;
}
protected Table NewQualitySumTable(double kmw, int num, int weight) {
return new Table(ColsMM(9.5, 10, 19.5))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetMarginsMM(1, 0, 1, 0)
.AddCell(NewCell(new KernedParagraph(weight == 0 ? "-" : UseOe ? $"{Utils.KmwToOe(kmw):N0}" : $"{kmw:N1}", 10)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)).SetPaddingsMM(0, 0, 0, 2))
.AddCell(NewCell(new KernedParagraph($"{num:N0}", 10)
.SetTextAlignment(TextAlignment.RIGHT).SetFont(BF)).SetPaddingsMM(0, 0, 0, 0))
.AddCell(NewCell(new KernedParagraph($"{weight:N0}", 10)
.SetTextAlignment(TextAlignment.RIGHT).SetFont(BF)).SetPaddingsMM(0, 2, 0, 0));
}
protected Table NewQualitySectionTable(WineQualityStatisticsData.QualitySection sec) {
var tbl = new Table(4)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetBorder(new SolidBorder(BorderThickness))
.SetKeepTogether(true);
var bgColor = sec.Type == "R" ? new DeviceRgb(0xff, 0xc0, 0xc0) : sec.Type == "W" ? new DeviceRgb(0xc0, 0xff, 0xc0) : new DeviceRgb(0xe0, 0xe0, 0xe0);
tbl.AddCell(NewCell(new KernedParagraph(sec.Name, 14).SetFont(BF), colspan: 4)
.SetBackgroundColor(bgColor).SetPaddingsMM(1, 2, 1, 2));
foreach (var qualIds in QualIds) {
tbl.AddCell(NewCell().SetPadding(0).Add(NewQualityColumnTable(qualIds, sec))
.SetBorder(new SolidBorder(BorderThickness)));
}
foreach (var qualIds in QualIds) {
var quals = qualIds.Select(q => sec.Data.GetValueOrDefault(q, Array.Empty<(double Grad, double AvgKmw, int Num, int Weight)>()));
var weight = quals.Sum(q => q.Sum(kv => kv.Weight));
var num = quals.Sum(q => q.Sum(kv => kv.Num));
var kmw = quals.Sum(q => q.Sum(kv => kv.AvgKmw * kv.Weight)) / weight;
tbl.AddCell(NewCell().SetPaddingsMM(0.5f, 0, 0.5f, 0).Add(NewQualitySumTable(kmw, num, weight))
.SetBorder(new SolidBorder(BorderThickness)));
}
var totalWeight = sec.Data.Values.Sum(q => q.Sum(kv => kv.Weight));
var totalNum = sec.Data.Values.Sum(q => q.Sum(kv => kv.Num));
var totalKmw = sec.Data.Values.Sum(q => q.Sum(kv => kv.AvgKmw * kv.Weight)) / totalWeight;
tbl.AddCell(NewCell(colspan: 3).SetBackgroundColor(bgColor))
.AddCell(NewCell().SetPadding(0).Add(NewQualitySumTable(totalKmw, totalNum, totalWeight))
.SetBackgroundColor(bgColor));
return tbl;
}
}
}
@@ -0,0 +1,81 @@
@using RazorLight
@using Elwig.Helpers
@inherits TemplatePage<Elwig.Documents.WineQualityStatistics>
@model Elwig.Documents.WineQualityStatistics
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\WineQualityStatistics.css" />
<main>
<h1>Qualitätsstatistik</h1>
<h2>@Model.Filter</h2>
@foreach (var sec in Model.Data.Sections) {
<table>
<colgroup>
<col style="width: 25%;"/>
<col style="width: 25%;"/>
<col style="width: 25%;"/>
<col style="width: 25%;"/>
</colgroup>
<thead>
<tr>
<th colspan="4" class="header @(sec.Type == "R" ? "red" : sec.Type == "W" ? "green" : "")">
<h3>@sec.Name</h3>
</th>
</tr>
</thead>
<tbody>
<tr>
@foreach (var qualIds in Model.QualIds) {
<td class="container">
<div class="row">
<span class="units">[@(Model.UseOe ? "°Oe" : "°KMW")]</span>
<span class="units">[#]</span>
<span class="units">[kg]</span>
</div>
@foreach (var qualId in qualIds) {
<h4>@(Model.QualityLevels.GetValueOrDefault(qualId, qualId))</h4>
@foreach (var (grad, avgKmw, num, weight) in sec.Data.GetValueOrDefault(qualId, Array.Empty<(double, double, int, int)>())) {
<div class="row">
<span class="gradation">@(Model.UseOe ? $"{grad:N0}" : $"{grad:N1}")</span>
<span class="number">@($"{num:N0}")</span>
<span class="number">@($"{weight:N0}")</span>
</div>
}
}
</td>
}
</tr>
<tr>
@foreach (var qualIds in Model.QualIds) {
var quals = qualIds.Select(q => sec.Data.GetValueOrDefault(q, Array.Empty<(double Grad, double AvgKmw, int Num, int Weight)>()));
var weight = quals.Sum(q => q.Sum(kv => kv.Weight));
var num = quals.Sum(q => q.Sum(kv => kv.Num));
var kmw = quals.Sum(q => q.Sum(kv => kv.AvgKmw * kv.Weight)) / weight;
<td class="container bold">
<div class="row">
<span class="gradation">@(weight == 0 ? "-" : Model.UseOe ? $"{Utils.KmwToOe(kmw):N0}" : $"{kmw:N1}")</span>
<span class="number">@($"{num:N0}")</span>
<span class="number">@($"{weight:N0}")</span>
</div>
</td>
}
</tr>
</tbody>
<tfoot>
<tr>
@{
var totalWeight = sec.Data.Values.Sum(q => q.Sum(kv => kv.Weight));
var totalNum = sec.Data.Values.Sum(q => q.Sum(kv => kv.Num));
var totalKmw = sec.Data.Values.Sum(q => q.Sum(kv => kv.AvgKmw * kv.Weight)) / totalWeight;
}
<td colspan="4" class="container bold footer @(sec.Type == "R" ? "red" : sec.Type == "W" ? "green" : "")">
<div class="row" style="width: 24%; margin-left: 76%;">
<span class="gradation">@(totalWeight == 0 ? "-" : Model.UseOe ? $"{Utils.KmwToOe(totalKmw):N0}" : $"{totalKmw:N1}")</span>
<span class="number">@($"{totalNum:N0}")</span>
<span class="number">@($"{totalWeight:N0}")</span>
</div>
</td>
</tr>
</tfoot>
</table>
}
</main>
+97
View File
@@ -0,0 +1,97 @@
h1 {
text-align: center;
font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}
h3 {
font-weight: bold;
font-style: normal;
font-size: 14pt;
margin: 0;
text-align: left;
}
h4 {
font-weight: bold;
font-style: italic;
font-size: 10pt;
margin: 0;
text-align: center;
margin: 2mm 0 2mm 0;
}
.row:first-child { margin-top: 0.5mm; }
.row:last-child { margin-bottom: 0.5mm; }
.bold {
font-weight: bold;
}
table {
margin-top: 10mm;
break-inside: avoid;
}
table th,
table td {
border: var(--border-thickness) solid black;
vertical-align: top !important;
}
table .header {
padding: 1mm 2mm;
}
table .header,
table .footer {
background-color: #E0E0E0;
}
table .header.red,
table .footer.red {
background-color: #FFC0C0;
}
table .header.green,
table .footer.green {
background-color: #C0FFC0;
}
.row {
display: flex;
width: 100%;
font-size: 10pt;
}
.row span {
flex: 10mm 1 1;
display: block;
padding: 0 1mm;
}
.row .units {
text-align: center;
font-size: 8pt;
font-style: italic;
padding: 1mm;
}
.gradation {
text-align: center;
}
.number {
text-align: right;
}
.row span:first-child { flex-basis: 7.5mm; }
.row span:last-child { flex-basis: 17.5mm; }
+18 -20
View File
@@ -2,17 +2,15 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<PreserveCompilationContext>true</PreserveCompilationContext>
<ApplicationIcon>Resources\Images\Elwig.ico</ApplicationIcon>
<Version>1.1.2.2</Version>
<Version>0.13.9</Version>
<SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ApplicationManifest>App.manifest</ApplicationManifest>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
@@ -23,21 +21,21 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="bblanchon.PDFium.Win32" Version="156.0.8076" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="itext" Version="9.8.0" />
<PackageReference Include="itext.bouncy-castle-adapter" Version="9.8.0" />
<PackageReference Include="LinqKit" Version="1.3.11" />
<PackageReference Include="MailKit" Version="4.18.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.12" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="10.0.12" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.4258.31" />
<PackageReference Include="NJsonSchema" Version="11.6.1" />
<PackageReference Include="ScottPlot.WPF" Version="5.1.59" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.5" />
<PackageReference Include="System.IO.Hashing" Version="10.0.12" />
<PackageReference Include="System.IO.Ports" Version="10.0.12" />
<PackageReference Include="System.Management" Version="10.0.12" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="LinqKit" Version="1.3.8" />
<PackageReference Include="MailKit" Version="4.12.0" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.36" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="9.0.4" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
<PackageReference Include="NJsonSchema" Version="11.3.2" />
<PackageReference Include="PdfiumViewer" Version="2.13.0" />
<PackageReference Include="PdfiumViewer.Native.x86_64.no_v8-no_xfa" Version="2018.4.8.256" />
<PackageReference Include="RazorLight" Version="2.3.1" />
<PackageReference Include="ScottPlot.WPF" Version="5.0.55" />
<PackageReference Include="System.IO.Ports" Version="9.0.4" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.4" />
</ItemGroup>
</Project>
+107 -286
View File
@@ -1,22 +1,21 @@
using Elwig.Models;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using Elwig.Services;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using Elwig.Models.Entities;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.IO;
using System;
using System.Windows;
using Microsoft.Extensions.Logging;
using Microsoft.Data.Sqlite;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using Elwig.Models.Dtos;
using System.Reflection;
using System.Data;
namespace Elwig.Helpers {
public record struct MemberHistoryPoint(int Shares, int SharesRed, int SharesWhite, int SharesDormant);
public record struct AreaCmtBucket(int Area, int Obligation, int Right);
public record struct AreaComBucket(int Area, int Obligation, int Right);
public record struct UnderDelivery(int Weight, int Diff);
public record struct MemberBucket(string Name, int Area, int Obligation, int Right, int Delivery, int DeliveryStrict, int DeliveryTotal, int Payment);
public record struct MemberStat(string Variety, string Discr, int Weight);
@@ -44,14 +43,13 @@ namespace Elwig.Helpers {
public DbSet<WineAttr> WineAttributes { get; private set; }
public DbSet<WineCult> WineCultivations { get; private set; }
public DbSet<Branch> Branches { get; private set; }
public DbSet<AreaCmtType> AreaCommitmentTypes { get; private set; }
public DbSet<AreaComType> AreaCommitmentTypes { get; private set; }
public DbSet<Member> Members { get; private set; }
public DbSet<BillingAddr> BillingAddresses { get; private set; }
public DbSet<MemberTelNr> MemberTelephoneNrs { get; private set; }
public DbSet<MemberEmailAddr> MemberEmailAddrs { get; private set; }
public DbSet<MemberHistory> MemberHistory { get; private set; }
public DbSet<AreaCmtContract> AreaCommitmentContracts { get; private set; }
public DbSet<AreaCmt> AreaCommitments { get; private set; }
public DbSet<AreaCom> AreaCommitments { get; private set; }
public DbSet<Season> Seasons { get; private set; }
public DbSet<DeliverySchedule> DeliverySchedules { get; private set; }
public DbSet<DeliveryScheduleWineVar> DeliveryScheduleWineVarieties { get; private set; }
@@ -68,10 +66,9 @@ namespace Elwig.Helpers {
public DbSet<DeliveryPartBucket> DeliveryPartBuckets { get; private set; }
public DbSet<OverUnderDeliveryRow> OverUnderDeliveryRows { get; private set; }
public DbSet<AreaCmtUnderDeliveryRowSingle> AreaCmtUnderDeliveryRows { get; private set; }
public DbSet<MemberDeliveryPerVarietyRowSingle> MemberDeliveryPerVariantRows { get; private set; }
public DbSet<MemberAreaCmtTotalsRowSingle> MemberAreaCmtTotalsRows { get; private set; }
public DbSet<MemberAreaCmtsRow> MemberAreaCmtsRows { get; private set; }
public DbSet<AreaComUnderDeliveryRowSingle> AreaComUnderDeliveryRows { get; private set; }
public DbSet<MemberDeliveryPerVariantRowSingle> MemberDeliveryPerVariantRows { get; private set; }
public DbSet<MemberAreaComsRowSingle> MemberAreaComsRows { get; private set; }
public DbSet<CreditNoteDeliveryRowSingle> CreditNoteDeliveryRows { get; private set; }
public DbSet<CreditNoteRowSingle> CreditNoteRows { get; private set; }
public DbSet<WeightBreakdownRow> WeightBreakDownRows { get; private set; }
@@ -85,72 +82,7 @@ namespace Elwig.Helpers {
public static string? ConnectionStringOverride { get; set; } = null;
public static string ConnectionString => ConnectionStringOverride ?? $"Data Source=\"{App.Config.DatabaseFile}\"; Mode=ReadWrite; Foreign Keys=True; Cache=Default; Pooling=False";
private static readonly Func<AppDbContext, string?, bool, IAsyncEnumerable<Branch>> _compiledQueryBranches =
EF.CompileAsyncQuery<AppDbContext, string?, bool, Branch>((ctx, zwstid, includeWithoutMembers) => ctx.Branches
.Where(b => includeWithoutMembers || b.Members.Count > 0)
.Where(b => zwstid == null || b.ZwstId == zwstid)
.Include(b => b.PostalDest)
.OrderBy(b => b.Name));
private static readonly Func<AppDbContext, string?, IAsyncEnumerable<WineVar>> _compiledQueryWineVarieties =
EF.CompileAsyncQuery<AppDbContext, string?, WineVar>((ctx, sortid) => ctx.WineVarieties
.Where(v => sortid == null || v.SortId == sortid)
.OrderBy(v => v.Name));
private static readonly Func<AppDbContext, string?, bool, IAsyncEnumerable<WineAttr>> _compiledQueryWineAttributes =
EF.CompileAsyncQuery<AppDbContext, string?, bool, WineAttr>((ctx, attrid, includeNotActive) => ctx.WineAttributes
.Where(a => includeNotActive || a.IsActive)
.Where(a => attrid == null || a.AttrId == attrid)
.OrderBy(a => a.Name));
private static readonly Func<AppDbContext, string?, IAsyncEnumerable<WineCult>> _compiledQueryWineCultivations =
EF.CompileAsyncQuery<AppDbContext, string?, WineCult>((ctx, cultid) => ctx.WineCultivations
.Where(c => cultid == null || c.CultId == cultid)
.OrderBy(v => v.Name));
private static readonly Func<AppDbContext, bool, IAsyncEnumerable<WineQualLevel>> _compiledQueryWineQualityLevels =
EF.CompileAsyncQuery<AppDbContext, bool, WineQualLevel>((ctx, includePredicate) => ctx.WineQualityLevels
.Where(l => includePredicate || !l.IsPredicate)
.OrderBy(l => l.MinKmw));
private static readonly Func<AppDbContext, int?, bool, IAsyncEnumerable<Modifier>> _compiledQueryModifiers =
EF.CompileAsyncQuery<AppDbContext, int?, bool, Modifier>((ctx, year, incudeNotActive) => ctx.Modifiers
.Where(m => (year == null || m.Year == year) && (incudeNotActive || m.IsActive))
.OrderBy(m => m.Year).ThenBy(m => m.Ordering).ThenBy(m => m.Name));
private static readonly Func<AppDbContext, int?, bool, IAsyncEnumerable<Member>> _compiledQueryMembers =
EF.CompileAsyncQuery<AppDbContext, int?, bool, Member>((ctx, mgnr, includeNotActive) => ctx.Members
.Where(m => includeNotActive || m.IsActive)
.Where(m => mgnr == null || m.MgNr == mgnr)
.OrderBy(m => m.Name).ThenBy(m => m.GivenName).ThenBy(m => m.MgNr));
private static readonly Func<AppDbContext, int?, bool, IAsyncEnumerable<Member>> _compiledQueryMembersContactInfo =
EF.CompileAsyncQuery<AppDbContext, int?, bool, Member>((ctx, mgnr, includeNotActive) => ctx.Members
.Where(m => includeNotActive || m.IsActive)
.Where(m => mgnr == null || m.MgNr == mgnr)
.Include(m => m.EmailAddresses)
.Include(m => m.TelephoneNumbers)
.OrderBy(m => m.Name).ThenBy(m => m.GivenName).ThenBy(m => m.MgNr)
.AsSplitQuery());
private static readonly Func<AppDbContext, int?, IAsyncEnumerable<AreaCmt>> _compiledQueryAreaCommitments =
EF.CompileAsyncQuery<AppDbContext, int?, AreaCmt>((ctx, fbnr) => ctx.AreaCommitments
.Where(c => fbnr == null || c.FbNr == fbnr)
.OrderBy(c => c.FbNr).ThenBy(c => c.RevNr));
private static readonly Func<AppDbContext, int?, IAsyncEnumerable<Season>> _compiledQuerySeasons =
EF.CompileAsyncQuery<AppDbContext, int?, Season>((ctx, year) => ctx.Seasons
.Where(s => year == null || s.Year == year)
.OrderByDescending(s => s.Year));
private static readonly Func<AppDbContext, int?, IAsyncEnumerable<Season>> _compiledQuerySeasonsModifiers =
EF.CompileAsyncQuery<AppDbContext, int?, Season>((ctx, year) => ctx.Seasons
.Where(s => year == null || s.Year == year)
.Include(s => s.Modifiers)
.OrderByDescending(s => s.Year));
private readonly Dictionary<int, Dictionary<int, MemberHistoryPoint>> _memberHistory = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, AreaCmtBucket>>> _memberAreaCommitmentBuckets = [];
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 = [];
@@ -163,92 +95,66 @@ namespace Elwig.Helpers {
LogFile = new(file) {
AutoFlush = true
};
} catch (Exception exc) {
InteractionService.ShowException("Database Log", $"Unable to open database log file", exc);
} catch (Exception e) {
MessageBox.Show($"Unable to open database log file:\n\n{e.Message}", "Database Log", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
SavedLastWriteTime = LastWriteTime;
SavedChanges += OnSavedChanges;
}
private static void CreateFunctions(SqliteConnection cnx) {
cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => {
return value == null ? null : Regex.IsMatch(value, pattern);
}, true);
cnx.CreateFunction<string?, string?, string?>("NORMALIZE_ADDRESS", (address, locality) => {
return string.IsNullOrWhiteSpace(address) ? null : Normalizer.NormalizeAddress(address, locality);
}, true);
}
public static SqliteConnection Connect(string? connectionString = null) {
var cnx = new SqliteConnection(connectionString ?? ConnectionString);
CreateFunctions(cnx);
cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => value == null ? null : Regex.Match(value, pattern).Success, true);
cnx.Open();
return cnx;
}
public static async Task<SqliteConnection> ConnectAsync(string? connectionString = null) {
var cnx = new SqliteConnection(connectionString ?? ConnectionString);
CreateFunctions(cnx);
cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => value == null ? null : Regex.Match(value, pattern).Success, true);
await cnx.OpenAsync();
return cnx;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
optionsBuilder.UseSqlite(ConnectionString);
optionsBuilder.LogTo(Log, LogLevel.Information);
base.OnConfiguring(optionsBuilder);
public static async Task ExecuteBatch(SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
await (await cmd.ExecuteReaderAsync()).CloseAsync();
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<WbKg>().Navigation(k => k.AtKg).AutoInclude();
modelBuilder.Entity<WbKg>().Navigation(k => k.Gl).AutoInclude();
modelBuilder.Entity<AT_Kg>().Navigation(k => k.Gem).AutoInclude();
modelBuilder.Entity<PostalDest>().Navigation(p => p.Country).AutoInclude();
modelBuilder.Entity<PostalDest>().Navigation(p => p.AtPlz).AutoInclude();
modelBuilder.Entity<AT_PlzDest>().Navigation(p => p.AtPlz).AutoInclude();
modelBuilder.Entity<AT_PlzDest>().Navigation(p => p.Ort).AutoInclude();
modelBuilder.Entity<Member>().Navigation(m => m.Branch).AutoInclude();
modelBuilder.Entity<Member>().Navigation(m => m.DefaultWbKg).AutoInclude();
modelBuilder.Entity<Member>().Navigation(m => m.Country).AutoInclude();
modelBuilder.Entity<Member>().Navigation(m => m.PostalDest).AutoInclude();
modelBuilder.Entity<Member>().Navigation(m => m.BillingAddress).AutoInclude();
modelBuilder.Entity<BillingAddr>().Navigation(a => a.Country).AutoInclude();
modelBuilder.Entity<BillingAddr>().Navigation(a => a.PostalDest).AutoInclude();
modelBuilder.Entity<Modifier>().Navigation(m => m.Season).AutoInclude();
modelBuilder.Entity<Season>().Navigation(s => s.Currency).AutoInclude();
modelBuilder.Entity<PaymentVar>().Navigation(v => v.Season).AutoInclude();
modelBuilder.Entity<PaymentDeliveryPart>().Navigation(p => p.Variant).AutoInclude();
modelBuilder.Entity<Credit>().Navigation(c => c.Payment).AutoInclude();
modelBuilder.Entity<Delivery>().Navigation(d => d.Member).AutoInclude();
modelBuilder.Entity<Delivery>().Navigation(d => d.Season).AutoInclude();
modelBuilder.Entity<Delivery>().Navigation(d => d.Branch).AutoInclude();
modelBuilder.Entity<DeliveryPart>().Navigation(p => p.Quality).AutoInclude();
modelBuilder.Entity<DeliveryPart>().Navigation(p => p.Variety).AutoInclude();
modelBuilder.Entity<DeliveryPart>().Navigation(p => p.Attribute).AutoInclude();
modelBuilder.Entity<DeliveryPart>().Navigation(p => p.Cultivation).AutoInclude();
modelBuilder.Entity<DeliveryPart>().Navigation(p => p.Kg).AutoInclude();
modelBuilder.Entity<DeliveryPart>().Navigation(p => p.Rd).AutoInclude();
modelBuilder.Entity<DeliveryPartModifier>().Navigation(m => m.Modifier).AutoInclude();
modelBuilder.Entity<AreaCmtContract>().Navigation(c => c.Kg).AutoInclude();
modelBuilder.Entity<AreaCmtContract>().Navigation(c => c.Rd).AutoInclude();
modelBuilder.Entity<AreaCmt>().Navigation(c => c.Contract).AutoInclude();
modelBuilder.Entity<AreaCmt>().Navigation(c => c.WineCult).AutoInclude();
modelBuilder.Entity<AreaCmt>().Navigation(c => c.AreaCmtType).AutoInclude();
modelBuilder.Entity<AreaCmtType>().Navigation(c => c.WineVar).AutoInclude();
modelBuilder.Entity<AreaCmtType>().Navigation(c => c.WineAttr).AutoInclude();
modelBuilder.Entity<PaymentMember>().Navigation(c => c.Credit).AutoInclude();
modelBuilder.Entity<PaymentMember>().Navigation(c => c.Member).AutoInclude();
modelBuilder.Entity<PaymentMember>().Navigation(c => c.Variant).AutoInclude();
modelBuilder.Entity<DeliveryAncmt>().Navigation(a => a.Member).AutoInclude();
modelBuilder.Entity<DeliveryAncmt>().Navigation(a => a.Schedule).AutoInclude();
modelBuilder.Entity<DeliveryAncmt>().Navigation(a => a.Variety).AutoInclude();
modelBuilder.Entity<DeliverySchedule>().Navigation(s => s.Branch).AutoInclude();
modelBuilder.Entity<DeliverySchedule>().Navigation(s => s.Attribute).AutoInclude();
modelBuilder.Entity<DeliverySchedule>().Navigation(s => s.Cultivation).AutoInclude();
modelBuilder.Entity<MemberHistory>().Navigation(s => s.FromMember).AutoInclude();
modelBuilder.Entity<MemberHistory>().Navigation(s => s.ToMember).AutoInclude();
modelBuilder.Entity<MemberHistory>().Navigation(s => s.Currency).AutoInclude();
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();
}
public static async Task<(string Table, long RowId, string Parent, long FkId)[]> ForeignKeyCheck(SqliteConnection cnx) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = "PRAGMA foreign_key_check";
using var reader = await cmd.ExecuteReaderAsync();
var list = new List<(string, long, string, long)>();
while (await reader.ReadAsync()) {
var table = reader.GetString(0);
var rowid = reader.GetInt64(1);
var parent = reader.GetString(2);
var fkid = reader.GetInt64(3);
list.Add((table, rowid, parent, fkid));
}
return [.. list];
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
optionsBuilder.UseSqlite(ConnectionString);
optionsBuilder.UseLazyLoadingProxies();
optionsBuilder.LogTo(Log, LogLevel.Information);
base.OnConfiguring(optionsBuilder);
}
public override void Dispose() {
@@ -266,23 +172,23 @@ namespace Elwig.Helpers {
}
public async Task<bool> MgNrExists(int mgnr) {
return await _compiledQueryMembers.Invoke(this, mgnr, true).AnyAsync();
return await Members.FindAsync(mgnr) != null;
}
public async Task<bool> FbNrExists(int fbnr) {
return await _compiledQueryAreaCommitments.Invoke(this, fbnr).AnyAsync();
return await AreaCommitments.FindAsync(fbnr) != null;
}
public async Task<bool> SortIdExists(string sortId) {
return await _compiledQueryWineVarieties.Invoke(this, sortId).AnyAsync();
return await WineVarieties.FindAsync(sortId) != null;
}
public async Task<bool> AttrIdExists(string attrId) {
return await _compiledQueryWineAttributes.Invoke(this, attrId, true).AnyAsync();
return await WineAttributes.FindAsync(attrId) != null;
}
public async Task<bool> CultIdExists(string cultId) {
return await _compiledQueryWineCultivations.Invoke(this, cultId).AnyAsync();
return await WineCultivations.FindAsync(cultId) != null;
}
public async Task<int> NextMgNr() {
@@ -294,117 +200,94 @@ namespace Elwig.Helpers {
public async Task<int> NextFbNr() {
int c = 0;
(await AreaCommitmentContracts.OrderBy(ac => ac.FbNr).Select(ac => ac.FbNr).ToListAsync())
(await AreaCommitments.OrderBy(ac => ac.FbNr).Select(ac => ac.FbNr).ToListAsync())
.ForEach(a => { if (a <= c + 10000) c = a; });
return c + 1;
}
public async Task<int> NextRevNr(int fbnr) {
return (await AreaCommitments.Where(c => c.FbNr == fbnr).Select(c => (int?)c.RevNr).MaxAsync() ?? 0) + 1;
}
public async Task<int> NextLNr(DateOnly date, string zwstid) {
var dateStr = date.ToString("yyyy-MM-dd");
return (await Deliveries.Where(d => d.DateString == dateStr && d.ZwstId == zwstid).Select(d => (int?)d.LNr).MaxAsync() ?? 0) + 1;
int c = 0;
(await Deliveries.Where(d => d.DateString == dateStr && d.ZwstId == zwstid).Select(d => d.LNr).ToListAsync())
.ForEach(a => { if (a <= c + 100) c = a; });
return c + 1;
}
public async Task<int> NextDId(int year) {
return (await Deliveries.Where(d => d.Year == year).Select(d => (int?)d.DId).MaxAsync() ?? 0) + 1;
int c = 0;
(await Deliveries.Where(d => d.Year == year).Select(d => d.DId).ToListAsync())
.ForEach(a => { if (a <= c + 100) c = a; });
return c + 1;
}
public async Task<int> NextDPNr(int year, int did) {
return (await DeliveryParts.Where(p => p.Year == year && p.DId == did).Select(p => (int?)p.DPNr).MaxAsync() ?? 0) + 1;
int c = 0;
(await DeliveryParts.Where(p => p.Year == year && p.DId == did).Select(d => d.DPNr).ToListAsync())
.ForEach(a => { if (a <= c + 100) c = a; });
return c + 1;
}
public async Task<int> NextRdNr(int kgnr) {
return (await WbRde.Where(r => r.KgNr == kgnr).Select(r => (int?)r.RdNr).MaxAsync() ?? 0) + 1;
int c = 0;
(await WbRde.Where(r => r.KgNr == kgnr).Select(r => r.RdNr).ToListAsync())
.ForEach(a => { if (a <= c + 100) c = a; });
return c + 1;
}
public async Task<int> NextAvNr(int year) {
return (await PaymentVariants.Where(v => v.Year == year).Select(v => (int?)v.AvNr).MaxAsync() ?? 0) + 1;
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<int> NextDsNr(int year) {
return (await DeliverySchedules.Where(s => s.Year == year).Select(v => (int?)v.DsNr).MaxAsync() ?? 0) + 1;
int c = 0;
(await DeliverySchedules.Where(s => s.Year == year).Select(s => s.DsNr).ToListAsync())
.ForEach(a => { if (a <= c + 100) c = a; });
return c + 1;
}
public async Task<int> NextHistNr() {
return (await MemberHistory.Select(c => (int?)c.HistNr).MaxAsync() ?? 0) + 1;
public async Task<WineQualLevel> GetWineQualityLevel(double kmw) {
return await WineQualityLevels
.Where(q => !q.IsPredicate && (q.MinKmw == null || q.MinKmw <= kmw))
.OrderBy(q => q.MinKmw)
.LastAsync();
}
public IAsyncEnumerable<Branch> FetchBranches(string? zwstid = null, bool includeWithoutMembers = true) {
return _compiledQueryBranches.Invoke(this, zwstid, includeWithoutMembers);
}
public IAsyncEnumerable<WineVar> FetchWineVarieties() {
return _compiledQueryWineVarieties.Invoke(this, null);
}
public IAsyncEnumerable<WineAttr> FetchWineAttributes(bool incudeNotActive = true) {
return _compiledQueryWineAttributes.Invoke(this, null, incudeNotActive);
}
public IAsyncEnumerable<WineCult> FetchWineCultivations() {
return _compiledQueryWineCultivations.Invoke(this, null);
}
public IAsyncEnumerable<WineQualLevel> FetchWineQualityLevels(bool includePredicate = true) {
return _compiledQueryWineQualityLevels.Invoke(this, includePredicate);
}
public IAsyncEnumerable<Modifier> FetchModifiers(int? year, bool incudeNotActive = true) {
return _compiledQueryModifiers.Invoke(this, year, incudeNotActive);
}
public IAsyncEnumerable<Member> FetchMembers(int? mgnr = null, bool includeNotActive = false, bool includeContactInfo = false) {
if (includeContactInfo) {
return _compiledQueryMembersContactInfo.Invoke(this, mgnr, mgnr != null || includeNotActive);
} else {
return _compiledQueryMembers.Invoke(this, mgnr, mgnr != null || includeNotActive);
}
}
public IAsyncEnumerable<Season> FetchSeasons(int? year = null, bool includeModifiers = false) {
if (includeModifiers) {
return _compiledQuerySeasonsModifiers.Invoke(this, year);
} else {
return _compiledQuerySeasons.Invoke(this, year);
}
}
public async Task UpdateDeliveryPartModifiers(DeliveryPart part, IEnumerable<string> oldModIds, IEnumerable<string> newModIds) {
foreach (var m in await FetchModifiers(part.Year).ToListAsync()) {
public void UpdateDeliveryPartModifiers(DeliveryPart part, IEnumerable<Modifier> oldModifiers, IEnumerable<Modifier> newModifiers) {
foreach (var m in Modifiers.Where(m => m.Year == part.Year)) {
var mod = new DeliveryPartModifier {
Year = part.Year,
DId = part.DId,
DPNr = part.DPNr,
ModId = m.ModId,
};
var old = oldModIds.Contains(m.ModId);
if (newModIds.Contains(m.ModId)) {
if (!old) {
var old = oldModifiers.Where(pa => pa.ModId == m.ModId).FirstOrDefault();
if (newModifiers.Any(md => md.ModId == m.ModId)) {
if (old == null) {
Add(mod);
} else {
Update(mod);
}
} else {
if (old) {
if (old != null) {
Remove(mod);
}
}
}
}
public async Task UpdateDeliveryScheduleWineVarieties(DeliverySchedule schedule, IEnumerable<(string, int)> oldVarieties, IEnumerable<(string, int)> newVarieties) {
foreach (var v in await FetchWineVarieties().ToArrayAsync()) {
public void UpdateDeliveryScheduleWineVarieties(DeliverySchedule schedule, IEnumerable<(WineVar, int)> oldVarieties, IEnumerable<(WineVar, int)> newVarieties) {
foreach (var v in WineVarieties) {
var e = new DeliveryScheduleWineVar {
Year = schedule.Year,
DsNr = schedule.DsNr,
SortId = v.SortId,
Priority = 1,
};
var o = oldVarieties.Where(x => x.Item1 == e.SortId).Select(x => x.Item2).FirstOrDefault(-1);
var n = newVarieties.Where(x => x.Item1 == e.SortId).Select(x => x.Item2).FirstOrDefault(-1);
var o = oldVarieties.Where(x => x.Item1.SortId == e.SortId).Select(x => x.Item2).FirstOrDefault(-1);
var n = newVarieties.Where(x => x.Item1.SortId == e.SortId).Select(x => x.Item2).FirstOrDefault(-1);
if (n != -1) {
e.Priority = n;
if (o == -1) {
@@ -420,26 +303,10 @@ namespace Elwig.Helpers {
}
}
private async Task FetchMemberHistory(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var history = new Dictionary<int, MemberHistoryPoint>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, shares, shares_red, shares_white, shares_dormant FROM v_member_history WHERE year = {year}";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
history[mgnr] = new(reader.GetInt32(1), reader.GetInt32(2), reader.GetInt32(3), reader.GetInt32(4));
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberHistory[year] = history;
}
private async Task FetchMemberAreaCommitmentBuckets(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var buckets = new Dictionary<int, Dictionary<string, AreaCmtBucket>>();
var buckets = new Dictionary<int, Dictionary<string, AreaComBucket>>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, bucket, area, min_kg, max_kg FROM v_area_commitment_bucket WHERE year = {year}";
using var reader = await cmd.ExecuteReaderAsync();
@@ -526,51 +393,7 @@ namespace Elwig.Helpers {
_memberUnderDelivery[year] = buckets;
}
public async Task<MemberHistoryPoint> GetMemberHistory(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberHistory.ContainsKey(year))
await FetchMemberHistory(year, cnx);
return _memberHistory[year].GetValueOrDefault(mgnr, new());
}
public async Task<Dictionary<int, SortedDictionary<DateOnly, MemberHistoryPoint>>> GetMemberHistory() {
var today = DateOnly.FromDateTime(DateTime.Today).AddDays(1);
var history = new Dictionary<int, SortedDictionary<DateOnly, MemberHistoryPoint>>();
var members = await Members.ToListAsync();
foreach (var m in members) {
history[m.MgNr] = new() {
[today] = new(m.Shares, m.SharesRed, m.SharesWhite, m.SharesDormant),
};
}
var hist = await MemberHistory.OrderByDescending(h => h.DateNoticeString).ToListAsync();
foreach (var h in hist) {
if (h.FromMgNr is int from) {
var latest = history[from].Last().Value;
switch (h.FromType) {
case BusinessShareType.Normal: latest.Shares += h.Shares; break;
case BusinessShareType.Red: latest.SharesRed += h.Shares; break;
case BusinessShareType.White: latest.SharesWhite += h.Shares; break;
case BusinessShareType.Dormant: latest.SharesDormant += h.Shares; break;
}
history[from][h.DateNotice] = latest;
}
if (h.ToMgNr is int to) {
var latest = history[to].Last().Value;
switch (h.ToType) {
case BusinessShareType.Normal: latest.Shares -= h.Shares; break;
case BusinessShareType.Red: latest.SharesRed -= h.Shares; break;
case BusinessShareType.White: latest.SharesWhite -= h.Shares; break;
case BusinessShareType.Dormant: latest.SharesDormant -= h.Shares; break;
}
history[to][h.DateNotice] = latest;
}
}
return history;
}
public async Task<Dictionary<string, AreaCmtBucket>> GetMemberAreaCommitmentBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
public async Task<Dictionary<string, AreaComBucket>> GetMemberAreaCommitmentBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberAreaCommitmentBuckets.ContainsKey(year))
await FetchMemberAreaCommitmentBuckets(year, cnx);
return _memberAreaCommitmentBuckets[year].GetValueOrDefault(mgnr, []);
@@ -609,12 +432,10 @@ namespace Elwig.Helpers {
var paymentBuckets = await GetMemberPaymentBuckets(year, mgnr, cnx);
if (ownCnx) await cnx.DisposeAsync();
var varieties = await WineVarieties.ToDictionaryAsync(v => v.SortId);
var attributes = await WineAttributes.ToDictionaryAsync(a => a.AttrId);
var buckets = new Dictionary<string, MemberBucket>();
foreach (var id in rightsAndObligations.Keys.Union(deliveryBuckets.Keys).Union(paymentBuckets.Keys)) {
var variety = varieties.GetValueOrDefault(id[..2]);
var attribute = attributes.GetValueOrDefault(id[2..]);
var variety = await WineVarieties.FindAsync(id[..2]);
var attribute = await WineAttributes.FindAsync(id[2..]);
var name = (variety?.Name ?? "") + (id[2..] == "_" ? " (kein Qual.Wein)" : attribute != null ? $" ({attribute})" : "");
buckets[id] = new(
name,
+37 -112
View File
@@ -1,59 +1,44 @@
using Elwig.Services;
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Elwig.Helpers {
public static class AppDbUpdater {
// Don't forget to update value in Tests/fetch-resources.bat!
public static readonly int RequiredSchemaVersion = 42;
public static readonly int RequiredSchemaVersion = 32;
private static int VersionOffset = 0;
public static async Task<Version> CheckDb() {
if (!File.Exists(App.Config.DatabaseFile))
throw new FileNotFoundException($"Die Datei \"{App.Config.DatabaseFile}\" exisitiert nicht");
using var cnx = AppDbContext.Connect();
long? applId, schemaVers;
using (var cnx = await AppDbContext.ConnectAsync()) {
var applId = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA application_id") ?? 0;
if (applId != 0x454C5747) throw new Exception($"Invalid application_id in database (0x{applId:X08})");
await cnx.QuickCheck();
applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
if (applId != 0x454C5747)
throw new FileFormatException($"Invalid application_id in database (0x{applId:X08})");
schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0;
VersionOffset = (int)(schemaVers % 100);
if (VersionOffset != 0) {
// schema was modified manually/externally
// TODO issue warning
}
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
}
await UpdateDbSchema(cnx, (int)(schemaVers / 100), RequiredSchemaVersion);
await UpdateDbSchema((int)(schemaVers / 100), RequiredSchemaVersion);
var userVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA user_version") ?? 0;
var v = new Version((int)(userVers >> 24), (int)((userVers >> 16) & 0xFF), (int)(userVers & 0xFFFF));
Version v;
using (var cnx = await AppDbContext.ConnectAsync()) {
var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version") ?? 0;
v = new Version((int)(userVers >> 24), (int)((userVers >> 16) & 0xFF), (int)((userVers >> 8) & 0xFF), (int)(userVers & 0xFF));
if (App.Version > v) {
long vers = (App.Version.Major << 24) | (App.Version.Minor << 16) | (App.Version.Build << 8) | App.Version.Revision;
await cnx.ExecuteBatch($"PRAGMA user_version = {vers}");
}
if (App.Version > v) {
long vers = (App.Version.Major << 24) | (App.Version.Minor << 16) | App.Version.Build;
await AppDbContext.ExecuteBatch(cnx, $"PRAGMA user_version = {vers}");
}
return v;
}
private static async Task UpdateDbSchema(int fromVersion, int toVersion) {
private static async Task UpdateDbSchema(SqliteConnection cnx, int fromVersion, int toVersion) {
if (fromVersion == toVersion) {
return;
} else if (fromVersion > toVersion) {
@@ -63,103 +48,43 @@ namespace Elwig.Helpers {
}
var asm = Assembly.GetExecutingAssembly();
(int From, int To, string Name)[] scripts = [.. asm.GetManifestResourceNames()
(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)];
.OrderBy(s => s.Item1).ThenBy(s => s.Item2)
.ToArray();
List<(int ToVersion, string File)> toExecute = [];
List<string> toExecute = [];
var vers = fromVersion;
while (vers < toVersion) {
var (_, to, name) = scripts.Last(s => s.From == vers);
toExecute.Add((to, name));
var (_, to, name) = scripts.Where(s => s.From == vers).Last();
toExecute.Add(name);
vers = to;
}
if (toExecute.Count == 0)
return;
var backup = Path.ChangeExtension(App.Config.DatabaseFile, $".v{fromVersion}.sqlite3");
File.Copy(App.Config.DatabaseFile, backup, true);
try {
using var cnx = await AppDbContext.ConnectAsync();
await cnx.ExecuteBatch("PRAGMA locking_mode = EXCLUSIVE");
await cnx.IntegrityCheck();
await cnx.ForeignKeyCheck();
foreach (var (to, script) in toExecute) {
await cnx.ExecuteEmbeddedScript(asm, script);
if (to == 42) {
await UpdateDbSchema_41_To_42(cnx);
}
}
await cnx.IntegrityCheck();
await cnx.ForeignKeyCheck();
await cnx.ExecuteBatch("VACUUM");
await cnx.ExecuteBatch($"PRAGMA schema_version = {toVersion * 100 + VersionOffset}");
} catch (Exception) {
File.Move(backup, App.Config.DatabaseFile, true);
throw;
} finally {
File.Delete(backup);
await AppDbContext.ExecuteBatch(cnx, """
PRAGMA locking_mode = EXCLUSIVE;
BEGIN EXCLUSIVE;
""");
foreach (var script in toExecute) {
await AppDbContext.ExecuteEmbeddedScript(cnx, asm, script);
}
}
private static async Task UpdateDbSchema_41_To_42(SqliteConnection cnx) {
List<(int MgNr, string LfbisNr, string Name, string Address, int Plz)> members = [];
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = """
SELECT m.mgnr, m.lfbis_nr,
COALESCE(a.name, (COALESCE(m.prefix || ' ', '') || COALESCE(m.given_name || ' ', '') || m.name || COALESCE(' ' || m.middle_names, '') || COALESCE(' ' || m.suffix, ''))),
COALESCE(a.address, m.address),
COALESCE(aplz.plz, mplz.plz)
FROM member m
JOIN postal_dest mp ON (mp.country, mp.id) = (m.country, m.postal_dest)
JOIN AT_plz_dest ma ON (ma.country, ma.id) = (mp.country, mp.id)
JOIN AT_plz mplz ON mplz.plz = ma.plz
LEFT JOIN member_billing_address a ON a.mgnr = m.mgnr
LEFT JOIN postal_dest ap ON (ap.country, ap.id) = (a.country, a.postal_dest)
LEFT JOIN AT_plz_dest aa ON (aa.country, aa.id) = (ap.country, mp.id)
LEFT JOIN AT_plz aplz ON aplz.plz = aa.plz
WHERE organic
""";
using var reader = await cmd.ExecuteReaderAsync();
var header = await reader.GetColumnSchemaAsync();
while (await reader.ReadAsync()) {
members.Add((reader.GetInt32(0), reader.GetString(1), reader.GetString(2), reader.GetString(3), reader.GetInt32(4)));
}
var violations = await AppDbContext.ForeignKeyCheck(cnx);
if (violations.Length > 0) {
throw new Exception($"Foreign key violations ({violations.Length}):\n" + string.Join("\n", violations
.Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}")));
}
if (members.Count > 0)
InteractionService.ShowInformation("Datenbank-Update", "Es wird versucht, die Bio-Kontrollstellen-Informationen der Mitglieder automatisch abzurufen. Dies könnte einen kurzen Moment dauern.");
try {
Mouse.OverrideCursor = Cursors.Wait;
foreach (var m in members) {
string? oocId = null;
string? authCode = null;
var certs = await OrganicService.FetchTracesCertificates(lfbisNr: m.LfbisNr, address: (m.Name, m.Address, m.Plz.ToString()));
if (certs.Length > 0) {
oocId = certs[0].OperatorId;
authCode = certs[0].AuthorityCode;
}
if (oocId != null || authCode != null) {
oocId = oocId == null ? "NULL" : $"'{oocId}'";
authCode = authCode == null ? "NULL" : $"'{authCode}'";
await cnx.ExecuteBatch($"UPDATE member SET ooc_id = {oocId}, org_auth_code = {authCode} WHERE mgnr = {m.MgNr}");
}
}
} finally {
Mouse.OverrideCursor = null;
}
await AppDbContext.ExecuteBatch(cnx, $"""
COMMIT;
VACUUM;
PRAGMA schema_version = {toVersion * 100 + VersionOffset};
""");
}
}
}
+49 -49
View File
@@ -1,5 +1,6 @@
using Elwig.Models.Entities;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -13,77 +14,76 @@ namespace Elwig.Helpers.Billing {
protected readonly Season Season;
protected readonly Dictionary<string, string> Attributes;
protected readonly Dictionary<string, (decimal?, decimal?)> Modifiers;
protected readonly Dictionary<string, (string, string?, string?, int?, decimal?)> AreaCmtTypes;
protected readonly Dictionary<string, (string, string?, string?, int?, decimal?)> AreaComTypes;
protected Billing(int year, Season season,
Dictionary<string, string> attributes,
Dictionary<string, (decimal?, decimal?)> modifiers,
Dictionary<string, (string, string?, string?, int?, decimal?)> areaCmtTypes
) {
public Billing(int year) {
Year = year;
Season = season;
Attributes = attributes;
Modifiers = modifiers;
AreaCmtTypes = areaCmtTypes;
}
protected static async Task<(Season, Dictionary<string, string>, Dictionary<string, (decimal?, decimal?)>, Dictionary<string, (string, string?, string?, int?, decimal?)>)> LoadData(AppDbContext ctx, int year) {
var season = await ctx.FetchSeasons(year).SingleOrDefaultAsync() ?? throw new ArgumentException("Invalid season");
var attributes = await ctx.FetchWineAttributes().ToDictionaryAsync(a => a.AttrId, a => a.Name);
var modifiers = await ctx.FetchModifiers(year).ToDictionaryAsync(m => m.ModId, m => (m.Abs, m.Rel));
var areaCmtTypes = ctx.AreaCommitmentTypes.ToDictionary(v => v.VtrgId, v => (v.SortId, v.AttrId, v.Discriminator, v.MinKgPerHa, v.PenaltyAmount));
return (season, attributes, modifiers, areaCmtTypes);
}
public static async Task<Billing> Create(int year) {
using var ctx = new AppDbContext();
var (season, attributes, modifiers, areaCmtTypes) = await LoadData(ctx, year);
return new Billing(year, season, attributes, modifiers, areaCmtTypes);
Season = ctx.Seasons.Find(Year)!;
Attributes = ctx.WineAttributes.ToDictionary(a => a.AttrId, a => a.Name);
Modifiers = ctx.Modifiers.Where(m => m.Year == Year).Include(m => m.Season).ToDictionary(m => m.ModId, m => (m.Abs, m.Rel));
AreaComTypes = ctx.AreaCommitmentTypes.ToDictionary(v => v.VtrgId, v => (v.SortId, v.AttrId, v.Discriminator, v.MinKgPerHa, v.PenaltyAmount));
}
public async Task FinishSeason() {
using var cnx = await AppDbContext.ConnectAsync();
await cnx.ExecuteBatch($"""
await AppDbContext.ExecuteBatch(cnx, $"""
UPDATE season
SET (start_date, end_date) = (SELECT MIN(date), MAX(date) FROM delivery WHERE year = {Year})
WHERE year = {Year};
""");
}
public async Task AutoAdjustBusinessShares(DateOnly date, int allowanceKg = 0, double allowanceShares = 0, int allowanceKgPerShare = 0, double allowanceRel = 0, int addMinShares = 1) {
if (addMinShares < 1) addMinShares = 1;
public async Task AutoAdjustBusinessShares(DateOnly date, int allowanceKg = 0, double allowanceBs = 0, int allowanceKgPerBs = 0, double allowanceRel = 0, int addMinBs = 1) {
if (addMinBs < 1) addMinBs = 1;
using var cnx = await AppDbContext.ConnectAsync();
await cnx.ExecuteBatch($"""
DELETE FROM member_history WHERE source = 'elwig' AND reason = '{MemberHistory.REASON_PRESCRIPTION}' AND deduct_year = {Year};
INSERT INTO member_history (histnr, from_mgnr, from_type, to_mgnr, to_type, date_notice, date_effective, reason, source, deduct_year, shares, value_per_share, currency, comment)
SELECT COALESCE((SELECT MAX(histnr) AS histnr FROM member_history), 0) + ROW_NUMBER() OVER(ORDER BY m.mgnr),
NULL, NULL, u.mgnr, 1, '{date:yyyy-MM-dd}', '{date:yyyy-MM-dd}', '{MemberHistory.REASON_PRESCRIPTION}', 'elwig', s.year,
CEIL((u.diff - {allowanceKg}.0 - {allowanceKgPerShare}.0 * u.shares) / s.max_kg_per_share
- {allowanceShares.ToString(CultureInfo.InvariantCulture)}
- {allowanceRel.ToString(CultureInfo.InvariantCulture)} * u.shares) AS adjust_shares,
s.share_value / POW(10, s.precision - 2), s.currency, 'durch automatische Nachzeichnung'
await AppDbContext.ExecuteBatch(cnx, $"""
UPDATE member
SET business_shares = member.business_shares - h.business_shares
FROM member_history h
WHERE h.date = '{Year}-11-30' AND h.type = 'auto' AND h.mgnr = member.mgnr AND member.active;
INSERT INTO member_history (mgnr, date, type, business_shares)
SELECT u.mgnr,
'{date:yyyy-MM-dd}',
'auto',
CEIL((u.diff - {allowanceKg}.0 - {allowanceKgPerBs}.0 * u.business_shares) / s.max_kg_per_bs
- {allowanceBs.ToString(CultureInfo.InvariantCulture)}
- {allowanceRel.ToString(CultureInfo.InvariantCulture)} * u.business_shares) AS bs
FROM v_total_under_delivery u
JOIN season s ON s.year = u.year
JOIN member m ON m.mgnr = u.mgnr
WHERE s.year = {Year} AND adjust_shares >= {addMinShares} AND m.active
WHERE s.year = {Year} AND bs >= {addMinBs} AND m.active
ON CONFLICT DO UPDATE
SET business_shares = excluded.business_shares;
UPDATE member
SET business_shares = member.business_shares + h.business_shares
FROM member_history h
WHERE h.date = '{Year}-11-30' AND h.type = 'auto' AND h.mgnr = member.mgnr;
""");
}
public async Task UnAdjustBusinessShares() {
using var cnx = await AppDbContext.ConnectAsync();
await cnx.ExecuteBatch($"""
DELETE FROM member_history WHERE source = 'elwig' AND reason = '{MemberHistory.REASON_PRESCRIPTION}' AND deduct_year = {Year};
await AppDbContext.ExecuteBatch(cnx, $"""
UPDATE member
SET business_shares = member.business_shares - h.business_shares
FROM member_history h
WHERE h.date = '{Year}-11-30' AND h.type = 'auto' AND h.mgnr = member.mgnr AND member.active;
DELETE FROM member_history WHERE date = '{Year}-11-30' AND type = 'auto';
""");
}
public async Task CalculateBuckets(
bool? honorContractedField = null,
bool? honorGebundenField = null,
bool? allowAttributesIntoLower = null,
bool? avoidUnderDeliveries = null,
SqliteConnection? cnx = null
) {
using var ctx = new AppDbContext();
var honorContracted = honorContractedField ?? Season.Billing_HonorContracted;
var honorGebunden = honorGebundenField ?? Season.Billing_HonorGebunden;
var allowAttrsIntoLower = allowAttributesIntoLower ?? Season.Billing_AllowAttrsIntoLower;
var avoidUnderDlvrs = avoidUnderDeliveries ?? Season.Billing_AvoidUnderDeliveries;
var attrVals = ctx.WineAttributes.ToDictionary(a => a.AttrId, a => (a.IsStrict, a.FillLower));
@@ -96,10 +96,10 @@ namespace Elwig.Helpers.Billing {
var deliveries = new List<(int, int, int, string, int, double, string, string?, string[], bool?)>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
SELECT mgnr, did, dpnr, sortid, weight, kmw, qualid, attrid, modifiers, contracted
SELECT mgnr, did, dpnr, sortid, weight, kmw, qualid, attrid, modifiers, gebunden
FROM v_delivery
WHERE year = {Year}
ORDER BY mgnr, sortid, abgewertet ASC, {(honorContracted ? "contracted IS NOT NULL DESC, contracted DESC," : "")}
ORDER BY mgnr, sortid, abgewertet ASC, {(honorGebunden ? "gebunden IS NOT NULL DESC, gebunden DESC," : "")}
attribute_prio DESC, COALESCE(attrid, '~'), kmw DESC, lsnr, dpnr
""";
using var reader = await cmd.ExecuteReaderAsync();
@@ -115,14 +115,14 @@ namespace Elwig.Helpers.Billing {
}
int lastMgNr = 0;
Dictionary<string, AreaCmtBucket>? rightsAndObligations = null;
Dictionary<string, AreaComBucket>? rightsAndObligations = null;
Dictionary<string, int> used = [];
foreach (var (mgnr, did, dpnr, sortid, weight, kmw, qualid, attrid, modifiers, contracted) in deliveries) {
foreach (var (mgnr, did, dpnr, sortid, weight, kmw, qualid, attrid, modifiers, gebunden) in deliveries) {
if (lastMgNr != mgnr) {
rightsAndObligations = await ctx.GetMemberAreaCommitmentBuckets(Year, mgnr);
used = [];
}
if ((honorContracted && contracted == false) ||
if ((honorGebunden && gebunden == false) ||
rightsAndObligations == null || rightsAndObligations.Count == 0 ||
qualid == "WEI" || qualid == "RSW" || qualid == "LDW")
{
@@ -140,7 +140,7 @@ namespace Elwig.Helpers.Billing {
foreach (var p in Utils.Permutate(attributes, attributes.Intersect(attrForced))) {
var c = p.Count();
var key = sortid + string.Join("", p);
if (rightsAndObligations.TryGetValue(key, out AreaCmtBucket value)) {
if (rightsAndObligations.TryGetValue(key, out AreaComBucket value)) {
int i = (c == 0) ? 1 : 2;
var u = used.GetValueOrDefault(key, 0);
var vr = Math.Max(0, Math.Min(value.Right - u, w));
@@ -157,9 +157,9 @@ namespace Elwig.Helpers.Billing {
lastMgNr = mgnr;
}
await cnx.ExecuteBatch($"UPDATE delivery_part_bucket SET value = 0 WHERE year = {Year}");
await AppDbContext.ExecuteBatch(cnx, $"UPDATE delivery_part_bucket SET value = 0 WHERE year = {Year}");
if (inserts.Count > 0) {
await cnx.ExecuteBatch($"""
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
@@ -237,7 +237,7 @@ namespace Elwig.Helpers.Billing {
if (needed == 0) break;
}
await cnx.ExecuteBatch($"""
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
+33 -35
View File
@@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
@@ -14,7 +13,7 @@ namespace Elwig.Helpers.Billing {
public enum CalculationMode { Elwig, WgMaster }
public enum CurveMode { Oe, Kmw }
public record struct Curve(CurveMode Mode, Dictionary<double, decimal> Normal, Dictionary<double, decimal>? Contracted);
public record struct Curve(CurveMode Mode, Dictionary<double, decimal> Normal, Dictionary<double, decimal>? Gebunden);
public static JsonSchema? Schema { get; private set; }
@@ -93,14 +92,6 @@ namespace Elwig.Helpers.Billing {
Mode = (mode == "elwig") ? CalculationMode.Elwig : CalculationMode.WgMaster;
}
public BillingData() {
Data = new JsonObject {
["mode"] = "elwig",
["version"] = 1,
};
Mode = CalculationMode.Elwig;
}
protected static JsonObject ParseJson(string json) {
if (Schema == null) throw new InvalidOperationException("Schema has to be initialized first");
try {
@@ -116,10 +107,6 @@ namespace Elwig.Helpers.Billing {
return new(ParseJson(json));
}
public string ToJsonString(JsonSerializerOptions? options = null) {
return Data.ToJsonString(options);
}
protected JsonArray GetCurvesEntry() {
return Data[Mode == CalculationMode.Elwig ? "curves" : "Kurven"]?.AsArray() ?? throw new InvalidOperationException();
}
@@ -179,10 +166,10 @@ namespace Elwig.Helpers.Billing {
} else {
throw new InvalidOperationException();
}
var ctrd = obj["ctrd"] ?? obj["geb"];
if (ctrd is JsonObject) {
c2 = GetCurveData(ctrd.AsObject(), cMode);
} else if (ctrd?.AsValue().TryGetValue(out decimal v) == true) {
var geb = obj["geb"];
if (geb is JsonObject) {
c2 = GetCurveData(geb.AsObject(), cMode);
} else if (geb?.AsValue().TryGetValue(out decimal v) == true) {
var splitVal = GetCurveValueAt(c1, quw);
c2 = c1.ToDictionary(e => e.Key, e => e.Value + (e.Key >= quw ? v : 0));
c2[quw] = splitVal + v;
@@ -274,7 +261,7 @@ namespace Elwig.Helpers.Billing {
protected static JsonNode GraphEntryToJson(GraphEntry entry) {
try {
if (entry.ContractedFlatBonus == null) {
if (entry.GebundenFlatBonus == null) {
return JsonValue.Create((decimal)entry.DataGraph.DataY.Distinct().Single());
}
} catch { }
@@ -286,10 +273,10 @@ namespace Elwig.Helpers.Billing {
curve["data"] = GraphToJson(entry.DataGraph, entry.Mode.ToString().ToLower());
if (entry.ContractedFlatBonus != null) {
curve["ctrd"] = (decimal)entry.ContractedFlatBonus;
} else if (entry.ContractedGraph != null) {
curve["ctrd"] = GraphToJson(entry.ContractedGraph, entry.Mode.ToString().ToLower());
if (entry.GebundenFlatBonus != null) {
curve["geb"] = (decimal)entry.GebundenFlatBonus;
} else if (entry.GebundenGraph != null) {
curve["geb"] = GraphToJson(entry.GebundenGraph, entry.Mode.ToString().ToLower());
}
return curve;
@@ -425,7 +412,7 @@ namespace Elwig.Helpers.Billing {
}
}
public static BillingData FromGraphEntries(
public static JsonObject FromGraphEntries(
IEnumerable<GraphEntry> graphEntries,
BillingData? origData = null,
IEnumerable<RawVaribute>? vaributes = null,
@@ -459,29 +446,40 @@ namespace Elwig.Helpers.Billing {
}
}
CollapsePaymentData(payment, payment.DeepClone().AsObject(), vaributes ?? [.. payment.Select(e => new RawVaribute(e.Key))], useDefaultPayment);
CollapsePaymentData(qualityWei, qualityWei.DeepClone().AsObject(), vaributes ?? [.. qualityWei.Select(e => new RawVaribute(e.Key))], useDefaultQuality);
CollapsePaymentData(payment, payment.DeepClone().AsObject(), vaributes ?? payment.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultPayment);
CollapsePaymentData(qualityWei, qualityWei.DeepClone().AsObject(), vaributes ?? qualityWei.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultQuality);
var data = new JsonObject {
["mode"] = "elwig",
["version"] = 1,
};
if (origData?.ConsiderDelieryModifiers == true)
data["consider_delivery_modifiers"] = true;
if (origData?.ConsiderContractPenalties == true)
data["consider_contract_penalties"] = true;
if (origData?.ConsiderTotalPenalty == true)
data["consider_total_penalty"] = true;
if (origData?.ConsiderAutoBusinessShares == true)
data["consider_auto_business_shares"] = true;
BillingData data = origData != null && origData.Mode == CalculationMode.Elwig ? new BillingData((JsonObject)origData.Data.DeepClone()) : new BillingData();
if (payment.Count == 0) {
data.Data["payment"] = 0;
data["payment"] = 0;
} else if (payment.Count == 1 && payment.First().Key == "default") {
data.Data["payment"] = payment.Single().Value?.DeepClone();
data["payment"] = payment.Single().Value?.DeepClone();
} else {
data.Data["payment"] = payment;
data["payment"] = payment;
}
if (qualityWei.Count == 1 && qualityWei.First().Key == "default") {
data.Data["quality"] = new JsonObject() {
data["quality"] = new JsonObject() {
["WEI"] = qualityWei.Single().Value?.DeepClone()
};
} else if (qualityWei.Count >= 1) {
data.Data["quality"] = new JsonObject() {
data["quality"] = new JsonObject() {
["WEI"] = qualityWei
};
} else {
data.Data.Remove("quality");
}
data.Data["curves"] = curves;
data["curves"] = curves;
return data;
}
+33 -70
View File
@@ -10,47 +10,23 @@ namespace Elwig.Helpers.Billing {
public class BillingVariant : Billing {
protected readonly int AvNr;
protected PaymentVar PaymentVariant;
protected PaymentBillingData Data;
protected readonly PaymentVar PaymentVariant;
protected readonly PaymentBillingData Data;
protected BillingVariant(int year, int avnr, Season season,
Dictionary<string, string> attributes,
Dictionary<string, (decimal?, decimal?)> modifiers,
Dictionary<string, (string, string?, string?, int?, decimal?)> areaCmtTypes,
PaymentVar paymentVar, PaymentBillingData data) :
base(year, season, attributes, modifiers, areaCmtTypes) {
public BillingVariant(int year, int avnr) : base(year) {
AvNr = avnr;
PaymentVariant = paymentVar;
Data = data;
}
protected static async Task<(PaymentVar, PaymentBillingData)> LoadData(AppDbContext ctx, int year, int avnr) {
var paymentVar = await ctx.PaymentVariants.Where(v => v.Year == year && v.AvNr == avnr).SingleAsync();
var data = PaymentBillingData.FromJson(paymentVar.Data, await Utils.GetVaributes(ctx, year, onlyDelivered: false));
return (paymentVar, data);
}
public static async Task<BillingVariant> Create(int year, int avnr) {
using var ctx = new AppDbContext();
var (season, attributes, modifiers, areaCmtTypes) = await LoadData(ctx, year);
var (paymentVar, data) = await LoadData(ctx, year, avnr);
return new BillingVariant(year, avnr, season, attributes, modifiers, areaCmtTypes, paymentVar, data);
PaymentVariant = ctx.PaymentVariants.Include(v => v.Season).Where(v => v.Year == Year && v.AvNr == AvNr).Single() ?? throw new ArgumentException("PaymentVar not found");
Data = PaymentBillingData.FromJson(PaymentVariant.Data, Utils.GetVaributes(ctx, Year, onlyDelivered: false));
}
public async Task Calculate(bool strictPrices = true, bool? honorContracted = null, bool? allowAttrsIntoLower = null, bool? avoidUnderDeliveries = null) {
if (PaymentVariant == null || Data == null) throw new Exception("Call Load before Calculate");
public async Task Calculate(bool? honorGebunden = null, bool? allowAttrsIntoLower = null, bool? avoidUnderDeliveries = null) {
using var cnx = await AppDbContext.ConnectAsync();
using var tx = await cnx.BeginTransactionAsync();
await CalculateBuckets(honorContracted, allowAttrsIntoLower, avoidUnderDeliveries, cnx);
await CalculateBuckets(honorGebunden, allowAttrsIntoLower, avoidUnderDeliveries, cnx);
await DeleteInDb(cnx);
await SetCalcTime(cnx);
KeyNotFoundException? exception = null;
try {
await CalculatePrices(cnx, strictPrices);
} catch (KeyNotFoundException e) {
if (strictPrices) throw;
exception = e;
}
await CalculatePrices(cnx);
if (Data.ConsiderDelieryModifiers) {
await CalculateDeliveryModifiers(cnx);
}
@@ -58,28 +34,26 @@ namespace Elwig.Helpers.Billing {
await CalculateMemberModifiers(cnx);
}
await tx.CommitAsync();
if (exception != null)
throw exception;
}
public async Task Commit() {
await Revert();
using var cnx = await AppDbContext.ConnectAsync();
await cnx.ExecuteBatch($"""
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,
IIF(lc.amount < 0, 0, ROUND(lp.amount / POW(10, s.precision - 2))) AS prev_net_amount,
IIF(m.accounting, s.vat_normal, s.vat_flatrate) AS vat,
IIF(lc.amount >= 0, ROUND(lp.amount / POW(10, s.precision - 2)), 0) AS prev_net_amount,
IIF(m.buchführend, s.vat_normal, s.vat_flatrate) AS vat,
ROUND(IIF({Data.ConsiderTotalPenalty}, COALESCE(b.total_penalty, 0), 0) / POW(10, s.precision - 2)) +
ROUND(IIF({Data.ConsiderContractPenalties}, COALESCE(u.total_penalty, 0), 0) / POW(10, 4 - 2)) +
ROUND(IIF({Data.ConsiderAutoBusinessShares}, -COALESCE(a.total_amount, 0), 0)) +
ROUND(IIF({Data.ConsiderAutoBusinessShares}, -COALESCE(a.total_amount, 0), 0) / POW(10, s.precision - 2)) +
IIF({Data.ConsiderCustomModifiers}, COALESCE(x.amount, 0), 0)
AS modifiers,
IIF(lc.amount < 0, 0, lc.modifiers) AS prev_modifiers
IIF(lc.amount >= 0, lc.modifiers, 0) 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
@@ -98,29 +72,29 @@ namespace Elwig.Helpers.Billing {
LEFT JOIN v_penalty_area_commitments u ON (u.year, u.mgnr) = (s.year, m.mgnr)
LEFT JOIN v_auto_business_shares a ON (a.year, a.mgnr) = (s.year, m.mgnr)
LEFT JOIN payment_custom x ON (x.year, x.mgnr) = (s.year, m.mgnr)
WHERE s.year = {Year} AND v.avnr = {AvNr} AND p.amount != COALESCE(lp.amount, 0);
WHERE s.year = {Year} AND v.avnr = {AvNr};
""");
await cnx.ExecuteBatch($"""
await AppDbContext.ExecuteBatch(cnx, $"""
UPDATE payment_variant SET test_variant = FALSE WHERE (year, avnr) = ({Year}, {AvNr});
""");
}
public async Task Revert() {
using var cnx = await AppDbContext.ConnectAsync();
await cnx.ExecuteBatch($"""
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 cnx.ExecuteBatch($"""
await AppDbContext.ExecuteBatch(cnx, $"""
UPDATE payment_variant SET calc_time = UNIXEPOCH() WHERE (year, avnr) = ({Year}, {AvNr})
""");
}
protected async Task DeleteInDb(SqliteConnection cnx) {
await cnx.ExecuteBatch($"""
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});
@@ -134,14 +108,14 @@ namespace Elwig.Helpers.Billing {
var multiplier = 0.50;
var includePredecessor = true;
var modName = "Treue%";
await cnx.ExecuteBatch($"""
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.weight_total * COALESCE(m.abs, 0)),
ROUND(s.sum * COALESCE(m.abs, 0)),
COALESCE(m.rel, 0)
FROM (SELECT {Year} AS year, m.mgnr,
ROUND(AVG(COALESCE(a.weight_total, b.weight_total)) * {multiplier}) AS baseline,
COUNT(*) = {lastYears} AND MIN(COALESCE(a.weight_total, b.weight_total)) > 0 AS allowed
ROUND(AVG(COALESCE(a.sum, b.sum)) * {multiplier}) AS baseline,
COUNT(*) = {lastYears} AND MIN(COALESCE(a.sum, b.sum)) > 0 AS allowed
FROM member m
LEFT JOIN v_stat_member a ON a.mgnr = m.mgnr
FULL OUTER JOIN v_stat_member b ON b.mgnr = m.predecessor_mgnr AND b.year = a.year AND {(includePredecessor ? "TRUE" : "FALSE")}
@@ -150,13 +124,13 @@ namespace Elwig.Helpers.Billing {
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 weight_total >= baseline
WHERE sum >= baseline
ON CONFLICT DO UPDATE
SET mod_abs = mod_abs + excluded.mod_abs,
mod_rel = mod_rel + excluded.mod_rel
""");
}
await cnx.ExecuteBatch($"""
await AppDbContext.ExecuteBatch(cnx, $"""
INSERT INTO payment_member (year, avnr, mgnr, net_amount, mod_abs, mod_rel)
SELECT x.year, {AvNr}, x.mgnr, 0, COALESCE(x.mod_abs * POW(10, s.precision - 2), 0), COALESCE(x.mod_rel, 0)
FROM payment_custom x
@@ -168,9 +142,8 @@ namespace Elwig.Helpers.Billing {
""");
}
protected async Task CalculatePrices(SqliteConnection cnx, bool strict = true) {
var invalid = new HashSet<string>();
var parts = new List<(int Year, int DId, int DPNr, int BktNr, string SortId, string? AttrId, string? CultId, string Discr, int Value, double Oe, double Kmw, string QualId, bool AttrAreaCmt)>();
protected async Task CalculatePrices(SqliteConnection cnx) {
var parts = new List<(int Year, int DId, int DPNr, int BktNr, string SortId, string? AttrId, string? CultId, string Discr, int Value, double Oe, double Kmw, string QualId, bool AttrAreaCom)>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
SELECT d.year, d.did, d.dpnr, b.bktnr, d.sortid, d.attrid, d.cultid, b.discr, b.value, d.oe, d.kmw, d.qualid, COALESCE(a.area_com, TRUE)
@@ -195,35 +168,25 @@ namespace Elwig.Helpers.Billing {
foreach (var part in parts) {
if (part.Value == 0)
continue;
var unctrd = part.Discr == "_";
var ungeb = part.Discr == "_";
var payAttrId = (part.Discr is "" or "_") ? null : part.Discr;
var attrId = part.AttrAreaCmt ? payAttrId : part.AttrId;
var ctrd = !unctrd && (payAttrId == attrId || !part.AttrAreaCmt);
decimal price = 0;
try {
price = Data.CalculatePrice(part.SortId, attrId, part.CultId, part.QualId, ctrd, part.Oe, part.Kmw);
} catch (KeyNotFoundException e) {
invalid.Add(e.Message.Split('\'')[1]);
}
var attrId = part.AttrAreaCom ? payAttrId : part.AttrId;
var geb = !ungeb && (payAttrId == attrId || !part.AttrAreaCom);
var price = Data.CalculatePrice(part.SortId, attrId, part.CultId, part.QualId, geb, part.Oe, part.Kmw);
var priceL = PaymentVariant.Season.DecToDb(price);
inserts.Add((part.Year, part.DId, part.DPNr, part.BktNr, priceL, priceL * part.Value));
}
var msg = invalid.Count == 0 ? null : "Für folgende Sorten wurde noch keine Preiskurve festgelegt: " + string.Join(", ", invalid);
if (msg != null && strict)
throw new KeyNotFoundException(msg);
await cnx.ExecuteBatch($"""
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})"))};
""");
if (msg != null)
throw new KeyNotFoundException(msg);
}
protected async Task CalculateDeliveryModifiers(SqliteConnection cnx) {
var netMod = Data.NetWeightModifier.ToString().Replace(',', '.');
var grossMod = Data.GrossWeightModifier.ToString().Replace(',', '.');
await cnx.ExecuteBatch($"""
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, 0, IIF(d.net_weight, {netMod}, {grossMod})
FROM delivery_part d
@@ -232,7 +195,7 @@ namespace Elwig.Helpers.Billing {
SET mod_rel = mod_rel + excluded.mod_rel;
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) * d.weight, COALESCE(m.rel, 0)
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)
+8 -9
View File
@@ -3,7 +3,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace Elwig.Helpers.Billing {
public class EditBillingData : BillingData {
@@ -71,14 +70,14 @@ namespace Elwig.Helpers.Billing {
return (curves, dict3);
}
private static async Task<List<GraphEntry>> CreateGraphEntries(
private static List<GraphEntry> CreateGraphEntries(
AppDbContext ctx, int precision,
Dictionary<int, Curve> curves,
Dictionary<int, List<RawVaribute>> entries
) {
var vars = await ctx.FetchWineVarieties().ToDictionaryAsync(v => v.SortId, v => v);
var attrs = await ctx.FetchWineAttributes().ToDictionaryAsync(a => a.AttrId, a => a);
var cults = await ctx.FetchWineCultivations().ToDictionaryAsync(c => c.CultId, c => c);
var vars = ctx.WineVarieties.ToDictionary(v => v.SortId, v => v);
var attrs = ctx.WineAttributes.ToDictionary(a => a.AttrId, a => a);
var cults = ctx.WineCultivations.ToDictionary(c => c.CultId, c => c);
return entries
.Select(e => new GraphEntry(e.Key, precision, curves[e.Key], e.Value
.Select(s => new Varibute(s, vars, attrs, cults))
@@ -86,18 +85,18 @@ namespace Elwig.Helpers.Billing {
.ToList();
}
public async Task<IEnumerable<GraphEntry>> GetPaymentGraphEntries(AppDbContext ctx, Season season) {
public IEnumerable<GraphEntry> GetPaymentGraphEntries(AppDbContext ctx, Season season) {
var root = GetPaymentEntry();
var (curves, entries) = GetGraphEntries(root);
return (await CreateGraphEntries(ctx, season.Precision, curves, entries)).Where(e => e.Vaributes.Count > 0);
return CreateGraphEntries(ctx, season.Precision, curves, entries).Where(e => e.Vaributes.Count > 0);
}
public async Task<IEnumerable<GraphEntry>> GetQualityGraphEntries(AppDbContext ctx, Season season, int idOffset = 0) {
public IEnumerable<GraphEntry> GetQualityGraphEntries(AppDbContext ctx, Season season, int idOffset = 0) {
var root = GetQualityEntry();
if (root == null || root["WEI"] is not JsonNode qualityWei)
return [];
var (curves, entries) = GetGraphEntries(qualityWei);
var list = (await CreateGraphEntries(ctx, season.Precision, curves, entries)).Where(e => e.Vaributes.Count > 0);
var list = CreateGraphEntries(ctx, season.Precision, curves, entries).Where(e => e.Vaributes.Count > 0);
foreach (var e in list) {
e.Id += idOffset;
e.Abgewertet = true;
+17 -19
View File
@@ -1,13 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
namespace Elwig.Helpers.Billing {
public class GraphEntry {
public const int MinX = 50;
public const int MinXCtrd = 73;
public const int MinXGeb = 73;
public const int MaxX = 120;
public int Id { get; set; }
@@ -15,11 +14,11 @@ namespace Elwig.Helpers.Billing {
public bool Abgewertet { get; set; }
public Graph DataGraph { get; set; }
public Graph? ContractedGraph { get; set; }
public double? ContractedFlatBonus {
public Graph? GebundenGraph { get; set; }
public double? GebundenFlatBonus {
get {
try {
var val = ContractedGraph?.DataX.Zip(ContractedGraph.DataY)
var val = GebundenGraph?.DataX.Zip(GebundenGraph.DataY)
.Select(e => Math.Round(e.Second - DataGraph.GetPriceAtOe(e.First), Precision))
.Distinct()
.Single();
@@ -30,10 +29,10 @@ namespace Elwig.Helpers.Billing {
}
set {
if (value is not double v) return;
var values = Enumerable.Range(MinXCtrd, MaxX - MinXCtrd + 1)
var values = Enumerable.Range(MinXGeb, MaxX - MinXGeb + 1)
.Select(i => Math.Round(DataGraph.GetPriceAtOe(i) + v, Precision))
.ToArray();
ContractedGraph = new Graph(values, Precision, MinXCtrd, MaxX);
GebundenGraph = new Graph(values, Precision, MinXGeb, MaxX);
}
}
@@ -41,7 +40,6 @@ namespace Elwig.Helpers.Billing {
public string VaributeStringSimple => (Abgewertet ? "Abgew.: " : "") + (Vaributes.Count != 0 ? (Vaributes.Count >= 25 ? "Restliche Sorten" : string.Join(", ", Vaributes.Select(c => c.Listing))) : "-");
public string VaributeString => Vaributes.Count != 0 ? string.Join("\n", Vaributes.Select(c => c.FullName)) : "-";
public string VaributeStringChange => (Abgewertet ? "A." : "") + string.Join(",", Vaributes.Select(c => c.Listing));
public Brush? Color => Vaributes.Select(v => v.Variety?.Color).Distinct().SingleOrDefault();
private readonly int Precision;
public GraphEntry(int id, int precision, BillingData.CurveMode mode) {
@@ -52,39 +50,39 @@ namespace Elwig.Helpers.Billing {
Vaributes = [];
}
public GraphEntry(int id, int precision, BillingData.CurveMode mode, Dictionary<double, decimal> data, Dictionary<double, decimal>? contracted) :
public GraphEntry(int id, int precision, BillingData.CurveMode mode, Dictionary<double, decimal> data, Dictionary<double, decimal>? gebunden) :
this(id, precision, mode) {
DataGraph = new Graph(data, precision, MinX, MaxX);
if (contracted != null) ContractedGraph = new Graph(contracted, precision, MinXCtrd, MaxX);
if (gebunden != null) GebundenGraph = new Graph(gebunden, precision, MinXGeb, MaxX);
}
public GraphEntry(int id, int precision, BillingData.Curve curve, List<Varibute> vaributes) :
this(id, precision, curve.Mode) {
DataGraph = new Graph(curve.Normal, precision, MinX, MaxX);
if (curve.Contracted != null)
ContractedGraph = new Graph(curve.Contracted, precision, MinXCtrd, MaxX);
if (curve.Gebunden != null)
GebundenGraph = new Graph(curve.Gebunden, precision, MinXGeb, MaxX);
Vaributes = vaributes;
}
private GraphEntry(int id, int precision, BillingData.CurveMode mode, Graph dataGraph, Graph? contractedGraph, List<Varibute> vaributes) {
private GraphEntry(int id, int precision, BillingData.CurveMode mode, Graph dataGraph, Graph? gebundenGraph, List<Varibute> vaributes) {
Id = id;
Precision = precision;
Mode = mode;
DataGraph = dataGraph;
ContractedGraph = contractedGraph;
GebundenGraph = gebundenGraph;
Vaributes = vaributes;
}
public void AddContractedGraph() {
ContractedGraph ??= new Graph(Precision, MinXCtrd, MaxX);
public void AddGebundenGraph() {
GebundenGraph ??= new Graph(Precision, MinXGeb, MaxX);
}
public void RemoveContractedGraph() {
ContractedGraph = null;
public void RemoveGebundenGraph() {
GebundenGraph = null;
}
public GraphEntry Copy(int id) {
return new GraphEntry(id, Precision, Mode, (Graph)DataGraph.Clone(), (Graph?)ContractedGraph?.Clone(), []);
return new GraphEntry(id, Precision, Mode, (Graph)DataGraph.Clone(), (Graph?)GebundenGraph?.Clone(), []);
}
}
}
+2 -2
View File
@@ -45,9 +45,9 @@ namespace Elwig.Helpers.Billing {
return dict;
}
public decimal CalculatePrice(string sortid, string? attrid, string? cultid, string qualid, bool contracted, double oe, double kmw) {
public decimal CalculatePrice(string sortid, string? attrid, string? cultid, string qualid, bool gebunden, double oe, double kmw) {
var curve = GetQualityCurve(qualid, sortid, attrid, cultid) ?? GetCurve(sortid, attrid, cultid);
return GetCurveValueAt((contracted ? curve.Contracted : null) ?? curve.Normal, curve.Mode == CurveMode.Oe ? oe : kmw);
return GetCurveValueAt((gebunden ? curve.Gebunden : null) ?? curve.Normal, curve.Mode == CurveMode.Oe ? oe : kmw);
}
private Curve LookupCurve(JsonValue val) {
+18 -51
View File
@@ -8,18 +8,24 @@ using System.Threading.Tasks;
namespace Elwig.Helpers {
public class ClientParameters {
public enum Type { Matzen, Winzerkeller, Weinland, Baden, Seewinkel };
public enum Type { Matzen, Winzerkeller, Weinland, Baden };
public bool IsMatzen => Client == Type.Matzen;
public bool IsWinzerkeller => Client == Type.Winzerkeller;
public bool IsWeinland => Client == Type.Weinland;
public bool IsBaden => Client == Type.Baden;
public bool IsSeewinkel => Client == Type.Seewinkel;
public bool IsWolkersdorf => IsWinzerkeller && App.ZwstId == "W";
public bool IsHaugsdorf => IsWinzerkeller && App.ZwstId == "H";
public bool IsSitzendorf => IsWinzerkeller && App.ZwstId == "S";
public bool IsGrInzersdorf => IsWeinland;
public bool IsPamhagen => IsSeewinkel;
public bool HasNetWeighing(string? zwstId) => IsMatzen || (IsWinzerkeller && zwstId == "W");
public bool HasNetWeighing(Branch? b) => HasNetWeighing(b?.ZwstId);
public bool HasNetWeighing() => HasNetWeighing(App.ZwstId);
public bool HasBoxWeighing(string? zwstId) => IsWinzerkeller && (zwstId != "W");
public bool HasBoxWeighing(Branch? b) => HasBoxWeighing(b?.ZwstId);
public bool HasBoxWeighing() => HasBoxWeighing(App.ZwstId);
public string NameToken;
public string NameShort;
@@ -49,19 +55,12 @@ namespace Elwig.Helpers {
public string? Bic;
public string? UstIdNr;
public string? LfbisNr;
public string? OrganicOperatorId;
public string? OrganicAuthorityCode;
public string? PhoneNr;
public string? FaxNr;
public string? EmailAddress;
public string? Website;
public bool EnableMemberHistory;
public int ModeBusinessShares;
public bool HasRedWhite => ModeBusinessShares == 1;
public int ModeMemberDataSheetShares;
public int ModeDeliveryNoteStats;
public int ModeWineQualityStatistics;
public int OrderingMemberList;
@@ -81,7 +80,7 @@ namespace Elwig.Helpers {
public int ExportEbicsVersion;
public int ExportEbicsAddress;
public (int? AllowanceKg, double? AllowanceShares, int? AllowanceKgPerShare, double? AllowancePercent, int? MinShares) AutoAdjustShares;
public (int? AllowanceKg, double? AllowanceBs, int? AllowanceKgPerBs, double? AllowancePercent, int? MinBs) AutoAdjustBs;
public ClientParameters(AppDbContext ctx) : this(ctx.ClientParameters.ToDictionary(e => e.Param, e => e.Value)) { }
@@ -101,8 +100,6 @@ namespace Elwig.Helpers {
Client = Type.Weinland; break;
case "Winzergenossenschaft Baden - Bad Vöslau":
Client = Type.Baden; break;
case "Winzerkeller Seewinkel":
Client = Type.Seewinkel; break;
};
Plz = int.Parse(parameters["CLIENT_PLZ"] ?? "");
@@ -116,22 +113,7 @@ namespace Elwig.Helpers {
UstIdNr = parameters.GetValueOrDefault("CLIENT_USTIDNR");
Bic = parameters.GetValueOrDefault("CLIENT_BIC");
Iban = parameters.GetValueOrDefault("CLIENT_IBAN");
OrganicOperatorId = parameters.GetValueOrDefault("CLIENT_ORGANIC_OPERATORID");
OrganicAuthorityCode = parameters.GetValueOrDefault("CLIENT_ORGANIC_AUTHORITYCODE");
EnableMemberHistory = (parameters.GetValueOrDefault("ENABLE_MEMBERHISTORY")?.ToUpper()) switch {
"1" or "TRUE" or "YES" or "JA" => true,
_ => false,
};
switch (parameters.GetValueOrDefault("MODE_BUSINESSSHARES", "SIMPLE")?.ToUpper()) {
case "SIMPLE": ModeBusinessShares = 0; break;
case "RED_WHITE": ModeBusinessShares = 1; break;
}
switch (parameters.GetValueOrDefault("MODE_MEMBERDATASHEET_SHARES", "FULL")?.ToUpper()) {
case "NONE": ModeMemberDataSheetShares = 0; break;
case "FULL": ModeMemberDataSheetShares = 1; break;
}
switch (parameters.GetValueOrDefault("MODE_DELIVERYNOTE_STATS", "SHORT")?.ToUpper()) {
case "NONE": ModeDeliveryNoteStats = 0; break;
case "GA_ONLY": ModeDeliveryNoteStats = 1; break;
@@ -196,7 +178,7 @@ namespace Elwig.Helpers {
}
var autoAdjust = (parameters.GetValueOrDefault("AUTOADJUST_BUSINESSSHARES") ?? "").Split(';');
AutoAdjustShares = autoAdjust.Length == 5 ? (
AutoAdjustBs = autoAdjust.Length == 5 ? (
int.TryParse(autoAdjust[0], out var v1) ? v1 : null,
double.TryParse(autoAdjust[1], out var v2) ? v2 : null,
int.TryParse(autoAdjust[2], out var v3) ? v3 : null,
@@ -209,16 +191,6 @@ namespace Elwig.Helpers {
}
private IEnumerable<(string, string?)> GetParamValues() {
string businessShares = "SIMPLE";
switch (ModeBusinessShares) {
case 0: businessShares = "SIMPLE"; break;
case 1: businessShares = "RED_WHITE"; break;
}
string memberDataSheetShares = "FULL";
switch (ModeMemberDataSheetShares) {
case 0: memberDataSheetShares = "NONE"; break;
case 1: memberDataSheetShares = "FULL"; break;
}
string deliveryNoteStats = "SHORT";
switch (ModeDeliveryNoteStats) {
case 0: deliveryNoteStats = "NONE"; break;
@@ -240,15 +212,15 @@ namespace Elwig.Helpers {
case 1: orderingMemberList = "NAME"; break;
case 2: orderingMemberList = "KG"; break;
}
string mailSendPostal = "WISH";
switch (MailSendPostal) {
string mailSendPostal = "MGNR";
switch (MailOrdering) {
case 0: mailSendPostal = "NONE"; break;
case 1: mailSendPostal = "NO_EMAIL"; break;
case 2: mailSendPostal = "WISH"; break;
case 3: mailSendPostal = "ALL"; break;
}
string mailSendEmail = "WISH";
switch (MailSendEmail) {
string mailSendEmail = "MGNR";
switch (MailOrdering) {
case 0: mailSendEmail = "NONE"; break;
case 1: mailSendEmail = "WISH"; break;
case 2: mailSendEmail = "ALL"; break;
@@ -265,9 +237,9 @@ namespace Elwig.Helpers {
case 1: exportEbicsAddress = "LINES"; break;
case 2: exportEbicsAddress = "FULL"; break;
}
string autoAdjust = $"{AutoAdjustShares.AllowanceKg};{AutoAdjustShares.AllowanceShares?.ToString(CultureInfo.InvariantCulture)};" +
$"{AutoAdjustShares.AllowanceKgPerShare};{AutoAdjustShares.AllowancePercent?.ToString(CultureInfo.InvariantCulture)};" +
$"{AutoAdjustShares.MinShares}";
string autoAdjust = $"{AutoAdjustBs.AllowanceKg};{AutoAdjustBs.AllowanceBs?.ToString(CultureInfo.InvariantCulture)};" +
$"{AutoAdjustBs.AllowanceKgPerBs};{AutoAdjustBs.AllowancePercent?.ToString(CultureInfo.InvariantCulture)};" +
$"{AutoAdjustBs.MinBs}";
return [
("CLIENT_NAME_TOKEN", NameToken),
("CLIENT_NAME_SHORT", NameShort),
@@ -285,11 +257,6 @@ namespace Elwig.Helpers {
("CLIENT_USTIDNR", UstIdNr),
("CLIENT_BIC", Bic),
("CLIENT_IBAN", Iban),
("CLIENT_ORGANIC_OPERATORID", OrganicOperatorId),
("CLIENT_ORGANIC_AUTHORITYCODE", OrganicAuthorityCode),
("ENABLE_MEMBERHISTORY", EnableMemberHistory ? "YES" : "NO"),
("MODE_BUSINESSSHARES", businessShares),
("MODE_MEMBERDATASHEET_SHARES", memberDataSheetShares),
("MODE_DELIVERYNOTE_STATS", deliveryNoteStats),
("MODE_WINEQUALITYSTATISTICS", modeWineQualityStatistics),
("ORDERING_MEMBERLIST", orderingMemberList),
+4 -36
View File
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -6,8 +5,6 @@ using Microsoft.Extensions.Configuration;
namespace Elwig.Helpers {
public enum WeighingMode { Gross, Net, Box }
public record struct ScaleConfig {
public string Id;
public string? Type;
@@ -17,11 +14,10 @@ namespace Elwig.Helpers {
public string? Filling;
public string? Limit;
public bool Required;
public bool SyncTime;
public string? Log;
public string? _Log;
public ScaleConfig(string id, string? type, string? model, string? cnx, string? empty, string? filling, string? limit, bool? required, bool? syncTime, string? log) {
public ScaleConfig(string id, string? type, string? model, string? cnx, string? empty, string? filling, string? limit, bool? required, string? log) {
Id = id;
Type = type;
Model = model;
@@ -30,7 +26,6 @@ namespace Elwig.Helpers {
Filling = filling;
Limit = limit;
Required = required ?? true;
SyncTime = syncTime ?? true;
_Log = log;
Log = log != null ? Path.Combine(App.DataPath, log) : null;
}
@@ -39,30 +34,18 @@ namespace Elwig.Helpers {
public class Config {
private static readonly string[] TrueValues = ["1", "true", "yes", "on"];
private static readonly string[] FalseValues = ["0", "false", "no", "off"];
private readonly string FileName;
public bool Debug;
public string DatabaseFile = Path.Combine(App.DataPath, "database.sqlite3");
public string DatabaseFile = App.DataPath + "database.sqlite3";
public string? DatabaseLog = null;
public string? Branch = null;
public WeighingMode? WeighingMode;
public bool BackupAuto = true;
public string BackupPath = Path.Combine(App.DataPath, "backups");
public int BackupRetainHours = 36;
public int BackupRetainDays = 8;
public int BackupRetainWeeks = 6;
public int BackupRetainMonths = 18;
public string? UpdateUrl = null;
public bool UpdateAuto = false;
public string? SyncUrl = null;
public string SyncUsername = "";
public string SyncPassword = "";
public bool SyncMain = false;
public string? SmtpHost = null;
public int? SmtpPort = null;
@@ -70,10 +53,9 @@ namespace Elwig.Helpers {
public string? SmtpUsername = null;
public string? SmtpPassword = null;
public string? SmtpFrom = null;
public double SmtpWait = 0;
public (string Host, int Port, string Mode, string Username, string Password, string From, double Wait)? Smtp =>
public (string Host, int Port, string Mode, string Username, string Password, string From)? Smtp =>
SmtpHost == null || SmtpPort == null || SmtpMode == null || SmtpUsername == null || SmtpPassword == null || SmtpFrom == null ?
null : (SmtpHost, (int)SmtpPort, SmtpMode, SmtpUsername, SmtpPassword, SmtpFrom, SmtpWait);
null : (SmtpHost, (int)SmtpPort, SmtpMode, SmtpUsername, SmtpPassword, SmtpFrom);
public IList<ScaleConfig> Scales;
private readonly List<ScaleConfig> ScaleList = [];
@@ -92,23 +74,11 @@ namespace Elwig.Helpers {
DatabaseLog = log != null ? Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, log) : null;
Branch = config["general:branch"];
Debug = TrueValues.Contains(config["general:debug"]?.ToLower());
var weighing = config["general:weighing"];
WeighingMode = weighing != null && Enum.TryParse<WeighingMode>(weighing, true, out var w) ? w : null;
BackupAuto = !FalseValues.Contains(config["backup:auto"]?.ToLower());
BackupPath = Path.Combine(Path.GetDirectoryName(DatabaseFile) ?? App.DataPath, config["backup:path"] ?? "backups");
BackupRetainHours = int.TryParse(config["backup:retain_hours"], out var hours) ? hours : 36;
BackupRetainDays = int.TryParse(config["backup:retain_days"], out var days) ? days : 8;
BackupRetainWeeks = int.TryParse(config["backup:retain_weeks"], out var weeks) ? weeks : 6;
BackupRetainMonths = int.TryParse(config["backup:retain_months"], out var months) ? months : 18;
UpdateUrl = config["update:url"];
UpdateAuto = TrueValues.Contains(config["update:auto"]?.ToLower());
SyncUrl = config["sync:url"];
SyncUsername = config["sync:username"] ?? "";
SyncPassword = config["sync:password"] ?? "";
SyncMain = TrueValues.Contains(config["sync:main"]?.ToLower());
SmtpHost = config["smtp:host"];
SmtpPort = config["smtp:port"]?.All(char.IsAsciiDigit) == true && config["smtp:port"]?.Length > 0 ? int.Parse(config["smtp:port"]!) : null;
@@ -116,7 +86,6 @@ namespace Elwig.Helpers {
SmtpUsername = config["smtp:username"];
SmtpPassword = config["smtp:password"];
SmtpFrom = config["smtp:from"];
SmtpWait = double.TryParse(config["smtp:wait"], out var res) ? res : 0;
var scales = config.AsEnumerable().Where(i => i.Key.StartsWith("scale.")).GroupBy(i => i.Key.Split(':')[0][6..]).Select(i => i.Key).Order();
ScaleList.Clear();
@@ -126,7 +95,6 @@ namespace Elwig.Helpers {
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"],
config[$"scale.{s}:required"] != null ? TrueValues.Contains(config[$"scale.{s}:required"]?.ToLower()) : null,
config[$"scale.{s}:synctime"] != null ? !FalseValues.Contains(config[$"scale.{s}:synctime"]?.ToLower()) : null,
config[$"scale.{s}:log"]
));
}
+11 -7
View File
@@ -6,9 +6,9 @@ using System;
namespace Elwig.Helpers.Export {
using Row = Tuple<(string?, string?, string?, string?, string, int, string, bool, int), (string, int, string, string, string, int, string, double, double)>;
using Row = Tuple<(string?, string?, string?, string?, string, int, string, int), (string, int, string, string, string, int, string, double, double)>;
public class Bki : CsvFile<Row> {
public class Bki : Csv<Row> {
private readonly string _clientData;
@@ -29,7 +29,7 @@ namespace Elwig.Helpers.Export {
using var cnx = await AppDbContext.ConnectAsync();
using var cmd = cnx.CreateCommand();
cmd.CommandText = $"""
SELECT lfbis_nr, name, other_names, billing_name, address, plz, ort, vollablieferer, area,
SELECT lfbis_nr, name, other_names, billing_name, address, plz, ort, area,
date, weight, type, sortid, qualid, year, hkid, kmw, oe
FROM v_bki_delivery
WHERE year = {year}
@@ -38,24 +38,28 @@ namespace Elwig.Helpers.Export {
List<Row> rows = [];
while (await r.ReadAsync()) {
rows.Add(new(
(r.IsDBNull(0) ? null : r.GetString(0), r.IsDBNull(1) ? null : r.GetString(1), r.IsDBNull(2) ? null : r.GetString(2), r.IsDBNull(3) ? null : r.GetString(3), r.GetString(4), r.GetInt32(5), r.GetString(6), r.GetBoolean(7), r.GetInt32(8)),
(r.GetString(9), r.GetInt32(10), r.GetString(11), r.GetString(12), r.GetString(13), r.GetInt32(14), r.GetString(15), r.GetDouble(16), r.GetDouble(17))
(r.IsDBNull(0) ? null : r.GetString(0), r.IsDBNull(1) ? null : r.GetString(1), r.IsDBNull(2) ? null : r.GetString(2), r.IsDBNull(3) ? null : r.GetString(3), r.GetString(4), r.GetInt32(5), r.GetString(6), r.GetInt32(7)),
(r.GetString(8), r.GetInt32(9), r.GetString(10), r.GetString(11), r.GetString(12), r.GetInt32(13), r.GetString(14), r.GetDouble(15), r.GetDouble(16))
));
}
await ExportAsync(rows);
}
public void Export(int year) {
ExportAsync(year).GetAwaiter().GetResult();
}
public override string FormatRow(Row row) {
var (member, delivery) = row;
var (lfBisNr, familyName, name, billingName, address, plz, ort, vollablieferer, area) = member;
var (lfBisNr, familyName, name, billingName, address, plz, ort, area) = member;
var (date, weight, type, sortid, qualid, year, hkid, kmw, oe) = delivery;
var (n1, n2) = billingName == null ? (familyName, name) : Utils.SplitName(billingName, familyName);
var (a1, a2) = Utils.SplitAddress(address);
var memberData = $"{lfBisNr};{n1};{n2};{a1};\t{a2};{plz};{ort}";
var deliveryData = $"{string.Join(".", date.Split("-").Reverse())};{weight};TB;{(type == "W" ? "J" : "")};{(type == "R" ? "J" : "")};{sortid};;;{qualid};{year};{hkid};{kmw:0.0};{oe:0}";
var vollData = $"{(vollablieferer ? "J" : "N")};{(vollablieferer ? area / 10_000.0 : null):N4};{(vollablieferer ? area / 10_000.0 : null):N4};{area / 10_000.0:N4}";
var vollData = $"N;;;{area / 10_000.0}";
return $"{_clientData};{memberData};{deliveryData};{vollData}";
}
@@ -7,7 +7,7 @@ using System.Text;
using System.Threading.Tasks;
namespace Elwig.Helpers.Export {
public abstract class CsvFile<T> : IExporter<T> {
public abstract class Csv<T> : IExporter<T> {
public static string FileExtension => "csv";
@@ -15,11 +15,9 @@ namespace Elwig.Helpers.Export {
protected readonly char Separator;
protected string? Header;
public CsvFile(string filename, char separator = ';') :
this(filename, separator, Utils.UTF8) {
}
public Csv(string filename, char separator = ';') : this(filename, separator, Utils.UTF8) { }
public CsvFile(string filename, char separator, Encoding encoding) {
public Csv(string filename, char separator, Encoding encoding) {
_writer = new StreamWriter(filename, false, encoding);
Separator = separator;
}
@@ -29,6 +27,11 @@ namespace Elwig.Helpers.Export {
_writer.Dispose();
}
public ValueTask DisposeAsync() {
GC.SuppressFinalize(this);
return _writer.DisposeAsync();
}
public async Task ExportAsync(IEnumerable<T> data, IProgress<double>? progress = null) {
progress?.Report(0.0);
int count = data.Count() + 2, i = 0;
@@ -45,28 +48,14 @@ namespace Elwig.Helpers.Export {
progress?.Report(100.0);
}
public void Export(IEnumerable<T> data, IProgress<double>? progress = null) {
ExportAsync(data, progress).GetAwaiter().GetResult();
}
public string FormatRow(IEnumerable row) {
return string.Join(Separator, row);
}
public abstract string FormatRow(T row);
}
public class CsvSimple : CsvFile<IEnumerable<object?>> {
public CsvSimple(string filename, char separator, Encoding encoding) :
base(filename, separator, encoding) {
}
public CsvSimple(string filename, char separator = ';') :
base(filename, separator) {
}
public override string FormatRow(IEnumerable<object?> row) {
return string.Join(Separator, row.Select(i => {
var str = $"{i}";
return str.Contains(Separator) || str.Contains('\n') ? $"\"{str.Replace("\"", "\"\"")}\"" : str;
}));
}
}
}
-259
View File
@@ -1,259 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace Elwig.Helpers.Export {
public static class Database {
private static async Task<(long? ApplicationId, string? UserVersion, long? SchemaVersion, long FileSize)> GetMeta() {
long size = new FileInfo(App.Config.DatabaseFile).Length;
using var cnx = await AppDbContext.ConnectAsync();
var applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id");
var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version");
var schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version");
return (applId, userVers != null ? $"{userVers >> 24}.{(userVers >> 16) & 0xFF}.{(userVers >> 8) & 0xFF}.{userVers & 0xFF}" : null, schemaVers, size);
}
public static async Task ExportSqlite(string filename, bool zipFile) {
var tmp = filename + ".tmp";
try {
File.Delete(tmp);
if (zipFile) {
using var zip = ZipFile.Open(tmp, ZipArchiveMode.Create);
var version = zip.CreateEntry("version", CompressionLevel.NoCompression);
using (var writer = new StreamWriter(version.Open(), Utils.UTF8)) {
await writer.WriteAsync("elwig:1");
}
var (applId, userVers, schemaVers, size) = await GetMeta();
var meta = zip.CreateEntry("meta.json", CompressionLevel.NoCompression);
using (var writer = new StreamWriter(meta.Open(), Utils.UTF8)) {
var obj = new JsonObject {
["timestamp"] = $"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}",
["zwstid"] = App.ZwstId,
["device"] = Environment.MachineName,
["database"] = new JsonObject {
["application_id"] = applId,
["user_version"] = userVers,
["schema_version"] = schemaVers,
["file_size"] = size,
},
};
await writer.WriteAsync(obj.ToJsonString(Utils.JsonOpts));
}
var db = zip.CreateEntryFromFile(App.Config.DatabaseFile, "database.sqlite3", CompressionLevel.SmallestSize);
} else {
File.Copy(App.Config.DatabaseFile, tmp);
}
File.Move(tmp, filename, true);
} finally {
File.Delete(tmp);
}
}
public static async Task ExportSql(string filename, bool zipFile) {
var tmp = filename + ".tmp";
try {
File.Delete(tmp);
if (zipFile) {
using var zip = ZipFile.Open(tmp, ZipArchiveMode.Create);
var version = zip.CreateEntry("version", CompressionLevel.NoCompression);
using (var writer = new StreamWriter(version.Open(), Utils.UTF8)) {
await writer.WriteAsync("elwig:1");
}
var (applId, userVers, schemaVers, size) = await GetMeta();
var meta = zip.CreateEntry("meta.json", CompressionLevel.NoCompression);
using (var writer = new StreamWriter(meta.Open(), Utils.UTF8)) {
var obj = new JsonObject {
["timestamp"] = $"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}",
["zwstid"] = App.ZwstId,
["device"] = Environment.MachineName,
["database"] = new JsonObject {
["application_id"] = applId,
["user_version"] = userVers,
["schema_version"] = schemaVers,
["file_size"] = size,
},
};
await writer.WriteAsync(obj.ToJsonString(Utils.JsonOpts));
}
var sql = zip.CreateEntry("database.sql", CompressionLevel.SmallestSize);
using (var writer = new StreamWriter(sql.Open(), Utils.UTF8)) {
await ExportSql(writer);
}
} else {
using var stream = File.OpenWrite(tmp);
using var writer = new StreamWriter(stream, Utils.UTF8);
await ExportSql(writer);
}
File.Move(tmp, filename, true);
} finally {
File.Delete(tmp);
}
}
public static async Task ExportSql(StreamWriter writer) {
using var cnx = await AppDbContext.ConnectAsync();
var tables = new List<(string Name, string Sql)>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = "SELECT name, sql FROM sqlite_schema WHERE type = 'table'";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
tables.Add((reader.GetString(0), reader.GetString(1)));
}
}
var applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version") ?? 0;
var schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0;
await writer.WriteLineAsync($"-- Elwig database dump, {DateTime.Now:yyyy-MM-dd, HH:mm:ss}");
await writer.WriteLineAsync($"-- {Environment.MachineName}, Zwst. {App.BranchName}, {App.Client.Name}");
await writer.WriteLineAsync("BEGIN TRANSACTION;");
await writer.WriteLineAsync("PRAGMA foreign_keys=OFF;");
await writer.WriteLineAsync($"PRAGMA application_id=0x{applId:X8};");
await writer.WriteLineAsync($"PRAGMA user_version=0x{userVers:X8};");
foreach (var t in tables) {
await writer.WriteAsync(t.Sql);
await writer.WriteLineAsync(";");
var columnNames = new List<string>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"PRAGMA table_info({t.Name})";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
columnNames.Add(reader.GetString(1));
}
}
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT {string.Join(',', columnNames)} FROM {t.Name}";
using var reader = await cmd.ExecuteReaderAsync();
var columns = await reader.GetColumnSchemaAsync();
var values = new object[reader.FieldCount];
while (await reader.ReadAsync()) {
await writer.WriteAsync($"INSERT INTO {t.Name} VALUES (");
reader.GetValues(values);
for (int i = 0; i < columns.Count; i++) {
var c = columns[i];
var v = values[i];
if (i > 0) await writer.WriteAsync(",");
if (v == null || v is DBNull) {
await writer.WriteAsync("NULL");
} else if (c.DataTypeName == "TEXT") {
await writer.WriteAsync($"'{v.ToString()?.Replace("'", "''")}'");
} else {
await writer.WriteAsync(v.ToString()?.Replace(',', '.'));
}
}
await writer.WriteLineAsync(");");
}
}
}
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = "SELECT sql FROM sqlite_schema WHERE type != 'table' AND sql IS NOT NULL";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
await writer.WriteAsync(reader.GetString(0));
await writer.WriteLineAsync(";");
}
}
await writer.WriteLineAsync($"PRAGMA schema_version={schemaVers};");
await writer.WriteLineAsync("PRAGMA foreign_keys=ON;");
await writer.WriteLineAsync("COMMIT;");
await writer.WriteLineAsync("VACUUM;");
}
public static async Task Import(string filename) {
if (filename.EndsWith(".sql")) {
await ImportSql(filename, false);
} else if (filename.EndsWith(".sql.zip")) {
await ImportSql(filename, true);
} else if (filename.EndsWith(".sqlite3")) {
await ImportSqlite(filename, false);
} else if (filename.EndsWith(".sqlite3.zip")) {
await ImportSqlite(filename, true);
} else {
throw new ArgumentException($"Unknown file extension for importing: '{filename}'");
}
}
public static async Task ImportSql(string filename, bool zipFile = false) {
if (zipFile) {
using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
await zip.CheckIntegrity();
foreach (var entry in zip.Entries) {
if (entry.Name.EndsWith(".sql")) {
using var stream = entry.Open();
using var reader = new StreamReader(stream, Utils.UTF8);
await ImportSql(reader);
return;
}
}
throw new FileFormatException("ZIP archive has to contain at least one .sql file");
} else {
using var stream = File.Open(filename, FileMode.Open);
using var reader = new StreamReader(stream, Utils.UTF8);
await ImportSql(reader);
}
}
public static async Task ImportSqlite(string filename, bool zipFile = false, bool keepSource = true) {
if (zipFile) {
var newName = Path.ChangeExtension(App.Config.DatabaseFile, ".new.sqlite3");
try {
using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
await zip.CheckIntegrity();
foreach (var entry in zip.Entries) {
if (entry.Name.EndsWith(".sqlite3")) {
entry.ExtractToFile(newName);
await ImportSqlite(newName, keepSource: false);
return;
}
}
throw new FileFormatException("ZIP archive has to contain at least one .sqlite3 file");
} finally {
if (File.Exists(newName)) File.Delete(newName);
}
}
var oldName = Path.ChangeExtension(App.Config.DatabaseFile, ".old.sqlite3");
File.Move(App.Config.DatabaseFile, oldName, true);
if (keepSource) {
File.Copy(filename, App.Config.DatabaseFile, false);
} else {
File.Move(filename, App.Config.DatabaseFile, false);
}
using var cnx = await AppDbContext.ConnectAsync();
await cnx.ExecuteBatch("VACUUM");
}
public static async Task ImportSql(StreamReader reader) {
var newName = Path.ChangeExtension(App.Config.DatabaseFile, ".new.sqlite3");
File.Delete(newName);
try {
using (var cnx = await AppDbContext.ConnectAsync($"Data Source=\"{newName}\"; Mode=ReadWriteCreate; Foreign Keys=False; Cache=Default; Pooling=False")) {
await cnx.ExecuteBatch(await reader.ReadToEndAsync());
}
await ImportSqlite(newName, keepSource: false);
} finally {
if (File.Exists(newName)) File.Delete(newName);
}
}
}
}
+10 -1
View File
@@ -9,7 +9,7 @@ using System.Security;
using System.Threading.Tasks;
namespace Elwig.Helpers.Export {
public class Ebics(PaymentVar variant, string filename, int version, Ebics.AddressMode mode = Ebics.AddressMode.Full) : IExporter<Transaction> {
public class Ebics(PaymentVar variant, string filename, int version, Ebics.AddressMode mode = Ebics.AddressMode.Full) : IBankingExporter {
public enum AddressMode { Omit = 0, Lines = 1, Full = 2 }
@@ -28,6 +28,15 @@ namespace Elwig.Helpers.Export {
Writer.Dispose();
}
public ValueTask DisposeAsync() {
GC.SuppressFinalize(this);
return Writer.DisposeAsync();
}
public void Export(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) {
ExportAsync(transactions, progress).GetAwaiter().GetResult();
}
public async Task ExportAsync(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) {
if (transactions.Any(tx => tx.Amount < 0)) {
throw new ArgumentException("Tranaction amount may not be negative");
+15
View File
@@ -0,0 +1,15 @@
using Elwig.Models.Dtos;
using System;
namespace Elwig.Helpers.Export {
public class Elba : Csv<Transaction>, IBankingExporter {
public static new string FileExtension => "elba";
public Elba(string filename) : base(filename) { }
public override string FormatRow(Transaction row) {
throw new NotImplementedException();
}
}
}
File diff suppressed because it is too large. Load diff
+8
View File
@@ -0,0 +1,8 @@
using Elwig.Models.Dtos;
namespace Elwig.Helpers.Export {
/// <summary>
/// Interface for exporting banking data
/// </summary>
public interface IBankingExporter : IExporter<Transaction> { }
}
+2 -4
View File
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
namespace Elwig.Helpers.Export {
public interface IExporter<T> : IDisposable {
public interface IExporter<T> : IDisposable, IAsyncDisposable {
/// <summary>
/// The default file extension of the exported files to be used (whithout a preceding ".")
/// </summary>
@@ -14,9 +14,7 @@ namespace Elwig.Helpers.Export {
/// </summary>
/// <param name="data">The data to be exported</param>
/// <param name="progress">The progress object to report to</param>
void Export(IEnumerable<T> data, IProgress<double>? progress = null) {
ExportAsync(data, progress).GetAwaiter().GetResult();
}
void Export(IEnumerable<T> data, IProgress<double>? progress = null);
/// <summary>
/// Asynchronosly export the given data to the given file.
@@ -109,13 +109,8 @@ namespace Elwig.Helpers.Export {
<style:paragraph-properties fo:text-align="center"/>
<style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/>
</style:style>
<style:style style:name="sectionheader" style:family="table-cell" style:parent-style-name="default">
<style:table-cell-properties style:text-align-source="fix" style:repeat-content="false"/>
<style:paragraph-properties fo:text-align="left"/>
<style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold" fo:font-size="14pt"/>
</style:style>
<style:style style:name="th" style:family="table-cell" style:parent-style-name="default">
<style:table-cell-properties style:text-align-source="fix" style:repeat-content="false" style:vertical-align="middle" fo:wrap-option="wrap"/>
<style:table-cell-properties style:text-align-source="fix" style:repeat-content="false" style:vertical-align="middle"/>
<style:paragraph-properties fo:text-align="center"/>
<style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/>
</style:style>
@@ -216,59 +211,35 @@ namespace Elwig.Helpers.Export {
}
}
await Content.WriteAsync($"""
<table:table-row>
{FormatCell(table.FullName, colSpan: totalSpan, style: "header")}
</table:table-row>
<table:table-row>
{FormatCell(table.Subtitle, colSpan: totalSpan, style: "subheader")}
</table:table-row>
<table:table-row>
<table:table-cell table:number-columns-repeated="{totalSpan}"/>
</table:table-row>
""");
await Content.WriteAsync(
$" <table:table-row>\r\n" +
FormatCell(table.FullName, colSpan: totalSpan, style: "header") +
$" </table:table-row>\r\n" +
$" <table:table-row>\r\n" +
FormatCell(table.Subtitle, colSpan: totalSpan, style: "subheader") +
$" </table:table-row>\r\n" +
$" <table:table-row>\r\n" +
$" <table:table-cell table:number-columns-repeated=\"{totalSpan}\"/>\r\n" +
$" </table:table-row>\r\n" +
$" <table:table-row>\r\n");
foreach (var (name, span, units) in table.ColumnNames.Zip(table.ColumnSpans, table.ColumnUnits)) {
var hasUnits = units.Length > 0;
await Content.WriteAsync(FormatCell(name, colSpan: span, rowSpan: hasUnits ? 1 : 2, style: "th"));
}
await Content.WriteAsync(" </table:table-row>\r\n <table:table-row>\r\n");
foreach (var (span, units) in table.ColumnSpans.Zip(table.ColumnUnits)) {
if (units.Length == 0) {
await Content.WriteAsync($" <table:covered-table-cell table:number-columns-repeated=\"{span}\"/>\r\n");
continue;
}
foreach (var u in units) {
await Content.WriteAsync(FormatCell(u == null ? null : $"[{u}]", style: "th"));
}
}
await Content.WriteAsync(" </table:table-row>\r\n");
var firstSection = true;
foreach (var sec in table.GetData()) {
if (!firstSection) {
await Content.WriteAsync($"""
<table:table-row>
<table:table-cell table:number-columns-repeated="{totalSpan}"/>
</table:table-row>
<table:table-row>
<table:table-cell table:number-columns-repeated="{totalSpan}"/>
</table:table-row>
""");
}
if (sec.Name != null) {
await Content.WriteAsync($"""
<table:table-row>
{FormatCell(sec.Name, colSpan: totalSpan, style: "sectionheader")}
</table:table-row>
""");
}
await Content.WriteAsync(" <table:table-row>\r\n");
foreach (var (name, span, units) in table.ColumnNames.Zip(table.ColumnSpans, table.ColumnUnits)) {
var hasUnits = units.Length > 0;
await Content.WriteAsync(FormatCell(name, colSpan: span, rowSpan: hasUnits ? 1 : 2, style: "th"));
}
await Content.WriteAsync(" </table:table-row>\r\n <table:table-row>\r\n");
foreach (var (span, units) in table.ColumnSpans.Zip(table.ColumnUnits)) {
if (units.Length == 0) {
await Content.WriteAsync($" <table:covered-table-cell table:number-columns-repeated=\"{span}\"/>\r\n");
continue;
}
foreach (var u in units) {
await Content.WriteAsync(FormatCell(u == null ? null : $"[{u}]", style: "th"));
}
}
await Content.WriteAsync(" </table:table-row>\r\n");
foreach (var row in sec.Rows) {
await FormatRow(row, table.ColumnUnits, mergeSubRowCells);
}
firstSection = false;
foreach (var row in table.GetData()) {
await FormatRow(row, table.ColumnUnits, mergeSubRowCells);
}
await Content.WriteAsync(" </table:table>\r\n");
@@ -343,8 +314,7 @@ namespace Elwig.Helpers.Export {
}
c = $"<{ct} office:value-type=\"float\" calcext:value-type=\"float\" office:value=\"{v.ToString(CultureInfo.InvariantCulture)}\"{add}><text:p>{data}</text:p></{ct}>";
} else {
var lines = string.Join("", data.ToString()?.Split('\n').Select(l => $"<text:p>{SecurityElement.Escape(l)}</text:p>") ?? []);
c = $"<{ct} office:value-type=\"string\" calcext:value-type=\"string\"{add}>{lines}</{ct}>";
c = $"<{ct} office:value-type=\"string\" calcext:value-type=\"string\"{add}><text:p>{SecurityElement.Escape(data.ToString())}</text:p></{ct}>";
}
return $" {c}\r\n" + (colSpan > 1 ? $" <table:covered-table-cell table:number-columns-repeated=\"{colSpan - 1}\"/>\r\n" : "");
-62
View File
@@ -1,62 +0,0 @@
using Elwig.Models.Entities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Elwig.Helpers.Export {
public class VCard : IExporter<Member> {
public static string FileExtension => "vcf";
private readonly StreamWriter _writer;
public VCard(string filename) :
this(filename, Utils.UTF8) {
}
public VCard(string filename, Encoding encoding) {
_writer = new StreamWriter(filename, false, encoding);
}
public void Dispose() {
GC.SuppressFinalize(this);
_writer.Dispose();
}
public async Task ExportAsync(IEnumerable<Member> data, IProgress<double>? progress = null) {
progress?.Report(0.0);
int count = data.Count() + 1, i = 0;
foreach (var row in data) {
var billingAddr = row.BillingAddress != null ? $"ADR;TYPE=work;LANGUAGE=de;LABEL=\"{Escape(row.BillingAddress.FullName)}\\n{Escape(row.BillingAddress.Address)}\\n{row.BillingAddress.PostalDest.AtPlz?.Plz} {Escape(row.BillingAddress.PostalDest.AtPlz?.Ort.Name)}\\nÖsterreich\":;;{Escape(row.BillingAddress.Address)};{Escape(row.BillingAddress.PostalDest.AtPlz?.Ort.Name)};;{row.BillingAddress.PostalDest.AtPlz?.Plz};Österreich\r\n" : null;
var tel = string.Join("", row.TelephoneNumbers
.Where(n => n.Type != "fax")
.Select(n => $"TEL;TYPE={(n.Type == "mobile" ? "cell" : "voice")}:{Escape(n.Number)}\r\n"));
var email = string.Join("", row.EmailAddresses.Select(a => $"EMAIL:{Escape(a.Address)}\r\n"));
await _writer.WriteLineAsync($"""
BEGIN:VCARD
VERSION:4.0
UID:mg{row.MgNr}@{App.Client.NameToken.ToLower()}.elwig.at
NOTE:MgNr. {row.MgNr}
FN:{Escape(row.AdministrativeName)}
N:{Escape(row.Name)};{Escape(row.GivenName)};{Escape(row.MiddleName)};{Escape(row.Prefix)};{Escape(row.Suffix)}
KIND:{(row.IsJuridicalPerson ? "org" : "individual")}
ADR{(billingAddr == null ? "" : ";TYPE=home")};LANGUAGE=de;LABEL="{Escape(row.Address)}\n{row.PostalDest.AtPlz?.Plz} {Escape(row.PostalDest.AtPlz?.Ort.Name)}\nÖsterreich":;;{Escape(row.Address)};{Escape(row.PostalDest.AtPlz?.Ort.Name)};;{row.PostalDest.AtPlz?.Plz};Österreich
{billingAddr}{tel}{email}REV:{row.ModifiedAt.ToUniversalTime():yyyyMMdd\THHmmss\Z}
END:VCARD
""");
progress?.Report(100.0 * ++i / count);
}
await _writer.FlushAsync();
progress?.Report(100.0);
}
private static string? Escape(string? text) {
return text?.Replace("\\", "\\\\").Replace(",", "\\,").Replace(";", "\\;").Replace("\n", "\\n").Replace("\r", "");
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
namespace Elwig.Helpers {
public enum ExportMode {
Show, SaveList, SavePdf, Print, Email, Vcf, Export, Upload
Show, SaveList, SavePdf, Print, Email, Export, Upload
}
}
+1 -119
View File
@@ -1,22 +1,12 @@
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.IO.Hashing;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Elwig.Helpers {
public static partial class Extensions {
static partial class Extensions {
[LibraryImport("msvcrt.dll")]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
@@ -105,113 +95,5 @@ namespace Elwig.Helpers {
await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken);
progress.Report(100.0);
}
public static async Task CheckIntegrity(this ZipArchive zip) {
var crc = new Crc32();
foreach (var entry in zip.Entries) {
crc.Reset();
using var stream = entry.Open();
await crc.AppendAsync(stream);
if (crc.GetCurrentHashAsUInt32() != entry.Crc32)
throw new InvalidDataException($"CRC-32 mismatch in '{entry.FullName}'");
}
}
public static async Task ExecuteBatch(this SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.NextResultAsync()) ;
}
public static async Task ExecuteEmbeddedScript(this 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(this SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
return await cmd.ExecuteScalarAsync();
}
public static async Task QuickCheck(this SqliteConnection cnx) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = "PRAGMA quick_check";
using var reader = await cmd.ExecuteReaderAsync();
var list = new List<string>();
while (await reader.ReadAsync()) {
list.Add(reader.GetString(0));
}
if (list.Count != 1 || list[0] != "ok") {
throw new FileFormatException($"Integrity problems ({list.Count}):\n" + string.Join("\n", list.Take(50)));
}
}
public static async Task IntegrityCheck(this SqliteConnection cnx) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = "PRAGMA integrity_check";
using var reader = await cmd.ExecuteReaderAsync();
var list = new List<string>();
while (await reader.ReadAsync()) {
list.Add(reader.GetString(0));
}
if (list.Count != 1 || list[0] != "ok") {
throw new FileFormatException($"Integrity problems ({list.Count}):\n" + string.Join("\n", list.Take(50)));
}
}
public static async Task ForeignKeyCheck(this SqliteConnection cnx) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = "PRAGMA foreign_key_check";
using var reader = await cmd.ExecuteReaderAsync();
var list = new List<(string Table, long RowId, string Parent, long FkId)>();
while (await reader.ReadAsync()) {
var table = reader.GetString(0);
var rowid = reader.GetInt64(1);
var parent = reader.GetString(2);
var fkid = reader.GetInt64(3);
list.Add((table, rowid, parent, fkid));
}
if (list.Count > 0) {
throw new InvalidDataException($"Foreign key violations ({list.Count}):\n" + string.Join("\n", list.Take(50)
.Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}")));
}
}
public static IEnumerable<T> Join<T>(this IEnumerable<T> src, Func<T> separatorFactory) {
var srcArr = src.ToArray();
for (int i = 0; i < srcArr.Length; i++) {
yield return srcArr[i];
if (i < srcArr.Length - 1) {
yield return separatorFactory();
}
}
}
public static void AddToolTipCell(this Grid grid, string text, int row, int col, int colSpan = 1, bool bold = false, bool alignRight = false, bool alignCenter = false, Brush? color = null) {
var tb = new TextBlock() {
Text = text,
TextAlignment = alignRight ? TextAlignment.Right : alignCenter ? TextAlignment.Center : TextAlignment.Left,
Margin = new(0, 12 * row, 0, 0),
FontWeight = bold ? FontWeights.Bold : FontWeights.Normal,
};
if (color != null) tb.Foreground = color;
tb.SetValue(Grid.ColumnProperty, col);
tb.SetValue(Grid.ColumnSpanProperty, colSpan);
grid.Children.Add(tb);
}
public static (int Year, int Week) GetYearAndWeek(this DateTime date) {
return (ISOWeek.GetYear(date), ISOWeek.GetWeekOfYear(date));
}
public static (int Year, int Tertial) GetYearAndTertial(this DateTime date) {
return (date.Month <= 7 ? date.Year - 1 : date.Year, date.Month == 12 || date.Month <= 3 ? 2 : date.Month <= 7 ? 3 : 1);
}
}
}
-303
View File
@@ -1,303 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace Elwig.Helpers {
public static partial class Normalizer {
[GeneratedRegex(@"(?=(?:(?:bundes)?straße|gasse|weg|platz|allee|ring|zeile?|steig|hof|markt|kai|ufer|brücke|tor|stiege|promenade|gang|siedlung)\b)|[\s-]+", RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)]
private static partial Regex GeneratedStreetNameWordsRegex();
private static readonly Regex StreetNameWordsRegex = GeneratedStreetNameWordsRegex();
private static readonly HashSet<string> StreetTypes = [
"Straße", "Gasse", "Weg", "Platz", "Allee", "Ring", "Zeile", "Zeil", "Steig", "Hof",
"Markt", "Kai", "Ufer", "Brücke", "Tor", "Stiege", "Promenade", "Gang", "Siedlung"];
private static readonly HashSet<string> StreetArticles = ["der", "die", "das", "des", "dem", "den"];
private static readonly HashSet<string> StreetPrepositions = [
"an", "am", "auf", "aus", "bei", "beim", "hinter", "hinterm", "in", "im", "ins", "nach",
"neben", "über", "unter", "von", "vom", "van", "vor", "zu", "zum", "zur", "zwischen",
"durch", "mit"];
private static readonly HashSet<string> StreetNumbers = [ "Zwei", "Drei", "Vier", "Fünf", "Sechs", "Sieben" ];
private static readonly HashSet<string> StreetAdjectives = [
"alter", "alte", "alten", "altem", "altes",
"äußere", "äußere", "äußeren", "äußerem", "äußeres",
// "breiter", "breite", "breiten", "breitem", "breites",
"dritter", "dritte", "dritten", "drittem", "drittes",
"erster", "erste", "ersten", "erstem", "erstes",
"großer", "große", "großen", "großem", "großes",
"hoher", "hohe", "hohen", "hohem", "hohes",
"innerer", "innere", "inneren", "innerem", "inneres",
"kleiner", "kleine", "kleinen", "kleinem", "kleines",
"kurzer", "kurze", "kurzen", "kurzem", "kurzes",
"langer", "lange", "langen", "langem", "langes",
"linker", "linke", "linken", "linkem", "linkes",
"mittlerer", "mittlere", "mittleren", "mittlerem", "mittleres",
"neuer", "neue", "neuen", "neuem", "neues",
"oberer", "obere", "oberen", "oberem", "oberes",
"rechter", "rechte", "rechten", "rechtem", "rechtes",
"schmaler", "schmale", "schmalen", "schmalem", "schmales",
"schöner", "schöne", "schönen", "schönem", "schönes",
"unterer", "untere", "unteren", "unterem", "unteres",
"hinterer", "hintere", "hinteren", "hinterem", "hinteres",
"vorderer", "vordere", "vorderen", "vorderem", "vorderes",
"runder", "runde", "runden", "rundem", "rundes",
"kalter", "kalte", "kalten", "kaltem", "kaltes"];
private static readonly HashSet<string> StreetLastNames = [
"Habsburger", "Lothringer", "Leitzenberger", "Luyderer", "Preiner", "Radinger", "Seefelder", "Staribacher", "Gschmeidler",
"Hannover", "Münster", "Salzgitter", "Speyer", "Trier"];
private static readonly HashSet<string> StreetPersonTitles = [
"St.", "Familie",
"Dr.", "DDr.", "Hofrat", "Baurat", "Kommerzialrat", "Dir.", "Ing.", "Prof.", "Mag.", "DI", "OSR", "Präsident",
"Landeshauptmann", "LH", "Bürgermeister", "Bgm.", "Minister",
"Kaiser", "Kaiserin", "König", "Königin", "Erzherzog", "Erzherzogin", "Herzog", "Fürst", "Graf", "Gräfin", "Markgraf", "Kronprinz", "Prinz", "Ritter", "Freiherr", "Lord",
"Abt", "Bischof", "Propst", "Dechant", "Kardinal", "Monsignore", "Pfarrer", "Pater", "Prälat",
"Oberst"];
private static readonly HashSet<string> StreetGivenNames = [
"Adalbert", "Adam", "Adele", "Adelheid", "Adolf", "Adolph", "Adrian", "Albert", "Albrecht", "Alexander", "Alfons", "Alfred", "Alois", "Aloisia",
"Andreas", "Andrea", "Andre", "Anna", "Anton", "Arnold", "Arthur", "August", "Augustin", "Augustine", "Agnes", "Angelika", "Ada", "Adelheid", "Anni", "Anita",
"Barbara", "Beate", "Beatrix", "Benedikt", "Bernhard", "Berta", "Bertha", "Berthold", "Brüder", "Bruno", "Brigitte",
"Camillo", "Carl", "Caspar", "Catharina", "Christian", "Christine", "Christof", "Christoph", "Clara", "Clemens", "Conrad", "Claus", "Camillo", "Claudia",
"Daniel", "Dietmar", "Dietrich",
"Edgar", "Edi", "Edith", "Edmund", "Egon", "Elsa", "Eduard", "Emmerich", "Erhard", "Erich", "Ernst", "Erwin", "Eugen", "Ewald", "Eberhard",
"Elisabeth", "Emanuel", "Emerich", "Emil", "Emma", "Engelbert", "Elias",
"Franz", "Ferdinand", "Friedrich", "Felix", "Florian", "Fritz", "Franziska", "Fred",
"Georg", "Gregor", "Gustav", "Gabriel", "Gebrüder", "Gabriele", "Gerhard", "Gerhart", "Gertrude", "Gottfried", "Gottlieb", "Grete",
"Günther", "Günter",
"Hans", "Hanns", "Heinrich", "Heinz", "Helmut", "Helmuth", "Hermann", "Herta", "Herbert", "Hubert", "Harald", "Horst", "Helene", "Helga",
"Hieronymus", "Hilde", "Hugo",
"Isabelle", "Ignaz", "Ilse", "Ingeborg", "Isidor",
"Josef", "Josefine", "Joseph", "Johann", "Johanna", "Johan", "Johannes", "Hannes", "Harry", "Jakob", "Jacob", "Jan", "Julian", "Julius", "Julia", "Jürgen",
"Karl", "Kasimir", "Konrad", "Klemens", "Klara", "Katharina", "Kurt", "Kaspar", "Klaus", "Koloman", "Konstantin", "Killian",
"Leopold", "Leo", "Lorenz", "Lothar", "Louise", "Ludmilla", "Ludwig", "Lukas", "Ladislaus", "Lambert", "Laurenz", "Liese",
"Lise", "Lilly", "Lina", "Lothar", "Lotte", "Luise", "Leonhard", "Lotte", "Martha",
"Maria", "Marie", "Michael", "Martin", "Max", "Maximilian", "Manuel", "Manuela", "Moritz", "Marianne", "Manfred", "Marc",
"Mathias", "Matthias", "Matthäus",
"Margarete", "Margarethe",
"Nico", "Niko", "Niklas", "Niclas", "Nikolaus", "Norbert",
"Otto", "Ottokar", "Oskar", "Olga", "Oswald", "Othmar",
"Paul", "Pauline", "Patrick", "Pepi", "Peter", "Philipp", "Philip", "Petrus",
"Robert", "Rudolf", "Richard", "Ruprecht", "Raffael", "Rolf", "Roman", "Rosa", "Rosalia", "Rupert", "Raimund",
"Sigmund", "Siegfried", "Simon", "Stanislaus", "Stefan", "Stephan", "Sepp", "Samuel", "Sebastian", "Susi",
"Theodor", "Thomas", "Theresia", "Thaddäus", "Theo", "Theophil", "Tina", "Titus", "Toni", "Trude",
"Ullrich", "Ulrich", "Udo",
"Viktor", "Vinzenz", "Victor",
"Wolfgang", "Willibald", "Wilhelm", "Willhelm", "Wilhelmine", "Walter", "Walther", "Wenzel", "Werner", "Willi",
"Zacharias"];
private static readonly HashSet<string> LocalityNames = [
"Groß-Enzersdorf",
"Groß Burgstall", "Klein Eberharts", "Klein-Engersdorf", "Groß-Enzersdorf", "Klein-Erlauf",
"Groß Gerungs", "Klein Gundholz", "Klein-Harras", "Klein-Heinrichschlag", "Groß-Höbarten",
"Klein-Jetzelsdorf", "Klein-Mariazell", "Groß Meinharts", "Klein-Meiseldorf", "Groß-Neusiedl", "Klein-Neusiedl", "Klein-Pöchlarn",
"Groß-Radischen", "Klein-Radischen", "Groß Raßberg", "Klein Reinprechts", "Klein-Reinprechtsdorf", "Groß-Reipersdorf", "Klein-Ruprechts",
"Groß-Schollach", "Klein-Schollach", "Groß-Schweinbarth", "Groß-Siegharts", "Klein-Siegharts", "Groß Sierning",
"Klein-Ulrichschlag", "Klein Weinberg", "Klein Wetzles", "Klein-Wien",
];
private static bool IsGivenName(this string word) {
return StreetGivenNames.Contains(word);
}
private static bool IsFamilyName(this string word) {
if (StreetLastNames.Contains(word))
return true;
return false;
}
private static bool IsPersonalName(this string word) {
return word.IsGivenName() || word.IsFamilyName();
}
private static bool IsGeographicalDerivativeName(this string word) {
if (!word.EndsWith("er") || word.IsPersonalName() || word == "Weißer")
return false;
return word.StartsWith("St. ") || word.StartsWith("Ober ") || word.StartsWith("Unter ") || word.StartsWith("Maria") ||
word.EndsWith("sdorfer") || word.EndsWith("ndorfer") || word.EndsWith("ldorfer") ||
word.EndsWith("felder") ||
word.EndsWith("sthaler") || word.EndsWith("nthaler") || word.EndsWith("lthaler") ||
word.EndsWith("dauer") || word.EndsWith("lauer") || word.EndsWith("mauer") || word.EndsWith("nauer") || word.EndsWith("ßauer") ||
word.EndsWith("lbacher") || word.EndsWith("nbacher") || word.EndsWith("ßbacher") ||
word.EndsWith("ringer") || word.EndsWith("singer") ||
word.EndsWith("sbrunner") || word.EndsWith("nbrunner") || word.EndsWith("lbrunner") || word.EndsWith("abrunner") || word.EndsWith("ingbrunner") ||
word.EndsWith("sburger") || word.EndsWith("nburger") || word.EndsWith("mburger") || word.EndsWith("lburger") || word.EndsWith("neuburger") ||
word.EndsWith("berger") ||
word.EndsWith("winkler") ||
word.EndsWith("lsteiner") || word.EndsWith("nsteiner") ||
word.EndsWith("städter") ||
word.EndsWith("stättner") || word.EndsWith("stettner") ||
word.EndsWith("brücker") ||
word.EndsWith("further") || word.EndsWith("furter") ||
word.EndsWith("aaer") ||
word.EndsWith("seer") || word.EndsWith("leer") ||
word.EndsWith("tzner") ||
word.EndsWith("zeller") ||
word.EndsWith("rther") ||
word.EndsWith("sdner") ||
word.EndsWith("gerner") || word.EndsWith("berner") ||
word.EndsWith("frieder") ||
word.EndsWith("litzer") || word.EndsWith("mitzer") || word.EndsWith("eutzer") ||
word.EndsWith("loner") ||
word.EndsWith("skirchner") || word.EndsWith("nkirchner") || word.EndsWith("ingkirchner") ||
word.EndsWith("ziger") ||
word.EndsWith("ließer") ||
word.EndsWith("gerer") ||
word.EndsWith("kreuzer") ||
word == "Wiener" || word == "Brünner" || word == "Znaimer" || word == "Triester" || word == "Prager" || word == "Berliner" || word == "Basler" ||
word == "Retzer" || word == "Tullner" || word == "Badner" ||
word == "Gruber" || word == "Sulzer" || word == "Harraser" || word == "Dörfleser" || word == "Neusiedler" ||
word == "Dornauer" || word == "Siebenhauser" || word == "Alser" ||
word == "Bockenheimer";
}
public static string NormalizeAddress(string address, string? locality = null) {
var idx = address.IndexOf(',');
if (idx >= 0) address = address[..idx];
var (name, nr) = Utils.SplitAddress(address);
return (NormalizeStreetName(name, locality) + " " + nr).Trim();
}
public static string NormalizeStreetName(string name, string? locality = null) {
name = name.Trim().ToLowerInvariant();
if (name == "") {
return "";
} else if (name == "nr.") {
if (locality == null)
return "Nr.";
name = locality.Trim().ToLowerInvariant();
}
name = name.Replace("hautp", "haupt")
.Replace("leop.", "leopold ").Replace("ferd.", "ferdinand ").Replace("gottfr.", "gottfried ")
.Replace("heinr.", "heinrich").Replace("joh.", "johann ").Replace("jos.", "josef ")
.Replace("msgr.", "monsignore ").Replace("pf.", "pfarrer ")
.Replace("präs.", "präsident ");
name = name.Replace("str.", "straße").Replace("strasse", "straße")
.Replace("bruecke", "brücke").Replace("pl.", "platz")
.Replace("gr.", "groß").Replace("kl.", "klein")
.Replace("sankt-", "st.").Replace("sankt ", "st.")
.Replace("wr.", "wiener ")
.Replace(".", ". ");
List<string> words = [.. StreetNameWordsRegex.Split(name)
.Where(w => !string.IsNullOrWhiteSpace(w))
.Select(w => StreetArticles.Contains(w) || StreetPrepositions.Contains(w) ? w : string.Concat(w[0..1].ToUpper(), w.AsSpan(1)))
.Select(w => w == "Ddr." ? "DDr." : w == "Lh" || w == "Osr" ? w.ToUpper() : w)
];
if (words[0] == "Di") words[0] = "DI";
var temp = "|" + string.Join('|', words) + "|";
temp = temp.Replace("|Dipl.|Ing.|", "|DI|").Replace("|Dipl|Ing.|", "|DI|")
.Replace("|Kaiser|Ebersdorf|", "|Kaiser-Ebersdorf|")
.Replace("|Kalk|Deutz|", "|Kalk-Deutz|").Replace("|Kalk|Deutzer|", "|Kalk-Deutzer|")
.Replace("|Hohe|Wand|", "|Hohe Wand|").Replace("|Hohen|Wand|", "|Hohen Wand|").Replace("|Hohewand|", "|Hohe Wand|")
.Replace("|Alten|Markt|", "|Altenmarkt|")
.Replace("|Kirch|Hof|", "|Kirchhof|").Replace("|Bisc|Hof|", "|Bischof|")
.Replace("|Wiener|Neustadt|", "|Wiener Neustadt|").Replace("|Wiener|Neustädter|", "|Wiener Neustädter|")
.Replace("|St.|Pölten|", "|St. Pölten|").Replace("|St.|Pöltner|", "|St. Pöltner|")
.Replace("|Gross|", "|Groß|")
.Replace("|Nr|", "|Nr.|").Replace("|Und|", "|und|");
words = [.. temp.Trim('|').Split('|')];
if (words.Count > 1 && (words[0] == "Groß" || words[0] == "Klein") && !StreetTypes.Contains(words[1])) {
var w = words[0] + words[1].ToLowerInvariant();
words.RemoveRange(0, 2);
words.Insert(0, w);
}
if (words[0].StartsWith("Groß") || words[0].StartsWith("Klein")) {
var repl = LocalityNames.FirstOrDefault(n => n.Replace(" ", "").Replace("-", "").Equals(words[0], StringComparison.InvariantCultureIgnoreCase));
if (repl != null) words[0] = repl;
}
if (words.Count > 2 && words[^2] == "in") {
// remove suffix "in ..."
words.RemoveRange(words.Count - 2, 2);
}
char sep = name.Contains('-') ? '-' : ' ';
if (words.Count <= 1) {
// single word
} else if (StreetTypes.Contains(words[0]) && !StreetTypes.Contains(words[^1])) {
// "Straße der ..." / "Straße des ..."
sep = ' ';
} else if (StreetTypes.Contains(words[^1])) {
// "... Straße" / "...straße"
var type = words[^1];
var isCompound = true;
var isAdjective = StreetAdjectives.Contains(words[^2].ToLowerInvariant());
var isGeographical = false;
var isPerson = false;
var geo = words.FindLastIndex(IsGeographicalDerivativeName);
if (geo >= 0 && geo < words.Count - 2 && words[^2] != "Haupt" && words[^2] != "Bundes" && words[^2] != "Keller") {
isPerson = true;
isGeographical = true;
} else if (words.Any(w => w.Contains(' ') && !w.IsGeographicalDerivativeName())) {
isPerson = true;
isGeographical = true;
} else if (StreetNumbers.Contains(words[0])) {
isPerson = true;
isGeographical = true;
} else if (!StreetPrepositions.Contains(words[0]) && !StreetArticles.Contains(words[0]) && words.FindIndex(StreetPrepositions.Contains) > 0) {
isPerson = true;
isGeographical = true;
}
if (words.Count(w => w.Contains('.')) >= 2 || words.Contains("van") || words.Contains("von") || words.Contains("St.") ||
StreetPersonTitles.Contains(words[0]) || words.Any(w => w.IsGivenName()))
{
isPerson = true;
}
if (!isPerson && isAdjective || StreetArticles.Contains(words[^2]) || StreetPrepositions.Contains(words[^2])) {
isCompound = false;
}
if (!isPerson && words[^2].EndsWith("er") && !StreetLastNames.Contains(words[^2])) {
isGeographical = IsGeographicalDerivativeName(words[^2]);
} else if (words.Count >= 3 && words.Contains("Bad")) {
isPerson = true;
isGeographical = true;
}
sep = isPerson ? '-' : ' ';
if (isPerson && words.Count == 2 && !isGeographical) {
isPerson = false;
isCompound = true;
}
if (isGeographical || isPerson) {
isCompound = false;
}
if (isCompound) {
var w = words[^2] + type.ToLowerInvariant();
words.RemoveRange(words.Count - 2, 2);
words.Add(w);
}
if (isPerson) {
words = [.. words.Select(w => w.Replace(' ', '-'))];
}
} else {
// unknown
}
if (sep == '-' && StreetPrepositions.Contains(words[0]) && words[0] != "von" && words[0] != "van") {
sep = ' ';
int i;
for (i = 0; StreetPrepositions.Contains(words[i]) && i < words.Count; i++);
var word = string.Join('-', words.Skip(i));
words = [.. words.Take(i).Append(word)];
}
var norm = string.Join(sep, words);
return string.Concat(norm[0..1].ToUpperInvariant(), norm.AsSpan(1));
}
}
}
+35
View File
@@ -0,0 +1,35 @@
using RazorLight;
using System;
using System.Threading.Tasks;
namespace Elwig.Helpers.Printing {
public static class Html {
private static RazorLightEngine? Engine = null;
public static bool IsReady => Engine != null;
public static async Task Init(Action? evtHandler = null) {
var e = new RazorLightEngineBuilder()
.UseFileSystemProject(App.DocumentsPath)
.UseMemoryCachingProvider()
.Build();
await e.CompileTemplateAsync("Document");
await e.CompileTemplateAsync("BusinessDocument");
await e.CompileTemplateAsync("BusinessLetter");
await e.CompileTemplateAsync("DeliveryNote");
await e.CompileTemplateAsync("CreditNote");
await e.CompileTemplateAsync("DeliveryJournal");
await e.CompileTemplateAsync("Letterhead");
await e.CompileTemplateAsync("DeliveryConfirmation");
Engine = e;
evtHandler?.Invoke();
}
public static async Task<string> CompileRenderAsync(string key, object model) {
if (Engine == null) throw new InvalidOperationException("The razor engine has not been initialized yet");
return await Engine.CompileRenderAsync(key, model);
}
}
}
+81 -10
View File
@@ -1,17 +1,77 @@
using Elwig.Services;
using System.Threading.Tasks;
using Elwig.Windows;
using System.Diagnostics;
using System;
using System.IO;
using System.Collections.Generic;
using System.Windows;
using System.Text.RegularExpressions;
using System.Linq;
using System.Net.Sockets;
using PdfiumViewer;
using System.Drawing.Printing;
namespace Elwig.Helpers.Printing {
public static class Pdf {
public static void Init(Action? evtHandler = null) {
PdfiumNative.FPDF_InitLibrary();
private static readonly string WinziPrint = new string[] { App.InstallPath }
.Union(Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? [])
.Select(x => Path.Combine(x, "WinziPrint.exe"))
.Where(File.Exists)
.FirstOrDefault() ?? throw new FileNotFoundException("WiniPrint executable not found");
private static Process? WinziPrintProc;
public static bool IsReady => WinziPrintProc != null;
public static async Task Init(Action? evtHandler = null) {
// NOTE: If the WinziPrint daemon is already running this will succeed, but the process will fail.
// Should be no problem, as long as the daemon is not closed
var p = new Process() { StartInfo = new() {
FileName = WinziPrint,
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
} };
p.StartInfo.ArgumentList.Add("-D");
p.StartInfo.ArgumentList.Add("-d");
p.StartInfo.ArgumentList.Add(App.TempPath);
p.Start();
await p.StandardOutput.ReadLineAsync();
WinziPrintProc = p;
evtHandler?.Invoke();
}
public static void Cleanup() {
PdfiumNative.FPDF_DestroyLibrary();
public static Task Cleanup() {
WinziPrintProc?.Kill(true);
WinziPrintProc?.Close();
return Task.CompletedTask;
}
public static async Task<(int Pages, IEnumerable<int> PerDoc)> Convert(string htmlPath, string pdfPath, bool doublePaged = false, IProgress<double>? progress = null) {
return await Convert([htmlPath], pdfPath, doublePaged, progress);
}
public static async Task<(int Pages, IEnumerable<int> PerDoc)> Convert(IEnumerable<string> htmlPath, string pdfPath, bool doublePaged = false, IProgress<double>? progress = null) {
if (WinziPrintProc == null) throw new InvalidOperationException("The WinziPrint process has not been initialized yet");
progress?.Report(0.0);
using var client = new TcpClient("127.0.0.1", 30983);
using var stream = client.GetStream();
await stream.WriteAsync(Utils.UTF8.GetBytes(
"-e utf-8;-p;" + (doublePaged ? "-2;" : "") +
$"{string.Join(';', htmlPath)};{pdfPath}" +
"\r\n"));
using var reader = new StreamReader(stream);
while (true) {
var line = await reader.ReadLineAsync() ?? throw new IOException("Invalid response from WinziPrint");
if (line.StartsWith("error:")) {
throw new IOException($"WinziPrint: {line[6..].Trim()}");
} else if (line.StartsWith("progress:")) {
var parts = line[9..].Trim().Split('/').Select(int.Parse).ToArray();
progress?.Report(100.0 * parts[0] / parts[1]);
} else if (line.StartsWith("success:")) {
var m = Regex.Match(line, @"([0-9]+) pages \(([0-9, ]+)\)");
return (int.Parse(m.Groups[1].Value), m.Groups[2].Value.Split(", ").Select(int.Parse).ToList());
}
}
}
public static void Show(TempFile file, string title) {
@@ -28,13 +88,24 @@ namespace Elwig.Helpers.Printing {
});
}
public static void Print(string path, int copies = 1, bool doublePaged = false) {
public static async Task Print(string path, int copies = 1, bool doublePaged = false) {
await Print(path, new() {
Copies = (short)copies,
Collate = true,
Duplex = doublePaged ? Duplex.Vertical : Duplex.Simplex,
});
}
public static Task Print(string path, PrinterSettings settings) {
try {
var printer = new PdfPrinter();
printer.Print(path, copies, doublePaged);
} catch (Exception exc) {
InteractionService.ShowException("Fehler beim Drucken", "Beim Drucken ist ein Fehler aufgetreten", exc);
using var doc = PdfDocument.Load(path);
using var printDoc = doc.CreatePrintDocument(PdfPrintMode.CutMargin);
printDoc.PrinterSettings = settings;
printDoc.Print();
} catch (Exception e) {
MessageBox.Show("Beim Drucken ist ein Fehler aufgetreten:\n\n" + e.Message, "Fehler beim Drucken", MessageBoxButton.OK, MessageBoxImage.Error);
}
return Task.CompletedTask;
}
}
}
-83
View File
@@ -1,83 +0,0 @@
using System;
using System.IO;
using System.Printing;
namespace Elwig.Helpers.Printing {
public class PdfPrinter {
public string PrinterName { get; init; }
public PdfPrinter(string? printerName = null) {
PrinterName = printerName ?? GetDefaultPrinterName() ?? throw new InvalidOperationException("No default Windows printer is configured.");
}
public void Print(string pdfPath, int copies = 1, bool doublePaged = false) {
if (!File.Exists(pdfPath)) throw new FileNotFoundException("PDF file not found.", pdfPath);
IntPtr document = PdfiumNative.FPDF_LoadDocument(pdfPath, null);
if (document == IntPtr.Zero) throw new InvalidOperationException("PDFium could not open the PDF.");
try {
int pageCount = PdfiumNative.FPDF_GetPageCount(document);
if (pageCount <= 0)
return;
using var printer = new Win32Printer.Printer(PrinterName);
printer.TrySetDuplex(doublePaged ? Duplexing.TwoSidedLongEdge : Duplexing.OneSided);
printer.StartDocument(Path.GetFileName(pdfPath));
bool documentStarted = true;
try {
for (int i = 0; i < copies; i++) {
for (int j = 0; j < pageCount; j++) {
PrintPage(document, printer, j);
}
}
printer.EndDocument();
documentStarted = false;
} finally {
if (documentStarted) printer.AbortDocument();
}
} finally {
PdfiumNative.FPDF_CloseDocument(document);
}
}
private static void PrintPage(IntPtr document, Win32Printer.Printer printer, int pageIndex) {
if (!PdfiumNative.FPDF_GetPageSizeByIndex(document, pageIndex, out double pageWidthPt, out double pageHeightPt)) {
throw new InvalidOperationException($"Unable to obtain PDF page {pageIndex} size.");
}
IntPtr page = PdfiumNative.FPDF_LoadPage(document, pageIndex);
if (page == IntPtr.Zero)
throw new InvalidOperationException($"Unable to load PDF page {pageIndex}.");
try {
int renderWidth = (int)Math.Round(pageWidthPt / 72.0 * printer.DpiX);
int renderHeight = (int)Math.Round(pageHeightPt / 72.0 * printer.DpiY);
int offsetX = (printer.PrintableWidth - renderWidth) / 2;
int offsetY = (printer.PrintableHeight - renderHeight) / 2;
printer.StartPage();
try {
PdfiumNative.FPDF_RenderPage(printer.Hdc, page, offsetX, offsetY, renderWidth, renderHeight, 0, PdfiumNative.FPDF_PRINTING | PdfiumNative.FPDF_ANNOT);
} finally {
printer.EndPage();
}
} finally {
PdfiumNative.FPDF_ClosePage(page);
}
}
private static string? GetDefaultPrinterName() {
using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows NT\CurrentVersion\Windows");
string? printer = key?.GetValue("Device") as string;
if (string.IsNullOrWhiteSpace(printer))
return null;
// Device has the form:
// Printer Name,winspool,PORT
int comma = printer.IndexOf(',');
return comma >= 0 ? printer[..comma] : printer;
}
}
}
-38
View File
@@ -1,38 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Elwig.Helpers.Printing {
internal partial class PdfiumNative {
public const int FPDF_ANNOT = 0x01;
public const int FPDF_PRINTING = 0x800;
[LibraryImport("pdfium.dll")]
public static partial void FPDF_InitLibrary();
[LibraryImport("pdfium.dll")]
public static partial void FPDF_DestroyLibrary();
[LibraryImport("pdfium.dll", StringMarshalling = StringMarshalling.Utf8)]
public static partial IntPtr FPDF_LoadDocument(string filePath, string? password);
[LibraryImport("pdfium.dll")]
public static partial void FPDF_CloseDocument(IntPtr document);
[LibraryImport("pdfium.dll")]
public static partial int FPDF_GetPageCount(IntPtr document);
[LibraryImport("pdfium.dll")]
public static partial IntPtr FPDF_LoadPage(IntPtr document, int pageIndex);
[LibraryImport("pdfium.dll")]
public static partial void FPDF_ClosePage(IntPtr page);
[LibraryImport("pdfium.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool FPDF_GetPageSizeByIndex(IntPtr document, int pageIndex, out double width, out double height);
[LibraryImport("pdfium.dll")]
public static partial void FPDF_RenderPage(IntPtr hdc, IntPtr page, int startX, int startY, int sizeX, int sizeY, int rotate, int flags);
}
}
-239
View File
@@ -1,239 +0,0 @@
using System;
using System.ComponentModel;
using System.Printing;
using System.Runtime.InteropServices;
namespace Elwig.Helpers.Printing {
public partial class Win32Printer {
private const int DM_OUT_BUFFER = 2;
private const int HORZRES = 8;
private const int VERTRES = 10;
private const int LOGPIXELSX = 88;
private const int LOGPIXELSY = 90;
private const int PHYSICALWIDTH = 110;
private const int PHYSICALHEIGHT = 111;
private const int PHYSICALOFFSETX = 112;
private const int PHYSICALOFFSETY = 113;
private const int DM_DUPLEX = 0x1000;
private const short DMDUP_SIMPLEX = 1;
private const short DMDUP_VERTICAL = 2;
private const short DMDUP_HORIZONTAL = 3;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct DOCINFO {
public int cbSize;
[MarshalAs(UnmanagedType.LPWStr)]
public string? lpszDocName;
[MarshalAs(UnmanagedType.LPWStr)]
public string? lpszOutput;
[MarshalAs(UnmanagedType.LPWStr)]
public string? lpszDatatype;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct DEVMODE {
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string dmDeviceName;
public short dmSpecVersion;
public short dmDriverVersion;
public short dmSize;
public short dmDriverExtra;
public int dmFields;
public short dmOrientation;
public short dmPaperSize;
public short dmPaperLength;
public short dmPaperWidth;
public short dmScale;
public short dmCopies;
public short dmDefaultSource;
public short dmPrintQuality;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string dmFormName;
public short dmLogPixels;
public int dmBitsPerPel;
public int dmPelsWidth;
public int dmPelsHeight;
public int dmDisplayFlags;
public int dmDisplayFrequency;
public int dmICMMethod;
public int dmICMIntent;
public int dmMediaType;
public int dmDitherType;
public int dmReserved1;
public int dmReserved2;
public int dmPanningWidth;
public int dmPanningHeight;
}
[DllImport("winspool.drv", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool OpenPrinter(string pPrinterName, out IntPtr phPrinter, IntPtr pDefault);
[DllImport("winspool.drv", SetLastError = true)]
private static extern bool ClosePrinter(IntPtr hPrinter);
[DllImport("winspool.drv", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern int DocumentProperties(IntPtr hwnd, IntPtr hPrinter, string pDeviceName, IntPtr pDevModeOutput, IntPtr pDevModeInput, int fMode);
[DllImport("gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr CreateDC(string? pDriver, string? pDevice, string? pOutput, IntPtr pInitData);
[DllImport("gdi32.dll", SetLastError = true)]
private static extern bool DeleteDC(IntPtr hdc);
[DllImport("gdi32.dll", SetLastError = true)]
private static extern int GetDeviceCaps(IntPtr hdc, int index);
[DllImport("gdi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern int StartDoc(IntPtr hdc, ref DOCINFO lpdi);
[DllImport("gdi32.dll", SetLastError = true)]
private static extern int EndDoc(IntPtr hdc);
[DllImport("gdi32.dll", SetLastError = true)]
private static extern int AbortDoc(IntPtr hdc);
[DllImport("gdi32.dll", SetLastError = true)]
private static extern int StartPage(IntPtr hdc);
[DllImport("gdi32.dll", SetLastError = true)]
private static extern int EndPage(IntPtr hdc);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GlobalFree(IntPtr hMem);
[DllImport("kernel32.dll")]
private static extern IntPtr GlobalLock(IntPtr hMem);
[DllImport("kernel32.dll")]
private static extern bool GlobalUnlock(IntPtr hMem);
public class Printer : IDisposable {
private readonly string _printerName;
private readonly IntPtr _printerHandle;
private readonly IntPtr _devMode;
private readonly IntPtr _hdc;
public IntPtr Hdc => _hdc;
public int PrintableWidth => GetDeviceCaps(_hdc, HORZRES);
public int PrintableHeight => GetDeviceCaps(_hdc, VERTRES);
public int DpiX => GetDeviceCaps(_hdc, LOGPIXELSX);
public int DpiY => GetDeviceCaps(_hdc, LOGPIXELSY);
public int PhysicalWidth => GetDeviceCaps(_hdc, PHYSICALWIDTH);
public int PhysicalHeight => GetDeviceCaps(_hdc, PHYSICALHEIGHT);
public int PhysicalOffsetX => GetDeviceCaps(_hdc, PHYSICALOFFSETX);
public int PhysicalOffsetY => GetDeviceCaps(_hdc, PHYSICALOFFSETY);
public Printer(string printerName) {
_printerName = printerName;
if (!OpenPrinter(_printerName, out _printerHandle, IntPtr.Zero)) {
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Unable to open printer '{printerName}'.");
}
try {
_devMode = CreateDevMode();
_hdc = CreateDC(null, _printerName, null, _devMode);
if (_hdc == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Unable to create printer DC for '{_printerName}'.");
} catch {
if (_devMode != IntPtr.Zero) Marshal.FreeHGlobal(_devMode);
ClosePrinter(_printerHandle);
throw;
}
}
public bool TrySetDuplex(Duplexing duplex) {
var devMode = Marshal.PtrToStructure<DEVMODE>(_devMode);
switch (duplex) {
case Duplexing.OneSided: devMode.dmDuplex = DMDUP_SIMPLEX; break;
case Duplexing.TwoSidedShortEdge: devMode.dmDuplex = DMDUP_HORIZONTAL; break;
case Duplexing.TwoSidedLongEdge: devMode.dmDuplex = DMDUP_VERTICAL; break;
}
if ((devMode.dmFields & DM_DUPLEX) != 0) {
Marshal.StructureToPtr(devMode, _devMode, false);
return true;
} else {
devMode.dmFields |= DM_DUPLEX;
Marshal.StructureToPtr(devMode, _devMode, false);
return false;
}
}
private IntPtr CreateDevMode() {
int size = DocumentProperties(IntPtr.Zero, _printerHandle, _printerName, IntPtr.Zero, IntPtr.Zero, 0);
if (size <= 0)
throw new Win32Exception(Marshal.GetLastWin32Error(), "DocumentProperties failed.");
IntPtr devMode = Marshal.AllocHGlobal(size);
int result = DocumentProperties(IntPtr.Zero, _printerHandle, _printerName, devMode, IntPtr.Zero, DM_OUT_BUFFER);
if (result < 0) {
Marshal.FreeHGlobal(devMode);
throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to obtain printer DEVMODE.");
}
return devMode;
}
public void StartDocument(string documentName) {
var docInfo = new DOCINFO {
cbSize = Marshal.SizeOf<DOCINFO>(),
lpszDocName = documentName,
lpszDatatype = null,
lpszOutput = null
};
if (StartDoc(_hdc, ref docInfo) <= 0)
throw new Win32Exception(Marshal.GetLastWin32Error(), "StartDoc failed.");
}
public void StartPage() {
if (Win32Printer.StartPage(_hdc) <= 0)
throw new Win32Exception(Marshal.GetLastWin32Error(), "StartPage failed.");
}
public void EndPage() {
if (Win32Printer.EndPage(_hdc) <= 0)
throw new Win32Exception(Marshal.GetLastWin32Error(), "EndPage failed.");
}
public void EndDocument() {
if (EndDoc(_hdc) <= 0)
throw new Win32Exception( Marshal.GetLastWin32Error(), "EndDoc failed.");
}
public void AbortDocument() {
AbortDoc(_hdc);
}
public void Dispose() {
if (_hdc != IntPtr.Zero)
DeleteDC(_hdc);
if (_devMode != IntPtr.Zero)
Marshal.FreeHGlobal(_devMode);
if (_printerHandle != IntPtr.Zero)
ClosePrinter(_printerHandle);
}
}
}
}
-60
View File
@@ -1,60 +0,0 @@
using System;
using System.IO.Ports;
using System.Linq;
using System.Management;
namespace Elwig.Helpers {
public sealed class SerialPortWatcher : IDisposable {
private readonly ManagementEventWatcher _deviceArrivalWatcher;
private readonly ManagementEventWatcher _deviceRemovalWatcher;
private string[] _knownPorts;
public event EventHandler<string>? SerialPortConnected;
public event EventHandler<string>? SerialPortDisconnected;
public SerialPortWatcher() {
_knownPorts = SerialPort.GetPortNames();
_deviceArrivalWatcher = new ManagementEventWatcher("SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 2");
_deviceArrivalWatcher.EventArrived += OnDeviceArrived;
_deviceRemovalWatcher = new ManagementEventWatcher("SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 3");
_deviceRemovalWatcher.EventArrived += OnDeviceRemoved;
_deviceArrivalWatcher.Start();
_deviceRemovalWatcher.Start();
}
private void OnDeviceArrived(object sender, EventArrivedEventArgs evt) {
App.MainDispatcher.Invoke(() => {
// "synchronized"
string[] currentPorts = SerialPort.GetPortNames();
var newPorts = currentPorts.Except(_knownPorts).ToArray();
foreach (var port in newPorts)
SerialPortConnected?.Invoke(this, port);
_knownPorts = currentPorts;
});
}
private void OnDeviceRemoved(object sender, EventArrivedEventArgs evt) {
App.MainDispatcher.Invoke(() => {
// "synchronized"
string[] currentPorts = SerialPort.GetPortNames();
var removedPorts = _knownPorts.Except(currentPorts).ToArray();
foreach (var port in removedPorts)
SerialPortDisconnected?.Invoke(this, port);
_knownPorts = currentPorts;
});
}
public void Dispose() {
try {
_deviceArrivalWatcher?.Stop();
_deviceRemovalWatcher?.Stop();
} finally {
_deviceArrivalWatcher?.Dispose();
_deviceRemovalWatcher?.Dispose();
}
}
}
}
+112 -193
View File
@@ -1,50 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.IO.Ports;
using System.Net.Sockets;
using Elwig.Dialogs;
using Elwig.Documents;
using Elwig.Helpers.Billing;
using Elwig.Models;
using System.Text;
using System.Numerics;
using Elwig.Models.Entities;
using Elwig.Services;
using iText.Layout.Element;
using LinqKit;
using Elwig.Helpers.Billing;
using System.Runtime.InteropServices;
using System.Net.Http;
using System.Text.Json.Nodes;
using System.IO;
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.EntityFrameworkCore;
using MimeKit;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Linq.Expressions;
using System.Net.Http;
using System.Net.Sockets;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Collections;
using Elwig.Documents;
using MimeKit;
using System.Windows.Input;
using LinqKit;
using System.Linq.Expressions;
using Elwig.Models;
using Microsoft.Win32;
using System.Globalization;
using System.Threading;
using System.Windows.Markup;
namespace Elwig.Helpers {
public static partial class Utils {
public static readonly Encoding UTF8 = new UTF8Encoding(false, true);
public static readonly Encoding UTF8BOM = new UTF8Encoding(true, true);
public static readonly JsonSerializerOptions JsonOpts = new() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
public static int CurrentYear => DateTime.Now.Year;
public static int CurrentNextSeason => DateTime.Now.Year - (DateTime.Now.Month <= 3 ? 1 : 0);
public static int CurrentLastSeason => DateTime.Now.Year - (DateTime.Now.Month <= 6 ? 1 : 0);
public static int FollowingSeason => DateTime.Now.Year + (DateTime.Now.Month >= 11 ? 1 : 0);
public static DateTime Today => (DateTime.Now.Hour >= 3) ? DateTime.Today : DateTime.Today.AddDays(-1);
[GeneratedRegex("^serial://([A-Za-z0-9]+):([0-9]+)(,([5-9]),([NOEMSnoems]),(0|1|1\\.5|2|))?$", RegexOptions.Compiled)]
private static partial Regex GeneratedSerialRegex();
@@ -209,29 +206,16 @@ namespace Elwig.Helpers {
return Regex.Replace(iban.Trim(), ".{4}", "$0 ").Trim();
}
public static void RunBackground(string title, Func<Task> function) {
public static void RunBackground(string title, Func<Task> a) {
Task.Run(async () => {
try {
await function();
} catch (Exception exc) {
InteractionService.ShowException(title, exc);
await a();
} catch (Exception e) {
MessageBox.Show(e.ToString(), title, MessageBoxButton.OK, MessageBoxImage.Error);
}
});
}
public static async Task RunForeground(Func<Task> function) {
var isSTA = Thread.CurrentThread.GetApartmentState() == ApartmentState.STA;
if (isSTA) Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
await function();
} catch (Exception exc) {
InteractionService.ShowException(exc);
}
});
if (isSTA) Mouse.OverrideCursor = null;
}
public static void MailTo(string emailAddress) {
MailTo([emailAddress]);
}
@@ -303,11 +287,6 @@ namespace Elwig.Helpers {
return d.ShowDialog() == true ? (d.Weight, d.Reason) : null;
}
public static int? ShowTareCorrectionDialog(int? actualTare, int? targetTare = null) {
var d = new TareCorrectionDialog(actualTare, targetTare);
return d.ShowDialog() == true ? d.TargetTare - d.ActualTare : null;
}
public static (string?, int[])? ShowDeliverySplittingDialog(Delivery delivery) {
var d = new DeliverySplittingDialog(delivery);
return d.ShowDialog() == true ? (d.MgNr?.ToString() ?? d.LsNr, d.Weights ?? []) : null;
@@ -318,23 +297,28 @@ namespace Elwig.Helpers {
return d.ShowDialog() == true ? d.Price : null;
}
public static Footer GenerateFooter(string lineBreak, string separator) {
return new Footer(lineBreak, separator);
public static Footer GenerateFooter(string lineBreak, string seperator) {
return new Footer(lineBreak, seperator);
}
public class Footer {
private readonly List<List<List<object>>> Items = [[[]]];
private string Text = "";
private readonly string LineBreak;
private readonly string Separator;
private readonly string Seperator;
private bool FirstLine = true;
private bool FirstItemInLine = true;
public Footer(string lineBreak, string separator) {
public Footer(string lineBreak, string seperator) {
LineBreak = lineBreak;
Separator = separator;
Seperator = seperator;
}
public Footer Item(object? text) {
public Footer Item(string? text) {
if (text == null) return this;
Items[^1][^1].Add(text);
Text += FirstItemInLine ? (FirstLine ? "" : LineBreak) : Seperator;
Text += text;
FirstLine = false;
FirstItemInLine = false;
return this;
}
@@ -342,56 +326,13 @@ namespace Elwig.Helpers {
return text == null ? this : Item($"{name}: {text}");
}
public Footer NextGroup() {
Items[^1].Add([]);
return this;
}
public Footer NextLine() {
Items.Add([[]]);
return this;
}
public Footer RebalanceLines() {
for (int i = 0; i < Items.Count - 1; i++) {
var groups = Items[i];
var nextGroups = Items[i + 1];
if (groups.Count <= 1) continue;
var g = groups[^1];
var gl = g.Sum(i => (i is Text txt ? txt.GetText().Length : i.ToString()?.Length ?? 0) + Separator.Length);
var l1a = groups.SelectMany(g => g).Sum(i => (i is Text txt ? txt.GetText().Length : i.ToString()?.Length ?? 0) + Separator.Length) - Separator.Length;
var l2a = nextGroups.SelectMany(g => g).Sum(i => (i is Text txt ? txt.GetText().Length : i.ToString()?.Length ?? 0) + Separator.Length) - Separator.Length;
var l1b = l1a - gl;
var l2b = l2a + gl;
if (Math.Abs(l1b - l2b) <= Math.Abs(l1a - l2a)) {
Items[i].Remove(g);
Items[i + 1].Insert(0, g);
i--;
}
}
FirstItemInLine = true;
return this;
}
public override string ToString() {
return string.Join(LineBreak, Items.Select(l => string.Join(Separator, l.SelectMany(g => g).ToString())));
}
public IList<ILeafElement> ToLeafElements() {
var l = new List<ILeafElement>();
var first1 = true;
foreach (var line in Items) {
if (!first1) l.Add(new Text(LineBreak));
var first2 = true;
foreach (var group in line) {
foreach (var item in group) {
if (!first2) l.Add(new Text(Separator));
l.Add(item as ILeafElement ?? new Text(item.ToString() ?? ""));
first2 = false;
}
}
first1 = false;
}
return l;
return Text;
}
}
@@ -458,8 +399,8 @@ namespace Elwig.Helpers {
return output.OrderByDescending(l => l.Count());
}
public static async Task<List<RawVaribute>> GetVaributes(AppDbContext ctx, int year, bool onlyDelivered = true) {
var varieties = await ctx.FetchWineVarieties().Select(v => new RawVaribute(v.SortId, "", null)).ToListAsync();
public static List<RawVaribute> GetVaributes(AppDbContext ctx, int year, bool onlyDelivered = true) {
var varieties = ctx.WineVarieties.Select(v => new RawVaribute(v.SortId, "", null)).ToList();
var delivered = ctx.DeliveryParts
.Where(d => d.Year == year)
.Select(d => new RawVaribute(d.SortId, d.AttrId ?? "", d.CultId ?? ""))
@@ -468,11 +409,13 @@ namespace Elwig.Helpers {
return [.. (onlyDelivered ? delivered : delivered.Union(varieties)).Order()];
}
public static async Task<List<Varibute>> GetVaributeList(AppDbContext ctx, int year, bool onlyDelivered = true) {
var varieties = await ctx.FetchWineVarieties().ToDictionaryAsync(v => v.SortId, v => v);
var attributes = await ctx.FetchWineAttributes().ToDictionaryAsync(a => a.AttrId, a => a);
var cultivations = await ctx.FetchWineCultivations().ToDictionaryAsync(c => c.CultId, c => c);
return [.. (await GetVaributes(ctx, year, onlyDelivered)).Select(s => new Varibute(s, varieties, attributes, cultivations))];
public static List<Varibute> GetVaributeList(AppDbContext ctx, int year, bool onlyDelivered = true) {
var varieties = ctx.WineVarieties.ToDictionary(v => v.SortId, v => v);
var attributes = ctx.WineAttributes.ToDictionary(a => a.AttrId, a => a);
var cultivations = ctx.WineCultivations.ToDictionary(c => c.CultId, c => c);
return GetVaributes(ctx, year, onlyDelivered)
.Select(s => new Varibute(s, varieties, attributes, cultivations))
.ToList();
}
[LibraryImport("wininet.dll")]
@@ -487,8 +430,6 @@ namespace Elwig.Helpers {
var client = new HttpClient() {
Timeout = TimeSpan.FromSeconds(5),
};
client.DefaultRequestHeaders.UserAgent.Clear();
client.DefaultRequestHeaders.UserAgent.ParseAdd($"Elwig/{App.Version} ({App.Client?.NameToken}, {App.BranchName}, {Environment.MachineName}, {Environment.OSVersion})");
client.DefaultRequestHeaders.Accept.Clear();
if (accept != null)
client.DefaultRequestHeaders.Accept.Add(new(accept));
@@ -516,7 +457,7 @@ namespace Elwig.Helpers {
url = "https://sync.elwig.at/" + url[25..];
if (!url.EndsWith('/')) url += "/";
using var client = GetHttpClient(username, password, accept: "application/json");
using var content = new StreamContent(new FileStream(zip, FileMode.Open, FileAccess.Read));
var content = new StreamContent(new FileStream(zip, FileMode.Open, FileAccess.Read));
content.Headers.ContentType = new("application/zip");
using var res = await client.PutAsync(url + Path.GetFileName(zip), content);
res.EnsureSuccessStatusCode();
@@ -547,7 +488,7 @@ namespace Elwig.Helpers {
public static async Task<SmtpClient?> GetSmtpClient() {
if (App.Config.Smtp == null)
return null;
var (host, port, mode, username, password, _, _) = App.Config.Smtp.Value;
var (host, port, mode, username, password, _) = App.Config.Smtp.Value;
var client = new SmtpClient();
await client.ConnectAsync(host, port, mode == "starttls" ? SecureSocketOptions.StartTls : SecureSocketOptions.None);
await client.AuthenticateAsync(username, password);
@@ -557,80 +498,61 @@ namespace Elwig.Helpers {
public static async Task<bool> SendEmail(Member member, string subject, string text, IEnumerable<Document> docs) {
if (App.Config.Smtp == null)
return false;
return await Task.Run(async () => {
await AddSentMailBody(subject, text, 1);
SmtpClient? client = null;
try {
client = await GetSmtpClient();
using var msg = new MimeMessage();
msg.From.Add(new MailboxAddress(App.Client.NameFull, App.Config.Smtp.Value.From));
msg.To.AddRange(member.EmailAddresses.OrderBy(a => a.Nr).Select(a => new MailboxAddress(member.AdministrativeName, a.Address)));
msg.Subject = subject;
msg.Date = DateTime.Now;
var body = new Multipart("mixed") {
new TextPart("plain") { Text = text }
};
foreach (var doc in docs) {
var name = NormalizeFileName(doc.Title);
body.Add(doc.AsEmailAttachment($"{name}.pdf"));
}
msg.Body = body;
await client!.SendAsync(msg);
await AddSentMails([(
"email", member.MgNr, member.AdministrativeName,
member.EmailAddresses.OrderBy(a => a.Nr).Select(a => a.Address).ToArray(),
subject, docs.Select(d => d.Title).ToArray()
)]);
} catch (Exception exc) {
InteractionService.ShowException(exc);
return false;
} finally {
if (client != null)
await client.DisconnectAsync(true);
client?.Dispose();
SmtpClient? client = null;
try {
Mouse.OverrideCursor = Cursors.AppStarting;
client = await GetSmtpClient();
using var msg = new MimeMessage();
msg.From.Add(new MailboxAddress(App.Client.NameFull, App.Config.Smtp.Value.From));
msg.To.AddRange(member.EmailAddresses.OrderBy(a => a.Nr).Select(a => new MailboxAddress(member.AdministrativeName, a.Address)));
msg.Subject = subject;
var body = new Multipart("mixed") {
new TextPart("plain") { Text = text }
};
foreach (var doc in docs) {
var name = NormalizeFileName(doc.Title);
body.Add(doc.AsEmailAttachment($"{name}.pdf"));
}
return true;
});
msg.Body = body;
await client!.SendAsync(msg);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
} finally {
if (client != null)
await client.DisconnectAsync(true);
client?.Dispose();
Mouse.OverrideCursor = null;
}
return true;
}
public static async Task ExportDocument(Document doc, ExportMode mode, string? filename = null, (Member Member, string Subject, string Text)? emailData = null) {
public static async Task ExportDocument(Document doc, ExportMode mode, string? filename = null, (Member, string, string)? emailData = null) {
if (mode == ExportMode.Print && !App.Config.Debug) {
if (doc.IsPreview) {
InteractionService.ShowError("Vorläufiges Dokument", "Dieses Dokument ist als vorläufig markiert und kann daher nicht ausgedruckt werden!");
return;
}
using (var ctx = new AppDbContext()) {
await doc.Generate(ctx);
}
await doc.Generate();
await doc.Print();
} else if (mode == ExportMode.Email && emailData is (Member, string, string) e) {
if (doc.IsPreview) {
InteractionService.ShowError("Vorläufiges Dokument", "Dieses Dokument ist als vorläufig markiert und kann daher nicht verschickt werden!");
return;
}
using (var ctx = new AppDbContext()) {
await doc.Generate(ctx);
}
var success = await SendEmail(e.Member, e.Subject, e.Text, [doc]);
await doc.Generate();
var success = await SendEmail(e.Item1, e.Item2, e.Item3, [doc]);
if (success)
InteractionService.ShowInformation("E-Mail verschickt", "Die E-Mail wurde erfolgreich verschickt!\n\nEs kann einige Minuten dauern, bis die E-Mail im Posteingang des Empfängers aufscheint.");
MessageBox.Show("Die E-Mail wurde erfolgreich verschickt!", "E-Mail verschickt",
MessageBoxButton.OK, MessageBoxImage.Information);
} else if (mode == ExportMode.SavePdf) {
if (doc.IsPreview) {
InteractionService.ShowWarning("Vorläufiges Dokument", "Dieses Dokument ist als vorläufig markiert und sollte daher nicht langfristig gespeichert werden!");
}
filename = InteractionService.SaveFile(doc.Title, NormalizeFileName(filename ?? doc.Title), "pdf");
if (filename != null) {
using (var ctx = new AppDbContext()) {
await doc.Generate(ctx);
}
doc.SaveTo(filename);
if (!App.Config.Debug) Process.Start("explorer.exe", filename);
var d = new SaveFileDialog() {
FileName = $"{NormalizeFileName(filename ?? doc.Title)}.pdf",
DefaultExt = "pdf",
Filter = "PDF-Datei (*.pdf)|*.pdf",
Title = $"{doc.Title} speichern unter - Elwig"
};
if (d.ShowDialog() == true) {
await doc.Generate();
doc.SaveTo(d.FileName);
Process.Start("explorer.exe", d.FileName);
}
} else {
using (var ctx = new AppDbContext()) {
await doc.Generate(ctx);
}
await doc.Generate();
doc.Show();
}
}
@@ -640,7 +562,9 @@ namespace Elwig.Helpers {
Log = "Application",
Source = ".NET Runtime",
};
return [.. log.Entries.OfType<EventLogEntry>().Where(e => e.InstanceId == 1026).Where(e => e.Message.StartsWith("Application: Elwig.exe"))];
return log.Entries.Cast<EventLogEntry>()
.Where(e => e.Message.StartsWith("Application: Elwig.exe"))
.ToList();
}
public static int GetEntityIdetifierForPk(params object?[] primaryKey) {
@@ -662,16 +586,16 @@ namespace Elwig.Helpers {
}
}
public static Expression<Func<AreaCmt, bool>> ActiveAreaCommitments() => ActiveAreaCommitments(CurrentYear);
public static Expression<Func<AreaCmt, bool>> ActiveAreaCommitments(int year) =>
public static Expression<Func<AreaCom, bool>> ActiveAreaCommitments() => ActiveAreaCommitments(CurrentYear);
public static Expression<Func<AreaCom, bool>> ActiveAreaCommitments(int year) =>
c => (c.YearFrom == null || c.YearFrom <= year) && (c.YearTo == null || c.YearTo >= year);
public static IQueryable<AreaCmt> ActiveAreaCommitments(IQueryable<AreaCmt> query) => ActiveAreaCommitments(query, CurrentYear);
public static IQueryable<AreaCmt> ActiveAreaCommitments(IQueryable<AreaCmt> query, int year) =>
public static IQueryable<AreaCom> ActiveAreaCommitments(IQueryable<AreaCom> query) => ActiveAreaCommitments(query, CurrentYear);
public static IQueryable<AreaCom> ActiveAreaCommitments(IQueryable<AreaCom> query, int year) =>
query.Where(ActiveAreaCommitments(year));
public static IEnumerable<AreaCmt> ActiveAreaCommitments(IEnumerable<AreaCmt> query) => ActiveAreaCommitments(query, CurrentYear);
public static IEnumerable<AreaCmt> ActiveAreaCommitments(IEnumerable<AreaCmt> query, int year) =>
public static IEnumerable<AreaCom> ActiveAreaCommitments(IEnumerable<AreaCom> query) => ActiveAreaCommitments(query, CurrentYear);
public static IEnumerable<AreaCom> ActiveAreaCommitments(IEnumerable<AreaCom> query, int year) =>
query.Where(c => ActiveAreaCommitments(year).Invoke(c));
public static async Task<(DateTime DateTime, string Type, int MgNr, string Name, string[] Addresses, string Subject, string[] Attachments)[]> GetSentMails(DateOnly? fromDate = null, DateOnly? toDate = null) {
@@ -702,7 +626,7 @@ namespace Elwig.Helpers {
p[2],
int.Parse(p[3]),
p[4],
[.. p[5].Split(',').Select(a => a.Replace(" | ", "\n"))],
p[5].Split(',').Select(a => a.Replace(" | ", "\n")).ToArray(),
p[6],
p[7].Split(',')
));
@@ -730,9 +654,9 @@ namespace Elwig.Helpers {
}
public static async Task<string?> FindSentMailBody(DateTime target) {
var dt = $"{target:yyyy-MM-dd_HH-mm-ss}";
var dt = $"{target:yyyy-MM-dd_HH-mm-ss}_";
var filename = Directory.GetFiles(App.MailsPath, "????-??-??_??-??-??_*.txt")
.Where(n => Path.GetFileName(n)[..19].CompareTo(dt) <= 0)
.Where(n => Path.GetFileName(n).CompareTo(dt) <= 0)
.Order()
.LastOrDefault();
if (filename == null)
@@ -744,10 +668,5 @@ namespace Elwig.Helpers {
var filename = Path.Combine(App.MailsPath, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{NormalizeFileName(subject)}.txt");
await File.WriteAllTextAsync(filename, $"# {subject}\r\n# Vorgesehene Empfänger: {recipients}\r\n\r\n" + body, Utils.UTF8);
}
public static string GetTitle(string window, IEnumerable<string> filterNames, IEnumerable<string> filter) {
var f = string.Join(" / ", filterNames.Union(filter.Select(f => $"\"{f}\"")));
return window + (f.Length == 0 ? "" : ": " + f) + " - Elwig";
}
}
}
+15 -118
View File
@@ -136,7 +136,8 @@ namespace Elwig.Helpers {
return new(false, "PLZ zu kurz");
}
using var ctx = new AppDbContext();
if (!int.TryParse(input.Text, out var plz) || ctx.Postleitzahlen.Find(plz) == null) {
int plz = int.Parse(input.Text);
if (ctx.Postleitzahlen.Find(plz) == null) {
return new(false, "Ungültige PLZ");
}
return new(true, null);
@@ -422,7 +423,8 @@ namespace Elwig.Helpers {
}
using var ctx = new AppDbContext();
if (!int.TryParse(input.Text, out var nr) || !ctx.MgNrExists(nr).GetAwaiter().GetResult()) {
int nr = int.Parse(input.Text);
if (!ctx.MgNrExists(nr).GetAwaiter().GetResult()) {
return new(false, "Ungültige Mitgliedsnummer");
}
@@ -438,7 +440,8 @@ namespace Elwig.Helpers {
}
using var ctx = new AppDbContext();
if (!int.TryParse(input.Text, out var nr) || nr != m?.MgNr && ctx.MgNrExists(nr).GetAwaiter().GetResult()) {
int nr = int.Parse(input.Text);
if (nr != m?.MgNr && ctx.MgNrExists(nr).GetAwaiter().GetResult()) {
return new(false, "Mitgliedsnummer wird bereits verwendet");
}
@@ -473,7 +476,7 @@ namespace Elwig.Helpers {
return res;
} else if (!required && input.Text.Length == 0) {
return new(true, null);
} else if (!int.TryParse(input.Text, out var nr) ||!ctx.MgNrExists(nr).GetAwaiter().GetResult()) {
} else if (!ctx.MgNrExists(int.Parse(input.Text)).GetAwaiter().GetResult()) {
return new(false, "Ein Mitglied mit dieser Mitgliedsnummer existiert nicht");
}
@@ -534,7 +537,7 @@ namespace Elwig.Helpers {
return (parts[0]?.Length == 4) ? new(true, null) : new(false, "Datum ist ungültig");
} else if (p == 1) {
// only month and year provided
int m = parts[0] != null && parts[0] != "" && int.TryParse(parts[0], out var v) ? v : 0;
int m = parts[0] != null && parts[0] != "" ? int.Parse(parts[0] ?? "0") : 0;
if (parts[1]?.Length != 4 || parts[0]?.Length != 2 || m < 1 || m > 12)
return new(false, "Datum ist ungültig");
return new(true, null);
@@ -577,7 +580,7 @@ namespace Elwig.Helpers {
}
}
public static ValidationResult CheckFbNr(TextBox input, bool required, AreaCmtContract? c) {
public static ValidationResult CheckFbNr(TextBox input, bool required, AreaCom? c) {
var res = CheckInteger(input, required);
if (!res.IsValid) {
return res;
@@ -586,7 +589,8 @@ namespace Elwig.Helpers {
}
using var ctx = new AppDbContext();
if (!int.TryParse(input.Text, out var nr) || (nr != c?.FbNr && ctx.FbNrExists(nr).GetAwaiter().GetResult())) {
int nr = int.Parse(input.Text);
if (nr != c?.FbNr && ctx.FbNrExists(nr).GetAwaiter().GetResult()) {
return new(false, "Flächenbindungsnummer wird bereits verwendet");
}
@@ -601,7 +605,8 @@ namespace Elwig.Helpers {
return new(true, null);
}
if (!double.TryParse(input.Text, out var oe) || oe < 10 || oe >= 300) {
var oe = double.Parse(input.Text);
if (oe < 10 || oe >= 300) {
return new(false, "Ungültiger Oechsle-Wert");
}
@@ -616,120 +621,12 @@ namespace Elwig.Helpers {
return new(true, null);
}
if (!double.TryParse(input.Text, out var kmw) || kmw < 5 || kmw >= 50) {
var kmw = double.Parse(input.Text);
if (kmw < 5 || kmw >= 50) {
return new(false, "Ungültiger KMW-Wert");
}
return new(true, null);
}
public static ValidationResult CheckOrganicOperatorId(TextBox input, bool required) {
string text = "";
int pos = input.CaretIndex;
for (int i = 0, v = 0; i < input.Text.Length; i++) {
char ch = input.Text[i];
if (char.IsAsciiDigit(ch)) {
if (v == 3 && text[^1] != '-')
text += "-";
v++;
text += ch;
}
if (i == input.CaretIndex - 1) {
pos = text.Length;
} else if (v >= 10) {
break;
}
}
input.Text = text;
input.CaretIndex = pos;
if (text.Length == 0) {
return required ? new(false, "OOC-ID ist nicht optional") : new(true, null);
} else if (!text.StartsWith("040-") || text.Length != 11) {
return new(false, "OOC-ID ist ungültig");
}
return new(true, null);
}
public static ValidationResult CheckOrganicAuthorityCode(TextBox input, bool required) {
string text = "";
int pos = input.CaretIndex;
for (int i = 0, v = 0; i < input.Text.Length; i++) {
char ch = char.ToUpper(input.Text[i]);
if (v < 2 && char.IsAsciiLetter(ch)) {
v++;
text += ch;
} else if ((v == 2 || v == 5) && ch == '-' && text[^1] != '-') {
text += ch;
} else if (v >= 2 && v <= 4 && char.IsLetter(ch)) {
if (v == 2 && text[^1] != '-')
text += "-";
if (text.StartsWith("AT")) {
if (v == 2 && ch == 'B' || v == 3 && ch == 'I' || v == 4 && ch == 'O') {
v++;
text += ch;
}
} else if (text.StartsWith("DE")) {
if (v == 2 && ch == 'Ö' || v == 3 && ch == 'K' || v == 4 && ch == 'O') {
v++;
text += ch;
}
} else {
v++;
text += ch;
}
} else if (v >= 5 && v <= 7 && char.IsAsciiDigit(ch)) {
if (v == 5 && text[^1] != '-')
text += "-";
v++;
text += ch;
}
if (i == input.CaretIndex - 1) {
pos = text.Length;
} else if ((text.StartsWith("AT") || text.StartsWith("DE")) && v >= 10) {
break;
}
}
input.Text = text;
input.CaretIndex = pos;
if (text.Length == 0)
return required ? new(false, "Bio-Kontrollstellen-Code ist nicht optional") : new(true, null);
if (text.StartsWith("AT") || text.StartsWith("DE")) {
if (text.Length != 10) {
return new(false, "Bio-Kontrollstellen-Code ist ungültig");
}
} else {
return new(false, "Not implemented yet");
}
return new(true, null);
}
public static ValidationResult CheckAddress(TextBox input, bool required, string? locality = null) {
var text = input.Text;
var (name, nr) = Utils.SplitAddress(text);
if (!string.IsNullOrWhiteSpace(nr)) {
var street = Normalizer.NormalizeStreetName(name.Replace(",", ""), locality);
var idx = input.CaretIndex;
input.Text = $"{street} {nr}".Trim();
if (idx >= (text.Length - nr.Length)) {
input.CaretIndex = street.Length + (idx - (text.Length - nr.Length)) + 1;
} else if (idx > 0) {
input.CaretIndex = idx;
}
}
if (text.Length == 0)
return required ? new(false, "Adresse ist nicht optional") : new(true, null);
return new(true, null);
}
}
}
+13 -41
View File
@@ -1,27 +1,28 @@
using Elwig.Services;
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace Elwig.Helpers.Weighing {
public class AveryEventScale : Scale, IEventScale, IDisposable {
public string Manufacturer => "Avery";
public int InternalScaleNr => 1;
public string Model { get; private set; }
public string ScaleTerminalId { get; private set; }
public string ScaleId { get; private set; }
public bool IsReady { get; private set; }
public bool HasFillingClearance { get; private set; }
public event IEventScale.EventHandler<WeighingEventArgs>? WeighingEvent;
public event IEventScale.EventHandler<WeighingEventArgs> WeighingEvent;
private bool IsRunning = true;
private readonly Thread BackgroundThread;
public AveryEventScale(string id, string model, string cnx, string? log = null, bool required = true) :
base(cnx, null, null, null, log, true, !required) {
ScaleTerminalId = id;
public AveryEventScale(string id, string model, string cnx, string? empty = null, string? filling = null, int? limit = null, string? log = null) :
base(cnx, empty, filling, limit, log) {
ScaleId = id;
Model = model;
IsReady = true;
HasFillingClearance = false;
@@ -49,48 +50,19 @@ namespace Elwig.Helpers.Weighing {
var data = await Receive();
if (data != null)
RaiseWeighingEvent(new WeighingEventArgs(data.Value));
} catch (ThreadInterruptedException) {
// ignore
} catch (IOException) {
await Task.Delay(500);
await Reconnect();
} catch (TimeoutException) {
await Task.Delay(500);
await Reconnect();
} catch (Exception exc) {
InteractionService.ShowException("Waagenfehler", "Beim Wiegen ist ein Fehler Aufgetreten", exc, showExcType: true);
} catch (Exception ex) {
MessageBox.Show($"Beim Wiegen ist ein Fehler Aufgetreten:\n\n{ex.Message}", "Waagenfehler",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
protected async Task Reconnect() {
try { Reader.Close(); } catch { }
try { Stream.Close(); } catch { }
try { Serial?.Close(); } catch { }
while (IsRunning) {
try {
if (Connection.StartsWith("serial:")) {
Serial = Utils.OpenSerialConnection(Connection);
Stream = Serial.BaseStream;
} else if (Connection.StartsWith("tcp:")) {
Tcp = Utils.OpenTcpConnection(Connection);
Stream = Tcp.GetStream();
}
Reader = new(Stream, Encoding.ASCII, false, 512);
break;
} catch {
// ignore
}
await Task.Delay(1000);
}
}
protected async Task<WeighingResult?> Receive() {
var line = "";
while (line.Length < 33) {
var ch = Reader.Read();
if (ch == -1) {
throw new IOException("Connection closed");
return null;
} else if (line.Length > 0 || ch == ' ') {
line += char.ToString((char)ch);
}
@@ -99,7 +71,7 @@ namespace Elwig.Helpers.Weighing {
if (line == null || line == "") {
return null;
} else if (line.Length != 33 || line[0] != ' ' || line[9] != ' ' || line[15] != ' ' || line[20] != ' ' || line[32] != ' ') {
throw new FormatException($"Invalid event from scale: '{line}'");
throw new IOException($"Invalid event from scale: '{line}'");
}
var date = line[ 1.. 9];
@@ -109,7 +81,7 @@ namespace Elwig.Helpers.Weighing {
var unit = line[30..32];
if (unit != "kg") {
throw new WeighingException($"Unsupported unit in weighing event: '{unit}'");
throw new IOException($"Unsupported unit in weighing event: '{unit}'");
}
identNr = identNr.Length > 0 && identNr != "0" ? identNr : null;
@@ -5,17 +5,18 @@ using System.Text;
using System.Threading.Tasks;
namespace Elwig.Helpers.Weighing {
public class GassnerEnqScale : Scale, ICommandScale {
public class GassnerScale : Scale, ICommandScale {
public string Manufacturer => "Gassner";
public int InternalScaleNr => 1;
public string Model { get; private set; }
public string ScaleTerminalId { get; private set; }
public string ScaleId { get; private set; }
public bool IsReady { get; private set; }
public bool HasFillingClearance { get; private set; }
public GassnerEnqScale(string id, string model, string cnx, string? empty = null, string? filling = null, int? limit = null, string? log = null) :
public GassnerScale(string id, string model, string cnx, string? empty = null, string? filling = null, int? limit = null, string? log = null) :
base(cnx, empty, filling, limit, log) {
ScaleTerminalId = id;
ScaleId = id;
Model = model;
IsReady = true;
HasFillingClearance = false;
@@ -23,9 +24,10 @@ namespace Elwig.Helpers.Weighing {
Stream.ReadTimeout = 11000;
}
protected Task SendCommand(string command) => SendCommand(Encoding.ASCII.GetBytes(command));
protected Task SendCommand(char command) => SendCommand(Convert.ToByte(command));
protected async Task SendCommand(byte[] cmd) {
protected async Task SendCommand(byte command) {
byte[] cmd = [command];
await Stream.WriteAsync(cmd);
if (LogPath != null) await File.AppendAllTextAsync(LogPath, Encoding.ASCII.GetString(cmd));
}
@@ -34,7 +36,7 @@ namespace Elwig.Helpers.Weighing {
var line = await Reader.ReadUntilAsync('\x03');
if (LogPath != null) await File.AppendAllTextAsync(LogPath, $"{line}\r\n");
if (line == null || line.Length < 4 || !line.StartsWith('\x02')) {
throw new FormatException("Invalid response from scale");
throw new IOException("Invalid response from scale");
}
var status = line[1..3];
@@ -43,26 +45,19 @@ namespace Elwig.Helpers.Weighing {
switch (status[1]) {
case 'M': msg = "Waage in Bewegung"; break;
}
throw new WeighingException($"Waagenfehler {status}: {msg}");
throw new IOException($"Waagenfehler {status}: {msg}");
} else if (status[0] != ' ') {
throw new WeighingException($"Invalid response from scale (error code {status})");
throw new IOException($"Invalid response from scale (error code {status})");
}
return line[1..^1];
}
protected async Task<WeighingResult> Weigh(int? internalScaleNr, bool incIdentNr, bool retry = true) {
string record;
try {
await SendCommand(incIdentNr ? "\x05" : "?");
record = await ReceiveResponse();
} catch (IOException) {
if (!retry || Tcp == null) throw;
ReconnectTcp();
return await Weigh(internalScaleNr, incIdentNr, false);
}
protected async Task<WeighingResult> Weigh(bool incIdentNr) {
await SendCommand(incIdentNr ? '\x05' : '?');
string record = await ReceiveResponse();
if (record.Length != 45)
throw new FormatException("Invalid response from scale: Received record has invalid size");
throw new IOException("Invalid response from scale: Received record has invalid size");
var line = record[2..];
var brutto = line[ 0.. 7].Trim();
@@ -78,7 +73,6 @@ namespace Elwig.Helpers.Weighing {
GrossWeight = int.Parse(brutto),
TareWeight = int.Parse(tara),
NetWeight = int.Parse(netto),
ScaleId = scaleNr,
WeighingId = identNr,
FullWeighingId = identNr,
Date = DateOnly.TryParseExact(date, "yyyyMMdd", out var d) ? d : null,
@@ -86,12 +80,12 @@ namespace Elwig.Helpers.Weighing {
};
}
public async Task<WeighingResult> Weigh(int? scaleNr = null) {
return await Weigh(scaleNr, true);
public async Task<WeighingResult> Weigh() {
return await Weigh(true);
}
public async Task<WeighingResult> GetCurrentWeight(int? scaleNr = null) {
return await Weigh(scaleNr, false);
public async Task<WeighingResult> GetCurrentWeight() {
return await Weigh(false);
}
public async Task Empty() {
@@ -125,9 +119,8 @@ namespace Elwig.Helpers.Weighing {
await SetFillingClearance(false);
}
public async Task SetDateAndTime(DateTime dateTime) {
await SendCommand($"C{dateTime:yyyyMMddHHmmss}\r");
await ReceiveResponse();
public Task SetDateAndTime(DateTime dateTime) {
throw new NotImplementedException("Für Waagen vom Typ 'Gassner' ist diese Funktion noch nicht implementiert");
}
}
}
+2 -2
View File
@@ -10,13 +10,13 @@ namespace Elwig.Helpers.Weighing {
/// Get the current weight on the scale without performing a weighing process
/// </summary>
/// <returns>Result of the weighing process (probably without a weighing id)</returns>
Task<WeighingResult> GetCurrentWeight(int? scaleNr = null);
Task<WeighingResult> GetCurrentWeight();
/// <summary>
/// Perform a weighing process
/// </summary>
/// <returns>Result of the weighing process (including a weighing id)</returns>
Task<WeighingResult> Weigh(int? scaleNr = null);
Task<WeighingResult> Weigh();
/// <summary>
/// Empty the scale container or grant clearance to do so
Loaded 100 of 269 files, more files were not shown because too many files have changed in this diff. Show more