module Models -- data types & behaviours where import Text.Printf import Data.List -- types data Ledger = Ledger { modifier_entries :: [ModifierEntry], periodic_entries :: [PeriodicEntry], entries :: [Entry] } deriving (Eq) data ModifierEntry = ModifierEntry { -- aka "automated entry" valueexpr :: String, m_transactions :: [Transaction] } deriving (Eq) data PeriodicEntry = PeriodicEntry { periodexpr :: String, p_transactions :: [Transaction] } deriving (Eq) data Entry = Entry { date :: Date, status :: Status, code :: String, description :: String, transactions :: [Transaction] } deriving (Eq) data Transaction = Transaction { account :: Account, amount :: Amount } deriving (Eq) data Amount = Amount { currency :: String, quantity :: Double } deriving (Eq) type Date = String type Status = Bool type Account = String -- Amount arithmetic - ignores currency conversion instance Num Amount where abs (Amount c q) = Amount c (abs q) signum (Amount c q) = Amount c (signum q) fromInteger i = Amount "$" (fromInteger i) (+) = amountAdd (-) = amountSub (*) = amountMult Amount ca qa `amountAdd` Amount cb qb = Amount ca (qa + qb) Amount ca qa `amountSub` Amount cb qb = Amount ca (qa - qb) Amount ca qa `amountMult` Amount cb qb = Amount ca (qa * qb) -- show & display methods instance Show Ledger where show l = "Ledger with " ++ m ++ " modifier, " ++ p ++ " periodic, " ++ e ++ " normal entries:\n" ++ (concat $ map show (modifier_entries l)) ++ (concat $ map show (periodic_entries l)) ++ (concat $ map show (entries l)) where m = show $ length $ modifier_entries l p = show $ length $ periodic_entries l e = show $ length $ entries l instance Show ModifierEntry where show e = "= " ++ (valueexpr e) ++ "\n" ++ unlines (map show (m_transactions e)) instance Show PeriodicEntry where show e = "~ " ++ (periodexpr e) ++ "\n" ++ unlines (map show (p_transactions e)) instance Show Entry where show = showEntry -- a register entry is displayed as two or more lines like this: -- date description account amount balance -- DDDDDDDDDD dddddddddddddddddddd aaaaaaaaaaaaaaaaaaaaaaaaa AAAAAAAAAA AAAAAAAAAA -- aaaaaaaaaaaaaaaaaaaaaaaaa AAAAAAAAAA AAAAAAAAAA -- ... ... ... -- dateWidth = 10 -- descWidth = 20 -- acctWidth = 25 -- amtWidth = 10 -- balWidth = 10 showEntry :: Entry -> String showEntry e = unlines $ map fst (entryLines e) -- convert an Entry to entry lines (string, amount pairs) entryLines :: Entry -> [(String,Amount)] entryLines e = [firstline] ++ otherlines where t:ts = transactions e firstline = (entrydesc e ++ (show t), amount t) otherlines = map (\t -> (prependSpace $ show t, amount t)) ts prependSpace = (replicate 32 ' ' ++) entrydesc e = printf "%-10s %-20s " (date e) (take 20 $ description e) instance Show Transaction where show t = printf "%-25s %10s" (take 25 $ account t) (show $ amount t) instance Show Amount where show (Amount cur qty) = let roundedqty = printf "%.2f" qty in case roundedqty of "0.00" -> "0" otherwise -> cur ++ roundedqty -- in the register report we show entries plus a running balance showEntriesWithBalances :: [Entry] -> Amount -> String showEntriesWithBalances [] _ = "" showEntriesWithBalances (e:es) b = showEntryWithBalances e b ++ (showEntriesWithBalances es b') where b' = b + (entryBalance e) entryBalance :: Entry -> Amount entryBalance = sumTransactions . transactions showEntryWithBalances :: Entry -> Amount -> String showEntryWithBalances e b = unlines [s | (s,a,b) <- entryLinesWithBalances (entryLines e) b] entryLinesWithBalances :: [(String,Amount)] -> Amount -> [(String,Amount,Amount)] entryLinesWithBalances [] _ = [] entryLinesWithBalances ((str,amt):els) bal = [(str',amt,bal')] ++ entryLinesWithBalances els bal' where bal' = bal + amt str' = str ++ (showBalance bal') showBalance b = printf " %10.2s" (show b) -- misc autofillEntry :: Entry -> Entry autofillEntry e = Entry (date e) (status e) (code e) (description e) (autofillTransactions (transactions e)) autofillTransactions :: [Transaction] -> [Transaction] autofillTransactions ts = let (ns, as) = normalAndAutoTransactions ts in case (length as) of 0 -> ns 1 -> ns ++ [balanceTransaction $ head as] where balanceTransaction t = t{amount = -(sumTransactions ns)} otherwise -> error "too many blank transactions in this entry" normalAndAutoTransactions :: [Transaction] -> ([Transaction], [Transaction]) normalAndAutoTransactions ts = partition isNormal ts where isNormal t = (currency $ amount t) /= "AUTO" -- transactions sumTransactions :: [Transaction] -> Amount sumTransactions ts = sum [amount t | t <- ts] transactionsFromEntries :: [Entry] -> [Transaction] transactionsFromEntries es = concat $ map transactions es matchTransactionAccount :: String -> Transaction -> Bool matchTransactionAccount s t = s `isInfixOf` (account t) transactionsWithEntries :: [Entry] -> [(Transaction,Entry)] transactionsWithEntries es = [(t,e) | e <- es, t <- transactions e] showTransactionsWithBalances :: [(Transaction,Entry)] -> Amount -> String showTransactionsWithBalances [] _ = [] showTransactionsWithBalances tes b = unlines $ showTransactionsWithBalances' tes b where showTransactionsWithBalances' [] _ = [] showTransactionsWithBalances' ((t,e):rest) b = [showTransactionWithBalance t e b'] ++ (showTransactionsWithBalances' rest b') where b' = b + (amount t) showTransactionWithBalance :: Transaction -> Entry -> Amount -> String showTransactionWithBalance t e b = (entrydesc e) ++ (show t) ++ (showBalance b) transactionsMatching :: String -> Ledger -> [(Transaction,Entry)] transactionsMatching s l = filter (\(t,e) -> matchTransactionAccount s t) (transactionsWithEntries $ entries l) -- entries entriesMatching :: String -> Ledger -> [Entry] entriesMatching s l = filterEntriesByAccount s (entries l) filterEntriesByAccount :: String -> [Entry] -> [Entry] filterEntriesByAccount s es = filter (matchEntryAccount s) es matchEntryAccount :: String -> Entry -> Bool matchEntryAccount s e = any (matchTransactionAccount s) (transactions e) -- accounts accountsFromTransactions :: [Transaction] -> [Account] accountsFromTransactions ts = nub $ map account ts accountsUsed :: Ledger -> [Account] accountsUsed l = accountsFromTransactions $ transactionsFromEntries $ entries l -- ["a:b:c","d:e"] -> ["a","a:b","a:b:c","d","d:e"] expandAccounts :: [Account] -> [Account] expandAccounts l = nub $ concat $ map expand l where expand l' = map (concat . intersperse ":") (tail $ inits $ splitAtElement ':' l') splitAtElement :: Eq a => a -> [a] -> [[a]] splitAtElement e l = case dropWhile (e==) l of [] -> [] l' -> first : splitAtElement e rest where (first,rest) = break (e==) l' accountTree :: Ledger -> [Account] accountTree = sort . expandAccounts . accountsUsed