hledger-forecast 0.1.8 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +25 -26
- data/bin/hledger-forecast +2 -2
- data/example.yml +19 -3
- data/lib/hledger_forecast/cli.rb +198 -10
- data/lib/hledger_forecast/generator.rb +7 -5
- data/lib/hledger_forecast/summarize.rb +86 -83
- data/lib/hledger_forecast/version.rb +1 -1
- data/lib/hledger_forecast.rb +1 -1
- data/spec/command_spec.rb +6 -3
- data/spec/custom_spec.rb +4 -4
- data/spec/half-yearly_spec.rb +1 -1
- data/spec/monthly_spec.rb +3 -3
- data/spec/once_spec.rb +1 -1
- data/spec/quarterly_spec.rb +1 -1
- data/spec/start_date_spec.rb +12 -0
- data/spec/stubs/modifiers/forecast_modifiers.yml +13 -0
- data/spec/stubs/modifiers/output_modifiers.journal +44 -0
- data/spec/stubs/monthly/forecast_monthly.yml +1 -1
- data/spec/stubs/monthly/output_monthly.journal +3 -3
- data/spec/stubs/start_date/forecast_startdate.yml +26 -0
- data/spec/stubs/start_date/output_startdate.journal +56 -0
- data/spec/yearly_spec.rb +1 -1
- metadata +12 -3
- data/lib/hledger_forecast/options.rb +0 -77
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a21d32c5ee145d0bca5cb25f07a2fbb6f65126f16c5e15faf2ca11b3fbfac03c
|
4
|
+
data.tar.gz: bbf889db9f129ca62550399b2f2e7818936eee667d356bbeb52b476f305fe6b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0fa4ad6d50c30eac65acc292d96f500182e211b1a4b9e039dfcff0f204a32ad01d9c01a7570535301bffeb6be865c720d6b8be8ba3e17370a2b992586f351d5
|
7
|
+
data.tar.gz: 554a06876170c161d35b68eececa7904b190aaa0fb6611e2a8bd672fad9b02b48a5a5e26e013db857671fd6f50e33242cb19ddf907fafb31b1b1bd27b74f86df
|
data/README.md
CHANGED
@@ -8,7 +8,7 @@ See the [rationale](#brain-rationale) section for why this gem may be useful to
|
|
8
8
|
|
9
9
|
## :sparkles: Features
|
10
10
|
|
11
|
-
- :book: Uses
|
11
|
+
- :book: Uses simple YAML files to generate forecasts from periodic transactions
|
12
12
|
- :date: Generate forecasts between specified start and end dates
|
13
13
|
- :heavy_dollar_sign: Full currency support (uses the [RubyMoney](https://github.com/RubyMoney/money) gem)
|
14
14
|
- :computer: Simple and easy to use CLI
|
@@ -24,27 +24,25 @@ Assuming you have Ruby and [Rubygems](http://rubygems.org/pages/download) instal
|
|
24
24
|
|
25
25
|
Run:
|
26
26
|
|
27
|
-
hledger-forecast
|
27
|
+
hledger-forecast generate
|
28
28
|
|
29
29
|
> **Note**: This assumes that a `forecast.yml` exists in the current working directory
|
30
30
|
|
31
31
|
Running `hledger-forecast -h` shows the available options:
|
32
32
|
|
33
|
-
Usage: Hledger-Forecast [options]
|
33
|
+
Usage: Hledger-Forecast generate [options]
|
34
34
|
|
35
|
-
-f, --forecast FILE The FORECAST yaml file to generate from
|
36
35
|
-t, --transaction FILE The base TRANSACTIONS file to extend from
|
36
|
+
-f, --forecast FILE The FORECAST yaml file to generate from
|
37
37
|
-o, --output-file FILE The OUTPUT file to create
|
38
38
|
-s, --start-date DATE The date to start generating from (yyyy-mm-dd)
|
39
39
|
-e, --end-date DATE The date to start generating to (yyyy-mm-dd)
|
40
40
|
--force Force an overwrite of the output file
|
41
|
-
|
42
|
-
-h, --help Show this message
|
43
|
-
--version Show version
|
41
|
+
-h, --help Show this help message
|
44
42
|
|
45
43
|
Another example of a common command:
|
46
44
|
|
47
|
-
hledger-forecast -f my_forecast.yml -s 2023-05-01 -e 2024-12-31
|
45
|
+
hledger-forecast generate -f my_forecast.yml -s 2023-05-01 -e 2024-12-31
|
48
46
|
|
49
47
|
This will generate an output file (`my_forecast.journal`) from the forecast file between the two date ranges.
|
50
48
|
|
@@ -59,7 +57,9 @@ where:
|
|
59
57
|
- `transactions.journal` might be your bank transactions (your "_actuals_")
|
60
58
|
- `my_forecast.journal` is the generated forecast file
|
61
59
|
|
62
|
-
|
60
|
+
## :gear: Configuration
|
61
|
+
|
62
|
+
### The YAML file
|
63
63
|
|
64
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
|
65
65
|
|
@@ -89,9 +89,7 @@ Let's examine what's going on in this config file:
|
|
89
89
|
- Notice we're also using [virtual postings](https://hledger.org/1.29/hledger.html#virtual-postings) (designated by the brackets). This makes it easy to filter them out with the `-R` or `--real` option in Hledger
|
90
90
|
- We also have not specified a currency; the default (`USD`) will be used
|
91
91
|
|
92
|
-
###
|
93
|
-
|
94
|
-
#### Periods
|
92
|
+
### Periods
|
95
93
|
|
96
94
|
Besides monthly recurring transactions, the app also supports the following periods:
|
97
95
|
|
@@ -101,12 +99,11 @@ Besides monthly recurring transactions, the app also supports the following peri
|
|
101
99
|
- `once` - Generate _one-time_ transactions on a specified date
|
102
100
|
- `custom` - Generate transactions every _n days/weeks/months_
|
103
101
|
|
104
|
-
|
102
|
+
#### Custom period
|
105
103
|
|
106
104
|
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
105
|
|
108
106
|
```yaml
|
109
|
-
# forecast.yml
|
110
107
|
custom:
|
111
108
|
- description: Fortnightly hair and beauty spend
|
112
109
|
recurrence:
|
@@ -126,18 +123,17 @@ Where `quantity` is an integer and `period` is one of:
|
|
126
123
|
- weeks
|
127
124
|
- months
|
128
125
|
|
129
|
-
|
126
|
+
### Dates
|
130
127
|
|
131
128
|
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).
|
132
129
|
|
133
130
|
You can further control the dates at a period/top-level as well as at a transaction level:
|
134
131
|
|
135
|
-
|
132
|
+
#### Top level
|
136
133
|
|
137
134
|
In the example below, all transactions in the `monthly` block will be constrained by the end date:
|
138
135
|
|
139
136
|
```yaml
|
140
|
-
# forecast.yml
|
141
137
|
monthly:
|
142
138
|
- account: "[Assets:Bank]"
|
143
139
|
start: "2023-03-01"
|
@@ -146,12 +142,11 @@ monthly:
|
|
146
142
|
# details omitted for brevity
|
147
143
|
```
|
148
144
|
|
149
|
-
|
145
|
+
#### Transaction level
|
150
146
|
|
151
|
-
In the example below, only the single transaction will be constrained by the end date:
|
147
|
+
In the example below, only the single transaction will be constrained by the end date and controlled via an additional start date:
|
152
148
|
|
153
149
|
```yaml
|
154
|
-
# forecast.yml
|
155
150
|
monthly:
|
156
151
|
- account: "[Assets:Bank]"
|
157
152
|
start: "2023-03-01"
|
@@ -159,15 +154,17 @@ monthly:
|
|
159
154
|
- amount: 2000
|
160
155
|
category: "[Expenses:Mortgage]"
|
161
156
|
description: Mortgage
|
157
|
+
start: "2023-05-01"
|
162
158
|
end: "2025-01-01"
|
163
159
|
```
|
164
160
|
|
165
|
-
|
161
|
+
The addition of the `start` key means that while the block will start on 2023-03-01, the transaction for the mortgage won't start until `2023-05-01`.
|
162
|
+
|
163
|
+
### Additional settings
|
166
164
|
|
167
165
|
Additional settings in the config file to consider:
|
168
166
|
|
169
167
|
```yaml
|
170
|
-
# forecast.yml
|
171
168
|
settings:
|
172
169
|
currency: GBP # Specify the currency to use
|
173
170
|
show_symbol: true # Show the currency symbol?
|
@@ -175,11 +172,13 @@ settings:
|
|
175
172
|
thousands_separator: true # Separate thousands with a comma?
|
176
173
|
```
|
177
174
|
|
178
|
-
|
175
|
+
## :rainbow: Helpers
|
176
|
+
|
177
|
+
### Summarizing the forecast file
|
179
178
|
|
180
179
|
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:
|
181
180
|
|
182
|
-
hledger-forecast -f my_forecast.yml
|
181
|
+
hledger-forecast summarize -f my_forecast.yml
|
183
182
|
|
184
183
|
where `my_forecast.yml` is the config file to sum up.
|
185
184
|
|
@@ -189,8 +188,8 @@ Firstly, I've come to realise from reading countless blog and Reddit posts on [p
|
|
189
188
|
|
190
189
|
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.
|
191
190
|
|
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
|
191
|
+
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 performance to date and my future commitments? And what about my savings goal in 12 and 24 months time? Or, how much are my financial positions impacted if inflation increases by x%? It was at this point in my shift to PTA that I found it difficult to answer those questions quickly.
|
193
192
|
|
194
|
-
While there is support for [forecasting](https://hledger.org/1.29/hledger.html#forecasting) using periodic transactions
|
193
|
+
While there is support in Hledger for [forecasting](https://hledger.org/1.29/hledger.html#forecasting) using periodic transactions, 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?"_.
|
195
194
|
|
196
195
|
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/bin/hledger-forecast
CHANGED
@@ -3,10 +3,10 @@
|
|
3
3
|
require_relative '../lib/hledger_forecast'
|
4
4
|
|
5
5
|
begin
|
6
|
-
options = HledgerForecast::
|
6
|
+
subcommand, options = HledgerForecast::Cli.parse_commands
|
7
7
|
rescue RuntimeError => e
|
8
8
|
puts("ERROR: #{e}")
|
9
9
|
exit(1)
|
10
10
|
end
|
11
11
|
|
12
|
-
HledgerForecast::Cli.run(options)
|
12
|
+
HledgerForecast::Cli.run(subcommand, options)
|
data/example.yml
CHANGED
@@ -2,13 +2,29 @@ monthly:
|
|
2
2
|
- account: "[Assets:Bank]"
|
3
3
|
start: "2023-03-01"
|
4
4
|
transactions:
|
5
|
+
- amount: -100
|
6
|
+
category: "[Income:Bonus]"
|
7
|
+
description: Bonus
|
8
|
+
- amount: -2000
|
9
|
+
category: "[Income:Salary]"
|
10
|
+
description: Salary
|
11
|
+
- amount: 500.00
|
12
|
+
category: "[Expenses:Food]"
|
13
|
+
description: Food
|
14
|
+
- amount: 75
|
15
|
+
category: "[Expenses:Phone]"
|
16
|
+
description: New cell phone
|
17
|
+
start: "2023-08-01"
|
5
18
|
- amount: 1000.00
|
6
19
|
category: "[Expenses:Mortgage]"
|
7
20
|
description: Mortgage
|
8
21
|
end: "2024-01-01"
|
9
|
-
|
10
|
-
|
11
|
-
|
22
|
+
- account: "[Assets:Savings]"
|
23
|
+
start: "2023-03-01"
|
24
|
+
transactions:
|
25
|
+
- amount: -500
|
26
|
+
category: "[Income:Pension]"
|
27
|
+
description: Pension draw down
|
12
28
|
|
13
29
|
quarterly:
|
14
30
|
- account: "[Assets:Bank]"
|
data/lib/hledger_forecast/cli.rb
CHANGED
@@ -1,19 +1,163 @@
|
|
1
1
|
module HledgerForecast
|
2
|
+
# The Command Line Interface for the application
|
3
|
+
# Takes user arguments and translates them into actions
|
2
4
|
class Cli
|
3
|
-
def self.run(args)
|
4
|
-
end_date = args[:end_date]
|
5
|
-
start_date = args[:start_date]
|
6
|
-
forecast = File.read(args[:forecast_file])
|
7
|
-
transactions = args[:transactions_file] ? File.read(args[:transactions_file]) : nil
|
8
5
|
|
9
|
-
|
6
|
+
def self.run(subcommand, options)
|
7
|
+
case subcommand
|
8
|
+
when 'generate'
|
9
|
+
generate(options)
|
10
|
+
when 'summarize'
|
11
|
+
summarize(options)
|
12
|
+
else
|
13
|
+
puts "Unknown command: #{subcommand}"
|
14
|
+
exit(1)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.parse_commands(args = ARGV, _stdin = $stdin)
|
19
|
+
subcommand = nil
|
20
|
+
options = {}
|
21
|
+
|
22
|
+
global = OptionParser.new do |opts|
|
23
|
+
opts.banner = "Usage: hledger-forecast [subcommand] [options]"
|
24
|
+
opts.separator ""
|
25
|
+
opts.separator "Subcommands:"
|
26
|
+
opts.separator " generate Generate the forecast file"
|
27
|
+
opts.separator " summarize Summarize the forecast file and output to the terminal"
|
28
|
+
opts.separator ""
|
29
|
+
opts.separator "Options:"
|
30
|
+
|
31
|
+
opts.on_tail("-h", "--help", "Show this help message") do
|
32
|
+
puts opts
|
33
|
+
exit
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on_tail("-v", "--version", "Show version") do
|
37
|
+
puts VERSION
|
38
|
+
exit
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
begin
|
43
|
+
global.order!(args)
|
44
|
+
subcommand = args.shift || 'generate'
|
45
|
+
rescue OptionParser::InvalidOption => e
|
46
|
+
puts e
|
47
|
+
puts global
|
48
|
+
exit(1)
|
49
|
+
end
|
50
|
+
|
51
|
+
case subcommand
|
52
|
+
when 'generate'
|
53
|
+
options = parse_generate_options(args)
|
54
|
+
when 'summarize'
|
55
|
+
options = parse_summarize_options(args)
|
56
|
+
else
|
57
|
+
puts "Unknown subcommand: #{subcommand}"
|
58
|
+
puts global
|
59
|
+
exit(1)
|
60
|
+
end
|
61
|
+
|
62
|
+
return subcommand, options
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.parse_generate_options(args)
|
66
|
+
options = {}
|
67
|
+
|
68
|
+
OptionParser.new do |opts|
|
69
|
+
opts.banner = "Usage: Hledger-Forecast generate [options]"
|
70
|
+
opts.separator ""
|
71
|
+
|
72
|
+
opts.on("-t", "--transaction FILE",
|
73
|
+
"The base TRANSACTIONS file to extend from") do |file|
|
74
|
+
options[:transactions_file] = file if file && !file.empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
opts.on("-f", "--forecast FILE",
|
78
|
+
"The FORECAST yaml file to generate from") do |file|
|
79
|
+
options[:forecast_file] = file
|
80
|
+
options[:output_file] ||= file.sub(/\.yml$/, '.journal')
|
81
|
+
end
|
82
|
+
|
83
|
+
opts.on("-o", "--output-file FILE",
|
84
|
+
"The OUTPUT file to create") do |file|
|
85
|
+
options[:output_file] = file
|
86
|
+
end
|
87
|
+
|
88
|
+
opts.on("-s", "--start-date DATE",
|
89
|
+
"The date to start generating from (yyyy-mm-dd)") do |a|
|
90
|
+
options[:start_date] = a
|
91
|
+
end
|
92
|
+
|
93
|
+
opts.on("-e", "--end-date DATE",
|
94
|
+
"The date to start generating to (yyyy-mm-dd)") do |a|
|
95
|
+
options[:end_date] = a
|
96
|
+
end
|
97
|
+
|
98
|
+
opts.on("--force",
|
99
|
+
"Force an overwrite of the output file") do |a|
|
100
|
+
options[:force] = a
|
101
|
+
end
|
102
|
+
|
103
|
+
opts.on_tail("-h", "--help", "Show this help message") do
|
104
|
+
puts opts
|
105
|
+
exit
|
106
|
+
end
|
107
|
+
end.parse!(args)
|
108
|
+
|
109
|
+
options[:forecast_file] = "forecast.yml" unless options[:forecast_file]
|
110
|
+
options[:output_file] = "forecast.journal" unless options[:output_file]
|
111
|
+
|
112
|
+
today = Date.today
|
113
|
+
|
114
|
+
unless options[:start_date]
|
115
|
+
options[:default_dates] = true
|
116
|
+
options[:start_date] =
|
117
|
+
Date.new(today.year, today.month, 1).next_month.to_s
|
118
|
+
end
|
119
|
+
unless options[:end_date]
|
120
|
+
options[:default_dates] = true
|
121
|
+
options[:end_date] = Date.new(today.year + 3, 12, 31).to_s
|
122
|
+
end
|
123
|
+
|
124
|
+
options
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.parse_summarize_options(args)
|
128
|
+
options = {}
|
10
129
|
|
11
|
-
|
130
|
+
OptionParser.new do |opts|
|
131
|
+
opts.banner = "Usage: Hledger-Forecast summarize [options]"
|
132
|
+
opts.separator ""
|
133
|
+
|
134
|
+
opts.on("-f", "--forecast FILE",
|
135
|
+
"The FORECAST yaml file to summarize") do |file|
|
136
|
+
options[:forecast_file] = file
|
137
|
+
end
|
12
138
|
|
13
|
-
|
139
|
+
opts.on_tail("-h", "--help", "Show this help message") do
|
140
|
+
puts opts
|
141
|
+
exit
|
142
|
+
end
|
143
|
+
end.parse!(args)
|
144
|
+
|
145
|
+
options
|
146
|
+
end
|
14
147
|
|
15
|
-
|
16
|
-
|
148
|
+
def self.generate(options)
|
149
|
+
end_date = options[:end_date]
|
150
|
+
start_date = options[:start_date]
|
151
|
+
forecast = File.read(options[:forecast_file])
|
152
|
+
transactions = options[:transactions_file] ? File.read(options[:transactions_file]) : nil
|
153
|
+
|
154
|
+
# Generate the forecast
|
155
|
+
puts "[Using default dates: #{start_date} to #{end_date}]" if options[:default_dates]
|
156
|
+
|
157
|
+
transactions = Generator.generate(transactions, forecast, start_date, end_date)
|
158
|
+
|
159
|
+
output_file = options[:output_file]
|
160
|
+
if File.exist?(output_file) && !options[:force]
|
17
161
|
print "File '#{output_file}' already exists. Overwrite? (y/n): "
|
18
162
|
overwrite = gets.chomp.downcase
|
19
163
|
|
@@ -28,5 +172,49 @@ module HledgerForecast
|
|
28
172
|
puts "File '#{output_file}' has been created."
|
29
173
|
end
|
30
174
|
end
|
175
|
+
|
176
|
+
def self.summarize(options)
|
177
|
+
forecast = File.read(options[:forecast_file])
|
178
|
+
puts Summarize.generate(forecast)
|
179
|
+
end
|
180
|
+
|
181
|
+
# def self.run(args)
|
182
|
+
# end_date = args[:end_date]
|
183
|
+
# start_date = args[:start_date]
|
184
|
+
# forecast = File.read(args[:forecast_file])
|
185
|
+
# transactions = args[:transactions_file] ? File.read(args[:transactions_file]) : nil
|
186
|
+
#
|
187
|
+
# # Output the summary
|
188
|
+
# return HledgerForecast::Summarize.generate(forecast) if args[:summarize]
|
189
|
+
#
|
190
|
+
# # Generate the forecast
|
191
|
+
# unless args[:skip]
|
192
|
+
#
|
193
|
+
# puts "[Using default dates: #{start_date} to #{end_date}]" if args[:default_dates]
|
194
|
+
#
|
195
|
+
# transactions = Generator.generate(transactions, forecast, start_date, end_date)
|
196
|
+
#
|
197
|
+
# output_file = args[:output_file]
|
198
|
+
# if File.exist?(output_file) && !args[:force]
|
199
|
+
# print "File '#{output_file}' already exists. Overwrite? (y/n): "
|
200
|
+
# overwrite = gets.chomp.downcase
|
201
|
+
#
|
202
|
+
# if overwrite == 'y'
|
203
|
+
# File.write(output_file, transactions)
|
204
|
+
# puts "File '#{output_file}' has been overwritten."
|
205
|
+
# else
|
206
|
+
# puts "Operation aborted. File '#{output_file}' was not overwritten."
|
207
|
+
# end
|
208
|
+
# else
|
209
|
+
# File.write(output_file, transactions)
|
210
|
+
# puts "File '#{output_file}' has been created."
|
211
|
+
# end
|
212
|
+
# end
|
213
|
+
#
|
214
|
+
# # Check for missing transactions
|
215
|
+
# return unless args[:check] && args[:transactions_file] && args[:forecast_file]
|
216
|
+
#
|
217
|
+
# HledgerForecast::Checker.check(args)
|
218
|
+
# end
|
31
219
|
end
|
32
220
|
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module HledgerForecast
|
2
2
|
# Generates journal entries based on a YAML forecast file.
|
3
3
|
# on forecast data and optional existing transactions.
|
4
|
-
#
|
5
4
|
class Generator
|
6
5
|
class << self
|
7
6
|
attr_accessor :settings
|
@@ -26,7 +25,7 @@ module HledgerForecast
|
|
26
25
|
formatted_transaction = transaction.clone
|
27
26
|
|
28
27
|
formatted_transaction['amount'] =
|
29
|
-
Money.from_cents(formatted_transaction['amount'].
|
28
|
+
Money.from_cents(formatted_transaction['amount'].to_f * 100, @settings[:currency]).format(
|
30
29
|
symbol: @settings[:show_symbol],
|
31
30
|
sign_before_symbol: @settings[:sign_before_symbol],
|
32
31
|
thousands_separator: @settings[:thousands_separator] ? ',' : nil
|
@@ -89,9 +88,12 @@ module HledgerForecast
|
|
89
88
|
|
90
89
|
if date_matches
|
91
90
|
forecast['transactions'].each do |transaction|
|
92
|
-
|
91
|
+
transaction_start_date = transaction['start'] ? Date.parse(transaction['start']) : nil
|
92
|
+
transaction_end_date = transaction['end'] ? Date.parse(transaction['end']) : nil
|
93
93
|
|
94
|
-
|
94
|
+
if (transaction_start_date && date < transaction_start_date) || (transaction_end_date && date > transaction_end_date)
|
95
|
+
next
|
96
|
+
end
|
95
97
|
|
96
98
|
write_transactions(output_file, date, account, format_transaction(transaction))
|
97
99
|
end
|
@@ -99,7 +101,7 @@ module HledgerForecast
|
|
99
101
|
end
|
100
102
|
end
|
101
103
|
|
102
|
-
def self.
|
104
|
+
def self.generate(transactions, forecast, start_date, end_date)
|
103
105
|
start_date = Date.parse(start_date)
|
104
106
|
end_date = Date.parse(end_date)
|
105
107
|
forecast_data = YAML.safe_load(forecast)
|
@@ -1,132 +1,135 @@
|
|
1
1
|
module HledgerForecast
|
2
2
|
# Summarise a forecast YAML file and output it to the CLI
|
3
3
|
class Summarize
|
4
|
-
|
5
|
-
|
6
|
-
forecast_data[period]&.each do |entry|
|
7
|
-
entry['transactions'].each do |transaction|
|
8
|
-
category_totals[transaction['category']] += transaction['amount']
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
category_totals
|
13
|
-
end
|
4
|
+
@table = nil
|
5
|
+
@generator = nil
|
14
6
|
|
15
|
-
def self.
|
16
|
-
|
17
|
-
period_total = 0
|
7
|
+
def self.init_table
|
8
|
+
table = Terminal::Table.new
|
18
9
|
|
19
|
-
|
20
|
-
|
21
|
-
formatted_amount = amount.to_i < 0 ? formatted_amount.green : formatted_amount.red
|
22
|
-
puts " #{category.ljust(40)}#{formatted_amount}"
|
23
|
-
period_total += amount
|
24
|
-
end
|
10
|
+
table.add_row([{ value: 'FORECAST SUMMARY'.bold, colspan: 3, alignment: :center }])
|
11
|
+
table.add_separator
|
25
12
|
|
26
|
-
|
27
|
-
formatted_period_total = period_total.to_i < 0 ? formatted_period_total.green : formatted_period_total.red
|
28
|
-
puts " TOTAL".ljust(42) + formatted_period_total
|
13
|
+
@table = table
|
29
14
|
end
|
30
15
|
|
31
|
-
def self.
|
32
|
-
|
33
|
-
|
34
|
-
grand_total = 0
|
16
|
+
def self.init_generator(forecast_data)
|
17
|
+
generator = HledgerForecast::Generator
|
18
|
+
generator.configure_settings(forecast_data)
|
35
19
|
|
36
|
-
|
37
|
-
|
38
|
-
|
20
|
+
@generator = generator
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.sum_transactions(forecast_data, period)
|
24
|
+
category_total = Hash.new(0)
|
25
|
+
forecast_data[period]&.each do |entry|
|
26
|
+
entry['transactions'].each do |transaction|
|
27
|
+
category_total[transaction['category']] += transaction['amount']
|
28
|
+
end
|
39
29
|
end
|
40
30
|
|
41
|
-
|
42
|
-
total
|
31
|
+
category_total
|
43
32
|
end
|
44
33
|
|
45
34
|
def self.sum_custom_transactions(forecast_data)
|
46
|
-
|
35
|
+
category_total = Hash.new(0)
|
47
36
|
custom_periods = []
|
48
37
|
|
49
38
|
forecast_data['custom']&.each do |entry|
|
50
39
|
period_data = {}
|
51
40
|
period_data[:quantity] = entry['recurrence']['quantity']
|
52
41
|
period_data[:period] = entry['recurrence']['period']
|
53
|
-
period_data[:description] = entry['transactions'].first['description']
|
54
42
|
period_data[:category] = entry['transactions'].first['category']
|
55
43
|
period_data[:amount] = entry['transactions'].first['amount']
|
56
44
|
|
57
45
|
entry['transactions'].each do |transaction|
|
58
|
-
|
46
|
+
category_total[transaction['category']] += transaction['amount']
|
59
47
|
end
|
60
48
|
|
61
49
|
custom_periods << period_data
|
62
50
|
end
|
63
51
|
|
64
|
-
{ totals:
|
52
|
+
{ totals: category_total, periods: custom_periods }
|
65
53
|
end
|
66
54
|
|
67
|
-
def self.
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
%w[monthly quarterly half-yearly yearly once custom].each do |period|
|
72
|
-
category_totals_by_period[period] = sum_transactions(forecast_data, period)
|
73
|
-
end
|
55
|
+
def self.format_amount(amount)
|
56
|
+
formatted_amount = @generator.format_transaction({ 'amount' => amount })['amount']
|
57
|
+
amount.to_f < 0 ? formatted_amount.green : formatted_amount.red
|
58
|
+
end
|
74
59
|
|
75
|
-
|
60
|
+
def self.add_rows_to_table(row_data, period_total, custom: false)
|
61
|
+
if custom
|
62
|
+
row_data[:periods].each do |period|
|
63
|
+
@table.add_row [{ value: period[:category], alignment: :left },
|
64
|
+
{ value: "every #{period[:quantity]} #{period[:period]}", alignment: :right },
|
65
|
+
{ value: format_amount(period[:amount]), alignment: :right }]
|
76
66
|
|
77
|
-
|
78
|
-
|
67
|
+
period_total += period[:amount]
|
68
|
+
end
|
69
|
+
else
|
70
|
+
row_data.each do |category, amount|
|
71
|
+
@table.add_row [{ value: category, colspan: 2, alignment: :left },
|
72
|
+
{ value: format_amount(amount), alignment: :right }]
|
79
73
|
|
80
|
-
|
74
|
+
period_total += amount
|
75
|
+
end
|
76
|
+
end
|
81
77
|
|
82
|
-
|
83
|
-
|
78
|
+
period_total
|
79
|
+
end
|
84
80
|
|
81
|
+
def self.add_categories_to_table(categories, forecast_data)
|
85
82
|
first_period = true
|
86
|
-
|
87
|
-
|
88
|
-
next if
|
83
|
+
categories.each do |period, total|
|
84
|
+
category_total = total.reject { |_, amount| amount == 0 }
|
85
|
+
next if category_total.empty?
|
86
|
+
|
87
|
+
sorted_category_total = sort_transactions(category_total)
|
89
88
|
|
90
|
-
table.add_separator unless first_period
|
91
|
-
table.add_row([{ value: period.capitalize, colspan: 3, alignment: :center }])
|
89
|
+
@table.add_separator unless first_period
|
90
|
+
@table.add_row([{ value: period.capitalize.bold, colspan: 3, alignment: :center }])
|
92
91
|
|
93
92
|
period_total = 0
|
93
|
+
period_total += if period == 'custom'
|
94
|
+
add_rows_to_table(sum_custom_transactions(forecast_data), period_total, custom: true)
|
95
|
+
else
|
96
|
+
add_rows_to_table(sorted_category_total, period_total)
|
97
|
+
end
|
94
98
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
99
|
+
format_total("#{period.capitalize} TOTAL", period_total)
|
100
|
+
first_period = false
|
101
|
+
end
|
102
|
+
end
|
114
103
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
{ value: formatted_period_total, alignment: :right }]
|
104
|
+
def self.sort_transactions(category_total)
|
105
|
+
negatives = category_total.select { |_, amount| amount < 0 }.sort_by { |_, amount| amount }
|
106
|
+
positives = category_total.select { |_, amount| amount > 0 }.sort_by { |_, amount| -amount }
|
119
107
|
|
120
|
-
|
108
|
+
negatives.concat(positives).to_h
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.format_total(text, total)
|
112
|
+
@table.add_row [{ value: text.bold, colspan: 2, alignment: :left },
|
113
|
+
{ value: format_amount(total).bold, alignment: :right }]
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.generate(forecast)
|
117
|
+
forecast_data = YAML.safe_load(forecast)
|
118
|
+
|
119
|
+
init_table
|
120
|
+
init_generator(forecast_data)
|
121
|
+
|
122
|
+
category_totals = {}
|
123
|
+
%w[monthly quarterly half-yearly yearly once custom].each do |period|
|
124
|
+
category_totals[period] = sum_transactions(forecast_data, period)
|
121
125
|
end
|
122
126
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
{ value: formatted_grand_total, alignment: :right }]
|
127
|
+
add_categories_to_table(category_totals, forecast_data)
|
128
|
+
|
129
|
+
@table.add_separator
|
130
|
+
format_total("TOTAL", category_totals.values.map(&:values).flatten.sum)
|
128
131
|
|
129
|
-
puts table
|
132
|
+
puts @table
|
130
133
|
end
|
131
134
|
end
|
132
135
|
end
|
data/lib/hledger_forecast.rb
CHANGED
@@ -12,7 +12,7 @@ Money.locale_backend = nil
|
|
12
12
|
Money.rounding_mode = BigDecimal::ROUND_HALF_UP
|
13
13
|
|
14
14
|
require_relative 'hledger_forecast/version'
|
15
|
-
require_relative 'hledger_forecast/options'
|
16
15
|
require_relative 'hledger_forecast/generator'
|
17
16
|
require_relative 'hledger_forecast/summarize'
|
17
|
+
require_relative 'hledger_forecast/checker'
|
18
18
|
require_relative 'hledger_forecast/cli'
|
data/spec/command_spec.rb
CHANGED
@@ -2,11 +2,14 @@ require_relative '../lib/hledger_forecast'
|
|
2
2
|
|
3
3
|
RSpec.describe 'command' do
|
4
4
|
it 'uses the CLI to generate an output' do
|
5
|
-
|
5
|
+
# Delete the file if it exists
|
6
|
+
generated_journal = './test_output.journal'
|
7
|
+
File.delete(generated_journal) if File.exist?(generated_journal)
|
8
|
+
|
9
|
+
system("./bin/hledger-forecast generate -t ./spec/stubs/transactions.journal -f ./spec/stubs/monthly/forecast_monthly.yml -o ./test_output.journal -s 2023-03-01 -e 2023-05-30 --force")
|
6
10
|
|
7
11
|
expected_output = File.read('spec/stubs/monthly/output_monthly.journal')
|
8
|
-
generated_journal = File.read('./test_output.journal')
|
9
12
|
|
10
|
-
expect(generated_journal).to eq(expected_output)
|
13
|
+
expect(File.read(generated_journal)).to eq(expected_output)
|
11
14
|
end
|
12
15
|
end
|
data/spec/custom_spec.rb
CHANGED
@@ -5,7 +5,7 @@ RSpec.describe 'generate' do
|
|
5
5
|
transactions = File.read('spec/stubs/transactions.journal')
|
6
6
|
forecast = File.read('spec/stubs/custom/forecast_custom_days.yml')
|
7
7
|
|
8
|
-
generated_journal = HledgerForecast::Generator.
|
8
|
+
generated_journal = HledgerForecast::Generator.generate(transactions, forecast, '2023-03-01',
|
9
9
|
'2023-03-10')
|
10
10
|
|
11
11
|
expected_output = File.read('spec/stubs/custom/output_custom_days.journal')
|
@@ -16,7 +16,7 @@ RSpec.describe 'generate' do
|
|
16
16
|
transactions = File.read('spec/stubs/transactions.journal')
|
17
17
|
forecast = File.read('spec/stubs/custom/forecast_custom_weeks.yml')
|
18
18
|
|
19
|
-
generated_journal = HledgerForecast::Generator.
|
19
|
+
generated_journal = HledgerForecast::Generator.generate(transactions, forecast, '2023-03-01',
|
20
20
|
'2023-04-30')
|
21
21
|
|
22
22
|
expected_output = File.read('spec/stubs/custom/output_custom_weeks.journal')
|
@@ -27,7 +27,7 @@ RSpec.describe 'generate' do
|
|
27
27
|
transactions = File.read('spec/stubs/transactions.journal')
|
28
28
|
forecast = File.read('spec/stubs/custom/forecast_custom_weeks_twice.yml')
|
29
29
|
|
30
|
-
generated_journal = HledgerForecast::Generator.
|
30
|
+
generated_journal = HledgerForecast::Generator.generate(transactions, forecast, '2023-03-01',
|
31
31
|
'2023-03-30')
|
32
32
|
|
33
33
|
expected_output = File.read('spec/stubs/custom/output_custom_weeks_twice.journal')
|
@@ -38,7 +38,7 @@ RSpec.describe 'generate' do
|
|
38
38
|
transactions = File.read('spec/stubs/transactions.journal')
|
39
39
|
forecast = File.read('spec/stubs/custom/forecast_custom_months.yml')
|
40
40
|
|
41
|
-
generated_journal = HledgerForecast::Generator.
|
41
|
+
generated_journal = HledgerForecast::Generator.generate(transactions, forecast, '2023-03-01',
|
42
42
|
'2024-02-28')
|
43
43
|
|
44
44
|
expected_output = File.read('spec/stubs/custom/output_custom_months.journal')
|
data/spec/half-yearly_spec.rb
CHANGED
@@ -5,7 +5,7 @@ RSpec.describe 'generate' do
|
|
5
5
|
transactions = File.read('spec/stubs/transactions.journal')
|
6
6
|
forecast = File.read('spec/stubs/half-yearly/forecast_half-yearly.yml')
|
7
7
|
|
8
|
-
generated_journal = HledgerForecast::Generator.
|
8
|
+
generated_journal = HledgerForecast::Generator.generate(transactions, forecast, '2023-03-01', '2024-04-30')
|
9
9
|
|
10
10
|
expected_output = File.read('spec/stubs/half-yearly/output_half-yearly.journal')
|
11
11
|
expect(generated_journal).to eq(expected_output)
|
data/spec/monthly_spec.rb
CHANGED
@@ -5,7 +5,7 @@ RSpec.describe 'generate' do
|
|
5
5
|
transactions = File.read('spec/stubs/transactions.journal')
|
6
6
|
forecast = File.read('spec/stubs/monthly/forecast_monthly.yml')
|
7
7
|
|
8
|
-
generated_journal = HledgerForecast::Generator.
|
8
|
+
generated_journal = HledgerForecast::Generator.generate(transactions, forecast, '2023-03-01', '2023-05-30')
|
9
9
|
|
10
10
|
expected_output = File.read('spec/stubs/monthly/output_monthly.journal')
|
11
11
|
expect(generated_journal).to eq(expected_output)
|
@@ -15,7 +15,7 @@ RSpec.describe 'generate' do
|
|
15
15
|
transactions = File.read('spec/stubs/transactions.journal')
|
16
16
|
forecast = File.read('spec/stubs/monthly/forecast_monthly_enddate.yml')
|
17
17
|
|
18
|
-
generated_journal = HledgerForecast::Generator.
|
18
|
+
generated_journal = HledgerForecast::Generator.generate(transactions, forecast, '2023-03-01', '2023-08-30')
|
19
19
|
|
20
20
|
expected_output = File.read('spec/stubs/monthly/output_monthly_enddate.journal')
|
21
21
|
expect(generated_journal).to eq(expected_output)
|
@@ -25,7 +25,7 @@ RSpec.describe 'generate' do
|
|
25
25
|
transactions = File.read('spec/stubs/transactions.journal')
|
26
26
|
forecast = File.read('spec/stubs/monthly/forecast_monthly_enddate_top.yml')
|
27
27
|
|
28
|
-
generated_journal = HledgerForecast::Generator.
|
28
|
+
generated_journal = HledgerForecast::Generator.generate(transactions, forecast, '2023-03-01', '2023-08-30')
|
29
29
|
|
30
30
|
expected_output = File.read('spec/stubs/monthly/output_monthly_enddate_top.journal')
|
31
31
|
expect(generated_journal).to eq(expected_output)
|
data/spec/once_spec.rb
CHANGED
@@ -5,7 +5,7 @@ RSpec.describe 'generate' do
|
|
5
5
|
transactions = File.read('spec/stubs/transactions.journal')
|
6
6
|
forecast = File.read('spec/stubs/once/forecast_once.yml')
|
7
7
|
|
8
|
-
generated_journal = HledgerForecast::Generator.
|
8
|
+
generated_journal = HledgerForecast::Generator.generate(transactions, forecast, '2023-03-01', '2024-04-30')
|
9
9
|
|
10
10
|
expected_output = File.read('spec/stubs/once/output_once.journal')
|
11
11
|
expect(generated_journal).to eq(expected_output)
|
data/spec/quarterly_spec.rb
CHANGED
@@ -5,7 +5,7 @@ RSpec.describe 'generate' do
|
|
5
5
|
transactions = File.read('spec/stubs/transactions.journal')
|
6
6
|
forecast = File.read('spec/stubs/quarterly/forecast_quarterly.yml')
|
7
7
|
|
8
|
-
generated_journal = HledgerForecast::Generator.
|
8
|
+
generated_journal = HledgerForecast::Generator.generate(transactions, forecast, '2023-03-01', '2023-10-30')
|
9
9
|
|
10
10
|
expected_output = File.read('spec/stubs/quarterly/output_quarterly.journal')
|
11
11
|
expect(generated_journal).to eq(expected_output)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative '../lib/hledger_forecast'
|
2
|
+
RSpec.describe 'generate' do
|
3
|
+
it 'generates a forecast with correct MONTHLY transactions that have a START DATE' do
|
4
|
+
transactions = File.read('spec/stubs/transactions.journal')
|
5
|
+
forecast = File.read('spec/stubs/start_date/forecast_startdate.yml')
|
6
|
+
|
7
|
+
generated_journal = HledgerForecast::Generator.generate(transactions, forecast, '2023-03-01', '2023-08-30')
|
8
|
+
|
9
|
+
expected_output = File.read('spec/stubs/start_date/output_startdate.journal')
|
10
|
+
expect(generated_journal).to eq(expected_output)
|
11
|
+
end
|
12
|
+
end
|
@@ -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 * Mortgage
|
10
|
+
[Expenses:Mortgage] £2,000.00
|
11
|
+
[Assets:Bank]
|
12
|
+
|
13
|
+
2023-03-01 * Food
|
14
|
+
[Expenses:Food] £100.00
|
15
|
+
[Assets:Bank]
|
16
|
+
|
17
|
+
2023-03-01 * Savings
|
18
|
+
[Assets:Bank] -£1,000.00
|
19
|
+
[Assets:Savings]
|
20
|
+
|
21
|
+
2023-04-01 * Mortgage
|
22
|
+
[Expenses:Mortgage] £2,000.00
|
23
|
+
[Assets:Bank]
|
24
|
+
|
25
|
+
2023-04-01 * Food
|
26
|
+
[Expenses:Food] £100.00
|
27
|
+
[Assets:Bank]
|
28
|
+
|
29
|
+
2023-04-01 * Savings
|
30
|
+
[Assets:Bank] -£1,000.00
|
31
|
+
[Assets:Savings]
|
32
|
+
|
33
|
+
2023-05-01 * Mortgage
|
34
|
+
[Expenses:Mortgage] £2,000.00
|
35
|
+
[Assets:Bank]
|
36
|
+
|
37
|
+
2023-05-01 * Food
|
38
|
+
[Expenses:Food] £100.00
|
39
|
+
[Assets:Bank]
|
40
|
+
|
41
|
+
2023-05-01 * Savings
|
42
|
+
[Assets:Bank] -£1,000.00
|
43
|
+
[Assets:Savings]
|
44
|
+
|
@@ -7,7 +7,7 @@
|
|
7
7
|
Assets:Bank
|
8
8
|
|
9
9
|
2023-03-01 * Mortgage
|
10
|
-
[Expenses:Mortgage] £2,000.
|
10
|
+
[Expenses:Mortgage] £2,000.55
|
11
11
|
[Assets:Bank]
|
12
12
|
|
13
13
|
2023-03-01 * Food
|
@@ -19,7 +19,7 @@
|
|
19
19
|
[Assets:Savings]
|
20
20
|
|
21
21
|
2023-04-01 * Mortgage
|
22
|
-
[Expenses:Mortgage] £2,000.
|
22
|
+
[Expenses:Mortgage] £2,000.55
|
23
23
|
[Assets:Bank]
|
24
24
|
|
25
25
|
2023-04-01 * Food
|
@@ -31,7 +31,7 @@
|
|
31
31
|
[Assets:Savings]
|
32
32
|
|
33
33
|
2023-05-01 * Mortgage
|
34
|
-
[Expenses:Mortgage] £2,000.
|
34
|
+
[Expenses:Mortgage] £2,000.55
|
35
35
|
[Assets:Bank]
|
36
36
|
|
37
37
|
2023-05-01 * Food
|
@@ -0,0 +1,26 @@
|
|
1
|
+
settings:
|
2
|
+
currency: GBP
|
3
|
+
|
4
|
+
monthly:
|
5
|
+
- start: "2023-03-01"
|
6
|
+
account: "[Assets:Bank]"
|
7
|
+
transactions:
|
8
|
+
- description: Monthly Mortgage
|
9
|
+
start: "2023-06-01"
|
10
|
+
category: "[Expenses:Mortgage]"
|
11
|
+
amount: 2000
|
12
|
+
- description: Monthly Food
|
13
|
+
category: "[Expenses:Food]"
|
14
|
+
amount: 100
|
15
|
+
|
16
|
+
quarterly:
|
17
|
+
- start: "2023-04-01"
|
18
|
+
account: "[Assets:Bank]"
|
19
|
+
transactions:
|
20
|
+
- description: Quarterly Mortgage
|
21
|
+
start: "2023-07-01"
|
22
|
+
category: "[Expenses:Mortgage]"
|
23
|
+
amount: 1000
|
24
|
+
- description: Quarterly Food
|
25
|
+
category: "[Expenses:Food]"
|
26
|
+
amount: 50
|
@@ -0,0 +1,56 @@
|
|
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 * Monthly Food
|
10
|
+
[Expenses:Food] £100.00
|
11
|
+
[Assets:Bank]
|
12
|
+
|
13
|
+
2023-04-01 * Monthly Food
|
14
|
+
[Expenses:Food] £100.00
|
15
|
+
[Assets:Bank]
|
16
|
+
|
17
|
+
2023-04-01 * Quarterly Food
|
18
|
+
[Expenses:Food] £50.00
|
19
|
+
[Assets:Bank]
|
20
|
+
|
21
|
+
2023-05-01 * Monthly Food
|
22
|
+
[Expenses:Food] £100.00
|
23
|
+
[Assets:Bank]
|
24
|
+
|
25
|
+
2023-06-01 * Monthly Mortgage
|
26
|
+
[Expenses:Mortgage] £2,000.00
|
27
|
+
[Assets:Bank]
|
28
|
+
|
29
|
+
2023-06-01 * Monthly Food
|
30
|
+
[Expenses:Food] £100.00
|
31
|
+
[Assets:Bank]
|
32
|
+
|
33
|
+
2023-07-01 * Monthly Mortgage
|
34
|
+
[Expenses:Mortgage] £2,000.00
|
35
|
+
[Assets:Bank]
|
36
|
+
|
37
|
+
2023-07-01 * Monthly Food
|
38
|
+
[Expenses:Food] £100.00
|
39
|
+
[Assets:Bank]
|
40
|
+
|
41
|
+
2023-07-01 * Quarterly Mortgage
|
42
|
+
[Expenses:Mortgage] £1,000.00
|
43
|
+
[Assets:Bank]
|
44
|
+
|
45
|
+
2023-07-01 * Quarterly Food
|
46
|
+
[Expenses:Food] £50.00
|
47
|
+
[Assets:Bank]
|
48
|
+
|
49
|
+
2023-08-01 * Monthly Mortgage
|
50
|
+
[Expenses:Mortgage] £2,000.00
|
51
|
+
[Assets:Bank]
|
52
|
+
|
53
|
+
2023-08-01 * Monthly Food
|
54
|
+
[Expenses:Food] £100.00
|
55
|
+
[Assets:Bank]
|
56
|
+
|
data/spec/yearly_spec.rb
CHANGED
@@ -5,7 +5,7 @@ RSpec.describe 'generate' do
|
|
5
5
|
transactions = File.read('spec/stubs/transactions.journal')
|
6
6
|
forecast = File.read('spec/stubs/yearly/forecast_yearly.yml')
|
7
7
|
|
8
|
-
generated_journal = HledgerForecast::Generator.
|
8
|
+
generated_journal = HledgerForecast::Generator.generate(transactions, forecast, '2023-03-01', '2024-04-30')
|
9
9
|
|
10
10
|
expected_output = File.read('spec/stubs/yearly/output_yearly.journal')
|
11
11
|
expect(generated_journal).to eq(expected_output)
|
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.
|
4
|
+
version: 0.2.0
|
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-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: highline
|
@@ -100,7 +100,6 @@ files:
|
|
100
100
|
- lib/hledger_forecast.rb
|
101
101
|
- lib/hledger_forecast/cli.rb
|
102
102
|
- lib/hledger_forecast/generator.rb
|
103
|
-
- lib/hledger_forecast/options.rb
|
104
103
|
- lib/hledger_forecast/summarize.rb
|
105
104
|
- lib/hledger_forecast/version.rb
|
106
105
|
- spec/command_spec.rb
|
@@ -109,6 +108,7 @@ files:
|
|
109
108
|
- spec/monthly_spec.rb
|
110
109
|
- spec/once_spec.rb
|
111
110
|
- spec/quarterly_spec.rb
|
111
|
+
- spec/start_date_spec.rb
|
112
112
|
- spec/stubs/custom/forecast_custom_days.yml
|
113
113
|
- spec/stubs/custom/forecast_custom_months.yml
|
114
114
|
- spec/stubs/custom/forecast_custom_weeks.yml
|
@@ -119,6 +119,8 @@ files:
|
|
119
119
|
- spec/stubs/custom/output_custom_weeks_twice.journal
|
120
120
|
- spec/stubs/half-yearly/forecast_half-yearly.yml
|
121
121
|
- spec/stubs/half-yearly/output_half-yearly.journal
|
122
|
+
- spec/stubs/modifiers/forecast_modifiers.yml
|
123
|
+
- spec/stubs/modifiers/output_modifiers.journal
|
122
124
|
- spec/stubs/monthly/forecast_monthly.yml
|
123
125
|
- spec/stubs/monthly/forecast_monthly_enddate.yml
|
124
126
|
- spec/stubs/monthly/forecast_monthly_enddate_top.yml
|
@@ -131,6 +133,8 @@ files:
|
|
131
133
|
- spec/stubs/once/output_once.journal
|
132
134
|
- spec/stubs/quarterly/forecast_quarterly.yml
|
133
135
|
- spec/stubs/quarterly/output_quarterly.journal
|
136
|
+
- spec/stubs/start_date/forecast_startdate.yml
|
137
|
+
- spec/stubs/start_date/output_startdate.journal
|
134
138
|
- spec/stubs/transactions.journal
|
135
139
|
- spec/stubs/yearly/forecast_yearly.yml
|
136
140
|
- spec/stubs/yearly/output_yearly.journal
|
@@ -165,6 +169,7 @@ test_files:
|
|
165
169
|
- spec/monthly_spec.rb
|
166
170
|
- spec/once_spec.rb
|
167
171
|
- spec/quarterly_spec.rb
|
172
|
+
- spec/start_date_spec.rb
|
168
173
|
- spec/stubs/custom/forecast_custom_days.yml
|
169
174
|
- spec/stubs/custom/forecast_custom_months.yml
|
170
175
|
- spec/stubs/custom/forecast_custom_weeks.yml
|
@@ -175,6 +180,8 @@ test_files:
|
|
175
180
|
- spec/stubs/custom/output_custom_weeks_twice.journal
|
176
181
|
- spec/stubs/half-yearly/forecast_half-yearly.yml
|
177
182
|
- spec/stubs/half-yearly/output_half-yearly.journal
|
183
|
+
- spec/stubs/modifiers/forecast_modifiers.yml
|
184
|
+
- spec/stubs/modifiers/output_modifiers.journal
|
178
185
|
- spec/stubs/monthly/forecast_monthly.yml
|
179
186
|
- spec/stubs/monthly/forecast_monthly_enddate.yml
|
180
187
|
- spec/stubs/monthly/forecast_monthly_enddate_top.yml
|
@@ -187,6 +194,8 @@ test_files:
|
|
187
194
|
- spec/stubs/once/output_once.journal
|
188
195
|
- spec/stubs/quarterly/forecast_quarterly.yml
|
189
196
|
- spec/stubs/quarterly/output_quarterly.journal
|
197
|
+
- spec/stubs/start_date/forecast_startdate.yml
|
198
|
+
- spec/stubs/start_date/output_startdate.journal
|
190
199
|
- spec/stubs/transactions.journal
|
191
200
|
- spec/stubs/yearly/forecast_yearly.yml
|
192
201
|
- spec/stubs/yearly/output_yearly.journal
|
@@ -1,77 +0,0 @@
|
|
1
|
-
module HledgerForecast
|
2
|
-
class Options
|
3
|
-
def self.parse_command_line_options(args = ARGV, _stdin = $stdin)
|
4
|
-
options = {}
|
5
|
-
|
6
|
-
OptionParser.new do |opts|
|
7
|
-
opts.banner = "Usage: Hledger-Forecast [options]"
|
8
|
-
opts.separator ""
|
9
|
-
|
10
|
-
opts.on("-f", "--forecast FILE",
|
11
|
-
"The FORECAST yaml file to generate from") do |file|
|
12
|
-
options[:forecast_file] = file
|
13
|
-
options[:output_file] ||= file.sub(/\.yml$/, '.journal')
|
14
|
-
end
|
15
|
-
|
16
|
-
opts.on("-t", "--transaction FILE",
|
17
|
-
"The base TRANSACTIONS file to extend from") do |file|
|
18
|
-
options[:transactions_file] = file if file && !file.empty?
|
19
|
-
end
|
20
|
-
|
21
|
-
opts.on("-o", "--output-file FILE",
|
22
|
-
"The OUTPUT file to create") do |file|
|
23
|
-
options[:output_file] = file
|
24
|
-
end
|
25
|
-
|
26
|
-
opts.on("-s", "--start-date DATE",
|
27
|
-
"The date to start generating from (yyyy-mm-dd)") do |a|
|
28
|
-
options[:start_date] = a
|
29
|
-
end
|
30
|
-
|
31
|
-
opts.on("-e", "--end-date DATE",
|
32
|
-
"The date to start generating to (yyyy-mm-dd)") do |a|
|
33
|
-
options[:end_date] = a
|
34
|
-
end
|
35
|
-
|
36
|
-
opts.on("--summarize",
|
37
|
-
"Summarize the forecast file and output to the terminal") do |a|
|
38
|
-
options[:summarize] = a
|
39
|
-
end
|
40
|
-
|
41
|
-
opts.on("--force",
|
42
|
-
"Force an overwrite of the output file") do |a|
|
43
|
-
options[:force] = a
|
44
|
-
end
|
45
|
-
|
46
|
-
opts.on_tail("-h", "--help", "Show this message") do
|
47
|
-
puts opts
|
48
|
-
exit
|
49
|
-
end
|
50
|
-
|
51
|
-
opts.on_tail("--version", "Show version") do
|
52
|
-
puts VERSION
|
53
|
-
exit
|
54
|
-
end
|
55
|
-
|
56
|
-
opts.parse!(args)
|
57
|
-
end
|
58
|
-
|
59
|
-
options[:forecast_file] = "forecast.yml" unless options[:forecast_file]
|
60
|
-
options[:output_file] = "forecast.journal" unless options[:output_file]
|
61
|
-
|
62
|
-
today = Date.today
|
63
|
-
|
64
|
-
unless options[:start_date]
|
65
|
-
options[:default_dates] = true
|
66
|
-
options[:start_date] =
|
67
|
-
Date.new(today.year, today.month, 1).next_month.to_s
|
68
|
-
end
|
69
|
-
unless options[:end_date]
|
70
|
-
options[:default_dates] = true
|
71
|
-
options[:end_date] = Date.new(today.year + 3, 12, 31).to_s
|
72
|
-
end
|
73
|
-
|
74
|
-
return options
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|