hledger-forecast 1.2.1 → 1.4.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 +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
|
|