hledger-forecast 0.1.0
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 +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
|
+

|
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
|