{-|

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 FlexibleContexts  #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Rank2Types        #-}
{-# LANGUAGE RecordWildCards   #-}

{-# LANGUAGE NamedFieldPuns #-}
module Hledger.Data.Transaction (
  -- * Transaction
  nulltransaction,
  transaction,
  txnTieKnot,
  txnUntieKnot,
  transactionCheckBalanced,
  -- * operations
  showAccountName,
  hasRealPostings,
  realPostings,
  assignmentPostings,
  virtualPostings,
  balancedVirtualPostings,
  transactionsPostings,
  BalancingOpts(..),
  balancingOpts,
  isTransactionBalanced,
  balanceTransaction,
  balanceTransactionHelper,
  transactionTransformPostings,
  transactionApplyValuation,
  transactionToCost,
  transactionApplyAliases,
  transactionMapPostings,
  transactionMapPostingAmounts,
  -- nonzerobalanceerror,
  -- * date operations
  transactionDate2,
  -- * transaction description parts
  transactionPayee,
  transactionNote,
  -- payeeAndNoteFromDescription,
  -- * rendering
  showTransaction,
  showTransactionOneLineAmounts,
  -- showPostingLine,
  showPostingLines,
  -- * GenericSourcePos
  sourceFilePath,
  sourceFirstLine,
  showGenericSourcePos,
  annotateErrorWithTransaction,
  transactionFile,
  -- * tests
  tests_Transaction
)
where

import Data.Default (Default(..))
import Data.Foldable (asum)
import Data.List (intercalate, partition)
import Data.List.Extra (nubSort)
import Data.Maybe (fromMaybe, isNothing, mapMaybe)
import Data.Text (Text)
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 qualified Data.Map as M
import Safe (maximumDef)

import Hledger.Utils
import Hledger.Data.Types
import Hledger.Data.Dates
import Hledger.Data.Posting
import Hledger.Data.Amount
import Hledger.Data.Valuation
import Text.Tabular.AsciiWide

sourceFilePath :: GenericSourcePos -> FilePath
sourceFilePath :: GenericSourcePos -> FilePath
sourceFilePath = \case
    GenericSourcePos FilePath
fp Int
_ Int
_ -> FilePath
fp
    JournalSourcePos FilePath
fp (Int, Int)
_ -> FilePath
fp

sourceFirstLine :: GenericSourcePos -> Int
sourceFirstLine :: GenericSourcePos -> Int
sourceFirstLine = \case
    GenericSourcePos FilePath
_ Int
line Int
_ -> Int
line
    JournalSourcePos FilePath
_ (Int
line, Int
_) -> Int
line

-- | Render source position in human-readable form.
-- Keep in sync with Hledger.UI.ErrorScreen.hledgerparseerrorpositionp (temporary). XXX
showGenericSourcePos :: GenericSourcePos -> String
showGenericSourcePos :: GenericSourcePos -> FilePath
showGenericSourcePos = \case
    GenericSourcePos FilePath
fp Int
line Int
column -> FilePath -> FilePath
forall a. Show a => a -> FilePath
show FilePath
fp FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
" (line " FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ Int -> FilePath
forall a. Show a => a -> FilePath
show Int
line FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
", column " FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ Int -> FilePath
forall a. Show a => a -> FilePath
show Int
column FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
")"
    JournalSourcePos FilePath
fp (Int
line, Int
line') -> FilePath -> FilePath
forall a. Show a => a -> FilePath
show FilePath
fp FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
" (lines " FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ Int -> FilePath
forall a. Show a => a -> FilePath
show Int
line FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
"-" FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ Int -> FilePath
forall a. Show a => a -> FilePath
show Int
line' FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
")"

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

-- | 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 -> Text
transactionPayee = Tag -> Text
forall a b. (a, b) -> a
fst (Tag -> Text) -> (Transaction -> Tag) -> Transaction -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Tag
payeeAndNoteFromDescription (Text -> Tag) -> (Transaction -> Text) -> Transaction -> Tag
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> Text
tdescription

transactionNote :: Transaction -> Text
transactionNote :: Transaction -> Text
transactionNote = Tag -> Text
forall a b. (a, b) -> b
snd (Tag -> Text) -> (Transaction -> Tag) -> Transaction -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Tag
payeeAndNoteFromDescription (Text -> Tag) -> (Transaction -> Text) -> Transaction -> Tag
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> Text
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 :: Text -> Tag
payeeAndNoteFromDescription Text
t
  | Text -> Bool
T.null Text
n = (Text
t, Text
t)
  | Bool
otherwise = (Text -> Text
T.strip Text
p, Text -> Text
T.strip (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ Int -> Text -> Text
T.drop Int
1 Text
n)
  where
    (Text
p, Text
n) = (Char -> Bool) -> Text -> Tag
T.span (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/= Char
'|') Text
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 -> Text
showTransaction = Text -> Text
TL.toStrict (Text -> Text) -> (Transaction -> Text) -> Transaction -> Text
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 -> Text
showTransactionOneLineAmounts = Text -> Text
TL.toStrict (Text -> Text) -> (Transaction -> Text) -> Transaction -> Text
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 =
      Text -> Builder
TB.fromText Text
descriptionline Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
newline
    Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> (Text -> Builder) -> [Text] -> 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) -> (Text -> Builder) -> Text -> Builder
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Builder
TB.fromText) [Text]
newlinecomments
    Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> (Text -> Builder) -> [Text] -> 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) -> (Text -> Builder) -> Text -> Builder
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Builder
TB.fromText) (Bool -> [Posting] -> [Text]
postingsAsLines Bool
onelineamounts ([Posting] -> [Text]) -> [Posting] -> [Text]
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 :: Text
descriptionline = Text -> Text
T.stripEnd (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ [Text] -> Text
T.concat [Text
date, Text
status, Text
code, Text
desc, Text
samelinecomment]
    date :: Text
date = Day -> Text
showDate (Transaction -> Day
tdate Transaction
t) Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> (Day -> Text) -> Maybe Day -> Text
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Text
"" ((Text
"="Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<>) (Text -> Text) -> (Day -> Text) -> Day -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Day -> Text
showDate) (Transaction -> Maybe Day
tdate2 Transaction
t)
    status :: Text
status | Transaction -> Status
tstatus Transaction
t Status -> Status -> Bool
forall a. Eq a => a -> a -> Bool
== Status
Cleared = Text
" *"
           | Transaction -> Status
tstatus Transaction
t Status -> Status -> Bool
forall a. Eq a => a -> a -> Bool
== Status
Pending = Text
" !"
           | Bool
otherwise            = Text
""
    code :: Text
code = if Text -> Bool
T.null (Transaction -> Text
tcode Transaction
t) then Text
"" else Text -> Text -> Text -> Text
wrap Text
" (" Text
")" (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ Transaction -> Text
tcode Transaction
t
    desc :: Text
desc = if Text -> Bool
T.null Text
d then Text
"" else Text
" " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
d where d :: Text
d = Transaction -> Text
tdescription Transaction
t
    (Text
samelinecomment, [Text]
newlinecomments) =
      case Text -> [Text]
renderCommentLines (Transaction -> Text
tcomment Transaction
t) of []   -> (Text
"",[])
                                              Text
c:[Text]
cs -> (Text
c,[Text]
cs)
    newline :: Builder
newline = Char -> Builder
TB.singleton Char
'\n'

-- | Render a transaction or posting's comment as indented, semicolon-prefixed comment lines.
-- The first line (unless empty) will have leading space, subsequent lines will have a larger indent.
renderCommentLines :: Text -> [Text]
renderCommentLines :: Text -> [Text]
renderCommentLines Text
t =
  case Text -> [Text]
T.lines Text
t of
    []      -> []
    [Text
l]     -> [Text -> Text
commentSpace (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ Text -> Text
comment Text
l]        -- single-line comment
    (Text
"":[Text]
ls) -> Text
"" Text -> [Text] -> [Text]
forall a. a -> [a] -> [a]
: (Text -> Text) -> [Text] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map (Text -> Text
lineIndent (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
comment) [Text]
ls  -- multi-line comment with empty first line
    (Text
l:[Text]
ls)  -> Text -> Text
commentSpace (Text -> Text
comment Text
l) Text -> [Text] -> [Text]
forall a. a -> [a] -> [a]
: (Text -> Text) -> [Text] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map (Text -> Text
lineIndent (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
comment) [Text]
ls
  where
    comment :: Text -> Text
comment = (Text
"; "Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<>)

-- | Given a transaction and its postings, render the postings, suitable
-- for `print` output. Normally this output will be valid journal syntax which
-- hledger can reparse (though it may include no-longer-valid balance assertions).
--
-- Explicit amounts are shown, any implicit amounts are not.
--
-- Postings with multicommodity explicit amounts are handled as follows:
-- if onelineamounts is true, these amounts are shown on one line,
-- comma-separated, and the output will not be valid journal syntax.
-- Otherwise, they are shown as several similar postings, one per commodity.
--
-- The output will appear to be a balanced transaction.
-- Amounts' display precisions, which may have been limited by commodity
-- directives, will be increased if necessary to ensure this.
--
-- Posting amounts will be aligned with each other, starting about 4 columns
-- beyond the widest account name (see postingAsLines for details).
postingsAsLines :: Bool -> [Posting] -> [Text]
postingsAsLines :: Bool -> [Posting] -> [Text]
postingsAsLines Bool
onelineamounts [Posting]
ps = (([Text], Int, Int) -> [Text]) -> [([Text], Int, Int)] -> [Text]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap ([Text], Int, Int) -> [Text]
forall a b c. (a, b, c) -> a
first3 [([Text], Int, Int)]
linesWithWidths
  where
    linesWithWidths :: [([Text], Int, Int)]
linesWithWidths = (Posting -> ([Text], Int, Int))
-> [Posting] -> [([Text], Int, Int)]
forall a b. (a -> b) -> [a] -> [b]
map (Bool -> Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
postingAsLines Bool
False Bool
onelineamounts Int
maxacctwidth Int
maxamtwidth) [Posting]
ps
    maxacctwidth :: Int
maxacctwidth = Int -> [Int] -> Int
forall a. Ord a => a -> [a] -> a
maximumDef Int
0 ([Int] -> Int) -> [Int] -> Int
forall a b. (a -> b) -> a -> b
$ (([Text], Int, Int) -> Int) -> [([Text], Int, Int)] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ([Text], Int, Int) -> Int
forall a b c. (a, b, c) -> b
second3 [([Text], Int, Int)]
linesWithWidths
    maxamtwidth :: Int
maxamtwidth  = Int -> [Int] -> Int
forall a. Ord a => a -> [a] -> a
maximumDef Int
0 ([Int] -> Int) -> [Int] -> Int
forall a b. (a -> b) -> a -> b
$ (([Text], Int, Int) -> Int) -> [([Text], Int, Int)] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ([Text], Int, Int) -> Int
forall a b c. (a, b, c) -> c
third3 [([Text], Int, Int)]
linesWithWidths

-- | Render one posting, on one or more lines, suitable for `print` output.
-- There will be an indented account name, plus one or more of status flag,
-- posting amount, balance assertion, same-line comment, next-line comments.
--
-- If the posting's amount is implicit or if elideamount is true, no amount is shown.
--
-- If the posting's amount is explicit and multi-commodity, multiple similar
-- postings are shown, one for each commodity, to help produce parseable journal syntax.
-- Or if onelineamounts is true, such amounts are shown on one line, comma-separated
-- (and the output will not be valid journal syntax).
--
-- By default, 4 spaces (2 if there's a status flag) are shown between
-- account name and start of amount area, which is typically 12 chars wide
-- and contains a right-aligned amount (so 10-12 visible spaces between
-- account name and amount is typical).
-- When given a list of postings to be aligned with, the whitespace will be
-- increased if needed to match the posting with the longest account name.
-- This is used to align the amounts of a transaction's postings.
--
-- Also returns the account width and amount width used.
postingAsLines :: Bool -> Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
postingAsLines :: Bool -> Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
postingAsLines Bool
elideamount Bool
onelineamounts Int
acctwidth Int
amtwidth Posting
p =
    (([Text] -> [Text]) -> [[Text]] -> [Text]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap ([Text] -> [Text] -> [Text]
forall a. [a] -> [a] -> [a]
++ [Text]
newlinecomments) [[Text]]
postingblocks, Int
thisacctwidth, Int
thisamtwidth)
  where
    -- This needs to be converted to strict Text in order to strip trailing
    -- spaces. This adds a small amount of inefficiency, and the only difference
    -- is whether there are trailing spaces in print (and related) reports. This
    -- could be removed and we could just keep everything as a Text Builder, but
    -- would require adding trailing spaces to 42 failing tests.
    postingblocks :: [[Text]]
postingblocks = [(Text -> Text) -> [Text] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map Text -> Text
T.stripEnd ([Text] -> [Text]) -> (Text -> [Text]) -> Text -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> [Text]
T.lines (Text -> [Text]) -> (Text -> Text) -> Text -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
TL.toStrict (Text -> [Text]) -> Text -> [Text]
forall a b. (a -> b) -> a -> b
$
                       [Cell] -> Text
render [ Align -> Text -> Cell
textCell Align
BottomLeft Text
statusandaccount
                              , Align -> Text -> Cell
textCell Align
BottomLeft Text
"  "
                              , Align -> [WideBuilder] -> Cell
Cell Align
BottomLeft [WideBuilder -> WideBuilder
pad WideBuilder
amt]
                              , Align -> [WideBuilder] -> Cell
Cell Align
BottomLeft [WideBuilder
assertion]
                              , Align -> Text -> Cell
textCell Align
BottomLeft Text
samelinecomment
                              ]
                    | WideBuilder
amt <- [WideBuilder]
shownAmounts]
    render :: [Cell] -> Text
render = TableOpts -> Header Cell -> Text
renderRow TableOpts
forall a. Default a => a
def{tableBorders :: Bool
tableBorders=Bool
False, borderSpaces :: Bool
borderSpaces=Bool
False} (Header Cell -> Text) -> ([Cell] -> Header Cell) -> [Cell] -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Properties -> [Header Cell] -> Header Cell
forall h. Properties -> [Header h] -> Header h
Group Properties
NoLine ([Header Cell] -> Header Cell)
-> ([Cell] -> [Header Cell]) -> [Cell] -> Header Cell
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Cell -> Header Cell) -> [Cell] -> [Header Cell]
forall a b. (a -> b) -> [a] -> [b]
map Cell -> Header Cell
forall h. h -> Header h
Header
    pad :: WideBuilder -> WideBuilder
pad WideBuilder
amt = Builder -> Int -> WideBuilder
WideBuilder (Text -> Builder
TB.fromText (Text -> Builder) -> Text -> Builder
forall a b. (a -> b) -> a -> b
$ Int -> Text -> Text
T.replicate Int
w Text
" ") Int
w WideBuilder -> WideBuilder -> WideBuilder
forall a. Semigroup a => a -> a -> a
<> WideBuilder
amt
      where w :: Int
w = Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
12 Int
amtwidth Int -> Int -> Int
forall a. Num a => a -> a -> a
- WideBuilder -> Int
wbWidth WideBuilder
amt  -- min. 12 for backwards compatibility

    assertion :: WideBuilder
assertion = WideBuilder
-> (BalanceAssertion -> WideBuilder)
-> Maybe BalanceAssertion
-> WideBuilder
forall b a. b -> (a -> b) -> Maybe a -> b
maybe WideBuilder
forall a. Monoid a => a
mempty ((Builder -> Int -> WideBuilder
WideBuilder (Char -> Builder
TB.singleton Char
' ') Int
1 WideBuilder -> WideBuilder -> WideBuilder
forall a. Semigroup a => a -> a -> a
<>)(WideBuilder -> WideBuilder)
-> (BalanceAssertion -> WideBuilder)
-> BalanceAssertion
-> WideBuilder
forall b c a. (b -> c) -> (a -> b) -> a -> c
.BalanceAssertion -> WideBuilder
showBalanceAssertion) (Maybe BalanceAssertion -> WideBuilder)
-> Maybe BalanceAssertion -> WideBuilder
forall a b. (a -> b) -> a -> b
$ Posting -> Maybe BalanceAssertion
pbalanceassertion Posting
p
    -- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned
    statusandaccount :: Text
statusandaccount = Text -> Text
lineIndent (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Maybe Int -> Maybe Int -> Bool -> Bool -> Text -> Text
fitText (Int -> Maybe Int
forall a. a -> Maybe a
Just (Int -> Maybe Int) -> Int -> Maybe Int
forall a b. (a -> b) -> a -> b
$ Int
2 Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
acctwidth) Maybe Int
forall a. Maybe a
Nothing Bool
False Bool
True (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ Posting -> Text
pstatusandacct Posting
p
    thisacctwidth :: Int
thisacctwidth = Text -> Int
textWidth (Text -> Int) -> Text -> Int
forall a b. (a -> b) -> a -> b
$ Posting -> Text
pacctstr Posting
p

    pacctstr :: Posting -> Text
pacctstr Posting
p' = Maybe Int -> PostingType -> Text -> Text
showAccountName Maybe Int
forall a. Maybe a
Nothing (Posting -> PostingType
ptype Posting
p') (Posting -> Text
paccount Posting
p')
    pstatusandacct :: Posting -> Text
pstatusandacct Posting
p' = Posting -> Text
pstatusprefix Posting
p' Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Posting -> Text
pacctstr Posting
p'
    pstatusprefix :: Posting -> Text
pstatusprefix Posting
p' = case Posting -> Status
pstatus Posting
p' of
        Status
Unmarked -> Text
""
        Status
s        -> FilePath -> Text
T.pack (Status -> FilePath
forall a. Show a => a -> FilePath
show Status
s) Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" "

    -- currently prices are considered part of the amount string when right-aligning amounts
    -- Since we will usually be calling this function with the knot tied between
    -- amtwidth and thisamtwidth, make sure thisamtwidth does not depend on
    -- amtwidth at all.
    shownAmounts :: [WideBuilder]
shownAmounts
      | Bool
elideamount = [WideBuilder
forall a. Monoid a => a
mempty]
      | Bool
otherwise   = AmountDisplayOpts -> MixedAmount -> [WideBuilder]
showMixedAmountLinesB AmountDisplayOpts
noColour{displayOneLine :: Bool
displayOneLine=Bool
onelineamounts} (MixedAmount -> [WideBuilder]) -> MixedAmount -> [WideBuilder]
forall a b. (a -> b) -> a -> b
$ Posting -> MixedAmount
pamount Posting
p
    thisamtwidth :: Int
thisamtwidth = Int -> [Int] -> Int
forall a. Ord a => a -> [a] -> a
maximumDef Int
0 ([Int] -> Int) -> [Int] -> Int
forall a b. (a -> b) -> a -> b
$ (WideBuilder -> Int) -> [WideBuilder] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map WideBuilder -> Int
wbWidth [WideBuilder]
shownAmounts

    (Text
samelinecomment, [Text]
newlinecomments) =
      case Text -> [Text]
renderCommentLines (Posting -> Text
pcomment Posting
p) of []   -> (Text
"",[])
                                              Text
c:[Text]
cs -> (Text
c,[Text]
cs)

-- | Render a balance assertion, as the =[=][*] symbol and expected amount.
showBalanceAssertion :: BalanceAssertion -> WideBuilder
showBalanceAssertion :: BalanceAssertion -> WideBuilder
showBalanceAssertion BalanceAssertion{Bool
GenericSourcePos
Amount
baposition :: BalanceAssertion -> GenericSourcePos
bainclusive :: BalanceAssertion -> Bool
batotal :: BalanceAssertion -> Bool
baamount :: BalanceAssertion -> Amount
baposition :: GenericSourcePos
bainclusive :: Bool
batotal :: Bool
baamount :: Amount
..} =
    Char -> WideBuilder
singleton Char
'=' WideBuilder -> WideBuilder -> WideBuilder
forall a. Semigroup a => a -> a -> a
<> WideBuilder
eq WideBuilder -> WideBuilder -> WideBuilder
forall a. Semigroup a => a -> a -> a
<> WideBuilder
ast WideBuilder -> WideBuilder -> WideBuilder
forall a. Semigroup a => a -> a -> a
<> Char -> WideBuilder
singleton Char
' ' WideBuilder -> WideBuilder -> WideBuilder
forall a. Semigroup a => a -> a -> a
<> AmountDisplayOpts -> Amount -> WideBuilder
showAmountB AmountDisplayOpts
forall a. Default a => a
def{displayZeroCommodity :: Bool
displayZeroCommodity=Bool
True} Amount
baamount
  where
    eq :: WideBuilder
eq  = if Bool
batotal     then Char -> WideBuilder
singleton Char
'=' else WideBuilder
forall a. Monoid a => a
mempty
    ast :: WideBuilder
ast = if Bool
bainclusive then Char -> WideBuilder
singleton Char
'*' else WideBuilder
forall a. Monoid a => a
mempty
    singleton :: Char -> WideBuilder
singleton Char
c = Builder -> Int -> WideBuilder
WideBuilder (Char -> Builder
TB.singleton Char
c) Int
1

-- | Render a posting, simply. Used in balance assertion errors.
-- showPostingLine p =
--   lineIndent $
--   if pstatus p == Cleared then "* " else "" ++  -- XXX show !
--   showAccountName Nothing (ptype p) (paccount p) ++
--   "    " ++
--   showMixedAmountOneLine (pamount p) ++
--   assertion
--   where
--     -- XXX extract, handle ==
--     assertion = maybe "" ((" = " ++) . showAmountWithZeroCommodity . baamount) $ pbalanceassertion p

-- | Render a posting, at the appropriate width for aligning with
-- its siblings if any. Used by the rewrite command.
showPostingLines :: Posting -> [Text]
showPostingLines :: Posting -> [Text]
showPostingLines Posting
p = ([Text], Int, Int) -> [Text]
forall a b c. (a, b, c) -> a
first3 (([Text], Int, Int) -> [Text]) -> ([Text], Int, Int) -> [Text]
forall a b. (a -> b) -> a -> b
$ Bool -> Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
postingAsLines Bool
False Bool
False Int
maxacctwidth Int
maxamtwidth Posting
p
  where
    linesWithWidths :: [([Text], Int, Int)]
linesWithWidths = (Posting -> ([Text], Int, Int))
-> [Posting] -> [([Text], Int, Int)]
forall a b. (a -> b) -> [a] -> [b]
map (Bool -> Bool -> Int -> Int -> Posting -> ([Text], Int, Int)
postingAsLines Bool
False Bool
False Int
maxacctwidth Int
maxamtwidth) ([Posting] -> [([Text], Int, Int)])
-> (Maybe Transaction -> [Posting])
-> Maybe Transaction
-> [([Text], Int, Int)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Posting]
-> (Transaction -> [Posting]) -> Maybe Transaction -> [Posting]
forall b a. b -> (a -> b) -> Maybe a -> b
maybe [Posting
p] Transaction -> [Posting]
tpostings (Maybe Transaction -> [([Text], Int, Int)])
-> Maybe Transaction -> [([Text], Int, Int)]
forall a b. (a -> b) -> a -> b
$ Posting -> Maybe Transaction
ptransaction Posting
p
    maxacctwidth :: Int
maxacctwidth = Int -> [Int] -> Int
forall a. Ord a => a -> [a] -> a
maximumDef Int
0 ([Int] -> Int) -> [Int] -> Int
forall a b. (a -> b) -> a -> b
$ (([Text], Int, Int) -> Int) -> [([Text], Int, Int)] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ([Text], Int, Int) -> Int
forall a b c. (a, b, c) -> b
second3 [([Text], Int, Int)]
linesWithWidths
    maxamtwidth :: Int
maxamtwidth  = Int -> [Int] -> Int
forall a. Ord a => a -> [a] -> a
maximumDef Int
0 ([Int] -> Int) -> [Int] -> Int
forall a b. (a -> b) -> a -> b
$ (([Text], Int, Int) -> Int) -> [([Text], Int, Int)] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map ([Text], Int, Int) -> Int
forall a b c. (a, b, c) -> c
third3 [([Text], Int, Int)]
linesWithWidths

-- | Prepend a suitable indent for a posting (or transaction/posting comment) line.
lineIndent :: Text -> Text
lineIndent :: Text -> Text
lineIndent = (Text
"    "Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<>)

-- | Prepend the space required before a same-line comment.
commentSpace :: Text -> Text
commentSpace :: Text -> Text
commentSpace = (Text
"  "Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<>)

-- | Show an account name, clipped to the given width if any, and
-- appropriately bracketed/parenthesised for the given posting type.
showAccountName :: Maybe Int -> PostingType -> AccountName -> Text
showAccountName :: Maybe Int -> PostingType -> Text -> Text
showAccountName Maybe Int
w = PostingType -> Text -> Text
fmt
  where
    fmt :: PostingType -> Text -> Text
fmt PostingType
RegularPosting         = (Text -> Text)
-> (Int -> Text -> Text) -> Maybe Int -> Text -> Text
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Text -> Text
forall a. a -> a
id Int -> Text -> Text
T.take Maybe Int
w
    fmt PostingType
VirtualPosting         = Text -> Text -> Text -> Text
wrap Text
"(" Text
")" (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text -> Text)
-> (Int -> Text -> Text) -> Maybe Int -> Text -> Text
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Text -> Text
forall a. a -> a
id (Int -> Text -> Text
T.takeEnd (Int -> Text -> Text) -> (Int -> Int) -> Int -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Int -> Int
forall a. Num a => a -> a -> a
subtract Int
2) Maybe Int
w
    fmt PostingType
BalancedVirtualPosting = Text -> Text -> Text -> Text
wrap Text
"[" Text
"]" (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text -> Text)
-> (Int -> Text -> Text) -> Maybe Int -> Text -> Text
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Text -> Text
forall a. a -> a
id (Int -> Text -> Text
T.takeEnd (Int -> Text -> Text) -> (Int -> Int) -> Int -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Int -> Int
forall a. Num a => a -> a -> a
subtract Int
2) Maybe Int
w

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

data BalancingOpts = BalancingOpts
  { BalancingOpts -> Bool
ignore_assertions_ :: Bool  -- ^ Ignore balance assertions
  , BalancingOpts -> Bool
infer_prices_      :: Bool  -- ^ Infer prices in unbalanced multicommodity amounts
  , BalancingOpts -> Maybe (Map Text AmountStyle)
commodity_styles_  :: Maybe (M.Map CommoditySymbol AmountStyle)  -- ^ commodity display styles
  } deriving (Int -> BalancingOpts -> FilePath -> FilePath
[BalancingOpts] -> FilePath -> FilePath
BalancingOpts -> FilePath
(Int -> BalancingOpts -> FilePath -> FilePath)
-> (BalancingOpts -> FilePath)
-> ([BalancingOpts] -> FilePath -> FilePath)
-> Show BalancingOpts
forall a.
(Int -> a -> FilePath -> FilePath)
-> (a -> FilePath) -> ([a] -> FilePath -> FilePath) -> Show a
showList :: [BalancingOpts] -> FilePath -> FilePath
$cshowList :: [BalancingOpts] -> FilePath -> FilePath
show :: BalancingOpts -> FilePath
$cshow :: BalancingOpts -> FilePath
showsPrec :: Int -> BalancingOpts -> FilePath -> FilePath
$cshowsPrec :: Int -> BalancingOpts -> FilePath -> FilePath
Show)

instance Default BalancingOpts where def :: BalancingOpts
def = BalancingOpts
balancingOpts

balancingOpts :: BalancingOpts
balancingOpts :: BalancingOpts
balancingOpts = BalancingOpts :: Bool -> Bool -> Maybe (Map Text AmountStyle) -> BalancingOpts
BalancingOpts
  { ignore_assertions_ :: Bool
ignore_assertions_ = Bool
False
  , infer_prices_ :: Bool
infer_prices_      = Bool
True
  , commodity_styles_ :: Maybe (Map Text AmountStyle)
commodity_styles_  = Maybe (Map Text AmountStyle)
forall a. Maybe a
Nothing
  }

-- | Check that this transaction would appear balanced to a human when displayed.
-- On success, returns the empty list, otherwise one or more error messages.
--
-- In more detail:
-- For the real postings, and separately for the balanced virtual postings:
--
-- 1. Convert amounts to cost where possible
--
-- 2. When there are two or more non-zero amounts
--    (appearing non-zero when displayed, using the given display styles if provided),
--    are they a mix of positives and negatives ?
--    This is checked separately to give a clearer error message.
--    (Best effort; could be confused by postings with multicommodity amounts.)
--
-- 3. Does the amounts' sum appear non-zero when displayed ?
--    (using the given display styles if provided)
--
transactionCheckBalanced :: BalancingOpts -> Transaction -> [String]
transactionCheckBalanced :: BalancingOpts -> Transaction -> [FilePath]
transactionCheckBalanced BalancingOpts{Maybe (Map Text AmountStyle)
commodity_styles_ :: Maybe (Map Text AmountStyle)
commodity_styles_ :: BalancingOpts -> Maybe (Map Text AmountStyle)
commodity_styles_} Transaction
t = [FilePath]
errs
  where
    ([Posting]
rps, [Posting]
bvps) = (Transaction -> [Posting]
realPostings Transaction
t, Transaction -> [Posting]
balancedVirtualPostings Transaction
t)

    -- check for mixed signs, detecting nonzeros at display precision
    canonicalise :: MixedAmount -> MixedAmount
canonicalise = (MixedAmount -> MixedAmount)
-> (Map Text AmountStyle -> MixedAmount -> MixedAmount)
-> Maybe (Map Text AmountStyle)
-> MixedAmount
-> MixedAmount
forall b a. b -> (a -> b) -> Maybe a -> b
maybe MixedAmount -> MixedAmount
forall a. a -> a
id Map Text AmountStyle -> MixedAmount -> MixedAmount
canonicaliseMixedAmount Maybe (Map Text AmountStyle)
commodity_styles_
    signsOk :: [Posting] -> Bool
signsOk [Posting]
ps =
      case (MixedAmount -> Bool) -> [MixedAmount] -> [MixedAmount]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not(Bool -> Bool) -> (MixedAmount -> Bool) -> MixedAmount -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
.MixedAmount -> Bool
mixedAmountLooksZero) ([MixedAmount] -> [MixedAmount]) -> [MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> a -> b
$ (Posting -> MixedAmount) -> [Posting] -> [MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map (MixedAmount -> MixedAmount
canonicalise(MixedAmount -> MixedAmount)
-> (Posting -> MixedAmount) -> Posting -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
.MixedAmount -> MixedAmount
mixedAmountCost(MixedAmount -> MixedAmount)
-> (Posting -> MixedAmount) -> Posting -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Posting -> MixedAmount
pamount) [Posting]
ps of
        [MixedAmount]
nonzeros | [MixedAmount] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [MixedAmount]
nonzeros Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
2
                   -> [Bool] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length ([Bool] -> [Bool]
forall a. Ord a => [a] -> [a]
nubSort ([Bool] -> [Bool]) -> [Bool] -> [Bool]
forall a b. (a -> b) -> a -> b
$ (MixedAmount -> Maybe Bool) -> [MixedAmount] -> [Bool]
forall a b. (a -> Maybe b) -> [a] -> [b]
mapMaybe MixedAmount -> Maybe Bool
isNegativeMixedAmount [MixedAmount]
nonzeros) Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
1
        [MixedAmount]
_          -> Bool
True
    (Bool
rsignsok, Bool
bvsignsok)       = ([Posting] -> Bool
signsOk [Posting]
rps, [Posting] -> Bool
signsOk [Posting]
bvps)

    -- check for zero sum, at display precision
    (MixedAmount
rsum, MixedAmount
bvsum)               = ([Posting] -> MixedAmount
sumPostings [Posting]
rps, [Posting] -> MixedAmount
sumPostings [Posting]
bvps)
    (MixedAmount
rsumcost, MixedAmount
bvsumcost)       = (MixedAmount -> MixedAmount
mixedAmountCost MixedAmount
rsum, MixedAmount -> MixedAmount
mixedAmountCost MixedAmount
bvsum)
    (MixedAmount
rsumdisplay, MixedAmount
bvsumdisplay) = (MixedAmount -> MixedAmount
canonicalise MixedAmount
rsumcost, MixedAmount -> MixedAmount
canonicalise MixedAmount
bvsumcost)
    (Bool
rsumok, Bool
bvsumok)           = (MixedAmount -> Bool
mixedAmountLooksZero MixedAmount
rsumdisplay, MixedAmount -> Bool
mixedAmountLooksZero MixedAmount
bvsumdisplay)

    -- generate error messages, showing amounts with their original precision
    errs :: [FilePath]
errs = (FilePath -> Bool) -> [FilePath] -> [FilePath]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not(Bool -> Bool) -> (FilePath -> Bool) -> FilePath -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
.FilePath -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null) [FilePath
rmsg, FilePath
bvmsg]
      where
        rmsg :: FilePath
rmsg
          | Bool -> Bool
not Bool
rsignsok  = FilePath
"real postings all have the same sign"
          | Bool -> Bool
not Bool
rsumok    = FilePath
"real postings' sum should be 0 but is: " FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ MixedAmount -> FilePath
showMixedAmount MixedAmount
rsumcost
          | Bool
otherwise     = FilePath
""
        bvmsg :: FilePath
bvmsg
          | Bool -> Bool
not Bool
bvsignsok = FilePath
"balanced virtual postings all have the same sign"
          | Bool -> Bool
not Bool
bvsumok   = FilePath
"balanced virtual postings' sum should be 0 but is: " FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ MixedAmount -> FilePath
showMixedAmount MixedAmount
bvsumcost
          | Bool
otherwise     = FilePath
""

-- | Legacy form of transactionCheckBalanced.
isTransactionBalanced :: BalancingOpts -> Transaction -> Bool
isTransactionBalanced :: BalancingOpts -> Transaction -> Bool
isTransactionBalanced BalancingOpts
bopts = [FilePath] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null ([FilePath] -> Bool)
-> (Transaction -> [FilePath]) -> Transaction -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. BalancingOpts -> Transaction -> [FilePath]
transactionCheckBalanced BalancingOpts
bopts

-- | Balance this transaction, ensuring that its postings
-- (and its balanced virtual postings) sum to 0,
-- by inferring a missing amount or conversion price(s) if needed.
-- Or if balancing is not possible, because the amounts don't sum to 0 or
-- because there's more than one missing amount, return an error message.
--
-- Transactions with balance assignments can have more than one
-- missing amount; to balance those you should use the more powerful
-- journalBalanceTransactions.
--
-- The "sum to 0" test is done using commodity display precisions,
-- if provided, so that the result agrees with the numbers users can see.
--
balanceTransaction ::
     BalancingOpts
  -> Transaction
  -> Either String Transaction
balanceTransaction :: BalancingOpts -> Transaction -> Either FilePath Transaction
balanceTransaction BalancingOpts
bopts = ((Transaction, [(Text, MixedAmount)]) -> Transaction)
-> Either FilePath (Transaction, [(Text, MixedAmount)])
-> Either FilePath Transaction
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Transaction, [(Text, MixedAmount)]) -> Transaction
forall a b. (a, b) -> a
fst (Either FilePath (Transaction, [(Text, MixedAmount)])
 -> Either FilePath Transaction)
-> (Transaction
    -> Either FilePath (Transaction, [(Text, MixedAmount)]))
-> Transaction
-> Either FilePath Transaction
forall b c a. (b -> c) -> (a -> b) -> a -> c
. BalancingOpts
-> Transaction
-> Either FilePath (Transaction, [(Text, MixedAmount)])
balanceTransactionHelper BalancingOpts
bopts

-- | Helper used by balanceTransaction and balanceTransactionWithBalanceAssignmentAndCheckAssertionsB;
-- use one of those instead. It also returns a list of accounts
-- and amounts that were inferred.
balanceTransactionHelper ::
     BalancingOpts
  -> Transaction
  -> Either String (Transaction, [(AccountName, MixedAmount)])
balanceTransactionHelper :: BalancingOpts
-> Transaction
-> Either FilePath (Transaction, [(Text, MixedAmount)])
balanceTransactionHelper BalancingOpts
bopts Transaction
t = do
  (Transaction
t', [(Text, MixedAmount)]
inferredamtsandaccts) <- Map Text AmountStyle
-> Transaction
-> Either FilePath (Transaction, [(Text, MixedAmount)])
inferBalancingAmount (Map Text AmountStyle
-> Maybe (Map Text AmountStyle) -> Map Text AmountStyle
forall a. a -> Maybe a -> a
fromMaybe Map Text AmountStyle
forall k a. Map k a
M.empty (Maybe (Map Text AmountStyle) -> Map Text AmountStyle)
-> Maybe (Map Text AmountStyle) -> Map Text AmountStyle
forall a b. (a -> b) -> a -> b
$ BalancingOpts -> Maybe (Map Text AmountStyle)
commodity_styles_ BalancingOpts
bopts) (Transaction
 -> Either FilePath (Transaction, [(Text, MixedAmount)]))
-> Transaction
-> Either FilePath (Transaction, [(Text, MixedAmount)])
forall a b. (a -> b) -> a -> b
$
    if BalancingOpts -> Bool
infer_prices_ BalancingOpts
bopts then Transaction -> Transaction
inferBalancingPrices Transaction
t else Transaction
t
  case BalancingOpts -> Transaction -> [FilePath]
transactionCheckBalanced BalancingOpts
bopts Transaction
t' of
    []   -> (Transaction, [(Text, MixedAmount)])
-> Either FilePath (Transaction, [(Text, MixedAmount)])
forall a b. b -> Either a b
Right (Transaction -> Transaction
txnTieKnot Transaction
t', [(Text, MixedAmount)]
inferredamtsandaccts)
    [FilePath]
errs -> FilePath -> Either FilePath (Transaction, [(Text, MixedAmount)])
forall a b. a -> Either a b
Left (FilePath -> Either FilePath (Transaction, [(Text, MixedAmount)]))
-> FilePath -> Either FilePath (Transaction, [(Text, MixedAmount)])
forall a b. (a -> b) -> a -> b
$ Transaction -> [FilePath] -> FilePath
transactionBalanceError Transaction
t' [FilePath]
errs

-- | Generate a transaction balancing error message, given the transaction
-- and one or more suberror messages.
transactionBalanceError :: Transaction -> [String] -> String
transactionBalanceError :: Transaction -> [FilePath] -> FilePath
transactionBalanceError Transaction
t [FilePath]
errs =
  Transaction -> FilePath -> FilePath
annotateErrorWithTransaction Transaction
t (FilePath -> FilePath) -> FilePath -> FilePath
forall a b. (a -> b) -> a -> b
$
  FilePath -> [FilePath] -> FilePath
forall a. [a] -> [[a]] -> [a]
intercalate FilePath
"\n" ([FilePath] -> FilePath) -> [FilePath] -> FilePath
forall a b. (a -> b) -> a -> b
$ FilePath
"could not balance this transaction:" FilePath -> [FilePath] -> [FilePath]
forall a. a -> [a] -> [a]
: [FilePath]
errs

annotateErrorWithTransaction :: Transaction -> String -> String
annotateErrorWithTransaction :: Transaction -> FilePath -> FilePath
annotateErrorWithTransaction Transaction
t FilePath
s =
  [FilePath] -> FilePath
unlines [ GenericSourcePos -> FilePath
showGenericSourcePos (GenericSourcePos -> FilePath) -> GenericSourcePos -> FilePath
forall a b. (a -> b) -> a -> b
$ Transaction -> GenericSourcePos
tsourcepos Transaction
t, FilePath
s
          , Text -> FilePath
T.unpack (Text -> FilePath) -> (Text -> Text) -> Text -> FilePath
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
T.stripEnd (Text -> FilePath) -> Text -> FilePath
forall a b. (a -> b) -> a -> b
$ Transaction -> Text
showTransaction Transaction
t
          ]

-- | Infer up to one missing amount for this transactions's real postings, and
-- likewise for its balanced virtual postings, if needed; or return an error
-- message if we can't. Returns the updated transaction and any inferred posting amounts,
-- with the corresponding accounts, in order).
--
-- We can infer a missing amount when there are multiple postings and exactly
-- one of them is amountless. If the amounts had price(s) the inferred amount
-- have the same price(s), and will be converted to the price commodity.
inferBalancingAmount ::
     M.Map CommoditySymbol AmountStyle -- ^ commodity display styles
  -> Transaction
  -> Either String (Transaction, [(AccountName, MixedAmount)])
inferBalancingAmount :: Map Text AmountStyle
-> Transaction
-> Either FilePath (Transaction, [(Text, MixedAmount)])
inferBalancingAmount Map Text AmountStyle
styles t :: Transaction
t@Transaction{tpostings :: Transaction -> [Posting]
tpostings=[Posting]
ps}
  | [Posting] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Posting]
amountlessrealps Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
1
      = FilePath -> Either FilePath (Transaction, [(Text, MixedAmount)])
forall a b. a -> Either a b
Left (FilePath -> Either FilePath (Transaction, [(Text, MixedAmount)]))
-> FilePath -> Either FilePath (Transaction, [(Text, MixedAmount)])
forall a b. (a -> b) -> a -> b
$ Transaction -> [FilePath] -> FilePath
transactionBalanceError Transaction
t
        [FilePath
"can't have more than one real posting with no amount"
        ,FilePath
"(remember to put two or more spaces between account and amount)"]
  | [Posting] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Posting]
amountlessbvps Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
1
      = FilePath -> Either FilePath (Transaction, [(Text, MixedAmount)])
forall a b. a -> Either a b
Left (FilePath -> Either FilePath (Transaction, [(Text, MixedAmount)]))
-> FilePath -> Either FilePath (Transaction, [(Text, MixedAmount)])
forall a b. (a -> b) -> a -> b
$ Transaction -> [FilePath] -> FilePath
transactionBalanceError Transaction
t
        [FilePath
"can't have more than one balanced virtual posting with no amount"
        ,FilePath
"(remember to put two or more spaces between account and amount)"]
  | Bool
otherwise
      = let psandinferredamts :: [(Posting, Maybe MixedAmount)]
psandinferredamts = (Posting -> (Posting, Maybe MixedAmount))
-> [Posting] -> [(Posting, Maybe MixedAmount)]
forall a b. (a -> b) -> [a] -> [b]
map Posting -> (Posting, Maybe MixedAmount)
inferamount [Posting]
ps
            inferredacctsandamts :: [(Text, MixedAmount)]
inferredacctsandamts = [(Posting -> Text
paccount Posting
p, MixedAmount
amt) | (Posting
p, Just MixedAmount
amt) <- [(Posting, Maybe MixedAmount)]
psandinferredamts]
        in (Transaction, [(Text, MixedAmount)])
-> Either FilePath (Transaction, [(Text, MixedAmount)])
forall a b. b -> Either a b
Right (Transaction
t{tpostings :: [Posting]
tpostings=((Posting, Maybe MixedAmount) -> Posting)
-> [(Posting, Maybe MixedAmount)] -> [Posting]
forall a b. (a -> b) -> [a] -> [b]
map (Posting, Maybe MixedAmount) -> Posting
forall a b. (a, b) -> a
fst [(Posting, Maybe MixedAmount)]
psandinferredamts}, [(Text, MixedAmount)]
inferredacctsandamts)
  where
    ([Posting]
amountfulrealps, [Posting]
amountlessrealps) = (Posting -> Bool) -> [Posting] -> ([Posting], [Posting])
forall a. (a -> Bool) -> [a] -> ([a], [a])
partition Posting -> Bool
hasAmount (Transaction -> [Posting]
realPostings Transaction
t)
    realsum :: MixedAmount
realsum = [Posting] -> MixedAmount
sumPostings [Posting]
amountfulrealps
    ([Posting]
amountfulbvps, [Posting]
amountlessbvps) = (Posting -> Bool) -> [Posting] -> ([Posting], [Posting])
forall a. (a -> Bool) -> [a] -> ([a], [a])
partition Posting -> Bool
hasAmount (Transaction -> [Posting]
balancedVirtualPostings Transaction
t)
    bvsum :: MixedAmount
bvsum = [Posting] -> MixedAmount
sumPostings [Posting]
amountfulbvps

    inferamount :: Posting -> (Posting, Maybe MixedAmount)
    inferamount :: Posting -> (Posting, Maybe MixedAmount)
inferamount Posting
p =
      let
        minferredamt :: Maybe MixedAmount
minferredamt = case Posting -> PostingType
ptype Posting
p of
          PostingType
RegularPosting         | Bool -> Bool
not (Posting -> Bool
hasAmount Posting
p) -> MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just MixedAmount
realsum
          PostingType
BalancedVirtualPosting | Bool -> Bool
not (Posting -> Bool
hasAmount Posting
p) -> MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just MixedAmount
bvsum
          PostingType
_                                          -> Maybe MixedAmount
forall a. Maybe a
Nothing
      in
        case Maybe MixedAmount
minferredamt of
          Maybe MixedAmount
Nothing -> (Posting
p, Maybe MixedAmount
forall a. Maybe a
Nothing)
          Just MixedAmount
a  -> (Posting
p{pamount :: MixedAmount
pamount=MixedAmount
a', poriginal :: Maybe Posting
poriginal=Posting -> Maybe Posting
forall a. a -> Maybe a
Just (Posting -> Maybe Posting) -> Posting -> Maybe Posting
forall a b. (a -> b) -> a -> b
$ Posting -> Posting
originalPosting Posting
p}, MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
Just MixedAmount
a')
            where
              -- Inferred amounts are converted to cost.
              -- Also ensure the new amount has the standard style for its commodity
              -- (since the main amount styling pass happened before this balancing pass);
              a' :: MixedAmount
a' = Map Text AmountStyle -> MixedAmount -> MixedAmount
styleMixedAmount Map Text AmountStyle
styles (MixedAmount -> MixedAmount)
-> (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. MixedAmount -> MixedAmount
mixedAmountCost (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ MixedAmount -> MixedAmount
maNegate MixedAmount
a

-- | Infer prices for this transaction's posting amounts, if needed to make
-- the postings balance, and if possible. This is done once for the real
-- postings and again (separately) for the balanced virtual postings. When
-- it's not possible, the transaction is left unchanged.
--
-- The simplest example is a transaction with two postings, each in a
-- different commodity, with no prices specified. In this case we'll add a
-- price to the first posting such that it can be converted to the commodity
-- of the second posting (with -B), and such that the postings balance.
--
-- In general, we can infer a conversion price when the sum of posting amounts
-- contains exactly two different commodities and no explicit prices.  Also
-- all postings are expected to contain an explicit amount (no missing
-- amounts) in a single commodity. Otherwise no price inferring is attempted.
--
-- The transaction itself could contain more than two commodities, and/or
-- prices, if they cancel out; what matters is that the sum of posting amounts
-- contains exactly two commodities and zero prices.
--
-- There can also be more than two postings in either of the commodities.
--
-- We want to avoid excessive display of digits when the calculated price is
-- an irrational number, while hopefully also ensuring the displayed numbers
-- make sense if the user does a manual calculation. This is (mostly) achieved
-- in two ways:
--
-- - when there is only one posting in the "from" commodity, a total price
--   (@@) is used, and all available decimal digits are shown
--
-- - otherwise, a suitable averaged unit price (@) is applied to the relevant
--   postings, with display precision equal to the summed display precisions
--   of the two commodities being converted between, or 2, whichever is larger.
--
-- (We don't always calculate a good-looking display precision for unit prices
-- when the commodity display precisions are low, eg when a journal doesn't
-- use any decimal places. The minimum of 2 helps make the prices shown by the
-- print command a bit less surprising in this case. Could do better.)
--
inferBalancingPrices :: Transaction -> Transaction
inferBalancingPrices :: Transaction -> Transaction
inferBalancingPrices t :: Transaction
t@Transaction{tpostings :: Transaction -> [Posting]
tpostings=[Posting]
ps} = Transaction
t{tpostings :: [Posting]
tpostings=[Posting]
ps'}
  where
    ps' :: [Posting]
ps' = (Posting -> Posting) -> [Posting] -> [Posting]
forall a b. (a -> b) -> [a] -> [b]
map (Transaction -> PostingType -> Posting -> Posting
priceInferrerFor Transaction
t PostingType
BalancedVirtualPosting (Posting -> Posting) -> (Posting -> Posting) -> Posting -> Posting
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> PostingType -> Posting -> Posting
priceInferrerFor Transaction
t PostingType
RegularPosting) [Posting]
ps

-- | Generate a posting update function which assigns a suitable balancing
-- price to the posting, if and as appropriate for the given transaction and
-- posting type (real or balanced virtual). If we cannot or should not infer
-- prices, just act as the identity on postings.
priceInferrerFor :: Transaction -> PostingType -> (Posting -> Posting)
priceInferrerFor :: Transaction -> PostingType -> Posting -> Posting
priceInferrerFor Transaction
t PostingType
pt = (Posting -> Posting)
-> ((Amount, Amount) -> Posting -> Posting)
-> Maybe (Amount, Amount)
-> Posting
-> Posting
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Posting -> Posting
forall a. a -> a
id (Amount, Amount) -> Posting -> Posting
inferprice Maybe (Amount, Amount)
inferFromAndTo
  where
    postings :: [Posting]
postings     = (Posting -> Bool) -> [Posting] -> [Posting]
forall a. (a -> Bool) -> [a] -> [a]
filter ((PostingType -> PostingType -> Bool
forall a. Eq a => a -> a -> Bool
==PostingType
pt)(PostingType -> Bool)
-> (Posting -> PostingType) -> Posting -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Posting -> PostingType
ptype) ([Posting] -> [Posting]) -> [Posting] -> [Posting]
forall a b. (a -> b) -> a -> b
$ Transaction -> [Posting]
tpostings Transaction
t
    pcommodities :: [Text]
pcommodities = (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
$ (Posting -> [Amount]) -> [Posting] -> [Amount]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (MixedAmount -> [Amount]
amounts (MixedAmount -> [Amount])
-> (Posting -> MixedAmount) -> Posting -> [Amount]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Posting -> MixedAmount
pamount) [Posting]
postings
    sumamounts :: [Amount]
sumamounts   = MixedAmount -> [Amount]
amounts (MixedAmount -> [Amount]) -> MixedAmount -> [Amount]
forall a b. (a -> b) -> a -> b
$ [Posting] -> MixedAmount
sumPostings [Posting]
postings  -- amounts normalises to one amount per commodity & price

    -- We can infer prices if there are no prices given, exactly two commodities in the normalised
    -- sum of postings in this transaction, and these two have opposite signs. The amount we are
    -- converting from is the first commodity to appear in the ordered list of postings, and the
    -- commodity we are converting to is the other. If we cannot infer prices, return Nothing.
    inferFromAndTo :: Maybe (Amount, Amount)
inferFromAndTo = case [Amount]
sumamounts of
      [Amount
a,Amount
b] | Bool
noprices, Bool
oppositesigns -> [Maybe (Amount, Amount)] -> Maybe (Amount, Amount)
forall (t :: * -> *) (f :: * -> *) a.
(Foldable t, Alternative f) =>
t (f a) -> f a
asum ([Maybe (Amount, Amount)] -> Maybe (Amount, Amount))
-> [Maybe (Amount, Amount)] -> Maybe (Amount, Amount)
forall a b. (a -> b) -> a -> b
$ (Text -> Maybe (Amount, Amount))
-> [Text] -> [Maybe (Amount, Amount)]
forall a b. (a -> b) -> [a] -> [b]
map Text -> Maybe (Amount, Amount)
orderIfMatches [Text]
pcommodities
        where
          noprices :: Bool
noprices      = (Amount -> Bool) -> [Amount] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
all (Maybe AmountPrice -> Bool
forall a. Maybe a -> Bool
isNothing (Maybe AmountPrice -> Bool)
-> (Amount -> Maybe AmountPrice) -> Amount -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Amount -> Maybe AmountPrice
aprice) [Amount]
sumamounts
          oppositesigns :: Bool
oppositesigns = Quantity -> Quantity
forall a. Num a => a -> a
signum (Amount -> Quantity
aquantity Amount
a) Quantity -> Quantity -> Bool
forall a. Eq a => a -> a -> Bool
/= Quantity -> Quantity
forall a. Num a => a -> a
signum (Amount -> Quantity
aquantity Amount
b)
          orderIfMatches :: Text -> Maybe (Amount, Amount)
orderIfMatches Text
x | Text
x Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Amount -> Text
acommodity Amount
a = (Amount, Amount) -> Maybe (Amount, Amount)
forall a. a -> Maybe a
Just (Amount
a,Amount
b)
                           | Text
x Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Amount -> Text
acommodity Amount
b = (Amount, Amount) -> Maybe (Amount, Amount)
forall a. a -> Maybe a
Just (Amount
b,Amount
a)
                           | Bool
otherwise         = Maybe (Amount, Amount)
forall a. Maybe a
Nothing
      [Amount]
_ -> Maybe (Amount, Amount)
forall a. Maybe a
Nothing

    -- For each posting, if the posting type matches, there is only a single amount in the posting,
    -- and the commodity of the amount matches the amount we're converting from,
    -- then set its price based on the ratio between fromamount and toamount.
    inferprice :: (Amount, Amount) -> Posting -> Posting
inferprice (Amount
fromamount, Amount
toamount) Posting
posting
        | [Amount
a] <- MixedAmount -> [Amount]
amounts (Posting -> MixedAmount
pamount Posting
posting), Posting -> PostingType
ptype Posting
posting PostingType -> PostingType -> Bool
forall a. Eq a => a -> a -> Bool
== PostingType
pt, Amount -> Text
acommodity Amount
a Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Amount -> Text
acommodity Amount
fromamount
            = Posting
posting{ pamount :: MixedAmount
pamount   = Amount -> MixedAmount
mixedAmount Amount
a{aprice :: Maybe AmountPrice
aprice=AmountPrice -> Maybe AmountPrice
forall a. a -> Maybe a
Just AmountPrice
conversionprice}
                     , poriginal :: Maybe Posting
poriginal = Posting -> Maybe Posting
forall a. a -> Maybe a
Just (Posting -> Maybe Posting) -> Posting -> Maybe Posting
forall a b. (a -> b) -> a -> b
$ Posting -> Posting
originalPosting Posting
posting }
        | Bool
otherwise = Posting
posting
      where
        -- If only one Amount in the posting list matches fromamount we can use TotalPrice.
        -- Otherwise divide the conversion equally among the Amounts by using a unit price.
        conversionprice :: AmountPrice
conversionprice = case (Text -> Bool) -> [Text] -> [Text]
forall a. (a -> Bool) -> [a] -> [a]
filter (Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Amount -> Text
acommodity Amount
fromamount) [Text]
pcommodities of
            [Text
_] -> Amount -> AmountPrice
TotalPrice (Amount -> AmountPrice) -> Amount -> AmountPrice
forall a b. (a -> b) -> a -> b
$ Amount -> Amount
forall a. Num a => a -> a
negate Amount
toamount
            [Text]
_   -> Amount -> AmountPrice
UnitPrice  (Amount -> AmountPrice) -> Amount -> AmountPrice
forall a b. (a -> b) -> a -> b
$ Amount -> Amount
forall a. Num a => a -> a
negate Amount
unitprice Amount -> AmountPrecision -> Amount
`withPrecision` AmountPrecision
unitprecision

        unitprice :: Amount
unitprice     = Amount -> Quantity
aquantity Amount
fromamount Quantity -> Amount -> Amount
`divideAmount` Amount
toamount
        unitprecision :: AmountPrecision
unitprecision = case (AmountStyle -> AmountPrecision
asprecision (AmountStyle -> AmountPrecision) -> AmountStyle -> AmountPrecision
forall a b. (a -> b) -> a -> b
$ Amount -> AmountStyle
astyle Amount
fromamount, AmountStyle -> AmountPrecision
asprecision (AmountStyle -> AmountPrecision) -> AmountStyle -> AmountPrecision
forall a b. (a -> b) -> a -> b
$ Amount -> AmountStyle
astyle Amount
toamount) of
            (Precision Word8
a, Precision Word8
b) -> Word8 -> AmountPrecision
Precision (Word8 -> AmountPrecision)
-> (Word8 -> Word8) -> Word8 -> AmountPrecision
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Word8 -> Word8 -> Word8
forall a. Ord a => a -> a -> a
max Word8
2 (Word8 -> AmountPrecision) -> Word8 -> AmountPrecision
forall a b. (a -> b) -> a -> b
$ Word8 -> Word8 -> Word8
forall p. (Ord p, Num p, Bounded p) => p -> p -> p
saturatedAdd Word8
a Word8
b
            (AmountPrecision, AmountPrecision)
_                          -> AmountPrecision
NaturalPrecision
        saturatedAdd :: p -> p -> p
saturatedAdd p
a p
b = if p
forall a. Bounded a => a
maxBound p -> p -> p
forall a. Num a => a -> a -> a
- p
a p -> p -> Bool
forall a. Ord a => a -> a -> Bool
< p
b then p
forall a. Bounded a => a
maxBound else p
a p -> p -> p
forall a. Num a => a -> a -> a
+ p
b

-- Get a transaction's secondary date, defaulting to the primary date.
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

-- | 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 Text AmountStyle
-> Day
-> Day
-> ValuationType
-> Transaction
-> Transaction
transactionApplyValuation PriceOracle
priceoracle Map Text AmountStyle
styles Day
periodlast Day
today ValuationType
v =
  (Posting -> Posting) -> Transaction -> Transaction
transactionTransformPostings (PriceOracle
-> Map Text AmountStyle
-> Day
-> Day
-> ValuationType
-> Posting
-> Posting
postingApplyValuation PriceOracle
priceoracle Map Text AmountStyle
styles Day
periodlast Day
today ValuationType
v)

-- | Convert this transaction's amounts to cost, and apply the appropriate amount styles.
transactionToCost :: M.Map CommoditySymbol AmountStyle -> Transaction -> Transaction
transactionToCost :: Map Text AmountStyle -> Transaction -> Transaction
transactionToCost Map Text AmountStyle
styles = (Posting -> Posting) -> Transaction -> Transaction
transactionTransformPostings (Map Text AmountStyle -> Posting -> Posting
postingToCost Map Text AmountStyle
styles)

-- | 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 FilePath Transaction
transactionApplyAliases [AccountAlias]
aliases Transaction
t =
  case (Posting -> Either FilePath Posting)
-> [Posting] -> Either FilePath [Posting]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
(a -> m b) -> t a -> m (t b)
mapM ([AccountAlias] -> Posting -> Either FilePath Posting
postingApplyAliases [AccountAlias]
aliases) ([Posting] -> Either FilePath [Posting])
-> [Posting] -> Either FilePath [Posting]
forall a b. (a -> b) -> a -> b
$ Transaction -> [Posting]
tpostings Transaction
t of
    Right [Posting]
ps -> Transaction -> Either FilePath Transaction
forall a b. b -> Either a b
Right (Transaction -> Either FilePath Transaction)
-> Transaction -> Either FilePath 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 FilePath
err -> FilePath -> Either FilePath Transaction
forall a b. a -> Either a b
Left FilePath
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)

-- | The file path from which this transaction was parsed.
transactionFile :: Transaction -> FilePath
transactionFile :: Transaction -> FilePath
transactionFile Transaction{GenericSourcePos
tsourcepos :: GenericSourcePos
tsourcepos :: Transaction -> GenericSourcePos
tsourcepos} =
  case GenericSourcePos
tsourcepos of
    GenericSourcePos FilePath
f Int
_ Int
_ -> FilePath
f
    JournalSourcePos FilePath
f (Int, Int)
_   -> FilePath
f

-- tests

tests_Transaction :: TestTree
tests_Transaction :: TestTree
tests_Transaction =
  FilePath -> [TestTree] -> TestTree
tests FilePath
"Transaction" [

      FilePath -> [TestTree] -> TestTree
tests FilePath
"showPostingLines" [
          FilePath -> Assertion -> TestTree
test FilePath
"null posting" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Posting -> [Text]
showPostingLines Posting
nullposting [Text] -> [Text] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?= [Text
"                   0"]
        , FilePath -> Assertion -> TestTree
test FilePath
"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 :: Text
paccount = Text
"a"
                  , pamount :: MixedAmount
pamount = [Amount] -> MixedAmount
forall (t :: * -> *). Foldable t => t Amount -> MixedAmount
mixed [Quantity -> Amount
usd Quantity
1, Quantity -> Amount
hrs Quantity
2]
                  , pcomment :: Text
pcomment = Text
"pcomment1\npcomment2\n  tag3: val3  \n"
                  , ptype :: PostingType
ptype = PostingType
RegularPosting
                  , ptags :: [Tag]
ptags = [(Text
"ptag1", Text
"val1"), (Text
"ptag2", Text
"val2")]
                  }
           in Posting -> [Text]
showPostingLines Posting
p [Text] -> [Text] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
              [ Text
"    * a         $1.00  ; pcomment1"
              , Text
"    ; pcomment2"
              , Text
"    ;   tag3: val3  "
              , Text
"    * a         2.00h  ; pcomment1"
              , Text
"    ; pcomment2"
              , Text
"    ;   tag3: val3  "
              ]
        ]

    , let
        -- one implicit amount
        timp :: Transaction
timp = Transaction
nulltransaction {tpostings :: [Posting]
tpostings = [Text
"a" Text -> Amount -> Posting
`post` Quantity -> Amount
usd Quantity
1, Text
"b" Text -> Amount -> Posting
`post` Amount
missingamt]}
        -- explicit amounts, balanced
        texp :: Transaction
texp = Transaction
nulltransaction {tpostings :: [Posting]
tpostings = [Text
"a" Text -> Amount -> Posting
`post` Quantity -> Amount
usd Quantity
1, Text
"b" Text -> Amount -> Posting
`post` Quantity -> Amount
usd (-Quantity
1)]}
        -- explicit amount, only one posting
        texp1 :: Transaction
texp1 = Transaction
nulltransaction {tpostings :: [Posting]
tpostings = [Text
"(a)" Text -> Amount -> Posting
`post` Quantity -> Amount
usd Quantity
1]}
        -- explicit amounts, two commodities, explicit balancing price
        texp2 :: Transaction
texp2 = Transaction
nulltransaction {tpostings :: [Posting]
tpostings = [Text
"a" Text -> Amount -> Posting
`post` Quantity -> Amount
usd Quantity
1, Text
"b" Text -> Amount -> Posting
`post` (Quantity -> Amount
hrs (-Quantity
1) Amount -> Amount -> Amount
`at` Quantity -> Amount
usd Quantity
1)]}
        -- explicit amounts, two commodities, implicit balancing price
        texp2b :: Transaction
texp2b = Transaction
nulltransaction {tpostings :: [Posting]
tpostings = [Text
"a" Text -> Amount -> Posting
`post` Quantity -> Amount
usd Quantity
1, Text
"b" Text -> Amount -> Posting
`post` Quantity -> Amount
hrs (-Quantity
1)]}
        -- one missing amount, not the last one
        t3 :: Transaction
t3 = Transaction
nulltransaction {tpostings :: [Posting]
tpostings = [Text
"a" Text -> Amount -> Posting
`post` Quantity -> Amount
usd Quantity
1, Text
"b" Text -> Amount -> Posting
`post` Amount
missingamt, Text
"c" Text -> Amount -> Posting
`post` Quantity -> Amount
usd (-Quantity
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 FilePath -> [TestTree] -> TestTree
tests FilePath
"postingsAsLines" [
              FilePath -> Assertion -> TestTree
test FilePath
"null-transaction" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [Text]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
nulltransaction) [Text] -> [Text] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?= []
            , FilePath -> Assertion -> TestTree
test FilePath
"implicit-amount" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [Text]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
timp) [Text] -> [Text] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
                  [ Text
"    a           $1.00"
                  , Text
"    b" -- implicit amount remains implicit
                  ]
            , FilePath -> Assertion -> TestTree
test FilePath
"explicit-amounts" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [Text]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
texp) [Text] -> [Text] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
                  [ Text
"    a           $1.00"
                  , Text
"    b          $-1.00"
                  ]
            , FilePath -> Assertion -> TestTree
test FilePath
"one-explicit-amount" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [Text]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
texp1) [Text] -> [Text] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
                  [ Text
"    (a)           $1.00"
                  ]
            , FilePath -> Assertion -> TestTree
test FilePath
"explicit-amounts-two-commodities" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [Text]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
texp2) [Text] -> [Text] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
                  [ Text
"    a             $1.00"
                  , Text
"    b    -1.00h @ $1.00"
                  ]
            , FilePath -> Assertion -> TestTree
test FilePath
"explicit-amounts-not-explicitly-balanced" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [Text]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
texp2b) [Text] -> [Text] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
                  [ Text
"    a           $1.00"
                  , Text
"    b          -1.00h"
                  ]
            , FilePath -> Assertion -> TestTree
test FilePath
"implicit-amount-not-last" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Bool -> [Posting] -> [Text]
postingsAsLines Bool
False (Transaction -> [Posting]
tpostings Transaction
t3) [Text] -> [Text] -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
                  [Text
"    a           $1.00", Text
"    b", Text
"    c          $-1.00"]
            -- , test "ensure-visibly-balanced" $
            --    in postingsAsLines False (tpostings t4) @?=
            --       ["    a          $-0.01", "    b           $0.005", "    c           $0.005"]

            ]

    , FilePath -> Assertion -> TestTree
test FilePath
"inferBalancingAmount" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ do
         ((Transaction, [(Text, MixedAmount)]) -> Transaction
forall a b. (a, b) -> a
fst ((Transaction, [(Text, MixedAmount)]) -> Transaction)
-> Either FilePath (Transaction, [(Text, MixedAmount)])
-> Either FilePath Transaction
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Map Text AmountStyle
-> Transaction
-> Either FilePath (Transaction, [(Text, MixedAmount)])
inferBalancingAmount Map Text AmountStyle
forall k a. Map k a
M.empty Transaction
nulltransaction) Either FilePath Transaction
-> Either FilePath Transaction -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?= Transaction -> Either FilePath Transaction
forall a b. b -> Either a b
Right Transaction
nulltransaction
         ((Transaction, [(Text, MixedAmount)]) -> Transaction
forall a b. (a, b) -> a
fst ((Transaction, [(Text, MixedAmount)]) -> Transaction)
-> Either FilePath (Transaction, [(Text, MixedAmount)])
-> Either FilePath Transaction
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Map Text AmountStyle
-> Transaction
-> Either FilePath (Transaction, [(Text, MixedAmount)])
inferBalancingAmount Map Text AmountStyle
forall k a. Map k a
M.empty Transaction
nulltransaction{tpostings :: [Posting]
tpostings = [Text
"a" Text -> Amount -> Posting
`post` Quantity -> Amount
usd (-Quantity
5), Text
"b" Text -> Amount -> Posting
`post` Amount
missingamt]}) Either FilePath Transaction
-> Either FilePath Transaction -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
           Transaction -> Either FilePath Transaction
forall a b. b -> Either a b
Right Transaction
nulltransaction{tpostings :: [Posting]
tpostings = [Text
"a" Text -> Amount -> Posting
`post` Quantity -> Amount
usd (-Quantity
5), Text
"b" Text -> Amount -> Posting
`post` Quantity -> Amount
usd Quantity
5]}
         ((Transaction, [(Text, MixedAmount)]) -> Transaction
forall a b. (a, b) -> a
fst ((Transaction, [(Text, MixedAmount)]) -> Transaction)
-> Either FilePath (Transaction, [(Text, MixedAmount)])
-> Either FilePath Transaction
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Map Text AmountStyle
-> Transaction
-> Either FilePath (Transaction, [(Text, MixedAmount)])
inferBalancingAmount Map Text AmountStyle
forall k a. Map k a
M.empty Transaction
nulltransaction{tpostings :: [Posting]
tpostings = [Text
"a" Text -> Amount -> Posting
`post` Quantity -> Amount
usd (-Quantity
5), Text
"b" Text -> Amount -> Posting
`post` (Quantity -> Amount
eur Quantity
3 Amount -> Amount -> Amount
@@ Quantity -> Amount
usd Quantity
4), Text
"c" Text -> Amount -> Posting
`post` Amount
missingamt]}) Either FilePath Transaction
-> Either FilePath Transaction -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
           Transaction -> Either FilePath Transaction
forall a b. b -> Either a b
Right Transaction
nulltransaction{tpostings :: [Posting]
tpostings = [Text
"a" Text -> Amount -> Posting
`post` Quantity -> Amount
usd (-Quantity
5), Text
"b" Text -> Amount -> Posting
`post` (Quantity -> Amount
eur Quantity
3 Amount -> Amount -> Amount
@@ Quantity -> Amount
usd Quantity
4), Text
"c" Text -> Amount -> Posting
`post` Quantity -> Amount
usd Quantity
1]}

    , FilePath -> [TestTree] -> TestTree
tests FilePath
"showTransaction" [
          FilePath -> Assertion -> TestTree
test FilePath
"null transaction" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Transaction -> Text
showTransaction Transaction
nulltransaction Text -> Text -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?= Text
"0000-01-01\n\n"
        , FilePath -> Assertion -> TestTree
test FilePath
"non-null transaction" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ Transaction -> Text
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 :: Text
tcode = Text
"code"
              , tdescription :: Text
tdescription = Text
"desc"
              , tcomment :: Text
tcomment = Text
"tcomment1\ntcomment2\n"
              , ttags :: [Tag]
ttags = [(Text
"ttag1", Text
"val1")]
              , tpostings :: [Posting]
tpostings =
                  [ Posting
nullposting
                      { pstatus :: Status
pstatus = Status
Cleared
                      , paccount :: Text
paccount = Text
"a"
                      , pamount :: MixedAmount
pamount = [Amount] -> MixedAmount
forall (t :: * -> *). Foldable t => t Amount -> MixedAmount
mixed [Quantity -> Amount
usd Quantity
1, Quantity -> Amount
hrs Quantity
2]
                      , pcomment :: Text
pcomment = Text
"\npcomment2\n"
                      , ptype :: PostingType
ptype = PostingType
RegularPosting
                      , ptags :: [Tag]
ptags = [(Text
"ptag1", Text
"val1"), (Text
"ptag2", Text
"val2")]
                      }
                  ]
              } Text -> Text -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
          [Text] -> Text
T.unlines
            [ Text
"2012-05-14=2012-05-15 (code) desc  ; tcomment1"
            , Text
"    ; tcomment2"
            , Text
"    * a         $1.00"
            , Text
"    ; pcomment2"
            , Text
"    * a         2.00h"
            , Text
"    ; pcomment2"
            , Text
""
            ]
        , FilePath -> Assertion -> TestTree
test FilePath
"show a balanced transaction" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          (let t :: Transaction
t =
                 Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
                   Integer
0
                   Text
""
                   GenericSourcePos
nullsourcepos
                   (Integer -> Int -> Int -> Day
fromGregorian Integer
2007 Int
01 Int
28)
                   Maybe Day
forall a. Maybe a
Nothing
                   Status
Unmarked
                   Text
""
                   Text
"coopportunity"
                   Text
""
                   []
                   [ Posting
posting {paccount :: Text
paccount = Text
"expenses:food:groceries", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
47.18), ptransaction :: Maybe Transaction
ptransaction = Transaction -> Maybe Transaction
forall a. a -> Maybe a
Just Transaction
t}
                   , Posting
posting {paccount :: Text
paccount = Text
"assets:checking", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd (-Quantity
47.18)), ptransaction :: Maybe Transaction
ptransaction = Transaction -> Maybe Transaction
forall a. a -> Maybe a
Just Transaction
t}
                   ]
            in Transaction -> Text
showTransaction Transaction
t) Text -> Text -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
          ([Text] -> Text
T.unlines
             [ Text
"2007-01-28 coopportunity"
             , Text
"    expenses:food:groceries          $47.18"
             , Text
"    assets:checking                 $-47.18"
             , Text
""
             ])
        , FilePath -> Assertion -> TestTree
test FilePath
"show an unbalanced transaction, should not elide" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          (Transaction -> Text
showTransaction
             (Transaction -> Transaction
txnTieKnot (Transaction -> Transaction) -> Transaction -> Transaction
forall a b. (a -> b) -> a -> b
$
              Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
                Integer
0
                Text
""
                GenericSourcePos
nullsourcepos
                (Integer -> Int -> Int -> Day
fromGregorian Integer
2007 Int
01 Int
28)
                Maybe Day
forall a. Maybe a
Nothing
                Status
Unmarked
                Text
""
                Text
"coopportunity"
                Text
""
                []
                [ Posting
posting {paccount :: Text
paccount = Text
"expenses:food:groceries", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
47.18)}
                , Posting
posting {paccount :: Text
paccount = Text
"assets:checking", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd (-Quantity
47.19))}
                ])) Text -> Text -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
          ([Text] -> Text
T.unlines
             [ Text
"2007-01-28 coopportunity"
             , Text
"    expenses:food:groceries          $47.18"
             , Text
"    assets:checking                 $-47.19"
             , Text
""
             ])
        , FilePath -> Assertion -> TestTree
test FilePath
"show a transaction with one posting and a missing amount" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          (Transaction -> Text
showTransaction
             (Transaction -> Transaction
txnTieKnot (Transaction -> Transaction) -> Transaction -> Transaction
forall a b. (a -> b) -> a -> b
$
              Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
                Integer
0
                Text
""
                GenericSourcePos
nullsourcepos
                (Integer -> Int -> Int -> Day
fromGregorian Integer
2007 Int
01 Int
28)
                Maybe Day
forall a. Maybe a
Nothing
                Status
Unmarked
                Text
""
                Text
"coopportunity"
                Text
""
                []
                [Posting
posting {paccount :: Text
paccount = Text
"expenses:food:groceries", pamount :: MixedAmount
pamount = MixedAmount
missingmixedamt}])) Text -> Text -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
          ([Text] -> Text
T.unlines [Text
"2007-01-28 coopportunity", Text
"    expenses:food:groceries", Text
""])
        , FilePath -> Assertion -> TestTree
test FilePath
"show a transaction with a priced commodityless amount" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          (Transaction -> Text
showTransaction
             (Transaction -> Transaction
txnTieKnot (Transaction -> Transaction) -> Transaction -> Transaction
forall a b. (a -> b) -> a -> b
$
              Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
                Integer
0
                Text
""
                GenericSourcePos
nullsourcepos
                (Integer -> Int -> Int -> Day
fromGregorian Integer
2010 Int
01 Int
01)
                Maybe Day
forall a. Maybe a
Nothing
                Status
Unmarked
                Text
""
                Text
"x"
                Text
""
                []
                [ Posting
posting {paccount :: Text
paccount = Text
"a", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Quantity -> Amount
num Quantity
1 Amount -> Amount -> Amount
`at` (Quantity -> Amount
usd Quantity
2 Amount -> AmountPrecision -> Amount
`withPrecision` Word8 -> AmountPrecision
Precision Word8
0)}
                , Posting
posting {paccount :: Text
paccount = Text
"b", pamount :: MixedAmount
pamount = MixedAmount
missingmixedamt}
                ])) Text -> Text -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
          ([Text] -> Text
T.unlines [Text
"2010-01-01 x", Text
"    a          1 @ $2", Text
"    b", Text
""])
        ]
    , FilePath -> [TestTree] -> TestTree
tests FilePath
"balanceTransaction" [
         FilePath -> Assertion -> TestTree
test FilePath
"detect unbalanced entry, sign error" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          Either FilePath Transaction -> Assertion
forall b a. (HasCallStack, Eq b, Show b) => Either a b -> Assertion
assertLeft
            (BalancingOpts -> Transaction -> Either FilePath Transaction
balanceTransaction BalancingOpts
forall a. Default a => a
def
               (Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
                  Integer
0
                  Text
""
                  GenericSourcePos
nullsourcepos
                  (Integer -> Int -> Int -> Day
fromGregorian Integer
2007 Int
01 Int
28)
                  Maybe Day
forall a. Maybe a
Nothing
                  Status
Unmarked
                  Text
""
                  Text
"test"
                  Text
""
                  []
                  [Posting
posting {paccount :: Text
paccount = Text
"a", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
1)}, Posting
posting {paccount :: Text
paccount = Text
"b", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
1)}]))
        ,FilePath -> Assertion -> TestTree
test FilePath
"detect unbalanced entry, multiple missing amounts" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          Either FilePath Transaction -> Assertion
forall b a. (HasCallStack, Eq b, Show b) => Either a b -> Assertion
assertLeft (Either FilePath Transaction -> Assertion)
-> Either FilePath Transaction -> Assertion
forall a b. (a -> b) -> a -> b
$
             BalancingOpts -> Transaction -> Either FilePath Transaction
balanceTransaction BalancingOpts
forall a. Default a => a
def
               (Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
                  Integer
0
                  Text
""
                  GenericSourcePos
nullsourcepos
                  (Integer -> Int -> Int -> Day
fromGregorian Integer
2007 Int
01 Int
28)
                  Maybe Day
forall a. Maybe a
Nothing
                  Status
Unmarked
                  Text
""
                  Text
"test"
                  Text
""
                  []
                  [ Posting
posting {paccount :: Text
paccount = Text
"a", pamount :: MixedAmount
pamount = MixedAmount
missingmixedamt}
                  , Posting
posting {paccount :: Text
paccount = Text
"b", pamount :: MixedAmount
pamount = MixedAmount
missingmixedamt}
                  ])
        ,FilePath -> Assertion -> TestTree
test FilePath
"one missing amount is inferred" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          (Posting -> MixedAmount
pamount (Posting -> MixedAmount)
-> (Transaction -> Posting) -> Transaction -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Posting] -> Posting
forall a. [a] -> a
last ([Posting] -> Posting)
-> (Transaction -> [Posting]) -> Transaction -> Posting
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> [Posting]
tpostings (Transaction -> MixedAmount)
-> Either FilePath Transaction -> Either FilePath MixedAmount
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$>
           BalancingOpts -> Transaction -> Either FilePath Transaction
balanceTransaction BalancingOpts
forall a. Default a => a
def
             (Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
                Integer
0
                Text
""
                GenericSourcePos
nullsourcepos
                (Integer -> Int -> Int -> Day
fromGregorian Integer
2007 Int
01 Int
28)
                Maybe Day
forall a. Maybe a
Nothing
                Status
Unmarked
                Text
""
                Text
""
                Text
""
                []
                [Posting
posting {paccount :: Text
paccount = Text
"a", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
1)}, Posting
posting {paccount :: Text
paccount = Text
"b", pamount :: MixedAmount
pamount = MixedAmount
missingmixedamt}])) Either FilePath MixedAmount
-> Either FilePath MixedAmount -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
          MixedAmount -> Either FilePath MixedAmount
forall a b. b -> Either a b
Right (Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Quantity -> Amount
usd (-Quantity
1))
        ,FilePath -> Assertion -> TestTree
test FilePath
"conversion price is inferred" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          (Posting -> MixedAmount
pamount (Posting -> MixedAmount)
-> (Transaction -> Posting) -> Transaction -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Posting] -> Posting
forall a. [a] -> a
head ([Posting] -> Posting)
-> (Transaction -> [Posting]) -> Transaction -> Posting
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Transaction -> [Posting]
tpostings (Transaction -> MixedAmount)
-> Either FilePath Transaction -> Either FilePath MixedAmount
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$>
           BalancingOpts -> Transaction -> Either FilePath Transaction
balanceTransaction BalancingOpts
forall a. Default a => a
def
             (Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
                Integer
0
                Text
""
                GenericSourcePos
nullsourcepos
                (Integer -> Int -> Int -> Day
fromGregorian Integer
2007 Int
01 Int
28)
                Maybe Day
forall a. Maybe a
Nothing
                Status
Unmarked
                Text
""
                Text
""
                Text
""
                []
                [ Posting
posting {paccount :: Text
paccount = Text
"a", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
1.35)}
                , Posting
posting {paccount :: Text
paccount = Text
"b", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
eur (-Quantity
1))}
                ])) Either FilePath MixedAmount
-> Either FilePath MixedAmount -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?=
          MixedAmount -> Either FilePath MixedAmount
forall a b. b -> Either a b
Right (Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Quantity -> Amount
usd Quantity
1.35 Amount -> Amount -> Amount
@@ Quantity -> Amount
eur Quantity
1)
        ,FilePath -> Assertion -> TestTree
test FilePath
"balanceTransaction balances based on cost if there are unit prices" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          Either FilePath Transaction -> Assertion
forall a b. (HasCallStack, Eq a, Show a) => Either a b -> Assertion
assertRight (Either FilePath Transaction -> Assertion)
-> Either FilePath Transaction -> Assertion
forall a b. (a -> b) -> a -> b
$
          BalancingOpts -> Transaction -> Either FilePath Transaction
balanceTransaction BalancingOpts
forall a. Default a => a
def
            (Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
               Integer
0
               Text
""
               GenericSourcePos
nullsourcepos
               (Integer -> Int -> Int -> Day
fromGregorian Integer
2011 Int
01 Int
01)
               Maybe Day
forall a. Maybe a
Nothing
               Status
Unmarked
               Text
""
               Text
""
               Text
""
               []
               [ Posting
posting {paccount :: Text
paccount = Text
"a", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Quantity -> Amount
usd Quantity
1 Amount -> Amount -> Amount
`at` Quantity -> Amount
eur Quantity
2}
               , Posting
posting {paccount :: Text
paccount = Text
"a", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Quantity -> Amount
usd (-Quantity
2) Amount -> Amount -> Amount
`at` Quantity -> Amount
eur Quantity
1}
               ])
        ,FilePath -> Assertion -> TestTree
test FilePath
"balanceTransaction balances based on cost if there are total prices" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          Either FilePath Transaction -> Assertion
forall a b. (HasCallStack, Eq a, Show a) => Either a b -> Assertion
assertRight (Either FilePath Transaction -> Assertion)
-> Either FilePath Transaction -> Assertion
forall a b. (a -> b) -> a -> b
$
          BalancingOpts -> Transaction -> Either FilePath Transaction
balanceTransaction BalancingOpts
forall a. Default a => a
def
            (Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
               Integer
0
               Text
""
               GenericSourcePos
nullsourcepos
               (Integer -> Int -> Int -> Day
fromGregorian Integer
2011 Int
01 Int
01)
               Maybe Day
forall a. Maybe a
Nothing
               Status
Unmarked
               Text
""
               Text
""
               Text
""
               []
               [ Posting
posting {paccount :: Text
paccount = Text
"a", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Quantity -> Amount
usd Quantity
1 Amount -> Amount -> Amount
@@ Quantity -> Amount
eur Quantity
1}
               , Posting
posting {paccount :: Text
paccount = Text
"a", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Quantity -> Amount
usd (-Quantity
2) Amount -> Amount -> Amount
@@ Quantity -> Amount
eur (-Quantity
1)}
               ])
        ]
    , FilePath -> [TestTree] -> TestTree
tests FilePath
"isTransactionBalanced" [
         FilePath -> Assertion -> TestTree
test FilePath
"detect balanced" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          HasCallStack => FilePath -> Bool -> Assertion
FilePath -> Bool -> Assertion
assertBool FilePath
"" (Bool -> Assertion) -> Bool -> Assertion
forall a b. (a -> b) -> a -> b
$
          BalancingOpts -> Transaction -> Bool
isTransactionBalanced BalancingOpts
forall a. Default a => a
def (Transaction -> Bool) -> Transaction -> Bool
forall a b. (a -> b) -> a -> b
$
          Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
            Integer
0
            Text
""
            GenericSourcePos
nullsourcepos
            (Integer -> Int -> Int -> Day
fromGregorian Integer
2009 Int
01 Int
01)
            Maybe Day
forall a. Maybe a
Nothing
            Status
Unmarked
            Text
""
            Text
"a"
            Text
""
            []
            [ Posting
posting {paccount :: Text
paccount = Text
"b", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
1.00)}
            , Posting
posting {paccount :: Text
paccount = Text
"c", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd (-Quantity
1.00))}
            ]
        ,FilePath -> Assertion -> TestTree
test FilePath
"detect unbalanced" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          HasCallStack => FilePath -> Bool -> Assertion
FilePath -> Bool -> Assertion
assertBool FilePath
"" (Bool -> Assertion) -> Bool -> Assertion
forall a b. (a -> b) -> a -> b
$
          Bool -> Bool
not (Bool -> Bool) -> Bool -> Bool
forall a b. (a -> b) -> a -> b
$
          BalancingOpts -> Transaction -> Bool
isTransactionBalanced BalancingOpts
forall a. Default a => a
def (Transaction -> Bool) -> Transaction -> Bool
forall a b. (a -> b) -> a -> b
$
          Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
            Integer
0
            Text
""
            GenericSourcePos
nullsourcepos
            (Integer -> Int -> Int -> Day
fromGregorian Integer
2009 Int
01 Int
01)
            Maybe Day
forall a. Maybe a
Nothing
            Status
Unmarked
            Text
""
            Text
"a"
            Text
""
            []
            [ Posting
posting {paccount :: Text
paccount = Text
"b", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
1.00)}
            , Posting
posting {paccount :: Text
paccount = Text
"c", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd (-Quantity
1.01))}
            ]
        ,FilePath -> Assertion -> TestTree
test FilePath
"detect unbalanced, one posting" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          HasCallStack => FilePath -> Bool -> Assertion
FilePath -> Bool -> Assertion
assertBool FilePath
"" (Bool -> Assertion) -> Bool -> Assertion
forall a b. (a -> b) -> a -> b
$
          Bool -> Bool
not (Bool -> Bool) -> Bool -> Bool
forall a b. (a -> b) -> a -> b
$
          BalancingOpts -> Transaction -> Bool
isTransactionBalanced BalancingOpts
forall a. Default a => a
def (Transaction -> Bool) -> Transaction -> Bool
forall a b. (a -> b) -> a -> b
$
          Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
            Integer
0
            Text
""
            GenericSourcePos
nullsourcepos
            (Integer -> Int -> Int -> Day
fromGregorian Integer
2009 Int
01 Int
01)
            Maybe Day
forall a. Maybe a
Nothing
            Status
Unmarked
            Text
""
            Text
"a"
            Text
""
            []
            [Posting
posting {paccount :: Text
paccount = Text
"b", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
1.00)}]
        ,FilePath -> Assertion -> TestTree
test FilePath
"one zero posting is considered balanced for now" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          HasCallStack => FilePath -> Bool -> Assertion
FilePath -> Bool -> Assertion
assertBool FilePath
"" (Bool -> Assertion) -> Bool -> Assertion
forall a b. (a -> b) -> a -> b
$
          BalancingOpts -> Transaction -> Bool
isTransactionBalanced BalancingOpts
forall a. Default a => a
def (Transaction -> Bool) -> Transaction -> Bool
forall a b. (a -> b) -> a -> b
$
          Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
            Integer
0
            Text
""
            GenericSourcePos
nullsourcepos
            (Integer -> Int -> Int -> Day
fromGregorian Integer
2009 Int
01 Int
01)
            Maybe Day
forall a. Maybe a
Nothing
            Status
Unmarked
            Text
""
            Text
"a"
            Text
""
            []
            [Posting
posting {paccount :: Text
paccount = Text
"b", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
0)}]
        ,FilePath -> Assertion -> TestTree
test FilePath
"virtual postings don't need to balance" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          HasCallStack => FilePath -> Bool -> Assertion
FilePath -> Bool -> Assertion
assertBool FilePath
"" (Bool -> Assertion) -> Bool -> Assertion
forall a b. (a -> b) -> a -> b
$
          BalancingOpts -> Transaction -> Bool
isTransactionBalanced BalancingOpts
forall a. Default a => a
def (Transaction -> Bool) -> Transaction -> Bool
forall a b. (a -> b) -> a -> b
$
          Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
            Integer
0
            Text
""
            GenericSourcePos
nullsourcepos
            (Integer -> Int -> Int -> Day
fromGregorian Integer
2009 Int
01 Int
01)
            Maybe Day
forall a. Maybe a
Nothing
            Status
Unmarked
            Text
""
            Text
"a"
            Text
""
            []
            [ Posting
posting {paccount :: Text
paccount = Text
"b", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
1.00)}
            , Posting
posting {paccount :: Text
paccount = Text
"c", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd (-Quantity
1.00))}
            , Posting
posting {paccount :: Text
paccount = Text
"d", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
100), ptype :: PostingType
ptype = PostingType
VirtualPosting}
            ]
        ,FilePath -> Assertion -> TestTree
test FilePath
"balanced virtual postings need to balance among themselves" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          HasCallStack => FilePath -> Bool -> Assertion
FilePath -> Bool -> Assertion
assertBool FilePath
"" (Bool -> Assertion) -> Bool -> Assertion
forall a b. (a -> b) -> a -> b
$
          Bool -> Bool
not (Bool -> Bool) -> Bool -> Bool
forall a b. (a -> b) -> a -> b
$
          BalancingOpts -> Transaction -> Bool
isTransactionBalanced BalancingOpts
forall a. Default a => a
def (Transaction -> Bool) -> Transaction -> Bool
forall a b. (a -> b) -> a -> b
$
          Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
            Integer
0
            Text
""
            GenericSourcePos
nullsourcepos
            (Integer -> Int -> Int -> Day
fromGregorian Integer
2009 Int
01 Int
01)
            Maybe Day
forall a. Maybe a
Nothing
            Status
Unmarked
            Text
""
            Text
"a"
            Text
""
            []
            [ Posting
posting {paccount :: Text
paccount = Text
"b", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
1.00)}
            , Posting
posting {paccount :: Text
paccount = Text
"c", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd (-Quantity
1.00))}
            , Posting
posting {paccount :: Text
paccount = Text
"d", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
100), ptype :: PostingType
ptype = PostingType
BalancedVirtualPosting}
            ]
        ,FilePath -> Assertion -> TestTree
test FilePath
"balanced virtual postings need to balance among themselves (2)" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$
          HasCallStack => FilePath -> Bool -> Assertion
FilePath -> Bool -> Assertion
assertBool FilePath
"" (Bool -> Assertion) -> Bool -> Assertion
forall a b. (a -> b) -> a -> b
$
          BalancingOpts -> Transaction -> Bool
isTransactionBalanced BalancingOpts
forall a. Default a => a
def (Transaction -> Bool) -> Transaction -> Bool
forall a b. (a -> b) -> a -> b
$
          Integer
-> Text
-> GenericSourcePos
-> Day
-> Maybe Day
-> Status
-> Text
-> Text
-> Text
-> [Tag]
-> [Posting]
-> Transaction
Transaction
            Integer
0
            Text
""
            GenericSourcePos
nullsourcepos
            (Integer -> Int -> Int -> Day
fromGregorian Integer
2009 Int
01 Int
01)
            Maybe Day
forall a. Maybe a
Nothing
            Status
Unmarked
            Text
""
            Text
"a"
            Text
""
            []
            [ Posting
posting {paccount :: Text
paccount = Text
"b", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
1.00)}
            , Posting
posting {paccount :: Text
paccount = Text
"c", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd (-Quantity
1.00))}
            , Posting
posting {paccount :: Text
paccount = Text
"d", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd Quantity
100), ptype :: PostingType
ptype = PostingType
BalancedVirtualPosting}
            , Posting
posting {paccount :: Text
paccount = Text
"3", pamount :: MixedAmount
pamount = Amount -> MixedAmount
mixedAmount (Quantity -> Amount
usd (-Quantity
100)), ptype :: PostingType
ptype = PostingType
BalancedVirtualPosting}
            ]
        ]
    ]