hledger-forecast 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +33 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +20 -0
- data/.ruby-version +1 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +41 -0
- data/LICENSE +21 -0
- data/README.md +119 -0
- data/bin/hledger-forecast +13 -0
- data/example.yaml +26 -0
- data/hledger-forecast.gemspec +25 -0
- data/lib/hledger_forecast/command.rb +30 -0
- data/lib/hledger_forecast/generator.rb +93 -0
- data/lib/hledger_forecast/options.rb +67 -0
- data/lib/hledger_forecast/version.rb +3 -0
- data/lib/hledger_forecast.rb +15 -0
- data/spec/monthly_spec.rb +33 -0
- data/spec/once_spec.rb +13 -0
- data/spec/quarterly_spec.rb +13 -0
- data/spec/stubs/monthly/forecast_monthly.yml +13 -0
- data/spec/stubs/monthly/forecast_monthly_enddate.yml +14 -0
- data/spec/stubs/monthly/forecast_monthly_enddate_top.yml +14 -0
- data/spec/stubs/monthly/forecast_monthly_modifier.yml +11 -0
- data/spec/stubs/once/forecast_once.yml +10 -0
- data/spec/stubs/quarterly/forecast_quarterly.yml +10 -0
- data/spec/stubs/transactions.journal +8 -0
- data/spec/stubs/yearly/forecast_yearly.yml +10 -0
- data/spec/yearly_spec.rb +13 -0
- metadata +126 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e605f8a4436c2b52e4b37e06aec308ef70d0fb4a7572d0791820c73ddafeaa1f
|
4
|
+
data.tar.gz: f62dc41d637ea7107ec282b41d61ed0040c3fc3f4b84ec139a1711cf09f94af5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b44389e7cc8ee05d8f6d35a0a649e11d96c2bcc3149f5b2ccf92c563db206dae54ee65a39eb9982f9c9e0dc16688ca42ef012e95aa38b99c9ef378f0ab57d3f9
|
7
|
+
data.tar.gz: 5d64a7679916fa88216dc7daf3a4fc3607002114b025925d4e65e84c1c0a6b4243c1f389d7cb8ca0ba28209c6736ed9cf1ec5c8bcf3ddd264949d6be387f321a
|
@@ -0,0 +1,33 @@
|
|
1
|
+
name: Continuous Integration
|
2
|
+
|
3
|
+
on:
|
4
|
+
pull_request:
|
5
|
+
branches: [main]
|
6
|
+
push:
|
7
|
+
branches: [main]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
strategy:
|
13
|
+
matrix:
|
14
|
+
ruby-version:
|
15
|
+
- 3.1.2
|
16
|
+
- 3.0
|
17
|
+
|
18
|
+
steps:
|
19
|
+
- uses: actions/checkout@v3
|
20
|
+
- name: Update package
|
21
|
+
run: sudo apt-get update
|
22
|
+
- name: Install packages
|
23
|
+
run: sudo apt-get -y install ledger hledger
|
24
|
+
- name: Set up Ruby
|
25
|
+
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
26
|
+
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
27
|
+
uses: ruby/setup-ruby@v1
|
28
|
+
# uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
|
29
|
+
with:
|
30
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems
|
31
|
+
ruby-version: ${{ matrix.ruby-version }}
|
32
|
+
- name: Run tests
|
33
|
+
run: rspec
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Layout/LineLength:
|
2
|
+
Max: 120
|
3
|
+
|
4
|
+
Style/StringLiterals:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
Style/RedundantReturn:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Metrics/ClassLength:
|
11
|
+
Enabled: False
|
12
|
+
|
13
|
+
Metrics/MethodLength:
|
14
|
+
Enabled: False
|
15
|
+
|
16
|
+
Metrics/AbcSize:
|
17
|
+
Enabled: False
|
18
|
+
|
19
|
+
Style/NumericPredicate:
|
20
|
+
Enabled: False
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.0.0
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
hledger-forecast (0.1.0)
|
5
|
+
highline (~> 2.1.0)
|
6
|
+
money (~> 6.16.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
concurrent-ruby (1.2.2)
|
12
|
+
diff-lcs (1.5.0)
|
13
|
+
highline (2.1.0)
|
14
|
+
i18n (1.12.0)
|
15
|
+
concurrent-ruby (~> 1.0)
|
16
|
+
money (6.16.0)
|
17
|
+
i18n (>= 0.6.4, <= 2)
|
18
|
+
rspec (3.12.0)
|
19
|
+
rspec-core (~> 3.12.0)
|
20
|
+
rspec-expectations (~> 3.12.0)
|
21
|
+
rspec-mocks (~> 3.12.0)
|
22
|
+
rspec-core (3.12.1)
|
23
|
+
rspec-support (~> 3.12.0)
|
24
|
+
rspec-expectations (3.12.2)
|
25
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
26
|
+
rspec-support (~> 3.12.0)
|
27
|
+
rspec-mocks (3.12.5)
|
28
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
29
|
+
rspec-support (~> 3.12.0)
|
30
|
+
rspec-support (3.12.0)
|
31
|
+
|
32
|
+
PLATFORMS
|
33
|
+
arm64-darwin-21
|
34
|
+
|
35
|
+
DEPENDENCIES
|
36
|
+
hledger-forecast!
|
37
|
+
money
|
38
|
+
rspec (~> 3.12)
|
39
|
+
|
40
|
+
BUNDLED WITH
|
41
|
+
2.4.10
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 Oli Morris
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# Hledger-Forecast
|
2
|
+
|
3
|
+
![Tests](https://github.com/olimorris/hledger-forecast/workflows/ci/badge.svg)
|
4
|
+
|
5
|
+
> **Warning**: This is still in the early stages of development and the API is likely to change
|
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).
|
8
|
+
|
9
|
+
See the [rationale](#brain-rationale) section for why this gem may be useful to you.
|
10
|
+
|
11
|
+
## :sparkles: Features
|
12
|
+
|
13
|
+
- :book: Uses a simple YAML config file to generate periodic transactions
|
14
|
+
- :date: Specify start and end dates for forecasts
|
15
|
+
- :heavy_dollar_sign: Full currency support (uses the [RubyMoney](https://github.com/RubyMoney/money) gem)
|
16
|
+
- :computer: Simple and easy to use CLI
|
17
|
+
|
18
|
+
## :package: Installation
|
19
|
+
|
20
|
+
Assuming you have Ruby and [Rubygems](http://rubygems.org/pages/download) installed on your system, simply run:
|
21
|
+
|
22
|
+
gem install --user hledger-forecast
|
23
|
+
|
24
|
+
## :rocket: Usage
|
25
|
+
|
26
|
+
Simply run:
|
27
|
+
|
28
|
+
hledger-forecast
|
29
|
+
|
30
|
+
Running `hledger-forecast -h` shows the available options:
|
31
|
+
|
32
|
+
Usage: Hledger-Forecast [options]
|
33
|
+
|
34
|
+
-f, --forecast FILE The FORECAST yaml file to generate from
|
35
|
+
-t, --transaction FILE The base TRANSACTIONS file to extend from
|
36
|
+
-o, --output-file FILE The OUTPUT file to create
|
37
|
+
-s, --start-date DATE The date to start generating from (yyyy-mm-dd)
|
38
|
+
-e, --end-date DATE The date to start generating to (yyyy-mm-dd)
|
39
|
+
-h, --help Show this message
|
40
|
+
--version Show version
|
41
|
+
|
42
|
+
To then include in Hledger:
|
43
|
+
|
44
|
+
hledger -f transactions.journal -f forecast.journal
|
45
|
+
|
46
|
+
where `transactions.journal` might be your bank transactions (your "actuals") and `forecast.journal` is the file generated with the `-o` option from above (your "forecast").
|
47
|
+
|
48
|
+
### A simple config file
|
49
|
+
|
50
|
+
Firstly, create a `yml` file which will contain the transactions you'd like to forecast:
|
51
|
+
|
52
|
+
```yaml
|
53
|
+
# forecast.yml
|
54
|
+
monthly:
|
55
|
+
- account: "[Assets:Bank]"
|
56
|
+
start: "2023-03-01"
|
57
|
+
transactions:
|
58
|
+
- amount: 2000
|
59
|
+
category: "[Expenses:Mortgage]"
|
60
|
+
description: Mortgage
|
61
|
+
- amount: 500
|
62
|
+
category: "[Expenses:Food]"
|
63
|
+
description: Food
|
64
|
+
```
|
65
|
+
|
66
|
+
Let's examine what's going on in this config file:
|
67
|
+
|
68
|
+
- Firstly, we're telling the app to create two monthly transactions and repeat them, forever, starting from March 2023. In this case, forever will be the `end_date` specified when running the app
|
69
|
+
- If you ran the app with `hledger-forecast -s 2023-04-01` then no transactions would be generated for March as the start date is greater than the periodic start date
|
70
|
+
- Notice we're also using [virtual postings](https://hledger.org/1.29/hledger.html#virtual-postings) (designated by the brackets) to be explicit to Hledger. This also makes it easy to filter them out with the `-R` or `--real` option in Hledger
|
71
|
+
- We also have not specified a currency; the default (`USD`) will be used
|
72
|
+
|
73
|
+
### Extending the config file
|
74
|
+
|
75
|
+
#### Periods
|
76
|
+
|
77
|
+
If you'd like to add quarterly, yearly or one-off transactions, use the following keys:
|
78
|
+
|
79
|
+
- `quarterly`
|
80
|
+
- `yearly`
|
81
|
+
- `once`
|
82
|
+
|
83
|
+
The structure of the config file remains exactly the same.
|
84
|
+
|
85
|
+
> **Note**: A quarterly transaction will repeat for every 3 months from the start date
|
86
|
+
|
87
|
+
#### Currency
|
88
|
+
|
89
|
+
To specify a currency:
|
90
|
+
|
91
|
+
```yaml
|
92
|
+
# forecast.yml
|
93
|
+
settings:
|
94
|
+
currency: GBP
|
95
|
+
```
|
96
|
+
|
97
|
+
#### Additional settings
|
98
|
+
|
99
|
+
Additional settings in the config file:
|
100
|
+
|
101
|
+
```yaml
|
102
|
+
# forecast.yml
|
103
|
+
settings:
|
104
|
+
show_symbol: true # Show the currency symbol?
|
105
|
+
sign_before_symbol: true # Show the negative sign before the symbol?
|
106
|
+
thousands_separator: true # Separate thousands with a comma?
|
107
|
+
```
|
108
|
+
|
109
|
+
## :brain: Rationale
|
110
|
+
|
111
|
+
Firstly, I've come to realise from reading countless blog and Reddit posts on [plain text accounting](https://plaintextaccounting.org), that everyone does it __completely__ differently!
|
112
|
+
|
113
|
+
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.
|
114
|
+
|
115
|
+
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 for 12, 24 months time? It was at this point in my shift to PTA that I hit a wall.
|
116
|
+
|
117
|
+
While Hledger provides support 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 difficult and tiresome.
|
118
|
+
|
119
|
+
With this gem, my aim was to make it easy for users to change a config file, re-run a CLI command and be able to open a text file and see the changes. No guesswork. No surprises.
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/hledger_forecast'
|
4
|
+
# require 'hledger_forecast'
|
5
|
+
|
6
|
+
begin
|
7
|
+
options = HledgerForecast::Options.parse_command_line_options
|
8
|
+
rescue RuntimeError => e
|
9
|
+
puts("ERROR: #{e}")
|
10
|
+
exit(1)
|
11
|
+
end
|
12
|
+
|
13
|
+
HledgerForecast::Command.run(options)
|
data/example.yaml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
monthly:
|
2
|
+
- date: "2023-03-01"
|
3
|
+
account: "[Assets:Bank]"
|
4
|
+
transactions:
|
5
|
+
- description: Mortgage
|
6
|
+
category: "[Expenses:Mortgage]"
|
7
|
+
amount: £1,000.00
|
8
|
+
- description: Food
|
9
|
+
category: "[Expenses:Food]"
|
10
|
+
amount: £500.00
|
11
|
+
|
12
|
+
quarterly:
|
13
|
+
- date: "2023-04-01"
|
14
|
+
account: "[Assets:Bank]"
|
15
|
+
transactions:
|
16
|
+
- description: Bonus
|
17
|
+
category: "[Income:Bonus]"
|
18
|
+
amount: -£1,000.00
|
19
|
+
|
20
|
+
yearly:
|
21
|
+
- date: "2023-04-01"
|
22
|
+
account: "[Assets:Bank]"
|
23
|
+
transactions:
|
24
|
+
- description: Annual Bonus
|
25
|
+
category: "[Income:Bonus]"
|
26
|
+
amount: -£2,000.00
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'hledger_forecast/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'hledger-forecast'
|
5
|
+
s.version = HledgerForecast::VERSION
|
6
|
+
s.authors = ['Oli Morris']
|
7
|
+
s.summary = 'Utility to generate forecasts in Hledger'
|
8
|
+
s.description = 'Uses a YAML file to generate monthly, quarterly, yearly and one-off transactions for better forecasting in Hledger'
|
9
|
+
s.email = 'olimorris@users.noreply.github.com'
|
10
|
+
s.homepage = 'https://github.com/olimorris/hledger-forecast'
|
11
|
+
s.license = 'MIT'
|
12
|
+
|
13
|
+
s.required_ruby_version = '>= 3.0.0'
|
14
|
+
|
15
|
+
s.add_dependency "highline", "~> 2.1.0"
|
16
|
+
s.add_dependency "money", "~> 6.16.0"
|
17
|
+
s.add_development_dependency 'rspec', '~> 3.12'
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
s.test_files = `git ls-files -- spec/*`.split("\n")
|
21
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map do |f|
|
22
|
+
File.basename(f)
|
23
|
+
end
|
24
|
+
s.require_paths = ['lib']
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module HledgerForecast
|
2
|
+
class Command
|
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
|
+
|
9
|
+
puts "[Using default dates: #{start_date} to #{end_date}]" if args[:default_dates]
|
10
|
+
|
11
|
+
transactions = Generator.create_journal_entries(transactions, forecast, start_date, end_date)
|
12
|
+
|
13
|
+
output_file = args[:output_file]
|
14
|
+
if File.exist?(output_file)
|
15
|
+
print "File '#{output_file}' already exists. Overwrite? (y/n): "
|
16
|
+
overwrite = gets.chomp.downcase
|
17
|
+
|
18
|
+
if overwrite == 'y'
|
19
|
+
File.write(output_file, transactions)
|
20
|
+
puts "File '#{output_file}' has been overwritten."
|
21
|
+
else
|
22
|
+
puts "Operation aborted. File '#{output_file}' was not overwritten."
|
23
|
+
end
|
24
|
+
else
|
25
|
+
File.write(output_file, transactions)
|
26
|
+
puts "File '#{output_file}' has been created."
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module HledgerForecast
|
2
|
+
# Generates journal entries based on a YAML forecast file.
|
3
|
+
# on forecast data and optional existing transactions.
|
4
|
+
#
|
5
|
+
class Generator
|
6
|
+
class << self
|
7
|
+
attr_accessor :settings
|
8
|
+
end
|
9
|
+
|
10
|
+
self.settings = {}
|
11
|
+
|
12
|
+
def self.configure_settings(forecast_data)
|
13
|
+
@settings[:currency] = Money::Currency.new(forecast_data.fetch('settings', {}).fetch('currency', 'USD'))
|
14
|
+
@settings[:show_symbol] = forecast_data.fetch('settings', {}).fetch('show_symbol', true)
|
15
|
+
@settings[:sign_before_symbol] = forecast_data.fetch('settings', {}).fetch('sign_before_symbol', true)
|
16
|
+
@settings[:thousands_separator] = forecast_data.fetch('settings', {}).fetch('thousands_separator', true)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.write_transactions(output, date, account, transaction)
|
20
|
+
output.concat("#{date} * #{transaction['description']}\n")
|
21
|
+
output.concat(" #{transaction['category']} #{transaction['amount']}\n")
|
22
|
+
output.concat(" #{account}\n\n")
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.format_transaction(transaction)
|
26
|
+
formatted_transaction = transaction.clone
|
27
|
+
|
28
|
+
formatted_transaction['amount'] =
|
29
|
+
Money.from_cents(formatted_transaction['amount'].to_i * 100, @settings[:currency]).format(
|
30
|
+
symbol: @settings[:show_symbol],
|
31
|
+
sign_before_symbol: @settings[:sign_before_symbol],
|
32
|
+
thousands_separator: @settings[:thousands_separator] ? ',' : nil
|
33
|
+
)
|
34
|
+
|
35
|
+
formatted_transaction
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.process_forecast(output_file, forecast_data, type, date)
|
39
|
+
forecast_data[type]&.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
|
+
|
44
|
+
next if end_date && date > end_date
|
45
|
+
|
46
|
+
date_matches = case type
|
47
|
+
when 'monthly'
|
48
|
+
date.day == start_date.day
|
49
|
+
when 'quarterly'
|
50
|
+
date.day == start_date.day && date.month % 3 == start_date.month % 3
|
51
|
+
when 'yearly'
|
52
|
+
date.day == start_date.day && date.month == start_date.month
|
53
|
+
when 'once'
|
54
|
+
date == start_date
|
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_file, date, account, format_transaction(transaction))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.create_journal_entries(transactions, forecast, start_date, end_date)
|
70
|
+
start_date = Date.parse(start_date)
|
71
|
+
end_date = Date.parse(end_date)
|
72
|
+
forecast_data = YAML.safe_load(forecast)
|
73
|
+
|
74
|
+
configure_settings(forecast_data)
|
75
|
+
|
76
|
+
output = ''
|
77
|
+
output.concat(transactions) if transactions
|
78
|
+
|
79
|
+
date = start_date
|
80
|
+
|
81
|
+
while date <= end_date
|
82
|
+
process_forecast(output, forecast_data, 'monthly', date)
|
83
|
+
process_forecast(output, forecast_data, 'quarterly', date)
|
84
|
+
process_forecast(output, forecast_data, 'yearly', date)
|
85
|
+
process_forecast(output, forecast_data, 'once', date)
|
86
|
+
|
87
|
+
date = date.next_day
|
88
|
+
end
|
89
|
+
|
90
|
+
output
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module HledgerForecast
|
2
|
+
class Options
|
3
|
+
def self.parse_command_line_options(args = ARGV, _stdin = $stdin)
|
4
|
+
cli = HighLine.new
|
5
|
+
options = {}
|
6
|
+
|
7
|
+
OptionParser.new do |opts|
|
8
|
+
opts.banner = "Usage: Hledger-Forecast [options]"
|
9
|
+
opts.separator ""
|
10
|
+
|
11
|
+
opts.on("-f", "--forecast FILE",
|
12
|
+
"The FORECAST yaml file to generate from") do |file|
|
13
|
+
options[:forecast_file] = file
|
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_tail("-h", "--help", "Show this message") do
|
37
|
+
puts opts
|
38
|
+
exit
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on_tail("--version", "Show version") do
|
42
|
+
puts VERSION
|
43
|
+
exit
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.parse!(args)
|
47
|
+
end
|
48
|
+
|
49
|
+
options[:forecast_file] = "forecast.yml" unless options[:forecast_file]
|
50
|
+
options[:output_file] = "forecast.journal" unless options[:output_file]
|
51
|
+
|
52
|
+
today = Date.today
|
53
|
+
|
54
|
+
unless options[:start_date]
|
55
|
+
options[:default_dates] = true
|
56
|
+
options[:start_date] =
|
57
|
+
Date.new(today.year, today.month, 1).next_month.to_s
|
58
|
+
end
|
59
|
+
unless options[:end_date]
|
60
|
+
options[:default_dates] = true
|
61
|
+
options[:end_date] = Date.new(today.year + 3, 12, 31).to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
return options
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'highline'
|
5
|
+
require 'money'
|
6
|
+
require 'optparse'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
Money.locale_backend = nil
|
10
|
+
Money.rounding_mode = BigDecimal::ROUND_HALF_UP
|
11
|
+
|
12
|
+
require_relative 'hledger_forecast/version'
|
13
|
+
require_relative 'hledger_forecast/options'
|
14
|
+
require_relative 'hledger_forecast/generator'
|
15
|
+
require_relative 'hledger_forecast/command'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative '../lib/hledger_forecast'
|
2
|
+
|
3
|
+
RSpec.describe 'generate' do
|
4
|
+
it 'generates a forecast with correct MONTHLY transactions' do
|
5
|
+
transactions = File.read('spec/stubs/transactions.journal')
|
6
|
+
forecast = File.read('spec/stubs/monthly/forecast_monthly.yml')
|
7
|
+
|
8
|
+
generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01', '2023-05-30')
|
9
|
+
|
10
|
+
expected_output = File.read('spec/stubs/monthly/output_monthly.journal')
|
11
|
+
expect(generated_journal).to eq(expected_output)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'generates a forecast with correct MONTHLY transactions that have an end date' do
|
15
|
+
transactions = File.read('spec/stubs/transactions.journal')
|
16
|
+
forecast = File.read('spec/stubs/monthly/forecast_monthly_enddate.yml')
|
17
|
+
|
18
|
+
generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01', '2023-08-30')
|
19
|
+
|
20
|
+
expected_output = File.read('spec/stubs/monthly/output_monthly_enddate.journal')
|
21
|
+
expect(generated_journal).to eq(expected_output)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'generates a forecast with correct MONTHLY transactions that have an end date, at the top level' do
|
25
|
+
transactions = File.read('spec/stubs/transactions.journal')
|
26
|
+
forecast = File.read('spec/stubs/monthly/forecast_monthly_enddate_top.yml')
|
27
|
+
|
28
|
+
generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01', '2023-08-30')
|
29
|
+
|
30
|
+
expected_output = File.read('spec/stubs/monthly/output_monthly_enddate_top.journal')
|
31
|
+
expect(generated_journal).to eq(expected_output)
|
32
|
+
end
|
33
|
+
end
|
data/spec/once_spec.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative '../lib/hledger_forecast'
|
2
|
+
|
3
|
+
RSpec.describe 'generate' do
|
4
|
+
it 'generates a forecast with correct ONCE transactions' do
|
5
|
+
transactions = File.read('spec/stubs/transactions.journal')
|
6
|
+
forecast = File.read('spec/stubs/once/forecast_once.yml')
|
7
|
+
|
8
|
+
generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01', '2024-04-30')
|
9
|
+
|
10
|
+
expected_output = File.read('spec/stubs/once/output_once.journal')
|
11
|
+
expect(generated_journal).to eq(expected_output)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative '../lib/hledger_forecast'
|
2
|
+
|
3
|
+
RSpec.describe 'generate' do
|
4
|
+
it 'generates a forecast with correct QUARTERLY transactions' do
|
5
|
+
transactions = File.read('spec/stubs/transactions.journal')
|
6
|
+
forecast = File.read('spec/stubs/quarterly/forecast_quarterly.yml')
|
7
|
+
|
8
|
+
generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01', '2023-10-30')
|
9
|
+
|
10
|
+
expected_output = File.read('spec/stubs/quarterly/output_quarterly.journal')
|
11
|
+
expect(generated_journal).to eq(expected_output)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
settings:
|
2
|
+
currency: GBP
|
3
|
+
|
4
|
+
monthly:
|
5
|
+
- start: "2023-03-01"
|
6
|
+
account: "[Assets:Bank]"
|
7
|
+
transactions:
|
8
|
+
- description: Mortgage
|
9
|
+
end: "2023-06-01"
|
10
|
+
category: "[Expenses:Mortgage]"
|
11
|
+
amount: 2,000.00
|
12
|
+
- description: Food
|
13
|
+
category: "[Expenses:Food]"
|
14
|
+
amount: 100.00
|
@@ -0,0 +1,14 @@
|
|
1
|
+
settings:
|
2
|
+
currency: GBP
|
3
|
+
|
4
|
+
monthly:
|
5
|
+
- start: "2023-03-01"
|
6
|
+
end: "2023-06-01"
|
7
|
+
account: "[Assets:Bank]"
|
8
|
+
transactions:
|
9
|
+
- description: Mortgage
|
10
|
+
category: "[Expenses:Mortgage]"
|
11
|
+
amount: 2,000.00
|
12
|
+
- description: Food
|
13
|
+
category: "[Expenses:Food]"
|
14
|
+
amount: 100.00
|
data/spec/yearly_spec.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative '../lib/hledger_forecast'
|
2
|
+
|
3
|
+
RSpec.describe 'generate' do
|
4
|
+
it 'generates a forecast with correct YEARLY transactions' do
|
5
|
+
transactions = File.read('spec/stubs/transactions.journal')
|
6
|
+
forecast = File.read('spec/stubs/yearly/forecast_yearly.yml')
|
7
|
+
|
8
|
+
generated_journal = HledgerForecast::Generator.create_journal_entries(transactions, forecast, '2023-03-01', '2024-04-30')
|
9
|
+
|
10
|
+
expected_output = File.read('spec/stubs/yearly/output_yearly.journal')
|
11
|
+
expect(generated_journal).to eq(expected_output)
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hledger-forecast
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Oli Morris
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-04-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: highline
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.1.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.1.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: money
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 6.16.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 6.16.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.12'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.12'
|
55
|
+
description: Uses a YAML file to generate monthly, quarterly, yearly and one-off transactions
|
56
|
+
for better forecasting in Hledger
|
57
|
+
email: olimorris@users.noreply.github.com
|
58
|
+
executables:
|
59
|
+
- hledger-forecast
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- ".github/workflows/ci.yml"
|
64
|
+
- ".gitignore"
|
65
|
+
- ".rubocop.yml"
|
66
|
+
- ".ruby-version"
|
67
|
+
- Gemfile
|
68
|
+
- Gemfile.lock
|
69
|
+
- LICENSE
|
70
|
+
- README.md
|
71
|
+
- bin/hledger-forecast
|
72
|
+
- example.yaml
|
73
|
+
- hledger-forecast.gemspec
|
74
|
+
- lib/hledger_forecast.rb
|
75
|
+
- lib/hledger_forecast/command.rb
|
76
|
+
- lib/hledger_forecast/generator.rb
|
77
|
+
- lib/hledger_forecast/options.rb
|
78
|
+
- lib/hledger_forecast/version.rb
|
79
|
+
- spec/monthly_spec.rb
|
80
|
+
- spec/once_spec.rb
|
81
|
+
- spec/quarterly_spec.rb
|
82
|
+
- spec/stubs/monthly/forecast_monthly.yml
|
83
|
+
- spec/stubs/monthly/forecast_monthly_enddate.yml
|
84
|
+
- spec/stubs/monthly/forecast_monthly_enddate_top.yml
|
85
|
+
- spec/stubs/monthly/forecast_monthly_modifier.yml
|
86
|
+
- spec/stubs/once/forecast_once.yml
|
87
|
+
- spec/stubs/quarterly/forecast_quarterly.yml
|
88
|
+
- spec/stubs/transactions.journal
|
89
|
+
- spec/stubs/yearly/forecast_yearly.yml
|
90
|
+
- spec/yearly_spec.rb
|
91
|
+
homepage: https://github.com/olimorris/hledger-forecast
|
92
|
+
licenses:
|
93
|
+
- MIT
|
94
|
+
metadata: {}
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 3.0.0
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
requirements: []
|
110
|
+
rubygems_version: 3.4.10
|
111
|
+
signing_key:
|
112
|
+
specification_version: 4
|
113
|
+
summary: Utility to generate forecasts in Hledger
|
114
|
+
test_files:
|
115
|
+
- spec/monthly_spec.rb
|
116
|
+
- spec/once_spec.rb
|
117
|
+
- spec/quarterly_spec.rb
|
118
|
+
- spec/stubs/monthly/forecast_monthly.yml
|
119
|
+
- spec/stubs/monthly/forecast_monthly_enddate.yml
|
120
|
+
- spec/stubs/monthly/forecast_monthly_enddate_top.yml
|
121
|
+
- spec/stubs/monthly/forecast_monthly_modifier.yml
|
122
|
+
- spec/stubs/once/forecast_once.yml
|
123
|
+
- spec/stubs/quarterly/forecast_quarterly.yml
|
124
|
+
- spec/stubs/transactions.journal
|
125
|
+
- spec/stubs/yearly/forecast_yearly.yml
|
126
|
+
- spec/yearly_spec.rb
|