hledger-forecast 1.2.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/README.md +209 -195
- 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 +26 -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/transactions/default.rb +47 -5
- 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
- data/spec/verbose_output_spec.rb +27 -0
- metadata +13 -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",
|
@@ -84,6 +91,11 @@ module HledgerForecast
|
|
84
91
|
options[:transaction_file] = file
|
85
92
|
end
|
86
93
|
|
94
|
+
opts.on("-v", "--verbose",
|
95
|
+
"Don't group transactions by type in the output file") do
|
96
|
+
options[:verbose] = true
|
97
|
+
end
|
98
|
+
|
87
99
|
opts.on("--force",
|
88
100
|
"Force an overwrite of the output file") do
|
89
101
|
options[:force] = true
|
@@ -100,7 +112,8 @@ module HledgerForecast
|
|
100
112
|
end
|
101
113
|
end.parse!(args)
|
102
114
|
|
103
|
-
options[:forecast_file] = "forecast.
|
115
|
+
options[:forecast_file] = "forecast.csv" unless options[:forecast_file]
|
116
|
+
options[:file_type] = "csv" unless options[:file_type]
|
104
117
|
options[:output_file] = "forecast.journal" unless options[:output_file]
|
105
118
|
|
106
119
|
options
|
@@ -114,7 +127,12 @@ module HledgerForecast
|
|
114
127
|
opts.separator ""
|
115
128
|
|
116
129
|
opts.on("-f", "--forecast FILE",
|
117
|
-
"The path to the FORECAST
|
130
|
+
"The path to the FORECAST csv/yml file to summarize") do |file|
|
131
|
+
options[:file_type] = if File.extname(file) == '.csv'
|
132
|
+
"csv"
|
133
|
+
else
|
134
|
+
"yml"
|
135
|
+
end
|
118
136
|
options[:forecast_file] = file
|
119
137
|
end
|
120
138
|
|
@@ -159,6 +177,7 @@ module HledgerForecast
|
|
159
177
|
forecast = File.read(options[:forecast_file])
|
160
178
|
|
161
179
|
begin
|
180
|
+
forecast = HledgerForecast::CSVParser.parse(forecast) if options[:file_type] == "csv"
|
162
181
|
transactions = Generator.generate(forecast, options)
|
163
182
|
rescue StandardError => e
|
164
183
|
puts "An error occurred while generating transactions: #{e.message}"
|
@@ -166,6 +185,7 @@ module HledgerForecast
|
|
166
185
|
end
|
167
186
|
|
168
187
|
output_file = options[:output_file]
|
188
|
+
|
169
189
|
if File.exist?(output_file) && !options[:force]
|
170
190
|
print "\nFile '#{output_file}' already exists. Overwrite? (y/n): "
|
171
191
|
overwrite = gets.chomp.downcase
|
@@ -184,6 +204,8 @@ module HledgerForecast
|
|
184
204
|
|
185
205
|
def self.summarize(options)
|
186
206
|
config = File.read(options[:forecast_file])
|
207
|
+
config = HledgerForecast::CSVParser.parse(config) if options[:file_type] == "csv"
|
208
|
+
|
187
209
|
summarizer = Summarizer.summarize(config, options)
|
188
210
|
|
189
211
|
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'],
|
@@ -33,14 +33,56 @@ module HledgerForecast
|
|
33
33
|
def process_block(block)
|
34
34
|
block[:transactions].each do |to, transactions|
|
35
35
|
to = get_header(block[:to], to)
|
36
|
-
block[:descriptions] = get_descriptions(transactions)
|
37
|
-
frequency = get_periodic_rules(block[:type], block[:frequency])
|
38
36
|
|
39
|
-
|
40
|
-
|
37
|
+
if block[:type] == "custom"
|
38
|
+
process_custom_transactions(block, to, transactions)
|
39
|
+
else
|
40
|
+
process_standard_transactions(block, to, transactions)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def process_custom_transactions(block, to, transactions)
|
46
|
+
transactions.each do |t|
|
47
|
+
frequency = get_periodic_rules(block[:type], t[:frequency])
|
48
|
+
|
49
|
+
header = build_header(block, to, frequency, t[:description])
|
50
|
+
footer = build_footer(block)
|
51
|
+
output << build_transaction(header, [t], footer)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_standard_transactions(block, to, transactions)
|
56
|
+
if @options[:verbose]
|
57
|
+
transactions.map do |t|
|
58
|
+
# Skip transactions that have been marked as tracked
|
59
|
+
next if t[:track]
|
41
60
|
|
42
|
-
|
61
|
+
frequency = get_periodic_rules(block[:type], block[:frequency])
|
62
|
+
header = build_header(block, to, frequency, t[:description])
|
63
|
+
footer = build_footer(block)
|
64
|
+
output << build_transaction(header, [t], footer)
|
65
|
+
end
|
66
|
+
return
|
43
67
|
end
|
68
|
+
|
69
|
+
block[:descriptions] = get_descriptions(transactions)
|
70
|
+
frequency = get_periodic_rules(block[:type], block[:frequency])
|
71
|
+
header = build_header(block, to, frequency, block[:descriptions])
|
72
|
+
footer = build_footer(block)
|
73
|
+
output << build_transaction(header, transactions, footer)
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_header(block, to, frequency, description)
|
77
|
+
"#{frequency} #{block[:from]}#{to} * #{description}\n"
|
78
|
+
end
|
79
|
+
|
80
|
+
def build_footer(block)
|
81
|
+
" #{block[:account]}\n\n"
|
82
|
+
end
|
83
|
+
|
84
|
+
def build_transaction(header, transactions, footer)
|
85
|
+
{ header: header, transactions: write_transactions(transactions), footer: footer }
|
44
86
|
end
|
45
87
|
|
46
88
|
def get_header(block, transaction)
|
@@ -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
|
|