QFX File Format

Thanks for making this library available. As one of the few remaining users of Microsoft Money, I’m constantly having to band-aid things. I needed a way to convert a CSV file that my bank provides into a QFX file, and this gave me the ability to do that.

I was having all kinds of trouble until I realized that OfxResponseDocument.Save appears to be writing the file in UTF-8 BOM, which Money couldn’t handle. It was easy enough to rewrite the file. But I just wanted to provide this for the other poor soul still using MS Money.

var fileContents = File.ReadAllText(qfxFilePath);
File.WriteAllText(qfxFilePath, fileContents, new UTF8Encoding(false));

@onskee,

It appears that you are utilizing the Aspose.Finance for .NET library to convert a CSV file into the QFX file format. Could you please provide more details regarding any issues or limitations you have encountered with Aspose.Finance? Additionally, we would appreciate it if you could share complete information, including sample files (both input and output) and a runnable code example that demonstrates the issue. This will help us investigate the matter and assist you effectively.

Hi, thanks for replying. Sorry I missed the notification.

This is the the full code for the console app. .Net Core v8.0, Windows 11.

using Aspose.Finance.Ofx;
using Aspose.Finance.Ofx.CreditCard;
using Aspose.Finance.Ofx.Signon;
using CsvHelper;
using System.Globalization;
using System.Numerics; 
using System.Text;


if (args != null && args.Length > 0)
{
	string csvFilePath = args[0];
	string qfxFilePath = "Transactions.qfx";
	Console.WriteLine($"Converting {csvFilePath} to {qfxFilePath}");

	GenerateQfx(ReadCsv(csvFilePath), qfxFilePath); 

	// Fix the file to just be in UTF8 instead of UTF8 with BOM
//	var fileContents = File.ReadAllText(qfxFilePath);
//	File.WriteAllText(qfxFilePath, fileContents, new UTF8Encoding(false));
}
else
{
	Console.WriteLine("Usage: CsvToQfx <csv file>");
}

static List<CsvTransaction> ReadCsv(string csvFilePath)
{
	List<CsvTransaction> transactions = new List<CsvTransaction>();
	using (var reader = new StreamReader(csvFilePath))
	{
		using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
		{
			var records = new List<CsvTransaction>();
			csv.Read(); // Read the first row (header row)
			csv.ReadHeader(); // Parse the header row so fields can be accessed by name


			while (csv.Read())
			{
				var transaction = new CsvTransaction()
				{
					Date = csv.GetField<string>("Date"),
					Description = csv.GetField<string>("Description"),
					OriginalDescription = csv.GetField<string>("Original Description"),
					Category = csv.GetField<string>("Category"),
					Amount = csv.GetField<decimal>("Amount"),
					Status = csv.GetField<string>("Status")
				};
				records.Add(transaction);
			}

			Console.WriteLine($"Read {records.Count} transactions from {csvFilePath}");
			return records;
		}
	} 
}
 

static void GenerateQfx(List<CsvTransaction> transactions, string qfxFilePath)
{

	var doc = new OfxResponseDocument();
	doc.SignonResponseMessageSetV1 = new SignonResponseMessageSetV1();
	var signonResponse = new SignonResponse();

	doc.SignonResponseMessageSetV1.SignonResponse = signonResponse;
	signonResponse.Status = new Status();
	signonResponse.Status.Code = "0";
	signonResponse.Status.Severity = SeverityEnum.INFO;
	signonResponse.Status.Message = "SUCCESS";
	signonResponse.Language = LanguageEnum.ENG;
	signonResponse.ServerDate = DateTime.Today.ToString("yyyyMMddHHmmss") + "[0:GMT]";
	//signonResponse.ProfileUpdateDate = DateTime.Today.ToString("yyyyMMdd");
	var fi = new FinancialInstitution();
	fi.Organization = "XYZ";
	fi.FinancialInstitutionId = "67811";
	signonResponse.FinancialInstitution = fi; 

	doc.CreditcardResponseMessageSetV1 = new CreditcardResponseMessageSetV1();
	var stmtResponse = new CreditCardStatementTransactionResponse();
	doc.CreditcardResponseMessageSetV1.CreditCardStatementTransactionResponses.Add(stmtResponse);
		 
	stmtResponse.TransactionUniqueId = "0";
	stmtResponse.Status = new Status();
	stmtResponse.Status.Code = "0";
	stmtResponse.Status.Severity = SeverityEnum.INFO;
	stmtResponse.CreditCardStatementResponse = new CreditCardStatementResponse();
	stmtResponse.CreditCardStatementResponse.Currency  = CurrencyEnum.USD;
	stmtResponse.CreditCardStatementResponse.CreditCardAccountFrom = new CreditCardAccount();
	stmtResponse.CreditCardStatementResponse.CreditCardAccountFrom.AccountId = "4995";

	stmtResponse.CreditCardStatementResponse.BankTransactionList = new BankTransactionList();
	stmtResponse.CreditCardStatementResponse.BankTransactionList.StartDate = DateTime.Parse(transactions.OrderBy(x => x.Date).First().Date).ToString("yyyyMMddHHmmss") + "[0:GMT]";
	stmtResponse.CreditCardStatementResponse.BankTransactionList.EndDate = DateTime.Parse(transactions.OrderByDescending(x => x.Date).First().Date).ToString("yyyyMMddHHmmss") + "[0:GMT]";


	var i = 1;
	foreach (var transaction in transactions.Where(x => x.Status != "Declined")) {

		var tran = new StatementTransaction();
		
		tran.TransactionType = transaction.Amount < 0 ? TransactionEnum.CREDIT : TransactionEnum.DEBIT;
		tran.PostedDate = DateTime.Parse(transaction.Date).ToString("yyyyMMddHHmmss") + "[0:GMT]";
		
		tran.TransactionAmount = (transaction.Amount * -1).ToString("0.00", CultureInfo.InvariantCulture);

		if(transaction.Description.Length > 32) {
			tran.Name = transaction.Description.Substring(0, 32).Trim();
		}
		else
		{
			tran.Name = transaction.Description;
		}
		 

		tran.FinancialInstitutionTransactionId = DateTime.Today.ToString("yyyyMMdd") + i.ToString().PadLeft(3, '0');
		stmtResponse.CreditCardStatementResponse.BankTransactionList.StatementTransactions.Add(tran);
		i++;
	}

	var ledger = new LedgerBalance();
	ledger.BalanceAmount = transactions.Sum(x => x.Amount).ToString(CultureInfo.InvariantCulture);
	ledger.DateAsOf = DateTime.Today.ToString("yyyyMMddHHmmss") + "[0:GMT]";
	stmtResponse.CreditCardStatementResponse.LedgerBalance = ledger;

	var avail = new AvailableBalance();
	avail.BalanceAmount = "20000.00";
	avail.DateAsOf = DateTime.Today.ToString("yyyyMMddHHmmss") + "[0:GMT]";
	stmtResponse.CreditCardStatementResponse.AvailableBalance = avail;

	Console.WriteLine($"Generated {transactions.Count} transactions in {qfxFilePath}");
	doc.Save(qfxFilePath, new SaveOptions() { SaveFormat = SaveFormat.SGML });

}

This is the object that the CSV gets loaded into.


internal class CsvTransaction
{
	public string? Date { get; set; }
	public string? Description { get; set; }
	public string? OriginalDescription { get; set; }
	public string? Category { get; set; }
	public decimal Amount { get; set; }
	public string? Status { get; set; }
}
 

With lines 18, 19 commented out
bom.jpg (34.6 KB)
Uncommenting 18, 19:
utf8.jpg (27.9 KB)
Sample CSV:

Date,Description,Original Description,Category,Amount,Status
2024-09-28,“Test”,“Test”,Category Pending,11.05,Pending

Sample BOM output:
Transactions.zip (614 Bytes)

@onskee,

Thanks for providing us resource files, code snippet and details.

We will soon evaluate your issue using your resource files and get back to you.

@onskee,

After reviewing your code snippet and resource files, it appears there may be no issues with Aspose.Finance. Could you please provide more details on any issues or limitations you’re experiencing with the Aspose.Finance for .NET API?

It seems Aspose.Finance’s OfxResponseDocument.Save functionality addresses your needs, which Microsoft Money could not handle by default. However, after adjusting the source CSV file, MS Money was able to accomplish the task. You only wanted to share this information to benefit other Microsoft Money users, correct?