#ofx2csv.tcl# -*- tcl -*-# The next line is executed by /bin/sh, but not tcl \exec wish "$0" ${1+"$@"}# This program will process a OFX 1.x file that can typically be# obtained from a bank or credit card.# The object of the exercise is to produce a CSV type file from# the transactions contained in the OFX file. # I have modeled the output to be something like I get from# processing a QIF file from Quicken. ie I have fields# Date Amount Tranid Source Memo Status Category# The first five fields can be generally be extracted from the# OFX file. Note that the Memo field might be empty or contain# overfill from the source field or some actual info.# I always use an "X" for the status to indicate a cleared# transaction.# The bank/credit card cannot assign a category so the user# has to supply this field. Note that the code always uses# "UNKNOWN" as the initial contents of this field.# I use the "Category" field as a foreign key when I insert the# CSV field into my Postgresql Database so it is important that# the value actually matches an element in the appropriate table.# To prevent mistakes I have placed all of the choices for the# category field into a listbox.# The basic strategy is that the user will double click one of the UNKNOWN# fields to select it and then double click the appropriate category# to use from the listbox of choices. This will cause the UNKNOWN# value to be replaced with the users choice of category.# After editing the CSV file the user can then save the file and# later import the CSV file into their favorite database.# Enjoy!# Author: Jerry LeVan# Env: Recent TCL/TK# Date: Feb 18, 2006# Version: 0.7# Version: 0.8 Feb 2, 2007# This is free ware use as you like (but keep my name...)# Possible transaction tags# Value = Enum [CREDIT | DEBIT | INT | DIV | FEE | SRVCHG | DEP#| ATM | POS | XFER | CHECK | PAYMENT | CASH | DIRECTDEP | DIRECTDEBIT | REPEATPMT | OTHER]package require Tk
package require Tktable
set version 0.8set sep |# items cannot contain white space, hence the quotesset menuitems {
Auto
Auto:Fuel
Auto:Service
"Bank Charges"
Charity
"Check Deposit""[Checking Bank One]"
Clothing
Computer
"Credit Card""Customer Deposit"
Dining
Education
Entertainment
Gifts
Groceries
"Home Repair""Home Repair:Yard Work"
Household
Insurance:Auto
Insurance:Home
"Interest Earned""Interest Paid"[MasterCard-Marijo]
Medical
Miscellaneous
"Mortgage Int"
Pension
Recreation
Salary
"[Savings at Bank One]"
Taxes
Taxes:Federal
Taxes:Other
Taxes:Property
Taxes:State
Telephone
Travel
Utilities
"Utilities:Cable TV""Utilities:Gas & Electric"
Utilities:Water
"[Visa - Jerry]"}# Change values to be more in line with Quickenproc cleanUp { key value }{set thevalue $valueif{$key eq "DTPOSTED"}{ # fiddle with the dateset myDate [string range $value07]set thevalue ""append thevalue [string range $myDate03]-[string range $myDate45]-[string range $myDate67]}if{$key eq "TRNTYPE"&&$value eq "CREDIT"}{set thevalue "DEP"}if{$key eq "TRNTYPE"&&$value eq "DEBIT"}{set thevalue "WITHD"}if{$key eq "TRNAMT"}{set thevalue [format"%.2f"$value]}return$thevalue}# Generate and process one complete transactionproc processOne { trans row}{global output sep table_data
array set items {
date "" amount "" tranid ""source"" memo "NONE" status "X" category "UNKNOWN"}set items(row)[expr{$row+1}]foreach obj $trans{ #puts $objregexp{.*<(.*)>(.*)\}*}$obj all key value
set value [string trim $value]set value [cleanUp $key$value]switch-exact --$key{
TRNTYPE {set items(tranid)$value}
DTPOSTED {set items(date)$value}
TRNAMT {set items(amount)$value}
NAME {set items(source)$value}
MEMO {set items(memo)$value}
CHECKNUM {set items(tranid)$value}}}set col -1foreach var {row date amount tranid source memo status category}{set table_data($row,$col)$items($var)incr col
}}# Split the list of tranactions and process each oneproc analyzeOneThing { transaction row }{ # split on new lineset tranlist [split$transaction\n ] # pitch clutter of first tag, last tag set tranlist [lrange$tranlist1 end-1]foreach item [list$tranlist]{
processOne $item$row}}# Get the OFX file to process.proc openFile {}{set fileName [tk_getOpenFile -parent .frm.t]if{$fileName!=""}{set f [open$fileName r]set theFile [read$f]close$freturn[list$fileName$theFile]}return""}# Save the contents of the Text Widget to a file.proc saveFile {}{global table_data sep
set fname [tk_getSaveFile -initialdir "~"\-defaultextension ".csv"]if{$fname!=""}{if{[catch{set f [open$fname"w"]set rows [.frm.t cget -rows]set cols [.frm.t cget -cols] #puts "$rows $cols"incr rows -2incr cols -2for{set j 0}{$j<=$rows}{incr j}{set line ""for{set i 0}{$i<=$cols}{incr i }{if{$i==$cols}{append line $table_data($j,$i)}else{append line $table_data($j,$i)$sep}}puts$f$line}close$f
tk_messageBox -message"Write Completed Ok."-icon info} result]}{
tk_messageBox -message"File Save of $fname failed. Reason : $result"-type ok
}}}# Get the file to process, extract the transactions and process each one...proc processFile {}{global table_data
set tmp [openFile]if{$tmp==""}{exit}set f [lindex$tmp0]set theFile [lindex$tmp1]focus-force .frm.t
# Empty the text widget#.frm.t delete 1.0 end## Break out the list of transactions...#set items [regexp-all -inline {<STMTTRN>.*?</STMTTRN>}$theFile]if{$items==""}{
tk_messageBox -message"No Transactions Found in $f"-type ok -icon info}set rowCnt [llength$items].frm.t configure -rows [incr rowCnt]
array set table_data {-1 date -1 amount -1 tranid -1source-1 memo -1 status -1 category }set row 0foreach chunk $items{
analyzeOneThing $chunk$rowincr row
}}proc make_table { parent }{global table_data
global menuitems
global colHeaders
table $parent.t \-variable table_data \-yscrollcommand "$parent.sy set"\-xscrollcommand "$parent.sx set"\-titlerows 1\-titlecols 1\-roworigin -1\-colorigin -1\-colstretchmode last \-invertselected 0\-selectmode browse \-selecttype cell \-padx 4\-coltagcommand colproc \-autoclear 1\-cols 8-rows 10$parent.t config -bg white
scrollbar$parent.sx -orient horizontal -command [list$parent.t xview]scrollbar$parent.sy -command " $parent.t yview"set colnum 0foreach header $colHeaders{set table_data(-1,$colnum)$headerincr colnum
}$parent.t tag config title -bg lightblue
$parent.t tag config title -fg black
$parent.t tag configure sel -bg blue
$parent.t tag configure sel -fg red
$parent.t tag config anchorcenter -anchor c
$parent.t tag config anchorwest -anchor w
$parent.t tag config anchoreast -anchor e
$parent.t tag col anchoreast -1$parent.t tag row anchorcenter -1$parent.t width -15 # build a popup menu...menu$parent.t.menu-tearoff 0set cnt 0foreach thing $menuitems{incr cnt
if{$cnt==20}{set brk 1;set cnt 0}else{set brk 0}$parent.t.menu add command -columnbreak $brk-label"$thing"\-command [list setItem $thing]}return$parent.t
}proc setItem newString {global lastx lasty
.frm.t set[.frm.t index @$lastx,$lasty]$newString}proc colproc {col}{if{$col==-1||$col==1}{return anchoreast
}else{return anchorwest }}# Build the text widget for displaying the results as a CSV fileproc BuildGui {}{global menuitems categoryCol lastx lasty version
# Create a "menu bar"menu.menubar
# make it the menu for the application window. config -menu.menubar
# create the File menumenu.menubar.file-tearoff 0 # add this menu to the menubar.menubar add cascade -label"File"-menu.menubar.file # add the menu items with actions # add a scripts menumenu.menubar.replace -tearoff 0.menubar add cascade -label"Category Choices"-underline 0-menu.menubar.replace
# build a table object and position it in the frameset f [frame.frm ]set theTable [make_table $f]pack$f-fill both -expand yes
pack.frm.sx -side bottom -fill x
pack.frm.sy -side right -fill y
pack$theTable-side top -fill both -expand yes
# populate the menus.menubar.replace add command -label"Replace With..."-command showReplaceInfo
.menubar.file add command -label"Open OFX File..."-command processFile
.menubar.file add command -label"Save As..."-command saveFile
.menubar.file add command -label"Quit"-command exit #Support the MouseWheelbind.frm.t <Button-4>{.frm.t yview scroll -5 units }bind.frm.t <Button-5>{.frm.t yview scroll +5 units }bind.frm.t <MouseWheel>{if{%D <0}{.frm.t yview scroll +5 units
}else{.frm.t yview scroll -5 units
}} # bind the popup menubind.frm.t <Control-Button-1>{if{[.frm.t index @%x,%y col ]!=$categoryCol}returnset lastx %x
set lasty %y
tk_popup .frm.t.menu%X %Y
#puts "x=%x, y=%y\nX=%X, Y=%Y\n cell: [.frm.t get @%x,%y]\n @%x,%y\n" #puts "row: [.frm.t index @%x,%y]\n"}
wm title ."Ofx2Csv $version"}# Action routine to replace field in Text Widjet with selected# field in the list box.proc changeItem {theTable }{global table_data
# Get the selected itemset tabIndex [$theTable curselection]if{$tabIndex==""}{
tk_messageBox -title OFX2CSV \-message"No Replacement Selected."\-type ok \-icon errorreturn}set theItem [$theTable get $tabIndex]set theCell [.frm.t tag cell sel ]if{$theCell ne ""}{set table_data($theCell)$theItemfocus-force .frm.t
}else{
tk_messageBox -title OFX2CSV \-message"Nothing Selected in CSV File"\-type ok -icon error}}# Build and display the list box which contains all of# the foreign keys.# Not really needed anymore...proc showReplaceInfo {}{global menuitems
destroy.showList
toplevel .showList
listbox.showList.choices -width 30-height 10\-borderwidth 3\-exportselection 0\-relief groove \-yscrollcommand {.showList.sy set}scrollbar.showList.sy -command [list.showList.choices yview]frame.showList.bframe
pack.showList.bframe -side bottom
button.showList.bframe.showtab -text"Edit Item"\-command [list changeItem .showList.choices ]button.showList.bframe.exit-text"Exit Edit Selection"-command {destroy.showList }pack.showList.bframe.showtab -side left
pack.showList.bframe.exit-side left
pack.showList.choices -side left -expand true -fill both -padx 4-pady 4pack.showList.sy -side right -fill y -padx 4bind.showList.choices <Double-1>[list changeItem .showList.choices ] # load the listboxforeach item $menuitems{.showList.choices insert end $item}}#### Start Here ##### table headersset colHeaders { date amount tranid source memo status category }# This is the column we have to be careful about# and which the popup menu operates.set categoryCol 6# lastx and lasty will be the coordinates of the button click# that summons the popup menu.set lastx 0set lasty 0# Set up the interface
BuildGui