diff --git a/OFX-to-XLSX/.gitignore b/OFX-to-XLSX/.gitignore new file mode 100644 index 0000000..8a41c8b --- /dev/null +++ b/OFX-to-XLSX/.gitignore @@ -0,0 +1,54 @@ +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# Click-Once directory +[Pp]ublish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg + +# The packages folder can be ignored because of Package Restore except build/, which is used as an MSBuild target. +**/[Pp]ackages/* +!**/[Pp]ackages/build/ + +# XML Documentation file +Easy-Core.xml diff --git a/OFX-to-XLSX/Documentation/OFX_102_Samples/1.ofx b/OFX-to-XLSX/Documentation/OFX_102_Samples/1.ofx new file mode 100644 index 0000000..3897c5c --- /dev/null +++ b/OFX-to-XLSX/Documentation/OFX_102_Samples/1.ofx @@ -0,0 +1,72 @@ + +OFXHEADER:100 +DATA:OFXSGML +VERSION:102 +SECURITY:NONE +ENCODING:USASCII +CHARSET:1252 +COMPRESSION:NONE +OLDFILEUID:NONE +NEWFILEUID:NONE + + + + + +0 +INFO + +00000000000000 +POR + + + + +1001 + +0 +INFO + + +BRL + +0237 +99999 +CHECKING + + +20190509120000 +20190509120000 + +CREDIT +20190430120000 +10,00 +N1013F +3243801 +RESGATE INVEST + + +CREDIT +20190430120000 +24783,31 +N10153 +3243801 +RESGATE INVEST + + +DEBIT +20190430120000 +-22785,76 +N10167 +936909 +APLICACAO EM FUNDOS + + + +17071,23 +00000000 + + + + + diff --git a/OFX-to-XLSX/Documentation/OFX_102_Samples/2.ofx b/OFX-to-XLSX/Documentation/OFX_102_Samples/2.ofx new file mode 100644 index 0000000..73216a5 --- /dev/null +++ b/OFX-to-XLSX/Documentation/OFX_102_Samples/2.ofx @@ -0,0 +1,76 @@ +OFXHEADER:100 +DATA:OFXSGML +VERSION:102 +SECURITY:NONE +ENCODING:USASCII +CHARSET:1252 +COMPRESSION:NONE +OLDFILEUID:NONE +NEWFILEUID:NONE + + + + + + 0 + INFO + + 20140304100000[-03:EST] + POR + + MYBANK + MYBANK + + + + + + 1001 (can be a GUID) + + 0 + INFO + + + BRL + + 0341 + 9999999999 + CHECKING + + + 20131205100000[-03:EST] or 20240601040000.000 + 20140228100000[-03:EST] + + DEBIT + 20131209100000[-03:EST] + -666.66 + 20131209001 (can be hex encoded) + 20131209001 + PROPERTY TAX + RSHOP + + + CREDIT + 20131209100000[-03:EST] + 99.99 + 20131209002 + 20131209002 + REND PAGO APLIC AUT MAIS + + + DEBIT + 20131210100000[-03:EST] + -77.77 + 20131210001 + 20131210001 + SISDEB + + + + -9999.99 + 20140304100000[-03:EST] + + + + + diff --git a/OFX-to-XLSX/Documentation/OFX_102_Samples/3.ofx b/OFX-to-XLSX/Documentation/OFX_102_Samples/3.ofx new file mode 100644 index 0000000..071eab8 --- /dev/null +++ b/OFX-to-XLSX/Documentation/OFX_102_Samples/3.ofx @@ -0,0 +1,78 @@ +OFXHEADER:100 +DATA:OFXSGML +VERSION:102 +SECURITY:NONE +ENCODING:USASCII +CHARSET:1252 +COMPRESSION:NONE +OLDFILEUID:NONE +NEWFILEUID:NONE + + + + + + 0 + INFO + + 20140203182251[-3:GMT] + ENG + + SANTANDER + SANTANDER + + + + + + 1 + + 0 + INFO + + + BRL + + 033 + 9999999999999 + CHECKING + + + 20140203182251[-3:GMT] + 20140203182251[-3:GMT] + + OTHER + 20131107000000[-3:GMT] + -11.11 + 00510367 + 00510367 + 0 + DEBITO VISA ELECTRON BRASIL + + + OTHER + 20131107000000[-3:GMT] + -222,22 + 00000105 + 00000105 + 0 + COMPENSACAO INTERNA DE CHEQUE + + + OTHER + 20131108000000[-3:GMT] + -333.33 + 00330867 + 00330867 + 0 + DEBITO VISA ELECTRON BRASIL + + + + 9999.99 + 20140203182251[-3:GMT] + + + + + \ No newline at end of file diff --git a/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin1.doc b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin1.doc new file mode 100644 index 0000000..8b92478 Binary files /dev/null and b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin1.doc differ diff --git a/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin2.doc b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin2.doc new file mode 100644 index 0000000..e2d658c Binary files /dev/null and b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin2.doc differ diff --git a/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin3.doc b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin3.doc new file mode 100644 index 0000000..71ba350 Binary files /dev/null and b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin3.doc differ diff --git a/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin4.doc b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin4.doc new file mode 100644 index 0000000..eb24342 Binary files /dev/null and b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin4.doc differ diff --git a/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin5.doc b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin5.doc new file mode 100644 index 0000000..939fb42 Binary files /dev/null and b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin5.doc differ diff --git a/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin6.doc b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin6.doc new file mode 100644 index 0000000..b13a474 Binary files /dev/null and b/OFX-to-XLSX/Documentation/OFX_102_Specification/Ofexfin6.doc differ diff --git a/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.Designer.cs b/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.Designer.cs new file mode 100644 index 0000000..250f10b --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.Designer.cs @@ -0,0 +1,155 @@ +namespace OFX_To_XLSX_Converter +{ + partial class ConverterForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + labelOpenFile = new Label(); + openFileDialog = new OpenFileDialog(); + buttonOpenFile = new Button(); + textBoxOfxFile = new TextBox(); + textBoxXlsxFile = new TextBox(); + buttonSaveFile = new Button(); + saveFileDialog = new SaveFileDialog(); + buttonConvert = new Button(); + labelSaveFile = new Label(); + richTextBoxResults = new RichTextBox(); + SuspendLayout(); + // + // labelOpenFile + // + labelOpenFile.AutoSize = true; + labelOpenFile.Location = new Point(12, 9); + labelOpenFile.Name = "labelOpenFile"; + labelOpenFile.Size = new Size(142, 25); + labelOpenFile.TabIndex = 0; + labelOpenFile.Text = "Choose OFX File"; + // + // openFileDialog + // + openFileDialog.FileName = "openFileDialog"; + openFileDialog.Filter = "OFX Files|*.ofx|All Files|*.*"; + // + // buttonOpenFile + // + buttonOpenFile.Location = new Point(613, 34); + buttonOpenFile.Name = "buttonOpenFile"; + buttonOpenFile.Size = new Size(175, 34); + buttonOpenFile.TabIndex = 1; + buttonOpenFile.Text = "Browse..."; + buttonOpenFile.UseVisualStyleBackColor = true; + buttonOpenFile.Click += OnOpenFileClicked; + // + // textBoxOfxFile + // + textBoxOfxFile.Location = new Point(12, 37); + textBoxOfxFile.Name = "textBoxOfxFile"; + textBoxOfxFile.Size = new Size(595, 31); + textBoxOfxFile.TabIndex = 2; + // + // textBoxXlsxFile + // + textBoxXlsxFile.Location = new Point(12, 121); + textBoxXlsxFile.Name = "textBoxXlsxFile"; + textBoxXlsxFile.Size = new Size(595, 31); + textBoxXlsxFile.TabIndex = 3; + // + // buttonSaveFile + // + buttonSaveFile.Location = new Point(613, 118); + buttonSaveFile.Name = "buttonSaveFile"; + buttonSaveFile.Size = new Size(175, 34); + buttonSaveFile.TabIndex = 4; + buttonSaveFile.Text = "Set Name..."; + buttonSaveFile.UseVisualStyleBackColor = true; + buttonSaveFile.Click += OnSaveFileClicked; + // + // saveFileDialog + // + saveFileDialog.DefaultExt = "xlsx"; + saveFileDialog.Filter = "Excel Files|*.xlsx|All Files|*.*"; + // + // buttonConvert + // + buttonConvert.Location = new Point(12, 220); + buttonConvert.Name = "buttonConvert"; + buttonConvert.Size = new Size(776, 84); + buttonConvert.TabIndex = 5; + buttonConvert.Text = "Convert!"; + buttonConvert.UseVisualStyleBackColor = true; + buttonConvert.Click += OnConvertFileClicked; + // + // labelSaveFile + // + labelSaveFile.AutoSize = true; + labelSaveFile.Location = new Point(12, 93); + labelSaveFile.Name = "labelSaveFile"; + labelSaveFile.Size = new Size(148, 25); + labelSaveFile.TabIndex = 6; + labelSaveFile.Text = "Save To XLSX File"; + // + // richTextBoxResults + // + richTextBoxResults.Location = new Point(12, 310); + richTextBoxResults.Name = "richTextBoxResults"; + richTextBoxResults.Size = new Size(776, 163); + richTextBoxResults.TabIndex = 7; + richTextBoxResults.Text = ""; + // + // ConverterForm + // + AutoScaleDimensions = new SizeF(10F, 25F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(800, 485); + Controls.Add(richTextBoxResults); + Controls.Add(labelSaveFile); + Controls.Add(buttonConvert); + Controls.Add(buttonSaveFile); + Controls.Add(textBoxXlsxFile); + Controls.Add(textBoxOfxFile); + Controls.Add(buttonOpenFile); + Controls.Add(labelOpenFile); + Name = "ConverterForm"; + Text = "Convert OFX To XLSX"; + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Label labelOpenFile; + private OpenFileDialog openFileDialog; + private Button buttonOpenFile; + private TextBox textBoxOfxFile; + private TextBox textBoxXlsxFile; + private Button buttonSaveFile; + private SaveFileDialog saveFileDialog; + private Button buttonConvert; + private Label labelSaveFile; + private RichTextBox richTextBoxResults; + } +} \ No newline at end of file diff --git a/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.cs b/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.cs new file mode 100644 index 0000000..f22a204 --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.cs @@ -0,0 +1,57 @@ +using OFX_To_XLSX; + +namespace OFX_To_XLSX_Converter; + +public partial class ConverterForm : Form +{ + public ConverterForm() + { + InitializeComponent(); + } + + private void OnOpenFileClicked(object sender, EventArgs args) + { + if (openFileDialog.ShowDialog() == DialogResult.OK) + textBoxOfxFile.Text = openFileDialog.FileName; + } + + private void OnSaveFileClicked(object sender, EventArgs args) + { + if (saveFileDialog.ShowDialog() == DialogResult.OK ) + textBoxXlsxFile.Text = saveFileDialog.FileName; + } + + private void OnConvertFileClicked(object sender, EventArgs args) + { + richTextBoxResults.ResetText(); + + if (string.IsNullOrWhiteSpace(textBoxOfxFile.Text) || string.IsNullOrWhiteSpace(textBoxXlsxFile.Text)) + { + richTextBoxResults.AppendText("Must provide both OFX and XLSX files."); + return; + } + else if (File.Exists(textBoxOfxFile.Text) == false || Directory.Exists(Path.GetDirectoryName(textBoxXlsxFile.Text)) == false) + { + richTextBoxResults.AppendText("OFX file and directory to save XLSX file must both exist."); + return; + } + + try + { + var converter = new OfxParser(); + var ofx = converter.ReadOfxFile(textBoxOfxFile.Text); + + if (ofx == null) + richTextBoxResults.AppendText("Failed reading OFX file."); + else if (converter.ConvertOfxToXlsx(textBoxXlsxFile.Text, ofx)) + richTextBoxResults.AppendText("Converted OFX to XLSX successfully."); + else + richTextBoxResults.AppendText("Failed converting OFX to XLSX."); + } + catch (Exception e) + { + richTextBoxResults.AppendText("Failed converting OFX to XLSX."); + richTextBoxResults.AppendText($"Error details {e.GetType().Name}, {e.Message}."); + } + } +} diff --git a/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.resx b/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.resx new file mode 100644 index 0000000..fd94e54 --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX-Converter/ConverterForm.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 203, 17 + + \ No newline at end of file diff --git a/OFX-to-XLSX/OFX-To-XLSX-Converter/OFX-To-XLSX-Converter.csproj b/OFX-to-XLSX/OFX-To-XLSX-Converter/OFX-To-XLSX-Converter.csproj new file mode 100644 index 0000000..33d6bbe --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX-Converter/OFX-To-XLSX-Converter.csproj @@ -0,0 +1,21 @@ + + + + WinExe + net8.0-windows + $(MSBuildProjectName.Replace("-", "_")) + true + enable + enable + NF Software Inc. + Copyright 2024 $(Company) + 1.0.0.0 + 1.0.0.0 + 1.0.0 + + + + + + + \ No newline at end of file diff --git a/OFX-to-XLSX/OFX-To-XLSX-Converter/Program.cs b/OFX-to-XLSX/OFX-To-XLSX-Converter/Program.cs new file mode 100644 index 0000000..96ec68c --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX-Converter/Program.cs @@ -0,0 +1,14 @@ +namespace OFX_To_XLSX_Converter; + +internal static class Program +{ + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + ApplicationConfiguration.Initialize(); + Application.Run(new ConverterForm()); + } +} diff --git a/OFX-to-XLSX/OFX-To-XLSX.sln b/OFX-to-XLSX/OFX-To-XLSX.sln new file mode 100644 index 0000000..ca09366 --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35027.167 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OFX-To-XLSX-Converter", "OFX-To-XLSX-Converter\OFX-To-XLSX-Converter.csproj", "{47377EB8-AD53-428F-912D-17D359CB5901}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OFX-To-XLSX", "OFX-To-XLSX\OFX-To-XLSX.csproj", "{361385AD-7B50-41F0-8E11-28E86F4555A1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {47377EB8-AD53-428F-912D-17D359CB5901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47377EB8-AD53-428F-912D-17D359CB5901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47377EB8-AD53-428F-912D-17D359CB5901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47377EB8-AD53-428F-912D-17D359CB5901}.Release|Any CPU.Build.0 = Release|Any CPU + {361385AD-7B50-41F0-8E11-28E86F4555A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {361385AD-7B50-41F0-8E11-28E86F4555A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {361385AD-7B50-41F0-8E11-28E86F4555A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {361385AD-7B50-41F0-8E11-28E86F4555A1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1B7E494D-101C-43C0-ACA7-C3B9A19A79FF} + EndGlobalSection +EndGlobal diff --git a/OFX-to-XLSX/OFX-To-XLSX/Models/OfxAccount.cs b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxAccount.cs new file mode 100644 index 0000000..a85a90e --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxAccount.cs @@ -0,0 +1,19 @@ +namespace OFX_To_XLSX; + +public class OfxAccount +{ + /// + /// Routing and transit number. + /// + public string? BankId { get; set; } + + /// + /// Account number. + /// + public string? AccountId { get; set; } + + /// + /// Type of account. + /// + public string? AccountType { get; set; } +} diff --git a/OFX-to-XLSX/OFX-To-XLSX/Models/OfxFile.cs b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxFile.cs new file mode 100644 index 0000000..77a207f --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxFile.cs @@ -0,0 +1,49 @@ +namespace OFX_To_XLSX; + +public class OfxFile +{ + /// + /// Contains the header data from the OFX file. + /// + public OfxHeader Header { get; init; } = new(); + + /// + /// The Signon message set includes the signon message, USERPASS change message, and challenge message, which must appear in that order. + /// + public OfxSignOnResponse SignOnResponse { get; init; } = new(); + + /// + /// Contains details on the account the OFX file is for. + /// + public OfxAccount Account { get; init; } = new(); + + /// + /// Default currency for the statement. + /// + public string? Currency { get; set; } + + /// + /// Start date of statement requested. + /// + public DateTime? Start { get; set; } + + /// + /// End date of statement requested. + /// + public DateTime? End { get; set; } + + /// + /// Ledger balance amount. + /// + public decimal? Balance { get; set; } + + /// + /// Balance date. + /// + public DateTime? BalanceDate { get; set; } + + /// + /// Contains the transactions from the OFX file. + /// + public List Transactions { get; init; } = []; +} diff --git a/OFX-to-XLSX/OFX-To-XLSX/Models/OfxHeader.cs b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxHeader.cs new file mode 100644 index 0000000..5e2b8ec --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxHeader.cs @@ -0,0 +1,49 @@ +namespace OFX_To_XLSX; + +public class OfxHeader +{ + /// + /// OFXHEADER specifies the version number of the Open Financial Exchange headers. + /// + public int HeaderVersion { get; set; } + + /// + /// DATA specifies the content type, in this case OFXSGML. + /// + public string? ContentType { get; set; } + + /// + /// VERSION specifies the version number of the Document Type Definition (DTD) used for parsing. + /// + public int Version { get; set; } + + /// + /// SECURITY defines the type of application-level security, if any, that is used for the OFX block. The values for SECURITY can be NONE or TYPE1. + /// + public string? Security { get; set; } + + /// + /// ENCODING defines the text encoding used for character data. The values for ENCODING can be UNICODE or USASCII. + /// + public string? Encoding { get; set; } + + /// + /// CHARSET defines the character set used for character data. + /// + public string? CharacterSet { get; set; } + + /// + /// A future version of the specification will define compression. + /// + public string? Compression { get; set; } + + /// + /// NEWFILEUID uniquely identifies this request file. + /// + public string? NewFileId { get; set; } + + /// + /// OLDFILEUID is used together with NEWFILEUID only when the client and server support file-based error recovery. + /// + public string? OldFileId { get; set; } +} diff --git a/OFX-to-XLSX/OFX-To-XLSX/Models/OfxSignOnResponse.cs b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxSignOnResponse.cs new file mode 100644 index 0000000..31009ad --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxSignOnResponse.cs @@ -0,0 +1,34 @@ +namespace OFX_To_XLSX; + +public class OfxSignOnResponse +{ + /// + /// Organization defining this Financial Institution name space. + /// + public string? Organization { get; set; } + + /// + /// Financial Institution ID. + /// + public string? OrganizationId { get; set; } + + /// + /// Contains any error code that occurs during sign-on. + /// + public int Status { get; set; } + + /// + /// Contains any severity level of error codes that occur during sign-on. + /// + public string? StatusSeverity { get; set; } + + /// + /// Language used in text responses. + /// + public string? Language { get; set; } + + /// + /// Date and time of the server response. + /// + public DateTime? ServerTime { get; set; } +} diff --git a/OFX-to-XLSX/OFX-To-XLSX/Models/OfxTransaction.cs b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxTransaction.cs new file mode 100644 index 0000000..6d8c981 --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX/Models/OfxTransaction.cs @@ -0,0 +1,39 @@ +namespace OFX_To_XLSX; + +public class OfxTransaction +{ + /// + /// Transaction type. + /// + public string? TransactionType { get; set; } + + /// + /// Date transaction was posted to account. + /// + public DateTime? Posted { get; set; } + + /// + /// Amount of transaction. + /// + public decimal Amount { get; set; } + + /// + /// Transaction ID issued by financial institution. + /// + public string? TransactionId { get; set; } + + /// + /// Check (or other reference) number. + /// + public string? CheckNumber { get; set; } + + /// + /// Name of payee or description of transaction. + /// + public string? Name { get; set; } + + /// + /// Extra information (not in NAME). + /// + public string? Memo { get; set; } +} diff --git a/OFX-to-XLSX/OFX-To-XLSX/OFX-To-XLSX.csproj b/OFX-to-XLSX/OFX-To-XLSX/OFX-To-XLSX.csproj new file mode 100644 index 0000000..78a0027 --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX/OFX-To-XLSX.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + $(MSBuildProjectName.Replace("-", "_")) + enable + enable + NF Software Inc. + Copyright 2024 $(Company) + 1.0.0.0 + 1.0.0.0 + 1.0.0 + Library + + + + + + + + + diff --git a/OFX-to-XLSX/OFX-To-XLSX/OfxParser.cs b/OFX-to-XLSX/OFX-To-XLSX/OfxParser.cs new file mode 100644 index 0000000..6076dfe --- /dev/null +++ b/OFX-to-XLSX/OFX-To-XLSX/OfxParser.cs @@ -0,0 +1,219 @@ +using easy_core; +using SpreadsheetLight; +using System; +using System.Globalization; +using System.Xml; +using System.Xml.Linq; + +namespace OFX_To_XLSX; + +public class OfxParser +{ + /// + /// Reads the specified file and parses the OFX data. + /// + /// The path to the file to parse. + public OfxFile? ReadOfxFile(string file) + { + if (File.Exists(file) == false) + return null; + + var text = File.ReadAllText(file); + var lines = text.ToLines().Where(x => string.IsNullOrWhiteSpace(x) == false).Select(x => x.Trim()).ToArray(); + var ofx = new OfxFile(); + + while (lines.Length > 0 && lines[0].FirstOrDefault() != '<') + { + if (lines[0].StartsWith("OFXHEADER")) + ofx.Header.HeaderVersion = int.TryParse(lines[0].Split(':').Last(), out var version) ? version : 0; + else if (lines[0].StartsWith("DATA")) + ofx.Header.ContentType = lines[0].Split(':').Skip(1).FirstOrDefault(); + else if (lines[0].StartsWith("VERSION")) + ofx.Header.Version = int.TryParse(lines[0].Split(':').Last(), out var version) ? version : 0; + else if (lines[0].StartsWith("SECURITY")) + ofx.Header.Security = lines[0].Split(':').Skip(1).FirstOrDefault(); + else if (lines[0].StartsWith("ENCODING")) + ofx.Header.Encoding = lines[0].Split(':').Skip(1).FirstOrDefault(); + else if (lines[0].StartsWith("CHARSET")) + ofx.Header.CharacterSet = lines[0].Split(':').Skip(1).FirstOrDefault(); + else if (lines[0].StartsWith("COMPRESSION")) + ofx.Header.Compression = lines[0].Split(':').Skip(1).FirstOrDefault(); + else if (lines[0].StartsWith("OLDFILEUID")) + ofx.Header.OldFileId = lines[0].Split(':').Skip(1).FirstOrDefault(); + else if (lines[0].StartsWith("NEWFILEUID")) + ofx.Header.NewFileId = lines[0].Split(':').Skip(1).FirstOrDefault(); + + lines = lines.Skip(1).ToArray(); + } + + var xml = ConvertOfxToXml(lines); + + if (string.IsNullOrWhiteSpace(xml)) + return null; + + ParseXmlData(xml, ofx); + return ofx; + } + + private string ConvertOfxToXml(string[] lines) + { + using var writer = new StringWriter(); + using var xml = XmlWriter.Create(writer); + + xml.WriteStartDocument(); + + foreach (var line in lines) + { + if (line.StartsWith("')) + xml.WriteEndElement(); + else if (line.StartsWith('<') && line.EndsWith('>')) + xml.WriteStartElement(line.TrimStart('<').TrimEnd('>')); + else if (line.Contains('>') && line.Length > line.IndexOf('>') + 1) + xml.WriteElementString(line.Split('>').First().TrimStart('<'), line.Split('>').Last()); + } + + xml.WriteEndDocument(); + xml.Flush(); + + return writer.ToString(); + } + + private void ParseXmlData(string xml, OfxFile ofx) + { + var document = XDocument.Parse(xml); + + var signon = document.Descendants("SONRS").SingleOrDefault(); + var account = document.Descendants("BANKACCTFROM").FirstOrDefault(); + var ledger = document.Descendants("LEDGERBAL").FirstOrDefault(); + var transactions = document.Descendants("STMTTRN"); + + if (signon != null && signon.HasElements) + { + var institution = signon.Element("FI"); + + if (institution != null) + { + ofx.SignOnResponse.Organization = institution.Element("ORG")?.Value; + ofx.SignOnResponse.OrganizationId = institution.Element("FID")?.Value; + } + + var status = signon.Element("STATUS"); + + if (status != null) + { + ofx.SignOnResponse.Status = int.TryParse(status.Element("CODE")?.Value, out int code) ? code : 0; + ofx.SignOnResponse.StatusSeverity = status.Element("SEVERITY")?.Value; + } + + ofx.SignOnResponse.Language = signon.Element("LANGUAGE")?.Value; + ofx.SignOnResponse.ServerTime = ParseXmlDate(signon.Element("DTSERVER")); + } + + if (account != null && account.HasElements) + { + ofx.Account.BankId = account.Element("BANKID")?.Value; + ofx.Account.AccountId = account.Element("ACCTID")?.Value; + ofx.Account.AccountType = account.Element("ACCTTYPE")?.Value; + } + + ofx.Currency = document.Descendants("CURDEF").FirstOrDefault()?.Value; + ofx.Start = ParseXmlDate(document.Descendants("DTSTART").FirstOrDefault()); + ofx.End = ParseXmlDate(document.Descendants("DTEND").FirstOrDefault()); + + if (ledger != null && ledger.HasElements) + { + ofx.Balance = decimal.TryParse(ledger.Element("BALAMT")?.Value, out decimal balance) ? balance : null; + ofx.BalanceDate = ParseXmlDate(ledger.Element("DTASOF")); + } + + foreach (var transaction in transactions.Where(x => x.HasElements)) + { + ofx.Transactions.Add(new OfxTransaction + { + TransactionType = transaction.Element("TRNTYPE")?.Value, + Amount = decimal.TryParse(transaction.Element("TRNAMT")?.Value, out decimal amount) ? amount : 0, + Posted = ParseXmlDate(transaction.Element("DTPOSTED")), + TransactionId = transaction.Element("FITID")?.Value, + CheckNumber = transaction.Element("CHECKNUM")?.Value, + Name = transaction.Element("NAME")?.Value, + Memo = transaction.Element("MEMO")?.Value, + }); + } + } + + private DateTime? ParseXmlDate(XElement? element) => DateTime.TryParseExact(element?.Value[..14], "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime time) ? time : null; + + /// + /// Converts the provided OFX file into an XLSX file and saves it at the specified path. + /// + /// The path and filename to save the XLSX file to. + /// Object containing the parsed OFX data. + /// + /// + /// + /// + public bool ConvertOfxToXlsx(string file, OfxFile ofx, bool includeHeader = false, bool includeSignOn = false, bool includeDetails = true, bool useTableHeader = true) + { + // Prepare styles + var excel = new SLDocument(); + + var title = excel.CreateStyle(); + var accent1 = excel.CreateStyle(); + var accent2 = excel.CreateStyle(); + var dates = excel.CreateStyle(); + var numbers = excel.CreateStyle(); + + title.ApplyNamedCellStyle(SLNamedCellStyleValues.Title); + accent1.ApplyNamedCellStyle(SLNamedCellStyleValues.Accent1); + accent2.ApplyNamedCellStyle(SLNamedCellStyleValues.Accent2); + dates.FormatCode = "yyyy-mm-dd hh:mm:ss"; + numbers.FormatCode = "0.00"; + + // Prepare data + if (useTableHeader) + { + excel.SetCellValue("A1", "Transaction Id"); + excel.SetCellValue("B1", "Transaction Type"); + excel.SetCellValue("C1", "Posted"); + excel.SetCellValue("D1", "Amount"); + excel.SetCellValue("E1", "Detail"); + excel.SetCellValue("F1", "Reference"); + excel.SetCellValue("G1", "Memo"); + + excel.SetCellStyle("A1", "G1", accent1); + } + + var offset = useTableHeader ? 2 : 1; + + for (var i = 0; i < ofx.Transactions.Count; i++) + { + excel.SetCellValue($"A{i + offset}", ofx.Transactions[i].TransactionId); + excel.SetCellValue($"B{i + offset}", ofx.Transactions[i].TransactionType); + + if (ofx.Transactions[i].Posted != null) + excel.SetCellValue($"C{i + offset}", ofx.Transactions[i].Posted!.Value); + + excel.SetCellValue($"D{i + offset}", ofx.Transactions[i].Amount); + excel.SetCellValue($"E{i + offset}", ofx.Transactions[i].Name); + excel.SetCellValue($"F{i + offset}", ofx.Transactions[i].CheckNumber); + excel.SetCellValue($"G{i + offset}", ofx.Transactions[i].Memo); + } + + excel.SetCellStyle("C" + offset, "C" + (offset + ofx.Transactions.Count - 1), dates); + excel.SetCellStyle("D" + offset, "D" + (offset + ofx.Transactions.Count - 1), numbers); + + // Autofit + excel.AutoFitColumn("A", "G"); + + // Save + try + { + excel.SaveAs(file); + return true; + } + catch + { + return false; + } + } +} diff --git a/README.md b/README.md index 2e700b4..b9788ba 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ These tools are intentionally pragmatic: > If you’re looking for curated, stable, long-term maintained tooling… this repo probably isn’t it. > If you’re looking for *“I just need this done once”* helpers — welcome. 🙂 -## Contents +# Contents -### ✅ Extract Copilot Chat +## ✅ Extract Copilot Chat Export Copilot chat transcripts from **JSON files saved via browser DevTools (Network tab)** into **Markdown**. @@ -24,7 +24,13 @@ Copilot UIs change frequently and export options are inconsistent across account 2. Save the conversation JSON response(s) to disk 3. Convert to readable `.md` files you can archive, search, and sort yourself -## Extract Copilot Chat +## ✅ OFX to XLSX Converter + +Convert basic **OFX (Open Financial Exchange)** bank export files into simple **Excel (.xlsx)** spreadsheets. + +This is a **best-effort parser**, built for practical use with real-world bank exports — not a full spec-compliant OFX implementation. + +# Extract Copilot Chat ### What it does @@ -94,7 +100,65 @@ Each markdown file includes: * **Deduping is heuristic**: It aims to remove common duplicates but may occasionally merge distinct messages that are identical in content. * **No guarantee of completeness**: If the network response didn’t include older turns, the output won’t either. -## Contributing +# OFX to XLSX Converter + +### What it does + +* Reads a single `.ofx` file +* Extracts common transaction fields, including: + * Date + * Amount + * Description / Memo + * Transaction type (if available) + * Account / balance info (when present) +* Outputs a flat `.xlsx` file for easy filtering, sorting, and analysis + +### Supported scenarios + +* Typical bank export files (checking / credit accounts) +* OFX files that resemble: + * SGML-style (older OFX 1.x) + * Loosely structured, non-strict variants many banks produce + +### Not supported + +* Full OFX 2.x / XML compliance +* Every possible tag or edge case +* Investment account structures (partial at best) +* Multi-account aggregation in a single file + +## How to use + +### Run + +```bash +dotnet run --project easy-mini-tools/OFX-To-XLSX/OFX-To-XLSX-Converter +``` + +## Output format + +The generated spreadsheet includes: + +* One row per transaction +* Columns for: + * Type + * Date + * Amount + * Id + * Check Number + * Name + * Description +* Minimal formatting — intended for **post-processing in Excel**, not presentation + +## Known limitations / disclaimers + +* **Best-effort parsing**: OFX files are notoriously inconsistent between institutions. +* **Loose parsing strategy**: The tool prioritizes “getting usable data out” over strict correctness. +* **Field coverage varies**: Some banks omit fields or use non-standard tags. +* **No validation guarantees**: Always sanity-check totals and balances against your source data. +* **Encoding quirks**: Some OFX files include odd encoding/line break issues that may require manual cleanup. + +# Contributing PRs are welcome, but the spirit of this repo is **small + useful + low-maintenance**. @@ -104,22 +168,27 @@ If you submit improvements, please keep them aligned with the theme: * No dependency explosions * Prefer “drop-in and run” simplicity -## Repo structure (suggested) +# Repo structure A simple layout that stays scalable as you add more mini-tools: - / - easy-mini-tools/ - ExtractCopilotChat/ - Program.cs - ExtractCopilotChat.csproj - README.md - LICENSE +``` +/ + easy-mini-tools/ + ExtractCopilotChat/ + Program.cs + ExtractCopilotChat.slnx + OFX-To-XLSX/ + Program.cs + OFX-To-XLSX.sln + README.md + LICENSE +``` -## Authors +# Authors * **NF Software Inc.** -## License +# License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details