2021-01-24PX Multiple changes:

- Added CsvImportProcess (porting ProcessCsv as a background process), not finished yet
- added comment window on member editing
- Changed GUI to allow editing member account balance
- Added Enable/Disable feature for Import/Export, in order to get rid of stupid messages at startup/shutdown
- Added configuration for Schatzmeister/Schriftfuehrer contact to configuration
- Fixed CurrentDebtLevel > 0 was able to prevent BalanceDegrade / postdegrade code from working
- Fixed Member list wasn't refreshed after CSV import
- Changed MembershipDonation enum type to Donation (as non-members can donate too)
This commit is contained in:
phantomix 2022-01-24 22:04:24 +01:00
parent 105afdb203
commit 4d16e11b6e
21 changed files with 512 additions and 186 deletions

View File

@ -34,7 +34,11 @@ namespace dezentrale
public static MemberList members = new MemberList();
public static MvList mvList = new MvList();
public static bool MoneyTransfersLoaded { get { return moneyTransfers != null; } }
public static bool MoneyTransfersLoaded
{
get { return moneyTransfers != null; }
set { moneyTransfers = value ? MoneyTransferList.LoadFromFile() : null; }
}
private static MoneyTransferList moneyTransfers = null;
public static MoneyTransferList MoneyTransfers
{
@ -220,8 +224,13 @@ namespace dezentrale
case eMode.Cronjob:
Cronjob.Run();
break;
case eMode.Export:
{
case eMode.Export:
{
if (!Program.config.ImportExport.Enabled)
{
Console.WriteLine("ImportExport is disabled in configuration");
return 1;
}
ExportProcess export = new ExportProcess()
{
ImportExportSettings = Program.config.ImportExport,
@ -239,7 +248,12 @@ namespace dezentrale
} break;
case eMode.Import:
{
{
if(!Program.config.ImportExport.Enabled)
{
Console.WriteLine("ImportExport is disabled in configuration");
return 1;
}
ImportProcess import = new ImportProcess()
{
ImportExportSettings = Program.config.ImportExport,

View File

@ -16,52 +16,40 @@ namespace dezentrale.core
//! \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)
//! \param smContact Mail contact entry for the Schatzmeister account, in order to send notification mails for some events
public static void Run(Member m, ConfigEMail smContact)
{
Console.WriteLine($"Processing member {m.Number:D3} ({m.Nickname})...");
CheckGreeting(m);
CheckSpawning(m);
// CheckReducedFeeValidity(m);
CheckBalance_PreDegrade(m, sm);
CheckBalance_PreDegrade(m, smContact);
BalanceDegrade(m);
CheckBalance_PostDegrade(m, sm);
CheckBalance_PostDegrade(m, smContact);
}
//! \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);
Run(m, Program.config.Schatzmeister);
}
//! \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)
//! partial List (e.g. by checking member entries from GUI)sm
//! \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);
foreach (Member m in Program.members.Entries) Run(m, Program.config.Schatzmeister);
//GenerateReport();
if (!Program.config.LastCronjobRun.Equals(ProgramStartTime))
{
Member schriftfuehrer = Program.members.Find(Member.eRole.Schriftfuehrer);
try { if (sm != null) new MemberReport(true).Send(sm); }
try { new MemberReport(true).Send(Program.config.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); }
try { new MemberReport(false).Send(Program.config.Schriftfuehrer); }
catch (Exception ex) { Console.WriteLine($"Cronjob.GenerateReport(): Error while sending report to schriftfuehrer: {ex.Message}"); }
Console.WriteLine("Cronjob.Run(): Member Reports sent.");
}
@ -79,7 +67,7 @@ namespace dezentrale.core
}
else
{
foreach (Member m in partialList) Run(m, sm);
foreach (Member m in partialList) Run(m, Program.config.Schatzmeister);
}
}
@ -262,11 +250,11 @@ namespace dezentrale.core
private static DateTime ProgramStartTime { get; set; } = DateTime.Now;
//! \short Necessary actions prior to BalanceDegrade.
private static void CheckBalance_PreDegrade(Member m, Member sm = null)
private static void CheckBalance_PreDegrade(Member m, ConfigEMail smContact = null)
{
}
private static void CheckBalance_PostDegrade(Member m, Member sm = null)
private static void CheckBalance_PostDegrade(Member m, ConfigEMail smContact = null)
{
if (m.Status != Member.eStatus.Active || m.PaymentAmount == 0) return;
@ -286,7 +274,7 @@ namespace dezentrale.core
if(!skipInsufficientNotify)
{
if (debtLevelDecrease) m.DebtLevel--;
if (debtLevelDecrease) m.DebtLevel--;
else m.DebtLevel = currentDebtLevel;
//We don't set skipInsufficientNotify here as we don't want to not-warn
//if it's still negative
@ -300,10 +288,10 @@ namespace dezentrale.core
{
//Member has given much money
m.StartLogEvent($"Excess amount of payments", LogEvent.eEventType.EMail, "automatic");
if (sm != null)
if (smContact != null)
{
FormMail above200 = FormMail.GenerateBalanceAbove200NotifySM().ReplaceReflect(m);
above200.To = $"{sm.EMailName} <{sm.EMail}>";
above200.To = smContact.ToString();
try
{
m.CurrentLog.SubEvents.Add(above200.Send());
@ -325,7 +313,11 @@ namespace dezentrale.core
case 0:
{
//all is fine.
//all is fine.
if (debtLevelDecrease || debtLevelIncrease)
{
m.StartLogEvent($"Debt level changed", LogEvent.eEventType.DataChange, "automatic");
}
} break;
case -1:
@ -369,25 +361,22 @@ namespace dezentrale.core
});
Console.WriteLine($"Cannot send Insufficient amount #2 notification: {ex.Message}");
}
if (sm != null)
FormMail below2sm = FormMail.GenerateBalanceNegativeNotify2SM().ReplaceReflect(m);
below2sm.To = smContact.ToString();
try
{
FormMail below2sm = FormMail.GenerateBalanceNegativeNotify2SM().ReplaceReflect(m);
below2sm.To = $"{sm.EMailName} <{sm.EMail}>";
try
m.CurrentLog.SubEvents.Add(below2sm.Send());
} catch(Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
m.CurrentLog.SubEvents.Add(below2sm.Send());
} catch(Exception ex)
{
m.CurrentLog.SubEvents.Add(new LogSubEvent()
{
Type = LogEvent.eEventType.Error,
Topic = "Email notification error",
Details = ex.Message,
});
Console.WriteLine($"Cannot send Insufficient amount #2 SM notification: {ex.Message}");
}
} else
Console.WriteLine("ERROR: CheckBalance_PostDegrade: sm = null");
Type = LogEvent.eEventType.Error,
Topic = "Email notification error",
Details = ex.Message,
});
Console.WriteLine($"Cannot send Insufficient amount #2 SM notification: {ex.Message}");
}
} else
{
m.StartLogEvent($"Deactivation (insufficient payment)", LogEvent.eEventType.Deactivation, "automatic");
@ -420,6 +409,7 @@ namespace dezentrale.core
break;
}
//If there are data changes to the member, there's a log and we need to store everything
if (m.CurrentLog != null)
{
m.LastCronjobBalanceMail = DateTime.Now;

124
core/CsvImportProcess.cs Normal file
View File

@ -0,0 +1,124 @@
using System;
using System.IO;
using dezentrale.model;
using System.Collections.Generic;
using dezentrale.model.money;
namespace dezentrale.core
{
public class CsvImportProcess : BackgroundProcess
{
private string fileName = "";
public CsvImportProcess(string fileName)
{
this.fileName = fileName;
Caption = "Import CSV Money Transfers";
Steps = 3;
}
protected override bool Run()
{
uint step = 0;
LogTarget.LogLine($"Importing from {fileName}", LogEvent.ELogLevel.Info, "CsvImportProcess");
List<BankTransfer> tmpList = new List<BankTransfer>();
List<string> headlineFields = null;
List<Member> changedMembers = new List<Member>();
CsvFile csv = new CsvFile() { FieldSeparator = ';' };
try
{
LogTarget.StepStarted(++step, "Reading file...");
try
{
csv.ReadFile(fileName);
LogTarget.StepCompleted(step, "Done reading file.", true);
} catch(Exception ex)
{
LogTarget.LogLine($"Error: {ex.Message}", LogEvent.ELogLevel.Error, "CsvImportProcess");
return false;
}
List<List<string>> contents = csv.FileContents;
LogTarget.StepStarted(++step, $"Parsing file ({contents.Count} csv lines, including headline)...");
for(int csvLine = 0; csvLine < contents.Count; csvLine++)
{
List<string> l = contents[csvLine];
if (headlineFields == null)
{
//The first line is expected to have the headline field first, describing the contents
headlineFields = l;
string fields = "";
foreach(string s in l)
{
fields += $"\"{s}\",";
}
LogTarget.LogLine($"Headline fields: {fields}", LogEvent.ELogLevel.Info, "CsvImportProcess");
continue;
}
BankTransfer bt = new BankTransfer(headlineFields, l, fileName, csvLine);
MoneyTransfer duplicate = Program.MoneyTransfers.FindEqualEntry(bt);
if (duplicate != null)
{
LogTarget.LogLine($"Line {csvLine}: is duplicate of MoneyTransfer {duplicate.Id} (ValutaDate {duplicate.ValutaDateString})", LogEvent.ELogLevel.Info, "CsvImportProcess");
LogTarget.LogLine($" ValutaDate: {bt.ValutaDate}, Amount = {bt.AmountString} {bt.Currency}, Reason = \"{bt.TransferReason.Replace('\r', '\\').Replace('\n', '\\')}\"", LogEvent.ELogLevel.Info, "CsvImportProcess");
}
else
{
Program.MoneyTransfers.AddEntry(bt);
tmpList.Add(bt);
}
}
LogTarget.StepCompleted(step, $"Done parsing entries.", true);
LogTarget.StepStarted(++step, $"Assigning found {tmpList.Count} entries to members");
//try to assign transfers to the members
foreach (BankTransfer bt in tmpList)
{
if (bt.Amount < 0)
{
bt.TransferType = MoneyTransfer.eTransferType.RunningCost;
LogTarget.LogLine($"{bt.Id}: (from CSV line {bt.CsvLine}): Amount = {bt.AmountString} --> RunningCost", LogEvent.ELogLevel.Info, "CsvImportProcess");
continue;
}
foreach (Member m in Program.members.Entries)
{
if (m.CheckBankTransfer(bt))
{
LogTarget.LogLine($"{bt.Id}: (from CSV line {bt.CsvLine}): adding to member {m}", LogEvent.ELogLevel.Info, "CsvImportProcess");
bt.TransferType = MoneyTransfer.eTransferType.MembershipPayment;
changedMembers.Add(m);
m.StartLogEvent("Incoming bank transfer", LogEvent.eEventType.MembershipPayment, "automatic");
m.ApplyMoneyTransfer(bt); //this also stores the member entry
bt.AssignFixed = true;
break; //this is important. We don't want to assign this to multiple members.
}
}
if(!bt.AssignFixed)
{
//bt matches no member!
}
}
LogTarget.StepCompleted(step, $"Done assigning entries.", true);
LogTarget.StepStarted(++step, $"Storing money transfers");
//Store bank transfer list
Program.MoneyTransfers.Entries.Sort();
if (!Program.MoneyTransfers.SaveToFile())
return false;
LogTarget.StepCompleted(step, "", true);
return true;
}
catch (Exception ex)
{
LogTarget.LogLine($"Error while processing csv file \"{fileName}\": {ex.Message}", LogEvent.ELogLevel.Error, "CsvImportProcess");
return false;
}
}
}
}

View File

@ -192,7 +192,7 @@ namespace dezentrale.core
LogTarget.LogLine($"Cannot send payment receipts E-Mail: {ex.Message}", LogEvent.ELogLevel.Error);
m.CurrentLog.SubEvents.Add(new LogSubEvent() { Type = LogEvent.eEventType.Error, Topic = "Email notification error", Details = ex.Message, });
}
m.SaveToFile(true);
m.SaveToFile(null, true);
}
} else
{

View File

@ -154,7 +154,7 @@
</Compile>
<Compile Include="model\ConfigSmtp.cs" />
<Compile Include="model\Configuration.cs" />
<Compile Include="model\ConfigVSMail.cs" />
<Compile Include="model\ConfigEMail.cs" />
<Compile Include="model\FormMail.cs" />
<Compile Include="model\Member.cs" />
<Compile Include="model\LogEvent.cs" />
@ -214,6 +214,8 @@
<Compile Include="view\LvSelectFields.cs" />
<Compile Include="core\NewExportProcess.cs" />
<Compile Include="core\NewImportProcess.cs" />
<Compile Include="view\frmCommentChanges.cs" />
<Compile Include="core\CsvImportProcess.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />

19
model/ConfigEMail.cs Normal file
View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
namespace dezentrale.model
{
public class ConfigEMail
{
[XmlAttribute] public string EMailName { get; set; } = "EMailName";
[XmlAttribute] public string EMail { get; set; } = "user@example.com";
public override string ToString()
{
return $"{EMailName} <{EMail}>";
}
}
}

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
namespace dezentrale.model
{
public class ConfigVSMail
{
[XmlElement] public string VSName { get; set; } = "dezentrale Vorstand";
[XmlElement] public string VSEmail { get; set; } = "vorstand@dezentrale.space";
}
}

View File

@ -46,8 +46,9 @@ namespace dezentrale.model
//db-wide configuration (in DB folder)
[XmlElement("VS")] public ConfigVSMail VS { get; set; } = new ConfigVSMail();
[XmlElement] public ConfigVSMail Schatzmeister { get; set; } = new ConfigVSMail();
[XmlElement] public ConfigEMail VS { get; set; } = new ConfigEMail() { EMailName = "dezentrale Vorstand", EMail = "vorstand@dezentrale.space" };
[XmlElement] public ConfigEMail Schatzmeister { get; set; } = new ConfigEMail() { EMailName = "Schatzmeister", EMail = "kasse@dezentrale.space" };
[XmlElement] public ConfigEMail Schriftfuehrer { get; set; } = new ConfigEMail() { EMailName = "Schriftfuehrer", EMail = "vorstand@dezentrale.space" };
[XmlElement] public uint RegularPaymentAmount { get; set; } = 3200; //cents
[XmlElement] public string RegularPaymentCurrency { get; set; } = "EUR";

View File

@ -52,7 +52,7 @@ namespace dezentrale.model
{
string[] addr = Program.config.Smtp.CcTo.Split(',', ';');
foreach(string s in addr)
message.CC.Add(new MailAddress(s));
if(!string.IsNullOrEmpty(s)) message.CC.Add(new MailAddress(s));
}
message.SubjectEncoding = Encoding.UTF8;
@ -407,6 +407,24 @@ namespace dezentrale.model
+ "Dies ist eine automatisch generierte E-Mail.\n",
};
}
public static FormMail GenerateDisabledMemberGettingPayments2SM()
{
return new FormMail()
{
To = "schatzmeister <sm@example.com>", //to be replaced!
Subject = "Inaktives Mitglied hat Mitgliedsbeitrag bezahlt",
Body = "Hallo Schatzmeister,\n"
+ "\n"
+ "Für folgenden deaktivierten Mitgliedsaccount wurde Mitgliedsbeitrag bezahlt:\n"
+ "Mitgliednummer: {NumberString}\n"
+ "Nickname: {Nickname} <{EMail}>\n"
+ "Monatsbeitrag: {PaymentAmountString} {PaymentAmountCurrency}\n"
+ "Kontostand: {AccountBalanceString} {PaymentAmountCurrency}\n"
+ "\n"
+ "--\n"
+ "Dies ist eine automatisch generierte E-Mail.\n",
};
}
public static FormMail GenerateBalanceAbove200NotifySM()
{
//Konto > 200 EUR

View File

@ -23,7 +23,7 @@ namespace dezentrale.model
MembershipFee,
MembershipPayment,
MembershipDonation,
Donation,
EMail,

View File

@ -71,6 +71,7 @@ namespace dezentrale.model
private bool paymentNotify = false;
private DateTime memberFormDate;
private DateTime reducedFeeValid;
private int debtLevel = 0;
//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.
@ -112,11 +113,11 @@ namespace dezentrale.model
[XmlElement] public DateTime MemberFormDate { get { return memberFormDate; } set { LogPropertyChange("MemberFormDate", memberFormDate, value); memberFormDate = value; } }
//internal management data
[XmlElement] public int DebtLevel { get { return debtLevel; } set { LogPropertyChange("DebtLevel", debtLevel, value); debtLevel = value; } }
[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] public int DebtLevel { get; set; } = 0;
[XmlElement] public DateTime LastCronjobBalanceMail { get; set; } = DateTime.Now;
[XmlElement] public DateTime LastCronjobReducedFeeMail { get; set; } = DateTime.Now;
[XmlElement] public DateTime MvEventDate { get; set; }
@ -160,9 +161,9 @@ namespace dezentrale.model
else return number.CompareTo(other.Number);
}
public bool SaveToFile(bool finishLog = true)
public bool SaveToFile(string comment = null, bool finishLog = true)
{
if (finishLog) FinishLogEvent();
if (finishLog) FinishLogEvent(comment);
string completePath = System.IO.Path.Combine(Program.config.DbDirectory, GetFileName());
Program.config.DbChangedSinceExport = true;
Program.config.LastDbLocalChange = DateTime.Now;
@ -174,23 +175,23 @@ namespace dezentrale.model
public bool CheckBankTransfer(BankTransfer t)
{
if (!evaluateAccountInCharge) return false;
if (!bankAccountInCharge.Equals(t.IBAN))
//on banktransfers, AccountInCharge is the receiver IBAN, we need the sender IBAN here
return false;
if (string.IsNullOrEmpty(BankTransferRegEx))
return true;
if (!evaluateAccountInCharge) return false;
try
{
return Regex.IsMatch(t.TransferReason, BankTransferRegEx);
{
if (!string.IsNullOrEmpty(BankTransferRegEx))
{
if (Regex.IsMatch(t.TransferReason, BankTransferRegEx))
return true;
}
}
catch (Exception ex)
{
Console.WriteLine($"Member {this.Number:D3} invalid RegEx: {ex.Message}");
return false;
}
return bankAccountInCharge.Equals(t.IBAN);
//on banktransfers, AccountInCharge is the receiver IBAN, we need the sender IBAN here
}
private void PaymentAdjustBalance(Int64 amount, string user = null)
@ -243,8 +244,8 @@ namespace dezentrale.model
LogEvent.eEventType evt = LogEvent.eEventType.Generic;
switch (t.TransferType)
{
case MoneyTransfer.eTransferType.MembershipDonation:
evt = LogEvent.eEventType.MembershipDonation; break;
case MoneyTransfer.eTransferType.Donation:
evt = LogEvent.eEventType.Donation; break;
case MoneyTransfer.eTransferType.MembershipFee:
evt = LogEvent.eEventType.MembershipFee; break;
case MoneyTransfer.eTransferType.MembershipPayment:
@ -264,38 +265,47 @@ namespace dezentrale.model
PaymentAdjustBalance(t.Amount);
LastPaymentProcessed = DateTime.Now;
//find out if we need to send a mail to Schatzmeister (i.e. amount is odd in respect to the monthly fee)
Int64 months = t.Amount / (Int64)PaymentAmount;
bool odd = months * (Int64)PaymentAmount != t.Amount;
if (odd)
bool odd = months * (Int64)PaymentAmount != t.Amount;
ConfigEMail smContact = Program.config.Schatzmeister;
FormMail fm = null;
if (this.Status == eStatus.Deleted || this.Status == eStatus.Disabled)
{
Member sm = Program.members.Find(eRole.Schatzmeister);
if (sm == null)
Console.WriteLine("Member.ApplyMoneyTransfer(): Error - Schatzmeister account not found!");
else
fm = FormMail.GenerateDisabledMemberGettingPayments2SM();
}
else if (odd)
{
fm = new FormMail()
{
To = $"{smContact}",
Subject = $"Schiefe Zahlung von Mitglied {Number} ({Nickname}, {t.AmountString} {t.Currency})",
Body = "s. Betreff.\n"
+ $"Type = {t.GetType()}\n"
+ $"TransferAmount = {t.AmountString}\n"
+ $"PaymentAmount = {PaymentAmountString } (monthly fee)\n"
+ $"accountBalance = {AccountBalanceString} (new)\n"
+ $"Next payment for this member is due at {PaymentDueMonth}\n"
+ "\n\n--\n(automatische mail)"
};
}
if (fm != null)
{
Console.WriteLine($"Member.ApplyMoneyTransfer(): sm={smContact}");
try
{
Console.WriteLine($"Member.ApplyMoneyTransfer(): sm={sm.Nickname}");
FormMail fm = new FormMail()
{
To = $"{sm.EMailName} <{sm.EMail}>",
Subject = $"Schiefe Zahlung von Mitglied {Number} ({Nickname}, {t.AmountString} {t.Currency})",
Body = "s. Betreff.\n"
+ $"Type = {t.GetType()}\n"
+ $"TransferAmount = {t.AmountString}\n"
+ $"PaymentAmount = {PaymentAmountString } (monthly fee)\n"
+ $"accountBalance = {AccountBalanceString} (new)\n"
+ $"Next payment for this member is due at {PaymentDueMonth}\n"
+ "\n\n--\n(automatische mail)"
};
try
{
CurrentLog.SubEvents.Add(fm.Send());
} catch(Exception ex)
{
CurrentLog.SubEvents.Add(new LogSubEvent() { Type = LogEvent.eEventType.Error, Topic = "Email notification error", Details = ex.Message, });
}
CurrentLog.SubEvents.Add(fm.Send());
}
catch (Exception ex)
{
CurrentLog.SubEvents.Add(new LogSubEvent() { Type = LogEvent.eEventType.Error, Topic = "Email notification error", Details = ex.Message, });
}
}
if (paymentNotify)
{
FormMail notify = FormMail.GenerateMemberPaymentNotify(odd).ReplaceReflect(t);
@ -310,7 +320,7 @@ namespace dezentrale.model
}
}
try { SaveToFile(false); }
try { SaveToFile(null, false); }
catch (Exception ex) { Console.WriteLine($"Error while saving member: {ex.Message}"); }
}
}

View File

@ -8,6 +8,7 @@ namespace dezentrale.model
{
public class MemberImportExport
{
[XmlAttribute] public bool Enabled { get; set; } = false;
[XmlElement] public string ZipFile { get; set; } = "fnord.zip";
[XmlElement] public string ZipPassword { get; set; } = "";
[XmlElement] public bool GpgEnabled { get; set; } = true;

View File

@ -6,7 +6,7 @@ namespace dezentrale.model
{
public MemberReport(bool memberList = false)
{
To = "{Nickname} <{EMail}>";
To = "{EMailName} <{EMail}>";
Subject = $"Automatic member statistics {DateTime.Now.ToString("yyyy-MM-dd")}";
int unGreetedMembers = 0;

View File

@ -45,8 +45,12 @@ namespace dezentrale.model
});
}
}
public void FinishLogEvent()
public void FinishLogEvent(string comment = null)
{
if(CurrentLog != null && !string.IsNullOrEmpty(comment))
{
CurrentLog.Details = string.IsNullOrEmpty(CurrentLog.Details) ? comment : CurrentLog.Details + $"\n{comment}";
}
CurrentLog = null;
}
}

View File

@ -22,11 +22,20 @@ namespace dezentrale.model.money
[XmlElement] public string RecipientOrDebitor { get; set; } = "";
[XmlElement] public string IBAN { get; set; } = "";
[XmlElement] public string BIC { get; set; } = "";
[XmlElement] public string Info { get; set; } = "";
[XmlElement] public string Info { get; set; } = "";
//During the CSV import process, generated BankTransfer objects will hold CsvFile/Line for later
//logging into member data / Main log (i.e. it is possible to track a MoneyTransfer from a
//Member object to a line from a specific csv file)
[XmlIgnore] public string CsvFile { get; set; } = null;
[XmlIgnore] public int CsvLine { get; set; } = 0;
public BankTransfer() : base() { }
public BankTransfer(List<string> csvHeadline, List<string> csvEntry) : base()
{
public BankTransfer(List<string> csvHeadline, List<string> csvEntry, string csvFile = "", int csvLineNumber = 0) : base()
{
CsvFile = csvFile;
CsvLine = csvLineNumber;
int lc = csvEntry.Count;
if (lc > csvHeadline.Count) lc = csvHeadline.Count;
for (int i = 0; i < lc; i++)
@ -58,7 +67,7 @@ namespace dezentrale.model.money
case "Waehrung": Currency = csvEntry[i]; break;
case "Info": Info = csvEntry[i]; break;
default:
throw new Exception($"invalid csv headline field: \"{csvHeadline[i]}\"");
throw new Exception($"invalid csv headline field: \"{csvHeadline[i]}\" csvFile {csvFile} line {csvLineNumber}");
}
}
}

View File

@ -13,7 +13,7 @@ namespace dezentrale.model.money
{
MembershipFee = LogEvent.eEventType.MembershipFee,
MembershipPayment = LogEvent.eEventType.MembershipPayment,
MembershipDonation = LogEvent.eEventType.MembershipDonation,
Donation = LogEvent.eEventType.Donation,
Unassigned,
RunningCost,
Ignored,
@ -42,7 +42,8 @@ namespace dezentrale.model.money
public bool Equals(MoneyTransfer other) { return Equals(other, true); }
public bool Equals(MoneyTransfer other, bool evaluateReason)
{
{
#warning This doesn't respect the account ID and may lead to false positives.
return this.ValutaDate.Equals(other.ValutaDate)
&& (this.Amount == other.Amount)
&& this.GetType().Equals(other.GetType())

View File

@ -157,7 +157,7 @@ namespace dezentrale.view
void CustomListView_MouseUp(object sender, MouseEventArgs e)
{
Console.WriteLine($"MouseUp(): MouseButtons={Control.MouseButtons}");
//Console.WriteLine($"MouseUp(): MouseButtons={Control.MouseButtons}");
if (resizeWhileMouseDown && (resizedWhileMouseDown != null))
{
//mono: Store new width.

41
view/frmCommentChanges.cs Normal file
View File

@ -0,0 +1,41 @@
using System;
//using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace dezentrale.view
{
public class frmCommentChanges : FormWithActionButtons
{
public string Comment { get; private set; } = "";
private TextBox tbComment;
public frmCommentChanges(string captionEntity, string changes)
{
this.StartPosition = FormStartPosition.CenterParent;
this.Size = new System.Drawing.Size(800, 600);
this.Text = $"Comment changes to \"{captionEntity}\"";
int w = this.ClientSize.Width;
int h = this.ClientSize.Height - 35;
this.Controls.Add(tbComment = new TextBox()
{
Location = new Point(groupOffset, 0 * line + tm + groupOffset),
Size = new Size(w - 2 * groupOffset, h - 12 - tm),
Multiline = true,
ScrollBars = ScrollBars.Both,
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
});
AddButton("Ok", btnOk_Click);
}
private void btnOk_Click(object sender, EventArgs e)
{
Comment = tbComment.Text;
this.Close();
}
}
}

View File

@ -33,8 +33,13 @@ namespace dezentrale.view
//Vorstand settings
private TextBox tbVSName;
private TextBox tbVSEmail;
private TextBox tbSMName;
private TextBox tbSMEmail;
private TextBox tbSFName;
private TextBox tbSFEmail;
//Import / Export settings
private CheckBox cbIeEnabled;
private TextBox tbIeZipFile;
private TextBox tbIeZipPassword;
private CheckBox cbIeGpgEnabled;
@ -237,7 +242,7 @@ namespace dezentrale.view
gui.Controls.Add(new Label()
{
Text = "Name:",
Text = "Vorstand:",
Location = new Point(lm, 9 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
@ -250,7 +255,7 @@ namespace dezentrale.view
});
gui.Controls.Add(new Label()
{
Text = "E-Mail:",
Text = "VS-Mail:",
Location = new Point(lm, 10 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
@ -261,6 +266,60 @@ namespace dezentrale.view
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
gui.Controls.Add(new Label()
{
Text = "Schatzmeister:",
Location = new Point(lm, 11 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
gui.Controls.Add(tbSMName = new TextBox()
{
Location = new Point(lm + 113, 11 * line + tm),
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
gui.Controls.Add(new Label()
{
Text = "SM-Mail:",
Location = new Point(lm, 12 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
gui.Controls.Add(tbSMEmail = new TextBox()
{
Location = new Point(lm + 113, 12 * line + tm),
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
gui.Controls.Add(new Label()
{
Text = "Schriftfuehrer:",
Location = new Point(lm, 13 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
gui.Controls.Add(tbSFName = new TextBox()
{
Location = new Point(lm + 113, 13 * line + tm),
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
gui.Controls.Add(new Label()
{
Text = "SF-Mail:",
Location = new Point(lm, 14 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
gui.Controls.Add(tbSFEmail = new TextBox()
{
Location = new Point(lm + 113, 14 * line + tm),
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
cbSmtpEnabled.Checked = Program.config.Smtp.Enabled;
tbSmtpHost.Text = Program.config.Smtp.Host;
@ -271,8 +330,12 @@ namespace dezentrale.view
tbSmtpPassword.Text = Program.config.Smtp.Password;
tbSmtpCcTo.Text = Program.config.Smtp.CcTo;
tbVSName.Text = Program.config.VS.VSName;
tbVSEmail.Text = Program.config.VS.VSEmail;
tbVSName.Text = Program.config.VS.EMailName;
tbVSEmail.Text = Program.config.VS.EMail;
tbSMName.Text = Program.config.Schatzmeister.EMailName;
tbSMEmail.Text = Program.config.Schatzmeister.EMail;
tbSFName.Text = Program.config.Schriftfuehrer.EMailName;
tbSFEmail.Text = Program.config.Schriftfuehrer.EMail;
return gui;
}
public TabPage BuildMoneyTransfersGui()
@ -286,16 +349,24 @@ namespace dezentrale.view
{
TabPage gui = new TabPage("Import / Export");
gui.Controls.Add(cbIeEnabled = new CheckBox()
{
Text = "Enable Import/Export with the settings below",
Location = new Point(lm + 113, 0 * line + tm),
Width = 400,
});
gui.Controls.Add(new Label()
{
Text = "Zip filename:",
Location = new Point(lm, 0 * line + tm + labelOffs),
Location = new Point(lm, 1 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
gui.Controls.Add(tbIeZipFile = new TextBox()
{
Location = new Point(lm + 113, 0 * line + tm),
Location = new Point(lm + 113, 1 * line + tm),
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
@ -303,14 +374,14 @@ namespace dezentrale.view
gui.Controls.Add(new Label()
{
Text = "Zip password:",
Location = new Point(lm, 1 * line + tm + labelOffs),
Location = new Point(lm, 2 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
gui.Controls.Add(tbIeZipPassword = new TextBox()
{
PasswordChar = '*',
Location = new Point(lm + 113, 1 * line + tm),
Location = new Point(lm + 113, 2 * line + tm),
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
@ -318,20 +389,20 @@ namespace dezentrale.view
gui.Controls.Add(cbIeGpgEnabled = new CheckBox()
{
Text = "Enable file encryption with GPG (AES + password)",
Location = new Point(lm + 113, 2 * line + tm),
Location = new Point(lm + 113, 3 * line + tm),
Width = 400,
});
gui.Controls.Add(new Label()
{
Text = "Gpg filename:",
Location = new Point(lm, 3 * line + tm + labelOffs),
Location = new Point(lm, 4 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
gui.Controls.Add(tbIeGpgFile = new TextBox()
{
Location = new Point(lm + 113, 3 * line + tm),
Location = new Point(lm + 113, 4 * line + tm),
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
@ -339,48 +410,33 @@ namespace dezentrale.view
gui.Controls.Add(new Label()
{
Text = "Gpg password:",
Location = new Point(lm, 4 * line + tm + labelOffs),
Location = new Point(lm, 5 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
gui.Controls.Add(tbIeGpgPassword = new TextBox()
{
PasswordChar = '*',
Location = new Point(lm + 113, 4 * line + tm),
Location = new Point(lm + 113, 5 * line + tm),
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
gui.Controls.Add(cbIeHgEnabled = new CheckBox()
{
Text = "Enable data synchronisation over mercurial",
Location = new Point(lm + 113, 5 * line + tm),
Location = new Point(lm + 113, 6 * line + tm),
Width = 400,
});
gui.Controls.Add(new Label()
{
Text = "HG UserName:",
Location = new Point(lm, 6 * line + tm + labelOffs),
Location = new Point(lm, 7 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
gui.Controls.Add(tbIeHgUserName = new TextBox()
{
Location = new Point(lm + 113, 6 * line + tm),
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
gui.Controls.Add(new Label()
{
Text = "HG password:",
Location = new Point(lm, 7 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
gui.Controls.Add(tbIeHgPassword = new TextBox()
{
PasswordChar = '*',
Location = new Point(lm + 113, 7 * line + tm),
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
@ -388,18 +444,34 @@ namespace dezentrale.view
gui.Controls.Add(new Label()
{
Text = "HG URL:",
Text = "HG password:",
Location = new Point(lm, 8 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
gui.Controls.Add(tbIeHgPassword = new TextBox()
{
PasswordChar = '*',
Location = new Point(lm + 113, 8 * line + tm),
Width = 200,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
gui.Controls.Add(new Label()
{
Text = "HG URL:",
Location = new Point(lm, 9 * line + tm + labelOffs),
Size = new Size(110, labelHeight),
TextAlign = ContentAlignment.BottomRight,
});
gui.Controls.Add(tbIeHgURL = new TextBox()
{
Location = new Point(lm + 113, 8 * line + tm),
Location = new Point(lm + 113, 9 * line + tm),
Width = 400,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
});
cbIeEnabled.Checked = Program.config.ImportExport.Enabled;
tbIeZipFile.Text = Program.config.ImportExport.ZipFile;
tbIeZipPassword.Text = Program.config.ImportExport.ZipPassword;
cbIeGpgEnabled.Checked = Program.config.ImportExport.GpgEnabled;
@ -472,12 +544,17 @@ namespace dezentrale.view
Program.config.Smtp.Password = tbSmtpPassword.Text;
Program.config.Smtp.CcTo = tbSmtpCcTo.Text;
Program.config.VS.VSName = tbVSName.Text;
Program.config.VS.VSEmail = tbVSEmail.Text;
Program.config.VS.EMailName = tbVSName.Text;
Program.config.VS.EMail = tbVSEmail.Text;
Program.config.Schatzmeister.EMailName = tbSMName.Text;
Program.config.Schatzmeister.EMail = tbSMEmail.Text;
Program.config.Schriftfuehrer.EMailName = tbSFName.Text;
Program.config.Schriftfuehrer.EMail = tbSFEmail.Text;
//use MoneyTransferList for that, not Program.config
//[XmlElement] public List<KeyValue> MoneyTransferRegEx { get; set; } = new List<KeyValue>();
//use MoneyTransferList for that, not Program.config
//[XmlElement] public List<KeyValue> MoneyTransferRegEx { get; set; } = new List<KeyValue>();
Program.config.ImportExport.Enabled = cbIeEnabled.Checked;
Program.config.ImportExport.ZipFile = tbIeZipFile.Text;
Program.config.ImportExport.ZipPassword = tbIeZipPassword.Text;
Program.config.ImportExport.GpgEnabled = cbIeGpgEnabled.Checked;

View File

@ -388,7 +388,7 @@ namespace dezentrale.view
Width = 100,
Anchor = AnchorStyles.Top | AnchorStyles.Left,
TextAlign = HorizontalAlignment.Right,
ReadOnly = true,
//ReadOnly = true,
});
parent.Controls.Add(new Label()
{
@ -695,9 +695,17 @@ namespace dezentrale.view
member.BankTransferRegEx = tbBankTransferRegEx.Text;
member.Remarks = tbRemarks.Text;
member.FinishLogEvent();
//member.FinishLogEvent(); //implicit in SaveToFile()
this.DialogResult = DialogResult.OK;
member.SaveToFile();
string comment = null;
if (!newMember)
{
frmCommentChanges commentChanges = new frmCommentChanges($"{member.Number:D3} ({ member.Nickname})", "");
commentChanges.ShowDialog();
comment = commentChanges.Comment;
}
member.SaveToFile(comment);
this.Close();
}

View File

@ -15,6 +15,7 @@ namespace dezentrale.view
private LVMoneyTransfers mtv;
private LvMv lstMv;
private MenuItem mnuFileExport, mnuFileImport;
private void BuildMoneyTransfers(Control parent)
{
parent.Controls.Add(mtv = new LVMoneyTransfers() { Dock = DockStyle.Fill, });
@ -31,7 +32,7 @@ namespace dezentrale.view
this.Size = new Size(640, 480);
this.FormClosing += (sender, e) =>
{
if(!enforceClosing && Program.config.DbChangedSinceExport)
if(!enforceClosing && Program.config.DbChangedSinceExport && Program.config.ImportExport.Enabled)
{
DialogResult dr =
MessageBox.Show("Database changed since last export.\r\n\r\n"
@ -51,8 +52,8 @@ namespace dezentrale.view
{ MenuItems = {
new MenuItem("&Configuration...", mnuMain_File_Configuration) { Enabled = true },
new MenuItem("-"),
new MenuItem("&Export database", mnuMain_File_Export),
new MenuItem("&Import database", mnuMain_File_Import),
(mnuFileExport = new MenuItem("&Export database", mnuMain_File_Export)),
(mnuFileImport = new MenuItem("&Import database", mnuMain_File_Import)),
new MenuItem("-"),
new MenuItem("&Quit", mnuMain_File_Quit),
} },
@ -88,6 +89,8 @@ namespace dezentrale.view
}
};
mnuFileImport.Enabled = mnuFileExport.Enabled = Program.config.ImportExport.Enabled;
TabPage tabMembers = new TabPage("Members");
tabMembers.Controls.Add(lstMembers = new LVMembers() { Dock = DockStyle.Fill });
@ -129,32 +132,35 @@ namespace dezentrale.view
lstMv.LoadFromList(Program.mvList.Entries);
//Check for needed import
DateTime now = DateTime.Now;
int totalHours = (int) now.Subtract(Program.config.LastDbImport).TotalHours;
int totalDays = totalHours / 24;
if (totalHours >= 8)
if (Program.config.ImportExport.Enabled)
{
totalHours %= 24;
string timeSpan = $"{totalDays} d, {totalHours} h";
if (Program.config.DbChangedSinceExport)
DateTime now = DateTime.Now;
int totalHours = (int) now.Subtract(Program.config.LastDbImport).TotalHours;
int totalDays = totalHours / 24;
if (totalHours >= 8)
{
MessageBox.Show( "Warning: There are local changes to DB since last export!\r\n"
+ "Please check if there are changes on the server side and then\r\n"
+ "- perform a manual backup-import-merge\r\n"
+ "- or simply an export if there are no changes.\r\n\r\n"
+ $"Last import was: {Program.config.LastDbImport}\r\n"
+ $"Last export was: {Program.config.LastDbExport}\r\n"
+ $"Last db change was {Program.config.LastDbLocalChange}\r\n");
}
else
{
if (totalDays > 365) timeSpan = "too long";
DialogResult dr = MessageBox.Show($"Last Db import was {timeSpan} ago.\r\nImport now?", "Import database", MessageBoxButtons.YesNo);
if (dr == DialogResult.Yes)
totalHours %= 24;
string timeSpan = $"{totalDays} d, {totalHours} h";
if (Program.config.DbChangedSinceExport)
{
mnuMain_File_Import(null, null);
MessageBox.Show( "Warning: There are local changes to DB since last export!\r\n"
+ "Please check if there are changes on the server side and then\r\n"
+ "- perform a manual backup-import-merge\r\n"
+ "- or simply an export if there are no changes.\r\n\r\n"
+ $"Last import was: {Program.config.LastDbImport}\r\n"
+ $"Last export was: {Program.config.LastDbExport}\r\n"
+ $"Last db change was {Program.config.LastDbLocalChange}\r\n");
}
else
{
if (totalDays > 365) timeSpan = "too long";
DialogResult dr = MessageBox.Show($"Last Db import was {timeSpan} ago.\r\nImport now?", "Import database", MessageBoxButtons.YesNo);
if (dr == DialogResult.Yes)
{
mnuMain_File_Import(null, null);
}
}
}
}
}
@ -165,8 +171,11 @@ namespace dezentrale.view
frmConfig.ShowDialog();
if (frmConfig.DialogResult == DialogResult.OK)
{
frmConfig.FillAndSaveConfig();
if(frmConfig.KeylockCombiChanged && Program.config.Smtp.Enabled)
frmConfig.FillAndSaveConfig();
mnuFileImport.Enabled = mnuFileExport.Enabled = Program.config.ImportExport.Enabled;
if (frmConfig.KeylockCombiChanged && Program.config.Smtp.Enabled)
{
DialogResult dr = MessageBox.Show("You've changed the keylock combination.\n Do you want to send an E-Mail to every active member to inform them?", "Keylock-Combi changed", MessageBoxButtons.YesNo);
if (dr == DialogResult.Yes)
@ -454,7 +463,19 @@ namespace dezentrale.view
DialogResult dr = ofd.ShowDialog();
//ofd.FilterIndex
if (dr == DialogResult.OK)
{
lstMembers.SuspendLayout();
ProcessCsv.ProcessCSV(ofd.FileName);
foreach (Member m in Program.members.Entries) lstMembers.UpdateEntry(m);
if (Program.MoneyTransfersLoaded)
{
//This will enforce reloading the moneytransfer list
Program.MoneyTransfersLoaded = false;
mtv.LoadFromList(Program.MoneyTransfers.Entries);
}
lstMembers.ResumeLayout(false);
}
}
private void mnuMain_Payments_Receipts(object sender, EventArgs e)
{