Compare commits

...

10 Commits

Author SHA1 Message Date
phantomix 86f6ad9739 2023-04-06 Added workaround draft for Mail+TLS1.0, still not working
Increased framework version to 4.8
2023-04-06 10:46:23 +02:00
phantomix 0cda515187 2022-03-29 Added more exception output to TestMail/StatusMail in order to track down posteo.de SSL errors 2022-03-29 23:43:03 +02:00
phantomix 538ba06221 2022-03-27 Fixed Windows \r\n TextBox issues by replacing \n->\r\n and back to \n when handling TextBox contents 2022-03-27 18:21:08 +02:00
phantomix 64ae7f6c4b 2022-02-18 Fixed CsvImportProcess wasn't used, Changed Member banktransferregex -> Contains 2022-02-18 22:34:28 +01:00
phantomix 4d16e11b6e 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)
2022-01-24 22:04:24 +01:00
phantomix 105afdb203 2022-01-19PX fixed mail sending didn't work with posteo, fixed multiple CC mail adresses didn't work 2022-01-19 22:14:16 +01:00
phantomix 747fc9008e 2022-01-07PX reordered program configuration in preparation to separate settings for local, db 2022-01-18 23:31:03 +01:00
phantomix d9d4d9c2be 211031PX Fixed CustomListView.cs: ItemSelection(T,selected) returned false when item was actually found
Added NewExportProcess.cs, NewImportProcess.cs in order to work towards changing import/export completely
2021-10-31 20:34:57 +01:00
phantomix a1c4512f2c 2021-07-18PX Added storage for CSV selected export fields, not fully working yet 2021-07-18 16:21:42 +02:00
phantomix 342511f03a 2021-07-18PX Added Member CSV export feature 2021-07-18 12:47:57 +02:00
28 changed files with 829 additions and 243 deletions

View File

@ -14,31 +14,16 @@ TODO
- Documentation
- 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: 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: Member list not reloaded after ProcessCSV (balance display is wrong)
- Bug: Import/Export gpg uses command line parameter for password - this can be read out by any system user via "ps uxa"
- 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
*/
namespace dezentrale
{
public class Program
{
public static uint VersionNumber { get; private set; } = 0x21052800;
public static uint VersionNumber { get; private set; } = 0x22032900;
public static string VersionString { get; private set; } = $"{VersionNumber:x}";
public static string AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
@ -49,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
{
@ -151,6 +140,29 @@ namespace dezentrale
Console.WriteLine($"dezentrale-members, Version {VersionString}");
Console.WriteLine($"Working directory: {DmDirectory}");
//See https://stackoverflow.com/questions/721161/how-to-detect-which-net-runtime-is-being-used-ms-vs-mono
bool isRunningOnMono = false;
try
{
isRunningOnMono = (Type.GetType("Mono.Runtime") != null);
} catch(Exception ex) { Console.WriteLine($"Mono detection failed. Assuming non-Mono. Error: {ex.Message}"); }
if(isRunningOnMono)
{
//Mono on OpenSSL (boringssl) will lead to Usage of TLS 1.0 instead of 1.2, even
//if this is explicitly set here!
Console.WriteLine("Mono detected. Setting MONO_TLS_PROVIDER to btls");
Environment.SetEnvironmentVariable("MONO_TLS_PROVIDER", "btls");
}
System.Net.ServicePointManager.Expect100Continue = true;
System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;
System.Net.ServicePointManager.ServerCertificateValidationCallback +=
(sender, certificate, chain, sslPolicyErrors) =>
{
return true;
};
List<string> clArgs = new List<string>();
foreach (string argIt in args)
{
@ -228,8 +240,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,
@ -247,7 +264,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,17 +250,23 @@ 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;
TimeSpan tsLastMail = ProgramStartTime.Subtract(m.LastCronjobBalanceMail);
bool skipInsufficientNotify = ((DateTime.Now.Day < 8) || (tsLastMail.TotalDays < 14));
if(m.Number == 37)
{
//Sixtus detected
Console.WriteLine("Sixtus detected");
}
int currentDebtLevel = 0; //no payments missed
//Account balance is capped to -2 monthly fees
@ -286,7 +280,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 +294,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 +319,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 +367,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 +415,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;

122
core/CsvImportProcess.cs Normal file
View File

@ -0,0 +1,122 @@
using System;
using System.IO;
using dezentrale.model;
using System.Collections.Generic;
using dezentrale.model.money;
namespace dezentrale.core
{
public class CsvImportProcess : BackgroundProcess
{
public string FileName { get; set; } = "";
public CsvImportProcess()
{
Caption = "Import CSV Money Transfers";
Steps = 5;
}
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;
}
}
}
}

1
core/NewExportProcess.cs Normal file
View File

@ -0,0 +1 @@


1
core/NewImportProcess.cs Normal file
View File

@ -0,0 +1 @@


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

@ -8,7 +8,7 @@
<OutputType>Exe</OutputType>
<RootNamespace>dezentrale</RootNamespace>
<AssemblyName>dezentrale-members</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<TargetFrameworkProfile />
@ -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" />
@ -210,6 +210,12 @@
<Compile Include="core\MvFinishProcess.cs" />
<Compile Include="view\LvAttachments.cs" />
<Compile Include="model\IAttachmentOwner.cs" />
<Compile Include="view\frmExportCsv.cs" />
<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

@ -8,19 +8,10 @@ namespace dezentrale.model
{
public class Configuration : XmlData
{
//Program-wide configuration (single-instance settings in main configuration)
[XmlElement] public ConfigSmtp Smtp { get; set; } = new ConfigSmtp();
[XmlElement] public ConfigVSMail VS { get; set; } = new ConfigVSMail();
[XmlElement] public MemberImportExport ImportExport{ get; set; } = new MemberImportExport();
[XmlElement] public string DbDirectory { get; set; } = DefaultDbDirectory;
//[XmlElement] public string DbBackupDirectory { get; set; } = DefaultDbBackupDirectory;
//[XmlElement] public string ImportExportDirectory { get; set; } = DefaultImportExportDirectory;
[XmlElement] public uint RegularPaymentAmount { get; set; } = 3200; //cents
[XmlElement] public string RegularPaymentCurrency { get; set; } = "EUR";
[XmlElement] public string LocalUser { get; set; } = "John Doe";
[XmlElement] public string KeylockCombination { get; set; } = "0000";
[XmlElement] public string LocalUser { get; set; } = "John Doe";
//UI: lstMembers: Columns
[XmlElement("MemberListColumn")] public List<ConfigLVColumn> MemberListColumns { get; set; } = new List<ConfigLVColumn>();
[XmlElement("MTListColumn")] public List<ConfigLVColumn> MTListColumns { get; set; } = new List<ConfigLVColumn>();
@ -28,17 +19,44 @@ namespace dezentrale.model
[XmlElement("MvInvitationsListColumn")]
public List<ConfigLVColumn> MvInvitationsListColumns{ get; set; } = new List<ConfigLVColumn>();
[XmlElement("AttachmentsColumn")]public List<ConfigLVColumn> AttachmentsColumns{ get; set; } = new List<ConfigLVColumn>();
[XmlElement] public List<KeyValue> MoneyTransferRegEx { get; set; } = new List<KeyValue>(); //This doesn't belong here! Move to new file within db-data!
[XmlElement] public DateTime LastCronjobRun { get; set; } = DateTime.Now; //This doesn't belong here! Move to new file within db-data!
[XmlElement("SelectFieldsColumn")]public List<ConfigLVColumn> SelectFieldsColumns{ get; set; } = new List<ConfigLVColumn>();
[XmlIgnore] public static string DefaultDbDirectory { get; private set; } = "db-data";
//[XmlIgnore] public static string DefaultDbBackupDirectory { get; private set; } = "db-backup";
//[XmlIgnore] public static string DefaultImportExportDirectory { get; private set; } = "import-export";
//Program-wide db metadata (multiple sub-objects in main configuration)
[XmlElement] public string DbDirectory { get; set; } = DefaultDbDirectory;
//[XmlElement] public string DbBackupDirectory { get; set; } = DefaultDbBackupDirectory;
//[XmlElement] public string ImportExportDirectory { get; set; } = DefaultImportExportDirectory;
[XmlElement] public DateTime LastDbLocalChange { get; set; }
[XmlElement] public DateTime LastDbExport { get; set; }
[XmlElement] public DateTime LastDbImport { get; set; }
[XmlElement] public bool DbChangedSinceExport { get; set; } = false;
public List<string> MemberCsvExportFields { get; set; } = new List<string>();
[XmlElement] public MemberImportExport ImportExport { get; set; } = new MemberImportExport();
//db-wide configuration (in DB folder)
[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";
[XmlElement] public string KeylockCombination { get; set; } = "0000";
[XmlElement] public List<KeyValue> MoneyTransferRegEx { get; set; } = new List<KeyValue>(); //This doesn't belong here! Move to new file within db-data!
[XmlElement] public DateTime LastCronjobRun { get; set; } = DateTime.Now; //This doesn't belong here! Move to new file within db-data!
[XmlIgnore] public static string DefaultDbDirectory { get; private set; } = "db-data";
//[XmlIgnore] public static string DefaultDbBackupDirectory { get; private set; } = "db-backup";
//[XmlIgnore] public static string DefaultImportExportDirectory { get; private set; } = "import-export";
}
}

View File

@ -81,7 +81,9 @@ namespace dezentrale.model
List<string> line = FileContents[l];
for (int f = 0; f < line.Count; f++)
{
sb.Append("\"" + line[f].Replace("\"", "\"\"") + "\"");
string s = line[f];
if (s == null) s = "null";
sb.Append("\"" + s.Replace("\"", "\"\"") + "\"");
if (f < line.Count - 1) sb.Append(FieldSeparator);
}

View File

@ -49,7 +49,11 @@ namespace dezentrale.model
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));
{
string[] addr = Program.config.Smtp.CcTo.Split(',', ';');
foreach(string s in addr)
if(!string.IsNullOrEmpty(s)) message.CC.Add(new MailAddress(s));
}
message.SubjectEncoding = Encoding.UTF8;
message.BodyEncoding = Encoding.UTF8;
@ -403,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,24 @@ 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(!t.TransferReason.ToLower().Contains(BankTransferRegEx.ToLower()))
return false;
//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 +245,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 +266,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 +321,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.
@ -200,6 +200,13 @@ namespace dezentrale.view
{
if (this.SelectedItems.Count < 1) return null;
return (T)this.SelectedItems[0].Tag;
}
public List<T> GetAllItems()
{
List<T> ret = new List<T>();
for (int i = 0; i < this.Items.Count; i++)
ret.Add((T)this.Items[i].Tag);
return ret;
}
public List<T> GetSelectedItems()
{
@ -217,6 +224,17 @@ namespace dezentrale.view
return ret;
}
public bool ItemSelection(T entry, bool selected)
{
foreach (ListViewItem lvi in this.Items)
if (lvi.Tag.Equals(entry))
{
lvi.Selected = selected;
return true;
}
return false;
}
public MenuItem AddMenuItem(string text, EventHandler handler = null, MenuItem parent = null, bool enabled = true)
{
MenuItem mi = new MenuItem() { Text = text };

41
view/LvSelectFields.cs Normal file
View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using dezentrale.model;
namespace dezentrale.view
{
public class LvSelectFields : CustomListView<string>
{
protected override List<ConfigLVDataHandler> DefaultColumns
{
get
{
return new List<ConfigLVDataHandler>()
{
new ConfigLVDataHandler()
{
Name = "field",
Display = "field",
Width = 120,
CustomToString = x => ((string)x),
},
};
}
}
public LvSelectFields() : base(Program.config.SelectFieldsColumns, LvSelectFields_ColumnsChanged)
{
}
private static void LvSelectFields_ColumnsChanged(object sender, ColumnsChangedArgs e)
{
Console.WriteLine("LvSelectFields_ColumnsChanged");
Program.config.SelectFieldsColumns.Clear();
foreach (ConfigLVDataHandler c in e.Columns) Program.config.SelectFieldsColumns.Add(new ConfigLVColumn(c));
XmlData.SaveToFile(Program.ConfigFile, Program.config);
}
}
}

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.Replace("\r\n", "\n");
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()
{
@ -611,7 +611,7 @@ namespace dezentrale.view
cbPaymentNotification.Checked = member.PaymentNotify;
tbBankTransferRegEx.ReadOnly = !cbEvaluateAccountInCharge.Checked;
tbBankTransferRegEx.Text = member.BankTransferRegEx;
tbRemarks.Text = member.Remarks;
tbRemarks.Text = member.Remarks.Replace("\n", "\r\n");
this.ResumeLayout(false);
}
@ -693,11 +693,19 @@ namespace dezentrale.view
member.BankAccountInCharge = tbBankAccountInCharge.Text;
member.PaymentNotify = cbPaymentNotification.Checked;
member.BankTransferRegEx = tbBankTransferRegEx.Text;
member.Remarks = tbRemarks.Text;
member.Remarks = tbRemarks.Text = member.Remarks.Replace("\r\n", "\n");
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();
}

116
view/frmExportCsv.cs Normal file
View File

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Windows.Forms;
using dezentrale.model;
namespace dezentrale.view
{
public class frmExportCsv<T> : FormWithActionButtons
{
private List<T> entities;
private LvSelectFields lv;
public List<string> SelectedProperties { get; set; } = new List<string>();
private bool ExportCsv()
{
SaveFileDialog sfd = new SaveFileDialog(){
FileName = $"{typeof(T)}_{entities.Count}_items.csv",
Filter = "Comma-Separated-Value files (*.csv)|*.csv"
};
DialogResult dr = sfd.ShowDialog();
if (dr != DialogResult.OK)
return false;
try
{
CsvFile csv = new CsvFile() {
};
//Build headline
csv.FileContents.Add(SelectedProperties);
foreach (T t in entities)
{
List<string> t_csv = new List<string>();
foreach (string p in SelectedProperties)
{
//Get propertyinfo from T
PropertyInfo pi = typeof(T).GetProperty(p);
if (!pi.CanRead) continue;
try
{
object o = pi.GetValue(t, null);
string propVal = (o == null ? "null" : o.ToString());
t_csv.Add(propVal);
} catch(Exception ex)
{
t_csv.Add(ex.Message);
}
}
csv.FileContents.Add(t_csv);
}
csv.SaveFile(sfd.FileName);
} catch(Exception ex)
{
MessageBox.Show($"Error({ex.GetType()}):\r\n{ex.Message}");
return false;
}
return true;
}
private void FormLoad(object sender, System.EventArgs e)
{
this.Text = $"Export {entities.Count} entities of type \"{typeof(T).ToString()}\"";
this.Controls.Add(lv = new LvSelectFields()
{
Left = 0,
Width = this.ClientSize.Width,
Top = 0,
Height = this.ClientSize.Height - 60,
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right,
});
foreach (var p in typeof(T).GetProperties())
{
lv.AddEntry(p.Name);
}
List<string> failToSelect = new List<string>();
foreach(string s in SelectedProperties)
{
if (!lv.ItemSelection(s, true)) {
failToSelect.Add(s);
}
}
foreach (string s in failToSelect) {
SelectedProperties.Remove(s);
}
this.AddButton("All fields", (se, ev) => {
SelectedProperties = lv.GetAllItems();
if (ExportCsv()) this.Close();
});
this.AddButton("Selected fields", (se, ev) => {
SelectedProperties = lv.GetSelectedItems();
if (ExportCsv()) this.Close();
});
this.AddButton("Cancel", (se, ev) => { this.Close(); });
}
public frmExportCsv(T t)
{
entities = new List<T> { t };
this.Load += FormLoad;
}
public frmExportCsv(List<T> l)
{
entities = l;
this.Load += FormLoad;
}
}
}

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),
} },
@ -65,7 +66,9 @@ namespace dezentrale.view
new MenuItem("Generate &Testdata", mnuMain_Members_Generate_Testdata),
new MenuItem("-"),
#endif
new MenuItem("Show numeric &info", lstMembers_mnuMain_Members_ShowInfo),
new MenuItem("Show numeric &info", lstMembers_mnuMain_Members_ShowInfo),
new MenuItem("Export &CSV (selected)", lstMembers_mnuMain_Members_CSV_selected),
new MenuItem("Export &CSV (all)", lstMembers_mnuMain_Members_CSV_all),
} },
new MenuItem("MV")
{ MenuItems = {
@ -86,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 });
@ -127,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);
}
}
}
}
}
@ -163,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)
@ -396,6 +407,25 @@ namespace dezentrale.view
MessageBox.Show(new MemberReport().Body);
}
private void MemberCsvExport(List<Member> list)
{
frmExportCsv<Member> frmCsv = new frmExportCsv<Member>(list)
{
SelectedProperties = Program.config.MemberCsvExportFields,
};
frmCsv.ShowDialog();
Program.config.MemberCsvExportFields = frmCsv.SelectedProperties;
XmlData.SaveToFile(Program.ConfigFile, Program.config);
}
private void lstMembers_mnuMain_Members_CSV_selected(object sender, EventArgs e)
{
MemberCsvExport(lstMembers.GetSelectedItems());
}
private void lstMembers_mnuMain_Members_CSV_all(object sender, EventArgs e)
{
MemberCsvExport(Program.members.Entries);
}
private void mnuMain_Payments_Add(object sender, EventArgs e)
{
frmMoneyTransfer addMoney = new frmMoneyTransfer();
@ -433,7 +463,26 @@ namespace dezentrale.view
DialogResult dr = ofd.ShowDialog();
//ofd.FilterIndex
if (dr == DialogResult.OK)
ProcessCsv.ProcessCSV(ofd.FileName);
{
lstMembers.SuspendLayout();
CsvImportProcess csvImport = new CsvImportProcess()
{
FileName = ofd.FileName,
};
frmProcessWithLog frmCsvImport = new frmProcessWithLog(csvImport, true);
dr = frmCsvImport.ShowDialog();
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)
{
@ -494,21 +543,35 @@ namespace dezentrale.view
lstMembers.ResumeLayout(false);
}
private bool SendMail(FormMail mail, Member m)
{
try
{
if (m == null) return true;
mail.Send(m);
} catch(Exception ex)
{
string msg = ex.Message;
while (ex.InnerException != null)
{
ex = ex.InnerException;
msg += "\r\n" + ex.Message;
}
MessageBox.Show($"Cannot send mail:\r\n{msg}");
}
return true;
}
private void lstMembers_TestMail(object sender, EventArgs e)
{
Member m = lstMembers.GetFirstSelectedItem();
m?.TestMail();
Member m = lstMembers.GetFirstSelectedItem();
FormMail testMail = FormMail.GenerateTestmail();
SendMail(testMail, m);
}
private void lstMembers_AccountStatusMail(object sender, EventArgs e)
{
Member m = lstMembers.GetFirstSelectedItem();
try
{
m?.AccountStatusMail();
} catch(Exception ex)
{
MessageBox.Show($"Cannot send account status mail:\r\n{ex.Message}");
}
Member m = lstMembers.GetFirstSelectedItem();
FormMail statusMail = FormMail.GenerateSingleMemberStatusReport();
SendMail(statusMail, m);
}
private void lstMembers_Edit(object sender, EventArgs e)

View File

@ -431,7 +431,7 @@ namespace dezentrale.view
cbCurrency.SelectedIndex = 1;
}
typeVal = mt.TransferType;
tbTransferReason.Text = mt.TransferReason;
tbTransferReason.Text = mt.TransferReason.Replace("\n", "\r\n");
}
foreach (MoneyTransfer.eTransferType type in Enum.GetValues(typeof(MoneyTransfer.eTransferType)))
@ -495,7 +495,7 @@ namespace dezentrale.view
mt.ValutaDate = valutaDate.Value;
mt.Amount = core.Utils.StringToInt64FP(tbAmount.Text);
mt.Currency = cbCurrency.Text;
mt.TransferReason = tbTransferReason.Text;
mt.TransferReason = tbTransferReason.Text.Replace("\r\n", "\n");
if (mt.GetType() == typeof(CashTransfer))
{

View File

@ -294,11 +294,11 @@ namespace dezentrale.view
mv.EventDate = dt;
mv.Place = tbPlace.Text;
mv.Agenda = tbMvAgenda.Text;
mv.Agenda = tbMvAgenda.Text.Replace("\r\n", "\n");
mv.InviteHeadline = tbInviteHeadline.Text;
mv.InviteBody = tbInviteBody.Text;
mv.InviteBody = tbInviteBody.Text.Replace("\r\n", "\n");
mv.Protocol = tbMvProtocol.Text;
mv.Protocol = tbMvProtocol.Text.Replace("\r\n", "\n");
}
private void BuildPageInvitationSettings(Control parent)
@ -500,12 +500,13 @@ namespace dezentrale.view
if (e.KeyChar != 13) return;
e.KeyChar = (char)0;
bool foundAuthCode = false;
string searchText = tbAuthCodePaste.Text.ToUpper();
foreach(MvInvitedMember mvi in mv.Members)
{
if (string.IsNullOrEmpty(mvi.AuthCode))
continue;
if (tbAuthCodePaste.Text.Contains(mvi.AuthCode))
if (searchText.Contains(mvi.AuthCode.ToUpper()))
{
foundAuthCode = true;
mvi.AttendedMv = true;
@ -607,11 +608,11 @@ namespace dezentrale.view
mvDate.Value = mv.EventDate;
mvTime.Value = mv.EventDate;
tbPlace.Text = mv.Place;
tbMvAgenda.Text = mv.Agenda;
tbMvAgenda.Text = mv.Agenda.Replace("\n", "\r\n");
tbInviteHeadline.Text = mv.InviteHeadline;
tbInviteBody.Text = mv.InviteBody;
tbInviteBody.Text = mv.InviteBody.Replace("\n", "\r\n");
tbMvProtocol.Text = mv.Protocol;
tbMvProtocol.Text = mv.Protocol.Replace("\n", "\r\n");
UpdateGui();