mirror of
https://github.com/simonmichael/hledger.git
synced 2024-11-09 00:15:48 +03:00
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:
parent
d4545966b5
commit
8412225ba5
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user