@@ -13,6 +13,7 @@ import (
1313 "github.com/howeyc/ledger"
1414 "github.com/howeyc/ledger/decimal"
1515 "github.com/howeyc/ledger/ledger/camt"
16+ "github.com/howeyc/ledger/ledger/qfx"
1617 "github.com/jbrukh/bayesian"
1718 "github.com/spf13/cobra"
1819)
@@ -222,6 +223,11 @@ func importCamt(accountSubstring, camtFileName string) {
222223 classifier := trainClassifier (generalLedger , matchingAccount )
223224
224225 entries , err := camt .ParseCamt (fileReader )
226+ if err != nil {
227+ fmt .Println ("CAMT parse error:" , err .Error ())
228+ return
229+ }
230+
225231 expenseAccount := ledger.Account {Name : "unknown:unknown" , Balance : decimal .Zero }
226232 camtAccount := ledger.Account {Name : matchingAccount , Balance : decimal .Zero }
227233 for _ , entry := range entries {
@@ -280,6 +286,82 @@ func importCamt(accountSubstring, camtFileName string) {
280286 }
281287}
282288
289+ func importQFX (accountSubstring , qfxFileName string ) {
290+ decScale := decimal .NewFromFloat (scaleFactor )
291+
292+ fileReader , err := os .Open (qfxFileName )
293+ if err != nil {
294+ fmt .Println ("QFX: " , err , qfxFileName )
295+ return
296+ }
297+ defer fileReader .Close ()
298+
299+ generalLedger , parseError := ledger .ParseLedgerFile (ledgerFilePath )
300+ if parseError != nil {
301+ fmt .Printf ("%s:%s\n " , ledgerFilePath , parseError .Error ())
302+ return
303+ }
304+
305+ matchingAccount , err := findMatchingAccount (generalLedger , accountSubstring )
306+ if err != nil {
307+ fmt .Println (err )
308+ return
309+ }
310+
311+ classifier := trainClassifier (generalLedger , matchingAccount )
312+
313+ entries , err := qfx .ParseQFX (fileReader )
314+ if err != nil {
315+ fmt .Println ("QFX parse error:" , err .Error ())
316+ return
317+ }
318+
319+ expenseAccount := ledger.Account {Name : "unknown:unknown" , Balance : decimal .Zero }
320+ qfxAccount := ledger.Account {Name : matchingAccount , Balance : decimal .Zero }
321+ for _ , entry := range entries {
322+ // QFX DTPOSTED is typically YYYYMMDDHHMMSS.XXX; we only care about the date.
323+ // Take the first 8 characters as YYYYMMDD.
324+ dateStr := entry .DtPosted
325+ if len (dateStr ) >= 8 {
326+ dateStr = dateStr [:8 ]
327+ }
328+ dateTime , err := time .Parse ("20060102" , dateStr )
329+ if err != nil {
330+ fmt .Println ("QFX date parse error:" , err .Error ())
331+ continue
332+ }
333+
334+ // Parse amount
335+ amount , err := decimal .NewFromString (entry .TrnAmt )
336+ if err != nil {
337+ fmt .Println ("QFX amount parse error:" , err .Error ())
338+ continue
339+ }
340+
341+ payee := entry .Memo
342+ inputPayeeWords := strings .Fields (payee )
343+
344+ expenseAccount .Name = predictAccount (classifier , inputPayeeWords )
345+ expenseAccount .Balance = amount
346+
347+ // Apply scale
348+ expenseAccount .Balance = expenseAccount .Balance .Mul (decScale )
349+
350+ // Account side is the opposite of expense
351+ qfxAccount .Balance = expenseAccount .Balance .Neg ()
352+
353+ // Create valid transaction for print in ledger format
354+ trans := & ledger.Transaction {Date : dateTime , Payee : payee }
355+ trans .AccountChanges = []ledger.Account {qfxAccount , expenseAccount }
356+
357+ // Comment with FITID if present
358+ if entry .FitID != "" {
359+ trans .Comments = []string {";" + entry .FitID }
360+ }
361+ WriteTransaction (os .Stdout , trans , 80 )
362+ }
363+ }
364+
283365// importCmd represents the import command
284366var importCmd = & cobra.Command {
285367 Use : "import <account-substring> <csv-file>" ,
@@ -289,8 +371,11 @@ var importCmd = &cobra.Command{
289371 accountSubstring := args [0 ]
290372 fileName := args [1 ]
291373
292- if strings .HasSuffix (strings .ToLower (fileName ), ".xml" ) {
374+ lower := strings .ToLower (fileName )
375+ if strings .HasSuffix (lower , ".xml" ) {
293376 importCamt (accountSubstring , fileName )
377+ } else if strings .HasSuffix (lower , ".qfx" ) || strings .HasSuffix (lower , ".ofx" ) {
378+ importQFX (accountSubstring , fileName )
294379 } else {
295380 importCSV (accountSubstring , fileName )
296381 }
0 commit comments