{-# LANGUAGE OverloadedStrings, RecordWildCards, FlexibleInstances #-} {-| An account-centric transactions report. -} module Hledger.Reports.AccountTransactionsReport ( AccountTransactionsReport, AccountTransactionsReportItem, accountTransactionsReport, accountTransactionsReportItems, transactionRegisterDate, tests_AccountTransactionsReport ) where import Data.List import Data.Ord import Data.Maybe import qualified Data.Text as T import Data.Time.Calendar import Hledger.Data import Hledger.Query import Hledger.Reports.ReportOptions import Hledger.Utils -- | An account transactions report represents transactions affecting -- a particular account (or possibly several accounts, but we don't -- use that). It is used eg by hledger-ui's and hledger-web's register -- view, and hledger's aregister report, where we want to show one row -- per transaction, in the context of the current account. Report -- items consist of: -- -- - the transaction, unmodified -- -- - the transaction as seen in the context of the current account and query, -- which means: -- -- - the transaction date is set to the "transaction context date", -- which can be different from the transaction's general date: -- if postings to the current account (and matched by the report query) -- have their own dates, it's the earliest of these dates. -- -- - the transaction's postings are filtered, excluding any which are not -- matched by the report query -- -- - a text description of the other account(s) posted to/from -- -- - a flag indicating whether there's more than one other account involved -- -- - the total increase/decrease to the current account -- -- - the report transactions' running total after this transaction; -- or if historical balance is requested (-H), the historical running total. -- The historical running total includes transactions from before the -- report start date if one is specified, filtered by the report query. -- The historical running total may or may not be the account's historical -- running balance, depending on the report query. -- -- Items are sorted by transaction register date (the earliest date the transaction -- posts to the current account), most recent first. -- Reporting intervals are currently ignored. -- type AccountTransactionsReport = (String -- label for the balance column, eg "balance" or "total" ,[AccountTransactionsReportItem] -- line items, one per transaction ) type AccountTransactionsReportItem = ( Transaction -- the transaction, unmodified ,Transaction -- the transaction, as seen from the current account ,Bool -- is this a split (more than one posting to other accounts) ? ,String -- a display string describing the other account(s), if any ,MixedAmount -- the amount posted to the current account(s) (or total amount posted) ,MixedAmount -- the register's running total or the current account(s)'s historical balance, after this transaction ) totallabel :: String totallabel = String "Period Total" balancelabel :: String balancelabel = String "Historical Total" accountTransactionsReport :: ReportSpec -> Journal -> Query -> Query -> AccountTransactionsReport accountTransactionsReport :: ReportSpec -> Journal -> Query -> Query -> AccountTransactionsReport accountTransactionsReport rspec :: ReportSpec rspec@ReportSpec{rsOpts :: ReportSpec -> ReportOpts rsOpts=ReportOpts ropts} Journal j Query reportq Query thisacctq = (String label, [AccountTransactionsReportItem] items) where -- a depth limit should not affect the account transactions report -- seems unnecessary for some reason XXX reportq' :: Query reportq' = -- filterQuery (not . queryIsDepth) Query reportq -- get all transactions ts1 :: [Transaction] ts1 = -- ptraceAtWith 5 (("ts1:\n"++).pshowTransactions) $ Journal -> [Transaction] jtxns Journal j -- apply any cur:SYM filters in reportq' symq :: Query symq = (Query -> Bool) -> Query -> Query filterQuery Query -> Bool queryIsSym Query reportq' ts2 :: [Transaction] ts2 = Int -> ([Transaction] -> String) -> [Transaction] -> [Transaction] forall a. Show a => Int -> (a -> String) -> a -> a ptraceAtWith Int 5 ((String "ts2:\n"String -> String -> String forall a. [a] -> [a] -> [a] ++)(String -> String) -> ([Transaction] -> String) -> [Transaction] -> String forall b c a. (b -> c) -> (a -> b) -> a -> c .[Transaction] -> String pshowTransactions) ([Transaction] -> [Transaction]) -> [Transaction] -> [Transaction] forall a b. (a -> b) -> a -> b $ (if Query -> Bool queryIsNull Query symq then [Transaction] -> [Transaction] forall a. a -> a id else (Transaction -> Transaction) -> [Transaction] -> [Transaction] forall a b. (a -> b) -> [a] -> [b] map (Query -> Transaction -> Transaction filterTransactionAmounts Query symq)) [Transaction] ts1 -- keep just the transactions affecting this account (via possibly realness or status-filtered postings) realq :: Query realq = (Query -> Bool) -> Query -> Query filterQuery Query -> Bool queryIsReal Query reportq' statusq :: Query statusq = (Query -> Bool) -> Query -> Query filterQuery Query -> Bool queryIsStatus Query reportq' ts3 :: [Transaction] ts3 = Int -> String -> [Transaction] -> [Transaction] forall a. Int -> String -> a -> a traceAt Int 3 (String "thisacctq: "String -> String -> String forall a. [a] -> [a] -> [a] ++Query -> String forall a. Show a => a -> String show Query thisacctq) ([Transaction] -> [Transaction]) -> [Transaction] -> [Transaction] forall a b. (a -> b) -> a -> b $ Int -> ([Transaction] -> String) -> [Transaction] -> [Transaction] forall a. Show a => Int -> (a -> String) -> a -> a ptraceAtWith Int 5 ((String "ts3:\n"String -> String -> String forall a. [a] -> [a] -> [a] ++)(String -> String) -> ([Transaction] -> String) -> [Transaction] -> String forall b c a. (b -> c) -> (a -> b) -> a -> c .[Transaction] -> String pshowTransactions) ([Transaction] -> [Transaction]) -> [Transaction] -> [Transaction] forall a b. (a -> b) -> a -> b $ (Transaction -> Bool) -> [Transaction] -> [Transaction] forall a. (a -> Bool) -> [a] -> [a] filter (Query -> Transaction -> Bool matchesTransaction Query thisacctq (Transaction -> Bool) -> (Transaction -> Transaction) -> Transaction -> Bool forall b c a. (b -> c) -> (a -> b) -> a -> c . Query -> Transaction -> Transaction filterTransactionPostings ([Query] -> Query And [Query realq, Query statusq])) [Transaction] ts2 -- maybe convert these transactions to cost or value -- PARTIAL: prices :: PriceOracle prices = Bool -> Journal -> PriceOracle journalPriceOracle (ReportOpts -> Bool infer_value_ ReportOpts ropts) Journal j styles :: Map CommoditySymbol AmountStyle styles = Journal -> Map CommoditySymbol AmountStyle journalCommodityStyles Journal j periodlast :: Day periodlast = Day -> Maybe Day -> Day forall a. a -> Maybe a -> a fromMaybe (String -> Day forall a. String -> a error' String "journalApplyValuation: expected a non-empty journal") (Maybe Day -> Day) -> Maybe Day -> Day forall a b. (a -> b) -> a -> b $ -- XXX shouldn't happen ReportSpec -> Journal -> Maybe Day reportPeriodOrJournalLastDay ReportSpec rspec Journal j mreportlast :: Maybe Day mreportlast = ReportSpec -> Maybe Day reportPeriodLastDay ReportSpec rspec multiperiod :: Bool multiperiod = ReportOpts -> Interval interval_ ReportOpts ropts Interval -> Interval -> Bool forall a. Eq a => a -> a -> Bool /= Interval NoInterval tval :: Transaction -> Transaction tval = case ReportOpts -> Maybe ValuationType value_ ReportOpts ropts of Just ValuationType v -> \Transaction t -> PriceOracle -> Map CommoditySymbol AmountStyle -> Day -> Maybe Day -> Day -> Bool -> Transaction -> ValuationType -> Transaction transactionApplyValuation PriceOracle prices Map CommoditySymbol AmountStyle styles Day periodlast Maybe Day mreportlast (ReportSpec -> Day rsToday ReportSpec rspec) Bool multiperiod Transaction t ValuationType v Maybe ValuationType Nothing -> Transaction -> Transaction forall a. a -> a id ts4 :: [Transaction] ts4 = Int -> ([Transaction] -> String) -> [Transaction] -> [Transaction] forall a. Show a => Int -> (a -> String) -> a -> a ptraceAtWith Int 5 ((String "ts4:\n"String -> String -> String forall a. [a] -> [a] -> [a] ++)(String -> String) -> ([Transaction] -> String) -> [Transaction] -> String forall b c a. (b -> c) -> (a -> b) -> a -> c .[Transaction] -> String pshowTransactions) ([Transaction] -> [Transaction]) -> [Transaction] -> [Transaction] forall a b. (a -> b) -> a -> b $ (Transaction -> Transaction) -> [Transaction] -> [Transaction] forall a b. (a -> b) -> [a] -> [b] map Transaction -> Transaction tval [Transaction] ts3 -- sort by the transaction's register date, for accurate starting balance -- these are not yet filtered by tdate, we want to search them all for priorps ts5 :: [Transaction] ts5 = Int -> ([Transaction] -> String) -> [Transaction] -> [Transaction] forall a. Show a => Int -> (a -> String) -> a -> a ptraceAtWith Int 5 ((String "ts5:\n"String -> String -> String forall a. [a] -> [a] -> [a] ++)(String -> String) -> ([Transaction] -> String) -> [Transaction] -> String forall b c a. (b -> c) -> (a -> b) -> a -> c .[Transaction] -> String pshowTransactions) ([Transaction] -> [Transaction]) -> [Transaction] -> [Transaction] forall a b. (a -> b) -> a -> b $ (Transaction -> Transaction -> Ordering) -> [Transaction] -> [Transaction] forall a. (a -> a -> Ordering) -> [a] -> [a] sortBy ((Transaction -> Day) -> Transaction -> Transaction -> Ordering forall a b. Ord a => (b -> a) -> b -> b -> Ordering comparing (Query -> Query -> Transaction -> Day transactionRegisterDate Query reportq' Query thisacctq)) [Transaction] ts4 (MixedAmount startbal,String label) | ReportOpts -> BalanceType balancetype_ ReportOpts ropts BalanceType -> BalanceType -> Bool forall a. Eq a => a -> a -> Bool == BalanceType HistoricalBalance = ([Posting] -> MixedAmount sumPostings [Posting] priorps, String balancelabel) | Bool otherwise = (MixedAmount nullmixedamt, String totallabel) where priorps :: [Posting] priorps = String -> [Posting] -> [Posting] forall a. Show a => String -> a -> a dbg5 String "priorps" ([Posting] -> [Posting]) -> [Posting] -> [Posting] forall a b. (a -> b) -> a -> b $ (Posting -> Bool) -> [Posting] -> [Posting] forall a. (a -> Bool) -> [a] -> [a] filter (Query -> Posting -> Bool matchesPosting (String -> Query -> Query forall a. Show a => String -> a -> a dbg5 String "priorq" (Query -> Query) -> Query -> Query forall a b. (a -> b) -> a -> b $ [Query] -> Query And [Query thisacctq, Query tostartdateq, Query datelessreportq])) ([Posting] -> [Posting]) -> [Posting] -> [Posting] forall a b. (a -> b) -> a -> b $ [Transaction] -> [Posting] transactionsPostings [Transaction] ts5 tostartdateq :: Query tostartdateq = case Maybe Day mstartdate of Just Day _ -> DateSpan -> Query Date (Maybe Day -> Maybe Day -> DateSpan DateSpan Maybe Day forall a. Maybe a Nothing Maybe Day mstartdate) Maybe Day Nothing -> Query None -- no start date specified, there are no prior postings mstartdate :: Maybe Day mstartdate = Bool -> Query -> Maybe Day queryStartDate (ReportOpts -> Bool date2_ ReportOpts ropts) Query reportq' datelessreportq :: Query datelessreportq = (Query -> Bool) -> Query -> Query filterQuery (Bool -> Bool not (Bool -> Bool) -> (Query -> Bool) -> Query -> Bool forall b c a. (b -> c) -> (a -> b) -> a -> c . Query -> Bool queryIsDateOrDate2) Query reportq' -- accountTransactionsReportItem will keep transactions of any date which have any posting inside the report period. -- Should we also require that transaction date is inside the report period ? -- Should we be filtering by reportq here to apply other query terms (?) -- Make it an option for now. filtertxns :: Bool filtertxns = ReportOpts -> Bool txn_dates_ ReportOpts ropts items :: [AccountTransactionsReportItem] items = [AccountTransactionsReportItem] -> [AccountTransactionsReportItem] forall a. [a] -> [a] reverse ([AccountTransactionsReportItem] -> [AccountTransactionsReportItem]) -> [AccountTransactionsReportItem] -> [AccountTransactionsReportItem] forall a b. (a -> b) -> a -> b $ Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount) -> [Transaction] -> [AccountTransactionsReportItem] accountTransactionsReportItems Query reportq' Query thisacctq MixedAmount startbal MixedAmount -> MixedAmount forall a. Num a => a -> a negate ([Transaction] -> [AccountTransactionsReportItem]) -> [Transaction] -> [AccountTransactionsReportItem] forall a b. (a -> b) -> a -> b $ (if Bool filtertxns then (Transaction -> Bool) -> [Transaction] -> [Transaction] forall a. (a -> Bool) -> [a] -> [a] filter (Query reportq' Query -> Transaction -> Bool `matchesTransaction`) else [Transaction] -> [Transaction] forall a. a -> a id) ([Transaction] -> [Transaction]) -> [Transaction] -> [Transaction] forall a b. (a -> b) -> a -> b $ [Transaction] ts5 pshowTransactions :: [Transaction] -> String pshowTransactions :: [Transaction] -> String pshowTransactions = [String] -> String forall a. Show a => a -> String pshow ([String] -> String) -> ([Transaction] -> [String]) -> [Transaction] -> String forall b c a. (b -> c) -> (a -> b) -> a -> c . (Transaction -> String) -> [Transaction] -> [String] forall a b. (a -> b) -> [a] -> [b] map (\Transaction t -> [String] -> String unwords [Day -> String forall a. Show a => a -> String show (Day -> String) -> Day -> String forall a b. (a -> b) -> a -> b $ Transaction -> Day tdate Transaction t, CommoditySymbol -> String T.unpack (CommoditySymbol -> String) -> CommoditySymbol -> String forall a b. (a -> b) -> a -> b $ Transaction -> CommoditySymbol tdescription Transaction t]) -- | Generate transactions report items from a list of transactions, -- using the provided user-specified report query, a query specifying -- which account to use as the focus, a starting balance, a sign-setting -- function and a balance-summing function. Or with a None current account -- query, this can also be used for the transactionsReport. accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount) -> [Transaction] -> [AccountTransactionsReportItem] accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount) -> [Transaction] -> [AccountTransactionsReportItem] accountTransactionsReportItems Query reportq Query thisacctq MixedAmount bal MixedAmount -> MixedAmount signfn = [Maybe AccountTransactionsReportItem] -> [AccountTransactionsReportItem] forall a. [Maybe a] -> [a] catMaybes ([Maybe AccountTransactionsReportItem] -> [AccountTransactionsReportItem]) -> ([Transaction] -> [Maybe AccountTransactionsReportItem]) -> [Transaction] -> [AccountTransactionsReportItem] forall b c a. (b -> c) -> (a -> b) -> a -> c . (MixedAmount, [Maybe AccountTransactionsReportItem]) -> [Maybe AccountTransactionsReportItem] forall a b. (a, b) -> b snd ((MixedAmount, [Maybe AccountTransactionsReportItem]) -> [Maybe AccountTransactionsReportItem]) -> ([Transaction] -> (MixedAmount, [Maybe AccountTransactionsReportItem])) -> [Transaction] -> [Maybe AccountTransactionsReportItem] forall b c a. (b -> c) -> (a -> b) -> a -> c . (MixedAmount -> Transaction -> (MixedAmount, Maybe AccountTransactionsReportItem)) -> MixedAmount -> [Transaction] -> (MixedAmount, [Maybe AccountTransactionsReportItem]) forall (t :: * -> *) a b c. Traversable t => (a -> b -> (a, c)) -> a -> t b -> (a, t c) mapAccumL (Query -> Query -> (MixedAmount -> MixedAmount) -> MixedAmount -> Transaction -> (MixedAmount, Maybe AccountTransactionsReportItem) accountTransactionsReportItem Query reportq Query thisacctq MixedAmount -> MixedAmount signfn) MixedAmount bal accountTransactionsReportItem :: Query -> Query -> (MixedAmount -> MixedAmount) -> MixedAmount -> Transaction -> (MixedAmount, Maybe AccountTransactionsReportItem) accountTransactionsReportItem :: Query -> Query -> (MixedAmount -> MixedAmount) -> MixedAmount -> Transaction -> (MixedAmount, Maybe AccountTransactionsReportItem) accountTransactionsReportItem Query reportq Query thisacctq MixedAmount -> MixedAmount signfn MixedAmount bal Transaction torig = (MixedAmount, Maybe AccountTransactionsReportItem) balItem -- 201403: This is used for both accountTransactionsReport and transactionsReport, which makes it a bit overcomplicated -- 201407: I've lost my grip on this, let's just hope for the best -- 201606: we now calculate change and balance from filtered postings, check this still works well for all callers XXX where tfiltered :: Transaction tfiltered@Transaction{tpostings :: Transaction -> [Posting] tpostings=[Posting] reportps} = Query -> Transaction -> Transaction filterTransactionPostings Query reportq Transaction torig tacct :: Transaction tacct = Transaction tfiltered{tdate :: Day tdate=Query -> Query -> Transaction -> Day transactionRegisterDate Query reportq Query thisacctq Transaction tfiltered} balItem :: (MixedAmount, Maybe AccountTransactionsReportItem) balItem = case [Posting] reportps of [] -> (MixedAmount bal, Maybe AccountTransactionsReportItem forall a. Maybe a Nothing) -- no matched postings in this transaction, skip it [Posting] _ -> (MixedAmount b, AccountTransactionsReportItem -> Maybe AccountTransactionsReportItem forall a. a -> Maybe a Just (Transaction torig, Transaction tacct, Int numotheraccts Int -> Int -> Bool forall a. Ord a => a -> a -> Bool > Int 1, String otheracctstr, MixedAmount a, MixedAmount b)) where ([Posting] thisacctps, [Posting] otheracctps) = (Posting -> Bool) -> [Posting] -> ([Posting], [Posting]) forall a. (a -> Bool) -> [a] -> ([a], [a]) partition (Query -> Posting -> Bool matchesPosting Query thisacctq) [Posting] reportps numotheraccts :: Int numotheraccts = [CommoditySymbol] -> Int forall (t :: * -> *) a. Foldable t => t a -> Int length ([CommoditySymbol] -> Int) -> [CommoditySymbol] -> Int forall a b. (a -> b) -> a -> b $ [CommoditySymbol] -> [CommoditySymbol] forall a. Eq a => [a] -> [a] nub ([CommoditySymbol] -> [CommoditySymbol]) -> [CommoditySymbol] -> [CommoditySymbol] forall a b. (a -> b) -> a -> b $ (Posting -> CommoditySymbol) -> [Posting] -> [CommoditySymbol] forall a b. (a -> b) -> [a] -> [b] map Posting -> CommoditySymbol paccount [Posting] otheracctps otheracctstr :: String otheracctstr | Query thisacctq Query -> Query -> Bool forall a. Eq a => a -> a -> Bool == Query None = [Posting] -> String summarisePostingAccounts [Posting] reportps -- no current account ? summarise all matched postings | Int numotheraccts Int -> Int -> Bool forall a. Eq a => a -> a -> Bool == Int 0 = [Posting] -> String summarisePostingAccounts [Posting] thisacctps -- only postings to current account ? summarise those | Bool otherwise = [Posting] -> String summarisePostingAccounts [Posting] otheracctps -- summarise matched postings to other account(s) a :: MixedAmount a = MixedAmount -> MixedAmount signfn (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount forall a b. (a -> b) -> a -> b $ MixedAmount -> MixedAmount forall a. Num a => a -> a negate (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount forall a b. (a -> b) -> a -> b $ [MixedAmount] -> MixedAmount forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a sum ([MixedAmount] -> MixedAmount) -> [MixedAmount] -> MixedAmount forall a b. (a -> b) -> a -> b $ (Posting -> MixedAmount) -> [Posting] -> [MixedAmount] forall a b. (a -> b) -> [a] -> [b] map Posting -> MixedAmount pamount [Posting] thisacctps b :: MixedAmount b = MixedAmount bal MixedAmount -> MixedAmount -> MixedAmount forall a. Num a => a -> a -> a + MixedAmount a -- | What is the transaction's date in the context of a particular account -- (specified with a query) and report query, as in an account register ? -- It's normally the transaction's general date, but if any posting(s) -- matched by the report query and affecting the matched account(s) have -- their own earlier dates, it's the earliest of these dates. -- Secondary transaction/posting dates are ignored. transactionRegisterDate :: Query -> Query -> Transaction -> Day transactionRegisterDate :: Query -> Query -> Transaction -> Day transactionRegisterDate Query reportq Query thisacctq Transaction t | [Posting] -> Bool forall (t :: * -> *) a. Foldable t => t a -> Bool null [Posting] thisacctps = Transaction -> Day tdate Transaction t | Bool otherwise = [Day] -> Day forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a minimum ([Day] -> Day) -> [Day] -> Day forall a b. (a -> b) -> a -> b $ (Posting -> Day) -> [Posting] -> [Day] forall a b. (a -> b) -> [a] -> [b] map Posting -> Day postingDate [Posting] thisacctps where reportps :: [Posting] reportps = Transaction -> [Posting] tpostings (Transaction -> [Posting]) -> Transaction -> [Posting] forall a b. (a -> b) -> a -> b $ Query -> Transaction -> Transaction filterTransactionPostings Query reportq Transaction t thisacctps :: [Posting] thisacctps = (Posting -> Bool) -> [Posting] -> [Posting] forall a. (a -> Bool) -> [a] -> [a] filter (Query -> Posting -> Bool matchesPosting Query thisacctq) [Posting] reportps -- -- | Generate a short readable summary of some postings, like -- -- "from (negatives) to (positives)". -- summarisePostings :: [Posting] -> String -- summarisePostings ps = -- case (summarisePostingAccounts froms, summarisePostingAccounts tos) of -- ("",t) -> "to "++t -- (f,"") -> "from "++f -- (f,t) -> "from "++f++" to "++t -- where -- (froms,tos) = partition (fromMaybe False . isNegativeMixedAmount . pamount) ps -- | Generate a simplified summary of some postings' accounts. -- To reduce noise, if there are both real and virtual postings, show only the real ones. summarisePostingAccounts :: [Posting] -> String summarisePostingAccounts :: [Posting] -> String summarisePostingAccounts [Posting] ps = (String -> [String] -> String forall a. [a] -> [[a]] -> [a] intercalate String ", " ([String] -> String) -> ([Posting] -> [String]) -> [Posting] -> String forall b c a. (b -> c) -> (a -> b) -> a -> c . (CommoditySymbol -> String) -> [CommoditySymbol] -> [String] forall a b. (a -> b) -> [a] -> [b] map (CommoditySymbol -> String T.unpack (CommoditySymbol -> String) -> (CommoditySymbol -> CommoditySymbol) -> CommoditySymbol -> String forall b c a. (b -> c) -> (a -> b) -> a -> c . CommoditySymbol -> CommoditySymbol accountSummarisedName) ([CommoditySymbol] -> [String]) -> ([Posting] -> [CommoditySymbol]) -> [Posting] -> [String] forall b c a. (b -> c) -> (a -> b) -> a -> c . [CommoditySymbol] -> [CommoditySymbol] forall a. Eq a => [a] -> [a] nub ([CommoditySymbol] -> [CommoditySymbol]) -> ([Posting] -> [CommoditySymbol]) -> [Posting] -> [CommoditySymbol] forall b c a. (b -> c) -> (a -> b) -> a -> c . (Posting -> CommoditySymbol) -> [Posting] -> [CommoditySymbol] forall a b. (a -> b) -> [a] -> [b] map Posting -> CommoditySymbol paccount) [Posting] displayps -- XXX pack where realps :: [Posting] realps = (Posting -> Bool) -> [Posting] -> [Posting] forall a. (a -> Bool) -> [a] -> [a] filter Posting -> Bool isReal [Posting] ps displayps :: [Posting] displayps | [Posting] -> Bool forall (t :: * -> *) a. Foldable t => t a -> Bool null [Posting] realps = [Posting] ps | Bool otherwise = [Posting] realps -- tests tests_AccountTransactionsReport :: TestTree tests_AccountTransactionsReport = String -> [TestTree] -> TestTree tests String "AccountTransactionsReport" [ ]