hledger-forecast 1.2.0 → 1.3.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 +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/README.md +207 -196
- data/example.csv +16 -0
- data/example.journal +19 -23
- data/example.yml +8 -3
- data/lib/hledger_forecast/calculator.rb +1 -1
- data/lib/hledger_forecast/cli.rb +21 -4
- data/lib/hledger_forecast/csv_parser.rb +106 -0
- data/lib/hledger_forecast/generator.rb +2 -1
- data/lib/hledger_forecast/summarizer.rb +1 -1
- data/lib/hledger_forecast/summarizer_formatter.rb +11 -3
- data/lib/hledger_forecast/transactions/default.rb +18 -1
- data/lib/hledger_forecast/transactions/trackers.rb +7 -0
- data/lib/hledger_forecast/version.rb +1 -1
- data/lib/hledger_forecast.rb +2 -1
- data/spec/command_spec.rb +9 -1
- data/spec/csv_and_yml_comparison_spec.rb +32 -0
- data/spec/csv_parser_spec.rb +110 -0
- data/spec/custom_spec.rb +6 -9
- data/spec/stubs/forecast.csv +5 -0
- data/spec/summarizer_spec.rb +7 -7
- metadata +11 -3
data/example.journal
CHANGED
|
@@ -1,51 +1,47 @@
|
|
|
1
1
|
~ monthly from 2023-03-01 * Salary, Bills, Food, New Kitchen
|
|
2
|
-
Income:Salary
|
|
3
|
-
Expenses:Bills
|
|
4
|
-
Expenses:Food
|
|
5
|
-
Expenses:House
|
|
2
|
+
Income:Salary $-3,500.00; Salary
|
|
3
|
+
Expenses:Bills $175.00 ; Bills
|
|
4
|
+
Expenses:Food $500.00 ; Food
|
|
5
|
+
Expenses:House $208.33 ; New Kitchen
|
|
6
6
|
Assets:Bank
|
|
7
7
|
|
|
8
8
|
~ monthly from 2023-03-01 to 2025-01-01 * Mortgage
|
|
9
|
-
Expenses:Mortgage
|
|
9
|
+
Expenses:Mortgage $2,000.00 ; Mortgage
|
|
10
10
|
Assets:Bank
|
|
11
11
|
|
|
12
12
|
~ monthly from 2023-03-01 to 2024-02-29 * Holiday
|
|
13
|
-
Expenses:Holiday
|
|
13
|
+
Expenses:Holiday $125.00 ; Holiday
|
|
14
14
|
Assets:Bank
|
|
15
15
|
|
|
16
|
-
~ monthly from 2023-03-01 to 2025-
|
|
17
|
-
Assets:Savings
|
|
16
|
+
~ monthly from 2023-03-01 to 2025-03-01 * Rainy day fund
|
|
17
|
+
Assets:Savings $300.00 ; Rainy day fund
|
|
18
18
|
Assets:Bank
|
|
19
19
|
|
|
20
20
|
~ monthly from 2024-01-01 * Pension draw down
|
|
21
|
-
Income:Pension
|
|
21
|
+
Income:Pension $-500.00 ; Pension draw down
|
|
22
22
|
Assets:Pension
|
|
23
23
|
|
|
24
24
|
~ every 3 months from 2023-04-01 * Quarterly bonus
|
|
25
|
-
Income:Bonus
|
|
25
|
+
Income:Bonus $-1,000.00; Quarterly bonus
|
|
26
26
|
Assets:Bank
|
|
27
27
|
|
|
28
28
|
~ every 6 months from 2023-04-01 * Top up holiday funds
|
|
29
|
-
Expenses:Holiday
|
|
29
|
+
Expenses:Holiday $500.00 ; Top up holiday funds
|
|
30
30
|
Assets:Bank
|
|
31
31
|
|
|
32
|
-
~ yearly from 2023-04-01 * Annual
|
|
33
|
-
Income:Bonus
|
|
32
|
+
~ yearly from 2023-04-01 * Annual bonus
|
|
33
|
+
Income:Bonus $-2,000.00; Annual bonus
|
|
34
34
|
Assets:Bank
|
|
35
35
|
|
|
36
36
|
~ every 2 weeks from 2023-03-01 * Hair and beauty
|
|
37
|
-
Expenses:Personal Care
|
|
37
|
+
Expenses:Personal Care $80.00 ; Hair and beauty
|
|
38
38
|
Assets:Bank
|
|
39
39
|
|
|
40
|
-
~ 2023-
|
|
41
|
-
Expenses:
|
|
40
|
+
~ every 5 weeks from 2023-03-01 * Misc expenses
|
|
41
|
+
Expenses:General Expenses $30.00 ; Misc expenses
|
|
42
42
|
Assets:Bank
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
Expenses:
|
|
46
|
-
Assets:Bank
|
|
47
|
-
|
|
48
|
-
= Expenses:Food date:2025-01-01..2025-12-31
|
|
49
|
-
Expenses:Food *0.05 ; Food - Inflation
|
|
50
|
-
Assets:Bank *-0.05
|
|
44
|
+
~ 2023-07-01 * [TRACKED] Refund for that damn laptop
|
|
45
|
+
Expenses:Shopping $-3,000.00; Refund for that damn laptop
|
|
46
|
+
Assets:Bank
|
|
51
47
|
|
data/example.yml
CHANGED
|
@@ -80,14 +80,19 @@ once:
|
|
|
80
80
|
track: true
|
|
81
81
|
|
|
82
82
|
custom:
|
|
83
|
-
-
|
|
84
|
-
account: "Assets:Bank"
|
|
83
|
+
- account: "Assets:Bank"
|
|
85
84
|
from: "2023-03-01"
|
|
86
|
-
roll-up: 26
|
|
87
85
|
transactions:
|
|
88
86
|
- amount: 80
|
|
89
87
|
category: "Expenses:Personal Care"
|
|
90
88
|
description: Hair and beauty
|
|
89
|
+
frequency: "every 2 weeks"
|
|
90
|
+
roll-up: 26
|
|
91
|
+
- amount: 30
|
|
92
|
+
category: "Expenses:General Expenses"
|
|
93
|
+
description: Misc expenses
|
|
94
|
+
frequency: "every 5 weeks"
|
|
95
|
+
roll-up: 10.4
|
|
91
96
|
|
|
92
97
|
settings:
|
|
93
98
|
currency: USD
|
data/lib/hledger_forecast/cli.rb
CHANGED
|
@@ -69,9 +69,16 @@ module HledgerForecast
|
|
|
69
69
|
opts.separator ""
|
|
70
70
|
|
|
71
71
|
opts.on("-f", "--forecast FILE",
|
|
72
|
-
"The path to the FORECAST
|
|
72
|
+
"The path to the FORECAST csv/yml file to generate from") do |file|
|
|
73
73
|
options[:forecast_file] = file
|
|
74
|
-
|
|
74
|
+
|
|
75
|
+
options[:file_type] = if File.extname(file) == '.csv'
|
|
76
|
+
"csv"
|
|
77
|
+
else
|
|
78
|
+
"yml"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
options[:output_file] ||= file.sub(options[:file_type], 'journal')
|
|
75
82
|
end
|
|
76
83
|
|
|
77
84
|
opts.on("-o", "--output-file FILE",
|
|
@@ -100,7 +107,8 @@ module HledgerForecast
|
|
|
100
107
|
end
|
|
101
108
|
end.parse!(args)
|
|
102
109
|
|
|
103
|
-
options[:forecast_file] = "forecast.
|
|
110
|
+
options[:forecast_file] = "forecast.csv" unless options[:forecast_file]
|
|
111
|
+
options[:file_type] = "csv" unless options[:file_type]
|
|
104
112
|
options[:output_file] = "forecast.journal" unless options[:output_file]
|
|
105
113
|
|
|
106
114
|
options
|
|
@@ -114,7 +122,12 @@ module HledgerForecast
|
|
|
114
122
|
opts.separator ""
|
|
115
123
|
|
|
116
124
|
opts.on("-f", "--forecast FILE",
|
|
117
|
-
"The path to the FORECAST
|
|
125
|
+
"The path to the FORECAST csv/yml file to summarize") do |file|
|
|
126
|
+
options[:file_type] = if File.extname(file) == '.csv'
|
|
127
|
+
"csv"
|
|
128
|
+
else
|
|
129
|
+
"yml"
|
|
130
|
+
end
|
|
118
131
|
options[:forecast_file] = file
|
|
119
132
|
end
|
|
120
133
|
|
|
@@ -159,6 +172,7 @@ module HledgerForecast
|
|
|
159
172
|
forecast = File.read(options[:forecast_file])
|
|
160
173
|
|
|
161
174
|
begin
|
|
175
|
+
forecast = HledgerForecast::CSVParser.parse(forecast) if options[:file_type] == "csv"
|
|
162
176
|
transactions = Generator.generate(forecast, options)
|
|
163
177
|
rescue StandardError => e
|
|
164
178
|
puts "An error occurred while generating transactions: #{e.message}"
|
|
@@ -166,6 +180,7 @@ module HledgerForecast
|
|
|
166
180
|
end
|
|
167
181
|
|
|
168
182
|
output_file = options[:output_file]
|
|
183
|
+
|
|
169
184
|
if File.exist?(output_file) && !options[:force]
|
|
170
185
|
print "\nFile '#{output_file}' already exists. Overwrite? (y/n): "
|
|
171
186
|
overwrite = gets.chomp.downcase
|
|
@@ -184,6 +199,8 @@ module HledgerForecast
|
|
|
184
199
|
|
|
185
200
|
def self.summarize(options)
|
|
186
201
|
config = File.read(options[:forecast_file])
|
|
202
|
+
config = HledgerForecast::CSVParser.parse(config) if options[:file_type] == "csv"
|
|
203
|
+
|
|
187
204
|
summarizer = Summarizer.summarize(config, options)
|
|
188
205
|
|
|
189
206
|
puts SummarizerFormatter.format(summarizer[:output], summarizer[:settings])
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module HledgerForecast
|
|
2
|
+
# Formats various items used throughout the application
|
|
3
|
+
class CSVParser
|
|
4
|
+
def self.parse(csv_data, cli_options = nil)
|
|
5
|
+
new.parse(csv_data, cli_options)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def parse(csv_data, _cli_options)
|
|
9
|
+
csv_data = CSV.parse(csv_data, headers: true)
|
|
10
|
+
yaml_data = {}
|
|
11
|
+
group_by_type(csv_data, yaml_data)
|
|
12
|
+
yaml_data.to_yaml
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def group_by_type(csv_data, yaml_data)
|
|
18
|
+
csv_data.group_by { |row| row['type'] }.each do |type, rows|
|
|
19
|
+
if type == 'settings'
|
|
20
|
+
handle_settings(rows, yaml_data)
|
|
21
|
+
else
|
|
22
|
+
yaml_data[type] ||= []
|
|
23
|
+
group_by_account_and_from(rows, yaml_data[type], type)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def handle_settings(rows, yaml_data)
|
|
29
|
+
yaml_data['settings'] ||= {}
|
|
30
|
+
rows.each do |row|
|
|
31
|
+
yaml_data['settings'][row['frequency']] = cast_to_proper_type(row['account'])
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def group_by_account_and_from(rows, yaml_rows, type)
|
|
36
|
+
rows.group_by { |row| [row['account'], row['from']] }.each do |(account, from), transactions|
|
|
37
|
+
yaml_rows << if type == 'custom'
|
|
38
|
+
build_custom_transaction(account, from, transactions)
|
|
39
|
+
else
|
|
40
|
+
build_transaction(account, from, transactions)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_transaction(account, from, transactions)
|
|
46
|
+
transaction = {
|
|
47
|
+
'account' => account,
|
|
48
|
+
'from' => Date.parse(from).strftime('%Y-%m-%d'),
|
|
49
|
+
'transactions' => []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
transactions.each do |row|
|
|
53
|
+
transaction['transactions'] << build_transaction_data(row)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
transaction
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_custom_transaction(account, from, transactions)
|
|
60
|
+
transaction = {
|
|
61
|
+
'account' => account,
|
|
62
|
+
'from' => Date.parse(from).strftime('%Y-%m-%d'),
|
|
63
|
+
'transactions' => []
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
transactions.each do |row|
|
|
67
|
+
transaction_data = build_transaction_data(row)
|
|
68
|
+
transaction_data['frequency'] = row['frequency']
|
|
69
|
+
transaction_data['roll-up'] = row['roll-up'].to_f if row['roll-up']
|
|
70
|
+
transaction['transactions'] << transaction_data
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
transaction
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def build_transaction_data(row)
|
|
77
|
+
transaction_data = {
|
|
78
|
+
'amount' => row['amount'].start_with?("=") ? row['amount'].to_s : row['amount'].to_f,
|
|
79
|
+
'category' => row['category'],
|
|
80
|
+
'description' => row['description']
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if row['to']
|
|
84
|
+
transaction_data['to'] = if row['to'].start_with?("=")
|
|
85
|
+
row['to']
|
|
86
|
+
else
|
|
87
|
+
Date.parse(row['to']).strftime('%Y-%m-%d')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
transaction_data['summary_exclude'] = true if row['summary_exclude'] && row['summary_exclude'].downcase == "true"
|
|
92
|
+
transaction_data['track'] = true if row['track'] && row['track'].downcase == "true"
|
|
93
|
+
|
|
94
|
+
transaction_data
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def cast_to_proper_type(str)
|
|
98
|
+
case str.downcase
|
|
99
|
+
when 'true', 'false'
|
|
100
|
+
str.downcase == 'true'
|
|
101
|
+
else
|
|
102
|
+
str
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -55,7 +55,8 @@ module HledgerForecast
|
|
|
55
55
|
description: t['description'],
|
|
56
56
|
to: t['to'] ? Calculator.new.evaluate_date(Date.parse(block['from']), t['to']) : nil,
|
|
57
57
|
modifiers: t['modifiers'] ? Transactions::Modifiers.get_modifiers(t, block) : [],
|
|
58
|
-
track: Transactions::Trackers.track?(t, block, @settings) ? true : false
|
|
58
|
+
track: Transactions::Trackers.track?(t, block, @settings) ? true : false,
|
|
59
|
+
frequency: t['frequency'] || nil
|
|
59
60
|
}
|
|
60
61
|
end
|
|
61
62
|
|
|
@@ -58,7 +58,7 @@ module HledgerForecast
|
|
|
58
58
|
|
|
59
59
|
output.last[:transactions] << {
|
|
60
60
|
amount: amount,
|
|
61
|
-
annualised_amount: amount * (
|
|
61
|
+
annualised_amount: amount * (t['roll-up'] || annualise(period)),
|
|
62
62
|
rolled_up_amount: 0,
|
|
63
63
|
category: t['category'],
|
|
64
64
|
exclude: t['summary_exclude'],
|
|
@@ -85,9 +85,17 @@ module HledgerForecast
|
|
|
85
85
|
# Sort the array
|
|
86
86
|
sorted_sums = sort_roll_up(sum_hash, :sum)
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
if @settings[:verbose]
|
|
89
|
+
sorted_sums.each do |hash|
|
|
90
|
+
@table.add_row [{ value: hash[:category], colspan: 1, alignment: :left },
|
|
91
|
+
{ value: hash[:descriptions], colspan: 1, alignment: :left },
|
|
92
|
+
{ value: format_amount(hash[:sum]), alignment: :right }]
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
sorted_sums.each do |hash|
|
|
96
|
+
@table.add_row [{ value: hash[:category], colspan: 2, alignment: :left },
|
|
97
|
+
{ value: format_amount(hash[:sum]), alignment: :right }]
|
|
98
|
+
end
|
|
91
99
|
end
|
|
92
100
|
end
|
|
93
101
|
|
|
@@ -13,7 +13,11 @@ module HledgerForecast
|
|
|
13
13
|
def generate
|
|
14
14
|
data.each_value do |blocks|
|
|
15
15
|
blocks.each do |block|
|
|
16
|
-
|
|
16
|
+
if block[:type] == "custom"
|
|
17
|
+
process_custom_block(block)
|
|
18
|
+
else
|
|
19
|
+
process_block(block)
|
|
20
|
+
end
|
|
17
21
|
end
|
|
18
22
|
end
|
|
19
23
|
|
|
@@ -30,10 +34,23 @@ module HledgerForecast
|
|
|
30
34
|
@output = []
|
|
31
35
|
end
|
|
32
36
|
|
|
37
|
+
def process_custom_block(block)
|
|
38
|
+
block[:transactions].each do |to, transactions|
|
|
39
|
+
to = get_header(block[:to], to)
|
|
40
|
+
|
|
41
|
+
transactions.each do |t|
|
|
42
|
+
header = "~ #{t[:frequency]} from #{block[:from]}#{to} * #{t[:description]}\n"
|
|
43
|
+
footer = " #{block[:account]}\n\n"
|
|
44
|
+
output << { header: header, transactions: write_transactions([t]), footer: footer }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
33
49
|
def process_block(block)
|
|
34
50
|
block[:transactions].each do |to, transactions|
|
|
35
51
|
to = get_header(block[:to], to)
|
|
36
52
|
block[:descriptions] = get_descriptions(transactions)
|
|
53
|
+
|
|
37
54
|
frequency = get_periodic_rules(block[:type], block[:frequency])
|
|
38
55
|
|
|
39
56
|
header = "#{frequency} #{block[:from]}#{to} * #{block[:descriptions]}\n"
|
|
@@ -32,6 +32,13 @@ module HledgerForecast
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def self.exists?(transaction, account, from, to, options)
|
|
35
|
+
|
|
36
|
+
if !options[:transaction_file]
|
|
37
|
+
puts "\nWarning: ".bold.yellow + "For tracked transactions, please specify a file with the `-t` flag"
|
|
38
|
+
puts "ERROR: ".bold.red + "Tracked transactions ignored for now"
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
35
42
|
# Format the money
|
|
36
43
|
amount = Formatter.format_money(transaction['amount'], options)
|
|
37
44
|
inverse_amount = Formatter.format_money(transaction['amount'] * -1, options)
|
data/lib/hledger_forecast.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
3
|
require 'colorize'
|
|
4
|
+
require 'csv'
|
|
4
5
|
require 'date'
|
|
5
6
|
require 'dentaku'
|
|
6
7
|
require 'highline'
|
|
@@ -14,6 +15,7 @@ Money.rounding_mode = BigDecimal::ROUND_HALF_UP
|
|
|
14
15
|
|
|
15
16
|
require_relative 'hledger_forecast/calculator'
|
|
16
17
|
require_relative 'hledger_forecast/cli'
|
|
18
|
+
require_relative 'hledger_forecast/csv_parser'
|
|
17
19
|
require_relative 'hledger_forecast/formatter'
|
|
18
20
|
require_relative 'hledger_forecast/generator'
|
|
19
21
|
require_relative 'hledger_forecast/settings'
|
|
@@ -24,4 +26,3 @@ require_relative 'hledger_forecast/version'
|
|
|
24
26
|
require_relative 'hledger_forecast/transactions/default'
|
|
25
27
|
require_relative 'hledger_forecast/transactions/modifiers'
|
|
26
28
|
require_relative 'hledger_forecast/transactions/trackers'
|
|
27
|
-
|
data/spec/command_spec.rb
CHANGED
|
@@ -14,7 +14,6 @@ JOURNAL
|
|
|
14
14
|
|
|
15
15
|
RSpec.describe 'command' do
|
|
16
16
|
it 'uses the CLI to generate an output' do
|
|
17
|
-
# Delete the file if it exists
|
|
18
17
|
generated_journal = './test_output.journal'
|
|
19
18
|
File.delete(generated_journal) if File.exist?(generated_journal)
|
|
20
19
|
|
|
@@ -22,4 +21,13 @@ RSpec.describe 'command' do
|
|
|
22
21
|
|
|
23
22
|
expect(File.read(generated_journal)).to eq(output)
|
|
24
23
|
end
|
|
24
|
+
|
|
25
|
+
it 'uses the CLI to generate an output with a CSV config file' do
|
|
26
|
+
generated_journal = './test_output.journal'
|
|
27
|
+
File.delete(generated_journal) if File.exist?(generated_journal)
|
|
28
|
+
|
|
29
|
+
system("./bin/hledger-forecast generate -f ./spec/stubs/forecast.csv -o ./test_output.journal --force")
|
|
30
|
+
|
|
31
|
+
expect(File.read(generated_journal)).to eq(output)
|
|
32
|
+
end
|
|
25
33
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require_relative '../lib/hledger_forecast'
|
|
2
|
+
|
|
3
|
+
RSpec.describe 'CSV and yml outputs' do
|
|
4
|
+
it 'should return the same value when ran through hledger' do
|
|
5
|
+
generated_journal = './test_output.journal'
|
|
6
|
+
File.delete(generated_journal) if File.exist?(generated_journal)
|
|
7
|
+
|
|
8
|
+
system("./bin/hledger-forecast generate -f example.csv -o ./test_output.journal -t ./spec/stubs/transactions_not_found.journal --force")
|
|
9
|
+
csv_output = `hledger -f ./test_output.journal --forecast bal -b=2023-01 -e=2023-06`
|
|
10
|
+
|
|
11
|
+
system("./bin/hledger-forecast generate -f example.yml -o ./test_output.journal -t ./spec/stubs/transactions_not_found.journal --force")
|
|
12
|
+
yml_output = `hledger -f ./test_output.journal --forecast bal -b=2023-01 -e=2023-06`
|
|
13
|
+
|
|
14
|
+
expect(csv_output).to eq(yml_output)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# it 'check that it can fail!' do
|
|
18
|
+
# generated_journal = './test_output.journal'
|
|
19
|
+
# File.delete(generated_journal) if File.exist?(generated_journal)
|
|
20
|
+
#
|
|
21
|
+
# system("./bin/hledger-forecast generate -f ./spec/stubs/csv_and_yml/forecast.csv -o ./test_output.journal --force > /dev/null 2>&1")
|
|
22
|
+
#
|
|
23
|
+
# ### CHANGE DATE!!!!!!!!!!!!!!!!
|
|
24
|
+
# csv_output = `hledger -f ./test_output.journal --forecast bal -b=2023-01 -e=2023-03`
|
|
25
|
+
# ### CHANGE DATE!!!!!!!!!!!!!!!!
|
|
26
|
+
#
|
|
27
|
+
# system("./bin/hledger-forecast generate -f ./spec/stubs/csv_and_yml/forecast.yml -o ./test_output.journal --force > /dev/null 2>&1")
|
|
28
|
+
# yml_output = `hledger -f ./test_output.journal --forecast bal -b=2023-01 -e=2023-06`
|
|
29
|
+
#
|
|
30
|
+
# expect(csv_output).not_to eq(yml_output)
|
|
31
|
+
# end
|
|
32
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require_relative '../lib/hledger_forecast'
|
|
2
|
+
|
|
3
|
+
input = <<~CSV
|
|
4
|
+
type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
|
|
5
|
+
monthly,,Assets:Bank,01/03/2023,,Salary,Income:Salary,-3500,,,
|
|
6
|
+
monthly,,Assets:Bank,01/03/2023,01/01/2025,Mortgage,Expenses:Mortgage,2000,,,
|
|
7
|
+
monthly,,Assets:Bank,01/03/2023,,Bills,Expenses:Bills,175,,,
|
|
8
|
+
monthly,,Assets:Bank,01/03/2023,,Food,Expenses:Food,500,,,
|
|
9
|
+
monthly,,Assets:Bank,01/03/2023,,New Kitchen,Expenses:House,=5000/24,,,
|
|
10
|
+
monthly,,Assets:Bank,01/03/2023,=12,Holiday,Expenses:Holiday,125,,,
|
|
11
|
+
monthly,,Assets:Bank,01/03/2023,01/01/2025,Rainy day fund,Assets:Savings,300,,,
|
|
12
|
+
monthly,,Assets:Pension,01/01/2024,,Pension draw down,Income:Pension,-500,,,
|
|
13
|
+
quarterly,,Assets:Bank,01/04/2023,,Quarterly bonus,Income:Bonus,-1000,,,
|
|
14
|
+
half-yearly,,Assets:Bank,01/04/2023,,Top up holiday funds,Expenses:Holiday,500,,,
|
|
15
|
+
yearly,,Assets:Bank,01/04/2023,,Annual bonus,Income:Bonus,-2000,,,
|
|
16
|
+
once,,Assets:Bank,05/03/2023,,Refund for that damn laptop,Expenses:Shopping,-3000,,TRUE,TRUE
|
|
17
|
+
custom,every 2 weeks,Assets:Bank,01/03/2023,,Hair and beauty,Expenses:Personal Care,80,26,,
|
|
18
|
+
settings,currency,USD,,,,,,,,
|
|
19
|
+
settings,show_symbol,TRUE,,,,,,,,
|
|
20
|
+
settings,thousands_separator,TRUE,,,,,,,,
|
|
21
|
+
CSV
|
|
22
|
+
|
|
23
|
+
output = <<~YAML
|
|
24
|
+
---
|
|
25
|
+
monthly:
|
|
26
|
+
- account: Assets:Bank
|
|
27
|
+
from: '2023-03-01'
|
|
28
|
+
transactions:
|
|
29
|
+
- amount: -3500.0
|
|
30
|
+
category: Income:Salary
|
|
31
|
+
description: Salary
|
|
32
|
+
- amount: 2000.0
|
|
33
|
+
category: Expenses:Mortgage
|
|
34
|
+
description: Mortgage
|
|
35
|
+
to: '2025-01-01'
|
|
36
|
+
- amount: 175.0
|
|
37
|
+
category: Expenses:Bills
|
|
38
|
+
description: Bills
|
|
39
|
+
- amount: 500.0
|
|
40
|
+
category: Expenses:Food
|
|
41
|
+
description: Food
|
|
42
|
+
- amount: "=5000/24"
|
|
43
|
+
category: Expenses:House
|
|
44
|
+
description: New Kitchen
|
|
45
|
+
- amount: 125.0
|
|
46
|
+
category: Expenses:Holiday
|
|
47
|
+
description: Holiday
|
|
48
|
+
to: "=12"
|
|
49
|
+
- amount: 300.0
|
|
50
|
+
category: Assets:Savings
|
|
51
|
+
description: Rainy day fund
|
|
52
|
+
to: '2025-01-01'
|
|
53
|
+
- account: Assets:Pension
|
|
54
|
+
from: '2024-01-01'
|
|
55
|
+
transactions:
|
|
56
|
+
- amount: -500.0
|
|
57
|
+
category: Income:Pension
|
|
58
|
+
description: Pension draw down
|
|
59
|
+
quarterly:
|
|
60
|
+
- account: Assets:Bank
|
|
61
|
+
from: '2023-04-01'
|
|
62
|
+
transactions:
|
|
63
|
+
- amount: -1000.0
|
|
64
|
+
category: Income:Bonus
|
|
65
|
+
description: Quarterly bonus
|
|
66
|
+
half-yearly:
|
|
67
|
+
- account: Assets:Bank
|
|
68
|
+
from: '2023-04-01'
|
|
69
|
+
transactions:
|
|
70
|
+
- amount: 500.0
|
|
71
|
+
category: Expenses:Holiday
|
|
72
|
+
description: Top up holiday funds
|
|
73
|
+
yearly:
|
|
74
|
+
- account: Assets:Bank
|
|
75
|
+
from: '2023-04-01'
|
|
76
|
+
transactions:
|
|
77
|
+
- amount: -2000.0
|
|
78
|
+
category: Income:Bonus
|
|
79
|
+
description: Annual bonus
|
|
80
|
+
once:
|
|
81
|
+
- account: Assets:Bank
|
|
82
|
+
from: '2023-03-05'
|
|
83
|
+
transactions:
|
|
84
|
+
- amount: -3000.0
|
|
85
|
+
category: Expenses:Shopping
|
|
86
|
+
description: Refund for that damn laptop
|
|
87
|
+
summary_exclude: true
|
|
88
|
+
track: true
|
|
89
|
+
custom:
|
|
90
|
+
- account: Assets:Bank
|
|
91
|
+
from: '2023-03-01'
|
|
92
|
+
transactions:
|
|
93
|
+
- amount: 80.0
|
|
94
|
+
category: Expenses:Personal Care
|
|
95
|
+
description: Hair and beauty
|
|
96
|
+
frequency: every 2 weeks
|
|
97
|
+
roll-up: 26.0
|
|
98
|
+
settings:
|
|
99
|
+
currency: USD
|
|
100
|
+
show_symbol: true
|
|
101
|
+
thousands_separator: true
|
|
102
|
+
YAML
|
|
103
|
+
|
|
104
|
+
RSpec.describe 'CSV parser' do
|
|
105
|
+
it 'converts a CSV file to the YML output needed for the plugin' do
|
|
106
|
+
computed_yaml = HledgerForecast::CSVParser.parse(input)
|
|
107
|
+
|
|
108
|
+
expect(computed_yaml).to eq(output)
|
|
109
|
+
end
|
|
110
|
+
end
|
data/spec/custom_spec.rb
CHANGED
|
@@ -2,20 +2,17 @@ require_relative '../lib/hledger_forecast'
|
|
|
2
2
|
|
|
3
3
|
base_config = <<~YAML
|
|
4
4
|
custom:
|
|
5
|
-
-
|
|
5
|
+
- account: "[Assets:Bank]"
|
|
6
6
|
from: "2023-05-01"
|
|
7
|
-
account: "[Assets:Bank]"
|
|
8
7
|
transactions:
|
|
9
8
|
- amount: 80
|
|
10
9
|
category: "[Expenses:Personal Care]"
|
|
11
10
|
description: Hair and beauty
|
|
12
|
-
|
|
13
|
-
from: "2023-05-01"
|
|
14
|
-
account: "[Assets:Checking]"
|
|
15
|
-
transactions:
|
|
11
|
+
frequency: "every 2 weeks"
|
|
16
12
|
- amount: 50
|
|
17
13
|
category: "[Expenses:Groceries]"
|
|
18
14
|
description: Gotta feed that stomach
|
|
15
|
+
frequency: "every 5 days"
|
|
19
16
|
|
|
20
17
|
settings:
|
|
21
18
|
currency: GBP
|
|
@@ -28,19 +25,19 @@ base_output = <<~JOURNAL
|
|
|
28
25
|
|
|
29
26
|
~ every 5 days from 2023-05-01 * Gotta feed that stomach
|
|
30
27
|
[Expenses:Groceries] £50.00; Gotta feed that stomach
|
|
31
|
-
[Assets:
|
|
28
|
+
[Assets:Bank]
|
|
32
29
|
|
|
33
30
|
JOURNAL
|
|
34
31
|
|
|
35
32
|
calculated_config = <<~YAML
|
|
36
33
|
custom:
|
|
37
|
-
-
|
|
34
|
+
- account: "[Assets:Bank]"
|
|
38
35
|
from: "2023-05-01"
|
|
39
|
-
account: "[Assets:Bank]"
|
|
40
36
|
transactions:
|
|
41
37
|
- amount: 80
|
|
42
38
|
category: "[Expenses:Personal Care]"
|
|
43
39
|
description: Hair and beauty
|
|
40
|
+
frequency: "every 2 weeks"
|
|
44
41
|
to: "=6"
|
|
45
42
|
|
|
46
43
|
settings:
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
|
|
2
|
+
monthly,,Assets:Bank,01/03/2023,,Mortgage,Expenses:Mortgage,2000.55,,,
|
|
3
|
+
monthly,,Assets:Bank,01/03/2023,,Food,Expenses:Food,100,,,
|
|
4
|
+
monthly,,Assets:Savings,01/03/2023,,Savings,Assets:Bank,-1000,,,
|
|
5
|
+
settings,currency,GBP,,,,,,,,
|
data/spec/summarizer_spec.rb
CHANGED
|
@@ -20,22 +20,22 @@ config = <<~YAML
|
|
|
20
20
|
description: Savings
|
|
21
21
|
|
|
22
22
|
custom:
|
|
23
|
-
-
|
|
23
|
+
- account: "[Assets:Bank]"
|
|
24
24
|
from: "2023-05-01"
|
|
25
|
-
account: "[Assets:Bank]"
|
|
26
|
-
roll-up: 26
|
|
27
25
|
transactions:
|
|
28
26
|
- amount: 80
|
|
29
27
|
category: "[Expenses:Personal Care]"
|
|
30
28
|
description: Hair and beauty
|
|
31
|
-
|
|
29
|
+
frequency: "every 2 weeks"
|
|
30
|
+
roll-up: 26
|
|
31
|
+
- account: "[Assets:Checking]"
|
|
32
32
|
from: "2023-05-01"
|
|
33
|
-
account: "[Assets:Checking]"
|
|
34
|
-
roll-up: 73
|
|
35
33
|
transactions:
|
|
36
34
|
- amount: 50
|
|
37
35
|
category: "[Expenses:Groceries]"
|
|
38
36
|
description: Gotta feed that stomach
|
|
37
|
+
frequency: "every 5 days"
|
|
38
|
+
roll-up: 73
|
|
39
39
|
|
|
40
40
|
settings:
|
|
41
41
|
currency: GBP
|
|
@@ -57,7 +57,7 @@ RSpec.describe HledgerForecast::Summarizer do
|
|
|
57
57
|
|
|
58
58
|
expect(output.first).to include(:account, :from, :to, :type, :frequency)
|
|
59
59
|
expect(output.first[:amount]).to eq(2000.55)
|
|
60
|
-
expect(output.last[:rolled_up_amount]).to eq(
|
|
60
|
+
expect(output.last[:rolled_up_amount]).to eq((50.0 * 73.0) / 12.0) # ((50 * 73) / 12)
|
|
61
61
|
expect(output.length).to eq(5)
|
|
62
62
|
end
|
|
63
63
|
|