200304PX Changes over the last months

This commit is contained in:
phantomix 2020-03-04 17:49:33 +01:00
parent d1fa9744d3
commit 6ca52cfb83
21 changed files with 3111 additions and 318 deletions

View File

@ -15,26 +15,36 @@ TODO
- ErrorLog for all errors
- frmMain: Menu option to miss an MV for selected/checked users
- FormMail: Add mail for automatic member type setting to "Foerdermitglied"
- FormMail: Use manual type changing
- FormMail: Membership type was changed manually
- FormMail: Cronjob found unassigned MoneyTransfers
- frmEditEntry: Optional (checkbox) request for a comment on data changes, when hitting OK
- frmMain: Indicator that the data was modified after import + messagebox to remind user to export on quitting.
- add "database changed since last import/export" warning (e.g. you forgot to push your changes)
- frmMain: Member List: Column "monthly fee", Column "Last payment", disabled by default
- Configuration window: MoneyTransferRegEx
- Bug: Generating testdata doesn't remove old xml files, thus the memberlist will be mixed after next restart
- Bug: Database import fails on some PCs (seems that writing xml files during decompression is problematic)
- Bug: Member list not reloaded after ProcessCSV (balance display is wrong)
- CronjobConfig
- CustomListView: implement generic filtering
- Improve import/export handling, e.g. check for newer database on program start
- Debt handling: Store explicit flags or status for "one month behind"
or "two months behind", in order to have an escalation chain
- PGP for mails
DONE
----
- Member: add database field for contact name e.g.: name <mail@example.com> filled with nickname by default
- Testmail feature for every member account
- 2019-12-04: frmProcessWithLog: Implementation for non-immediate GUI actions (process csv, cronjob, import/export) with background thread
- Bug: Database import fails on some PCs (seems that writing xml files during decompression is problematic)
*/
namespace dezentrale
{
public class Program
{
public static uint VersionNumber { get; private set; } = 0x19120400;
public static uint VersionNumber { get; private set; } = 0x20022500;
public static string VersionString { get; private set; } = $"{VersionNumber:x}";
public static string AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
@ -233,142 +243,12 @@ namespace dezentrale
Console.WriteLine("You must provide an input file by using --csvinput=<file>");
return 1;
}
if (!ProcessCSV(csvInput))
if (!ProcessCsv.ProcessCSV(csvInput))
return 1;
break;
}
return 0;
}
public static bool ProcessCSV(string fileName)
{
CsvFile csv = new CsvFile();
csv.FieldSeparator = ';';
try
{
List<BankTransfer> tmpList = new List<BankTransfer>();
csv.ReadFile(fileName);
List<string> headlineFields = null;
List<Member> changedMembers = new List<Member>();
foreach (List<string> l in csv.FileContents)
{
if (headlineFields == null)
{
//The first line is expected to have the headline field first, describing the contents
headlineFields = l;
continue;
}
BankTransfer bt = new BankTransfer(headlineFields, l);
MoneyTransfer duplicate = MoneyTransfers.FindEqualEntry(bt);
if (duplicate != null)
{
Console.WriteLine("Duplicate MoneyTransfer found");
Console.WriteLine($"ValutaDate: {bt.ValutaDate}, Amount = {bt.AmountString} {bt.Currency}, Reason = \"{bt.TransferReason.Replace('\r','\\').Replace('\n','\\')}\"");
} else
{
MoneyTransfers.AddEntry(bt);
tmpList.Add(bt);
}
}
//try to assign transfers to the members
foreach (BankTransfer bt in tmpList)
{
if (bt.Amount < 0)
{
bt.TransferType = MoneyTransfer.eTransferType.RunningCost;
Console.WriteLine($"{bt.Id}: Amount = {bt.AmountString} --> RunningCost");
continue;
}
foreach (Member m in members.Entries)
{
if (m.CheckBankTransfer(bt))
{
bt.TransferType = MoneyTransfer.eTransferType.MembershipPayment;
changedMembers.Add(m);
m.StartLogEvent("Incoming bank transfer", LogEvent.eEventType.MembershipPayment, "automatic");
m.ApplyMoneyTransfer(bt);
bt.AssignFixed = true;
break; //this is important. We don't want to assign this to multiple members.
}
}
}
//Store bank transfer list
Console.WriteLine("ProcessCSV(): Storing money transfers...");
MoneyTransfers.Entries.Sort();
if (!MoneyTransfers.SaveToFile())
return false;
bool ret = true;
/*
//automaticly saved in ApplyMoneyTransfer()
foreach (Member m in changedMembers)
{
if (m.CurrentLog != null)
{
Console.Write($"ProcessCSV(): Storing {m.GetFileName()}... ");
try
{
if (!m.SaveToFile()) return false;
Console.WriteLine("OK");
} catch(Exception ex2)
{
Console.WriteLine($"ERROR: {ex2.Message}");
ret = false;
}
}
}*/
//TBD: mail to schatzmeister if there are unassigned transfers
return ret;
}
catch (Exception ex)
{
Console.WriteLine($"Error while processing csv file \"{fileName}\":");
Console.WriteLine(ex.Message);
return false;
}
}
public static string PadLeft(string input, uint totalLength)
{
if (input.Length < totalLength)
return new string(' ', (int)(totalLength - input.Length)) + input;
else
return input;
}
//TBD: This is an utility function and should reside in an extra class
public static string Int64FPToString(Int64 value)
{
//float fValue = ((float) value) / 100;
//return $"{fValue:0.00}";
Int64 intPart = value / 100;
Int64 decPart = (intPart < 0 ? -1 : 1) * (value % 100);
return $"{intPart},{decPart:D2}";
}
//TBD: This is an utility function and should reside in an extra class
public static Int64 StringToInt64FP(string value)
{
if (value == null) return 0;
if (!value.Contains(".") && !value.Contains(","))
{
return Convert.ToInt64(value) * 100;
}
int i = value.IndexOf('.');
if (i < 0) i = value.IndexOf(',');
string intPart = value.Substring(0, i);
string decPart = value.Substring(i + 1);
Int64 ip64 = Convert.ToInt64(intPart);
return (ip64 * 100) + (ip64 > 0 ? Convert.ToInt64(decPart) : -Convert.ToInt64(decPart));
}
}
}

View File

@ -4,28 +4,52 @@ using System.Threading;
namespace dezentrale.core
{
//! \short Common functionality for complex background behaviours
//! \brief Provides a threading solution for program behaviours that run in
//! background. Also acts as an interface for controlling and
//! presentation code (GUI)
//! \todo Implement cancelling correctly
public abstract class BackgroundProcess
{
public IProcessController LogTarget { get; set; } = null;
public bool CancelButton { get; protected set; } = false;
//! Target for logging operations performed by the inheriting process classes
public IProcessController LogTarget { get; set; }
//! Can be set by the inheriting class to define whether the GUI shall allow the user to cancel the process
public bool CancelButton { get; protected set; }
//! Textual representation of the process, for displaying
public string Caption { get; protected set; } = "BackgroundProcess";
//! Sub-Steps of the process, to allow the user a better estimation of the duration
public uint Steps { get; protected set; } = 1;
protected abstract bool Run();
private Thread thread = null;
protected abstract bool Run(); //!< Base requirement for inheriting classes: Implement a process. \return if it was successful
private Thread thread = null; //!< Thread instance to be run in background
//! \short Starts an async process run.
//! \return the generated Thread instance, so the application code can watch it too
public Thread StartRun()
{
if (thread != null) return null;
if (LogTarget == null) return null;
thread = new Thread(RunAbstract) { IsBackground = true };
thread.Start();
return thread;
}
//! \internal Helper function for enforcing the ActionCompleted call
private void RunAbstract()
{
LogTarget.ActionCompleted(Run());
}
}
//! \short Example for a BackgroundProcess
//! \brief Implements a threaded solution for logging hello world strings
//! with delays in between them, to simulate background actions
public class HelloWorldProcess : BackgroundProcess
{
public HelloWorldProcess()

View File

@ -4,10 +4,87 @@ using System.Linq;
using System.Text;
using dezentrale.model;
namespace dezentrale
namespace dezentrale.core
{
//! \short Functionality for automatic processing that can be run in a scheduled way
//! \brief A huge port of the dezentrale-members software is designed to help with
//! previously manual tasks by defining an automated manner / refresh user
//! data according to repeated actions, e.g. membership payments.
public class Cronjob
{
//! \brief Perform a Run on one Member. This will perform all necessary actions
//! at this point.
//! \param m Member to check
//! \param sm Member entry for the Schatzmeister account, in order to send notification mails for some events
public static void Run(Member m, Member sm)
{
Console.WriteLine($"Processing member {m.Number:D3} ({m.Nickname})...");
CheckGreeting(m);
CheckSpawning(m);
CheckBalance_PreDegrade(m, sm);
BalanceDegrade(m);
CheckBalance_PostDegrade(m, sm);
}
//! \short Helper function for Run(m, sm). Determines sm automatically.
//! \param m Member to check
public static void Run(Member m)
{
Member sm = Program.members.Find(Member.eRole.Schatzmeister);
if (sm == null)
{
Console.WriteLine($"Cronjob.Run(Member {m.Number}, {m.Nickname}): ERROR, cannot find sm account!");
}
else
Run(m, sm);
}
//! \short Perform a cronjob run on a List of members.
//! \brief Perform a cronjob run on a List of members. This may be a
//! partial List (e.g. by checking member entries from GUI)
//! \param partialList List of members to process. If null, the program will process the complete member list.
public static void Run(List<Member> partialList = null)
{
Member sm = Program.members.Find(Member.eRole.Schatzmeister);
if (sm == null)
{
Console.WriteLine($"Cronjob.Run(List of {partialList.Count} members): ERROR, cannot find schatzmeister account!");
}
if (partialList == null || partialList == Program.members.Entries)
{
foreach (Member m in Program.members.Entries) Run(m, sm);
//GenerateReport();
if (!Program.config.LastCronjobRun.Equals(ProgramStartTime))
{
Member schriftfuehrer = Program.members.Find(Member.eRole.Schriftfuehrer);
try { if (sm != null) new MemberReport(true).Send(sm); }
catch (Exception ex) { Console.WriteLine($"Cronjob.GenerateReport(): Error while sending report to Schatzmeister: {ex.Message}"); }
try { if (schriftfuehrer != null) new MemberReport(false).Send(schriftfuehrer); }
catch (Exception ex) { Console.WriteLine($"Cronjob.GenerateReport(): Error while sending report to schriftfuehrer: {ex.Message}"); }
Console.WriteLine("Cronjob.Run(): Member Reports sent.");
}
Program.config.LastCronjobRun = ProgramStartTime;
try
{
XmlData.SaveToFile(Program.ConfigFile, Program.config);
Console.WriteLine("Cronjob.Run(): Saved config.");
}
catch (Exception ex)
{
Console.WriteLine($"Cronjob.Run(): Error while saving config: {ex.Message}");
}
}
else
{
foreach (Member m in partialList) Run(m, sm);
}
}
//! \short Send greetings E-Mail to new member, if conditions are met.
//! \note Also sends a mail to vorstand mailinglist
//! \param m Member to check
private static void CheckGreeting(Member m)
{
if (m.Status == Member.eStatus.Uninitialized && m.EMail.Length > 0)
@ -43,6 +120,9 @@ namespace dezentrale
m.SaveToFile();
}
}
//! \short Check if a previously greeted member is ready to become active.
//! \note Also sends a mail to vorstand mailinglist
private static void CheckSpawning(Member m)
{
if(m.Status != Member.eStatus.Greeted)
@ -103,16 +183,19 @@ namespace dezentrale
m.Status = Member.eStatus.Active;
m.SaveToFile();
}
}
}
//! \brief This shall degrade the balance by a membership fee per month.
//! Calls to this function within the same month will not degrade
//! balance. Subsequent calls at the end of the month, and then
//! at the start of the next month, will degrade balance.
//! \param m Member to check
private static void BalanceDegrade(Member m)
{
if (m.Status == Member.eStatus.Greeted && (DateTime.Now < m.SpawnDate)) return;
if (m.Status == Member.eStatus.Uninitialized || m.Status == Member.eStatus.Disabled || m.Status == Member.eStatus.Deleted) return;
//this shall degrade the balance by a membership fee per month.
//calls to this function within the same month must not degrade balance.
//subsequent calls at the end of the month, and then at the start of the next month, must degrade balance.
uint thisMonth = (uint) DateTime.Now.Month;
uint thisYear = (uint) DateTime.Now.Year;
@ -131,10 +214,13 @@ namespace dezentrale
}
}
//During program execution, we want to keep a constant timestamp for
//cronjob activity, to be able to compare for multiple runs (e.g. member list)
//and not to store it every time to disk
//! \brief During program execution, we want to keep a constant
//! timestamp for cronjob activity, to be able to compare for
//! multiple runs (e.g. member list) and not to store it every
//! time to disk
private static DateTime ProgramStartTime { get; set; } = DateTime.Now;
//! \short Necessary actions prior to BalanceDegrade.
private static void CheckBalance_PreDegrade(Member m, Member sm = null)
{
if (m.Status != Member.eStatus.Active || m.PaymentAmount == 0) return;
@ -191,66 +277,5 @@ namespace dezentrale
if (m.CurrentLog != null)
m.SaveToFile();
}
private static void GenerateReport()
{
if (!Program.config.LastCronjobRun.Equals(ProgramStartTime))
{
Member schatzmeister = Program.members.Find(Member.eRole.Schatzmeister);
Member schriftfuehrer = Program.members.Find(Member.eRole.Schriftfuehrer);
try { if (schatzmeister != null) new MemberReport(true).Send(schatzmeister); }
catch(Exception ex) { Console.WriteLine($"Cronjob.GenerateReport(): Error while sending report to Schatzmeister: {ex.Message}"); }
try { if (schriftfuehrer != null) new MemberReport(false).Send(schriftfuehrer); }
catch(Exception ex) { Console.WriteLine($"Cronjob.GenerateReport(): Error while sending report to schriftfuehrer: {ex.Message}"); }
Console.WriteLine("Cronjob.Run(): Member Reports sent.");
}
}
public static void Run(Member m, Member sm)
{
Console.WriteLine($"Processing member {m.Number:D3} ({m.Nickname})...");
CheckGreeting(m);
CheckSpawning(m);
CheckBalance_PreDegrade (m, sm);
BalanceDegrade(m);
CheckBalance_PostDegrade(m, sm);
}
public static void Run(Member m)
{
Member sm = Program.members.Find(Member.eRole.Schatzmeister);
if (sm == null)
{
Console.WriteLine($"Cronjob.Run(Member {m.Number}, {m.Nickname}): ERROR, cannot find sm account!");
}
else
Run(m, sm);
}
public static void Run(List<Member> partialList = null)
{
Member sm = Program.members.Find(Member.eRole.Schatzmeister);
if(sm == null)
{
Console.WriteLine($"Cronjob.Run(List of {partialList.Count} members): ERROR, cannot find sm account!");
}
if (partialList == null || partialList == Program.members.Entries)
{
foreach (Member m in Program.members.Entries) Run(m, sm);
GenerateReport();
Program.config.LastCronjobRun = ProgramStartTime;
try
{
XmlData.SaveToFile(Program.ConfigFile, Program.config);
Console.WriteLine("Cronjob.Run(): Saved config.");
}
catch (Exception ex)
{
Console.WriteLine($"Cronjob.Run(): Error while saving config: {ex.Message}");
}
} else
{
foreach (Member m in partialList) Run(m, sm);
}
}
}
}

View File

@ -6,8 +6,10 @@ using dezentrale.model;
namespace dezentrale.core
{
/** \brief Export functionality for the database contents
*/
//! \short Export functionality for the database contents
//! \brief The normal export workflow is to pack every xml file from the
//! data directory to the zip file configured in the Configuration.
//! After that, a mercurial or git commit is issued, followed by a push.
public class ExportProcess : ImportExportBase
{
public ExportProcess()
@ -16,6 +18,13 @@ namespace dezentrale.core
Steps = 6;
}
//! \short Run export
//! \brief Zips the files in the data folder, then runs gpg to encrypt
//! and commits/pushes it to a hg or git repo
//! \return true if the import was successful.
//! \note May lead to a broken repository structure ("multiple heads")
//! if push fails
//! \todo Re-Implement validation (commented out)
protected override bool Run()
{
LogTarget.StepStarted(0, "Preparing export");

View File

@ -4,10 +4,20 @@ using dezentrale.model;
namespace dezentrale.core
{
//! \short Logging interface for files, gui and processes.
public interface ILogger
{
//! \short Clear a log window, if available
void Clear();
//! \short Log the string to the output, only appending a newline
//! \param text The informational payload, free-text. Can be multi-line too
void LogRaw(string text);
//! \short Log a formatted line using the given info from the parameters
//! \param text The informational payload, free-text. use one text line only
//! \param logLevel Severity of the log event
//! \param module Current code unit, can be used to sort out debugging issues later
void LogLine(string text, LogEvent.ELogLevel logLevel = LogEvent.ELogLevel.Info, string module = "");
}
}

View File

@ -1,6 +1,7 @@
using System;
namespace dezentrale.core
{
//! \short Interface for controlling processes and for getting more detailled information from them
public interface IProcessController : ILogger
{
void ActionCompleted(bool success);

View File

@ -4,20 +4,42 @@ using System.Threading;
namespace dezentrale.core
{
//! \short Support functionality for import/export processes
//! \brief Provides methods for executing git/mercurial, holds variables
//! used by the subclasses. Makes use of an ILogger instance
//! for program output.
public abstract class ImportExportBase : BackgroundProcess
{
public model.MemberImportExport ImportExportSettings { get; set; } = null;
public string MemberDir { get; set; } = null; //! Directory that holds the application settings
public string InputDir { get; set; } = null; //! Import working directory
public string OutputDir { get; set; } = null; //! Export working directory
public model.MemberImportExport ImportExportSettings { get; set; } = null; //!< Instance of the MemberImportExport settings from the configuration class
public string MemberDir { get; set; } = null; //!< Directory that holds the application settings
public string InputDir { get; set; } = null; //!< Import working directory
public string OutputDir { get; set; } = null; //!< Export working directory
public static bool Gpg(string args, string gpgPassword, core.ILogger log = null)
//! \brief Run the gpg command with a specified passphrase, in batch-mode without user interaction
//! \param args Custom args to be appended to the gpg call
//! \param gpgPassword Passphrase to be used by GPG to encrypt/decrypt stuff
//! \param log Optional logger instance. If ommitted, Console.WriteLine() will be used
//! \return true if the program executed successfully
//! \return false if gpg was not found or there was another error (see log output)
public static bool Gpg(string args, string gpgPassword, ILogger log = null)
{
string argsFull = $"--batch --yes --passphrase \"{gpgPassword}\" {args}";
return RunProcess("gpg", argsFull, ".", log);
}
public static bool RunProcess(string cmd, string args, string workingDir = ".", core.ILogger log = null)
//! \short Run a background process (executable)
//! \brief Start a process given by command and arguments,
//! wait for it to finish and log the results.
//! \param cmd Command to execute
//! \param args Argument string (arguments are space-separated).
//! \param workingDir Directory to work in for the external program.
//! \param log Optional logger instance. If ommitted, Console.WriteLine() will be used
//! \return true if the program executed successfully
//! \return false if gpg was not found or there was another error (see log output)
//! \note Spaces within arguments that aren't catched by quotes or escaped with
//! backslash will lead to broken argument parsing by the external program
public static bool RunProcess(string cmd, string args, string workingDir = ".", ILogger log = null)
{
try
{

View File

@ -6,6 +6,10 @@ using dezentrale.model;
namespace dezentrale.core
{
//! \short Import functionality for the database contents
//! \brief The normal export workflow is to pull the newest commits from mercurial
//! or git and update/checkout them. After that, there is a packed zip file
//! available that will be unpacked to the data directory
public class ImportProcess : ImportExportBase
{
public ImportProcess()
@ -13,6 +17,14 @@ namespace dezentrale.core
Caption = "Database import";
Steps = 5;
}
//! \short Run import
//! \brief Requests mercurial/git commits from remote, applies them
//! locally, then decrypts using gpg and unzips the result to
//! the member directory.
//! \return true if the import was successful.
//! \note This may corrupt your local db if it fails!
//! \todo Recover from failure by restoring backups
protected override bool Run()
{

108
core/ProcessCsv.cs Normal file
View File

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.IO;
using dezentrale.model;
using dezentrale.model.money;
namespace dezentrale.core
{
public class ProcessCsv
{
public static bool ProcessCSV(string fileName)
{
CsvFile csv = new CsvFile();
csv.FieldSeparator = ';';
try
{
List<BankTransfer> tmpList = new List<BankTransfer>();
csv.ReadFile(fileName);
List<string> headlineFields = null;
List<Member> changedMembers = new List<Member>();
foreach (List<string> l in csv.FileContents)
{
if (headlineFields == null)
{
//The first line is expected to have the headline field first, describing the contents
headlineFields = l;
continue;
}
BankTransfer bt = new BankTransfer(headlineFields, l);
MoneyTransfer duplicate = Program.MoneyTransfers.FindEqualEntry(bt);
if (duplicate != null)
{
Console.WriteLine("Duplicate MoneyTransfer found");
Console.WriteLine($"ValutaDate: {bt.ValutaDate}, Amount = {bt.AmountString} {bt.Currency}, Reason = \"{bt.TransferReason.Replace('\r', '\\').Replace('\n', '\\')}\"");
}
else
{
Program.MoneyTransfers.AddEntry(bt);
tmpList.Add(bt);
}
}
//try to assign transfers to the members
foreach (BankTransfer bt in tmpList)
{
if (bt.Amount < 0)
{
bt.TransferType = MoneyTransfer.eTransferType.RunningCost;
Console.WriteLine($"{bt.Id}: Amount = {bt.AmountString} --> RunningCost");
continue;
}
foreach (Member m in Program.members.Entries)
{
if (m.CheckBankTransfer(bt))
{
bt.TransferType = MoneyTransfer.eTransferType.MembershipPayment;
changedMembers.Add(m);
m.StartLogEvent("Incoming bank transfer", LogEvent.eEventType.MembershipPayment, "automatic");
m.ApplyMoneyTransfer(bt);
bt.AssignFixed = true;
break; //this is important. We don't want to assign this to multiple members.
}
}
}
//Store bank transfer list
Console.WriteLine("ProcessCSV(): Storing money transfers...");
Program.MoneyTransfers.Entries.Sort();
if (!Program.MoneyTransfers.SaveToFile())
return false;
bool ret = true;
/*
//automaticly saved in ApplyMoneyTransfer()
foreach (Member m in changedMembers)
{
if (m.CurrentLog != null)
{
Console.Write($"ProcessCSV(): Storing {m.GetFileName()}... ");
try
{
if (!m.SaveToFile()) return false;
Console.WriteLine("OK");
} catch(Exception ex2)
{
Console.WriteLine($"ERROR: {ex2.Message}");
ret = false;
}
}
}*/
//TBD: mail to schatzmeister if there are unassigned transfers
return ret;
}
catch (Exception ex)
{
Console.WriteLine($"Error while processing csv file \"{fileName}\":");
Console.WriteLine(ex.Message);
return false;
}
}
}
}

41
core/Utils.cs Normal file
View File

@ -0,0 +1,41 @@
using System;
namespace dezentrale.core
{
public class Utils
{
public static string PadLeft(string input, uint totalLength)
{
if (input.Length < totalLength)
return new string(' ', (int)(totalLength - input.Length)) + input;
else
return input;
}
//TBD: This is an utility function and should reside in an extra class
public static string Int64FPToString(Int64 value)
{
//float fValue = ((float) value) / 100;
//return $"{fValue:0.00}";
Int64 intPart = value / 100;
Int64 decPart = (intPart < 0 ? -1 : 1) * (value % 100);
return $"{intPart},{decPart:D2}";
}
//TBD: This is an utility function and should reside in an extra class
public static Int64 StringToInt64FP(string value)
{
if (value == null) return 0;
if (!value.Contains(".") && !value.Contains(","))
{
return Convert.ToInt64(value) * 100;
}
int i = value.IndexOf('.');
if (i < 0) i = value.IndexOf(',');
string intPart = value.Substring(0, i);
string decPart = value.Substring(i + 1);
Int64 ip64 = Convert.ToInt64(intPart);
return (ip64 * 100) + (ip64 > 0 ? Convert.ToInt64(decPart) : -Convert.ToInt64(decPart));
}
}
}

View File

@ -60,7 +60,6 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Cronjob.cs" />
<Compile Include="ICSharpCode.SharpZipLib\BZip2\BZip2.cs" />
<Compile Include="ICSharpCode.SharpZipLib\BZip2\BZip2Constants.cs" />
<Compile Include="ICSharpCode.SharpZipLib\BZip2\BZip2Exception.cs" />
@ -190,9 +189,13 @@
<Compile Include="view\ConsoleLogger.cs" />
<Compile Include="core\ExportProcess.cs" />
<Compile Include="core\ImportExportBase.cs" />
<Compile Include="core\Cronjob.cs" />
<Compile Include="core\ProcessCsv.cs" />
<Compile Include="core\Utils.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="doxygen.conf" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">

2494
doxygen.conf Normal file

File diff suppressed because it is too large Load Diff

View File

@ -90,13 +90,23 @@ namespace dezentrale.model
//Build up Mail Message
MailMessage message = new MailMessage();
message.From = new MailAddress(Program.config.Smtp.From);
MailAddress fromAddress = new MailAddress(Program.config.Smtp.From);
message.From = fromAddress;
message.To.Add(new MailAddress(src.To));
if (!string.IsNullOrEmpty(Program.config.Smtp.CcTo))
message.CC.Add(new MailAddress(Program.config.Smtp.CcTo));
message.SubjectEncoding = Encoding.UTF8;
message.BodyEncoding = Encoding.UTF8;
//For spam reasons, we need to include an User-Agent and a valid message ID
//string mid;
//message.Headers.Add("User-Agent", $"dezentrale-members ver. {Program.VersionNumber} https://hg.macht-albern.de/dezentrale-members/");
//message.Headers.Add("User-Agent", @"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:60.0) Gecko/20100101 Thunderbird/60.7.0 Lightning/6.2.7");
message.Headers.Add("Message-Id",
$"<{Guid.NewGuid()}@{fromAddress.Host}>");
//System.Windows.Forms.MessageBox.Show($"Message-Id: {mid}");
message.Subject = src.Subject;
message.Body = src.Body;
@ -472,6 +482,21 @@ namespace dezentrale.model
+ "--\n"
+ "Dies ist eine automatisch generierte E-Mail.\n",
};
}
public static FormMail GenerateTestmail()
{
return new FormMail()
{
To = "{EMailName} <{EMail}>",
Subject = "dezentrale-members - Testmail",
Body = "Hallo {EMailName}!\n"
+ "\n"
+ "Dies ist ein Test der Mail-Einstellungen von dezentrale-members.\n"
+ "\n"
+ "--\n"
+ "Dies ist eine automatisch generierte E-Mail.\n",
};
}
}
}

View File

@ -42,89 +42,92 @@ namespace dezentrale.model
Normal,
}
private uint number = 0;
private eRole role = eRole.Normal;
private eType type = eType.Regulaer;
private eStatus status = eStatus.Uninitialized;
private string remarks = "";
private uint mvMiss = 0;
private Int64 accountBalance = 0;
private bool evaluateAccountInCharge = false;
private string bankAccountInCharge = ""; //CSV Import: this bank account is associated with the member
private string bankTransferRegEx = "";
private string nickname = "";
private string firstName = "";
private string lastName = "";
private string street = "";
private string houseNo = "";
private string zipcode = "";
private string city = "";
private uint number = 0;
private eRole role = eRole.Normal;
private eType type = eType.Regulaer;
private eStatus status = eStatus.Uninitialized;
private string remarks = "";
private uint mvMiss = 0;
private Int64 accountBalance = 0;
private bool evaluateAccountInCharge = false;
private string bankAccountInCharge = ""; //CSV Import: this bank account is associated with the member
private string bankTransferRegEx = "";
private string nickname = "";
private string firstName = "";
private string lastName = "";
private string street = "";
private string houseNo = "";
private string zipcode = "";
private string city = "";
private Country.eCountry countryCode = Country.eCountry.Germany;
private DateTime birthday;
private string email = "";
private string pgpFingerprint = "";
private bool mvInvitationByPost = false;
private string email = "";
private string emailName = ""; //Name to use via E-Mail
private string pgpFingerprint = "";
private bool mvInvitationByPost = false;
private DateTime spawnDate;
private UInt64 paymentAmount = Program.config.RegularPaymentAmount;
private UInt64 paymentAmount = Program.config.RegularPaymentAmount;
private ePaymentClass paymentClass = ePaymentClass.Normal;
private bool paymentNotify = false;
private bool paymentNotify = false;
private DateTime memberFormDate;
//This is a bit ugly but I didn't have a good idea how to solve it better.
//The main goal is to track every change to every property.
//Therefore, private variables are defined and the get/set methods are handled manually.
//A property change is then tracked using LogPropertyChange()
[XmlAttribute] public uint Number { get { return number; } set { LogPropertyChange("Number", number, value); number = value; } }
[XmlAttribute] public eType Type { get { return type; } set { LogPropertyChange("Type", type, value); type = value; } }
[XmlAttribute] public eStatus Status { get { return status; } set { LogPropertyChange("Status", status, value); status = value; } }
[XmlElement] public eRole Role { get { return role; } set { LogPropertyChange("Role", role, value); role = value; } }
[XmlElement] public string Remarks { get { return remarks; } set { LogPropertyChange("Remarks", remarks, value); remarks = value; } }
[XmlElement] public uint MvMissCounter { get { return mvMiss; } set { LogPropertyChange("MvMissCounter", mvMiss, value); mvMiss = value; } }
[XmlElement] public Int64 AccountBalance { get { return accountBalance;} set{LogPropertyChange("AccountBalance",accountBalance,value);accountBalance=value;}}
[XmlElement] public bool EvaluateAccountInCharge { get { return evaluateAccountInCharge; } set { LogPropertyChange("EvaluateAccountInCharge", evaluateAccountInCharge, value); evaluateAccountInCharge = value; } }
[XmlElement] public string BankAccountInCharge { get { return bankAccountInCharge; }set{LogPropertyChange("BankAccountInCharge", bankAccountInCharge, value); bankAccountInCharge = value;} }
[XmlElement] public string BankTransferRegEx { get { return bankTransferRegEx; } set { LogPropertyChange("BankTransferRegEx", bankTransferRegEx, value); bankTransferRegEx = value; } }
[XmlAttribute] public uint Number { get { return number; } set { LogPropertyChange("Number", number, value); number = value; } }
[XmlAttribute] public eType Type { get { return type; } set { LogPropertyChange("Type", type, value); type = value; } }
[XmlAttribute] public eStatus Status { get { return status; } set { LogPropertyChange("Status", status, value); status = value; } }
[XmlElement] public eRole Role { get { return role; } set { LogPropertyChange("Role", role, value); role = value; } }
[XmlElement] public string Remarks { get { return remarks; } set { LogPropertyChange("Remarks", remarks, value); remarks = value; } }
[XmlElement] public uint MvMissCounter { get { return mvMiss; } set { LogPropertyChange("MvMissCounter", mvMiss, value); mvMiss = value; } }
[XmlElement] public Int64 AccountBalance { get { return accountBalance; } set { LogPropertyChange("AccountBalance", accountBalance, value); accountBalance = value; } }
[XmlElement] public bool EvaluateAccountInCharge { get { return evaluateAccountInCharge; } set { LogPropertyChange("EvaluateAccountInCharge", evaluateAccountInCharge, value); evaluateAccountInCharge = value; } }
[XmlElement] public string BankAccountInCharge { get { return bankAccountInCharge; } set { LogPropertyChange("BankAccountInCharge", bankAccountInCharge, value); bankAccountInCharge = value; } }
[XmlElement] public string BankTransferRegEx { get { return bankTransferRegEx; } set { LogPropertyChange("BankTransferRegEx", bankTransferRegEx, value); bankTransferRegEx = value; } }
//personal data
[XmlAttribute] public string Nickname { get { return nickname; } set { LogPropertyChange("NickName", nickname, value); nickname = value; } }
[XmlElement] public string FirstName { get { return firstName; } set { LogPropertyChange("FirstName", firstName, value); firstName = value; } }
[XmlElement] public string LastName { get { return lastName; } set { LogPropertyChange("LastName", lastName, value); lastName = value; } }
[XmlElement] public string Street { get { return street; } set { LogPropertyChange("Street", street, value); street = value; } }
[XmlElement] public string HouseNumber { get { return houseNo; } set { LogPropertyChange("HouseNumber", houseNo, value); houseNo = value; } }
[XmlElement] public string Zipcode { get { return zipcode; } set { LogPropertyChange("Zipcode", zipcode, value); zipcode = value; } }
[XmlElement] public string City { get { return city; } set { LogPropertyChange("City", city, value); city = value; } }
[XmlIgnore] public Country.eCountry CountryCode{ get {return countryCode;} set { LogPropertyChange("CountryCode", countryCode,value); countryCode = value; } }
[XmlElement("Country")] public uint CountryUInt32{ get { return (uint)CountryCode; } set { CountryCode = (Country.eCountry)value; }}
[XmlElement] public DateTime Birthday { get { return birthday; } set { LogPropertyChange("Birthday", birthday, value); birthday = value; } }
[XmlElement] public string EMail { get { return email; } set { LogPropertyChange("EMail", email, value); email = value; } }
[XmlElement] public string PgpFingerprint { get{return pgpFingerprint;}set{ LogPropertyChange("PgpFingerprint",pgpFingerprint,value); pgpFingerprint = value; } }
[XmlAttribute] public string Nickname { get { return nickname; } set { LogPropertyChange("NickName", nickname, value); nickname = value; } }
[XmlElement] public string FirstName { get { return firstName; } set { LogPropertyChange("FirstName", firstName, value); firstName = value; } }
[XmlElement] public string LastName { get { return lastName; } set { LogPropertyChange("LastName", lastName, value); lastName = value; } }
[XmlElement] public string Street { get { return street; } set { LogPropertyChange("Street", street, value); street = value; } }
[XmlElement] public string HouseNumber { get { return houseNo; } set { LogPropertyChange("HouseNumber", houseNo, value); houseNo = value; } }
[XmlElement] public string Zipcode { get { return zipcode; } set { LogPropertyChange("Zipcode", zipcode, value); zipcode = value; } }
[XmlElement] public string City { get { return city; } set { LogPropertyChange("City", city, value); city = value; } }
[XmlIgnore] public Country.eCountry CountryCode { get { return countryCode; } set { LogPropertyChange("CountryCode", countryCode, value); countryCode = value; } }
[XmlElement("Country")] public uint CountryUInt32 { get { return (uint)CountryCode; } set { CountryCode = (Country.eCountry)value; } }
[XmlElement] public DateTime Birthday { get { return birthday; } set { LogPropertyChange("Birthday", birthday, value); birthday = value; } }
[XmlElement] public string EMail { get { return email; } set { LogPropertyChange("EMail", email, value); email = value; } }
[XmlElement] public string EMailName { get { return emailName; } set { LogPropertyChange("EMailName", emailName, value); emailName = value; } }
[XmlElement] public string PgpFingerprint { get { return pgpFingerprint; } set { LogPropertyChange("PgpFingerprint", pgpFingerprint, value); pgpFingerprint = value; } }
//membership organizational data
[XmlElement] public bool MvInvitationByPost { get { return mvInvitationByPost; } set { LogPropertyChange("MvInvitationByPost", mvInvitationByPost, value); mvInvitationByPost = value; } }
[XmlElement] public DateTime SpawnDate { get { return spawnDate; } set { LogPropertyChange("SpawnDate", spawnDate, value); spawnDate = value; } }
[XmlElement] public UInt64 PaymentAmount { get { return paymentAmount;} set { LogPropertyChange("PaymentAmount", paymentAmount, value); paymentAmount = value; } }
[XmlElement] public ePaymentClass PaymentClass { get { return paymentClass; } set { LogPropertyChange("PaymentClass", paymentClass, value); paymentClass = value; } }
[XmlElement] public bool PaymentNotify { get { return paymentNotify; } set { LogPropertyChange("PaymentNotify", paymentNotify, value); paymentNotify = value; } }
[XmlElement] public DateTime MemberFormDate { get { return memberFormDate; } set { LogPropertyChange("MemberFormDate", memberFormDate, value); memberFormDate = value;} }
[XmlElement] public bool MvInvitationByPost { get { return mvInvitationByPost; } set { LogPropertyChange("MvInvitationByPost", mvInvitationByPost, value); mvInvitationByPost = value; } }
[XmlElement] public DateTime SpawnDate { get { return spawnDate; } set { LogPropertyChange("SpawnDate", spawnDate, value); spawnDate = value; } }
[XmlElement] public UInt64 PaymentAmount { get { return paymentAmount; } set { LogPropertyChange("PaymentAmount", paymentAmount, value); paymentAmount = value; } }
[XmlElement] public ePaymentClass PaymentClass { get { return paymentClass; } set { LogPropertyChange("PaymentClass", paymentClass, value); paymentClass = value; } }
[XmlElement] public bool PaymentNotify { get { return paymentNotify; } set { LogPropertyChange("PaymentNotify", paymentNotify, value); paymentNotify = value; } }
[XmlElement] public DateTime MemberFormDate { get { return memberFormDate; } set { LogPropertyChange("MemberFormDate", memberFormDate, value); memberFormDate = value; } }
//internal management data
[XmlElement] public DateTime GreetedDate { get; set; }
[XmlElement] public DateTime LastPaymentProcessed { get; set; } = DateTime.Now;
[XmlElement] public uint PaymentsTotal { get; set; }
[XmlElement] public DateTime LastBalanceDegrade { get; set; } = DateTime.Now;
[XmlElement("MoneyTransferId")] public List<string> MoneyTransfersIds { get; set; } = new List<string>();
[XmlElement] public DateTime GreetedDate { get; set; }
[XmlElement] public DateTime LastPaymentProcessed { get; set; } = DateTime.Now;
[XmlElement] public uint PaymentsTotal { get; set; }
[XmlElement] public DateTime LastBalanceDegrade { get; set; } = DateTime.Now;
[XmlElement("MoneyTransferId")] public List<string> MoneyTransfersIds { get; set; } = new List<string>();
[XmlElement("LogEvent")] public List<LogEvent> Log { get; set; } = new List<LogEvent>();
[XmlIgnore] public string AccountBalanceString { get { return $"{((float)accountBalance / 100)}"; } }
[XmlIgnore] public string PaymentAmountString { get { return $"{((float)paymentAmount / 100)}"; } }
[XmlElement("LogEvent")] public List<LogEvent> Log { get; set; } = new List<LogEvent>();
[XmlIgnore] public string AccountBalanceString { get { return $"{((float)accountBalance / 100)}"; } }
[XmlIgnore] public string PaymentAmountString { get { return $"{((float)paymentAmount / 100)}"; } }
[XmlIgnore] public string PaymentAmountCurrency { get { return Program.config.RegularPaymentCurrency; } }
[XmlIgnore] public string PaymentDueMonth
[XmlIgnore]
public string PaymentDueMonth
{
get
{
int monthsPaid = (int) Math.Floor( (float)AccountBalance / (float)PaymentAmount ) + 1;
int monthsPaid = (int)Math.Floor((float)AccountBalance / (float)PaymentAmount) + 1;
DateTime dueMonth = DateTime.Now.AddMonths(monthsPaid);
if (dueMonth < spawnDate)
return spawnDate.ToString("yyyy-MM");
@ -149,7 +152,7 @@ namespace dezentrale.model
public int CompareTo(Member other)
{
if (other == null) return 1;
else return number.CompareTo(other.Number);
else return number.CompareTo(other.Number);
}
public LogEvent StartLogEvent(string topic, LogEvent.eEventType type, string user = null)
@ -174,7 +177,7 @@ namespace dezentrale.model
string ov = oldValue.ToString();
string nv = newValue.ToString();
if(!ov.Equals(nv))
if (!ov.Equals(nv))
CurrentLog.SubEvents.Add(new LogSubEvent()
{
Topic = $"{propertyName} changes from \"{oldValue.ToString()}\" to \"{newValue.ToString()}\"",
@ -187,7 +190,7 @@ namespace dezentrale.model
}
public bool SaveToFile(bool finishLog = true)
{
if(finishLog) FinishLogEvent();
if (finishLog) FinishLogEvent();
string completePath = System.IO.Path.Combine(Program.config.DbDirectory, GetFileName());
return XmlData.SaveToFile(completePath, this);
}
@ -207,7 +210,8 @@ namespace dezentrale.model
try
{
return Regex.IsMatch(t.TransferReason, BankTransferRegEx);
} catch(Exception ex)
}
catch (Exception ex)
{
Console.WriteLine($"Member {this.Number:D3} invalid RegEx: {ex.Message}");
return false;
@ -218,7 +222,7 @@ namespace dezentrale.model
{
if (CurrentLog == null) StartLogEvent("PaymentAdjustBalance", LogEvent.eEventType.MembershipPayment, user);
Int64 result = accountBalance + amount;
if(amount > 0) PaymentsTotal += (uint) amount;
if (amount > 0) PaymentsTotal += (uint)amount;
CurrentLog.SubEvents.Add(new LogSubEvent()
{
Topic = $"Amount = {((float)amount / 100)}",
@ -226,14 +230,21 @@ namespace dezentrale.model
Details = $"Old balance = {((float)accountBalance / 100)}\nAdd Amount = {((float)amount / 100)}\nNew balance = {((float)result / 100)}",
});
accountBalance = result;
}
}
public void TestMail()
{
FormMail testMail = FormMail.GenerateTestmail();
testMail.Send(this);
}
public void ApplyMoneyTransfer(MoneyTransfer t, string user = null)
{
Console.WriteLine($"Member.ApplyMoneyTransfer(): {t.Amount} to member {Number}");
if (CurrentLog == null) StartLogEvent($"MoneyTransfer ({t.AmountString} {t.Currency})", LogEvent.eEventType.MembershipPayment, user);
LogEvent.eEventType evt = LogEvent.eEventType.Generic;
switch(t.TransferType)
switch (t.TransferType)
{
case MoneyTransfer.eTransferType.MembershipDonation:
evt = LogEvent.eEventType.MembershipDonation; break;
@ -293,8 +304,8 @@ namespace dezentrale.model
}
}
try { SaveToFile(false); }
catch(Exception ex) { Console.WriteLine($"Error while saving member: {ex.Message}"); }
try { SaveToFile(false); }
catch (Exception ex) { Console.WriteLine($"Error while saving member: {ex.Message}"); }
}
}
}

View File

@ -29,10 +29,10 @@ namespace dezentrale.model
{
if (memberList && m.AccountBalance != 0)
{
string formattedBalance = Program.PadLeft(Program.Int64FPToString(m.AccountBalance), 8);
string formattedBalance = core.Utils.PadLeft(core.Utils.Int64FPToString(m.AccountBalance), 8);
//while (formattedBalance.Length < 8) formattedBalance = " " + formattedBalance;
string formattedStatus = Program.PadLeft($"{m.Status}", 8);
string formattedStatus = core.Utils.PadLeft($"{m.Status}", 8);
//while (formattedStatus.Length < 8) formattedStatus = formattedStatus + " ";
mList += $"{m.Number:D3} | {formattedBalance} | {formattedStatus} | {m.Nickname}\n";
}

View File

@ -31,7 +31,7 @@ namespace dezentrale.model.money
[XmlElement] public string Currency { get; set; } = "EUR";
[XmlElement] public string TransferReason { get; set; } = "";
[XmlIgnore] public string AmountString { get { return Program.Int64FPToString(Amount);/* $"{((float) Amount / 100)}";*/ } }
[XmlIgnore] public string AmountString { get { return core.Utils.Int64FPToString(Amount);/* $"{((float) Amount / 100)}";*/ } }
public override string ToString() { return Id; }
public MoneyTransfer()

57
view/CurrencyBox.cs Normal file
View File

@ -0,0 +1,57 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace dezentrale.view
{
public class CurrencyBox : Panel
{
public Int64 Value
{
get
{
return Convert.ToInt64(intPart.Text) * 100 + Convert.ToInt64(decimals.Text);
}
set
{
//todo: this might be wrong for negative values
intPart.Text = $"{(Int64) (value / 100)}";
decimals.Text = $"{(Int64) (value % 100)}";
}
}
private TextBox intPart;
private TextBox decimals;
public override Color BackColor
{
get { return base.BackColor; }
set { base.BackColor = value; }
}
public CurrencyBox()
{
this.MinimumSize = new Size(100,16);
this.Controls.Add(intPart = new TextBox()
{
Width = this.Width - 30,
Anchor = AnchorStyles.Left | AnchorStyles.Right,
Text = "0"
});
intPart.TextChanged += IntPart_TextChanged;
this.Controls.Add(decimals = new TextBox()
{
Left = 75,
Width = 25,
Anchor = AnchorStyles.Right,
Text = "00"
});
}
private void IntPart_TextChanged(object sender, EventArgs e)
{
throw new NotImplementedException();
}
}
}

View File

@ -151,7 +151,7 @@ namespace dezentrale.view
});
tbDbDirectory.Text = Program.config.DbDirectory;
tbRegularPayment.Text = Program.Int64FPToString(Program.config.RegularPaymentAmount);
tbRegularPayment.Text = core.Utils.Int64FPToString(Program.config.RegularPaymentAmount);
if (Program.config.RegularPaymentCurrency != "EUR")
{
cbRegularCurrency.Items.Add(Program.config.RegularPaymentCurrency);
@ -485,7 +485,7 @@ namespace dezentrale.view
public void FillAndSaveConfig()
{
Program.config.DbDirectory = tbDbDirectory.Text;
Program.config.RegularPaymentAmount = (uint) Program.StringToInt64FP(tbRegularPayment.Text);
Program.config.RegularPaymentAmount = (uint) core.Utils.StringToInt64FP(tbRegularPayment.Text);
Program.config.RegularPaymentCurrency = cbRegularCurrency.Text;
Program.config.LocalUser = tbLocalUser.Text;
if (Program.config.KeylockCombination != tbKeylockCombination.Text)

View File

@ -14,6 +14,7 @@ namespace dezentrale.view
{
private Member member;
private bool newMember = false;
private bool eMailNameDefaults = true;
private TextBox tbFirstName;
private TextBox tbLastName;
@ -24,6 +25,7 @@ namespace dezentrale.view
private TextBox tbZipcode;
private TextBox tbCity;
protected ComboBoxKV cbCountry;
private TextBox tbEMailName;
private TextBox tbEMail;
private TextBox tbPgpFingerprint;
private ComboBoxKV cbType;
@ -177,42 +179,98 @@ namespace dezentrale.view
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
DropDownStyle = ComboBoxStyle.DropDownList,
});
sc.Panel2.Controls.Add(new Label()
parent.Controls.Add(new Label()
{
Text = "EMail:",
Location = new Point(0, 3 * line + labelOffs),
Location = new Point(lm, 4 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
sc.Panel2.Controls.Add(tbEMail = new TextBox()
parent.Controls.Add(tbEMailName = new TextBox()
{
Location = new Point(118, 3 * line),
Width = w_pan2 - 118 + 2,
Location = new Point(lm + 118, 4 * line + tm),
Width = 183,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
tbNickname.TextChanged += (sender, e) =>
{
if(eMailNameDefaults)
{
tbEMailName.Text = tbNickname.Text;
}
else
if (tbEMailName.Text == tbNickname.Text)
{
eMailNameDefaults = true;
tbEMailName.ForeColor = System.Drawing.Color.DarkGray;
}
};
tbEMailName.TextChanged += (sender, e) =>
{
if(eMailNameDefaults)
{
if (tbEMailName.Text != tbNickname.Text)
{
eMailNameDefaults = false;
tbEMailName.ForeColor = System.Drawing.Color.Black;
}
} else
if (tbEMailName.Text == tbNickname.Text)
{
if(!eMailNameDefaults)
{
eMailNameDefaults = true;
tbEMailName.ForeColor = System.Drawing.Color.DarkGray;
}
}
};
parent.Controls.Add(new Label()
{
Text = "<",
Location = new Point(lm + 118 + 185, 4 * line + tm + labelOffs),
Size = new Size(12, labelHeight),
TextAlign = ContentAlignment.BottomRight,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
parent.Controls.Add(new Label()
{
Text = ">",
Location = new Point(w - 15, 4 * line + tm + labelOffs),
Size = new Size(12, labelHeight),
TextAlign = ContentAlignment.BottomRight,
Anchor = AnchorStyles.Top | AnchorStyles.Right,
});
parent.Controls.Add(tbEMail = new TextBox()
{
Location = new Point(lm + 118 + 205, 4 * line + tm),
Width = w - (lm + 118) - 222,
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
});
parent.Controls.Add(new Label()
{
Text = "PGP Fingerprint:",
Location = new Point(lm, 4 * line + tm + labelOffs),
Location = new Point(lm, 5 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
parent.Controls.Add(tbPgpFingerprint = new TextBox()
{
Location = new Point(lm + 118, 4 * line + tm),
Location = new Point(lm + 118, 5 * line + tm),
Width = w - (lm + 118),
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
});
parent.Controls.Add(new Label()
{
Text = "Membership type:",
Location = new Point(lm, 5 * line + tm + labelOffs),
Location = new Point(lm, 6 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
parent.Controls.Add(cbType = new ComboBoxKV()
{
Location = new Point(lm + 118, 5 * line + tm),
Location = new Point(lm + 118, 6 * line + tm),
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
DropDownStyle = ComboBoxStyle.DropDownList,
@ -220,13 +278,13 @@ namespace dezentrale.view
parent.Controls.Add(new Label()
{
Text = "Spawn Date:",
Location = new Point(lm, 6 * line + tm + labelOffs),
Location = new Point(lm, 7 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
parent.Controls.Add(dateSpawn = new DateTimePicker()
{
Location = new Point(lm + 118, 6 * line + tm),
Location = new Point(lm + 118, 7 * line + tm),
Width = 100,
Format = DateTimePickerFormat.Short,
//MinDate = new DateTime(1900, 01, 01, 00, 00, 00),
@ -235,13 +293,13 @@ namespace dezentrale.view
parent.Controls.Add(new Label()
{
Text = "Payment Amount:",
Location = new Point(lm, 7 * line + tm + labelOffs),
Location = new Point(lm, 8 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
parent.Controls.Add(tbPaymentAmount = new TextBox()
{
Location = new Point(lm + 118, 7 * line + tm),
Location = new Point(lm + 118, 8 * line + tm),
Width = 100,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
TextAlign = HorizontalAlignment.Right,
@ -249,20 +307,20 @@ namespace dezentrale.view
parent.Controls.Add(chkPaymentReduced = new CheckBox()
{
Text = "Reduced fee (document needed)",
Location = new Point(lm + 118 + 100 + 15, 7 * line + tm),
Location = new Point(lm + 118 + 100 + 15, 8 * line + tm),
Width = w - (lm + 118 + 100 + 15),
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
parent.Controls.Add(new Label()
{
Text = "Signed Date:",
Location = new Point(lm, 8 * line + tm + labelOffs),
Location = new Point(lm, 9 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
parent.Controls.Add(dateMemberForm = new DateTimePicker()
{
Location = new Point(lm + 118, 8 * line + tm),
Location = new Point(lm + 118, 9 * line + tm),
Width = 100,
Format = DateTimePickerFormat.Short,
//MinDate = new DateTime(1900, 01, 01, 00, 00, 00),
@ -512,7 +570,12 @@ namespace dezentrale.view
KeyValue kv = cbCountry.AddKV(display, c);
if (c.Code == member.CountryCode) cbCountry.SelectedItem = kv;
}
eMailNameDefaults = m.EMailName == m.Nickname;
tbEMail.Text = member.EMail;
tbEMailName.Text = m.EMailName;
if (eMailNameDefaults)
tbEMailName.ForeColor = System.Drawing.Color.DarkGray;
tbPgpFingerprint.Text = member.PgpFingerprint;
foreach (Member.eType type in Enum.GetValues(typeof(Member.eType)))
@ -522,7 +585,7 @@ namespace dezentrale.view
}
try { dateSpawn.Value = member.SpawnDate; }
catch (Exception) { if (!newMember) { MessageBox.Show("Invalid SpawnDate: " + member.SpawnDate + "!"); } }
tbPaymentAmount.Text = Program.Int64FPToString((Int64) member.PaymentAmount);
tbPaymentAmount.Text = core.Utils.Int64FPToString((Int64) member.PaymentAmount);
chkPaymentReduced.Checked = member.PaymentClass == Member.ePaymentClass.Reduced;
try { dateMemberForm.Value = member.MemberFormDate; }
catch (Exception) { if (!newMember) { MessageBox.Show("Invalid MemberFormDate: " + member.MemberFormDate + "!"); } }
@ -539,7 +602,7 @@ namespace dezentrale.view
}
tbMvMiss.Text = $"{member.MvMissCounter}";
tbAccountBalance.Text = Program.Int64FPToString(member.AccountBalance);
tbAccountBalance.Text = core.Utils.Int64FPToString(member.AccountBalance);
cbEvaluateAccountInCharge.Checked = member.EvaluateAccountInCharge;
tbBankAccountInCharge.ReadOnly = !cbEvaluateAccountInCharge.Checked;
tbBankAccountInCharge.Text = member.BankAccountInCharge;
@ -575,6 +638,7 @@ namespace dezentrale.view
member.CountryCode = c.Code;
}
member.EMail = tbEMail.Text;
member.EMailName = tbEMailName.Text;
member.PgpFingerprint = tbPgpFingerprint.Text;
if(cbType.SelectedItem != null)
{
@ -593,7 +657,7 @@ namespace dezentrale.view
}
try
{
member.PaymentAmount = (UInt64) Program.StringToInt64FP(tbPaymentAmount.Text);
member.PaymentAmount = (UInt64) core.Utils.StringToInt64FP(tbPaymentAmount.Text);
}
catch (Exception ex)
{
@ -618,7 +682,7 @@ namespace dezentrale.view
member.MvMissCounter = Convert.ToUInt32(tbMvMiss.Text);
try
{
member.AccountBalance = Program.StringToInt64FP(tbAccountBalance.Text);//Convert.ToInt32(tbAccountBalance.Text);
member.AccountBalance = core.Utils.StringToInt64FP(tbAccountBalance.Text);//Convert.ToInt32(tbAccountBalance.Text);
} catch(Exception ex)
{
Console.WriteLine($"Cannot convert Account balance to fixed point value:\n{ex.Message}");

View File

@ -37,7 +37,7 @@ namespace dezentrale.view
new MenuItem("&Configuration...", mnuMain_File_Configuration) { Enabled = true },
new MenuItem("-"),
new MenuItem("&Export database", mnuMain_File_Export),
new MenuItem("&Import database", mnuMain_File_Import),
new MenuItem("&Import database", mnuMain_File_Import),
new MenuItem("-"),
new MenuItem("&Quit", mnuMain_File_Quit),
} },
@ -75,7 +75,8 @@ namespace dezentrale.view
lstMembers.AddMenuItem("Cronjob selected member", lstMembers_CronjobSelected);
//lstMembers.AddMenuItem("Cronjob checked ones", lstMembers_CronjobChecked);
lstMembers.AddMenuItem("Cronjob all", lstMembers_CronjobAll);
//lstMembers.AddMenuItem("-", null);
lstMembers.AddMenuItem("-", null);
lstMembers.AddMenuItem("Send Testmail to member", lstMembers_TestMail);
//TBD: "Selected users missed an MV"
//lstMembers.AddMenuItem("Main Settings", null);
@ -158,7 +159,7 @@ namespace dezentrale.view
}
}
}
private void mnuMain_File_Quit(object sender, EventArgs e)
{
this.Close();
@ -234,7 +235,7 @@ namespace dezentrale.view
DialogResult dr = ofd.ShowDialog();
//ofd.FilterIndex
if (dr == DialogResult.OK)
Program.ProcessCSV(ofd.FileName);
ProcessCsv.ProcessCSV(ofd.FileName);
}
private void mnuMain_Help_About(object sender, EventArgs e)
@ -270,6 +271,12 @@ namespace dezentrale.view
lstMembers.ResumeLayout(false);
}
private void lstMembers_TestMail(object sender, EventArgs e)
{
Member m = lstMembers.GetFirstSelectedItem();
m?.TestMail();
}
private void lstMembers_Edit(object sender, EventArgs e)
{
Member m = lstMembers.GetFirstSelectedItem();

View File

@ -493,7 +493,7 @@ namespace dezentrale.view
mt.TransferType = (MoneyTransfer.eTransferType)((KeyValue)cbType.SelectedItem).Value;
}
mt.ValutaDate = valutaDate.Value;
mt.Amount = Program.StringToInt64FP(tbAmount.Text);
mt.Amount = core.Utils.StringToInt64FP(tbAmount.Text);
mt.Currency = cbCurrency.Text;
mt.TransferReason = tbTransferReason.Text;