{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-|

A transactions report. Like an EntriesReport, but with more
information such as a running balance.

-}

module Hledger.Reports.TransactionsReport (
  TransactionsReport,
  TransactionsReportItem,
  transactionsReport,
  transactionsReportByCommodity,
  triOrigTransaction,
  triDate,
  triAmount,
  triBalance,
  triCommodityAmount,
  triCommodityBalance,
  tests_TransactionsReport
)
where

import Data.List (sortBy)
import Data.List.Extra (nubSort)
import Data.Ord (comparing)
import Data.Text (Text)

import Hledger.Data
import Hledger.Query
import Hledger.Reports.ReportOptions
import Hledger.Reports.AccountTransactionsReport
import Hledger.Utils


-- | A transactions report includes a list of transactions touching multiple accounts
-- (posting-filtered and unfiltered variants), a running balance, and some
-- other information helpful for rendering a register view with or without a notion
-- of current account(s). Two kinds of report use this data structure, see transactionsReport
-- and accountTransactionsReport below for details.
type TransactionsReport = [TransactionsReportItem] -- line items, one per transaction
type TransactionsReportItem = (Transaction -- the original journal transaction, unmodified
                              ,Transaction -- the transaction as seen from a particular account, with postings maybe filtered
                              ,Bool        -- is this a split, ie more than one other account posting
                              ,Text        -- a display string describing the other account(s), if any
                              ,MixedAmount -- the amount posted to the current account(s) by the filtered postings (or total amount posted)
                              ,MixedAmount -- the running total of item amounts, starting from zero;
                                           -- or with --historical, the running total including items
                                           -- (matched by the report query) preceding the report period
                              )

triOrigTransaction :: (a, b, c, d, e, f) -> a
triOrigTransaction (a
torig,b
_,c
_,d
_,e
_,f
_) = a
torig
triDate :: (a, Transaction, c, d, e, f) -> Day
triDate (a
_,Transaction
tacct,c
_,d
_,e
_,f
_) = Transaction -> Day
tdate Transaction
tacct
triAmount :: (a, b, c, d, e, f) -> e
triAmount (a
_,b
_,c
_,d
_,e
a,f
_) = e
a
triBalance :: (a, b, c, d, e, f) -> f
triBalance (a
_,b
_,c
_,d
_,e
_,f
a) = f
a
triCommodityAmount :: CommoditySymbol -> (a, b, c, d, MixedAmount, f) -> MixedAmount
triCommodityAmount CommoditySymbol
c = CommoditySymbol -> MixedAmount -> MixedAmount
filterMixedAmountByCommodity CommoditySymbol
c  (MixedAmount -> MixedAmount)
-> ((a, b, c, d, MixedAmount, f) -> MixedAmount)
-> (a, b, c, d, MixedAmount, f)
-> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (a, b, c, d, MixedAmount, f) -> MixedAmount
forall a b c d e f. (a, b, c, d, e, f) -> e
triAmount
triCommodityBalance :: CommoditySymbol -> (a, b, c, d, e, MixedAmount) -> MixedAmount
triCommodityBalance CommoditySymbol
c = CommoditySymbol -> MixedAmount -> MixedAmount
filterMixedAmountByCommodity CommoditySymbol
c  (MixedAmount -> MixedAmount)
-> ((a, b, c, d, e, MixedAmount) -> MixedAmount)
-> (a, b, c, d, e, MixedAmount)
-> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (a, b, c, d, e, MixedAmount) -> MixedAmount
forall a b c d e f. (a, b, c, d, e, f) -> f
triBalance

-- | Select transactions from the whole journal. This is similar to a
-- "postingsReport" except with transaction-based report items which
-- are ordered most recent first. XXX Or an EntriesReport - use that instead ?
-- This is used by hledger-web's journal view.
transactionsReport :: ReportSpec -> Journal -> Query -> TransactionsReport
transactionsReport :: ReportSpec -> Journal -> Query -> TransactionsReport
transactionsReport ReportSpec
rspec Journal
j Query
q = TransactionsReport
items
   where
     -- XXX items' first element should be the full transaction with all postings
     items :: TransactionsReport
items = TransactionsReport -> TransactionsReport
forall a. [a] -> [a]
reverse (TransactionsReport -> TransactionsReport)
-> TransactionsReport -> TransactionsReport
forall a b. (a -> b) -> a -> b
$ Query
-> Query
-> MixedAmount
-> (MixedAmount -> MixedAmount)
-> [(Day, Transaction)]
-> TransactionsReport
accountTransactionsReportItems Query
q Query
None MixedAmount
nullmixedamt MixedAmount -> MixedAmount
forall a. a -> a
id [(Day, Transaction)]
ts
     ts :: [(Day, Transaction)]
ts    = ((Day, Transaction) -> (Day, Transaction) -> Ordering)
-> [(Day, Transaction)] -> [(Day, Transaction)]
forall a. (a -> a -> Ordering) -> [a] -> [a]
sortBy (((Day, Transaction) -> Day)
-> (Day, Transaction) -> (Day, Transaction) -> Ordering
forall a b. Ord a => (b -> a) -> b -> b -> Ordering
comparing (Day, Transaction) -> Day
forall a b. (a, b) -> a
fst) ([(Day, Transaction)] -> [(Day, Transaction)])
-> [(Day, Transaction)] -> [(Day, Transaction)]
forall a b. (a -> b) -> a -> b
$ (Transaction -> (Day, Transaction))
-> [Transaction] -> [(Day, Transaction)]
forall a b. (a -> b) -> [a] -> [b]
map (\Transaction
t -> (Transaction -> Day
date Transaction
t, Transaction
t)) ([Transaction] -> [(Day, Transaction)])
-> [Transaction] -> [(Day, Transaction)]
forall a b. (a -> b) -> a -> b
$ (Transaction -> Bool) -> [Transaction] -> [Transaction]
forall a. (a -> Bool) -> [a] -> [a]
filter (Query
q Query -> Transaction -> Bool
`matchesTransaction`) ([Transaction] -> [Transaction]) -> [Transaction] -> [Transaction]
forall a b. (a -> b) -> a -> b
$ Journal -> [Transaction]
jtxns (Journal -> [Transaction]) -> Journal -> [Transaction]
forall a b. (a -> b) -> a -> b
$ ReportSpec -> Journal -> Journal
journalApplyValuationFromOpts ReportSpec
rspec Journal
j
     date :: Transaction -> Day
date = ReportOpts -> Transaction -> Day
transactionDateFn (ReportOpts -> Transaction -> Day)
-> ReportOpts -> Transaction -> Day
forall a b. (a -> b) -> a -> b
$ ReportSpec -> ReportOpts
rsOpts ReportSpec
rspec

-- | Split a transactions report whose items may involve several commodities,
-- into one or more single-commodity transactions reports.
transactionsReportByCommodity :: TransactionsReport -> [(CommoditySymbol, TransactionsReport)]
transactionsReportByCommodity :: TransactionsReport -> [(CommoditySymbol, TransactionsReport)]
transactionsReportByCommodity TransactionsReport
tr =
  [(CommoditySymbol
c, CommoditySymbol -> TransactionsReport -> TransactionsReport
filterTransactionsReportByCommodity CommoditySymbol
c TransactionsReport
tr) | CommoditySymbol
c <- TransactionsReport -> [CommoditySymbol]
forall a b c d f.
[(a, b, c, d, MixedAmount, f)] -> [CommoditySymbol]
transactionsReportCommodities TransactionsReport
tr]
  where
    transactionsReportCommodities :: [(a, b, c, d, MixedAmount, f)] -> [CommoditySymbol]
transactionsReportCommodities = [CommoditySymbol] -> [CommoditySymbol]
forall a. Ord a => [a] -> [a]
nubSort ([CommoditySymbol] -> [CommoditySymbol])
-> ([(a, b, c, d, MixedAmount, f)] -> [CommoditySymbol])
-> [(a, b, c, d, MixedAmount, f)]
-> [CommoditySymbol]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Amount -> CommoditySymbol) -> [Amount] -> [CommoditySymbol]
forall a b. (a -> b) -> [a] -> [b]
map Amount -> CommoditySymbol
acommodity ([Amount] -> [CommoditySymbol])
-> ([(a, b, c, d, MixedAmount, f)] -> [Amount])
-> [(a, b, c, d, MixedAmount, f)]
-> [CommoditySymbol]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((a, b, c, d, MixedAmount, f) -> [Amount])
-> [(a, b, c, d, MixedAmount, f)] -> [Amount]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (MixedAmount -> [Amount]
amounts (MixedAmount -> [Amount])
-> ((a, b, c, d, MixedAmount, f) -> MixedAmount)
-> (a, b, c, d, MixedAmount, f)
-> [Amount]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (a, b, c, d, MixedAmount, f) -> MixedAmount
forall a b c d e f. (a, b, c, d, e, f) -> e
triAmount)

-- Remove transaction report items and item amount (and running
-- balance amount) components that don't involve the specified
-- commodity. Other item fields such as the transaction are left unchanged.
filterTransactionsReportByCommodity :: CommoditySymbol -> TransactionsReport -> TransactionsReport
filterTransactionsReportByCommodity :: CommoditySymbol -> TransactionsReport -> TransactionsReport
filterTransactionsReportByCommodity CommoditySymbol
c =
    TransactionsReport -> TransactionsReport
forall a b c d.
[(a, b, c, d, MixedAmount, MixedAmount)]
-> [(a, b, c, d, MixedAmount, MixedAmount)]
fixTransactionsReportItemBalances (TransactionsReport -> TransactionsReport)
-> (TransactionsReport -> TransactionsReport)
-> TransactionsReport
-> TransactionsReport
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((Transaction, Transaction, Bool, CommoditySymbol, MixedAmount,
  MixedAmount)
 -> TransactionsReport)
-> TransactionsReport -> TransactionsReport
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (CommoditySymbol
-> (Transaction, Transaction, Bool, CommoditySymbol, MixedAmount,
    MixedAmount)
-> TransactionsReport
forall a b c d f.
CommoditySymbol
-> (a, b, c, d, MixedAmount, f) -> [(a, b, c, d, MixedAmount, f)]
filterTransactionsReportItemByCommodity CommoditySymbol
c)
  where
    filterTransactionsReportItemByCommodity :: CommoditySymbol
-> (a, b, c, d, MixedAmount, f) -> [(a, b, c, d, MixedAmount, f)]
filterTransactionsReportItemByCommodity CommoditySymbol
c (a
t,b
t2,c
s,d
o,MixedAmount
a,f
bal)
      | CommoditySymbol
c CommoditySymbol -> [CommoditySymbol] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [CommoditySymbol]
cs = [(a, b, c, d, MixedAmount, f)
item']
      | Bool
otherwise   = []
      where
        cs :: [CommoditySymbol]
cs = (Amount -> CommoditySymbol) -> [Amount] -> [CommoditySymbol]
forall a b. (a -> b) -> [a] -> [b]
map Amount -> CommoditySymbol
acommodity ([Amount] -> [CommoditySymbol]) -> [Amount] -> [CommoditySymbol]
forall a b. (a -> b) -> a -> b
$ MixedAmount -> [Amount]
amounts MixedAmount
a
        item' :: (a, b, c, d, MixedAmount, f)
item' = (a
t,b
t2,c
s,d
o,MixedAmount
a',f
bal)
        a' :: MixedAmount
a' = CommoditySymbol -> MixedAmount -> MixedAmount
filterMixedAmountByCommodity CommoditySymbol
c MixedAmount
a

    fixTransactionsReportItemBalances :: [(a, b, c, d, MixedAmount, MixedAmount)]
-> [(a, b, c, d, MixedAmount, MixedAmount)]
fixTransactionsReportItemBalances [] = []
    fixTransactionsReportItemBalances [(a, b, c, d, MixedAmount, MixedAmount)
i] = [(a, b, c, d, MixedAmount, MixedAmount)
i]
    fixTransactionsReportItemBalances [(a, b, c, d, MixedAmount, MixedAmount)]
items = [(a, b, c, d, MixedAmount, MixedAmount)]
-> [(a, b, c, d, MixedAmount, MixedAmount)]
forall a. [a] -> [a]
reverse ([(a, b, c, d, MixedAmount, MixedAmount)]
 -> [(a, b, c, d, MixedAmount, MixedAmount)])
-> [(a, b, c, d, MixedAmount, MixedAmount)]
-> [(a, b, c, d, MixedAmount, MixedAmount)]
forall a b. (a -> b) -> a -> b
$ (a, b, c, d, MixedAmount, MixedAmount)
i(a, b, c, d, MixedAmount, MixedAmount)
-> [(a, b, c, d, MixedAmount, MixedAmount)]
-> [(a, b, c, d, MixedAmount, MixedAmount)]
forall a. a -> [a] -> [a]
:(MixedAmount
-> [(a, b, c, d, MixedAmount, MixedAmount)]
-> [(a, b, c, d, MixedAmount, MixedAmount)]
forall a b c d f.
MixedAmount
-> [(a, b, c, d, MixedAmount, f)]
-> [(a, b, c, d, MixedAmount, MixedAmount)]
go MixedAmount
startbal [(a, b, c, d, MixedAmount, MixedAmount)]
is)
      where
        (a, b, c, d, MixedAmount, MixedAmount)
i:[(a, b, c, d, MixedAmount, MixedAmount)]
is = [(a, b, c, d, MixedAmount, MixedAmount)]
-> [(a, b, c, d, MixedAmount, MixedAmount)]
forall a. [a] -> [a]
reverse [(a, b, c, d, MixedAmount, MixedAmount)]
items
        startbal :: MixedAmount
startbal = CommoditySymbol -> MixedAmount -> MixedAmount
filterMixedAmountByCommodity CommoditySymbol
c (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ (a, b, c, d, MixedAmount, MixedAmount) -> MixedAmount
forall a b c d e f. (a, b, c, d, e, f) -> f
triBalance (a, b, c, d, MixedAmount, MixedAmount)
i
        go :: MixedAmount
-> [(a, b, c, d, MixedAmount, f)]
-> [(a, b, c, d, MixedAmount, MixedAmount)]
go MixedAmount
_ [] = []
        go MixedAmount
bal ((a
t,b
t2,c
s,d
o,MixedAmount
amt,f
_):[(a, b, c, d, MixedAmount, f)]
is) = (a
t,b
t2,c
s,d
o,MixedAmount
amt,MixedAmount
bal')(a, b, c, d, MixedAmount, MixedAmount)
-> [(a, b, c, d, MixedAmount, MixedAmount)]
-> [(a, b, c, d, MixedAmount, MixedAmount)]
forall a. a -> [a] -> [a]
:MixedAmount
-> [(a, b, c, d, MixedAmount, f)]
-> [(a, b, c, d, MixedAmount, MixedAmount)]
go MixedAmount
bal' [(a, b, c, d, MixedAmount, f)]
is
          where bal' :: MixedAmount
bal' = MixedAmount
bal MixedAmount -> MixedAmount -> MixedAmount
`maPlus` MixedAmount
amt

-- tests

tests_TransactionsReport :: TestTree
tests_TransactionsReport = String -> [TestTree] -> TestTree
tests String
"TransactionsReport" [
 ]