convert command for transforming bank CSV exports to ledger format
This commit is contained in:
parent
ae69a216ac
commit
393e7d98d4
@ -34,12 +34,10 @@ getAndAddTransactions l = (do
|
||||
today <- getCurrentDay
|
||||
date <- liftM (fixSmartDate today . fromparse . parse smartdate "" . lowercase)
|
||||
$ askFor "date" (Just $ showDate today)
|
||||
-- cleared' <- askFor "cleared, y/n" (Just "n")
|
||||
-- let cleared = if cleared' == "y" then True else False
|
||||
description <- askFor "description" Nothing
|
||||
ps <- getPostings []
|
||||
let t = nullledgertxn{ltdate=date
|
||||
,ltstatus=False -- cleared
|
||||
,ltstatus=False
|
||||
,ltdescription=description
|
||||
,ltpostings=ps
|
||||
}
|
||||
|
||||
96
ConvertCommand.hs
Normal file
96
ConvertCommand.hs
Normal file
@ -0,0 +1,96 @@
|
||||
{-|
|
||||
|
||||
Convert account data in CSV format (eg downloaded from a bank) to ledger
|
||||
format, and print it on stdout.
|
||||
|
||||
Usage: hledger convert CSVFILE ACCOUNTNAME RULESFILE
|
||||
|
||||
ACCOUNTNAME is the base account to use for transactions. RULESFILE
|
||||
provides some rules to help convert the data. It should contain paragraphs
|
||||
separated by one blank line. The first paragraph is a single line of five
|
||||
comma-separated numbers, which are the csv field positions corresponding
|
||||
to the ledger transaction's date, status, code, description, and amount.
|
||||
All other paragraphs specify one or more regular expressions, followed by
|
||||
the ledger account to use when a transaction's description matches any of
|
||||
them. Here's an example rules file:
|
||||
|
||||
> 0,2,3,4,1
|
||||
>
|
||||
> ATM DEPOSIT
|
||||
> assets:bank:checking
|
||||
>
|
||||
> (TO|FROM) SAVINGS
|
||||
> assets:bank:savings
|
||||
>
|
||||
> ITUNES
|
||||
> BLOCKBUSTER
|
||||
> expenses:entertainment
|
||||
|
||||
Roadmap:
|
||||
Support for other formats will be added. To update a ledger file, pipe the
|
||||
output into the import command. The rules will move to a hledger config
|
||||
file. When no rule matches, accounts will be guessed based on similarity
|
||||
to descriptions in the current ledger, with interactive prompting and
|
||||
optional rule saving.
|
||||
|
||||
-}
|
||||
|
||||
module ConvertCommand where
|
||||
import Data.Maybe (isJust)
|
||||
import Data.List.Split (splitOn)
|
||||
import Options (Opt)
|
||||
import Ledger.Types (Ledger)
|
||||
import Ledger.Utils (strip)
|
||||
import System (getArgs)
|
||||
import Text.CSV (parseCSVFromFile, Record)
|
||||
import Text.Printf (printf)
|
||||
import Text.Regex.PCRE ((=~))
|
||||
import Data.Maybe
|
||||
import Ledger.Dates (firstJust, showDate)
|
||||
import System.Locale (defaultTimeLocale)
|
||||
import Data.Time.Format (parseTime)
|
||||
import Control.Monad (when)
|
||||
|
||||
|
||||
convert :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
convert opts args l = do
|
||||
when (length args /= 3) (error "please specify a csv file, base account, and import rules file.")
|
||||
let [csvfile,baseacct,rulesfile] = args
|
||||
rulesstr <- readFile rulesfile
|
||||
(fieldpositions,rules) <- parseRules rulesstr
|
||||
parse <- parseCSVFromFile csvfile
|
||||
let records = case parse of
|
||||
Left e -> error $ show e
|
||||
Right rs -> reverse rs
|
||||
mapM_ (print_ledger_txn (baseacct,fieldpositions,rules)) records
|
||||
|
||||
parseRules s = do
|
||||
let ls = map strip $ lines s
|
||||
let paras = splitOn [""] ls
|
||||
let fieldpositions = map read $ splitOn "," $ head $ head paras
|
||||
let rules = [(last p,init p) | p <- tail paras]
|
||||
return (fieldpositions,rules)
|
||||
|
||||
print_ledger_txn (baseacct,fieldpositions,rules) record@(a:b:c:d:e) = do
|
||||
let [date,cleared,number,description,amount] = map (record !!) fieldpositions
|
||||
amount' = strnegate amount where strnegate ('-':s) = s
|
||||
strnegate s = '-':s
|
||||
unknownacct | (read amount' :: Double) < 0 = "income:unknown"
|
||||
| otherwise = "expenses:unknown"
|
||||
putStrLn $ printf "%s%s %s" (fixdate date) (if not (null number) then printf " (%s)" number else "") description
|
||||
putStrLn $ printf " %-30s %15s" (fromMaybe unknownacct $ choose_acct rules description) (printf "$%s" amount' :: String)
|
||||
putStrLn $ printf " %s\n" baseacct
|
||||
print_ledger_txn _ _ = return ()
|
||||
|
||||
choose_acct rules description | null matches = Nothing
|
||||
| otherwise = Just $ fst $ head $ matches
|
||||
where matches = filter (any (description =~) . snd) rules
|
||||
|
||||
fixdate :: String -> String
|
||||
fixdate s = maybe "0000/00/00" showDate $
|
||||
firstJust
|
||||
[parseTime defaultTimeLocale "%Y/%m/%d" s
|
||||
,parseTime defaultTimeLocale "%Y-%m-%d" s
|
||||
,parseTime defaultTimeLocale "%m/%d/%Y" s
|
||||
,parseTime defaultTimeLocale "%m-%d-%Y" s
|
||||
]
|
||||
12
Options.hs
12
Options.hs
@ -25,15 +25,17 @@ timeprogname = "hours"
|
||||
usagehdr = (
|
||||
"Usage: hledger [OPTIONS] [COMMAND [PATTERNS]]\n" ++
|
||||
" hours [OPTIONS] [COMMAND [PATTERNS]]\n" ++
|
||||
" hledger convert CSVFILE ACCOUNTNAME RULESFILE\n" ++
|
||||
"\n" ++
|
||||
"When invoked as \"hours\", uses your timelog and --period today as defaults.\n" ++
|
||||
"\n" ++
|
||||
"COMMAND is one of (may be abbreviated):\n" ++
|
||||
" add - read new transactions interactively\n" ++
|
||||
" balance - show account balances\n" ++
|
||||
" histogram - show transaction counts per reporting interval\n" ++
|
||||
" print - show transactions as formatted data\n" ++
|
||||
" register - show transactions as a register\n" ++
|
||||
" add - prompt for new transactions and add them to the ledger\n" ++
|
||||
" balance - show accounts, with balances\n" ++
|
||||
" convert - convert CSV data to ledger format and print on stdout\n" ++
|
||||
" histogram - show transaction counts per day or other interval\n" ++
|
||||
" print - show transactions in ledger format\n" ++
|
||||
" register - show transactions as a register with running balance\n" ++
|
||||
#ifdef VTY
|
||||
" ui - run a simple curses-based text ui\n" ++
|
||||
#endif
|
||||
|
||||
@ -52,7 +52,8 @@ Executable hledger
|
||||
|
||||
Build-Depends: base, containers, haskell98, directory, parsec,
|
||||
regex-compat, regexpr>=0.5.1, old-locale, time,
|
||||
HUnit, mtl, bytestring, filepath, process, testpack
|
||||
HUnit, mtl, bytestring, filepath, process, testpack,
|
||||
regex-pcre, csv, split
|
||||
|
||||
Other-Modules:
|
||||
BalanceCommand
|
||||
|
||||
@ -38,6 +38,7 @@ module Main (
|
||||
module Utils,
|
||||
module Options,
|
||||
module BalanceCommand,
|
||||
module ConvertCommand,
|
||||
module PrintCommand,
|
||||
module RegisterCommand,
|
||||
module HistogramCommand,
|
||||
@ -60,6 +61,7 @@ import Utils (withLedgerDo)
|
||||
import Options
|
||||
import Tests
|
||||
import BalanceCommand
|
||||
import ConvertCommand
|
||||
import PrintCommand
|
||||
import RegisterCommand
|
||||
import HistogramCommand
|
||||
@ -81,6 +83,7 @@ main = do
|
||||
| Help `elem` opts = putStr $ usage
|
||||
| Version `elem` opts = putStr versionmsg
|
||||
| cmd `isPrefixOf` "balance" = withLedgerDo opts args balance
|
||||
| cmd `isPrefixOf` "convert" = withLedgerDo opts args convert
|
||||
| cmd `isPrefixOf` "print" = withLedgerDo opts args print'
|
||||
| cmd `isPrefixOf` "register" = withLedgerDo opts args register
|
||||
| cmd `isPrefixOf` "histogram" = withLedgerDo opts args histogram
|
||||
|
||||
Loading…
Reference in New Issue
Block a user