hledger-flow-0.17.0: src/Hledger/Flow/Reports.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Hledger.Flow.Reports
( generateReports,
)
where
import Control.Concurrent.STM
import Control.Monad (unless, when)
import Data.Char (toLower)
import Data.Either
import qualified Data.List as List
import Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Hledger.Flow.DateTime (timeAction)
import Hledger.Flow.BaseDir (relativeToBase)
import Hledger.Flow.Common
import Hledger.Flow.Logging
import Hledger.Flow.PathHelpers (AbsDir, AbsFile, RelDir, RelFile)
import Hledger.Flow.RuntimeOptions
import qualified Hledger.Flow.Types as FlowTypes
import Path ((</>), parent, reldir, relfile, parseRelDir, toFilePath)
import qualified Path.IO as PathIO
import qualified System.Directory as Directory
import System.Environment (lookupEnv)
import Prelude hiding (putStrLn, readFile, writeFile)
data ReportParams = ReportParams
{ ledgerFile :: FilePath,
reportYears :: [Integer],
outputDir :: AbsDir
}
deriving (Show)
type ReportGenerator = RuntimeOptions -> TChan FlowTypes.LogMessage -> FilePath -> AbsDir -> Integer -> IO (Either AbsFile AbsFile)
generateReports :: RuntimeOptions -> IO ()
generateReports opts = withConsoleLogging $ \ch -> do
when (showOptions opts) $ channelOutLn ch (T.pack (show opts))
(reports, diff) <- timeAction $ generateReports' opts ch
let failedAttempts = lefts reports
let failedText = if List.null failedAttempts then "" else "(and attempted to write " <> T.pack (show (length failedAttempts)) <> " more) "
channelOutLn ch $ "Generated " <> T.pack (show (length (rights reports))) <> " reports " <> failedText <> "in " <> T.pack (show diff)
generateReports' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> IO [Either AbsFile AbsFile]
generateReports' opts ch = do
let wipMsg =
"These reports can be used as a starting point for more tailored reports.\n"
<> "The first line of each report contains the command used - change the parameters and use it in your own reports.\n"
channelOutLn ch wipMsg
owners <- listOwners opts
ledgerEnvValue <- lookupEnv "LEDGER_FILE"
let hledgerJournal = fromMaybe (toFilePath (baseDir opts </> [relfile|all-years.journal|])) ledgerEnvValue
hledgerJournalExists <- Directory.doesFileExist hledgerJournal
unless hledgerJournalExists $ ioError $ userError $
"Unable to find journal file: " ++ hledgerJournal ++ "\nIs your LEDGER_FILE environment variable set correctly?"
let journalWithYears = baseDir opts </> [relfile|import/all-years.journal|]
let aggregateReportDir
| any ((== toFilePath [reldir|all|]) . map toLower . toFilePath) owners = baseDir opts </> [reldir|reports/_all_|]
| otherwise = baseDir opts </> [reldir|reports/all|]
aggregateYears <- includeYears ch journalWithYears
let aggregateParams =
ReportParams
{ ledgerFile = hledgerJournal,
reportYears = aggregateYears,
outputDir = aggregateReportDir
}
let aggregateOnlyReports = reportActions opts ch [transferBalance] aggregateParams
ownerParams <- ownerParameters opts ch owners
let ownerWithAggregateParams = (if length owners > 1 then [aggregateParams] else []) ++ ownerParams
let sharedOptions = (if prettyReports opts then ["--pretty-tables"] else []) ++ ["--depth", "2"]
let ownerWithAggregateReports = List.concat $ fmap (reportActions opts ch [incomeStatement sharedOptions, incomeMonthlyStatement sharedOptions, balanceSheet sharedOptions]) ownerWithAggregateParams
let ownerOnlyReports = List.concat $ fmap (reportActions opts ch [accountList, unknownTransactions]) ownerParams
parAwareActions opts (aggregateOnlyReports ++ ownerWithAggregateReports ++ ownerOnlyReports)
reportActions :: RuntimeOptions -> TChan FlowTypes.LogMessage -> [ReportGenerator] -> ReportParams -> [IO (Either AbsFile AbsFile)]
reportActions opts ch reports (ReportParams journal years reportsDir) = do
y <- years
map (\r -> r opts ch journal reportsDir y) reports
accountList :: ReportGenerator
accountList opts ch journal baseOutDir year = do
let reportArgs = ["accounts"]
generateReport opts ch journal year baseOutDir [relfile|accounts.txt|] reportArgs (not . T.null)
unknownTransactions :: ReportGenerator
unknownTransactions opts ch journal baseOutDir year = do
let reportArgs = ["print", "unknown"]
generateReport opts ch journal year baseOutDir [relfile|unknown-transactions.txt|] reportArgs (not . T.null)
incomeStatement :: [T.Text] -> ReportGenerator
incomeStatement sharedOptions opts ch journal baseOutDir year = do
let reportArgs = ["incomestatement"] ++ sharedOptions
generateReport opts ch journal year baseOutDir [relfile|income-expenses.txt|] reportArgs (not . T.null)
incomeMonthlyStatement :: [T.Text] -> ReportGenerator
incomeMonthlyStatement sharedOptions opts ch journal baseOutDir year = do
let reportArgs = ["incomestatement"] ++ sharedOptions ++ ["--monthly"]
generateReport opts ch journal year baseOutDir [relfile|monthly/income-expenses.txt|] reportArgs (not . T.null)
balanceSheet :: [T.Text] -> ReportGenerator
balanceSheet sharedOptions opts ch journal baseOutDir year = do
let reportArgs = ["balancesheet"] ++ sharedOptions ++ ["--flat"]
generateReport opts ch journal year baseOutDir [relfile|balance-sheet.txt|] reportArgs (not . T.null)
transferBalance :: ReportGenerator
transferBalance opts ch journal baseOutDir year = do
let reportArgs = ["balance"] ++ (if prettyReports opts then ["--pretty-tables"] else []) ++ ["--quarterly", "--flat", "--no-total", "transfer"]
generateReport opts ch journal year baseOutDir [relfile|transfer-balance.txt|] reportArgs (\txt -> (length $ T.lines txt) > 4)
generateReport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> FilePath -> Integer -> AbsDir -> RelFile -> [T.Text] -> (T.Text -> Bool) -> IO (Either AbsFile AbsFile)
generateReport opts ch journal year reportsDir fileName args successCheck = do
yearDir <- parseRelDir (show year)
let outputFile = reportsDir </> yearDir </> fileName
PathIO.createDirIfMissing True (parent outputFile)
let relativeJournal = relativeToBase opts journal
let relativeOutputFile = relativeToBase opts (toFilePath outputFile)
let confArgs = hledgerConfArgs opts
let reportArgs = confArgs ++ ["--file", T.pack journal, "--period", T.pack (show year)] ++ args
let reportDisplayArgs = confArgs ++ ["--file", T.pack relativeJournal, "--period", T.pack (show year)] ++ args
let hledger = T.pack (toFilePath (FlowTypes.hlPath (hledgerInfo opts)))
let cmdLabel = "hledger " <> showCmdArgs reportDisplayArgs
((exitCode, stdOut, _), _) <- timeAndExitOnErr opts ch cmdLabel dummyLogger channelErr (hledger, reportArgs, Nothing)
if (successCheck stdOut)
then do
T.writeFile (toFilePath outputFile) (cmdLabel <> "\n\n" <> stdOut)
logVerbose opts ch $ "Wrote " <> T.pack relativeOutputFile
return $ Right outputFile
else do
channelErrLn ch $ "Did not write '" <> T.pack relativeOutputFile <> "' (" <> cmdLabel <> ") " <> T.pack (show exitCode)
exists <- PathIO.doesFileExist outputFile
when exists (PathIO.removeFile outputFile)
return $ Left outputFile
ownerParameters :: RuntimeOptions -> TChan FlowTypes.LogMessage -> [RelDir] -> IO [ReportParams]
ownerParameters opts ch owners = do
let actions = map (ownerParameters' opts ch) owners
parAwareActions opts actions
ownerParameters' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> RelDir -> IO ReportParams
ownerParameters' opts ch owner = do
let ownerJournal = baseDir opts </> [reldir|import|] </> owner </> [relfile|all-years.journal|]
reportsDir = baseDir opts </> [reldir|reports|] </> owner
years <- includeYears ch ownerJournal
return $ ReportParams (toFilePath ownerJournal) years reportsDir