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

    An account-centric transactions report.

    -}

    module Hledger.Reports.AccountTransactionsReport (
      AccountTransactionsReport,
      AccountTransactionsReportItem,
      accountTransactionsReport,
      accountTransactionsReportItems,
      transactionRegisterDate,
      triOrigTransaction,
      triDate,
      triAmount,
      triBalance,
      triCommodityAmount,
      triCommodityBalance,
      accountTransactionsReportByCommodity,
      tests_AccountTransactionsReport
    )
    where

    import Data.List (mapAccumR, nub, partition, sortBy)
    import Data.List.Extra (nubSort)
    import Data.Maybe (catMaybes)
    import Data.Ord (Down(..), comparing)
    import Data.Text (Text)
    import qualified Data.Text as T
    import Data.Time.Calendar (Day)

    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":
    --     the earliest of the transaction date and any other posting dates
    --     of postings to the current account (matched by the report query).
    --
    --   - 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 = [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) ?
      ,Text        -- 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
      )

    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 :: Text -> (a, b, c, d, MixedAmount, f) -> MixedAmount
    triCommodityAmount Text
    c = Text -> MixedAmount -> MixedAmount
    filterMixedAmountByCommodity Text
    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 :: Text -> (a, b, c, d, e, MixedAmount) -> MixedAmount
    triCommodityBalance Text
    c = Text -> MixedAmount -> MixedAmount
    filterMixedAmountByCommodity Text
    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

    accountTransactionsReport :: ReportSpec -> Journal -> Query -> AccountTransactionsReport
    accountTransactionsReport :: ReportSpec -> Journal -> Query -> AccountTransactionsReport
    accountTransactionsReport rspec :: ReportSpec
    rspec@ReportSpec{_rsReportOpts :: ReportSpec -> ReportOpts
    _rsReportOpts=ReportOpts
    ropts} Journal
    j Query
    thisacctq = AccountTransactionsReport
    items
      where
        -- A depth limit should not affect the account transactions report; it should show all transactions in/below this account.
        -- Queries on currency or amount are also ignored at this stage; they are handled earlier, before valuation.
        reportq :: Query
    reportq = Query -> Query
    simplifyQuery (Query -> Query) -> Query -> Query
    forall a b. (a -> b) -> a -> b
    $ [Query] -> Query
    And [Query
    aregisterq, Query
    periodq]
          where
            aregisterq :: Query
    aregisterq = (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
    queryIsCurOrAmt) (Query -> Query) -> (Query -> Query) -> Query -> Query
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (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
    queryIsDepth) (Query -> Query) -> Query -> Query
    forall a b. (a -> b) -> a -> b
    $ ReportSpec -> Query
    _rsQuery ReportSpec
    rspec
            periodq :: Query
    periodq = DateSpan -> Query
    Date (DateSpan -> Query) -> (Period -> DateSpan) -> Period -> Query
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Period -> DateSpan
    periodAsDateSpan (Period -> Query) -> Period -> Query
    forall a b. (a -> b) -> a -> b
    $ ReportOpts -> Period
    period_ ReportOpts
    ropts
        amtq :: Query
    amtq = (Query -> Bool) -> Query -> Query
    filterQuery Query -> Bool
    queryIsCurOrAmt (Query -> Query) -> Query -> Query
    forall a b. (a -> b) -> a -> b
    $ ReportSpec -> Query
    _rsQuery ReportSpec
    rspec
        queryIsCurOrAmt :: Query -> Bool
    queryIsCurOrAmt Query
    q = Query -> Bool
    queryIsSym Query
    q Bool -> Bool -> Bool
    || Query -> Bool
    queryIsAmt Query
    q
        wd :: WhichDate
    wd = ReportOpts -> WhichDate
    whichDate ReportOpts
    ropts

        -- Note that within this function, we are only allowed limited
        -- transformation of the transaction postings: this is due to the need to
        -- pass the original transactions into accountTransactionsReportItem.
        -- Generally, we either include a transaction in full, or not at all.
        -- Do some limited filtering and valuing of the journal's transactions:
        -- - filter them by the account query if any,
        -- - discard amounts not matched by the currency and amount query if any,
        -- - then apply valuation if any.
        -- Additional reportq filtering, such as date filtering, happens down in
        -- accountTransactionsReportItem, which discards transactions with no matched postings.
        acctJournal :: Journal
    acctJournal =
            -- With most calls we will not require transaction prices past this point, and can get a big
            -- speed improvement by stripping them early. In some cases, such as in hledger-ui, we still
            -- want to keep prices around, so we can toggle between cost and no cost quickly. We can use
            -- the show_costs_ flag to be efficient when we can, and detailed when we have to.
              (if ReportOpts -> Bool
    show_costs_ ReportOpts
    ropts then Journal -> Journal
    forall a. a -> a
    id else (MixedAmount -> MixedAmount) -> Journal -> Journal
    journalMapPostingAmounts MixedAmount -> MixedAmount
    mixedAmountStripPrices)
            (Journal -> Journal) -> (Journal -> Journal) -> Journal -> Journal
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Int -> (Journal -> String) -> Journal -> Journal
    forall a. Int -> (a -> String) -> a -> a
    traceOrLogAtWith Int
    5 ((String
    "ts3:\n"String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++)(String -> String) -> (Journal -> String) -> Journal -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .[Transaction] -> String
    pshowTransactions([Transaction] -> String)
    -> (Journal -> [Transaction]) -> Journal -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .Journal -> [Transaction]
    jtxns)
            -- maybe convert these transactions to cost or value
            (Journal -> Journal) -> (Journal -> Journal) -> Journal -> Journal
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . ReportSpec -> Journal -> Journal
    journalApplyValuationFromOpts ReportSpec
    rspec
            (Journal -> Journal) -> (Journal -> Journal) -> Journal -> Journal
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Int -> (Journal -> String) -> Journal -> Journal
    forall a. Int -> (a -> String) -> a -> a
    traceOrLogAtWith Int
    5 ((String
    "ts2:\n"String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++)(String -> String) -> (Journal -> String) -> Journal -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .[Transaction] -> String
    pshowTransactions([Transaction] -> String)
    -> (Journal -> [Transaction]) -> Journal -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .Journal -> [Transaction]
    jtxns)
            -- apply any cur:SYM filters in reportq
            (Journal -> Journal) -> (Journal -> Journal) -> Journal -> Journal
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (if Query -> Bool
    queryIsNull Query
    amtq then Journal -> Journal
    forall a. a -> a
    id else Query -> Journal -> Journal
    filterJournalAmounts Query
    amtq)
            -- only consider transactions which match thisacctq (possibly excluding postings
            -- which are not real or have the wrong status)
            (Journal -> Journal) -> (Journal -> Journal) -> Journal -> Journal
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Int -> String -> Journal -> Journal
    forall a. Int -> String -> a -> a
    traceOrLogAt Int
    3 (String
    "thisacctq: "String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++Query -> String
    forall a. Show a => a -> String
    show Query
    thisacctq)
            (Journal -> Journal) -> Journal -> Journal
    forall a b. (a -> b) -> a -> b
    $ Int -> (Journal -> String) -> Journal -> Journal
    forall a. Int -> (a -> String) -> a -> a
    traceOrLogAtWith Int
    5 ((String
    "ts1:\n"String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++)(String -> String) -> (Journal -> String) -> Journal -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .[Transaction] -> String
    pshowTransactions([Transaction] -> String)
    -> (Journal -> [Transaction]) -> Journal -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .Journal -> [Transaction]
    jtxns)
              Journal
    j{jtxns :: [Transaction]
    jtxns = (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
    . Transaction -> Transaction
    relevantPostings) ([Transaction] -> [Transaction]) -> [Transaction] -> [Transaction]
    forall a b. (a -> b) -> a -> b
    $ Journal -> [Transaction]
    jtxns Journal
    j}
          where
            relevantPostings :: Transaction -> Transaction
    relevantPostings
              | Query -> Bool
    queryIsNull Query
    realq Bool -> Bool -> Bool
    && Query -> Bool
    queryIsNull Query
    statusq = Transaction -> Transaction
    forall a. a -> a
    id
              | Bool
    otherwise = Query -> Transaction -> Transaction
    filterTransactionPostings (Query -> Transaction -> Transaction)
    -> (Query -> Query) -> Query -> Transaction -> Transaction
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Query -> Query
    simplifyQuery (Query -> Transaction -> Transaction)
    -> Query -> Transaction -> Transaction
    forall a b. (a -> b) -> a -> b
    $ [Query] -> Query
    And [Query
    realq, Query
    statusq]
            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

        startbal :: MixedAmount
    startbal
          | ReportOpts -> BalanceAccumulation
    balanceaccum_ ReportOpts
    ropts BalanceAccumulation -> BalanceAccumulation -> Bool
    forall a. Eq a => a -> a -> Bool
    == BalanceAccumulation
    Historical = [Posting] -> MixedAmount
    sumPostings [Posting]
    priorps
          | Bool
    otherwise                         = MixedAmount
    nullmixedamt
          where
            priorps :: [Posting]
    priorps = String -> [Posting] -> [Posting]
    forall a. Show a => String -> a -> a
    dbg5 String
    "priorps" ([Posting] -> [Posting])
    -> (Journal -> [Posting]) -> Journal -> [Posting]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Journal -> [Posting]
    journalPostings (Journal -> [Posting]) -> Journal -> [Posting]
    forall a b. (a -> b) -> a -> b
    $ Query -> Journal -> Journal
    filterJournalPostings Query
    priorq Journal
    acctJournal
            priorq :: Query
    priorq = 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]
            tostartdateq :: Query
    tostartdateq =
              case Maybe Day
    mstartdate of
                Just Day
    _  -> DateSpan -> Query
    Date (Maybe EFDay -> Maybe EFDay -> DateSpan
    DateSpan Maybe EFDay
    forall a. Maybe a
    Nothing (Day -> EFDay
    Exact (Day -> EFDay) -> Maybe Day -> Maybe EFDay
    forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
    <$> 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

        items :: AccountTransactionsReport
    items =
            Query
    -> Query
    -> MixedAmount
    -> (MixedAmount -> MixedAmount)
    -> (Text -> Maybe AccountType)
    -> [(Day, Transaction)]
    -> AccountTransactionsReport
    accountTransactionsReportItems Query
    reportq Query
    thisacctq MixedAmount
    startbal MixedAmount -> MixedAmount
    maNegate (Journal -> Text -> Maybe AccountType
    journalAccountType Journal
    j)
          -- sort by the transaction's register date, then index, for accurate starting balance
          ([(Day, Transaction)] -> AccountTransactionsReport)
    -> ([Transaction] -> [(Day, Transaction)])
    -> [Transaction]
    -> AccountTransactionsReport
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Int
    -> ([(Day, Transaction)] -> String)
    -> [(Day, Transaction)]
    -> [(Day, Transaction)]
    forall a. Int -> (a -> String) -> a -> a
    traceAtWith Int
    5 ((String
    "ts4:\n"String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++)(String -> String)
    -> ([(Day, Transaction)] -> String)
    -> [(Day, Transaction)]
    -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .[Transaction] -> String
    pshowTransactions([Transaction] -> String)
    -> ([(Day, Transaction)] -> [Transaction])
    -> [(Day, Transaction)]
    -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .((Day, Transaction) -> Transaction)
    -> [(Day, Transaction)] -> [Transaction]
    forall a b. (a -> b) -> [a] -> [b]
    map (Day, Transaction) -> Transaction
    forall a b. (a, b) -> b
    snd)
          ([(Day, Transaction)] -> [(Day, Transaction)])
    -> ([Transaction] -> [(Day, Transaction)])
    -> [Transaction]
    -> [(Day, Transaction)]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . ((Day, Transaction) -> (Day, Transaction) -> Ordering)
    -> [(Day, Transaction)] -> [(Day, Transaction)]
    forall a. (a -> a -> Ordering) -> [a] -> [a]
    sortBy (((Day, Transaction) -> Down Day)
    -> (Day, Transaction) -> (Day, Transaction) -> Ordering
    forall a b. Ord a => (b -> a) -> b -> b -> Ordering
    comparing (Day -> Down Day
    forall a. a -> Down a
    Down (Day -> Down Day)
    -> ((Day, Transaction) -> Day) -> (Day, Transaction) -> Down Day
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Day, Transaction) -> Day
    forall a b. (a, b) -> a
    fst) ((Day, Transaction) -> (Day, Transaction) -> Ordering)
    -> ((Day, Transaction) -> (Day, Transaction) -> Ordering)
    -> (Day, Transaction)
    -> (Day, Transaction)
    -> Ordering
    forall a. Semigroup a => a -> a -> a
    <> ((Day, Transaction) -> Down Integer)
    -> (Day, Transaction) -> (Day, Transaction) -> Ordering
    forall a b. Ord a => (b -> a) -> b -> b -> Ordering
    comparing (Integer -> Down Integer
    forall a. a -> Down a
    Down (Integer -> Down Integer)
    -> ((Day, Transaction) -> Integer)
    -> (Day, Transaction)
    -> Down Integer
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Transaction -> Integer
    tindex (Transaction -> Integer)
    -> ((Day, Transaction) -> Transaction)
    -> (Day, Transaction)
    -> Integer
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Day, Transaction) -> Transaction
    forall a b. (a, b) -> b
    snd))
          ([(Day, Transaction)] -> [(Day, Transaction)])
    -> ([Transaction] -> [(Day, Transaction)])
    -> [Transaction]
    -> [(Day, Transaction)]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Transaction -> (Day, Transaction))
    -> [Transaction] -> [(Day, Transaction)]
    forall a b. (a -> b) -> [a] -> [b]
    map (\Transaction
    t -> (WhichDate -> Query -> Query -> Transaction -> Day
    transactionRegisterDate WhichDate
    wd Query
    reportq Query
    thisacctq Transaction
    t, Transaction
    t))
          ([Transaction] -> AccountTransactionsReport)
    -> [Transaction] -> AccountTransactionsReport
    forall a b. (a -> b) -> a -> b
    $ Journal -> [Transaction]
    jtxns Journal
    acctJournal

    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, Text -> String
    T.unpack (Text -> String) -> Text -> String
    forall a b. (a -> b) -> a -> b
    $ Transaction -> Text
    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, and a sign-setting
    -- function.
    -- Each transaction is accompanied by the date that should be shown for it
    -- in the report. This is not necessarily the transaction date - see
    -- transactionRegisterDate.
    accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount)
                                   -> (AccountName -> Maybe AccountType) -> [(Day, Transaction)]
                                   -> [AccountTransactionsReportItem]
    accountTransactionsReportItems :: Query
    -> Query
    -> MixedAmount
    -> (MixedAmount -> MixedAmount)
    -> (Text -> Maybe AccountType)
    -> [(Day, Transaction)]
    -> AccountTransactionsReport
    accountTransactionsReportItems Query
    reportq Query
    thisacctq MixedAmount
    bal MixedAmount -> MixedAmount
    signfn Text -> Maybe AccountType
    accttypefn =
        [Maybe AccountTransactionsReportItem] -> AccountTransactionsReport
    forall a. [Maybe a] -> [a]
    catMaybes ([Maybe AccountTransactionsReportItem]
     -> AccountTransactionsReport)
    -> ([(Day, Transaction)] -> [Maybe AccountTransactionsReportItem])
    -> [(Day, Transaction)]
    -> AccountTransactionsReport
    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])
    -> ([(Day, Transaction)]
        -> (MixedAmount, [Maybe AccountTransactionsReportItem]))
    -> [(Day, Transaction)]
    -> [Maybe AccountTransactionsReportItem]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (MixedAmount
     -> (Day, Transaction)
     -> (MixedAmount, Maybe AccountTransactionsReportItem))
    -> MixedAmount
    -> [(Day, Transaction)]
    -> (MixedAmount, [Maybe AccountTransactionsReportItem])
    forall (t :: * -> *) s a b.
    Traversable t =>
    (s -> a -> (s, b)) -> s -> t a -> (s, t b)
    mapAccumR (Query
    -> Query
    -> (MixedAmount -> MixedAmount)
    -> (Text -> Maybe AccountType)
    -> MixedAmount
    -> (Day, Transaction)
    -> (MixedAmount, Maybe AccountTransactionsReportItem)
    accountTransactionsReportItem Query
    reportq Query
    thisacctq MixedAmount -> MixedAmount
    signfn Text -> Maybe AccountType
    accttypefn) MixedAmount
    bal

    accountTransactionsReportItem :: Query -> Query -> (MixedAmount -> MixedAmount)
                                  -> (AccountName -> Maybe AccountType) -> MixedAmount -> (Day, Transaction)
                                  -> (MixedAmount, Maybe AccountTransactionsReportItem)
    accountTransactionsReportItem :: Query
    -> Query
    -> (MixedAmount -> MixedAmount)
    -> (Text -> Maybe AccountType)
    -> MixedAmount
    -> (Day, Transaction)
    -> (MixedAmount, Maybe AccountTransactionsReportItem)
    accountTransactionsReportItem Query
    reportq Query
    thisacctq MixedAmount -> MixedAmount
    signfn Text -> Maybe AccountType
    accttypefn MixedAmount
    bal (Day
    d, Transaction
    t)
        -- 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
        | [Posting] -> Bool
    forall a. [a] -> Bool
    forall (t :: * -> *) a. Foldable t => t a -> Bool
    null [Posting]
    reportps = (MixedAmount
    bal, Maybe AccountTransactionsReportItem
    forall a. Maybe a
    Nothing)  -- no matched postings in this transaction, skip it
        | Bool
    otherwise     = (MixedAmount
    bal', AccountTransactionsReportItem
    -> Maybe AccountTransactionsReportItem
    forall a. a -> Maybe a
    Just (Transaction
    t, Transaction
    tacct{tdate :: Day
    tdate=Day
    d}, Int
    numotheraccts Int -> Int -> Bool
    forall a. Ord a => a -> a -> Bool
    > Int
    1, Text
    otheracctstr, MixedAmount
    amt, MixedAmount
    bal'))
        where
          tacct :: Transaction
    tacct@Transaction{tpostings :: Transaction -> [Posting]
    tpostings=[Posting]
    reportps} = (Text -> Maybe AccountType) -> Query -> Transaction -> Transaction
    filterTransactionPostingsExtra Text -> Maybe AccountType
    accttypefn Query
    reportq Transaction
    t  -- TODO needs to consider --date2, #1731
          ([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 = [Text] -> Int
    forall a. [a] -> Int
    forall (t :: * -> *) a. Foldable t => t a -> Int
    length ([Text] -> Int) -> [Text] -> Int
    forall a b. (a -> b) -> a -> b
    $ [Text] -> [Text]
    forall a. Eq a => [a] -> [a]
    nub ([Text] -> [Text]) -> [Text] -> [Text]
    forall a b. (a -> b) -> a -> b
    $ (Posting -> Text) -> [Posting] -> [Text]
    forall a b. (a -> b) -> [a] -> [b]
    map Posting -> Text
    paccount [Posting]
    otheracctps
          otheracctstr :: Text
    otheracctstr | Query
    thisacctq Query -> Query -> Bool
    forall a. Eq a => a -> a -> Bool
    == Query
    None  = [Posting] -> Text
    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] -> Text
    summarisePostingAccounts [Posting]
    thisacctps   -- only postings to current account ? summarise those
                       | Bool
    otherwise          = [Posting] -> Text
    summarisePostingAccounts [Posting]
    otheracctps  -- summarise matched postings to other account(s)
          -- 202302: Impact of t on thisacct - normally the sum of thisacctps,
          -- but if they are null it probably means reportq is an account filter
          -- and we should sum otheracctps instead.
          -- This fixes hledger areg ACCT ACCT2 (#2007), hopefully it's correct in general.
          amt :: MixedAmount
    amt
            | [Posting] -> Bool
    forall a. [a] -> Bool
    forall (t :: * -> *) a. Foldable t => t a -> Bool
    null [Posting]
    thisacctps = MixedAmount -> MixedAmount
    signfn (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
    forall a b. (a -> b) -> a -> b
    $ [Posting] -> MixedAmount
    sumPostings [Posting]
    otheracctps
            | Bool
    otherwise       = MixedAmount -> MixedAmount
    signfn (MixedAmount -> MixedAmount)
    -> (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . MixedAmount -> MixedAmount
    maNegate (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
    forall a b. (a -> b) -> a -> b
    $ [Posting] -> MixedAmount
    sumPostings [Posting]
    thisacctps
          bal' :: MixedAmount
    bal' = MixedAmount
    bal MixedAmount -> MixedAmount -> MixedAmount
    `maPlus` MixedAmount
    amt

    -- TODO needs checking, cf #1731
    -- | What date should be shown for a transaction in an account register report ?
    -- This will be in context of a particular account (the "this account" query)
    -- and any additional report query. It could be:
    --
    -- - if postings are matched by both thisacctq and reportq, the earliest of those
    --   matched postings' dates (or their secondary dates if --date2 was used)
    --
    -- - the transaction date, or its secondary date if --date2 was used.
    --
    transactionRegisterDate :: WhichDate -> Query -> Query -> Transaction -> Day
    transactionRegisterDate :: WhichDate -> Query -> Query -> Transaction -> Day
    transactionRegisterDate WhichDate
    wd Query
    reportq Query
    thisacctq Transaction
    t
      | Bool -> Bool
    not (Bool -> Bool) -> Bool -> Bool
    forall a b. (a -> b) -> a -> b
    $ [Posting] -> Bool
    forall a. [a] -> Bool
    forall (t :: * -> *) a. Foldable t => t a -> Bool
    null [Posting]
    thisacctps = [Day] -> Day
    forall a. Ord a => [a] -> a
    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 (WhichDate -> Posting -> Day
    postingDateOrDate2 WhichDate
    wd) [Posting]
    thisacctps
      | Bool
    otherwise             = WhichDate -> Transaction -> Day
    transactionDateOrDate2 WhichDate
    wd Transaction
    t
      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] -> Text
    summarisePostingAccounts :: [Posting] -> Text
    summarisePostingAccounts [Posting]
    ps =
        Text -> [Text] -> Text
    T.intercalate Text
    ", " ([Text] -> Text) -> ([Text] -> [Text]) -> [Text] -> Text
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Text -> Text) -> [Text] -> [Text]
    forall a b. (a -> b) -> [a] -> [b]
    map Text -> Text
    accountSummarisedName ([Text] -> [Text]) -> ([Text] -> [Text]) -> [Text] -> [Text]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . [Text] -> [Text]
    forall a. Eq a => [a] -> [a]
    nub ([Text] -> Text) -> [Text] -> Text
    forall a b. (a -> b) -> a -> b
    $ (Posting -> Text) -> [Posting] -> [Text]
    forall a b. (a -> b) -> [a] -> [b]
    map Posting -> Text
    paccount [Posting]
    displayps
      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 a. [a] -> Bool
    forall (t :: * -> *) a. Foldable t => t a -> Bool
    null [Posting]
    realps = [Posting]
    ps
                  | Bool
    otherwise   = [Posting]
    realps

    -- | Split an  account transactions report whose items may involve several commodities,
    -- into one or more single-commodity account transactions reports.
    accountTransactionsReportByCommodity :: AccountTransactionsReport -> [(CommoditySymbol, AccountTransactionsReport)]
    accountTransactionsReportByCommodity :: AccountTransactionsReport -> [(Text, AccountTransactionsReport)]
    accountTransactionsReportByCommodity AccountTransactionsReport
    tr =
      [(Text
    c, Text -> AccountTransactionsReport -> AccountTransactionsReport
    filterAccountTransactionsReportByCommodity Text
    c AccountTransactionsReport
    tr) | Text
    c <- AccountTransactionsReport -> [Text]
    forall {a} {b} {c} {d} {f}.
    [(a, b, c, d, MixedAmount, f)] -> [Text]
    commodities AccountTransactionsReport
    tr]
      where
        commodities :: [(a, b, c, d, MixedAmount, f)] -> [Text]
    commodities = [Text] -> [Text]
    forall a. Ord a => [a] -> [a]
    nubSort ([Text] -> [Text])
    -> ([(a, b, c, d, MixedAmount, f)] -> [Text])
    -> [(a, b, c, d, MixedAmount, f)]
    -> [Text]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Amount -> Text) -> [Amount] -> [Text]
    forall a b. (a -> b) -> [a] -> [b]
    map Amount -> Text
    acommodity ([Amount] -> [Text])
    -> ([(a, b, c, d, MixedAmount, f)] -> [Amount])
    -> [(a, b, c, d, MixedAmount, f)]
    -> [Text]
    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 account 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.
    filterAccountTransactionsReportByCommodity :: CommoditySymbol -> AccountTransactionsReport -> AccountTransactionsReport
    filterAccountTransactionsReportByCommodity :: Text -> AccountTransactionsReport -> AccountTransactionsReport
    filterAccountTransactionsReportByCommodity Text
    comm =
        AccountTransactionsReport -> AccountTransactionsReport
    forall {a} {b} {c} {d}.
    [(a, b, c, d, MixedAmount, MixedAmount)]
    -> [(a, b, c, d, MixedAmount, MixedAmount)]
    fixTransactionsReportItemBalances (AccountTransactionsReport -> AccountTransactionsReport)
    -> (AccountTransactionsReport -> AccountTransactionsReport)
    -> AccountTransactionsReport
    -> AccountTransactionsReport
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (AccountTransactionsReportItem -> AccountTransactionsReport)
    -> AccountTransactionsReport -> AccountTransactionsReport
    forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
    concatMap (Text -> AccountTransactionsReportItem -> AccountTransactionsReport
    forall {a} {b} {c} {d} {f}.
    Text
    -> (a, b, c, d, MixedAmount, f) -> [(a, b, c, d, MixedAmount, f)]
    filterTransactionsReportItemByCommodity Text
    comm)
      where
        filterTransactionsReportItemByCommodity :: Text
    -> (a, b, c, d, MixedAmount, f) -> [(a, b, c, d, MixedAmount, f)]
    filterTransactionsReportItemByCommodity Text
    c (a
    t,b
    t2,c
    s,d
    o,MixedAmount
    a,f
    bal)
          | Text
    c Text -> [Text] -> Bool
    forall a. Eq a => a -> [a] -> Bool
    forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
    `elem` [Text]
    cs = [(a, b, c, d, MixedAmount, f)
    item']
          | Bool
    otherwise   = []
          where
            cs :: [Text]
    cs = (Amount -> Text) -> [Amount] -> [Text]
    forall a b. (a -> b) -> [a] -> [b]
    map Amount -> Text
    acommodity ([Amount] -> [Text]) -> [Amount] -> [Text]
    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' = Text -> MixedAmount -> MixedAmount
    filterMixedAmountByCommodity Text
    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 = Text -> MixedAmount -> MixedAmount
    filterMixedAmountByCommodity Text
    comm (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_AccountTransactionsReport :: TestTree
    tests_AccountTransactionsReport = String -> [TestTree] -> TestTree
    testGroup String
    "AccountTransactionsReport" [
     ]