Skip to content

Commit 9005b10

Browse files
authored
Merge pull request #57 from apauley/reports
Generate reports per owner per year
2 parents 0bd60bd + 69141b3 commit 9005b10

File tree

7 files changed

+157
-35
lines changed

7 files changed

+157
-35
lines changed

.github/ISSUE_TEMPLATE/bug_report.md

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ $ hledger-flow --show-options import
2626
```
2727

2828
**To Reproduce**
29+
30+
FYI, we have a repo with some example transactions which you can use to run `hledger-flow` on:
31+
https://github.com/apauley/hledger-flow-example
32+
33+
Can you reproduce your issue on these example files?
34+
2935
Steps to reproduce the behavior:
3036
1. Given this input (files or other input)
3137
2. And when running this exact command (with `--show-options`)

.github/ISSUE_TEMPLATE/feature_request.md

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ the runtime options with `--show-options` e.g:
2626
$ hledger-flow --show-options import
2727
```
2828

29+
**Our Example Statements Repository**
30+
31+
FYI, we have a repo with some example transactions which you can use to run `hledger-flow` on:
32+
https://github.com/apauley/hledger-flow-example
33+
34+
Can you give examples of what you would like by running `hledger-flow` on these files?
2935

3036
**Describe the solution you'd like**
3137
A clear and concise description of what you want to happen.

ChangeLog.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog for [hledger-flow](https://github.com/apauley/hledger-flow)
22

3+
## 0.12.1
4+
5+
Generate some reports per owner.
6+
7+
Report generation is still a work-in-progress.
8+
9+
https://github.com/apauley/hledger-flow/pull/57
10+
311
## 0.12.0
412

513
- Re-organised the command-line interface:

package.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: hledger-flow
2-
version: 0.12.0.99
2+
version: 0.12.1.0
33
synopsis: An hledger workflow focusing on automated statement import and classification.
44
category: Finance, Console
55
license: GPL-3

src/Hledger/Flow/Common.hs

+32
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ import Turtle
66
import Prelude hiding (FilePath, putStrLn)
77
import qualified Data.Text as T
88
import qualified Data.Text.IO as T
9+
import qualified Data.Text.Read as T
910
import qualified GHC.IO.Handle.FD as H
1011

12+
import Data.Char (isDigit)
1113
import Data.Maybe
14+
import Data.Either
15+
1216
import qualified Control.Foldl as Fold
1317
import qualified Data.Map.Strict as Map
1418
import Data.Time.LocalTime
@@ -446,3 +450,31 @@ extractImportDirs inputFile = do
446450
%"\n\nhledger-flow expects to find input files in this structure:\n"%
447451
"import/owner/bank/account/filestate/year/trxfile\n\n"%
448452
"Have a look at the documentation for a detailed explanation:\n"%s) inputFile (docURL "input-files")
453+
454+
listOwners :: HasBaseDir o => o -> Shell FilePath
455+
listOwners opts = fmap basename $ lsDirs $ (baseDir opts) </> "import"
456+
457+
intPath :: Integer -> FilePath
458+
intPath = fromText . (format d)
459+
460+
includeYears :: TChan LogMessage -> FilePath -> IO [Integer]
461+
includeYears ch includeFile = do
462+
txt <- readTextFile includeFile
463+
case includeYears' txt of
464+
Left msg -> do
465+
channelErrLn ch msg
466+
return []
467+
Right years -> return years
468+
469+
includeYears' :: Text -> Either Text [Integer]
470+
includeYears' txt = case partitionEithers (includeYears'' txt) of
471+
(errors, []) -> do
472+
let msg = format ("Unable to extract years from the following text:\n"%s%"\nErrors:\n"%s) txt (T.intercalate "\n" $ map T.pack errors)
473+
Left msg
474+
(_, years) -> Right years
475+
476+
includeYears'' :: Text -> [Either String Integer]
477+
includeYears'' txt = map extractDigits (T.lines txt)
478+
479+
extractDigits :: Text -> Either String Integer
480+
extractDigits txt = fmap fst $ (T.decimal . (T.filter isDigit)) txt

src/Hledger/Flow/Reports.hs

+58-33
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ module Hledger.Flow.Reports
66

77
import Turtle hiding (stdout, stderr, proc)
88
import Prelude hiding (FilePath, putStrLn, writeFile)
9-
import qualified Data.Text as T
10-
import qualified Hledger.Flow.Types as FlowTypes
119
import Hledger.Flow.Report.Types
1210
import Hledger.Flow.Common
1311
import Control.Concurrent.STM
12+
import Data.Either
13+
14+
import qualified Data.Text as T
15+
import qualified Hledger.Flow.Types as FlowTypes
16+
import qualified Data.List as List
1417

1518
generateReports :: ReportOptions -> IO ()
1619
generateReports opts = sh (
@@ -19,50 +22,72 @@ generateReports opts = sh (
1922
logHandle <- fork $ consoleChannelLoop ch
2023
liftIO $ if (showOptions opts) then channelOutLn ch (repr opts) else return ()
2124
(reports, diff) <- time $ liftIO $ generateReports' opts ch
22-
liftIO $ channelOutLn ch $ format ("Generated "%d%" reports in "%s) (length reports) $ repr diff
25+
let failedAttempts = lefts reports
26+
let failedText = if List.null failedAttempts then "" else format ("(and attempted to write "%d%" more) ") $ length failedAttempts
27+
liftIO $ channelOutLn ch $ format ("Generated "%d%" reports "%s%"in "%s) (length (rights reports)) failedText $ repr diff
2328
liftIO $ terminateChannelLoop ch
2429
wait logHandle
2530
)
2631

27-
generateReports' :: ReportOptions -> TChan FlowTypes.LogMessage -> IO [FilePath]
32+
generateReports' :: ReportOptions -> TChan FlowTypes.LogMessage -> IO [Either FilePath FilePath]
2833
generateReports' opts ch = do
29-
channelOutLn ch "Report generation has not been fully implemented yet. Keep an eye out for report pull requests: https://github.com/apauley/hledger-flow/pulls"
30-
ownerReports opts ch "everyone"
34+
let wipMsg = "Report generation is still a work-in-progress - please let me know how this can be more useful.\n\n"
35+
<> "Keep an eye out for report-related pull requests and issues, and feel free to submit some of your own:\n"
36+
<> "https://github.com/apauley/hledger-flow/pulls\n"
37+
<> "https://github.com/apauley/hledger-flow/issues\n"
38+
channelOutLn ch wipMsg
39+
owners <- single $ shellToList $ listOwners opts
40+
let baseJournal = journalFile opts []
41+
let baseReportDir = outputDir opts []
42+
years <- includeYears ch baseJournal
43+
let reportParams = [(baseJournal, baseReportDir)] ++ map (ownerParams opts) owners
44+
let actions = List.concat $ fmap (generateReports'' opts ch years) reportParams
45+
if (sequential opts) then sequence actions else single $ shellToList $ parallel actions
3146

32-
ownerReports :: ReportOptions -> TChan FlowTypes.LogMessage -> Text -> IO [FilePath]
33-
ownerReports opts ch owner = do
34-
let journal = (baseDir opts) </> "all-years" <.> "journal"
35-
let reportsDir = (baseDir opts) </> "reports" </> fromText owner
36-
let actions = map (\r -> r opts ch journal reportsDir) [accountList, incomeStatement]
37-
results <- if (sequential opts) then sequence actions else single $ shellToList $ parallel actions
38-
return $ map fst results
47+
generateReports'' :: ReportOptions -> TChan FlowTypes.LogMessage -> [Integer] -> (FilePath, FilePath) -> [IO (Either FilePath FilePath)]
48+
generateReports'' opts ch years (journal, reportsDir) = do
49+
y <- years
50+
let actions = map (\r -> r opts ch journal reportsDir y) [accountList, incomeStatement]
51+
map (fmap fst) actions
3952

40-
incomeStatement :: ReportOptions -> TChan FlowTypes.LogMessage -> FilePath -> FilePath -> IO (FilePath, FlowTypes.FullTimedOutput)
41-
incomeStatement opts ch journal reportsDir = do
42-
mktree reportsDir
43-
let outputFile = reportsDir </> "income-expenses" <.> "txt"
53+
incomeStatement :: ReportOptions -> TChan FlowTypes.LogMessage -> FilePath -> FilePath -> Integer -> IO (Either FilePath FilePath, FlowTypes.FullTimedOutput)
54+
incomeStatement opts ch journal reportsDir year = do
4455
let sharedOptions = ["--depth", "2", "--pretty-tables", "not:equity"]
45-
let reportArgs = ["incomestatement"] ++ sharedOptions ++ ["--average", "--yearly"]
46-
generateReport' opts ch journal outputFile reportArgs
56+
let reportArgs = ["incomestatement"] ++ sharedOptions
57+
generateReport opts ch journal reportsDir year ("income-expenses" <.> "txt") reportArgs
4758

48-
accountList :: ReportOptions -> TChan FlowTypes.LogMessage -> FilePath -> FilePath -> IO (FilePath, FlowTypes.FullTimedOutput)
49-
accountList opts ch journal reportsDir = do
50-
let outputFile = reportsDir </> "accounts" <.> "txt"
59+
accountList :: ReportOptions -> TChan FlowTypes.LogMessage -> FilePath -> FilePath -> Integer -> IO (Either FilePath FilePath, FlowTypes.FullTimedOutput)
60+
accountList opts ch journal reportsDir year = do
5161
let reportArgs = ["accounts"]
52-
generateReport' opts ch journal outputFile reportArgs
62+
generateReport opts ch journal reportsDir year ("accounts" <.> "txt") reportArgs
5363

54-
generateReport' :: ReportOptions -> TChan FlowTypes.LogMessage -> FilePath -> FilePath -> [Text] -> IO (FilePath, FlowTypes.FullTimedOutput)
55-
generateReport' opts ch journal outputFile args = do
56-
let reportsDir = directory outputFile
64+
generateReport :: ReportOptions -> TChan FlowTypes.LogMessage -> FilePath -> FilePath -> Integer -> FilePath -> [Text] -> IO (Either FilePath FilePath, FlowTypes.FullTimedOutput)
65+
generateReport opts ch journal baseOutDir year fileName args = do
66+
let reportsDir = baseOutDir </> intPath year
5767
mktree reportsDir
68+
let outputFile = reportsDir </> fileName
5869
let relativeJournal = relativeToBase opts journal
59-
let reportArgs = ["--file", format fp journal] ++ args
60-
let reportDisplayArgs = ["--file", format fp relativeJournal] ++ args
70+
let reportArgs = ["--file", format fp journal, "--period", repr year] ++ args
71+
let reportDisplayArgs = ["--file", format fp relativeJournal, "--period", repr year] ++ args
6172
let hledger = format fp $ FlowTypes.hlPath . hledgerInfo $ opts :: Text
6273
let cmdLabel = format ("hledger "%s) $ showCmdArgs reportDisplayArgs
6374
result@((exitCode, stdOut, _), _) <- timeAndExitOnErr opts ch cmdLabel dummyLogger channelErr procStrictWithErr (hledger, reportArgs, empty)
64-
if not (T.null stdOut) then do
65-
writeTextFile outputFile (cmdLabel <> "\n\n"<> stdOut)
66-
channelOutLn ch $ format ("Wrote "%fp) $ relativeToBase opts outputFile
67-
else channelErrLn ch $ format ("No report output for '"%s%"' "%s) cmdLabel (repr exitCode)
68-
return (outputFile, result)
75+
if not (T.null stdOut)
76+
then
77+
do
78+
writeTextFile outputFile (cmdLabel <> "\n\n"<> stdOut)
79+
channelOutLn ch $ format ("Wrote "%fp) $ relativeToBase opts outputFile
80+
return (Right outputFile, result)
81+
else
82+
do
83+
channelErrLn ch $ format ("No report output for '"%s%"' "%s) cmdLabel (repr exitCode)
84+
return (Left outputFile, result)
85+
86+
journalFile :: ReportOptions -> [FilePath] -> FilePath
87+
journalFile opts dirs = (foldl (</>) (baseDir opts) dirs) </> "all-years" <.> "journal"
88+
89+
outputDir :: ReportOptions -> [FilePath] -> FilePath
90+
outputDir opts dirs = foldl (</>) (baseDir opts) ("reports":dirs)
91+
92+
ownerParams :: ReportOptions -> FilePath -> (FilePath, FilePath)
93+
ownerParams opts owner = (journalFile opts ["import", owner], outputDir opts [owner])

test/Common/Unit.hs

+46-1
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,56 @@ import Prelude hiding (FilePath)
1010
import TestHelpers
1111
import Hledger.Flow.Common
1212

13+
import Data.Either
14+
import qualified Data.Text as T
15+
import qualified Data.List as List
16+
1317
testShowCmdArgs = TestCase (
1418
do
1519
let options = ["--number", "/tmp/file with spaces"]
1620
let expected = "--number '/tmp/file with spaces'"
1721
let actual = showCmdArgs options
1822
assertEqual "Convert command-line arguments to text" expected actual)
1923

20-
tests = TestList [testShowCmdArgs]
24+
testIncludeYears = TestCase (
25+
do
26+
let txterr = "Some text without years"
27+
let expectederr = ["Unable to extract years from the following text:", txterr, "Errors:"]
28+
let actualerr = (init . head) $ map (T.lines) $ lefts [includeYears' txterr] :: [Text]
29+
assertEqual "Get a list of years from an include file - error case" expectederr actualerr
30+
31+
let txt1 = "### Generated by hledger-flow - DO NOT EDIT ###\n\n" <>
32+
"!include import/2014-include.journal\n" <>
33+
"!include import/2015-include.journal\n" <>
34+
"!include import/2016-include.journal\n" <>
35+
"!include import/2017-include.journal\n" <>
36+
"!include import/2018-include.journal\n" <>
37+
"!include import/2019-include.journal"
38+
39+
let expected1 = Right [2014..2019]
40+
let actual1 = includeYears' txt1
41+
assertEqual "Get a list of years from an include file - success case 1" expected1 actual1
42+
43+
let txt2 = "!include 2019-include.journal"
44+
45+
let expected2 = Right [2019]
46+
let actual2 = includeYears' txt2
47+
assertEqual "Get a list of years from an include file - success case 2" expected2 actual2
48+
)
49+
50+
testExtractDigits = TestCase (
51+
do
52+
let txt1 = "A number: 321\nAnother number is 42, so is 0"
53+
54+
let expected1 = Right 321420
55+
let actual1 = extractDigits txt1
56+
assertEqual "Extract digits from text 1" expected1 actual1
57+
58+
let txt2 = "No numbers in this line"
59+
60+
let expected2 = Left "input does not start with a digit"
61+
let actual2 = extractDigits txt2
62+
assertEqual "Extract digits from text 2" expected2 actual2
63+
)
64+
65+
tests = TestList [testShowCmdArgs, testIncludeYears, testExtractDigits]

0 commit comments

Comments
 (0)