{-|

A 'Transaction' represents a movement of some commodity(ies) between two
or more accounts. It consists of multiple account 'Posting's which balance
to zero, a date, and optional extras like description, cleared status, and
tags.

-}

{-# LANGUAGE MultiWayIf        #-}
{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE OverloadedStrings #-}

module Hledger.Data.Transaction
( -- * Transaction
  nulltransaction
, transaction
, txnTieKnot
, txnUntieKnot
  -- * operations
, hasRealPostings
, realPostings
, assignmentPostings
, virtualPostings
, balancedVirtualPostings
, transactionsPostings
, transactionTransformPostings
, transactionApplyValuation
, transactionToCost
, transactionAddInferredEquityPostings
, transactionInferCostsFromEquity
, transactionApplyAliases
, transactionMapPostings
, transactionMapPostingAmounts
, transactionAmounts
, partitionAndCheckConversionPostings
  -- nonzerobalanceerror
  -- * date operations
, transactionDate2
, transactionDateOrDate2
  -- * transaction description parts
, transactionPayee
, transactionNote
  -- payeeAndNoteFromDescription
  -- * rendering
, showTransaction
, showTransactionOneLineAmounts
, showTransactionLineFirstPart
, showTransactionBeancount
, transactionFile
  -- * transaction errors
, annotateErrorWithTransaction
  -- * tests
, tests_Transaction
) where

import Control.Monad.Trans.State (StateT(..), evalStateT)
import Data.Bifunctor (first, second)
import Data.Foldable (foldlM)
import Data.Maybe (fromMaybe, isJust, mapMaybe)
import Data.Semigroup (Endo(..))
import Data.Text (Text)
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB
import Data.Time.Calendar (Day, fromGregorian)

import Hledger.Utils
import Hledger.Data.Types
import Hledger.Data.Dates
import Hledger.Data.Posting
import Hledger.Data.Amount
import Hledger.Data.Valuation
import Data.Decimal (normalizeDecimal, decimalPlaces)
import Data.Functor ((<&>))


instance HasAmounts Transaction where
  styleAmounts :: Map CommoditySymbol AmountStyle -> Transaction -> Transaction
styleAmounts Map CommoditySymbol AmountStyle
styles Transaction
t = Transaction
t{tpostings :: [Posting]
tpostings=Map CommoditySymbol AmountStyle -> [Posting] -> [Posting]
forall a. HasAmounts a => Map CommoditySymbol AmountStyle -> a -> a
styleAmounts Map CommoditySymbol AmountStyle
styles ([Posting] -> [Posting]) -> [Posting] -> [Posting]
forall a b. (a -> b) -> a -> b
$ Transaction -> [Posting]
tpostings Transaction
t}

nulltransaction :: Transaction
nulltransaction :: Transaction
nulltransaction = Transaction :: Integer
-> CommoditySymbol
-> (SourcePos, SourcePos)
-> Day
-> Maybe Day
-> Status
-> CommoditySymbol
-> CommoditySymbol
-> CommoditySymbol
-> [Tag]
-> [Posting]
-> Transaction
Transaction {
                    tindex :: Integer
tindex=Integer
0,
                    tsourcepos :: (SourcePos, SourcePos)
tsourcepos=(SourcePos, SourcePos)
nullsourcepos,
                    tdate :: Day
tdate=Day
nulldate,
                    tdate2 :: Maybe Day
tdate2=Maybe Day
forall a. Maybe a
Nothing,
                    tstatus :: Status
tstatus=Status
Unmarked,
                    tcode :: CommoditySymbol
tcode=CommoditySymbol
"",
                    tdescription :: CommoditySymbol
tdescription=CommoditySymbol
"",
                    tcomment :: CommoditySymbol
tcomment=CommoditySymbol
"",
                    ttags :: [Tag]
ttags=[],
                    tpostings :: [Posting]
tpostings=[],
                    tprecedingcomment :: CommoditySymbol
tprecedingcomment=CommoditySymbol
""
                  }

-- | Make a simple transaction with the given date and postings.
transaction :: Day -> [Posting] -> Transaction
transaction :: Day -> [Posting] -> Transaction
transaction Day
day [Posting]
ps = Transaction -> Transaction
txnTieKnot (Transaction -> Transaction) -> Transaction -> Transaction
forall a b. (a -> b) -> a -> b
$ Transaction
nulltransaction{tdate :: Day
tdate=Day
day, tpostings :: [Posting]
tpostings=[Posting]
ps}

transactionPayee :: Transaction -> Text
transactionPayee :: Transaction -> CommoditySymbol
transactionPayee = Tag -> CommoditySymbol
forall a b. (a, b) -> a
fst (Tag -> CommoditySymbol)
-> (Transaction -> Tag) -> Transaction -> CommoditySymbol
forall b c a. (b -> c) -> (a -> b) -> a -> c
. CommoditySymbol -> Tag
payeeAndNoteFromDescription (CommoditySymbol -> Tag)
-> (Transaction -> CommoditySymbol) -> Transaction -> Tag
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> CommoditySymbol
tdescription

transactionNote :: Transaction -> Text
transactionNote :: Transaction -> CommoditySymbol
transactionNote = Tag -> CommoditySymbol
forall a b. (a, b) -> b
snd (Tag -> CommoditySymbol)
-> (Transaction -> Tag) -> Transaction -> CommoditySymbol
forall b c a. (b -> c) -> (a -> b) -> a -> c
. CommoditySymbol -> Tag
payeeAndNoteFromDescription (CommoditySymbol -> Tag)
-> (Transaction -> CommoditySymbol) -> Transaction -> Tag
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> CommoditySymbol
tdescription

-- | Parse a transaction's description into payee and note (aka narration) fields,
-- assuming a convention of separating these with | (like Beancount).
-- Ie, everything up to the first | is the payee, everything after it is the note.
-- When there's no |, payee == note == description.
payeeAndNoteFromDescription :: Text -> (Text,Text)
payeeAndNoteFromDescription :: CommoditySymbol -> Tag
payeeAndNoteFromDescription CommoditySymbol
t
  | CommoditySymbol -> Bool
T.null CommoditySymbol
n = (CommoditySymbol
t, CommoditySymbol
t)
  | Bool
otherwise = (CommoditySymbol -> CommoditySymbol
T.strip CommoditySymbol
p, CommoditySymbol -> CommoditySymbol
T.strip (CommoditySymbol -> CommoditySymbol)
-> CommoditySymbol -> CommoditySymbol
forall a b. (a -> b) -> a -> b
$ Int -> CommoditySymbol -> CommoditySymbol
T.drop Int
1 CommoditySymbol
n)
  where
    (CommoditySymbol
p, CommoditySymbol
n) = (Char -> Bool) -> CommoditySymbol -> Tag
T.span (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/= Char
'|') CommoditySymbol
t

-- | Like payeeAndNoteFromDescription, but if there's no | then payee is empty.
payeeAndNoteFromDescription' :: Text -> (Text,Text)
payeeAndNoteFromDescription' :: CommoditySymbol -> Tag
payeeAndNoteFromDescription' CommoditySymbol
t =
  if Maybe Char -> Bool
forall a. Maybe a -> Bool
isJust (Maybe Char -> Bool) -> Maybe Char -> Bool
forall a b. (a -> b) -> a -> b
$ (Char -> Bool) -> CommoditySymbol -> Maybe Char
T.find (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
==Char
'|') CommoditySymbol
t then CommoditySymbol -> Tag
payeeAndNoteFromDescription CommoditySymbol
t else (CommoditySymbol
"",CommoditySymbol
t)


{-|
Render a journal transaction as text similar to the style of Ledger's print command.

Adapted from Ledger 2.x and 3.x standard format:

@
yyyy-mm-dd[ *][ CODE] description.........          [  ; comment...............]
    account name 1.....................  ...$amount1[  ; comment...............]
    account name 2.....................  ..$-amount1[  ; comment...............]

pcodewidth    = no limit -- 10          -- mimicking ledger layout.
pdescwidth    = no limit -- 20          -- I don't remember what these mean,
pacctwidth    = 35 minimum, no maximum  -- they were important at the time.
pamtwidth     = 11
pcommentwidth = no limit -- 22
@

The output will be parseable journal syntax.
To facilitate this, postings with explicit multi-commodity amounts
are displayed as multiple similar postings, one per commodity.
(Normally does not happen with this function).
-}
showTransaction :: Transaction -> Text
showTransaction :: Transaction -> CommoditySymbol
showTransaction = Text -> CommoditySymbol
TL.toStrict (Text -> CommoditySymbol)
-> (Transaction -> Text) -> Transaction -> CommoditySymbol
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Builder -> Text
TB.toLazyText (Builder -> Text)
-> (Transaction -> Builder) -> Transaction -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Bool -> Transaction -> Builder
showTransactionHelper Bool
False

-- | Like showTransaction, but explicit multi-commodity amounts
-- are shown on one line, comma-separated. In this case the output will
-- not be parseable journal syntax.
showTransactionOneLineAmounts :: Transaction -> Text
showTransactionOneLineAmounts :: Transaction -> CommoditySymbol
showTransactionOneLineAmounts = Text -> CommoditySymbol
TL.toStrict (Text -> CommoditySymbol)
-> (Transaction -> Text) -> Transaction -> CommoditySymbol
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Builder -> Text
TB.toLazyText (Builder -> Text)
-> (Transaction -> Builder) -> Transaction -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Bool -> Transaction -> Builder
showTransactionHelper Bool
True

-- | Helper for showTransaction*.
showTransactionHelper :: Bool -> Transaction -> TB.Builder
showTransactionHelper :: Bool -> Transaction -> Builder
showTransactionHelper Bool
onelineamounts Transaction
t =
      CommoditySymbol -> Builder
TB.fromText CommoditySymbol
descriptionline Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
newline
    Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> (CommoditySymbol -> Builder) -> [CommoditySymbol] -> Builder
forall (t :: * -> *) m a.
(Foldable t, Monoid m) =>
(a -> m) -> t a -> m
foldMap ((Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
newline) (Builder -> Builder)
-> (CommoditySymbol -> Builder) -> CommoditySymbol -> Builder
forall b c a. (b -> c) -> (a -> b) -> a -> c
. CommoditySymbol -> Builder
TB.fromText) [CommoditySymbol]
newlinecomments
    Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> (CommoditySymbol -> Builder) -> [CommoditySymbol] -> Builder
forall (t :: * -> *) m a.
(Foldable t, Monoid m) =>
(a -> m) -> t a -> m
foldMap ((Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
newline) (Builder -> Builder)
-> (CommoditySymbol -> Builder) -> CommoditySymbol -> Builder
forall b c a. (b -> c) -> (a -> b) -> a -> c
. CommoditySymbol -> Builder
TB.fromText) (Bool -> [Posting] -> [CommoditySymbol]
postingsAsLines Bool
onelineamounts ([Posting] -> [CommoditySymbol]) -> [Posting] -> [CommoditySymbol]
forall a b. (a -> b) -> a -> b
$ Transaction -> [Posting]
tpostings Transaction
t)
    Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
newline
  where
    descriptionline :: CommoditySymbol
descriptionline = CommoditySymbol -> CommoditySymbol
T.stripEnd (CommoditySymbol -> CommoditySymbol)
-> CommoditySymbol -> CommoditySymbol
forall a b. (a -> b) -> a -> b
$ Transaction -> CommoditySymbol
showTransactionLineFirstPart Transaction
t CommoditySymbol -> CommoditySymbol -> CommoditySymbol
forall a. Semigroup a => a -> a -> a
<> [CommoditySymbol] -> CommoditySymbol
T.concat [CommoditySymbol
desc, CommoditySymbol
samelinecomment]
    desc :: CommoditySymbol
desc = if CommoditySymbol -> Bool
T.null CommoditySymbol
d then CommoditySymbol
"" else CommoditySymbol
" " CommoditySymbol -> CommoditySymbol -> CommoditySymbol
forall a. Semigroup a => a -> a -> a
<> CommoditySymbol
d where d :: CommoditySymbol
d = Transaction -> CommoditySymbol
tdescription Transaction
t
    (CommoditySymbol
samelinecomment, [CommoditySymbol]
newlinecomments) =
      case CommoditySymbol -> [CommoditySymbol]
renderCommentLines (Transaction -> CommoditySymbol
tcomment Transaction
t) of []   -> (CommoditySymbol
"",[])
                                              CommoditySymbol
c:[CommoditySymbol]
cs -> (CommoditySymbol
c,[CommoditySymbol]
cs)
    newline :: Builder
newline = Char -> Builder
TB.singleton Char
'\n'

-- Useful when rendering error messages.
showTransactionLineFirstPart :: Transaction -> CommoditySymbol
showTransactionLineFirstPart Transaction
t = [CommoditySymbol] -> CommoditySymbol
T.concat [CommoditySymbol
date, CommoditySymbol
status, CommoditySymbol
code]
  where
    date :: CommoditySymbol
date = Day -> CommoditySymbol
showDate (Transaction -> Day
tdate Transaction
t) CommoditySymbol -> CommoditySymbol -> CommoditySymbol
forall a. Semigroup a => a -> a -> a
<> CommoditySymbol
-> (Day -> CommoditySymbol) -> Maybe Day -> CommoditySymbol
forall b a. b -> (a -> b) -> Maybe a -> b
maybe CommoditySymbol
"" ((CommoditySymbol
"="CommoditySymbol -> CommoditySymbol -> CommoditySymbol
forall a. Semigroup a => a -> a -> a
<>) (CommoditySymbol -> CommoditySymbol)
-> (Day -> CommoditySymbol) -> Day -> CommoditySymbol
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Day -> CommoditySymbol
showDate) (Transaction -> Maybe Day
tdate2 Transaction
t)
    status :: CommoditySymbol
status | Transaction -> Status
tstatus Transaction
t Status -> Status -> Bool
forall a. Eq a => a -> a -> Bool
== Status
Cleared = CommoditySymbol
" *"
           | Transaction -> Status
tstatus Transaction
t Status -> Status -> Bool
forall a. Eq a => a -> a -> Bool
== Status
Pending = CommoditySymbol
" !"
           | Bool
otherwise            = CommoditySymbol
""
    code :: CommoditySymbol
code = if CommoditySymbol -> Bool
T.null (Transaction -> CommoditySymbol
tcode Transaction
t) then CommoditySymbol
"" else CommoditySymbol
-> CommoditySymbol -> CommoditySymbol -> CommoditySymbol
wrap CommoditySymbol
" (" CommoditySymbol
")" (CommoditySymbol -> CommoditySymbol)
-> CommoditySymbol -> CommoditySymbol
forall a b. (a -> b) -> a -> b
$ Transaction -> CommoditySymbol
tcode Transaction
t

-- | Like showTransaction, but generates Beancount journal format.
showTransactionBeancount :: Transaction -> Text
showTransactionBeancount :: Transaction -> CommoditySymbol
showTransactionBeancount Transaction
t =
  -- https://beancount.github.io/docs/beancount_language_syntax.html
  -- similar to showTransactionHelper, but I haven't bothered with Builder
     CommoditySymbol
firstline CommoditySymbol -> CommoditySymbol -> CommoditySymbol
forall a. Semigroup a => a -> a -> a
<> CommoditySymbol
nl
  CommoditySymbol -> CommoditySymbol -> CommoditySymbol
forall a. Semigroup a => a -> a -> a
<> (CommoditySymbol -> CommoditySymbol)
-> [CommoditySymbol] -> CommoditySymbol
forall (t :: * -> *) m a.
(Foldable t, Monoid m) =>
(a -> m) -> t a -> m
foldMap ((CommoditySymbol -> CommoditySymbol -> CommoditySymbol
forall a. Semigroup a => a -> a -> a
<> CommoditySymbol
nl)) [CommoditySymbol]
newlinecomments
  CommoditySymbol -> CommoditySymbol -> CommoditySymbol
forall a. Semigroup a => a -> a -> a
<> (CommoditySymbol -> CommoditySymbol)
-> [CommoditySymbol] -> CommoditySymbol
forall (t :: * -> *) m a.
(Foldable t, Monoid m) =>
(a -> m) -> t a -> m
foldMap ((CommoditySymbol -> CommoditySymbol -> CommoditySymbol
forall a. Semigroup a => a -> a -> a
<> CommoditySymbol
nl)) ([Posting] -> [CommoditySymbol]
postingsAsLinesBeancount ([Posting] -> [CommoditySymbol]) -> [Posting] -> [CommoditySymbol]
forall a b. (a -> b) -> a -> b
$ Transaction -> [Posting]
tpostings Transaction
t)
  CommoditySymbol -> CommoditySymbol -> CommoditySymbol
forall a. Semigroup a => a -> a -> a
<> CommoditySymbol
nl
  where
    nl :: CommoditySymbol
nl = CommoditySymbol
"\n"
    firstline :: CommoditySymbol
firstline = [CommoditySymbol] -> CommoditySymbol
T.concat [CommoditySymbol
date, CommoditySymbol
status, CommoditySymbol
payee, CommoditySymbol
note, CommoditySymbol
tags, CommoditySymbol
samelinecomment]
    date :: CommoditySymbol
date = Day -> CommoditySymbol
showDate (Day -> CommoditySymbol) -> Day -> CommoditySymbol
forall a b. (a -> b) -> a -> b
$ Transaction -> Day
tdate Transaction
t
    status :: CommoditySymbol
status = if Transaction -> Status
tstatus Transaction
t Status -> Status -> Bool
forall a. Eq a => a -> a -> Bool
== Status
Pending then CommoditySymbol
" !" else CommoditySymbol
" *"
    (CommoditySymbol
payee,CommoditySymbol
note) =
      case CommoditySymbol -> Tag
payeeAndNoteFromDescription' (CommoditySymbol -> Tag) -> CommoditySymbol -> Tag
forall a b. (a -> b) -> a -> b
$ Transaction -> CommoditySymbol
tdescription Transaction
t of
        (CommoditySymbol
"",CommoditySymbol
"") -> (CommoditySymbol
"",      CommoditySymbol
""      )
        (CommoditySymbol
"",CommoditySymbol
n ) -> (CommoditySymbol
""     , CommoditySymbol -> CommoditySymbol
wrapq CommoditySymbol
n )
        (CommoditySymbol
p ,CommoditySymbol
"") -> (CommoditySymbol -> CommoditySymbol
wrapq CommoditySymbol
p, CommoditySymbol -> CommoditySymbol
wrapq CommoditySymbol
"")
        (CommoditySymbol
p ,CommoditySymbol
n ) -> (CommoditySymbol -> CommoditySymbol
wrapq CommoditySymbol
p, CommoditySymbol -> CommoditySymbol
wrapq CommoditySymbol
n )
      where
        wrapq :: CommoditySymbol -> CommoditySymbol
wrapq = CommoditySymbol
-> CommoditySymbol -> CommoditySymbol -> CommoditySymbol
wrap CommoditySymbol
" \"" CommoditySymbol
"\"" (CommoditySymbol -> CommoditySymbol)
-> (CommoditySymbol -> CommoditySymbol)
-> CommoditySymbol
-> CommoditySymbol
forall b c a. (b -> c) -> (a -> b) -> a -> c
. CommoditySymbol -> CommoditySymbol
escapeDoubleQuotes (CommoditySymbol -> CommoditySymbol)
-> (CommoditySymbol -> CommoditySymbol)
-> CommoditySymbol
-> CommoditySymbol
forall b c a. (b -> c) -> (a -> b) -> a -> c
. CommoditySymbol -> CommoditySymbol
escapeBackslash
    tags :: CommoditySymbol
tags = [CommoditySymbol] -> CommoditySymbol
T.concat ([CommoditySymbol] -> CommoditySymbol)
-> [CommoditySymbol] -> CommoditySymbol
forall a b. (a -> b) -> a -> b
$ (Tag -> CommoditySymbol) -> [Tag] -> [CommoditySymbol]
forall a b. (a -> b) -> [a] -> [b]
map ((CommoditySymbol
" #"CommoditySymbol -> CommoditySymbol -> CommoditySymbol
forall a. Semigroup a => a -> a -> a
<>)(CommoditySymbol -> CommoditySymbol)
-> (Tag -> CommoditySymbol) -> Tag -> CommoditySymbol
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Tag -> CommoditySymbol
forall a b. (a, b) -> a
fst) ([Tag] -> [CommoditySymbol]) -> [Tag] -> [CommoditySymbol]
forall a b. (a -> b) -> a -> b
$ Transaction -> [Tag]
ttags Transaction
t
    (CommoditySymbol
samelinecomment, [CommoditySymbol]
newlinecomments) =
      case CommoditySymbol -> [CommoditySymbol]
renderCommentLines (Transaction -> CommoditySymbol
tcomment Transaction
t) of []   -> (CommoditySymbol
"",[])
                                              CommoditySymbol
c:[CommoditySymbol]
cs -> (CommoditySymbol
c,[CommoditySymbol]
cs)

hasRealPostings :: Transaction -> Bool
hasRealPostings :: Transaction -> Bool
hasRealPostings = Bool -> Bool
not (Bool -> Bool) -> (Transaction -> Bool) -> Transaction -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Posting] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null ([Posting] -> Bool)
-> (Transaction -> [Posting]) -> Transaction -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> [Posting]
realPostings

realPostings :: Transaction -> [Posting]
realPostings :: Transaction -> [Posting]
realPostings = (Posting -> Bool) -> [Posting] -> [Posting]
forall a. (a -> Bool) -> [a] -> [a]
filter Posting -> Bool
isReal ([Posting] -> [Posting])
-> (Transaction -> [Posting]) -> Transaction -> [Posting]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> [Posting]
tpostings

assignmentPostings :: Transaction -> [Posting]
assignmentPostings :: Transaction -> [Posting]
assignmentPostings = (Posting -> Bool) -> [Posting] -> [Posting]
forall a. (a -> Bool) -> [a] -> [a]
filter Posting -> Bool
hasBalanceAssignment ([Posting] -> [Posting])
-> (Transaction -> [Posting]) -> Transaction -> [Posting]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> [Posting]
tpostings

virtualPostings :: Transaction -> [Posting]
virtualPostings :: Transaction -> [Posting]
virtualPostings = (Posting -> Bool) -> [Posting] -> [Posting]
forall a. (a -> Bool) -> [a] -> [a]
filter Posting -> Bool
isVirtual ([Posting] -> [Posting])
-> (Transaction -> [Posting]) -> Transaction -> [Posting]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> [Posting]
tpostings

balancedVirtualPostings :: Transaction -> [Posting]
balancedVirtualPostings :: Transaction -> [Posting]
balancedVirtualPostings = (Posting -> Bool) -> [Posting] -> [Posting]
forall a. (a -> Bool) -> [a] -> [a]
filter Posting -> Bool
isBalancedVirtual ([Posting] -> [Posting])
-> (Transaction -> [Posting]) -> Transaction -> [Posting]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> [Posting]
tpostings

transactionsPostings :: [Transaction] -> [Posting]
transactionsPostings :: [Transaction] -> [Posting]
transactionsPostings = (Transaction -> [Posting]) -> [Transaction] -> [Posting]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap Transaction -> [Posting]
tpostings

-- Get a transaction's secondary date, or the primary date if there is none.
transactionDate2 :: Transaction -> Day
transactionDate2 :: Transaction -> Day
transactionDate2 Transaction
t = Day -> Maybe Day -> Day
forall a. a -> Maybe a -> a
fromMaybe (Transaction -> Day
tdate Transaction
t) (Maybe Day -> Day) -> Maybe Day -> Day
forall a b. (a -> b) -> a -> b
$ Transaction -> Maybe Day
tdate2 Transaction
t

-- Get a transaction's primary or secondary date, as specified.
transactionDateOrDate2 :: WhichDate -> Transaction -> Day
transactionDateOrDate2 :: WhichDate -> Transaction -> Day
transactionDateOrDate2 WhichDate
PrimaryDate   = Transaction -> Day
tdate
transactionDateOrDate2 WhichDate
SecondaryDate = Transaction -> Day
transactionDate2

-- | Ensure a transaction's postings refer back to it, so that eg
-- relatedPostings works right.
txnTieKnot :: Transaction -> Transaction
txnTieKnot :: Transaction -> Transaction
txnTieKnot t :: Transaction
t@Transaction{tpostings :: Transaction -> [Posting]
tpostings=[Posting]
ps} = Transaction
t' where
    t' :: Transaction
t' = Transaction
t{tpostings :: [Posting]
tpostings=(Posting -> Posting) -> [Posting] -> [Posting]
forall a b. (a -> b) -> [a] -> [b]
map (Transaction -> Posting -> Posting
postingSetTransaction Transaction
t') [Posting]
ps}

-- | Ensure a transaction's postings do not refer back to it, so that eg
-- recursiveSize and GHCI's :sprint work right.
txnUntieKnot :: Transaction -> Transaction
txnUntieKnot :: Transaction -> Transaction
txnUntieKnot t :: Transaction
t@Transaction{tpostings :: Transaction -> [Posting]
tpostings=[Posting]
ps} = Transaction
t{tpostings :: [Posting]
tpostings=(Posting -> Posting) -> [Posting] -> [Posting]
forall a b. (a -> b) -> [a] -> [b]
map (\Posting
p -> Posting
p{ptransaction :: Maybe Transaction
ptransaction=Maybe Transaction
forall a. Maybe a
Nothing}) [Posting]
ps}

-- | Set a posting's parent transaction.
postingSetTransaction :: Transaction -> Posting -> Posting
postingSetTransaction :: Transaction -> Posting -> Posting
postingSetTransaction Transaction
t Posting
p = Posting
p{ptransaction :: Maybe Transaction
ptransaction=Transaction -> Maybe Transaction
forall a. a -> Maybe a
Just Transaction
t}

-- | Apply a transform function to this transaction's amounts.
transactionTransformPostings :: (Posting -> Posting) -> Transaction -> Transaction
transactionTransformPostings :: (Posting -> Posting) -> Transaction -> Transaction
transactionTransformPostings Posting -> Posting
f t :: Transaction
t@Transaction{tpostings :: Transaction -> [Posting]
tpostings=[Posting]
ps} = Transaction
t{tpostings :: [Posting]
tpostings=(Posting -> Posting) -> [Posting] -> [Posting]
forall a b. (a -> b) -> [a] -> [b]
map Posting -> Posting
f [Posting]
ps}

-- | Apply a specified valuation to this transaction's amounts, using
-- the provided price oracle, commodity styles, and reference dates.
-- See amountApplyValuation.
transactionApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> ValuationType -> Transaction -> Transaction
transactionApplyValuation :: PriceOracle
-> Map CommoditySymbol AmountStyle
-> Day
-> Day
-> ValuationType
-> Transaction
-> Transaction
transactionApplyValuation PriceOracle
priceoracle Map CommoditySymbol AmountStyle
styles Day
periodlast Day
today ValuationType
v =
  (Posting -> Posting) -> Transaction -> Transaction
transactionTransformPostings (PriceOracle
-> Map CommoditySymbol AmountStyle
-> Day
-> Day
-> ValuationType
-> Posting
-> Posting
postingApplyValuation PriceOracle
priceoracle Map CommoditySymbol AmountStyle
styles Day
periodlast Day
today ValuationType
v)

-- | Maybe convert this 'Transaction's amounts to cost.
transactionToCost :: ConversionOp -> Transaction -> Transaction
transactionToCost :: ConversionOp -> Transaction -> Transaction
transactionToCost ConversionOp
cost Transaction
t = Transaction
t{tpostings :: [Posting]
tpostings = (Posting -> Maybe Posting) -> [Posting] -> [Posting]
forall a b. (a -> Maybe b) -> [a] -> [b]
mapMaybe (ConversionOp -> Posting -> Maybe Posting
postingToCost ConversionOp
cost) ([Posting] -> [Posting]) -> [Posting] -> [Posting]
forall a b. (a -> b) -> a -> b
$ Transaction -> [Posting]
tpostings Transaction
t}

-- | Add inferred equity postings to a 'Transaction' using transaction prices.
transactionAddInferredEquityPostings :: Bool -> AccountName -> Transaction -> Transaction
transactionAddInferredEquityPostings :: Bool -> CommoditySymbol -> Transaction -> Transaction
transactionAddInferredEquityPostings Bool
verbosetags CommoditySymbol
equityAcct Transaction
t =
    Transaction
t{tpostings :: [Posting]
tpostings=(Posting -> [Posting]) -> [Posting] -> [Posting]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (Bool -> CommoditySymbol -> Posting -> [Posting]
postingAddInferredEquityPostings Bool
verbosetags CommoditySymbol
equityAcct) ([Posting] -> [Posting]) -> [Posting] -> [Posting]
forall a b. (a -> b) -> a -> b
$ Transaction -> [Posting]
tpostings Transaction
t}

type IdxPosting = (Int, Posting)

-- XXX Warning: The following code - for analysing equity conversion postings,
-- inferring missing costs and ignoring redundant costs -
-- is twisty and hard to follow.

label :: [Char] -> [Char] -> [Char]
label [Char]
s = (([Char]
s [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<> [Char]
": ")[Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++)

-- | Add costs inferred from equity postings in this transaction.
-- The name(s) of conversion equity accounts should be provided.
-- For every adjacent pair of conversion postings, it will first search the postings
-- with costs to see if any match. If so, it will tag these as matched.
-- If no postings with costs match, it will then search the postings without costs,
-- and will match the first such posting which matches one of the conversion amounts.
-- If it finds a match, it will add a cost and then tag it.
-- If the first argument is true, do a dry run instead: identify and tag
-- the costful and conversion postings, but don't add costs.
transactionInferCostsFromEquity :: Bool -> [AccountName] -> Transaction -> Either String Transaction
transactionInferCostsFromEquity :: Bool
-> [CommoditySymbol] -> Transaction -> Either [Char] Transaction
transactionInferCostsFromEquity Bool
dryrun [CommoditySymbol]
conversionaccts Transaction
t = (CommoditySymbol -> [Char])
-> Either CommoditySymbol Transaction -> Either [Char] Transaction
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first (Transaction -> [Char] -> [Char]
annotateErrorWithTransaction Transaction
t ([Char] -> [Char])
-> (CommoditySymbol -> [Char]) -> CommoditySymbol -> [Char]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. CommoditySymbol -> [Char]
T.unpack) (Either CommoditySymbol Transaction -> Either [Char] Transaction)
-> Either CommoditySymbol Transaction -> Either [Char] Transaction
forall a b. (a -> b) -> a -> b
$ do
  -- number the postings
  let npostings :: [(Int, Posting)]
npostings = [Int] -> [Posting] -> [(Int, Posting)]
forall a b. [a] -> [b] -> [(a, b)]
zip [Int
0..] ([Posting] -> [(Int, Posting)]) -> [Posting] -> [(Int, Posting)]
forall a b. (a -> b) -> a -> b
$ Transaction -> [Posting]
tpostings Transaction
t

  -- Identify all pairs of conversion postings and all other postings (with and without costs) in the transaction.
  ([((Int, Posting), (Int, Posting))]
conversionPairs, ([(Int, Posting)], [(Int, Posting)])
otherps) <- Bool
-> [CommoditySymbol]
-> [(Int, Posting)]
-> Either
     CommoditySymbol
     ([((Int, Posting), (Int, Posting))],
      ([(Int, Posting)], [(Int, Posting)]))
partitionAndCheckConversionPostings Bool
False [CommoditySymbol]
conversionaccts [(Int, Posting)]
npostings

  -- Generate a pure function that can be applied to each of this transaction's postings,
  -- possibly modifying it, to produce the following end result:
  -- 1. each pair of conversion postings, and the corresponding postings which balance them, are tagged for easy identification
  -- 2. each pair of balancing postings which did't have an explicit cost, have had a cost calculated and added to one of them
  -- 3. if any ambiguous situation was detected, an informative error is raised
  (Int, Posting) -> (Int, Posting)
processposting <- (((Int, Posting), (Int, Posting))
 -> StateT
      ([(Int, Posting)], [(Int, Posting)])
      (Either CommoditySymbol)
      ((Int, Posting) -> (Int, Posting)))
-> [((Int, Posting), (Int, Posting))]
-> ([(Int, Posting)], [(Int, Posting)])
-> Either CommoditySymbol ((Int, Posting) -> (Int, Posting))
transformIndexedPostingsF (Bool
-> ((Int, Posting), (Int, Posting))
-> StateT
     ([(Int, Posting)], [(Int, Posting)])
     (Either CommoditySymbol)
     ((Int, Posting) -> (Int, Posting))
addCostsToPostings Bool
dryrun) [((Int, Posting), (Int, Posting))]
conversionPairs ([(Int, Posting)], [(Int, Posting)])
otherps

  -- And if there was no error, use it to modify the transaction's postings.
  Transaction -> Either CommoditySymbol Transaction
forall (m :: * -> *) a. Monad m => a -> m a
return Transaction
t{tpostings :: [Posting]
tpostings = ((Int, Posting) -> Posting) -> [(Int, Posting)] -> [Posting]
forall a b. (a -> b) -> [a] -> [b]
map ((Int, Posting) -> Posting
forall a b. (a, b) -> b
snd ((Int, Posting) -> Posting)
-> ((Int, Posting) -> (Int, Posting)) -> (Int, Posting) -> Posting
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Int, Posting) -> (Int, Posting)
processposting) [(Int, Posting)]
npostings}

  where

    -- Generate the tricksy processposting function,
    -- which when applied to each posting in turn, rather magically has the effect of
    -- applying addCostsToPostings to each pair of conversion postings in the transaction,
    -- matching them with the other postings, tagging them and perhaps adding cost information to the other postings.
    -- General type:
    -- transformIndexedPostingsF :: (Monad m, Foldable t, Traversable t) =>
    --   (a -> StateT s m (a1 -> a1)) ->
    --   t a ->
    --   s ->
    --   m (a1 -> a1)
    -- Concrete type:
    transformIndexedPostingsF ::
      ((IdxPosting, IdxPosting) -> StateT ([IdxPosting],[IdxPosting]) (Either Text) (IdxPosting -> IdxPosting)) ->  -- state update function (addCostsToPostings with the bool applied)
      [(IdxPosting, IdxPosting)] ->   -- initial state: the pairs of adjacent conversion postings in the transaction
      ([IdxPosting],[IdxPosting]) ->  -- initial state: the other postings in the transaction, separated into costful and costless
      (Either Text (IdxPosting -> IdxPosting))  -- returns an error message or a posting transform function
    transformIndexedPostingsF :: (((Int, Posting), (Int, Posting))
 -> StateT
      ([(Int, Posting)], [(Int, Posting)])
      (Either CommoditySymbol)
      ((Int, Posting) -> (Int, Posting)))
-> [((Int, Posting), (Int, Posting))]
-> ([(Int, Posting)], [(Int, Posting)])
-> Either CommoditySymbol ((Int, Posting) -> (Int, Posting))
transformIndexedPostingsF ((Int, Posting), (Int, Posting))
-> StateT
     ([(Int, Posting)], [(Int, Posting)])
     (Either CommoditySymbol)
     ((Int, Posting) -> (Int, Posting))
updatefn = StateT
  ([(Int, Posting)], [(Int, Posting)])
  (Either CommoditySymbol)
  ((Int, Posting) -> (Int, Posting))
-> ([(Int, Posting)], [(Int, Posting)])
-> Either CommoditySymbol ((Int, Posting) -> (Int, Posting))
forall (m :: * -> *) s a. Monad m => StateT s m a -> s -> m a
evalStateT (StateT
   ([(Int, Posting)], [(Int, Posting)])
   (Either CommoditySymbol)
   ((Int, Posting) -> (Int, Posting))
 -> ([(Int, Posting)], [(Int, Posting)])
 -> Either CommoditySymbol ((Int, Posting) -> (Int, Posting)))
-> ([((Int, Posting), (Int, Posting))]
    -> StateT
         ([(Int, Posting)], [(Int, Posting)])
         (Either CommoditySymbol)
         ((Int, Posting) -> (Int, Posting)))
-> [((Int, Posting), (Int, Posting))]
-> ([(Int, Posting)], [(Int, Posting)])
-> Either CommoditySymbol ((Int, Posting) -> (Int, Posting))
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ([(Int, Posting) -> (Int, Posting)]
 -> (Int, Posting) -> (Int, Posting))
-> StateT
     ([(Int, Posting)], [(Int, Posting)])
     (Either CommoditySymbol)
     [(Int, Posting) -> (Int, Posting)]
-> StateT
     ([(Int, Posting)], [(Int, Posting)])
     (Either CommoditySymbol)
     ((Int, Posting) -> (Int, Posting))
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Endo (Int, Posting) -> (Int, Posting) -> (Int, Posting)
forall a. Endo a -> a -> a
appEndo (Endo (Int, Posting) -> (Int, Posting) -> (Int, Posting))
-> ([(Int, Posting) -> (Int, Posting)] -> Endo (Int, Posting))
-> [(Int, Posting) -> (Int, Posting)]
-> (Int, Posting)
-> (Int, Posting)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (((Int, Posting) -> (Int, Posting)) -> Endo (Int, Posting))
-> [(Int, Posting) -> (Int, Posting)] -> Endo (Int, Posting)
forall (t :: * -> *) m a.
(Foldable t, Monoid m) =>
(a -> m) -> t a -> m
foldMap ((Int, Posting) -> (Int, Posting)) -> Endo (Int, Posting)
forall a. (a -> a) -> Endo a
Endo) (StateT
   ([(Int, Posting)], [(Int, Posting)])
   (Either CommoditySymbol)
   [(Int, Posting) -> (Int, Posting)]
 -> StateT
      ([(Int, Posting)], [(Int, Posting)])
      (Either CommoditySymbol)
      ((Int, Posting) -> (Int, Posting)))
-> ([((Int, Posting), (Int, Posting))]
    -> StateT
         ([(Int, Posting)], [(Int, Posting)])
         (Either CommoditySymbol)
         [(Int, Posting) -> (Int, Posting)])
-> [((Int, Posting), (Int, Posting))]
-> StateT
     ([(Int, Posting)], [(Int, Posting)])
     (Either CommoditySymbol)
     ((Int, Posting) -> (Int, Posting))
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (((Int, Posting), (Int, Posting))
 -> StateT
      ([(Int, Posting)], [(Int, Posting)])
      (Either CommoditySymbol)
      ((Int, Posting) -> (Int, Posting)))
-> [((Int, Posting), (Int, Posting))]
-> StateT
     ([(Int, Posting)], [(Int, Posting)])
     (Either CommoditySymbol)
     [(Int, Posting) -> (Int, Posting)]
forall (t :: * -> *) (f :: * -> *) a b.
(Traversable t, Applicative f) =>
(a -> f b) -> t a -> f (t b)
traverse (((Int, Posting), (Int, Posting))
-> StateT
     ([(Int, Posting)], [(Int, Posting)])
     (Either CommoditySymbol)
     ((Int, Posting) -> (Int, Posting))
updatefn)

    -- A tricksy state update helper for processposting/transformIndexedPostingsF.
    -- Approximately: given a pair of conversion postings to match,
    -- and lists of the remaining unmatched costful and costless other postings,
    -- 1. find (and consume) two other postings which match the two conversion postings
    -- 2. add identifying tags to the four postings
    -- 3. add an explicit cost, if missing, to one of the matched other postings
    -- 4. or if there is a problem, raise an informative error or do nothing as appropriate.
    -- Or, if the first argument is true:
    -- do a dry run instead: find and consume, add tags, but don't add costs
    -- (and if there are no costful postings at all, do nothing).
    addCostsToPostings :: Bool -> (IdxPosting, IdxPosting) -> StateT ([IdxPosting], [IdxPosting]) (Either Text) (IdxPosting -> IdxPosting)
    addCostsToPostings :: Bool
-> ((Int, Posting), (Int, Posting))
-> StateT
     ([(Int, Posting)], [(Int, Posting)])
     (Either CommoditySymbol)
     ((Int, Posting) -> (Int, Posting))
addCostsToPostings Bool
dryrun' ((Int
n1, Posting
cp1), (Int
n2, Posting
cp2)) = (([(Int, Posting)], [(Int, Posting)])
 -> Either
      CommoditySymbol
      ((Int, Posting) -> (Int, Posting),
       ([(Int, Posting)], [(Int, Posting)])))
-> StateT
     ([(Int, Posting)], [(Int, Posting)])
     (Either CommoditySymbol)
     ((Int, Posting) -> (Int, Posting))
forall s (m :: * -> *) a. (s -> m (a, s)) -> StateT s m a
StateT ((([(Int, Posting)], [(Int, Posting)])
  -> Either
       CommoditySymbol
       ((Int, Posting) -> (Int, Posting),
        ([(Int, Posting)], [(Int, Posting)])))
 -> StateT
      ([(Int, Posting)], [(Int, Posting)])
      (Either CommoditySymbol)
      ((Int, Posting) -> (Int, Posting)))
-> (([(Int, Posting)], [(Int, Posting)])
    -> Either
         CommoditySymbol
         ((Int, Posting) -> (Int, Posting),
          ([(Int, Posting)], [(Int, Posting)])))
-> StateT
     ([(Int, Posting)], [(Int, Posting)])
     (Either CommoditySymbol)
     ((Int, Posting) -> (Int, Posting))
forall a b. (a -> b) -> a -> b
$ \([(Int, Posting)]
costps, [(Int, Posting)]
otherps) -> do
      -- Get the two conversion posting amounts, if possible
      Amount
ca1 <- Posting -> Either CommoditySymbol Amount
conversionPostingAmountNoCost Posting
cp1
      Amount
ca2 <- Posting -> Either CommoditySymbol Amount
conversionPostingAmountNoCost Posting
cp2
      let 
        -- All costful postings which match the conversion posting pair
        matchingCostPs :: [(Int, Posting)]
matchingCostPs =
          ([(Int, Posting)] -> [Char])
-> [(Int, Posting)] -> [(Int, Posting)]
forall a. Show a => (a -> [Char]) -> a -> a
dbg7With ([Char] -> [Char] -> [Char]
label [Char]
"matched costful postings"([Char] -> [Char])
-> ([(Int, Posting)] -> [Char]) -> [(Int, Posting)] -> [Char]
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Int -> [Char]
forall a. Show a => a -> [Char]
show(Int -> [Char])
-> ([(Int, Posting)] -> Int) -> [(Int, Posting)] -> [Char]
forall b c a. (b -> c) -> (a -> b) -> a -> c
.[(Int, Posting)] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length) ([(Int, Posting)] -> [(Int, Posting)])
-> [(Int, Posting)] -> [(Int, Posting)]
forall a b. (a -> b) -> a -> b
$ 
          ((Int, Posting) -> Maybe (Int, Posting))
-> [(Int, Posting)] -> [(Int, Posting)]
forall a b. (a -> Maybe b) -> [a] -> [b]
mapMaybe ((Posting -> Maybe Posting)
-> (Int, Posting) -> Maybe (Int, Posting)
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
(a -> m b) -> t a -> m (t b)
mapM ((Posting -> Maybe Posting)
 -> (Int, Posting) -> Maybe (Int, Posting))
-> (Posting -> Maybe Posting)
-> (Int, Posting)
-> Maybe (Int, Posting)
forall a b. (a -> b) -> a -> b
$ Amount -> Amount -> Posting -> Maybe Posting
costfulPostingIfMatchesBothAmounts Amount
ca1 Amount
ca2) [(Int, Posting)]
costps

        -- All other single-commodity postings whose amount matches at least one of the conversion postings,
        -- with an explicit cost added. Or in dry run mode, all other single-commodity postings.
        matchingOtherPs :: [(Int, (Posting, Amount))]
matchingOtherPs =
          ([(Int, (Posting, Amount))] -> [Char])
-> [(Int, (Posting, Amount))] -> [(Int, (Posting, Amount))]
forall a. Show a => (a -> [Char]) -> a -> a
dbg7With ([Char] -> [Char] -> [Char]
label [Char]
"matched costless postings"([Char] -> [Char])
-> ([(Int, (Posting, Amount))] -> [Char])
-> [(Int, (Posting, Amount))]
-> [Char]
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Int -> [Char]
forall a. Show a => a -> [Char]
show(Int -> [Char])
-> ([(Int, (Posting, Amount))] -> Int)
-> [(Int, (Posting, Amount))]
-> [Char]
forall b c a. (b -> c) -> (a -> b) -> a -> c
.[(Int, (Posting, Amount))] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length) ([(Int, (Posting, Amount))] -> [(Int, (Posting, Amount))])
-> [(Int, (Posting, Amount))] -> [(Int, (Posting, Amount))]
forall a b. (a -> b) -> a -> b
$
          if Bool
dryrun'
          then [(Int
n,(Posting
p, Amount
a)) | (Int
n,Posting
p) <- [(Int, Posting)]
otherps, let Just Amount
a = Posting -> Maybe Amount
postingSingleAmount Posting
p]
          else ((Int, Posting) -> Maybe (Int, (Posting, Amount)))
-> [(Int, Posting)] -> [(Int, (Posting, Amount))]
forall a b. (a -> Maybe b) -> [a] -> [b]
mapMaybe ((Posting -> Maybe (Posting, Amount))
-> (Int, Posting) -> Maybe (Int, (Posting, Amount))
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
(a -> m b) -> t a -> m (t b)
mapM ((Posting -> Maybe (Posting, Amount))
 -> (Int, Posting) -> Maybe (Int, (Posting, Amount)))
-> (Posting -> Maybe (Posting, Amount))
-> (Int, Posting)
-> Maybe (Int, (Posting, Amount))
forall a b. (a -> b) -> a -> b
$ Amount -> Amount -> Posting -> Maybe (Posting, Amount)
addCostIfMatchesOneAmount Amount
ca1 Amount
ca2) [(Int, Posting)]
otherps

        -- A function that adds a cost and/or tag to a numbered posting if appropriate.
        postingAddCostAndOrTag :: Int -> Posting -> (Int, Posting) -> (Int, Posting)
postingAddCostAndOrTag Int
np Posting
costp (Int
n,Posting
p) =
          (Int
n, if | Int
n Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
np            -> Posting
costp Posting -> [Tag] -> Posting
`postingAddTags` [(CommoditySymbol
"_price-matched",CommoditySymbol
"")]
                 | Int
n Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
n1 Bool -> Bool -> Bool
|| Int
n Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
n2 -> Posting
p     Posting -> [Tag] -> Posting
`postingAddTags` [(CommoditySymbol
"_conversion-matched",CommoditySymbol
"")]
                 | Bool
otherwise          -> Posting
p)

      -- Annotate any errors with the conversion posting pair
      (CommoditySymbol -> CommoditySymbol)
-> Either
     CommoditySymbol
     ((Int, Posting) -> (Int, Posting),
      ([(Int, Posting)], [(Int, Posting)]))
-> Either
     CommoditySymbol
     ((Int, Posting) -> (Int, Posting),
      ([(Int, Posting)], [(Int, Posting)]))
forall (p :: * -> * -> *) a b c.
Bifunctor p =>
(a -> b) -> p a c -> p b c
first ([Posting] -> CommoditySymbol -> CommoditySymbol
annotateWithPostings [Posting
cp1, Posting
cp2]) (Either
   CommoditySymbol
   ((Int, Posting) -> (Int, Posting),
    ([(Int, Posting)], [(Int, Posting)]))
 -> Either
      CommoditySymbol
      ((Int, Posting) -> (Int, Posting),
       ([(Int, Posting)], [(Int, Posting)])))
-> Either
     CommoditySymbol
     ((Int, Posting) -> (Int, Posting),
      ([(Int, Posting)], [(Int, Posting)]))
-> Either
     CommoditySymbol
     ((Int, Posting) -> (Int, Posting),
      ([(Int, Posting)], [(Int, Posting)]))
forall a b. (a -> b) -> a -> b
$
        if
          -- If a single costful posting matches the conversion postings,
          -- delete it from the list of costful postings in the state, delete the
          -- first matching costless posting from the list of costless postings
          -- in the state, and return the transformation function with the new state.
          | [(Int
np, Posting
costp)] <- [(Int, Posting)]
matchingCostPs
          , Just [(Int, Posting)]
newcostps <- Int -> [(Int, Posting)] -> Maybe [(Int, Posting)]
forall a b. Eq a => a -> [(a, b)] -> Maybe [(a, b)]
deleteIdx Int
np [(Int, Posting)]
costps
              -> ((Int, Posting) -> (Int, Posting),
 ([(Int, Posting)], [(Int, Posting)]))
-> Either
     CommoditySymbol
     ((Int, Posting) -> (Int, Posting),
      ([(Int, Posting)], [(Int, Posting)]))
forall a b. b -> Either a b
Right (Int -> Posting -> (Int, Posting) -> (Int, Posting)
postingAddCostAndOrTag Int
np Posting
costp, (if Bool
dryrun' then [(Int, Posting)]
costps else [(Int, Posting)]
newcostps, [(Int, Posting)]
otherps))

          -- If no costful postings match the conversion postings, but some
          -- of the costless postings match, check that the first such posting has a
          -- different amount from all the others, and if so add a cost to it,
          -- then delete it from the list of costless postings in the state,
          -- and return the transformation function with the new state.
          | [] <- [(Int, Posting)]
matchingCostPs
          , (Int
np, (Posting
costp, Amount
amt)):[(Int, (Posting, Amount))]
nps <- [(Int, (Posting, Amount))]
matchingOtherPs
          , Bool -> Bool
not (Bool -> Bool) -> Bool -> Bool
forall a b. (a -> b) -> a -> b
$ ((Int, (Posting, Amount)) -> Bool)
-> [(Int, (Posting, Amount))] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Amount -> Amount -> Bool
amountsMatch Amount
amt (Amount -> Bool)
-> ((Int, (Posting, Amount)) -> Amount)
-> (Int, (Posting, Amount))
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Posting, Amount) -> Amount
forall a b. (a, b) -> b
snd ((Posting, Amount) -> Amount)
-> ((Int, (Posting, Amount)) -> (Posting, Amount))
-> (Int, (Posting, Amount))
-> Amount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Int, (Posting, Amount)) -> (Posting, Amount)
forall a b. (a, b) -> b
snd) [(Int, (Posting, Amount))]
nps
          , Just [(Int, Posting)]
newotherps <- Int -> [(Int, Posting)] -> Maybe [(Int, Posting)]
forall a b. Eq a => a -> [(a, b)] -> Maybe [(a, b)]
deleteIdx Int
np [(Int, Posting)]
otherps
              -> ((Int, Posting) -> (Int, Posting),
 ([(Int, Posting)], [(Int, Posting)]))
-> Either
     CommoditySymbol
     ((Int, Posting) -> (Int, Posting),
      ([(Int, Posting)], [(Int, Posting)]))
forall a b. b -> Either a b
Right (Int -> Posting -> (Int, Posting) -> (Int, Posting)
postingAddCostAndOrTag Int
np Posting
costp, ([(Int, Posting)]
costps, if Bool
dryrun' then [(Int, Posting)]
otherps else [(Int, Posting)]
newotherps))

          -- Otherwise, do nothing, leaving the transaction unchanged.
          -- We don't want to be over-zealous reporting problems here
          -- since this is always called at least in dry run mode by
          -- journalFinalise > journalMarkRedundantCosts. (#2045)
          | Bool
otherwise -> ((Int, Posting) -> (Int, Posting),
 ([(Int, Posting)], [(Int, Posting)]))
-> Either
     CommoditySymbol
     ((Int, Posting) -> (Int, Posting),
      ([(Int, Posting)], [(Int, Posting)]))
forall a b. b -> Either a b
Right ((Int, Posting) -> (Int, Posting)
forall a. a -> a
id, ([(Int, Posting)]
costps, [(Int, Posting)]
otherps))

    -- If a posting with cost matches both the conversion amounts, return it along
    -- with the matching amount which must be present in another non-conversion posting.
    costfulPostingIfMatchesBothAmounts :: Amount -> Amount -> Posting -> Maybe Posting
    costfulPostingIfMatchesBothAmounts :: Amount -> Amount -> Posting -> Maybe Posting
costfulPostingIfMatchesBothAmounts Amount
a1 Amount
a2 Posting
costfulp = do
        a :: Amount
a@Amount{aprice :: Amount -> Maybe AmountPrice
aprice=Just AmountPrice
_} <- Posting -> Maybe Amount
postingSingleAmount Posting
costfulp
        if
           | Integer -> Amount -> Amount -> Bool -> Bool
forall a a. (Show a, Show a) => a -> Amount -> Amount -> a -> a
dbgamtmatch Integer
1 Amount
a1 Amount
a (Amount -> Amount -> Bool
amountsMatch (-Amount
a1) Amount
a)  Bool -> Bool -> Bool
&&  Integer -> Amount -> Amount -> Bool -> Bool
forall a a. (Show a, Show a) => a -> Amount -> Amount -> a -> a
dbgcostmatch Integer
2 Amount
a2 Amount
a (Amount -> Amount -> Bool
amountsMatch Amount
a2 (Amount -> Amount
amountCost Amount
a)) -> Posting -> Maybe Posting
forall a. a -> Maybe a
Just Posting
costfulp
           | Integer -> Amount -> Amount -> Bool -> Bool
forall a a. (Show a, Show a) => a -> Amount -> Amount -> a -> a
dbgamtmatch Integer
2 Amount
a2 Amount
a (Amount -> Amount -> Bool
amountsMatch (-Amount
a2) Amount
a)  Bool -> Bool -> Bool
&&  Integer -> Amount -> Amount -> Bool -> Bool
forall a a. (Show a, Show a) => a -> Amount -> Amount -> a -> a
dbgcostmatch Integer
1 Amount
a1 Amount
a (Amount -> Amount -> Bool
amountsMatch Amount
a1 (Amount -> Amount
amountCost Amount
a)) -> Posting -> Maybe Posting
forall a. a -> Maybe a
Just Posting
costfulp
           | Bool
otherwise -> Maybe Posting
forall a. Maybe a
Nothing
           where
            dbgamtmatch :: a -> Amount -> Amount -> a -> a
dbgamtmatch  a
n Amount
a Amount
b = [Char] -> a -> a
forall a. Show a => [Char] -> a -> a
dbg7 ([Char]
"conversion posting "     [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>a -> [Char]
forall a. Show a => a -> [Char]
show a
n[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>[Char]
" "[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>Amount -> [Char]
showAmount Amount
a[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>[Char]
" balances amount "[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>Amount -> [Char]
showAmountWithoutPrice Amount
b [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>[Char]
" of costful posting "[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>Amount -> [Char]
showAmount Amount
b[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>[Char]
" at precision "[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>Amount -> [Char]
dbgShowAmountPrecision Amount
a[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>[Char]
" ?")
            dbgcostmatch :: a -> Amount -> Amount -> a -> a
dbgcostmatch a
n Amount
a Amount
b = [Char] -> a -> a
forall a. Show a => [Char] -> a -> a
dbg7 ([Char]
"and\nconversion posting "[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>a -> [Char]
forall a. Show a => a -> [Char]
show a
n[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>[Char]
" "[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>Amount -> [Char]
showAmount Amount
a[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>[Char]
" matches cost "   [Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>Amount -> [Char]
showAmount (Amount -> Amount
amountCost Amount
b)[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>[Char]
" of costful posting "[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>Amount -> [Char]
showAmount Amount
b[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>[Char]
" at precision "[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>Amount -> [Char]
dbgShowAmountPrecision Amount
a[Char] -> [Char] -> [Char]
forall a. Semigroup a => a -> a -> a
<>[Char]
" ?") 

    -- Add a cost to a posting if it matches (negative) one of the
    -- supplied conversion amounts, adding the other amount as the cost.
    addCostIfMatchesOneAmount :: Amount -> Amount -> Posting -> Maybe (Posting, Amount)
    addCostIfMatchesOneAmount :: Amount -> Amount -> Posting -> Maybe (Posting, Amount)
addCostIfMatchesOneAmount Amount
a1 Amount
a2 Posting
p = do
        Amount
a <- Posting -> Maybe Amount
postingSingleAmount Posting
p
        let newp :: Amount -> Posting
newp Amount
cost = Posting
p{pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount Amount
a{aprice :: Maybe AmountPrice
aprice = AmountPrice -> Maybe AmountPrice
forall a. a -> Maybe a
Just (AmountPrice -> Maybe AmountPrice)
-> AmountPrice -> Maybe AmountPrice
forall a b. (a -> b) -> a -> b
$ Amount -> AmountPrice
TotalPrice Amount
cost}}
        if
           | Amount -> Amount -> Bool
amountsMatch (-Amount
a1) Amount
a -> (Posting, Amount) -> Maybe (Posting, Amount)
forall a. a -> Maybe a
Just (Amount -> Posting
newp Amount
a2, Amount
a2)
           | Amount -> Amount -> Bool
amountsMatch (-Amount
a2) Amount
a -> (Posting, Amount) -> Maybe (Posting, Amount)
forall a. a -> Maybe a
Just (Amount -> Posting
newp Amount
a1, Amount
a1)
           | Bool
otherwise            -> Maybe (Posting, Amount)
forall a. Maybe a
Nothing

    -- Get the single-commodity costless amount from a conversion posting, or raise an error.
    conversionPostingAmountNoCost :: Posting -> Either CommoditySymbol Amount
conversionPostingAmountNoCost Posting
p = case Posting -> Maybe Amount
postingSingleAmount Posting
p of
        Just a :: Amount
a@Amount{aprice :: Amount -> Maybe AmountPrice
aprice=Maybe AmountPrice
Nothing} -> Amount -> Either CommoditySymbol Amount
forall a b. b -> Either a b
Right Amount
a
        Just Amount{aprice :: Amount -> Maybe AmountPrice
aprice=Just AmountPrice
_} -> CommoditySymbol -> Either CommoditySymbol Amount
forall a b. a -> Either a b
Left (CommoditySymbol -> Either CommoditySymbol Amount)
-> CommoditySymbol -> Either CommoditySymbol Amount
forall a b. (a -> b) -> a -> b
$ [Posting] -> CommoditySymbol -> CommoditySymbol
annotateWithPostings [Posting
p] CommoditySymbol
"Conversion postings must not have a cost:"
        Maybe Amount
Nothing                    -> CommoditySymbol -> Either CommoditySymbol Amount
forall a b. a -> Either a b
Left (CommoditySymbol -> Either CommoditySymbol Amount)
-> CommoditySymbol -> Either CommoditySymbol Amount
forall a b. (a -> b) -> a -> b
$ [Posting] -> CommoditySymbol -> CommoditySymbol
annotateWithPostings [Posting
p] CommoditySymbol
"Conversion postings must have a single-commodity amount:"

    -- Do these amounts look the same when compared at the first's display precision ?
    amountsMatch :: Amount -> Amount -> Bool
amountsMatch Amount
a Amount
b = Amount -> Bool
amountLooksZero (Amount -> Bool) -> Amount -> Bool
forall a b. (a -> b) -> a -> b
$ AmountPrecision -> Amount -> Amount
amountSetPrecision (AmountStyle -> AmountPrecision
asprecision (AmountStyle -> AmountPrecision) -> AmountStyle -> AmountPrecision
forall a b. (a -> b) -> a -> b
$ Amount -> AmountStyle
astyle Amount
a) (Amount -> Amount) -> Amount -> Amount
forall a b. (a -> b) -> a -> b
$ Amount
a Amount -> Amount -> Amount
forall a. Num a => a -> a -> a
- Amount
b

    -- Delete a posting from the indexed list of postings based on either its
    -- index or its posting amount.
    -- Note: traversing the whole list to delete a single match is generally not efficient,
    -- but given that a transaction probably doesn't have more than four postings, it should
    -- still be more efficient than using a Map or another data structure. Even monster
    -- transactions with up to 10 postings, which are generally not a good
    -- idea, are still too small for there to be an advantage.
    -- XXX shouldn't assume transactions have few postings
    deleteIdx :: a -> [(a, b)] -> Maybe [(a, b)]
deleteIdx a
n = ((a, b) -> Bool) -> [(a, b)] -> Maybe [(a, b)]
forall a. (a -> Bool) -> [a] -> Maybe [a]
deleteUniqueMatch ((a
na -> a -> Bool
forall a. Eq a => a -> a -> Bool
==) (a -> Bool) -> ((a, b) -> a) -> (a, b) -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (a, b) -> a
forall a b. (a, b) -> a
fst)
    deleteUniqueMatch :: (a -> Bool) -> [a] -> Maybe [a]
deleteUniqueMatch a -> Bool
p (a
x:[a]
xs) | a -> Bool
p a
x       = if (a -> Bool) -> [a] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any a -> Bool
p [a]
xs then Maybe [a]
forall a. Maybe a
Nothing else [a] -> Maybe [a]
forall a. a -> Maybe a
Just [a]
xs
                               | Bool
otherwise = (a
xa -> [a] -> [a]
forall a. a -> [a] -> [a]
:) ([a] -> [a]) -> Maybe [a] -> Maybe [a]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (a -> Bool) -> [a] -> Maybe [a]
deleteUniqueMatch a -> Bool
p [a]
xs
    deleteUniqueMatch a -> Bool
_ []                 = Maybe [a]
forall a. Maybe a
Nothing
    annotateWithPostings :: [Posting] -> CommoditySymbol -> CommoditySymbol
annotateWithPostings [Posting]
xs CommoditySymbol
str = [CommoditySymbol] -> CommoditySymbol
T.unlines ([CommoditySymbol] -> CommoditySymbol)
-> [CommoditySymbol] -> CommoditySymbol
forall a b. (a -> b) -> a -> b
$ CommoditySymbol
str CommoditySymbol -> [CommoditySymbol] -> [CommoditySymbol]
forall a. a -> [a] -> [a]
: Bool -> [Posting] -> [CommoditySymbol]
postingsAsLines Bool
False [Posting]
xs

dbgShowAmountPrecision :: Amount -> [Char]
dbgShowAmountPrecision Amount
a =
  case AmountStyle -> AmountPrecision
asprecision (AmountStyle -> AmountPrecision) -> AmountStyle -> AmountPrecision
forall a b. (a -> b) -> a -> b
$ Amount -> AmountStyle
astyle Amount
a of
    Precision Word8
n      -> Word8 -> [Char]
forall a. Show a => a -> [Char]
show Word8
n
    AmountPrecision
NaturalPrecision -> Word8 -> [Char]
forall a. Show a => a -> [Char]
show (Word8 -> [Char]) -> Word8 -> [Char]
forall a b. (a -> b) -> a -> b
$ DecimalRaw Integer -> Word8
forall i. DecimalRaw i -> Word8
decimalPlaces (DecimalRaw Integer -> Word8) -> DecimalRaw Integer -> Word8
forall a b. (a -> b) -> a -> b
$ DecimalRaw Integer -> DecimalRaw Integer
forall i. Integral i => DecimalRaw i -> DecimalRaw i
normalizeDecimal (DecimalRaw Integer -> DecimalRaw Integer)
-> DecimalRaw Integer -> DecimalRaw Integer
forall a b. (a -> b) -> a -> b
$ Amount -> DecimalRaw Integer
aquantity Amount
a

-- Given the names of conversion equity accounts, sort the given indexed postings
-- into three lists of posting numbers (stored in two pairs), like so:
-- (conversion postings, (costful other postings, costless other postings)).
-- A true first argument activates its secondary function: check that all
-- conversion postings occur in adjacent pairs, otherwise return an error.
partitionAndCheckConversionPostings :: Bool -> [AccountName] -> [IdxPosting] -> Either Text ( [(IdxPosting, IdxPosting)], ([IdxPosting], [IdxPosting]) )
partitionAndCheckConversionPostings :: Bool
-> [CommoditySymbol]
-> [(Int, Posting)]
-> Either
     CommoditySymbol
     ([((Int, Posting), (Int, Posting))],
      ([(Int, Posting)], [(Int, Posting)]))
partitionAndCheckConversionPostings Bool
check [CommoditySymbol]
conversionaccts =
  -- Left fold processes postings in parse order, so that eg inferred costs
  -- will be added to the first (top-most) posting, not the last one.
  ((([((Int, Posting), (Int, Posting))],
   ([(Int, Posting)], [(Int, Posting)])),
  Maybe (Int, Posting))
 -> (Int, Posting)
 -> Either
      CommoditySymbol
      (([((Int, Posting), (Int, Posting))],
        ([(Int, Posting)], [(Int, Posting)])),
       Maybe (Int, Posting)))
-> (([((Int, Posting), (Int, Posting))],
     ([(Int, Posting)], [(Int, Posting)])),
    Maybe (Int, Posting))
-> [(Int, Posting)]
-> Either
     CommoditySymbol
     (([((Int, Posting), (Int, Posting))],
       ([(Int, Posting)], [(Int, Posting)])),
      Maybe (Int, Posting))
forall (t :: * -> *) (m :: * -> *) b a.
(Foldable t, Monad m) =>
(b -> a -> m b) -> b -> t a -> m b
foldlM (([((Int, Posting), (Int, Posting))],
  ([(Int, Posting)], [(Int, Posting)])),
 Maybe (Int, Posting))
-> (Int, Posting)
-> Either
     CommoditySymbol
     (([((Int, Posting), (Int, Posting))],
       ([(Int, Posting)], [(Int, Posting)])),
      Maybe (Int, Posting))
forall a a a.
IsString a =>
(([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])), Maybe a)
-> (a, Posting)
-> Either
     a
     (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
      Maybe (a, Posting))
select (([], ([], [])), Maybe (Int, Posting)
forall a. Maybe a
Nothing)
    -- The costless other postings are somehow reversed still; "second (second reverse" fixes that.
    ([(Int, Posting)]
 -> Either
      CommoditySymbol
      (([((Int, Posting), (Int, Posting))],
        ([(Int, Posting)], [(Int, Posting)])),
       Maybe (Int, Posting)))
-> (Either
      CommoditySymbol
      (([((Int, Posting), (Int, Posting))],
        ([(Int, Posting)], [(Int, Posting)])),
       Maybe (Int, Posting))
    -> Either
         CommoditySymbol
         ([((Int, Posting), (Int, Posting))],
          ([(Int, Posting)], [(Int, Posting)])))
-> [(Int, Posting)]
-> Either
     CommoditySymbol
     ([((Int, Posting), (Int, Posting))],
      ([(Int, Posting)], [(Int, Posting)]))
forall (f :: * -> *) a b. Functor f => f a -> (a -> b) -> f b
<&> ((([((Int, Posting), (Int, Posting))],
   ([(Int, Posting)], [(Int, Posting)])),
  Maybe (Int, Posting))
 -> ([((Int, Posting), (Int, Posting))],
     ([(Int, Posting)], [(Int, Posting)])))
-> Either
     CommoditySymbol
     (([((Int, Posting), (Int, Posting))],
       ([(Int, Posting)], [(Int, Posting)])),
      Maybe (Int, Posting))
-> Either
     CommoditySymbol
     ([((Int, Posting), (Int, Posting))],
      ([(Int, Posting)], [(Int, Posting)]))
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap ((([(Int, Posting)], [(Int, Posting)])
 -> ([(Int, Posting)], [(Int, Posting)]))
-> ([((Int, Posting), (Int, Posting))],
    ([(Int, Posting)], [(Int, Posting)]))
-> ([((Int, Posting), (Int, Posting))],
    ([(Int, Posting)], [(Int, Posting)]))
forall (p :: * -> * -> *) b c a.
Bifunctor p =>
(b -> c) -> p a b -> p a c
second (([(Int, Posting)] -> [(Int, Posting)])
-> ([(Int, Posting)], [(Int, Posting)])
-> ([(Int, Posting)], [(Int, Posting)])
forall (p :: * -> * -> *) b c a.
Bifunctor p =>
(b -> c) -> p a b -> p a c
second [(Int, Posting)] -> [(Int, Posting)]
forall a. [a] -> [a]
reverse) (([((Int, Posting), (Int, Posting))],
  ([(Int, Posting)], [(Int, Posting)]))
 -> ([((Int, Posting), (Int, Posting))],
     ([(Int, Posting)], [(Int, Posting)])))
-> ((([((Int, Posting), (Int, Posting))],
      ([(Int, Posting)], [(Int, Posting)])),
     Maybe (Int, Posting))
    -> ([((Int, Posting), (Int, Posting))],
        ([(Int, Posting)], [(Int, Posting)])))
-> (([((Int, Posting), (Int, Posting))],
     ([(Int, Posting)], [(Int, Posting)])),
    Maybe (Int, Posting))
-> ([((Int, Posting), (Int, Posting))],
    ([(Int, Posting)], [(Int, Posting)]))
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (([((Int, Posting), (Int, Posting))],
  ([(Int, Posting)], [(Int, Posting)])),
 Maybe (Int, Posting))
-> ([((Int, Posting), (Int, Posting))],
    ([(Int, Posting)], [(Int, Posting)]))
forall a b. (a, b) -> a
fst)
  where
    select :: (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])), Maybe a)
-> (a, Posting)
-> Either
     a
     (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
      Maybe (a, Posting))
select (([(a, (a, Posting))]
cs, others :: ([(a, Posting)], [(a, Posting)])
others@([(a, Posting)]
ps, [(a, Posting)]
os)), Maybe a
Nothing) np :: (a, Posting)
np@(a
_, Posting
p)
      | Posting -> Bool
isConversion Posting
p = (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
 Maybe (a, Posting))
-> Either
     a
     (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
      Maybe (a, Posting))
forall a b. b -> Either a b
Right (([(a, (a, Posting))]
cs, ([(a, Posting)], [(a, Posting)])
others),      (a, Posting) -> Maybe (a, Posting)
forall a. a -> Maybe a
Just (a, Posting)
np)
      | Posting -> Bool
hasCost Posting
p      = (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
 Maybe (a, Posting))
-> Either
     a
     (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
      Maybe (a, Posting))
forall a b. b -> Either a b
Right (([(a, (a, Posting))]
cs, ((a, Posting)
np(a, Posting) -> [(a, Posting)] -> [(a, Posting)]
forall a. a -> [a] -> [a]
:[(a, Posting)]
ps, [(a, Posting)]
os)), Maybe (a, Posting)
forall a. Maybe a
Nothing)
      | Bool
otherwise      = (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
 Maybe (a, Posting))
-> Either
     a
     (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
      Maybe (a, Posting))
forall a b. b -> Either a b
Right (([(a, (a, Posting))]
cs, ([(a, Posting)]
ps, (a, Posting)
np(a, Posting) -> [(a, Posting)] -> [(a, Posting)]
forall a. a -> [a] -> [a]
:[(a, Posting)]
os)), Maybe (a, Posting)
forall a. Maybe a
Nothing)
    select (([(a, (a, Posting))]
cs, others :: ([(a, Posting)], [(a, Posting)])
others@([(a, Posting)]
ps,[(a, Posting)]
os)), Just a
lst) np :: (a, Posting)
np@(a
_, Posting
p)
      | Posting -> Bool
isConversion Posting
p = (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
 Maybe (a, Posting))
-> Either
     a
     (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
      Maybe (a, Posting))
forall a b. b -> Either a b
Right (((a
lst, (a, Posting)
np)(a, (a, Posting)) -> [(a, (a, Posting))] -> [(a, (a, Posting))]
forall a. a -> [a] -> [a]
:[(a, (a, Posting))]
cs, ([(a, Posting)], [(a, Posting)])
others), Maybe (a, Posting)
forall a. Maybe a
Nothing)
      | Bool
check          = a
-> Either
     a
     (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
      Maybe (a, Posting))
forall a b. a -> Either a b
Left a
"Conversion postings must occur in adjacent pairs"
      | Bool
otherwise      = (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
 Maybe (a, Posting))
-> Either
     a
     (([(a, (a, Posting))], ([(a, Posting)], [(a, Posting)])),
      Maybe (a, Posting))
forall a b. b -> Either a b
Right (([(a, (a, Posting))]
cs, ([(a, Posting)]
ps, (a, Posting)
np(a, Posting) -> [(a, Posting)] -> [(a, Posting)]
forall a. a -> [a] -> [a]
:[(a, Posting)]
os)), Maybe (a, Posting)
forall a. Maybe a
Nothing)
    isConversion :: Posting -> Bool
isConversion Posting
p = Posting -> CommoditySymbol
paccount Posting
p CommoditySymbol -> [CommoditySymbol] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [CommoditySymbol]
conversionaccts
    hasCost :: Posting -> Bool
hasCost Posting
p = Maybe AmountPrice -> Bool
forall a. Maybe a -> Bool
isJust (Maybe AmountPrice -> Bool) -> Maybe AmountPrice -> Bool
forall a b. (a -> b) -> a -> b
$ Amount -> Maybe AmountPrice
aprice (Amount -> Maybe AmountPrice) -> Maybe Amount -> Maybe AmountPrice
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< Posting -> Maybe Amount
postingSingleAmount Posting
p

-- | Get a posting's amount if it is single-commodity.
postingSingleAmount :: Posting -> Maybe Amount
postingSingleAmount :: Posting -> Maybe Amount
postingSingleAmount Posting
p = case MixedAmount -> [Amount]
amountsRaw (Posting -> MixedAmount
pamount Posting
p) of
  [Amount
a] -> Amount -> Maybe Amount
forall a. a -> Maybe a
Just Amount
a
  [Amount]
_   -> Maybe Amount
forall a. Maybe a
Nothing

-- | Apply some account aliases to all posting account names in the transaction, as described by accountNameApplyAliases.
-- This can fail due to a bad replacement pattern in a regular expression alias.
transactionApplyAliases :: [AccountAlias] -> Transaction -> Either RegexError Transaction
transactionApplyAliases :: [AccountAlias] -> Transaction -> Either [Char] Transaction
transactionApplyAliases [AccountAlias]
aliases Transaction
t =
  case (Posting -> Either [Char] Posting)
-> [Posting] -> Either [Char] [Posting]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
(a -> m b) -> t a -> m (t b)
mapM ([AccountAlias] -> Posting -> Either [Char] Posting
postingApplyAliases [AccountAlias]
aliases) ([Posting] -> Either [Char] [Posting])
-> [Posting] -> Either [Char] [Posting]
forall a b. (a -> b) -> a -> b
$ Transaction -> [Posting]
tpostings Transaction
t of
    Right [Posting]
ps -> Transaction -> Either [Char] Transaction
forall a b. b -> Either a b
Right (Transaction -> Either [Char] Transaction)
-> Transaction -> Either [Char] Transaction
forall a b. (a -> b) -> a -> b
$ Transaction -> Transaction
txnTieKnot (Transaction -> Transaction) -> Transaction -> Transaction
forall a b. (a -> b) -> a -> b
$ Transaction
t{tpostings :: [Posting]
tpostings=[Posting]
ps}
    Left [Char]
err -> [Char] -> Either [Char] Transaction
forall a b. a -> Either a b
Left [Char]
err

-- | Apply a transformation to a transaction's postings.
transactionMapPostings :: (Posting -> Posting) -> Transaction -> Transaction
transactionMapPostings :: (Posting -> Posting) -> Transaction -> Transaction
transactionMapPostings Posting -> Posting
f t :: Transaction
t@Transaction{tpostings :: Transaction -> [Posting]
tpostings=[Posting]
ps} = Transaction
t{tpostings :: [Posting]
tpostings=(Posting -> Posting) -> [Posting] -> [Posting]
forall a b. (a -> b) -> [a] -> [b]
map Posting -> Posting
f [Posting]
ps}

-- | Apply a transformation to a transaction's posting amounts.
transactionMapPostingAmounts :: (MixedAmount -> MixedAmount) -> Transaction -> Transaction
transactionMapPostingAmounts :: (MixedAmount -> MixedAmount) -> Transaction -> Transaction
transactionMapPostingAmounts MixedAmount -> MixedAmount
f  = (Posting -> Posting) -> Transaction -> Transaction
transactionMapPostings ((MixedAmount -> MixedAmount) -> Posting -> Posting
postingTransformAmount MixedAmount -> MixedAmount
f)

-- | All posting amounts from this transactin, in order.
transactionAmounts :: Transaction -> [MixedAmount]
transactionAmounts :: Transaction -> [MixedAmount]
transactionAmounts = (Posting -> MixedAmount) -> [Posting] -> [MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map Posting -> MixedAmount
pamount ([Posting] -> [MixedAmount])
-> (Transaction -> [Posting]) -> Transaction -> [MixedAmount]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> [Posting]
tpostings

-- | The file path from which this transaction was parsed.
transactionFile :: Transaction -> FilePath
transactionFile :: Transaction -> [Char]
transactionFile Transaction{(SourcePos, SourcePos)
tsourcepos :: (SourcePos, SourcePos)
tsourcepos :: Transaction -> (SourcePos, SourcePos)
tsourcepos} = SourcePos -> [Char]
sourceName (SourcePos -> [Char]) -> SourcePos -> [Char]
forall a b. (a -> b) -> a -> b
$ (SourcePos, SourcePos) -> SourcePos
forall a b. (a, b) -> a
fst (SourcePos, SourcePos)
tsourcepos

-- Add transaction information to an error message.
annotateErrorWithTransaction :: Transaction -> String -> String
annotateErrorWithTransaction :: Transaction -> [Char] -> [Char]
annotateErrorWithTransaction Transaction
t [Char]
s =
  [[Char]] -> [Char]
unlines [ (SourcePos, SourcePos) -> [Char]
sourcePosPairPretty ((SourcePos, SourcePos) -> [Char])
-> (SourcePos, SourcePos) -> [Char]
forall a b. (a -> b) -> a -> b
$ Transaction -> (SourcePos, SourcePos)
tsourcepos Transaction
t, [Char]
s
          , CommoditySymbol -> [Char]
T.unpack (CommoditySymbol -> [Char])
-> (CommoditySymbol -> CommoditySymbol)
-> CommoditySymbol
-> [Char]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. CommoditySymbol -> CommoditySymbol
T.stripEnd (CommoditySymbol -> [Char]) -> CommoditySymbol -> [Char]
forall a b. (a -> b) -> a -> b
$ Transaction -> CommoditySymbol
showTransaction Transaction
t
          ]

-- tests

tests_Transaction :: TestTree
tests_Transaction :: TestTree
tests_Transaction =
  [Char] -> [TestTree] -> TestTree
testGroup [Char]
"Transaction" [

      [Char] -> [TestTree] -> TestTree
testGroup [Char]
"showPostingLines" [
          [Char] -> Assertion -> TestTree
testCase [Char]
"null posting" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Posting -> [CommoditySymbol]
showPostingLines Posting
nullposting [CommoditySymbol] -> [CommoditySymbol] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?= [CommoditySymbol
"                   0"]
        , [Char] -> Assertion -> TestTree
testCase [Char]
"non-null posting" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
           let p :: Posting
p =
                Posting
posting
                  { pstatus :: Status
pstatus = Status
Cleared
                  , paccount :: CommoditySymbol
paccount = CommoditySymbol
"a"
                  , pamount :: MixedAmount
pamount = [Amount] -> MixedAmount
forall (t :: * -> *). Foldable t => t Amount -> MixedAmount
mixed [DecimalRaw Integer -> Amount
usd DecimalRaw Integer
1, DecimalRaw Integer -> Amount
hrs DecimalRaw Integer
2]
                  , pcomment :: CommoditySymbol
pcomment = CommoditySymbol
"pcomment1\npcomment2\n  tag3: val3  \n"
                  , ptype :: PostingType
ptype = PostingType
RegularPosting
                  , ptags :: [Tag]
ptags = [(CommoditySymbol
"ptag1", CommoditySymbol
"val1"), (CommoditySymbol
"ptag2", CommoditySymbol
"val2")]
                  }
           in Posting -> [CommoditySymbol]
showPostingLines Posting
p [CommoditySymbol] -> [CommoditySymbol] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
              [ CommoditySymbol
"    * a         $1.00  ; pcomment1"
              , CommoditySymbol
"    ; pcomment2"
              , CommoditySymbol
"    ;   tag3: val3  "
              , CommoditySymbol
"    * a         2.00h  ; pcomment1"
              , CommoditySymbol
"    ; pcomment2"
              , CommoditySymbol
"    ;   tag3: val3  "
              ]
        ]

    , let
        -- one implicit amount
        timp :: Transaction
timp = Transaction
nulltransaction {tpostings :: [Posting]
tpostings = [CommoditySymbol
"a" CommoditySymbol -> Amount -> Posting
`post` DecimalRaw Integer -> Amount
usd DecimalRaw Integer
1, CommoditySymbol
"b" CommoditySymbol -> Amount -> Posting
`post` Amount
missingamt]}
        -- explicit amounts, balanced
        texp :: Transaction
texp = Transaction
nulltransaction {tpostings :: [Posting]
tpostings = [CommoditySymbol
"a" CommoditySymbol -> Amount -> Posting
`post` DecimalRaw Integer -> Amount
usd DecimalRaw Integer
1, CommoditySymbol
"b" CommoditySymbol -> Amount -> Posting
`post` DecimalRaw Integer -> Amount
usd (-DecimalRaw Integer
1)]}
        -- explicit amount, only one posting
        texp1 :: Transaction
texp1 = Transaction
nulltransaction {tpostings :: [Posting]
tpostings = [CommoditySymbol
"(a)" CommoditySymbol -> Amount -> Posting
`post` DecimalRaw Integer -> Amount
usd DecimalRaw Integer
1]}
        -- explicit amounts, two commodities, explicit balancing price
        texp2 :: Transaction
texp2 = Transaction
nulltransaction {tpostings :: [Posting]
tpostings = [CommoditySymbol
"a" CommoditySymbol -> Amount -> Posting
`post` DecimalRaw Integer -> Amount
usd DecimalRaw Integer
1, CommoditySymbol
"b" CommoditySymbol -> Amount -> Posting
`post` (DecimalRaw Integer -> Amount
hrs (-DecimalRaw Integer
1) Amount -> Amount -> Amount
`at` DecimalRaw Integer -> Amount
usd DecimalRaw Integer
1)]}
        -- explicit amounts, two commodities, implicit balancing price
        texp2b :: Transaction
texp2b = Transaction
nulltransaction {tpostings :: [Posting]
tpostings = [CommoditySymbol
"a" CommoditySymbol -> Amount -> Posting
`post` DecimalRaw Integer -> Amount
usd DecimalRaw Integer
1, CommoditySymbol
"b" CommoditySymbol -> Amount -> Posting
`post` DecimalRaw Integer -> Amount
hrs (-DecimalRaw Integer
1)]}
        -- one missing amount, not the last one
        t3 :: Transaction
t3 = Transaction
nulltransaction {tpostings :: [Posting]
tpostings = [CommoditySymbol
"a" CommoditySymbol -> Amount -> Posting
`post` DecimalRaw Integer -> Amount
usd DecimalRaw Integer
1, CommoditySymbol
"b" CommoditySymbol -> Amount -> Posting
`post` Amount
missingamt, CommoditySymbol
"c" CommoditySymbol -> Amount -> Posting
`post` DecimalRaw Integer -> Amount
usd (-DecimalRaw Integer
1)]}
        -- unbalanced amounts when precision is limited (#931)
        -- t4 = nulltransaction {tpostings = ["a" `post` usd (-0.01), "b" `post` usd (0.005), "c" `post` usd (0.005)]}
      in [Char] -> [TestTree] -> TestTree
testGroup [Char]
"postingsAsLines" [
              [Char] -> Assertion -> TestTree
testCase [Char]
"null-transaction" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [CommoditySymbol]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
nulltransaction) [CommoditySymbol] -> [CommoditySymbol] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?= []
            , [Char] -> Assertion -> TestTree
testCase [Char]
"implicit-amount" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [CommoditySymbol]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
timp) [CommoditySymbol] -> [CommoditySymbol] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
                  [ CommoditySymbol
"    a           $1.00"
                  , CommoditySymbol
"    b" -- implicit amount remains implicit
                  ]
            , [Char] -> Assertion -> TestTree
testCase [Char]
"explicit-amounts" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [CommoditySymbol]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
texp) [CommoditySymbol] -> [CommoditySymbol] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
                  [ CommoditySymbol
"    a           $1.00"
                  , CommoditySymbol
"    b          $-1.00"
                  ]
            , [Char] -> Assertion -> TestTree
testCase [Char]
"one-explicit-amount" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [CommoditySymbol]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
texp1) [CommoditySymbol] -> [CommoditySymbol] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
                  [ CommoditySymbol
"    (a)           $1.00"
                  ]
            , [Char] -> Assertion -> TestTree
testCase [Char]
"explicit-amounts-two-commodities" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [CommoditySymbol]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
texp2) [CommoditySymbol] -> [CommoditySymbol] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
                  [ CommoditySymbol
"    a             $1.00"
                  , CommoditySymbol
"    b    -1.00h @ $1.00"
                  ]
            , [Char] -> Assertion -> TestTree
testCase [Char]
"explicit-amounts-not-explicitly-balanced" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [CommoditySymbol]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
texp2b) [CommoditySymbol] -> [CommoditySymbol] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
                  [ CommoditySymbol
"    a           $1.00"
                  , CommoditySymbol
"    b          -1.00h"
                  ]
            , [Char] -> Assertion -> TestTree
testCase [Char]
"implicit-amount-not-last" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [CommoditySymbol]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
t3) [CommoditySymbol] -> [CommoditySymbol] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
                  [CommoditySymbol
"    a           $1.00", CommoditySymbol
"    b", CommoditySymbol
"    c          $-1.00"]
            -- , testCase "ensure-visibly-balanced" $
            --    in postingsAsLines False (tpostings t4) @?=
            --       ["    a          $-0.01", "    b           $0.005", "    c           $0.005"]

            ]

    , [Char] -> [TestTree] -> TestTree
testGroup [Char]
"showTransaction" [
          [Char] -> Assertion -> TestTree
testCase [Char]
"null transaction" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Transaction -> CommoditySymbol
showTransaction Transaction
nulltransaction CommoditySymbol -> CommoditySymbol -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?= CommoditySymbol
"0000-01-01\n\n"
        , [Char] -> Assertion -> TestTree
testCase [Char]
"non-null transaction" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Transaction -> CommoditySymbol
showTransaction
            Transaction
nulltransaction
              { tdate :: Day
tdate = Integer -> Int -> Int -> Day
fromGregorian Integer
2012 Int
05 Int
14
              , tdate2 :: Maybe Day
tdate2 = Day -> Maybe Day
forall a. a -> Maybe a
Just (Day -> Maybe Day) -> Day -> Maybe Day
forall a b. (a -> b) -> a -> b
$ Integer -> Int -> Int -> Day
fromGregorian Integer
2012 Int
05 Int
15
              , tstatus :: Status
tstatus = Status
Unmarked
              , tcode :: CommoditySymbol
tcode = CommoditySymbol
"code"
              , tdescription :: CommoditySymbol
tdescription = CommoditySymbol
"desc"
              , tcomment :: CommoditySymbol
tcomment = CommoditySymbol
"tcomment1\ntcomment2\n"
              , ttags :: [Tag]
ttags = [(CommoditySymbol
"ttag1", CommoditySymbol
"val1")]
              , tpostings :: [Posting]
tpostings =
                  [ Posting
nullposting
                      { pstatus :: Status
pstatus = Status
Cleared
                      , paccount :: CommoditySymbol
paccount = CommoditySymbol
"a"
                      , pamount :: MixedAmount
pamount = [Amount] -> MixedAmount
forall (t :: * -> *). Foldable t => t Amount -> MixedAmount
mixed [DecimalRaw Integer -> Amount
usd DecimalRaw Integer
1, DecimalRaw Integer -> Amount
hrs DecimalRaw Integer
2]
                      , pcomment :: CommoditySymbol
pcomment = CommoditySymbol
"\npcomment2\n"
                      , ptype :: PostingType
ptype = PostingType
RegularPosting
                      , ptags :: [Tag]
ptags = [(CommoditySymbol
"ptag1", CommoditySymbol
"val1"), (CommoditySymbol
"ptag2", CommoditySymbol
"val2")]
                      }
                  ]
              } CommoditySymbol -> CommoditySymbol -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
          [CommoditySymbol] -> CommoditySymbol
T.unlines
            [ CommoditySymbol
"2012-05-14=2012-05-15 (code) desc  ; tcomment1"
            , CommoditySymbol
"    ; tcomment2"
            , CommoditySymbol
"    * a         $1.00"
            , CommoditySymbol
"    ; pcomment2"
            , CommoditySymbol
"    * a         2.00h"
            , CommoditySymbol
"    ; pcomment2"
            , CommoditySymbol
""
            ]
        , [Char] -> Assertion -> TestTree
testCase [Char]
"show a balanced transaction" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          (let t :: Transaction
t =
                 Integer
-> CommoditySymbol
-> (SourcePos, SourcePos)
-> Day
-> Maybe Day
-> Status
-> CommoditySymbol
-> CommoditySymbol
-> CommoditySymbol
-> [Tag]
-> [Posting]
-> Transaction
Transaction
                   Integer
0
                   CommoditySymbol
""
                   (SourcePos, SourcePos)
nullsourcepos
                   (Integer -> Int -> Int -> Day
fromGregorian Integer
2007 Int
01 Int
28)
                   Maybe Day
forall a. Maybe a
Nothing
                   Status
Unmarked
                   CommoditySymbol
""
                   CommoditySymbol
"coopportunity"
                   CommoditySymbol
""
                   []
                   [ Posting
posting {paccount :: CommoditySymbol
paccount = CommoditySymbol
"expenses:food:groceries", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (DecimalRaw Integer -> Amount
usd DecimalRaw Integer
47.18), ptransaction :: Maybe Transaction
ptransaction = Transaction -> Maybe Transaction
forall a. a -> Maybe a
Just Transaction
t}
                   , Posting
posting {paccount :: CommoditySymbol
paccount = CommoditySymbol
"assets:checking", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (DecimalRaw Integer -> Amount
usd (-DecimalRaw Integer
47.18)), ptransaction :: Maybe Transaction
ptransaction = Transaction -> Maybe Transaction
forall a. a -> Maybe a
Just Transaction
t}
                   ]
            in Transaction -> CommoditySymbol
showTransaction Transaction
t) CommoditySymbol -> CommoditySymbol -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
          ([CommoditySymbol] -> CommoditySymbol
T.unlines
             [ CommoditySymbol
"2007-01-28 coopportunity"
             , CommoditySymbol
"    expenses:food:groceries          $47.18"
             , CommoditySymbol
"    assets:checking                 $-47.18"
             , CommoditySymbol
""
             ])
        , [Char] -> Assertion -> TestTree
testCase [Char]
"show an unbalanced transaction, should not elide" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          (Transaction -> CommoditySymbol
showTransaction
             (Transaction -> Transaction
txnTieKnot (Transaction -> Transaction) -> Transaction -> Transaction
forall a b. (a -> b) -> a -> b
$
              Integer
-> CommoditySymbol
-> (SourcePos, SourcePos)
-> Day
-> Maybe Day
-> Status
-> CommoditySymbol
-> CommoditySymbol
-> CommoditySymbol
-> [Tag]
-> [Posting]
-> Transaction
Transaction
                Integer
0
                CommoditySymbol
""
                (SourcePos, SourcePos)
nullsourcepos
                (Integer -> Int -> Int -> Day
fromGregorian Integer
2007 Int
01 Int
28)
                Maybe Day
forall a. Maybe a
Nothing
                Status
Unmarked
                CommoditySymbol
""
                CommoditySymbol
"coopportunity"
                CommoditySymbol
""
                []
                [ Posting
posting {paccount :: CommoditySymbol
paccount = CommoditySymbol
"expenses:food:groceries", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (DecimalRaw Integer -> Amount
usd DecimalRaw Integer
47.18)}
                , Posting
posting {paccount :: CommoditySymbol
paccount = CommoditySymbol
"assets:checking", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (DecimalRaw Integer -> Amount
usd (-DecimalRaw Integer
47.19))}
                ])) CommoditySymbol -> CommoditySymbol -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
          ([CommoditySymbol] -> CommoditySymbol
T.unlines
             [ CommoditySymbol
"2007-01-28 coopportunity"
             , CommoditySymbol
"    expenses:food:groceries          $47.18"
             , CommoditySymbol
"    assets:checking                 $-47.19"
             , CommoditySymbol
""
             ])
        , [Char] -> Assertion -> TestTree
testCase [Char]
"show a transaction with one posting and a missing amount" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          (Transaction -> CommoditySymbol
showTransaction
             (Transaction -> Transaction
txnTieKnot (Transaction -> Transaction) -> Transaction -> Transaction
forall a b. (a -> b) -> a -> b
$
              Integer
-> CommoditySymbol
-> (SourcePos, SourcePos)
-> Day
-> Maybe Day
-> Status
-> CommoditySymbol
-> CommoditySymbol
-> CommoditySymbol
-> [Tag]
-> [Posting]
-> Transaction
Transaction
                Integer
0
                CommoditySymbol
""
                (SourcePos, SourcePos)
nullsourcepos
                (Integer -> Int -> Int -> Day
fromGregorian Integer
2007 Int
01 Int
28)
                Maybe Day
forall a. Maybe a
Nothing
                Status
Unmarked
                CommoditySymbol
""
                CommoditySymbol
"coopportunity"
                CommoditySymbol
""
                []
                [Posting
posting {paccount :: CommoditySymbol
paccount = CommoditySymbol
"expenses:food:groceries", pamount :: MixedAmount
pamount = MixedAmount
missingmixedamt}])) CommoditySymbol -> CommoditySymbol -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
          ([CommoditySymbol] -> CommoditySymbol
T.unlines [CommoditySymbol
"2007-01-28 coopportunity", CommoditySymbol
"    expenses:food:groceries", CommoditySymbol
""])
        , [Char] -> Assertion -> TestTree
testCase [Char]
"show a transaction with a priced commodityless amount" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          (Transaction -> CommoditySymbol
showTransaction
             (Transaction -> Transaction
txnTieKnot (Transaction -> Transaction) -> Transaction -> Transaction
forall a b. (a -> b) -> a -> b
$
              Integer
-> CommoditySymbol
-> (SourcePos, SourcePos)
-> Day
-> Maybe Day
-> Status
-> CommoditySymbol
-> CommoditySymbol
-> CommoditySymbol
-> [Tag]
-> [Posting]
-> Transaction
Transaction
                Integer
0
                CommoditySymbol
""
                (SourcePos, SourcePos)
nullsourcepos
                (Integer -> Int -> Int -> Day
fromGregorian Integer
2010 Int
01 Int
01)
                Maybe Day
forall a. Maybe a
Nothing
                Status
Unmarked
                CommoditySymbol
""
                CommoditySymbol
"x"
                CommoditySymbol
""
                []
                [ Posting
posting {paccount :: CommoditySymbol
paccount = CommoditySymbol
"a", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ DecimalRaw Integer -> Amount
num DecimalRaw Integer
1 Amount -> Amount -> Amount
`at` (DecimalRaw Integer -> Amount
usd DecimalRaw Integer
2 Amount -> AmountPrecision -> Amount
`withPrecision` Word8 -> AmountPrecision
Precision Word8
0)}
                , Posting
posting {paccount :: CommoditySymbol
paccount = CommoditySymbol
"b", pamount :: MixedAmount
pamount = MixedAmount
missingmixedamt}
                ])) CommoditySymbol -> CommoditySymbol -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
          ([CommoditySymbol] -> CommoditySymbol
T.unlines [CommoditySymbol
"2010-01-01 x", CommoditySymbol
"    a          1 @ $2", CommoditySymbol
"    b", CommoditySymbol
""])
        ]
    ]