hledger-forecast 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +87 -26
- data/example.yml +18 -5
- data/hledger-forecast.gemspec +1 -0
- data/lib/hledger_forecast/generator.rb +33 -1
- data/lib/hledger_forecast/options.rb +1 -0
- data/lib/hledger_forecast/summarize.rb +73 -9
- data/lib/hledger_forecast/version.rb +1 -1
- data/lib/hledger_forecast.rb +1 -0
- data/spec/custom_spec.rb +47 -0
- data/spec/stubs/custom/forecast_custom_days.yml +14 -0
- data/spec/stubs/custom/forecast_custom_months.yml +14 -0
- data/spec/stubs/custom/forecast_custom_weeks.yml +14 -0
- data/spec/stubs/custom/forecast_custom_weeks_twice.yml +24 -0
- data/spec/stubs/custom/output_custom_days.journal +24 -0
- data/spec/stubs/custom/output_custom_months.journal +20 -0
- data/spec/stubs/custom/output_custom_weeks.journal +28 -0
- data/spec/stubs/custom/output_custom_weeks_twice.journal +44 -0
- data/spec/stubs/monthly/forecast_monthly.yml +6 -0
- data/spec/stubs/monthly/output_monthly.journal +12 -0
- metadata +35 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4e4232c29fd944d0483590e646c0c5e1566357ec8dfc9f942007cdea6121dcad
|
4
|
+
data.tar.gz: 2a8636024b63cdce5cfe7bbec937c123a9f78492bfb53ac873f40e9aa00d204d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a725ff17d252a8d2427ba4e2dd2c6fa90d10e4cc9e853a6d9293eebde6aa9a594c96a9e57f387c858767b03b56e3bb3717b49bee7adc64d32a47bcd93118ebe
|
7
|
+
data.tar.gz: 118d14cde7575795a9e6632b856b4a9a60392aa19d88ce4030e555f49b580aa0cd924a61e9d6970bfe1e26306b848af756337f50eb2fc48203662714f1edd4a3
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -2,18 +2,17 @@
|
|
2
2
|
|
3
3
|
[](https://github.com/olimorris/hledger-forecast/actions/workflows/ci.yml)
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
Uses a YAML file to generate monthly, quarterly, yearly and one-off transactions for better forecasting in [Hledger](https://github.com/simonmichael/hledger).
|
5
|
+
Uses a YAML file to generate periodic transactions for better forecasting in [Hledger](https://github.com/simonmichael/hledger).
|
8
6
|
|
9
7
|
See the [rationale](#brain-rationale) section for why this gem may be useful to you.
|
10
8
|
|
11
9
|
## :sparkles: Features
|
12
10
|
|
13
11
|
- :book: Uses a simple YAML config file to generate periodic transactions
|
14
|
-
- :date:
|
12
|
+
- :date: Generate forecasts between specified start and end dates
|
15
13
|
- :heavy_dollar_sign: Full currency support (uses the [RubyMoney](https://github.com/RubyMoney/money) gem)
|
16
14
|
- :computer: Simple and easy to use CLI
|
15
|
+
- :chart_with_upwards_trend: Summarize your forecasts by period and category and output to the CLI
|
17
16
|
|
18
17
|
## :package: Installation
|
19
18
|
|
@@ -23,10 +22,12 @@ Assuming you have Ruby and [Rubygems](http://rubygems.org/pages/download) instal
|
|
23
22
|
|
24
23
|
## :rocket: Usage
|
25
24
|
|
26
|
-
|
25
|
+
Run:
|
27
26
|
|
28
27
|
hledger-forecast
|
29
28
|
|
29
|
+
> **Note**: This assumes that a `forecast.yml` exists in the current working directory
|
30
|
+
|
30
31
|
Running `hledger-forecast -h` shows the available options:
|
31
32
|
|
32
33
|
Usage: Hledger-Forecast [options]
|
@@ -41,18 +42,26 @@ Running `hledger-forecast -h` shows the available options:
|
|
41
42
|
-h, --help Show this message
|
42
43
|
--version Show version
|
43
44
|
|
44
|
-
|
45
|
+
Another example of a common command:
|
46
|
+
|
47
|
+
hledger-forecast -f my_forecast.yml -s 2023-05-01 -e 2024-12-31
|
48
|
+
|
49
|
+
This will generate an output file (`my_forecast.journal`) from the forecast file between the two date ranges.
|
50
|
+
|
51
|
+
### Using with Hledger
|
45
52
|
|
46
|
-
|
53
|
+
To use the outputs in Hledger:
|
54
|
+
|
55
|
+
hledger -f transactions.journal -f my_forecast.journal
|
47
56
|
|
48
57
|
where:
|
49
58
|
|
50
59
|
- `transactions.journal` might be your bank transactions (your "_actuals_")
|
51
|
-
- `
|
60
|
+
- `my_forecast.journal` is the generated forecast file
|
52
61
|
|
53
62
|
### A simple config file
|
54
63
|
|
55
|
-
> **Note**: See the [example.yml](https://github.com/olimorris/hledger-forecast/blob/main/example.yml) file for
|
64
|
+
> **Note**: See the [example.yml](https://github.com/olimorris/hledger-forecast/blob/main/example.yml) file for an example of a complex config file
|
56
65
|
|
57
66
|
Firstly, create a `yml` file which will contain the transactions you'd like to forecast:
|
58
67
|
|
@@ -68,6 +77,9 @@ monthly:
|
|
68
77
|
- amount: 500
|
69
78
|
category: "[Expenses:Food]"
|
70
79
|
description: Food
|
80
|
+
|
81
|
+
settings:
|
82
|
+
currency: GBP
|
71
83
|
```
|
72
84
|
|
73
85
|
Let's examine what's going on in this config file:
|
@@ -81,34 +93,83 @@ Let's examine what's going on in this config file:
|
|
81
93
|
|
82
94
|
#### Periods
|
83
95
|
|
84
|
-
|
96
|
+
Besides monthly recurring transactions, the app also supports the following periods:
|
97
|
+
|
98
|
+
- `quarterly` - For transactions every _3 months_ from the given start date
|
99
|
+
- `half-yearly` - For transactions every _6 months_ from the given start date
|
100
|
+
- `yearly` - Generate transactions _once a year_ from the given start date
|
101
|
+
- `once` - Generate _one-time_ transactions on a specified date
|
102
|
+
- `custom` - Generate transactions every _n days/weeks/months_
|
103
|
+
|
104
|
+
##### Custom period
|
105
|
+
|
106
|
+
A custom period allows you to specify a given number of days, weeks or months for a transaction to repeat within. These can be included in the config file as follows:
|
107
|
+
|
108
|
+
```yaml
|
109
|
+
# forecast.yml
|
110
|
+
custom:
|
111
|
+
- description: Fortnightly hair and beauty spend
|
112
|
+
recurrence:
|
113
|
+
period: weeks
|
114
|
+
quantity: 2
|
115
|
+
account: "[Assets:Bank]"
|
116
|
+
start: "2023-03-01"
|
117
|
+
transactions:
|
118
|
+
- amount: 80
|
119
|
+
category: "[Expenses:Personal Care]"
|
120
|
+
description: Hair and beauty
|
121
|
+
```
|
122
|
+
|
123
|
+
Where `quantity` is an integer and `period` is one of:
|
124
|
+
|
125
|
+
- days
|
126
|
+
- weeks
|
127
|
+
- months
|
85
128
|
|
86
|
-
|
87
|
-
- `half-yearly`
|
88
|
-
- `yearly`
|
89
|
-
- `once`
|
129
|
+
#### Date constraints
|
90
130
|
|
91
|
-
The
|
131
|
+
The core of any solid forecast is predicting the correct periods that costs will fall into. When running the app from the CLI, you can specify specific dates to generate transactions over (see the [usage](#rocket-usage) section).
|
92
132
|
|
93
|
-
|
133
|
+
You can further control the dates at a period/top-level as well as at a transaction level:
|
94
134
|
|
95
|
-
|
135
|
+
##### Top level
|
96
136
|
|
97
|
-
|
137
|
+
In the example below, all transactions in the `monthly` block will be constrained by the end date:
|
98
138
|
|
99
139
|
```yaml
|
100
140
|
# forecast.yml
|
101
|
-
|
102
|
-
|
141
|
+
monthly:
|
142
|
+
- account: "[Assets:Bank]"
|
143
|
+
start: "2023-03-01"
|
144
|
+
end: "2025-01-01"
|
145
|
+
transactions:
|
146
|
+
# details omitted for brevity
|
147
|
+
```
|
148
|
+
|
149
|
+
##### Transaction level
|
150
|
+
|
151
|
+
In the example below, only the single transaction will be constrained by the end date:
|
152
|
+
|
153
|
+
```yaml
|
154
|
+
# forecast.yml
|
155
|
+
monthly:
|
156
|
+
- account: "[Assets:Bank]"
|
157
|
+
start: "2023-03-01"
|
158
|
+
transactions:
|
159
|
+
- amount: 2000
|
160
|
+
category: "[Expenses:Mortgage]"
|
161
|
+
description: Mortgage
|
162
|
+
end: "2025-01-01"
|
103
163
|
```
|
104
164
|
|
105
165
|
#### Additional settings
|
106
166
|
|
107
|
-
Additional settings in the config file:
|
167
|
+
Additional settings in the config file to consider:
|
108
168
|
|
109
169
|
```yaml
|
110
170
|
# forecast.yml
|
111
171
|
settings:
|
172
|
+
currency: GBP # Specify the currency to use
|
112
173
|
show_symbol: true # Show the currency symbol?
|
113
174
|
sign_before_symbol: true # Show the negative sign before the symbol?
|
114
175
|
thousands_separator: true # Separate thousands with a comma?
|
@@ -118,9 +179,9 @@ settings:
|
|
118
179
|
|
119
180
|
As your config file grows, it can be helpful to sum up the total amounts and output them in the CLI. This can be achieved by:
|
120
181
|
|
121
|
-
hledger-forecast -f
|
182
|
+
hledger-forecast -f my_forecast.yml --summarize
|
122
183
|
|
123
|
-
where `
|
184
|
+
where `my_forecast.yml` is the config file to sum up.
|
124
185
|
|
125
186
|
## :brain: Rationale
|
126
187
|
|
@@ -128,8 +189,8 @@ Firstly, I've come to realise from reading countless blog and Reddit posts on [p
|
|
128
189
|
|
129
190
|
My days working in financial modelling have meant that a big macro-enabled spreadsheet was my go-to tool. Growing tired with the manual approach of importing transactions, heavily manipulating them, watching Excel become increasingly slower lead me to PTA. It's been a wonderful discovery.
|
130
191
|
|
131
|
-
One of the aspects of my previous approach to personal finance that I liked was the monthly recap of my performance and the looking ahead to the future. Am I still on track to hit my year-end savings goal? Am I still on track to hit my savings goal
|
192
|
+
One of the aspects of my previous approach to personal finance that I liked was the monthly recap of my performance and the looking ahead to the future. Am I still on track to hit my year-end savings goal given my future commitments? Am I still on track to hit my savings goal in 12 and 24 months time? It was at this point in my shift to PTA that I found it difficult to answer those questions with Hledger.
|
132
193
|
|
133
|
-
While
|
194
|
+
While there is support for [forecasting](https://hledger.org/1.29/hledger.html#forecasting) using periodic transactions in Hledger, these are computed virtually at runtime. If I notice a big difference in my forecasted year-end balance compared to what I'm expecting, I want to investigate and start reconcilling. Computed transactions make this nigh on impossible to unpick. Also, I get a lot of value out of running different forecast scenarios and seeing the impact. For example, _"what's my savings balance looking like in 3 years time if I get the kitchen remodelled?"_.
|
134
195
|
|
135
|
-
With this gem, my aim was to make it easy for users to change
|
196
|
+
With this gem, my aim was to make it easy for users to change their config file, regenerate the forecast and open a journal file and see the transactions. Or, use multiple forecast files for different scenarios and pass them in turn to Hledger to observe the impact.
|
data/example.yml
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
monthly:
|
2
2
|
- account: "[Assets:Bank]"
|
3
|
-
|
3
|
+
start: "2023-03-01"
|
4
4
|
transactions:
|
5
5
|
- amount: 1000.00
|
6
6
|
category: "[Expenses:Mortgage]"
|
7
7
|
description: Mortgage
|
8
|
+
end: "2024-01-01"
|
8
9
|
- amount: 500.00
|
9
10
|
category: "[Expenses:Food]"
|
10
11
|
description: Food
|
11
12
|
|
12
13
|
quarterly:
|
13
14
|
- account: "[Assets:Bank]"
|
14
|
-
|
15
|
+
start: "2023-04-01"
|
15
16
|
transactions:
|
16
17
|
- amount: -1000.00
|
17
18
|
category: "[Income:Bonus]"
|
@@ -19,7 +20,7 @@ quarterly:
|
|
19
20
|
|
20
21
|
half-yearly:
|
21
22
|
- account: "[Assets:Bank]"
|
22
|
-
|
23
|
+
start: "2023-04-01"
|
23
24
|
transactions:
|
24
25
|
- amount: 500
|
25
26
|
category: "[Expenses:Holiday]"
|
@@ -27,7 +28,7 @@ half-yearly:
|
|
27
28
|
|
28
29
|
yearly:
|
29
30
|
- account: "[Assets:Bank]"
|
30
|
-
|
31
|
+
start: "2023-04-01"
|
31
32
|
transactions:
|
32
33
|
- amount: -2000.00
|
33
34
|
category: "[Income:Bonus]"
|
@@ -35,12 +36,24 @@ yearly:
|
|
35
36
|
|
36
37
|
once:
|
37
38
|
- account: "[Assets:Bank]"
|
38
|
-
|
39
|
+
start: "2024-01-01"
|
39
40
|
transactions:
|
40
41
|
- amount: 5000.00
|
41
42
|
category: "[Expenses:Car]"
|
42
43
|
description: Forecast new car cost
|
43
44
|
|
45
|
+
custom:
|
46
|
+
- description: Fuel every 5 days
|
47
|
+
recurrence:
|
48
|
+
period: days
|
49
|
+
quantity: 5
|
50
|
+
account: "[Assets:Bank]"
|
51
|
+
start: "2023-03-01"
|
52
|
+
transactions:
|
53
|
+
- amount: 150
|
54
|
+
category: "[Expenses:Car:Fuel]"
|
55
|
+
description: Car fuel
|
56
|
+
|
44
57
|
settings:
|
45
58
|
currency: GBP
|
46
59
|
show_symbol: true
|
data/hledger-forecast.gemspec
CHANGED
@@ -15,6 +15,7 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.add_dependency "highline", "~> 2.1.0"
|
16
16
|
s.add_dependency "money", "~> 6.16.0"
|
17
17
|
s.add_dependency "colorize", "~> 0.8.1"
|
18
|
+
s.add_dependency "terminal-table", "~> 3.0.2"
|
18
19
|
s.add_development_dependency 'rspec', '~> 3.12'
|
19
20
|
|
20
21
|
s.files = `git ls-files`.split("\n")
|
@@ -29,12 +29,43 @@ module HledgerForecast
|
|
29
29
|
Money.from_cents(formatted_transaction['amount'].to_i * 100, @settings[:currency]).format(
|
30
30
|
symbol: @settings[:show_symbol],
|
31
31
|
sign_before_symbol: @settings[:sign_before_symbol],
|
32
|
-
thousands_separator: @settings[:thousands_separator] ? ',' : nil
|
32
|
+
thousands_separator: @settings[:thousands_separator] ? ',' : nil
|
33
33
|
)
|
34
34
|
|
35
35
|
formatted_transaction
|
36
36
|
end
|
37
37
|
|
38
|
+
def self.process_custom(output, forecast_data, date)
|
39
|
+
forecast_data['custom']&.each do |forecast|
|
40
|
+
start_date = Date.parse(forecast['start'])
|
41
|
+
end_date = forecast['end'] ? Date.parse(forecast['end']) : nil
|
42
|
+
account = forecast['account']
|
43
|
+
period = forecast['recurrence']['period']
|
44
|
+
quantity = forecast['recurrence']['quantity']
|
45
|
+
|
46
|
+
next if end_date && date > end_date
|
47
|
+
|
48
|
+
date_matches = case period
|
49
|
+
when 'days'
|
50
|
+
(date - start_date).to_i % quantity == 0
|
51
|
+
when 'weeks'
|
52
|
+
(date - start_date).to_i % (quantity * 7) == 0
|
53
|
+
when 'months'
|
54
|
+
((date.year * 12 + date.month) - (start_date.year * 12 + start_date.month)) % quantity == 0 && date.day == start_date.day
|
55
|
+
end
|
56
|
+
|
57
|
+
if date_matches
|
58
|
+
forecast['transactions'].each do |transaction|
|
59
|
+
end_date = transaction['end'] ? Date.parse(transaction['end']) : nil
|
60
|
+
|
61
|
+
next unless end_date.nil? || date <= end_date
|
62
|
+
|
63
|
+
write_transactions(output, date, account, format_transaction(transaction))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
38
69
|
def self.process_forecast(output_file, forecast_data, type, date)
|
39
70
|
forecast_data[type]&.each do |forecast|
|
40
71
|
start_date = Date.parse(forecast['start'])
|
@@ -86,6 +117,7 @@ module HledgerForecast
|
|
86
117
|
process_forecast(output, forecast_data, 'half-yearly', date)
|
87
118
|
process_forecast(output, forecast_data, 'yearly', date)
|
88
119
|
process_forecast(output, forecast_data, 'once', date)
|
120
|
+
process_custom(output, forecast_data, date)
|
89
121
|
|
90
122
|
date = date.next_day
|
91
123
|
end
|
@@ -8,6 +8,7 @@ module HledgerForecast
|
|
8
8
|
category_totals[transaction['category']] += transaction['amount']
|
9
9
|
end
|
10
10
|
end
|
11
|
+
|
11
12
|
category_totals
|
12
13
|
end
|
13
14
|
|
@@ -17,13 +18,13 @@ module HledgerForecast
|
|
17
18
|
|
18
19
|
category_totals.each do |category, amount|
|
19
20
|
formatted_amount = generator.format_transaction({ 'amount' => amount })['amount']
|
20
|
-
formatted_amount = amount.to_i < 0 ? formatted_amount.
|
21
|
+
formatted_amount = amount.to_i < 0 ? formatted_amount.green : formatted_amount.red
|
21
22
|
puts " #{category.ljust(40)}#{formatted_amount}"
|
22
23
|
period_total += amount
|
23
24
|
end
|
24
25
|
|
25
26
|
formatted_period_total = generator.format_transaction({ 'amount' => period_total })['amount']
|
26
|
-
formatted_period_total = period_total.to_i < 0 ? formatted_period_total.
|
27
|
+
formatted_period_total = period_total.to_i < 0 ? formatted_period_total.green : formatted_period_total.red
|
27
28
|
puts " TOTAL".ljust(42) + formatted_period_total
|
28
29
|
end
|
29
30
|
|
@@ -32,7 +33,7 @@ module HledgerForecast
|
|
32
33
|
total = {}
|
33
34
|
grand_total = 0
|
34
35
|
|
35
|
-
periods.each do |period|
|
36
|
+
(periods + ['custom']).each do |period|
|
36
37
|
total[period] = sum_transactions(forecast_data, period)
|
37
38
|
grand_total += total[period]
|
38
39
|
end
|
@@ -41,11 +42,33 @@ module HledgerForecast
|
|
41
42
|
total
|
42
43
|
end
|
43
44
|
|
45
|
+
def self.sum_custom_transactions(forecast_data)
|
46
|
+
category_totals = Hash.new(0)
|
47
|
+
custom_periods = []
|
48
|
+
|
49
|
+
forecast_data['custom']&.each do |entry|
|
50
|
+
period_data = {}
|
51
|
+
period_data[:quantity] = entry['recurrence']['quantity']
|
52
|
+
period_data[:period] = entry['recurrence']['period']
|
53
|
+
period_data[:description] = entry['transactions'].first['description']
|
54
|
+
period_data[:category] = entry['transactions'].first['category']
|
55
|
+
period_data[:amount] = entry['transactions'].first['amount']
|
56
|
+
|
57
|
+
entry['transactions'].each do |transaction|
|
58
|
+
category_totals[transaction['category']] += transaction['amount']
|
59
|
+
end
|
60
|
+
|
61
|
+
custom_periods << period_data
|
62
|
+
end
|
63
|
+
|
64
|
+
{ totals: category_totals, periods: custom_periods }
|
65
|
+
end
|
66
|
+
|
44
67
|
def self.generate(forecast)
|
45
68
|
forecast_data = YAML.safe_load(forecast)
|
46
69
|
|
47
70
|
category_totals_by_period = {}
|
48
|
-
%w[monthly quarterly half-yearly yearly once].each do |period|
|
71
|
+
%w[monthly quarterly half-yearly yearly once custom].each do |period|
|
49
72
|
category_totals_by_period[period] = sum_transactions(forecast_data, period)
|
50
73
|
end
|
51
74
|
|
@@ -54,15 +77,56 @@ module HledgerForecast
|
|
54
77
|
generator = HledgerForecast::Generator
|
55
78
|
generator.configure_settings(forecast_data)
|
56
79
|
|
57
|
-
|
80
|
+
table = Terminal::Table.new
|
81
|
+
|
82
|
+
table.add_row([{ value: 'FORECAST SUMMARY', colspan: 3, alignment: :center }])
|
83
|
+
table.add_separator
|
84
|
+
|
85
|
+
first_period = true
|
58
86
|
category_totals_by_period.each do |period, category_totals|
|
59
|
-
|
60
|
-
|
87
|
+
non_zero_totals = category_totals.select { |_, amount| amount != 0 }
|
88
|
+
next if non_zero_totals.empty?
|
89
|
+
|
90
|
+
table.add_separator unless first_period
|
91
|
+
table.add_row([{ value: period.capitalize, colspan: 3, alignment: :center }])
|
92
|
+
|
93
|
+
period_total = 0
|
94
|
+
|
95
|
+
if period == 'custom'
|
96
|
+
custom_periods_data = sum_custom_transactions(forecast_data)
|
97
|
+
custom_periods_data[:periods].each do |custom_period|
|
98
|
+
formatted_amount = generator.format_transaction({ 'amount' => custom_period[:amount] })['amount']
|
99
|
+
formatted_amount = custom_period[:amount].to_i < 0 ? formatted_amount.green : formatted_amount.red
|
100
|
+
table.add_row [{ value: custom_period[:category], alignment: :left },
|
101
|
+
{ value: "every #{custom_period[:quantity]} #{custom_period[:period]}", alignment: :right }, { value: formatted_amount, alignment: :right }]
|
102
|
+
period_total += custom_period[:amount]
|
103
|
+
end
|
104
|
+
else
|
105
|
+
non_zero_totals.each do |category, amount|
|
106
|
+
formatted_amount = generator.format_transaction({ 'amount' => amount })['amount']
|
107
|
+
formatted_amount = amount.to_i < 0 ? formatted_amount.green : formatted_amount.red
|
108
|
+
|
109
|
+
table.add_row [{ value: category, colspan: 2, alignment: :left },
|
110
|
+
{ value: formatted_amount, alignment: :right }]
|
111
|
+
period_total += amount
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
formatted_period_total = generator.format_transaction({ 'amount' => period_total })['amount']
|
116
|
+
formatted_period_total = period_total.to_i < 0 ? formatted_period_total.green : formatted_period_total.red
|
117
|
+
table.add_row [{ value: "#{period.capitalize} TOTAL", colspan: 2, alignment: :left },
|
118
|
+
{ value: formatted_period_total, alignment: :right }]
|
119
|
+
|
120
|
+
first_period = false
|
61
121
|
end
|
62
122
|
|
123
|
+
table.add_separator
|
63
124
|
formatted_grand_total = generator.format_transaction({ 'amount' => grand_total })['amount']
|
64
|
-
formatted_grand_total = grand_total.to_i < 0 ? formatted_grand_total.
|
65
|
-
|
125
|
+
formatted_grand_total = grand_total.to_i < 0 ? formatted_grand_total.green : formatted_grand_total.red
|
126
|
+
table.add_row [{ value: 'TOTAL', colspan: 2, alignment: :left },
|
127
|
+
{ value: formatted_grand_total, alignment: :right }]
|
128
|
+
|
129
|
+
puts table
|
66
130
|
end
|
67
131
|
end
|
68
132
|
end
|
data/lib/hledger_forecast.rb
CHANGED
data/spec/custom_spec.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative '../lib/hledger_forecast'
|
2
|
+
|
3
|
+
RSpec.describe 'generate' do
|
4
|
+
it 'generates a forecast with correct CUSTOM DAILY transactions' do
|
5
|
+
transactions = File.read('spec/stubs/transactions.journal')
|
6
|
+
forecast = File.read('spec/stubs/custom/forecast_custom_days.yml')
|
7
|
+
|
8
|
+
generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01',
|
9
|
+
'2023-03-10')
|
10
|
+
|
11
|
+
expected_output = File.read('spec/stubs/custom/output_custom_days.journal')
|
12
|
+
expect(generated_journal).to eq(expected_output)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'generates a forecast with correct CUSTOM WEEKlY transactions' do
|
16
|
+
transactions = File.read('spec/stubs/transactions.journal')
|
17
|
+
forecast = File.read('spec/stubs/custom/forecast_custom_weeks.yml')
|
18
|
+
|
19
|
+
generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01',
|
20
|
+
'2023-04-30')
|
21
|
+
|
22
|
+
expected_output = File.read('spec/stubs/custom/output_custom_weeks.journal')
|
23
|
+
expect(generated_journal).to eq(expected_output)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'generates a forecast with MULTIPLE correct CUSTOM WEEKlY transactions' do
|
27
|
+
transactions = File.read('spec/stubs/transactions.journal')
|
28
|
+
forecast = File.read('spec/stubs/custom/forecast_custom_weeks_twice.yml')
|
29
|
+
|
30
|
+
generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01',
|
31
|
+
'2023-03-30')
|
32
|
+
|
33
|
+
expected_output = File.read('spec/stubs/custom/output_custom_weeks_twice.journal')
|
34
|
+
expect(generated_journal).to eq(expected_output)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'generates a forecast with correct CUSTOM MONTHLY transactions' do
|
38
|
+
transactions = File.read('spec/stubs/transactions.journal')
|
39
|
+
forecast = File.read('spec/stubs/custom/forecast_custom_months.yml')
|
40
|
+
|
41
|
+
generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01',
|
42
|
+
'2024-02-28')
|
43
|
+
|
44
|
+
expected_output = File.read('spec/stubs/custom/output_custom_months.journal')
|
45
|
+
expect(generated_journal).to eq(expected_output)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
custom:
|
2
|
+
- description: Repeat every 3 days
|
3
|
+
recurrence:
|
4
|
+
period: days
|
5
|
+
quantity: 3
|
6
|
+
account: "[Assets:Bank]"
|
7
|
+
start: "2023-03-01"
|
8
|
+
transactions:
|
9
|
+
- amount: 80
|
10
|
+
category: "[Expenses:Personal Care]"
|
11
|
+
description: Hair and beauty
|
12
|
+
|
13
|
+
settings:
|
14
|
+
currency: GBP
|
@@ -0,0 +1,14 @@
|
|
1
|
+
custom:
|
2
|
+
- description: Repeat every 5 months
|
3
|
+
recurrence:
|
4
|
+
period: months
|
5
|
+
quantity: 5
|
6
|
+
account: "[Assets:Bank]"
|
7
|
+
start: "2023-03-01"
|
8
|
+
transactions:
|
9
|
+
- amount: 100
|
10
|
+
category: "[Expenses:Personal Care]"
|
11
|
+
description: Hair and beauty
|
12
|
+
|
13
|
+
settings:
|
14
|
+
currency: GBP
|
@@ -0,0 +1,14 @@
|
|
1
|
+
custom:
|
2
|
+
- description: Repeat every 2 weeks
|
3
|
+
recurrence:
|
4
|
+
period: weeks
|
5
|
+
quantity: 2
|
6
|
+
account: "[Assets:Bank]"
|
7
|
+
start: "2023-03-01"
|
8
|
+
transactions:
|
9
|
+
- amount: 75
|
10
|
+
category: "[Expenses:Personal Care]"
|
11
|
+
description: Hair and beauty
|
12
|
+
|
13
|
+
settings:
|
14
|
+
currency: GBP
|
@@ -0,0 +1,24 @@
|
|
1
|
+
custom:
|
2
|
+
- description: Beauty expenses every 2 weeks
|
3
|
+
recurrence:
|
4
|
+
period: weeks
|
5
|
+
quantity: 2
|
6
|
+
account: "[Assets:Bank]"
|
7
|
+
start: "2023-03-01"
|
8
|
+
transactions:
|
9
|
+
- amount: 75
|
10
|
+
category: "[Expenses:Personal Care]"
|
11
|
+
description: Hair and beauty
|
12
|
+
- description: Car fuel every 5 days
|
13
|
+
recurrence:
|
14
|
+
period: days
|
15
|
+
quantity: 5
|
16
|
+
account: "[Assets:Bank]"
|
17
|
+
start: "2023-03-01"
|
18
|
+
transactions:
|
19
|
+
- amount: 150
|
20
|
+
category: "[Expenses:Car:Fuel]"
|
21
|
+
description: Car Fuel
|
22
|
+
|
23
|
+
settings:
|
24
|
+
currency: GBP
|
@@ -0,0 +1,24 @@
|
|
1
|
+
2023-02-01 * Opening balance
|
2
|
+
Assets:Bank £1,000.00
|
3
|
+
Equity:Opening balance
|
4
|
+
|
5
|
+
2023-02-05 * Mortgage payment
|
6
|
+
Expenses:Mortgage £1,500.00
|
7
|
+
Assets:Bank
|
8
|
+
|
9
|
+
2023-03-01 * Hair and beauty
|
10
|
+
[Expenses:Personal Care] £80.00
|
11
|
+
[Assets:Bank]
|
12
|
+
|
13
|
+
2023-03-04 * Hair and beauty
|
14
|
+
[Expenses:Personal Care] £80.00
|
15
|
+
[Assets:Bank]
|
16
|
+
|
17
|
+
2023-03-07 * Hair and beauty
|
18
|
+
[Expenses:Personal Care] £80.00
|
19
|
+
[Assets:Bank]
|
20
|
+
|
21
|
+
2023-03-10 * Hair and beauty
|
22
|
+
[Expenses:Personal Care] £80.00
|
23
|
+
[Assets:Bank]
|
24
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
2023-02-01 * Opening balance
|
2
|
+
Assets:Bank £1,000.00
|
3
|
+
Equity:Opening balance
|
4
|
+
|
5
|
+
2023-02-05 * Mortgage payment
|
6
|
+
Expenses:Mortgage £1,500.00
|
7
|
+
Assets:Bank
|
8
|
+
|
9
|
+
2023-03-01 * Hair and beauty
|
10
|
+
[Expenses:Personal Care] £100.00
|
11
|
+
[Assets:Bank]
|
12
|
+
|
13
|
+
2023-08-01 * Hair and beauty
|
14
|
+
[Expenses:Personal Care] £100.00
|
15
|
+
[Assets:Bank]
|
16
|
+
|
17
|
+
2024-01-01 * Hair and beauty
|
18
|
+
[Expenses:Personal Care] £100.00
|
19
|
+
[Assets:Bank]
|
20
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
2023-02-01 * Opening balance
|
2
|
+
Assets:Bank £1,000.00
|
3
|
+
Equity:Opening balance
|
4
|
+
|
5
|
+
2023-02-05 * Mortgage payment
|
6
|
+
Expenses:Mortgage £1,500.00
|
7
|
+
Assets:Bank
|
8
|
+
|
9
|
+
2023-03-01 * Hair and beauty
|
10
|
+
[Expenses:Personal Care] £75.00
|
11
|
+
[Assets:Bank]
|
12
|
+
|
13
|
+
2023-03-15 * Hair and beauty
|
14
|
+
[Expenses:Personal Care] £75.00
|
15
|
+
[Assets:Bank]
|
16
|
+
|
17
|
+
2023-03-29 * Hair and beauty
|
18
|
+
[Expenses:Personal Care] £75.00
|
19
|
+
[Assets:Bank]
|
20
|
+
|
21
|
+
2023-04-12 * Hair and beauty
|
22
|
+
[Expenses:Personal Care] £75.00
|
23
|
+
[Assets:Bank]
|
24
|
+
|
25
|
+
2023-04-26 * Hair and beauty
|
26
|
+
[Expenses:Personal Care] £75.00
|
27
|
+
[Assets:Bank]
|
28
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
2023-02-01 * Opening balance
|
2
|
+
Assets:Bank £1,000.00
|
3
|
+
Equity:Opening balance
|
4
|
+
|
5
|
+
2023-02-05 * Mortgage payment
|
6
|
+
Expenses:Mortgage £1,500.00
|
7
|
+
Assets:Bank
|
8
|
+
|
9
|
+
2023-03-01 * Hair and beauty
|
10
|
+
[Expenses:Personal Care] £75.00
|
11
|
+
[Assets:Bank]
|
12
|
+
|
13
|
+
2023-03-01 * Car Fuel
|
14
|
+
[Expenses:Car:Fuel] £150.00
|
15
|
+
[Assets:Bank]
|
16
|
+
|
17
|
+
2023-03-06 * Car Fuel
|
18
|
+
[Expenses:Car:Fuel] £150.00
|
19
|
+
[Assets:Bank]
|
20
|
+
|
21
|
+
2023-03-11 * Car Fuel
|
22
|
+
[Expenses:Car:Fuel] £150.00
|
23
|
+
[Assets:Bank]
|
24
|
+
|
25
|
+
2023-03-15 * Hair and beauty
|
26
|
+
[Expenses:Personal Care] £75.00
|
27
|
+
[Assets:Bank]
|
28
|
+
|
29
|
+
2023-03-16 * Car Fuel
|
30
|
+
[Expenses:Car:Fuel] £150.00
|
31
|
+
[Assets:Bank]
|
32
|
+
|
33
|
+
2023-03-21 * Car Fuel
|
34
|
+
[Expenses:Car:Fuel] £150.00
|
35
|
+
[Assets:Bank]
|
36
|
+
|
37
|
+
2023-03-26 * Car Fuel
|
38
|
+
[Expenses:Car:Fuel] £150.00
|
39
|
+
[Assets:Bank]
|
40
|
+
|
41
|
+
2023-03-29 * Hair and beauty
|
42
|
+
[Expenses:Personal Care] £75.00
|
43
|
+
[Assets:Bank]
|
44
|
+
|
@@ -14,6 +14,10 @@
|
|
14
14
|
[Expenses:Food] £100.00
|
15
15
|
[Assets:Bank]
|
16
16
|
|
17
|
+
2023-03-01 * Savings
|
18
|
+
[Assets:Bank] -£1,000.00
|
19
|
+
[Assets:Savings]
|
20
|
+
|
17
21
|
2023-04-01 * Mortgage
|
18
22
|
[Expenses:Mortgage] £2,000.00
|
19
23
|
[Assets:Bank]
|
@@ -22,6 +26,10 @@
|
|
22
26
|
[Expenses:Food] £100.00
|
23
27
|
[Assets:Bank]
|
24
28
|
|
29
|
+
2023-04-01 * Savings
|
30
|
+
[Assets:Bank] -£1,000.00
|
31
|
+
[Assets:Savings]
|
32
|
+
|
25
33
|
2023-05-01 * Mortgage
|
26
34
|
[Expenses:Mortgage] £2,000.00
|
27
35
|
[Assets:Bank]
|
@@ -30,3 +38,7 @@
|
|
30
38
|
[Expenses:Food] £100.00
|
31
39
|
[Assets:Bank]
|
32
40
|
|
41
|
+
2023-05-01 * Savings
|
42
|
+
[Assets:Bank] -£1,000.00
|
43
|
+
[Assets:Savings]
|
44
|
+
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hledger-forecast
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Oli Morris
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-04-
|
11
|
+
date: 2023-04-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: highline
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 0.8.1
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: terminal-table
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.0.2
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 3.0.2
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: rspec
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -90,10 +104,19 @@ files:
|
|
90
104
|
- lib/hledger_forecast/summarize.rb
|
91
105
|
- lib/hledger_forecast/version.rb
|
92
106
|
- spec/command_spec.rb
|
107
|
+
- spec/custom_spec.rb
|
93
108
|
- spec/half-yearly_spec.rb
|
94
109
|
- spec/monthly_spec.rb
|
95
110
|
- spec/once_spec.rb
|
96
111
|
- spec/quarterly_spec.rb
|
112
|
+
- spec/stubs/custom/forecast_custom_days.yml
|
113
|
+
- spec/stubs/custom/forecast_custom_months.yml
|
114
|
+
- spec/stubs/custom/forecast_custom_weeks.yml
|
115
|
+
- spec/stubs/custom/forecast_custom_weeks_twice.yml
|
116
|
+
- spec/stubs/custom/output_custom_days.journal
|
117
|
+
- spec/stubs/custom/output_custom_months.journal
|
118
|
+
- spec/stubs/custom/output_custom_weeks.journal
|
119
|
+
- spec/stubs/custom/output_custom_weeks_twice.journal
|
97
120
|
- spec/stubs/half-yearly/forecast_half-yearly.yml
|
98
121
|
- spec/stubs/half-yearly/output_half-yearly.journal
|
99
122
|
- spec/stubs/monthly/forecast_monthly.yml
|
@@ -131,16 +154,25 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
154
|
- !ruby/object:Gem::Version
|
132
155
|
version: '0'
|
133
156
|
requirements: []
|
134
|
-
rubygems_version: 3.4.
|
157
|
+
rubygems_version: 3.4.12
|
135
158
|
signing_key:
|
136
159
|
specification_version: 4
|
137
160
|
summary: Utility to generate forecasts in Hledger
|
138
161
|
test_files:
|
139
162
|
- spec/command_spec.rb
|
163
|
+
- spec/custom_spec.rb
|
140
164
|
- spec/half-yearly_spec.rb
|
141
165
|
- spec/monthly_spec.rb
|
142
166
|
- spec/once_spec.rb
|
143
167
|
- spec/quarterly_spec.rb
|
168
|
+
- spec/stubs/custom/forecast_custom_days.yml
|
169
|
+
- spec/stubs/custom/forecast_custom_months.yml
|
170
|
+
- spec/stubs/custom/forecast_custom_weeks.yml
|
171
|
+
- spec/stubs/custom/forecast_custom_weeks_twice.yml
|
172
|
+
- spec/stubs/custom/output_custom_days.journal
|
173
|
+
- spec/stubs/custom/output_custom_months.journal
|
174
|
+
- spec/stubs/custom/output_custom_weeks.journal
|
175
|
+
- spec/stubs/custom/output_custom_weeks_twice.journal
|
144
176
|
- spec/stubs/half-yearly/forecast_half-yearly.yml
|
145
177
|
- spec/stubs/half-yearly/output_half-yearly.journal
|
146
178
|
- spec/stubs/monthly/forecast_monthly.yml
|