hledger/hledger/Hledger/Cli/Commands/Print.hs
Stephen Morgan e233f001c5 fix!: utf-8: Use with-utf8 to ensure all files are read and written with utf8 encoding. (#1619)
May also fix #1154, #1033, #708, #536, #73: testing is needed.

This aims to solve all problems where misconfigured locales lead to
parsers failing on utf8-encoded data. This should hopefully avoid
encoding issues, but since it fundamentally alters how encoding is dealt
with it may lead to unexpected outcomes. Widespread testing on a number
of different platforms would be useful.
2022-05-22 13:12:19 +10:00

211 lines
8.4 KiB
Haskell

{-|
A ledger-compatible @print@ command.
-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Hledger.Cli.Commands.Print (
printmode
,print'
-- ,entriesReportAsText
,originalTransaction
)
where
import Data.Text (Text)
import Data.List (intersperse)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO (putStr, putStrLn) -- Only putStr and friends are safe
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB
import Lens.Micro ((^.), _Just, has)
import System.Console.CmdArgs.Explicit
import Hledger
import Hledger.Read.CsvReader (CSV, printCSV)
import Hledger.Cli.CliOptions
import Hledger.Cli.Utils
printmode = hledgerCommandMode
$(embedFileRelative "Hledger/Cli/Commands/Print.txt")
([let arg = "STR" in
flagReq ["match","m"] (\s opts -> Right $ setopt "match" s opts) arg
("show the transaction whose description is most similar to "++arg++", and is most recent")
,flagNone ["explicit","x"] (setboolopt "explicit")
"show all amounts explicitly"
,flagNone ["show-costs"] (setboolopt "show-costs")
"show transaction prices even with conversion postings"
,flagNone ["new"] (setboolopt "new")
"show only newer-dated transactions added in each file since last run"
,outputFormatFlag ["txt","csv","json","sql"]
,outputFileFlag
])
[generalflagsgroup1]
hiddenflags
([], Just $ argsFlag "[QUERY]")
-- | Print journal transactions in standard format.
print' :: CliOpts -> Journal -> IO ()
print' opts j = do
-- The print command should show all amounts with their original decimal places,
-- but as part of journal reading the posting amounts have already been normalised
-- according to commodity display styles, and currently it's not easy to avoid
-- that. For now we try to reverse it by increasing all amounts' decimal places
-- sufficiently to show the amount exactly. The displayed amounts may have minor
-- differences from the originals, such as trailing zeroes added.
let j' = journalMapPostingAmounts mixedAmountSetFullPrecision j
case maybestringopt "match" $ rawopts_ opts of
Nothing -> printEntries opts j'
Just desc -> printMatch opts j' $ T.pack $ dbg1 "finding best match for description" desc
printEntries :: CliOpts -> Journal -> IO ()
printEntries opts@CliOpts{reportspec_=rspec} j =
writeOutputLazyText opts . render $ entriesReport rspec j
where
fmt = outputFormatFromOpts opts
render | fmt=="txt" = entriesReportAsText opts
| fmt=="csv" = printCSV . entriesReportAsCsv
| fmt=="json" = toJsonText
| fmt=="sql" = entriesReportAsSql
| otherwise = error' $ unsupportedOutputFormatError fmt -- PARTIAL:
entriesReportAsText :: CliOpts -> EntriesReport -> TL.Text
entriesReportAsText opts =
TB.toLazyText . foldMap (TB.fromText . showTransaction . maybeStripPrices . whichtxn)
where
whichtxn
-- With -x, use the fully-inferred txn with all amounts & txn prices explicit.
| boolopt "explicit" (rawopts_ opts) = id
-- Or also, if any of -B/-V/-X/--value are active.
-- Because of #551, and because of print -V valuing only one
-- posting when there's an implicit txn price.
-- So -B/-V/-X/--value implies -x. Is this ok ?
| has (value . _Just) opts = id
-- By default, use the original as-written-in-the-journal txn.
| otherwise = originalTransaction
maybeStripPrices
-- Strip prices when inferring equity, unless the show_costs_ is set
| opts ^. infer_equity && not (opts ^. show_costs) =
transactionTransformPostings postingStripPrices
| otherwise = id
-- Replace this transaction's postings with the original postings if any, but keep the
-- current possibly rewritten account names, and the inferred values of any auto postings
originalTransaction t = t { tpostings = map originalPostingPreservingAccount $ tpostings t }
-- Get the original posting if any, but keep the current possibly rewritten account name, and
-- the inferred values of any auto postings
originalPostingPreservingAccount p = orig
{ paccount = paccount p
, pamount = pamount $ if isGenerated then p else orig }
where
orig = originalPosting p
isGenerated = "generated-posting" `elem` map fst (ptags p)
-- XXX
-- tests_showTransactions = [
-- "showTransactions" ~: do
-- -- "print expenses" ~:
-- do
-- let opts = defreportopts{query_="expenses"}
-- d <- getCurrentDay
-- showTransactions opts (queryFromOpts d opts) samplejournal `is` unlines
-- ["2008/06/03 * eat & shop"
-- ," expenses:food $1"
-- ," expenses:supplies $1"
-- ," assets:cash $-2"
-- ,""
-- ]
-- -- , "print report with depth arg" ~:
-- do
-- let opts = defreportopts{depth_=Just 2}
-- d <- getCurrentDay
-- showTransactions opts (queryFromOpts d opts) samplejournal `is` unlines
-- ["2008/01/01 income"
-- ," assets:bank:checking $1"
-- ," income:salary $-1"
-- ,""
-- ,"2008/06/01 gift"
-- ," assets:bank:checking $1"
-- ," income:gifts $-1"
-- ,""
-- ,"2008/06/03 * eat & shop"
-- ," expenses:food $1"
-- ," expenses:supplies $1"
-- ," assets:cash $-2"
-- ,""
-- ,"2008/12/31 * pay off"
-- ," liabilities:debts $1"
-- ," assets:bank:checking $-1"
-- ,""
-- ]
-- ]
entriesReportAsSql :: EntriesReport -> TL.Text
entriesReportAsSql txns = TB.toLazyText $ mconcat
[ TB.fromText "create table if not exists postings(id serial,txnidx int,date1 date,date2 date,status text,code text,description text,comment text,account text,amount numeric,commodity text,credit numeric,debit numeric,posting_status text,posting_comment text);\n"
, TB.fromText "insert into postings(txnidx,date1,date2,status,code,description,comment,account,amount,commodity,credit,debit,posting_status,posting_comment) values\n"
, mconcat . intersperse (TB.fromText ",") $ map values csv
, TB.fromText ";\n"
]
where
values vs = TB.fromText "(" <> mconcat (intersperse (TB.fromText ",") $ map toSql vs) <> TB.fromText ")\n"
toSql "" = TB.fromText "NULL"
toSql s = TB.fromText "'" <> TB.fromText (T.replace "'" "''" s) <> TB.fromText "'"
csv = concatMap (transactionToCSV . transactionMapPostingAmounts (mapMixedAmount setDecimalPoint)) txns
where
setDecimalPoint a = a{astyle=(astyle a){asdecimalpoint=Just '.'}}
entriesReportAsCsv :: EntriesReport -> CSV
entriesReportAsCsv txns =
["txnidx","date","date2","status","code","description","comment","account","amount","commodity","credit","debit","posting-status","posting-comment"] :
concatMap transactionToCSV txns
-- | Generate one CSV record per posting, duplicating the common transaction fields.
-- The txnidx field (transaction index) allows postings to be grouped back into transactions.
transactionToCSV :: Transaction -> CSV
transactionToCSV t =
map (\p -> T.pack (show idx):date:date2:status:code:description:comment:p)
(concatMap postingToCSV $ tpostings t)
where
idx = tindex t
description = tdescription t
date = showDate (tdate t)
date2 = maybe "" showDate $ tdate2 t
status = T.pack . show $ tstatus t
code = tcode t
comment = T.strip $ tcomment t
postingToCSV :: Posting -> CSV
postingToCSV p =
map (\(a@(Amount {aquantity=q,acommodity=c})) ->
-- commodity goes into separate column, so we suppress it, along with digit group
-- separators and prices
let a_ = amountStripPrices a{acommodity=""} in
let showamt = wbToText . showAmountB csvDisplay in
let amount = showamt a_ in
let credit = if q < 0 then showamt $ negate a_ else "" in
let debit = if q >= 0 then showamt a_ else "" in
[account, amount, c, credit, debit, status, comment])
. amounts $ pamount p
where
status = T.pack . show $ pstatus p
account = showAccountName Nothing (ptype p) (paccount p)
comment = T.strip $ pcomment p
-- --match
-- | Print the transaction most closely and recently matching a description
-- (and the query, if any).
printMatch :: CliOpts -> Journal -> Text -> IO ()
printMatch opts j desc = do
case journalSimilarTransaction opts j desc of
Nothing -> TIO.putStrLn "no matches found."
Just t -> TIO.putStr $ showTransaction t