hledger-forecast 1.5.1 → 2.0.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/release.yml +22 -0
- data/.github/workflows/{ci.yml → test.yml} +5 -5
- data/.rubocop.yml +17 -17
- data/README.md +8 -172
- data/example.journal +14 -14
- data/hledger-forecast.gemspec +1 -1
- data/lib/hledger_forecast/calculator.rb +5 -1
- data/lib/hledger_forecast/cli.rb +5 -18
- data/lib/hledger_forecast/generator.rb +52 -35
- data/lib/hledger_forecast/settings.rb +42 -27
- data/lib/hledger_forecast/summarizer.rb +28 -62
- data/lib/hledger_forecast/summarizer_formatter.rb +12 -3
- data/lib/hledger_forecast/transactions/default.rb +28 -57
- data/lib/hledger_forecast/transactions/trackers.rb +34 -40
- data/lib/hledger_forecast/utilities.rb +14 -0
- data/lib/hledger_forecast/version.rb +1 -1
- data/lib/hledger_forecast.rb +1 -2
- data/spec/cli_spec.rb +3 -12
- data/spec/computed_amounts_spec.rb +11 -22
- data/spec/custom_spec.rb +15 -35
- data/spec/half-yearly_spec.rb +6 -13
- data/spec/monthly_end_date_spec.rb +8 -18
- data/spec/monthly_end_date_transaction_spec.rb +20 -45
- data/spec/monthly_spec.rb +11 -28
- data/spec/once_spec.rb +6 -13
- data/spec/quarterly_spec.rb +5 -12
- data/spec/summarizer_spec.rb +11 -42
- data/spec/track_spec.rb +19 -49
- data/spec/verbose_output_spec.rb +3 -3
- data/spec/yearly_spec.rb +5 -12
- metadata +10 -18
- data/example.yml +0 -98
- data/lib/hledger_forecast/csv_parser.rb +0 -106
- data/spec/csv_and_yml_comparison_spec.rb +0 -32
- data/spec/csv_parser_spec.rb +0 -110
- data/spec/modifier_spec.rb +0 -102
- data/spec/stubs/forecast.yml +0 -19
@@ -1,41 +1,56 @@
|
|
1
1
|
module HledgerForecast
|
2
|
-
# Set the options from a user's
|
2
|
+
# Set the options from a user's config
|
3
3
|
class Settings
|
4
4
|
def self.config(forecast, cli_options)
|
5
5
|
settings = {}
|
6
|
+
settings[:max_amount] = 0
|
7
|
+
settings[:max_category] = 0
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
+
forecast.each do |row|
|
10
|
+
if row['type'] != 'settings'
|
11
|
+
category_length = row['category'].length
|
12
|
+
settings[:max_category] = category_length if category_length > settings[:max_category]
|
9
13
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
+
amount = if row['amount'].is_a?(Integer) || row['amount'].is_a?(Float)
|
15
|
+
((row['amount'] + 3) * 100).to_s
|
16
|
+
else
|
17
|
+
row['amount'].to_s
|
18
|
+
end
|
14
19
|
|
15
|
-
|
16
|
-
|
17
|
-
settings
|
18
|
-
end
|
20
|
+
settings[:max_amount] = amount.length if amount.length > settings[:max_amount]
|
21
|
+
end
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
23
|
+
if row['type'] == 'settings'
|
24
|
+
|
25
|
+
settings[:currency] = if row['frequency'] == "currency"
|
26
|
+
row['account']
|
27
|
+
else
|
28
|
+
"USD"
|
29
|
+
end
|
30
|
+
|
31
|
+
settings[:show_symbol] = if row['frequency'] == "show_symbol"
|
32
|
+
row['account']
|
33
|
+
else
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
settings[:sign_before_symbol] = if row['frequency'] == "sign_before_symbol"
|
38
|
+
row['account']
|
39
|
+
else
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
settings[:thousands_separator] = if row['frequency'] == "thousands_separator"
|
44
|
+
row['account']
|
45
|
+
else
|
46
|
+
","
|
47
|
+
end
|
35
48
|
end
|
49
|
+
|
50
|
+
settings.merge!(cli_options) if cli_options
|
36
51
|
end
|
37
52
|
|
38
|
-
|
53
|
+
settings
|
39
54
|
end
|
40
55
|
end
|
41
56
|
end
|
@@ -6,67 +6,46 @@ module HledgerForecast
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def summarize(config, cli_options = nil)
|
9
|
-
@forecast =
|
9
|
+
@forecast = CSV.parse(config, headers: true)
|
10
10
|
@settings = Settings.config(@forecast, cli_options)
|
11
11
|
|
12
|
-
|
12
|
+
{ output: generate(@forecast), settings: @settings }
|
13
13
|
end
|
14
14
|
|
15
15
|
private
|
16
16
|
|
17
17
|
def generate(forecast)
|
18
|
-
output = {}
|
19
|
-
forecast.each do |period, blocks|
|
20
|
-
next if %w[settings].include?(period)
|
21
|
-
|
22
|
-
blocks.each do |block|
|
23
|
-
key = if @settings[:roll_up].nil?
|
24
|
-
period
|
25
|
-
else
|
26
|
-
output.length
|
27
|
-
end
|
28
|
-
|
29
|
-
output[key] ||= []
|
30
|
-
output[key] << process_block(period, block)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
output = filter_out(flatten_and_merge(output))
|
35
|
-
output = calculate_rolled_up_amount(output) unless @settings[:roll_up].nil?
|
36
|
-
|
37
|
-
output
|
38
|
-
end
|
39
|
-
|
40
|
-
def process_block(period, block)
|
41
18
|
output = []
|
42
19
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
to: block['to'] ? Date.parse(block['to']) : nil,
|
47
|
-
type: period,
|
48
|
-
frequency: block['frequency'],
|
49
|
-
transactions: []
|
50
|
-
}
|
20
|
+
forecast.each do |row|
|
21
|
+
next if row['type'] == 'settings'
|
22
|
+
next if row['summary_exclude']
|
51
23
|
|
52
|
-
|
53
|
-
end
|
24
|
+
row['amount'] = Utilities.convert_amount(row['amount'])
|
54
25
|
|
55
|
-
|
56
|
-
|
57
|
-
|
26
|
+
begin
|
27
|
+
annualised_amount = row['roll-up'] ? row['amount'] * row['roll-up'].to_f : row['amount'] * annualise(row['type'])
|
28
|
+
rescue StandardError
|
29
|
+
puts "\nError: ".bold.red + 'Could not create an annualised ammount. Have you set the roll-up for your custom type transactions?'
|
30
|
+
exit
|
31
|
+
end
|
58
32
|
|
59
|
-
output
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
33
|
+
output << {
|
34
|
+
account: row['account'],
|
35
|
+
from: Date.parse(row['from']),
|
36
|
+
to: row['to'] ? Calculator.new.evaluate_date(Date.parse(row['from']), row['to']) : nil,
|
37
|
+
type: row['type'],
|
38
|
+
frequency: row['frequency'],
|
39
|
+
category: row['category'],
|
40
|
+
description: row['description'],
|
41
|
+
amount: row['amount'],
|
42
|
+
annualised_amount: annualised_amount.to_f,
|
43
|
+
exclude: row['summary_exclude']
|
67
44
|
}
|
68
45
|
end
|
69
46
|
|
47
|
+
output = calculate_rolled_up_amount(output) unless @settings[:roll_up].nil?
|
48
|
+
|
70
49
|
output
|
71
50
|
end
|
72
51
|
|
@@ -84,22 +63,9 @@ module HledgerForecast
|
|
84
63
|
annualise[period]
|
85
64
|
end
|
86
65
|
|
87
|
-
def
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
def flatten_and_merge(blocks)
|
92
|
-
blocks.values.flatten.flat_map do |block|
|
93
|
-
block[:transactions].map do |transaction|
|
94
|
-
block.slice(:account, :from, :to, :type, :frequency).merge(transaction)
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def calculate_rolled_up_amount(data)
|
100
|
-
data.map do |item|
|
101
|
-
item[:rolled_up_amount] = item[:annualised_amount] / annualise(@settings[:roll_up])
|
102
|
-
item
|
66
|
+
def calculate_rolled_up_amount(forecast)
|
67
|
+
forecast.each do |row|
|
68
|
+
row[:rolled_up_amount] = row[:annualised_amount] / annualise(@settings[:roll_up])
|
103
69
|
end
|
104
70
|
end
|
105
71
|
end
|
@@ -41,7 +41,7 @@ module HledgerForecast
|
|
41
41
|
@table.add_row([{ value: type.capitalize.bold, colspan: 3, alignment: :center }])
|
42
42
|
total = 0
|
43
43
|
items.each do |item|
|
44
|
-
total += item[:amount]
|
44
|
+
total += item[:amount].to_f
|
45
45
|
|
46
46
|
if @settings[:verbose]
|
47
47
|
@table.add_row [{ value: item[:category], alignment: :left },
|
@@ -63,7 +63,7 @@ module HledgerForecast
|
|
63
63
|
def sort(data)
|
64
64
|
data.each do |type, items|
|
65
65
|
data[type] = items.sort_by do |item|
|
66
|
-
value = item[:amount]
|
66
|
+
value = item[:amount].to_f
|
67
67
|
[value >= 0 ? 1 : 0, value >= 0 ? -value : value]
|
68
68
|
end
|
69
69
|
end
|
@@ -114,12 +114,21 @@ module HledgerForecast
|
|
114
114
|
|
115
115
|
def add_total_row_to_table(data, row_to_sum)
|
116
116
|
total = data.reduce(0) do |sum, item|
|
117
|
-
sum + item[row_to_sum]
|
117
|
+
sum + item[row_to_sum].to_f
|
118
118
|
end
|
119
119
|
|
120
|
+
income = data.reduce(0) do |sum, item|
|
121
|
+
sum += item[row_to_sum].to_f if item[row_to_sum].to_f < 0
|
122
|
+
sum
|
123
|
+
end
|
124
|
+
|
125
|
+
savings = (total / income * 100).to_f.round(2)
|
126
|
+
|
120
127
|
@table.add_separator
|
121
128
|
@table.add_row [{ value: "TOTAL".bold, colspan: 2, alignment: :left },
|
122
129
|
{ value: format_amount(total).bold, alignment: :right }]
|
130
|
+
@table.add_row [{ value: "as a % of income".italic, colspan: 2, alignment: :left },
|
131
|
+
{ value: "#{savings}%".italic, alignment: :right }]
|
123
132
|
end
|
124
133
|
|
125
134
|
def format_amount(amount)
|
@@ -6,15 +6,15 @@ module HledgerForecast
|
|
6
6
|
# Expenses:Groceries $250.00 ; Food expenses
|
7
7
|
# Assets:Checking
|
8
8
|
class Default
|
9
|
-
def self.generate(
|
10
|
-
new(
|
9
|
+
def self.generate(forecast, settings)
|
10
|
+
new(forecast, settings).generate
|
11
11
|
end
|
12
12
|
|
13
13
|
def generate
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
forecast.each do |row|
|
15
|
+
next if row[:type] == "settings"
|
16
|
+
|
17
|
+
process_transactions(row)
|
18
18
|
end
|
19
19
|
|
20
20
|
output
|
@@ -22,59 +22,34 @@ module HledgerForecast
|
|
22
22
|
|
23
23
|
private
|
24
24
|
|
25
|
-
attr_reader :
|
25
|
+
attr_reader :forecast, :settings, :output
|
26
26
|
|
27
|
-
def initialize(
|
28
|
-
@
|
29
|
-
@
|
27
|
+
def initialize(forecast, settings)
|
28
|
+
@forecast = forecast
|
29
|
+
@settings = settings
|
30
30
|
@output = []
|
31
31
|
end
|
32
32
|
|
33
|
-
def
|
34
|
-
|
35
|
-
|
36
|
-
|
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])
|
33
|
+
def process_transactions(row)
|
34
|
+
to = build_to_header(row[:to])
|
35
|
+
frequency = get_periodic_rules(row[:type], row[:frequency])
|
48
36
|
|
49
|
-
|
50
|
-
|
51
|
-
|
37
|
+
if @settings[:verbose]
|
38
|
+
description = row[:description]
|
39
|
+
transactions = [row]
|
40
|
+
else
|
41
|
+
description = get_descriptions(row[:transactions])
|
42
|
+
transactions = row[:transactions]
|
52
43
|
end
|
53
|
-
end
|
54
44
|
|
55
|
-
|
56
|
-
|
57
|
-
transactions.map do |t|
|
58
|
-
# Skip transactions that have been marked as tracked
|
59
|
-
next if t[:track]
|
60
|
-
|
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
|
67
|
-
end
|
45
|
+
header = build_header(row, frequency, to, description)
|
46
|
+
footer = build_footer(row)
|
68
47
|
|
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
48
|
output << build_transaction(header, transactions, footer)
|
74
49
|
end
|
75
50
|
|
76
|
-
def build_header(
|
77
|
-
"#{frequency} #{
|
51
|
+
def build_header(row, frequency, to, descriptions)
|
52
|
+
"#{frequency} #{row[:from]}#{to} * #{descriptions}\n"
|
78
53
|
end
|
79
54
|
|
80
55
|
def build_footer(block)
|
@@ -85,11 +60,8 @@ module HledgerForecast
|
|
85
60
|
{ header: header, transactions: write_transactions(transactions), footer: footer }
|
86
61
|
end
|
87
62
|
|
88
|
-
def
|
89
|
-
return " to #{
|
90
|
-
return " to #{block}" if block
|
91
|
-
|
92
|
-
return nil
|
63
|
+
def build_to_header(to)
|
64
|
+
return " to #{to}" if to
|
93
65
|
end
|
94
66
|
|
95
67
|
def get_descriptions(transactions)
|
@@ -110,7 +82,6 @@ module HledgerForecast
|
|
110
82
|
'yearly' => '~ yearly from',
|
111
83
|
'custom' => "~ #{frequency} from"
|
112
84
|
}
|
113
|
-
|
114
85
|
map[type]
|
115
86
|
end
|
116
87
|
|
@@ -119,11 +90,11 @@ module HledgerForecast
|
|
119
90
|
# Skip transactions that have been marked as tracked
|
120
91
|
next if t[:track]
|
121
92
|
|
122
|
-
t[:amount] = t[:amount].to_s.ljust(
|
123
|
-
t[:category] = t[:category].ljust(
|
93
|
+
t[:amount] = t[:amount].to_s.ljust(@settings[:max_amount] + 5)
|
94
|
+
t[:category] = t[:category].to_s.ljust(@settings[:max_category])
|
124
95
|
|
125
96
|
" #{t[:category]} #{t[:amount]}; #{t[:description]}\n"
|
126
|
-
end
|
97
|
+
end.compact
|
127
98
|
end
|
128
99
|
end
|
129
100
|
end
|
@@ -9,84 +9,78 @@ module HledgerForecast
|
|
9
9
|
# Expenses:Groceries $250.00 ; Food expenses
|
10
10
|
# Assets:Checking
|
11
11
|
class Trackers
|
12
|
-
def self.generate(
|
13
|
-
new(
|
12
|
+
def self.generate(forecast, options)
|
13
|
+
new(forecast, options).generate
|
14
14
|
end
|
15
15
|
|
16
16
|
def generate
|
17
|
-
return
|
17
|
+
return if @options[:no_track]
|
18
|
+
return nil unless tracked?(forecast)
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
process_tracked(block)
|
22
|
-
end
|
20
|
+
forecast.each do |row|
|
21
|
+
process_tracked(row)
|
23
22
|
end
|
24
23
|
|
25
24
|
output
|
26
25
|
end
|
27
26
|
|
28
|
-
def self.track?(
|
27
|
+
def self.track?(row, options)
|
29
28
|
now = Date.today
|
30
|
-
|
31
|
-
data['from'], now, options)
|
29
|
+
row['track'] && Date.parse(row['from']) <= now && !exists?(row, now, options)
|
32
30
|
end
|
33
31
|
|
34
|
-
def self.exists?(
|
35
|
-
|
36
|
-
if !options[:transaction_file]
|
32
|
+
def self.exists?(row, now, options)
|
33
|
+
unless options[:transaction_file]
|
37
34
|
puts "\nWarning: ".bold.yellow + "For tracked transactions, please specify a file with the `-t` flag"
|
38
35
|
puts "ERROR: ".bold.red + "Tracked transactions ignored for now"
|
39
36
|
return
|
40
37
|
end
|
41
38
|
|
42
39
|
# Format the money
|
43
|
-
amount = Formatter.format_money(
|
44
|
-
inverse_amount = Formatter.format_money(
|
40
|
+
amount = Formatter.format_money(row['amount'], options)
|
41
|
+
inverse_amount = Formatter.format_money(row['amount'] * -1, options)
|
45
42
|
|
46
|
-
|
43
|
+
from = Date.parse(row['from'])
|
44
|
+
category = row['category'].gsub('[', '\\[').gsub(']', '\\]').gsub('(', '\\(').gsub(')', '\\)')
|
47
45
|
|
48
46
|
# We run two commands and check to see if category +/- amount or account +/- amount exists
|
49
|
-
command1 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{
|
50
|
-
command2 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{
|
47
|
+
command1 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{now}" | tr -s '[:space:]' ' ' | grep -q -Eo "#{category} (#{amount}|#{inverse_amount})")
|
48
|
+
command2 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{now}" | tr -s '[:space:]' ' ' | grep -q -Eo "#{row['account']} (#{amount}|#{inverse_amount})")
|
51
49
|
|
52
50
|
system(command1) || system(command2)
|
53
51
|
end
|
54
52
|
|
55
53
|
private
|
56
54
|
|
57
|
-
attr_reader :
|
55
|
+
attr_reader :forecast, :options, :output
|
58
56
|
|
59
|
-
def initialize(
|
60
|
-
@
|
57
|
+
def initialize(forecast, options)
|
58
|
+
@forecast = forecast
|
61
59
|
@options = options
|
62
60
|
@output = []
|
63
61
|
end
|
64
62
|
|
65
|
-
def tracked?(
|
66
|
-
|
67
|
-
|
68
|
-
block[:transactions].any? do |_, transactions|
|
69
|
-
transactions.any? { |t| t[:track] }
|
70
|
-
end
|
71
|
-
end
|
63
|
+
def tracked?(forecast)
|
64
|
+
forecast.any? do |row|
|
65
|
+
return true if row[:track] == true
|
72
66
|
end
|
67
|
+
|
68
|
+
return false
|
73
69
|
end
|
74
70
|
|
75
|
-
def process_tracked(
|
76
|
-
|
77
|
-
|
78
|
-
next unless t[:track]
|
71
|
+
def process_tracked(row)
|
72
|
+
row[:transactions].each do |t|
|
73
|
+
next if t[:track] == false
|
79
74
|
|
80
|
-
|
81
|
-
|
75
|
+
category = t[:category].ljust(options[:max_category])
|
76
|
+
amount = t[:amount].to_s.ljust(options[:max_amount])
|
82
77
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
78
|
+
header = "~ #{Date.new(Date.today.year, Date.today.month,
|
79
|
+
1).next_month} * [TRACKED] #{t[:description]}\n"
|
80
|
+
transactions = " #{category} #{amount}; #{t[:description]}\n"
|
81
|
+
footer = " #{row[:account]}\n\n"
|
87
82
|
|
88
|
-
|
89
|
-
end
|
83
|
+
output << { header: header, transactions: [transactions], footer: footer }
|
90
84
|
end
|
91
85
|
end
|
92
86
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module HledgerForecast
|
2
|
+
class Utilities
|
3
|
+
def self.convert_amount(amount)
|
4
|
+
case amount
|
5
|
+
when /^-?\d+\.\d+$/ # Detects floating-point numbers (including negatives)
|
6
|
+
amount.to_f
|
7
|
+
when /^-?\d+$/ # Detects integers (including negatives)
|
8
|
+
amount.to_i
|
9
|
+
else
|
10
|
+
amount
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/hledger_forecast.rb
CHANGED
@@ -8,7 +8,6 @@ require 'highline'
|
|
8
8
|
require 'money'
|
9
9
|
require 'optparse'
|
10
10
|
require 'terminal-table'
|
11
|
-
require 'yaml'
|
12
11
|
|
13
12
|
Money.locale_backend = nil
|
14
13
|
Money.rounding_mode = BigDecimal::ROUND_HALF_UP
|
@@ -17,12 +16,12 @@ Money.default_currency = 'USD'
|
|
17
16
|
require_relative 'hledger_forecast/calculator'
|
18
17
|
require_relative 'hledger_forecast/cli'
|
19
18
|
require_relative 'hledger_forecast/comparator'
|
20
|
-
require_relative 'hledger_forecast/csv_parser'
|
21
19
|
require_relative 'hledger_forecast/formatter'
|
22
20
|
require_relative 'hledger_forecast/generator'
|
23
21
|
require_relative 'hledger_forecast/settings'
|
24
22
|
require_relative 'hledger_forecast/summarizer'
|
25
23
|
require_relative 'hledger_forecast/summarizer_formatter'
|
24
|
+
require_relative 'hledger_forecast/utilities'
|
26
25
|
require_relative 'hledger_forecast/version'
|
27
26
|
|
28
27
|
require_relative 'hledger_forecast/transactions/default'
|
data/spec/cli_spec.rb
CHANGED
@@ -2,12 +2,12 @@ require_relative '../lib/hledger_forecast'
|
|
2
2
|
|
3
3
|
output = <<~JOURNAL
|
4
4
|
~ monthly from 2023-03-01 * Mortgage, Food
|
5
|
-
Expenses:Mortgage £2,000.55; Mortgage
|
6
|
-
Expenses:Food £100.00
|
5
|
+
Expenses:Mortgage £2,000.55 ; Mortgage
|
6
|
+
Expenses:Food £100.00 ; Food
|
7
7
|
Assets:Bank
|
8
8
|
|
9
9
|
~ monthly from 2023-03-01 * Savings
|
10
|
-
Assets:Bank £-1,000.00; Savings
|
10
|
+
Assets:Bank £-1,000.00 ; Savings
|
11
11
|
Assets:Savings
|
12
12
|
|
13
13
|
JOURNAL
|
@@ -26,15 +26,6 @@ ensure
|
|
26
26
|
end
|
27
27
|
|
28
28
|
RSpec.describe 'command' do
|
29
|
-
it 'uses the CLI to generate an output' do
|
30
|
-
generated_journal = './test_output.journal'
|
31
|
-
File.delete(generated_journal) if File.exist?(generated_journal)
|
32
|
-
|
33
|
-
system("./bin/hledger-forecast generate -f ./spec/stubs/forecast.yml -o ./test_output.journal --force")
|
34
|
-
|
35
|
-
expect(File.read(generated_journal)).to eq(output)
|
36
|
-
end
|
37
|
-
|
38
29
|
it 'uses the CLI to generate an output with a CSV config file' do
|
39
30
|
generated_journal = './test_output.journal'
|
40
31
|
File.delete(generated_journal) if File.exist?(generated_journal)
|
@@ -1,29 +1,18 @@
|
|
1
1
|
require_relative '../lib/hledger_forecast'
|
2
2
|
|
3
|
-
config = <<~
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
monthly:
|
8
|
-
|
9
|
-
|
10
|
-
transactions:
|
11
|
-
- amount: "=5000/24"
|
12
|
-
category: "Expenses:House"
|
13
|
-
description: New Kitchen
|
14
|
-
- amount: "=25*4.3"
|
15
|
-
category: "Expenses:Food"
|
16
|
-
description: Monthly food shop
|
17
|
-
- amount: "=(102.50+3.25)/2"
|
18
|
-
category: "Expenses:Food"
|
19
|
-
description: Random food
|
20
|
-
YAML
|
3
|
+
config = <<~CSV
|
4
|
+
type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
|
5
|
+
monthly,,Liabilities:Amex,01/05/2023,,New kitchen,Expenses:House,=5000/24,,,
|
6
|
+
monthly,,Liabilities:Amex,01/05/2023,,Monthly food shop,Expenses:Food,=25*4.3,,,
|
7
|
+
monthly,,Liabilities:Amex,01/05/2023,,Random food,Expenses:Food,=(102.50+3.25)/2,,,
|
8
|
+
settings,currency,GBP,,,,,,,,
|
9
|
+
CSV
|
21
10
|
|
22
11
|
output = <<~JOURNAL
|
23
|
-
~ monthly from 2023-05-01 * New
|
24
|
-
Expenses:House £208.33
|
25
|
-
Expenses:Food £107.50
|
26
|
-
Expenses:Food £52.88
|
12
|
+
~ monthly from 2023-05-01 * New kitchen, Monthly food shop, Random food
|
13
|
+
Expenses:House £208.33 ; New kitchen
|
14
|
+
Expenses:Food £107.50 ; Monthly food shop
|
15
|
+
Expenses:Food £52.88 ; Random food
|
27
16
|
Liabilities:Amex
|
28
17
|
|
29
18
|
JOURNAL
|