{-|
A 'Transaction' represents a single balanced entry in the ledger file. It
normally contains two or more balanced 'Posting's.
-}moduleLedger.TransactionwhereimportLedger.UtilsimportLedger.TypesimportLedger.DatesimportLedger.PostingimportLedger.AmountimportLedger.Commodity(dollars,dollar,unknown)instanceShowTransactionwhereshow=showTransactionUnelidedinstanceShowModifierTransactionwhereshowt="= "++mtvalueexprt++"\n"++unlines(mapshow(mtpostingst))instanceShowPeriodicTransactionwhereshowt="~ "++ptperiodicexprt++"\n"++unlines(mapshow(ptpostingst))nulltransaction::Transactionnulltransaction=Transaction{tdate=nulldate,teffectivedate=Nothing,tstatus=False,tcode="",tdescription="",tcomment="",tpostings=[],tpreceding_comment_lines=""}{-|
Show a ledger entry, formatted for the print command. ledger 2.x's
standard format looks like this:
@
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
@
-}showTransaction::Transaction->StringshowTransaction=showTransaction'TrueFalseshowTransactionUnelided::Transaction->StringshowTransactionUnelided=showTransaction'FalseFalseshowTransactionForPrint::Bool->Transaction->StringshowTransactionForPrinteffective=showTransaction'FalseeffectiveshowTransaction'::Bool->Bool->Transaction->StringshowTransaction'elideeffectivet=unlines$[description]++showpostings(tpostingst)++[""]wheredescription=concat[date,status,code,desc,comment]date|effective=showdate$fromMaybe(tdatet)$teffectivedatet|otherwise=showdate(tdatet)++maybe""showedate(teffectivedatet)status=iftstatustthen" *"else""code=iflength(tcodet)>0thenprintf" (%s)"$tcodetelse""desc=' ':tdescriptiontcomment=ifnullcomthen""else" ; "++comwherecom=tcommenttshowdate=printf"%-10s".showDateshowedate=printf"=%s".showdateshowpostingsps|elide&&lengthps>1&&isTransactionBalancedt=mapshowposting(initps)++[showpostingnoamt(lastps)]|otherwise=mapshowpostingpswhereshowpostingp=showacctp++" "++showamount(pamountp)++showcomment(pcommentp)showpostingnoamtp=rstrip$showacctp++" "++showcomment(pcommentp)showacctp=" "++showstatusp++printf(printf"%%-%ds"w)(showAccountNameNothing(ptypep)(paccountp))w=maximum$map(length.paccount)psshowamount=printf"%12s".showMixedAmountOrZeroshowcomments=ifnullsthen""else" ; "++sshowstatusp=ifpstatuspthen"* "else""-- | Show an account name, clipped to the given width if any, and-- appropriately bracketed/parenthesised for the given posting type.showAccountName::MaybeInt->PostingType->AccountName->StringshowAccountNamew=fmtwherefmtRegularPosting=takew'fmtVirtualPosting=parenthesise.reverse.take(w'-2).reversefmtBalancedVirtualPosting=bracket.reverse.take(w'-2).reversew'=fromMaybe999999wparenthesises="("++s++")"brackets="["++s++"]"realPostings::Transaction->[Posting]realPostings=filterisReal.tpostingsvirtualPostings::Transaction->[Posting]virtualPostings=filterisVirtual.tpostingsbalancedVirtualPostings::Transaction->[Posting]balancedVirtualPostings=filterisBalancedVirtual.tpostings-- | Get the sums of a transaction's real, virtual, and balanced virtual postings.transactionPostingBalances::Transaction->(MixedAmount,MixedAmount,MixedAmount)transactionPostingBalancest=(sumPostings$realPostingst,sumPostings$virtualPostingst,sumPostings$balancedVirtualPostingst)-- | Is this transaction balanced ? A balanced transaction's real-- (non-virtual) postings sum to 0, and any balanced virtual postings-- also sum to 0.isTransactionBalanced::Transaction->BoolisTransactionBalancedt=isReallyZeroMixedAmountCostrsum&&isReallyZeroMixedAmountCostbvsumwhere(rsum,_,bvsum)=transactionPostingBalancest-- | Ensure that this entry is balanced, possibly auto-filling a missing-- amount first. We can auto-fill if there is just one non-virtual-- transaction without an amount. The auto-filled balance will be-- converted to cost basis if possible. If the entry can not be balanced,-- return an error message instead.balanceTransaction::Transaction->EitherStringTransactionbalanceTransactiont@Transaction{tpostings=ps}|lengthmissingamounts'>1=Left$printerr"could not balance this transaction (too many missing amounts)"|not$isTransactionBalancedt'=Left$printerr$nonzerobalanceerrort'|otherwise=Rightt'where(withamounts,missingamounts)=partitionhasAmount$filterisRealps(_,missingamounts')=partitionhasAmountpst'=t{tpostings=ps'}ps'|lengthmissingamounts==1=mapbalanceps|otherwise=pswherebalancep|isRealp&&not(hasAmountp)=p{pamount=costOfMixedAmount(-otherstotal)}|otherwise=pwhereotherstotal=sum$mappamountwithamountsprinterrs=intercalate"\n"[s,showTransactionUnelidedt]nonzerobalanceerror::Transaction->Stringnonzerobalanceerrort=printf"could not balance this transaction (%s%s%s)"rmsgsepbvmsgwhere(rsum,_,bvsum)=transactionPostingBalancestrmsg|isReallyZeroMixedAmountCostrsum=""|otherwise="real postings are off by "++showrsumbvmsg|isReallyZeroMixedAmountCostbvsum=""|otherwise="balanced virtual postings are off by "++showbvsumsep=ifnot(nullrmsg)&&not(nullbvmsg)then"; "else""-- | Convert the primary date to either the actual or effective date.ledgerTransactionWithDate::WhichDate->Transaction->TransactionledgerTransactionWithDateActualDatet=tledgerTransactionWithDateEffectiveDatet=txnTieKnott{tdate=fromMaybe(tdatet)(teffectivedatet)}-- | Ensure a transaction's postings refer back to it.txnTieKnot::Transaction->TransactiontxnTieKnott@Transaction{tpostings=ps}=t{tpostings=map(settxnt)ps}-- | Set a posting's parent transaction.settxn::Transaction->Posting->Postingsettxntp=p{ptransaction=Justt}tests_Transaction=TestList["showTransaction"~:doassertEqual"show a balanced transaction, eliding last amount"(unlines["2007/01/28 coopportunity"," expenses:food:groceries $47.18"," assets:checking",""])(lett=Transaction(parsedate"2007/01/28")NothingFalse"""coopportunity"""[PostingFalse"expenses:food:groceries"(Mixed[dollars47.18])""RegularPosting(Justt),PostingFalse"assets:checking"(Mixed[dollars(-47.18)])""RegularPosting(Justt)]""inshowTransactiont),"showTransaction"~:doassertEqual"show a balanced transaction, no eliding"(unlines["2007/01/28 coopportunity"," expenses:food:groceries $47.18"," assets:checking $-47.18",""])(lett=Transaction(parsedate"2007/01/28")NothingFalse"""coopportunity"""[PostingFalse"expenses:food:groceries"(Mixed[dollars47.18])""RegularPosting(Justt),PostingFalse"assets:checking"(Mixed[dollars(-47.18)])""RegularPosting(Justt)]""inshowTransactionUnelidedt)-- document some cases that arise in debug/testing:,"showTransaction"~:doassertEqual"show an unbalanced transaction, should not elide"(unlines["2007/01/28 coopportunity"," expenses:food:groceries $47.18"," assets:checking $-47.19",""])(showTransaction(txnTieKnot$Transaction(parsedate"2007/01/28")NothingFalse"""coopportunity"""[PostingFalse"expenses:food:groceries"(Mixed[dollars47.18])""RegularPostingNothing,PostingFalse"assets:checking"(Mixed[dollars(-47.19)])""RegularPostingNothing]"")),"showTransaction"~:doassertEqual"show an unbalanced transaction with one posting, should not elide"(unlines["2007/01/28 coopportunity"," expenses:food:groceries $47.18",""])(showTransaction(txnTieKnot$Transaction(parsedate"2007/01/28")NothingFalse"""coopportunity"""[PostingFalse"expenses:food:groceries"(Mixed[dollars47.18])""RegularPostingNothing]"")),"showTransaction"~:doassertEqual"show a transaction with one posting and a missing amount"(unlines["2007/01/28 coopportunity"," expenses:food:groceries ",""])(showTransaction(txnTieKnot$Transaction(parsedate"2007/01/28")NothingFalse"""coopportunity"""[PostingFalse"expenses:food:groceries"missingamt""RegularPostingNothing]"")),"showTransaction"~:doassertEqual"show a transaction with a priced commodityless amount"(unlines["2010/01/01 x"," a 1 @ $2"," b ",""])(showTransaction(txnTieKnot$Transaction(parsedate"2010/01/01")NothingFalse"""x"""[PostingFalse"a"(Mixed[Amountunknown1(Just$Mixed[Amountdollar{precision=0}2Nothing])])""RegularPostingNothing,PostingFalse"b"missingamt""RegularPostingNothing]""))]