add-in-and-out-field-rules-for-convert

* Support in-field and out-field for CSV files that use two columns for each kind of movement.
This commit is contained in:
Trygve Laugstøl 2011-05-29 21:50:59 +00:00
parent d4545966b5
commit 8412225ba5
2 changed files with 58 additions and 3 deletions

View File

@ -563,6 +563,13 @@ This says:
- if description contains TO SAVINGS or FROM SAVINGS, the - if description contains TO SAVINGS or FROM SAVINGS, the
transaction is a savings transfer transaction is a savings transfer
If your bank or external system uses two different columns for in and
out movements, use the `in-field` and `out-field` rules instead of
`amount-field`.
Note that the numbers are assumed to be positive, implying that an "out"
movement gets recorded as a transaction with a negative amount.
Notes: Notes:
- Lines beginning with ; or \# are ignored (but avoid using inside an - Lines beginning with ; or \# are ignored (but avoid using inside an

View File

@ -8,7 +8,6 @@ import Prelude hiding (getContents)
import Control.Monad (when, guard, liftM) import Control.Monad (when, guard, liftM)
import Data.Maybe import Data.Maybe
import Data.Time.Format (parseTime) import Data.Time.Format (parseTime)
import Hledger.Data.Dates (firstJust, showDate, parsedate)
import Safe (atDef, maximumDef) import Safe (atDef, maximumDef)
import Safe (readDef, readMay) import Safe (readDef, readMay)
import System.Directory (doesFileExist) import System.Directory (doesFileExist)
@ -23,8 +22,9 @@ import Text.Printf (hPrintf)
import Hledger.Cli.Options (Opt(Debug), progname_cli, rulesFileFromOpts) import Hledger.Cli.Options (Opt(Debug), progname_cli, rulesFileFromOpts)
import Hledger.Cli.Version (progversionstr) import Hledger.Cli.Version (progversionstr)
import Hledger.Data (Journal,AccountName,Transaction(..),Posting(..),PostingType(..))
import Hledger.Data.Amount (nullmixedamt, costOfMixedAmount) import Hledger.Data.Amount (nullmixedamt, costOfMixedAmount)
import Hledger.Data.Dates (firstJust, showDate, parsedate)
import Hledger.Data (Journal,AccountName,Transaction(..),Posting(..),PostingType(..))
import Hledger.Data.Journal (nullctx) import Hledger.Data.Journal (nullctx)
import Hledger.Read.JournalReader (someamount,ledgeraccountname) import Hledger.Read.JournalReader (someamount,ledgeraccountname)
import Hledger.Utils (strip, spacenonewline, restofline, parseWithCtx, assertParse, assertParseEqual, error', regexMatchesCI, regexReplaceCI) import Hledger.Utils (strip, spacenonewline, restofline, parseWithCtx, assertParse, assertParseEqual, error', regexMatchesCI, regexReplaceCI)
@ -41,6 +41,8 @@ data CsvRules = CsvRules {
codeField :: Maybe FieldPosition, codeField :: Maybe FieldPosition,
descriptionField :: Maybe FieldPosition, descriptionField :: Maybe FieldPosition,
amountField :: Maybe FieldPosition, amountField :: Maybe FieldPosition,
inField :: Maybe FieldPosition,
outField :: Maybe FieldPosition,
currencyField :: Maybe FieldPosition, currencyField :: Maybe FieldPosition,
baseCurrency :: Maybe String, baseCurrency :: Maybe String,
accountField :: Maybe FieldPosition, accountField :: Maybe FieldPosition,
@ -56,6 +58,8 @@ nullrules = CsvRules {
codeField=Nothing, codeField=Nothing,
descriptionField=Nothing, descriptionField=Nothing,
amountField=Nothing, amountField=Nothing,
inField=Nothing,
outField=Nothing,
currencyField=Nothing, currencyField=Nothing,
baseCurrency=Nothing, baseCurrency=Nothing,
accountField=Nothing, accountField=Nothing,
@ -97,7 +101,9 @@ convert opts args _ = do
else else
hPrintf stderr "using conversion rules file %s\n" rulesfile hPrintf stderr "using conversion rules file %s\n" rulesfile
rules <- liftM (either (error'.show) id) $ parseCsvRulesFile rulesfile rules <- liftM (either (error'.show) id) $ parseCsvRulesFile rulesfile
let invalid = validateRules rules
when debug $ hPrintf stderr "rules: %s\n" (show rules) when debug $ hPrintf stderr "rules: %s\n" (show rules)
when (isJust invalid) $ error (fromJust invalid)
let requiredfields = max 2 (maxFieldIndex rules + 1) let requiredfields = max 2 (maxFieldIndex rules + 1)
badrecords = take 1 $ filter ((< requiredfields).length) records badrecords = take 1 $ filter ((< requiredfields).length) records
if null badrecords if null badrecords
@ -125,6 +131,8 @@ maxFieldIndex r = maximumDef (-1) $ catMaybes [
,codeField r ,codeField r
,descriptionField r ,descriptionField r
,amountField r ,amountField r
,inField r
,outField r
,currencyField r ,currencyField r
,accountField r ,accountField r
,effectiveDateField r ,effectiveDateField r
@ -162,6 +170,18 @@ initialRulesFileContent =
"(TO|FROM) SAVINGS\n" ++ "(TO|FROM) SAVINGS\n" ++
"assets:bank:savings\n" "assets:bank:savings\n"
validateRules :: CsvRules -> Maybe String
validateRules rules = let
hasAccount = isJust $ accountField rules
hasIn = isJust $ inField rules
hasOut = isJust $ outField rules
in case (hasAccount, hasIn, hasOut) of
(True, True, _) -> Just "Don't specify in-field when specifying amount-field"
(True, _, True) -> Just "Don't specify out-field when specifying amount-field"
(_, False, True) -> Just "You have to specify in-field when specifying out-field"
(_, True, False) -> Just "You have to specify out-field when specifying in-field"
_ -> Nothing
-- rules file parser -- rules file parser
parseCsvRulesFile :: FilePath -> IO (Either ParseError CsvRules) parseCsvRulesFile :: FilePath -> IO (Either ParseError CsvRules)
@ -194,6 +214,8 @@ definitions = do
,codefield ,codefield
,descriptionfield ,descriptionfield
,amountfield ,amountfield
,infield
,outfield
,currencyfield ,currencyfield
,accountfield ,accountfield
,effectivedatefield ,effectivedatefield
@ -252,6 +274,20 @@ amountfield = do
r <- getState r <- getState
setState r{amountField=readMay v} setState r{amountField=readMay v}
infield = do
string "in-field"
many1 spacenonewline
v <- restofline
r <- getState
setState r{inField=readMay v}
outfield = do
string "out-field"
many1 spacenonewline
v <- restofline
r <- getState
setState r{outField=readMay v}
currencyfield = do currencyfield = do
string "currency-field" string "currency-field"
many1 spacenonewline many1 spacenonewline
@ -329,7 +365,7 @@ transactionFromCsvRecord rules fields =
comment = "" comment = ""
precomment = "" precomment = ""
baseacc = maybe (baseAccount rules) (atDef "" fields) (accountField rules) baseacc = maybe (baseAccount rules) (atDef "" fields) (accountField rules)
amountstr = maybe "" (atDef "" fields) (amountField rules) amountstr = getAmount rules fields
amountstr' = strnegate amountstr where strnegate ('-':s) = s amountstr' = strnegate amountstr where strnegate ('-':s) = s
strnegate s = '-':s strnegate s = '-':s
currency = maybe (fromMaybe "" $ baseCurrency rules) (atDef "" fields) (currencyField rules) currency = maybe (fromMaybe "" $ baseCurrency rules) (atDef "" fields) (currencyField rules)
@ -407,6 +443,18 @@ identify rules defacct desc | null matchingrules = (defacct,desc)
caseinsensitive = ("(?i)"++) caseinsensitive = ("(?i)"++)
getAmount :: CsvRules -> CsvRecord -> String
getAmount rules fields = case (accountField rules) of
Just f -> maybe "" (atDef "" fields) $ Just f
Nothing ->
case (c, d) of
(x, "") -> x
("", x) -> "-"++x
_ -> ""
where
c = maybe "" (atDef "" fields) (inField rules)
d = maybe "" (atDef "" fields) (outField rules)
tests_Hledger_Cli_Convert = TestList [ tests_Hledger_Cli_Convert = TestList [
"convert rules parsing: empty file" ~: do "convert rules parsing: empty file" ~: do