From 4d16e11b6ebd62b5b936b197d7ed0842b3e6b2a4 Mon Sep 17 00:00:00 2001 From: phantomix Date: Mon, 24 Jan 2022 22:04:24 +0100 Subject: [PATCH] 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) --- Program.cs | 22 ++++- core/Cronjob.cs | 80 ++++++++---------- core/CsvImportProcess.cs | 124 ++++++++++++++++++++++++++++ core/PaymentReceiptProcess.cs | 2 +- dezentrale-members.csproj | 4 +- model/ConfigEMail.cs | 19 +++++ model/ConfigVSMail.cs | 14 ---- model/Configuration.cs | 5 +- model/FormMail.cs | 20 ++++- model/LogEvent.cs | 2 +- model/Member.cs | 94 +++++++++++---------- model/MemberImportExport.cs | 1 + model/MemberReport.cs | 2 +- model/XmlLog.cs | 6 +- model/money/BankTransfer.cs | 17 +++- model/money/MoneyTransfer.cs | 5 +- view/CustomListView.cs | 2 +- view/frmCommentChanges.cs | 41 ++++++++++ view/frmConfiguration.cs | 149 ++++++++++++++++++++++++++-------- view/frmEditEntry.cs | 14 +++- view/frmMain.cs | 75 +++++++++++------ 21 files changed, 512 insertions(+), 186 deletions(-) create mode 100644 core/CsvImportProcess.cs create mode 100644 model/ConfigEMail.cs delete mode 100644 model/ConfigVSMail.cs create mode 100644 view/frmCommentChanges.cs diff --git a/Program.cs b/Program.cs index c300360..28e7db2 100644 --- a/Program.cs +++ b/Program.cs @@ -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, diff --git a/core/Cronjob.cs b/core/Cronjob.cs index 4364f8c..e222631 100644 --- a/core/Cronjob.cs +++ b/core/Cronjob.cs @@ -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 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; diff --git a/core/CsvImportProcess.cs b/core/CsvImportProcess.cs new file mode 100644 index 0000000..666be2c --- /dev/null +++ b/core/CsvImportProcess.cs @@ -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 tmpList = new List(); + List headlineFields = null; + List changedMembers = new List(); + + 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> contents = csv.FileContents; + LogTarget.StepStarted(++step, $"Parsing file ({contents.Count} csv lines, including headline)..."); + for(int csvLine = 0; csvLine < contents.Count; csvLine++) + { + List 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; + } + } + } +} \ No newline at end of file diff --git a/core/PaymentReceiptProcess.cs b/core/PaymentReceiptProcess.cs index bcbf53a..cb9e7d2 100644 --- a/core/PaymentReceiptProcess.cs +++ b/core/PaymentReceiptProcess.cs @@ -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 { diff --git a/dezentrale-members.csproj b/dezentrale-members.csproj index af02658..ef56b02 100644 --- a/dezentrale-members.csproj +++ b/dezentrale-members.csproj @@ -154,7 +154,7 @@ - + @@ -214,6 +214,8 @@ + + diff --git a/model/ConfigEMail.cs b/model/ConfigEMail.cs new file mode 100644 index 0000000..b5433ef --- /dev/null +++ b/model/ConfigEMail.cs @@ -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}>"; + } + } +} diff --git a/model/ConfigVSMail.cs b/model/ConfigVSMail.cs deleted file mode 100644 index 19bb6ef..0000000 --- a/model/ConfigVSMail.cs +++ /dev/null @@ -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"; - } -} diff --git a/model/Configuration.cs b/model/Configuration.cs index 2646b83..6796339 100644 --- a/model/Configuration.cs +++ b/model/Configuration.cs @@ -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"; diff --git a/model/FormMail.cs b/model/FormMail.cs index 2036b2a..3762a89 100644 --- a/model/FormMail.cs +++ b/model/FormMail.cs @@ -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 ", //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 diff --git a/model/LogEvent.cs b/model/LogEvent.cs index 7b66ce9..88f91ae 100644 --- a/model/LogEvent.cs +++ b/model/LogEvent.cs @@ -23,7 +23,7 @@ namespace dezentrale.model MembershipFee, MembershipPayment, - MembershipDonation, + Donation, EMail, diff --git a/model/Member.cs b/model/Member.cs index 3985adc..4e90676 100644 --- a/model/Member.cs +++ b/model/Member.cs @@ -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}"); } } } diff --git a/model/MemberImportExport.cs b/model/MemberImportExport.cs index e2b5ba9..ace0b3f 100644 --- a/model/MemberImportExport.cs +++ b/model/MemberImportExport.cs @@ -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; diff --git a/model/MemberReport.cs b/model/MemberReport.cs index 06a0770..4028d73 100644 --- a/model/MemberReport.cs +++ b/model/MemberReport.cs @@ -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; diff --git a/model/XmlLog.cs b/model/XmlLog.cs index 995b445..50b2b11 100644 --- a/model/XmlLog.cs +++ b/model/XmlLog.cs @@ -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; } } diff --git a/model/money/BankTransfer.cs b/model/money/BankTransfer.cs index fe2f1d6..d59625f 100644 --- a/model/money/BankTransfer.cs +++ b/model/money/BankTransfer.cs @@ -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 csvHeadline, List csvEntry) : base() - { + public BankTransfer(List csvHeadline, List 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}"); } } } diff --git a/model/money/MoneyTransfer.cs b/model/money/MoneyTransfer.cs index fdb9704..d8dd142 100644 --- a/model/money/MoneyTransfer.cs +++ b/model/money/MoneyTransfer.cs @@ -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()) diff --git a/view/CustomListView.cs b/view/CustomListView.cs index b64bc1f..35cb533 100644 --- a/view/CustomListView.cs +++ b/view/CustomListView.cs @@ -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. diff --git a/view/frmCommentChanges.cs b/view/frmCommentChanges.cs new file mode 100644 index 0000000..6ee83e0 --- /dev/null +++ b/view/frmCommentChanges.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/view/frmConfiguration.cs b/view/frmConfiguration.cs index d8886ff..c816fe3 100644 --- a/view/frmConfiguration.cs +++ b/view/frmConfiguration.cs @@ -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 MoneyTransferRegEx { get; set; } = new List(); + //use MoneyTransferList for that, not Program.config + //[XmlElement] public List MoneyTransferRegEx { get; set; } = new List(); + Program.config.ImportExport.Enabled = cbIeEnabled.Checked; Program.config.ImportExport.ZipFile = tbIeZipFile.Text; Program.config.ImportExport.ZipPassword = tbIeZipPassword.Text; Program.config.ImportExport.GpgEnabled = cbIeGpgEnabled.Checked; diff --git a/view/frmEditEntry.cs b/view/frmEditEntry.cs index cfb0d60..69a1c7a 100644 --- a/view/frmEditEntry.cs +++ b/view/frmEditEntry.cs @@ -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(); } diff --git a/view/frmMain.cs b/view/frmMain.cs index a2fa377..e163d04 100644 --- a/view/frmMain.cs +++ b/view/frmMain.cs @@ -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) {