using System; using System.Collections.Generic; using System.IO; using System.Linq; using dezentrale.model; using dezentrale.model.money; namespace dezentrale.core { public enum IntermediateFormat { Text, SvgInkscape092, SvgInkscapeNew, Latex } public interface IPaymentReceiptProcessData { List MemberList { get; set; } bool AllMembers { get; set; } string DataTemplate { get; set; } IntermediateFormat DataFormat { get; set; } string OutputDirectory { get; set; } string FileNamePattern { get; set; } DateTime StartDate { get; set; } DateTime EndDate { get; set; } string StartDateString { get; } string EndDateString { get; } bool SendEmail { get; set; } bool WriteProtectPdf { get; set; } } //In order to be able to ReplaceReflect, we need to have the SvgData and SvgFileName field separated in a class class IntermediateFile : ReplaceReflectEntity { public string FileName { get; set; } = ""; public string Data { get; set; } = ""; public IntermediateFile() { } public IntermediateFile(string fileName) { FileName = fileName; Data = File.ReadAllText(fileName); } } //! \short Import functionality for the database contents //! \brief The normal export workflow is to pull the newest commits from mercurial //! or git and update/checkout them. After that, there is a packed zip file //! available that will be unpacked to the data directory public class PaymentReceiptProcess : BackgroundProcess { private IPaymentReceiptProcessData data = null; public PaymentReceiptProcess(IPaymentReceiptProcessData data) { this.data = data; Caption = "Generate receipt PDF for each membership payment"; Steps = 6; } //! \short Run receipt generation process //! \brief For each member-MoneyTransfer combination, generate a //! temporary SVG output, then run Inkscape to generate PDF //! \return true if the generation was successful. protected override bool Run() { uint step = 0; try { List memberList; bool allMembers = data.AllMembers; if (data.MemberList == null || data.MemberList.Count < 1) { if(!data.AllMembers) { LogTarget.LogLine($"No member list provided", LogEvent.ELogLevel.Error, "PaymentReceiptProcess"); return false; } memberList = Program.members.Entries; } else { memberList = allMembers ? Program.members.Entries : data.MemberList; LogTarget.LogLine($"Processing {memberList.Count} entries (AllMembers={allMembers})", LogEvent.ELogLevel.Error, "PaymentReceiptProcess"); } // In preparation to send E-Mail and combine multiple files into one mail later, // we're using a List of KVP with key = filename, value = member reference List> intermediateFiles = new List>(); string intermediateExt = "txt"; string finalExt = ".pdf"; switch (data.DataFormat) { case IntermediateFormat.Text: intermediateExt = ".txt"; finalExt = ".txt"; break; case IntermediateFormat.SvgInkscape092: intermediateExt = ".svg"; break; case IntermediateFormat.SvgInkscapeNew: intermediateExt = ".svg"; break; case IntermediateFormat.Latex: intermediateExt = ".tex"; break; } LogTarget.StepStarted(step, "Loading source SVG template"); IntermediateFile svgTemplate = new IntermediateFile(data.DataTemplate); svgTemplate.FileName = data.FileNamePattern; LogTarget.LogLine($"Loaded \"{data.DataTemplate}\" ({svgTemplate.Data.Length} characters) into memory.", LogEvent.ELogLevel.Info, "PaymentReceiptProcess"); LogTarget.StepStarted(++step, "Filtering MoneyTransfers for the given timeframe"); List transfers = Program.MoneyTransfers.Entries.Where //All entries with (o=>((o.TransferType == MoneyTransfer.eTransferType.MembershipPayment) //Correct transfer type, && (o.ValutaDate.CompareTo(data.StartDate) >= 0) //After start date, && (o.ValutaDate.CompareTo(data.EndDate) <= 0) //Before end date )).ToList(); LogTarget.LogLine($"{transfers.Count} MoneyTransfers to process.", LogEvent.ELogLevel.Info, "PaymentReceiptProcess"); LogTarget.StepStarted(++step, "Processing all payments to generate temporary SVGs (main step)"); foreach(MoneyTransfer mt in transfers) { Member m = memberList.Find(x => x.Number == mt.MemberNumber); if(m == null) { //this is only an error, if all members are selected in the dialog, we can check this. if(allMembers) LogTarget.LogLine($"Cannot find member {mt.MemberNumber} referenced in MoneyTransfer {mt.Id} (from {mt.ValutaDate})", LogEvent.ELogLevel.Error, "PaymentReceiptProcess"); //but in every case, we need to skip this MoneyTransfer. continue; } if(!m.MoneyTransfersIds.Contains(mt.Id)) { LogTarget.LogLine($"Member {m.Nickname} ({m.Number}) doesn't reference MoneyTransfer {mt.Id} (from {mt.ValutaDate})", LogEvent.ELogLevel.Error, "PaymentReceiptProcess"); continue; } IntermediateFile output = svgTemplate.ReplaceReflect(mt).ReplaceReflect(m); LogTarget.LogLine($"Generating file \"{output.FileName}\"", LogEvent.ELogLevel.Info, "PaymentReceiptProcess"); File.WriteAllText(Path.Combine(data.OutputDirectory, output.FileName + intermediateExt), output.Data); intermediateFiles.Add(new KeyValuePair(output.FileName,m)); } LogTarget.StepStarted(++step, "Converting intermediate files to PDF"); foreach (KeyValuePair kvp in intermediateFiles) { string sourceFile = Path.Combine(data.OutputDirectory, kvp.Key + intermediateExt); string pdf = Path.Combine(data.OutputDirectory, kvp.Key + finalExt); switch (data.DataFormat) { case IntermediateFormat.Text: LogTarget.LogLine($"Skipping PDF generation for: \"{kvp.Key}{intermediateExt}\"", LogEvent.ELogLevel.Info, "PaymentReceiptProcess"); break; case IntermediateFormat.SvgInkscape092: ImportExportBase.RunProcess("inkscape", $"\"{sourceFile}\" --export-pdf={pdf}", ".", LogTarget); break; case IntermediateFormat.SvgInkscapeNew: ImportExportBase.RunProcess("inkscape", $"\"{sourceFile}\" --export-type=pdf --export-filename={pdf}", ".", LogTarget); break; case IntermediateFormat.Latex: ImportExportBase.RunProcess("pdflatex", $"\"{sourceFile}\"", ".", LogTarget); break; } if(data.WriteProtectPdf && data.DataFormat != IntermediateFormat.Text) { //Write-protecting pdf ImportExportBase.RunProcess("qpdf", $"--encrypt \"\" \"random-owner-pw\" 256 --modify=none -- \"{pdf}\" \"{pdf}.writeprotect\"", ".", LogTarget); File.Replace(pdf, pdf + ".writeprotect", pdf + ".writable"); } } LogTarget.StepStarted(++step, "E-Mail the PDFs to the members"); if (data.SendEmail) { FormMail receiptMail = Program.mailTemplates.MemberPaymentReceipts.ReplaceReflect(data); foreach (Member m in memberList) { //gather entries per member List outputs = new List(); foreach (KeyValuePair kvp in intermediateFiles) { if (m.Number == kvp.Value.Number) outputs.Add(Path.Combine(data.OutputDirectory, kvp.Key + finalExt)); } //send E-Mail m.StartLogEvent($"Payment receipts E-Mail ({data.StartDateString} ... {data.EndDateString})",LogEvent.eEventType.EMail, Program.config.LocalUser); try { m.CurrentLog.SubEvents.Add(receiptMail.Send(m, outputs)); } catch(Exception ex) { 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(null, true); } } else { LogTarget.LogLine($"This step is skipped.", LogEvent.ELogLevel.Info, "PaymentReceiptProcess"); } LogTarget.StepCompleted(4, $"E-Mail the PDFs to the members", true); return true; } catch(Exception ex) { LogTarget.LogLine($"An Error occurred: {ex.Message}", LogEvent.ELogLevel.Error, "PaymentReceiptProcess"); LogTarget.StepCompleted(step, $"E-Mail the PDFs to the members", false); return false; } } } }