{-# LANGUAGE OverloadedStrings #-}
{- Rabobank - Convert Rabobank CSV exports to QIF files
Copyright (C) 2012 Sander Venema <sander.venema@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. -}
-- |The module Rabobank has utilities to take a Rabobank CSV export file of
-- transactions, and transforming this into a QIF file ready for import into
-- your accounting software. If your accounting software can't import Rabobank
-- CSV files, most will be able to deal with QIF files.
--
-- The QIF file generated by this module will have the bare minimum amount of
-- information, namely a description, amount of money to be credited or
-- debited, account number, and the transaction date.
--
-- This module exports two functions namely 'toQif', which is the main function
-- that will take a ByteString of Rabobank CSV data and returns a ByteString with QIF
-- data, and 'baseName', which will generate a filename for the export file
-- based on current date and time.
module Rabobank (toQif, baseName) where
import ListUtils (takeWhileIndex)
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.ByteString.Conversion as B
import Data.Char (chr)
import Data.List (intercalate)
import Data.List.Split (splitPlaces)
import Data.Maybe (fromJust, isJust)
import Data.Time.Clock (getCurrentTime)
import Data.Time.Format (formatTime, defaultTimeLocale)
import Text.Printf (printf)
-- |'Transaction' is the internal data structure used to represent a
-- transaction. CSV data is turned into Transactions, and then, using the
-- accessor methods converted to a QIF representation.
data Transaction = Transaction { date :: BL.ByteString -- ^ The transaction date
, amount :: Double -- ^ The amount of the transaction
, description :: BL.ByteString -- ^ The transaction description
, payee :: BL.ByteString -- ^ The payee
, accountNumber :: BL.ByteString -- ^ The account number
} deriving (Show)
-- |'TransactionType' records whether a 'Transaction' has to be debited or
-- credited. We use this data structure in the 'creditDebit' function to
-- determine whether a transaction amount needs to be positive or negative. The
-- 'toTransactionType' function turns a ByteString containing "D" or "C" into a
-- 'TransactionType'.
data TransactionType = D -- ^ Debit
| C -- ^ Credit
deriving (Show, Eq)
-- |This function turns a string containing Rabobank CSV data to a string
-- containing QIF data, ready for writing to stdout or to a file.
toQif :: BL.ByteString -> BL.ByteString
toQif file = BL.append qifHeader $ BL.unlines (map transactionToQif (fileToTransactions records))
where rows = BL.lines $ BL.filter (/= '"') file
rawRecords = map (BL.split ',') rows
records = map (takeWhileIndex [2,3,4,5,6,10,11,12,13,14,15])
(filter (\x -> length x == 19) rawRecords)
-- |Turns a list of a list of list of a list of 'ByteString's into a list of
-- 'Transaction's.
fileToTransactions :: [[BL.ByteString]] -> [Transaction]
fileToTransactions = map fromJust . filter isJust . map rowToTransaction
-- |'transactionToQif' will turn a 'Transaction' into a 'ByteString' suitable
-- for QIF files. We simply call the accessor methods of the 'Transaction'
-- type, prefix them with the appropriate prefix letters as required by the QIF
-- specification, and then intercalate then with newlines, thereby returning a
-- 'ByteString' with this data, separated by newlines, just what we need.
transactionToQif :: Transaction -> BL.ByteString
transactionToQif t = BL.intercalate "\n" [
BL.cons 'P' (payee t), BL.cons 'M' (description t), BL.cons 'N' (accountNumber t),
BL.cons 'D' (date t), BL.cons 'T' (BL.pack (printf "%.2f" (amount t)))] `BL.append` "\n^"
-- |'rowToTransaction' turns a list of 'ByteString's to a 'Transaction'. It will
-- apply all the necessary corrections and reformatting necessary to turn a row
-- into a correct 'Transaction' for use with 'transactionToQif'.
rowToTransaction :: [BL.ByteString] -> Maybe Transaction
rowToTransaction
(date:debitcredit:amount:accountNumber:payee:descriptionfields) =
maybe Nothing
(\correctAmount -> Just (Transaction correctDate correctAmount correctDescription payee accountNumber))
maybeAmount
where correctDate = BL.pack $ intercalate "/" $ splitPlaces [4,2,2] $ BL.unpack date
maybeAmount = maybe Nothing (\amount ->
Just (creditDebit amount (toTransactionType debitcredit)))
(B.fromByteString' amount :: Maybe Double)
correctDescription = BL.intercalate ", " descriptionfields
rowToTransaction _ = Nothing
-- |Helper function to turn a 'ByteString' containing "D" or something else into a
-- 'TransactionType'. This is used for 'creditDebit' to determine if an amount
-- should be positive (added to the account) or negative (subtracted from the
-- account.
toTransactionType :: BL.ByteString -> TransactionType
toTransactionType c = if c == "D" then D else C
-- |'creditDebit' will make the given amount negative if 'transactiontype' == D
-- (for Debet), otherwise it will leave it untouched.
creditDebit :: Double -> TransactionType -> Double
creditDebit amount transactiontype =
if transactiontype == D then -1 * amount else amount
-- |'baseName' gets the current date and time and returns an 'IO FilePath' with the
-- timestamp and 'Rabobank_' as the prefix. This will be used as the filename
-- (minus extension) for the exported QIF file.
baseName :: IO FilePath
baseName = do
now <- getCurrentTime
return $ "Rabobank_" ++ (formatTime defaultTimeLocale "%Y%m%d%H%M%S" now) ++ ".qif"
-- |'qifHeader' returns the header at the start of the QIF file.
qifHeader :: BL.ByteString
qifHeader = BL.pack "!Type:Bank\n"