hledger-forecast 0.4.0 → 1.0.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/.gitignore +1 -0
- data/.rubocop.yml +3 -0
- data/README.md +47 -31
- data/hledger-forecast.gemspec +1 -0
- data/lib/hledger_forecast/calculator.rb +21 -0
- data/lib/hledger_forecast/cli.rb +2 -2
- data/lib/hledger_forecast/formatter.rb +24 -0
- data/lib/hledger_forecast/generator.rb +41 -258
- data/lib/hledger_forecast/settings.rb +41 -0
- data/lib/hledger_forecast/{summarize.rb → summarizer.rb} +41 -43
- data/lib/hledger_forecast/transactions/default.rb +88 -0
- data/lib/hledger_forecast/transactions/modifiers.rb +90 -0
- data/lib/hledger_forecast/transactions/trackers.rb +87 -0
- data/lib/hledger_forecast/version.rb +1 -1
- data/lib/hledger_forecast.rb +10 -4
- data/spec/custom_spec.rb +31 -4
- data/spec/modifier_spec.rb +21 -6
- data/spec/monthly_end_date_spec.rb +18 -33
- data/spec/monthly_end_date_transaction_spec.rb +44 -7
- data/spec/track_spec.rb +5 -67
- metadata +12 -7
- data/lib/hledger_forecast/tracker.rb +0 -37
@@ -1,37 +1,54 @@
|
|
1
1
|
module HledgerForecast
|
2
2
|
# Summarise a forecast YAML file and output it to the CLI
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
table = Terminal::Table.new
|
3
|
+
# TODO: Rename this to Summarizer and the main method becomes summarize
|
4
|
+
class Summarizer
|
5
|
+
def self.summarize(config, cli_options)
|
6
|
+
new.summarize(config, cli_options)
|
7
|
+
end
|
9
8
|
|
10
|
-
|
11
|
-
|
9
|
+
def summarize(config, cli_options = nil)
|
10
|
+
@forecast = YAML.safe_load(config)
|
11
|
+
@settings = Settings.config(@forecast, cli_options)
|
12
|
+
@table = Terminal::Table.new
|
12
13
|
|
13
|
-
@
|
14
|
+
generate(@forecast)
|
14
15
|
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
private
|
18
|
+
|
19
|
+
def generate(forecast)
|
20
|
+
init_table
|
21
|
+
|
22
|
+
category_totals = {}
|
23
|
+
%w[monthly quarterly half-yearly yearly once custom].each do |period|
|
24
|
+
category_totals[period] = sum_transactions(forecast, period)
|
25
|
+
end
|
19
26
|
|
20
|
-
|
27
|
+
add_categories_to_table(category_totals, forecast)
|
28
|
+
|
29
|
+
@table.add_separator
|
30
|
+
format_total("TOTAL", category_totals.values.map(&:values).flatten.sum)
|
31
|
+
|
32
|
+
@table
|
33
|
+
end
|
34
|
+
|
35
|
+
def init_table
|
36
|
+
@table.add_row([{ value: 'FORECAST SUMMARY'.bold, colspan: 3, alignment: :center }])
|
37
|
+
@table.add_separator
|
21
38
|
end
|
22
39
|
|
23
|
-
def
|
40
|
+
def sum_transactions(forecast, period)
|
24
41
|
category_total = Hash.new(0)
|
25
|
-
|
42
|
+
forecast[period]&.each do |entry|
|
26
43
|
entry['transactions'].each do |transaction|
|
27
|
-
category_total[transaction['category']] += transaction['amount']
|
44
|
+
category_total[transaction['category']] += Calculator.new.evaluate(transaction['amount'])
|
28
45
|
end
|
29
46
|
end
|
30
47
|
|
31
48
|
category_total
|
32
49
|
end
|
33
50
|
|
34
|
-
def
|
51
|
+
def sum_custom_transactions(forecast_data)
|
35
52
|
category_total = Hash.new(0)
|
36
53
|
custom_periods = []
|
37
54
|
|
@@ -51,12 +68,12 @@ module HledgerForecast
|
|
51
68
|
{ totals: category_total, periods: custom_periods }
|
52
69
|
end
|
53
70
|
|
54
|
-
def
|
55
|
-
formatted_amount =
|
71
|
+
def format_amount(amount)
|
72
|
+
formatted_amount = Formatter.format_money(amount, @settings)
|
56
73
|
amount.to_f < 0 ? formatted_amount.green : formatted_amount.red
|
57
74
|
end
|
58
75
|
|
59
|
-
def
|
76
|
+
def add_rows_to_table(row_data, period_total, custom: false)
|
60
77
|
if custom
|
61
78
|
row_data[:periods].each do |period|
|
62
79
|
@table.add_row [{ value: period[:category], alignment: :left },
|
@@ -77,7 +94,7 @@ module HledgerForecast
|
|
77
94
|
period_total
|
78
95
|
end
|
79
96
|
|
80
|
-
def
|
97
|
+
def add_categories_to_table(categories, forecast_data)
|
81
98
|
first_period = true
|
82
99
|
categories.each do |period, total|
|
83
100
|
category_total = total.reject { |_, amount| amount == 0 }
|
@@ -100,35 +117,16 @@ module HledgerForecast
|
|
100
117
|
end
|
101
118
|
end
|
102
119
|
|
103
|
-
def
|
120
|
+
def sort_transactions(category_total)
|
104
121
|
negatives = category_total.select { |_, amount| amount < 0 }.sort_by { |_, amount| amount }
|
105
122
|
positives = category_total.select { |_, amount| amount > 0 }.sort_by { |_, amount| -amount }
|
106
123
|
|
107
124
|
negatives.concat(positives).to_h
|
108
125
|
end
|
109
126
|
|
110
|
-
def
|
127
|
+
def format_total(text, total)
|
111
128
|
@table.add_row [{ value: text.bold, colspan: 2, alignment: :left },
|
112
|
-
|
113
|
-
end
|
114
|
-
|
115
|
-
def self.generate(forecast)
|
116
|
-
forecast_data = YAML.safe_load(forecast)
|
117
|
-
|
118
|
-
init_table
|
119
|
-
init_generator(forecast_data)
|
120
|
-
|
121
|
-
category_totals = {}
|
122
|
-
%w[monthly quarterly half-yearly yearly once custom].each do |period|
|
123
|
-
category_totals[period] = sum_transactions(forecast_data, period)
|
124
|
-
end
|
125
|
-
|
126
|
-
add_categories_to_table(category_totals, forecast_data)
|
127
|
-
|
128
|
-
@table.add_separator
|
129
|
-
format_total("TOTAL", category_totals.values.map(&:values).flatten.sum)
|
130
|
-
|
131
|
-
puts @table
|
129
|
+
{ value: format_amount(total).bold, alignment: :right }]
|
132
130
|
end
|
133
131
|
end
|
134
132
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module HledgerForecast
|
2
|
+
module Transactions
|
3
|
+
# Generate default hledger transactions
|
4
|
+
# Example output:
|
5
|
+
# ~ monthly from 2023-05-1 * Food expenses
|
6
|
+
# Expenses:Groceries $250.00 ; Food expenses
|
7
|
+
# Assets:Checking
|
8
|
+
class Default
|
9
|
+
def self.generate(data, options)
|
10
|
+
new(data, options).generate
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate
|
14
|
+
data.each_value do |blocks|
|
15
|
+
blocks.each do |block|
|
16
|
+
process_block(block)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
output
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :data, :options, :output
|
26
|
+
|
27
|
+
def initialize(data, options)
|
28
|
+
@data = data
|
29
|
+
@options = options
|
30
|
+
@output = []
|
31
|
+
end
|
32
|
+
|
33
|
+
def process_block(block)
|
34
|
+
block[:transactions].each do |to, transactions|
|
35
|
+
to = get_header(block[:to], to)
|
36
|
+
block[:descriptions] = get_descriptions(transactions)
|
37
|
+
frequency = get_periodic_rules(block[:type], block[:frequency])
|
38
|
+
|
39
|
+
header = "#{frequency} #{block[:from]}#{to} * #{block[:descriptions]}\n"
|
40
|
+
footer = " #{block[:account]}\n\n"
|
41
|
+
|
42
|
+
output << { header: header, transactions: write_transactions(transactions), footer: footer }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_header(block, transaction)
|
47
|
+
return " to #{transaction}" if transaction
|
48
|
+
return " to #{block}" if block
|
49
|
+
|
50
|
+
return nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_descriptions(transactions)
|
54
|
+
transactions.map do |t|
|
55
|
+
# Skip transactions that have been marked as tracked
|
56
|
+
next if t[:track]
|
57
|
+
|
58
|
+
t[:description]
|
59
|
+
end.compact.join(', ')
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_periodic_rules(type, frequency)
|
63
|
+
map = {
|
64
|
+
'once' => '~',
|
65
|
+
'monthly' => '~ monthly from',
|
66
|
+
'quarterly' => '~ every 3 months from',
|
67
|
+
'half-yearly' => '~ every 6 months from',
|
68
|
+
'yearly' => '~ yearly from',
|
69
|
+
'custom' => "~ #{frequency} from"
|
70
|
+
}
|
71
|
+
|
72
|
+
map[type]
|
73
|
+
end
|
74
|
+
|
75
|
+
def write_transactions(transactions)
|
76
|
+
transactions.map do |t|
|
77
|
+
# Skip transactions that have been marked as tracked
|
78
|
+
next if t[:track]
|
79
|
+
|
80
|
+
t[:amount] = t[:amount].to_s.ljust(options[:max_amount])
|
81
|
+
t[:category] = t[:category].ljust(options[:max_category])
|
82
|
+
|
83
|
+
" #{t[:category]} #{t[:amount]}; #{t[:description]}\n"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module HledgerForecast
|
2
|
+
module Transactions
|
3
|
+
# Generate auto-posting hledger transactions
|
4
|
+
# Example output:
|
5
|
+
# = Expenses:Groceries date:2024-01-01..2025-12-31
|
6
|
+
# Expenses:Groceries *0.1 ; Groceries
|
7
|
+
# Assets:Checking *-0.1
|
8
|
+
class Modifiers
|
9
|
+
def self.generate(data, options)
|
10
|
+
new(data, options).generate
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate
|
14
|
+
return nil unless modifiers?
|
15
|
+
|
16
|
+
process_modifier
|
17
|
+
|
18
|
+
output
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.get_modifiers(transaction, block)
|
22
|
+
modifiers = []
|
23
|
+
|
24
|
+
transaction['modifiers'].each do |modifier|
|
25
|
+
description = transaction['description']
|
26
|
+
description += " - #{modifier['description']}" unless modifier['description'].empty?
|
27
|
+
|
28
|
+
modifiers << {
|
29
|
+
account: block['account'],
|
30
|
+
amount: modifier['amount'],
|
31
|
+
category: transaction['category'],
|
32
|
+
description: description,
|
33
|
+
from: Date.parse(modifier['from'] || block['from']),
|
34
|
+
to: modifier['to'] ? Date.parse(modifier['to']) : nil
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
modifiers
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :data, :options, :output
|
44
|
+
|
45
|
+
def initialize(data, options)
|
46
|
+
@data = data
|
47
|
+
@options = options
|
48
|
+
@output = []
|
49
|
+
end
|
50
|
+
|
51
|
+
def modifiers?
|
52
|
+
@data.any? do |_, blocks|
|
53
|
+
blocks.any? do |block|
|
54
|
+
block[:transactions].any? do |_, transactions|
|
55
|
+
transactions.any? { |t| !t[:modifiers].empty? }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def process_modifier
|
62
|
+
get_transactions.each do |modifier|
|
63
|
+
account = modifier[:account].ljust(@options[:max_category])
|
64
|
+
category = modifier[:category].ljust(@options[:max_category])
|
65
|
+
# Fix the ljust by counting strings in amount
|
66
|
+
amount = modifier[:amount].to_s.ljust(@options[:max_amount] - 1)
|
67
|
+
to = modifier[:to] ? "..#{modifier[:to]}" : nil
|
68
|
+
|
69
|
+
header = "= #{modifier[:category]} date:#{modifier[:from]}#{to}\n"
|
70
|
+
transactions = " #{category} *#{amount}; #{modifier[:description]}\n"
|
71
|
+
footer = " #{account} *#{modifier[:amount] * -1}\n\n"
|
72
|
+
|
73
|
+
output << { header: header, transactions: [transactions], footer: footer }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_transactions
|
78
|
+
@data.each_with_object([]) do |(_key, blocks), result|
|
79
|
+
blocks.each do |block|
|
80
|
+
block[:transactions].each_value do |transactions|
|
81
|
+
transactions.each do |t|
|
82
|
+
result.concat(t[:modifiers]) if t[:modifiers]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module HledgerForecast
|
2
|
+
module Transactions
|
3
|
+
# Generate hledger transactions based on the non-existance of a transaction
|
4
|
+
# in your ledger. This is useful for ensuring that certain expenses are
|
5
|
+
# accounted for, even if you forget to enter them.
|
6
|
+
#
|
7
|
+
# Example output:
|
8
|
+
# ~ 2023-05-1 * [TRACKED] Food expenses
|
9
|
+
# Expenses:Groceries $250.00 ; Food expenses
|
10
|
+
# Assets:Checking
|
11
|
+
class Trackers
|
12
|
+
def self.generate(data, options)
|
13
|
+
new(data, options).generate
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate
|
17
|
+
return nil unless tracked?(data)
|
18
|
+
|
19
|
+
data.each_value do |blocks|
|
20
|
+
blocks.each do |block|
|
21
|
+
process_tracked(block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
output
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.track?(transaction, data, options)
|
29
|
+
now = Date.today
|
30
|
+
transaction['track'] && Date.parse(data['from']) <= now && !exists?(transaction, data['account'],
|
31
|
+
data['from'], now, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.exists?(transaction, account, from, to, options)
|
35
|
+
# Format the money
|
36
|
+
amount = Formatter.format_money(transaction['amount'], options)
|
37
|
+
inverse_amount = Formatter.format_money(transaction['amount'] * -1, options)
|
38
|
+
|
39
|
+
category = transaction['category'].gsub('[', '\\[').gsub(']', '\\]').gsub('(', '\\(').gsub(')', '\\)')
|
40
|
+
|
41
|
+
# We run two commands and check to see if category +/- amount or account +/- amount exists
|
42
|
+
command1 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{to}" | tr -s '[:space:]' ' ' | grep -q -Eo "#{category} (#{amount}|#{inverse_amount})")
|
43
|
+
command2 = %(hledger print -f #{options[:transaction_file]} "date:#{from}..#{to}" | tr -s '[:space:]' ' ' | grep -q -Eo "#{account} (#{amount}|#{inverse_amount})")
|
44
|
+
|
45
|
+
system(command1) || system(command2)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
attr_reader :data, :options, :output
|
51
|
+
|
52
|
+
def initialize(data, options)
|
53
|
+
@data = data
|
54
|
+
@options = options
|
55
|
+
@output = []
|
56
|
+
end
|
57
|
+
|
58
|
+
def tracked?(data)
|
59
|
+
data.any? do |_, blocks|
|
60
|
+
blocks.any? do |block|
|
61
|
+
block[:transactions].any? do |_, transactions|
|
62
|
+
transactions.any? { |t| t[:track] }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def process_tracked(block)
|
69
|
+
block[:transactions].each do |_to, transactions|
|
70
|
+
transactions.each do |t|
|
71
|
+
next unless t[:track]
|
72
|
+
|
73
|
+
category = t[:category].ljust(options[:max_category])
|
74
|
+
amount = t[:amount].to_s.ljust(options[:max_amount])
|
75
|
+
|
76
|
+
header = "~ #{Date.new(Date.today.year, Date.today.month,
|
77
|
+
1).next_month} * [TRACKED] #{t[:description]}\n"
|
78
|
+
transactions = " #{category} #{amount}; #{t[:description]}\n"
|
79
|
+
footer = " #{block[:account]}\n\n"
|
80
|
+
|
81
|
+
output << { header: header, transactions: [transactions], footer: footer }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/hledger_forecast.rb
CHANGED
@@ -12,8 +12,14 @@ require 'yaml'
|
|
12
12
|
Money.locale_backend = nil
|
13
13
|
Money.rounding_mode = BigDecimal::ROUND_HALF_UP
|
14
14
|
|
15
|
-
require_relative 'hledger_forecast/
|
16
|
-
require_relative 'hledger_forecast/generator'
|
17
|
-
require_relative 'hledger_forecast/summarize'
|
18
|
-
require_relative 'hledger_forecast/tracker'
|
15
|
+
require_relative 'hledger_forecast/calculator'
|
19
16
|
require_relative 'hledger_forecast/cli'
|
17
|
+
require_relative 'hledger_forecast/formatter'
|
18
|
+
require_relative 'hledger_forecast/generator'
|
19
|
+
require_relative 'hledger_forecast/settings'
|
20
|
+
require_relative 'hledger_forecast/summarizer'
|
21
|
+
require_relative 'hledger_forecast/version'
|
22
|
+
|
23
|
+
require_relative 'hledger_forecast/transactions/default'
|
24
|
+
require_relative 'hledger_forecast/transactions/modifiers'
|
25
|
+
require_relative 'hledger_forecast/transactions/trackers'
|
data/spec/custom_spec.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require_relative '../lib/hledger_forecast'
|
2
2
|
|
3
|
-
|
3
|
+
base_config = <<~YAML
|
4
4
|
custom:
|
5
5
|
- frequency: "every 2 weeks"
|
6
6
|
from: "2023-05-01"
|
@@ -21,7 +21,7 @@ config = <<~YAML
|
|
21
21
|
currency: GBP
|
22
22
|
YAML
|
23
23
|
|
24
|
-
|
24
|
+
base_output = <<~JOURNAL
|
25
25
|
~ every 2 weeks from 2023-05-01 * Hair and beauty
|
26
26
|
[Expenses:Personal Care] £80.00; Hair and beauty
|
27
27
|
[Assets:Bank]
|
@@ -32,9 +32,36 @@ output = <<~JOURNAL
|
|
32
32
|
|
33
33
|
JOURNAL
|
34
34
|
|
35
|
+
calculated_config = <<~YAML
|
36
|
+
custom:
|
37
|
+
- frequency: "every 2 weeks"
|
38
|
+
from: "2023-05-01"
|
39
|
+
account: "[Assets:Bank]"
|
40
|
+
transactions:
|
41
|
+
- amount: 80
|
42
|
+
category: "[Expenses:Personal Care]"
|
43
|
+
description: Hair and beauty
|
44
|
+
to: "=6"
|
45
|
+
|
46
|
+
settings:
|
47
|
+
currency: GBP
|
48
|
+
YAML
|
49
|
+
|
50
|
+
calculated_output = <<~JOURNAL
|
51
|
+
~ every 2 weeks from 2023-05-01 to 2023-10-31 * Hair and beauty
|
52
|
+
[Expenses:Personal Care] £80.00; Hair and beauty
|
53
|
+
[Assets:Bank]
|
54
|
+
|
55
|
+
JOURNAL
|
56
|
+
|
35
57
|
RSpec.describe 'generate' do
|
36
58
|
it 'generates a forecast with correct CUSTOM transactions' do
|
37
|
-
generated_journal = HledgerForecast::Generator.generate(
|
38
|
-
expect(generated_journal).to eq(
|
59
|
+
generated_journal = HledgerForecast::Generator.generate(base_config)
|
60
|
+
expect(generated_journal).to eq(base_output)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'generates a forecast with correct CUSTOM transactions and CALCULATED to dates' do
|
64
|
+
generated_journal = HledgerForecast::Generator.generate(calculated_config)
|
65
|
+
expect(generated_journal).to eq(calculated_output)
|
39
66
|
end
|
40
67
|
end
|
data/spec/modifier_spec.rb
CHANGED
@@ -5,7 +5,7 @@ base_config = <<~YAML
|
|
5
5
|
- account: "Assets:Bank"
|
6
6
|
from: "2023-01-01"
|
7
7
|
transactions:
|
8
|
-
- amount:
|
8
|
+
- amount: 300
|
9
9
|
category: "Expenses:Groceries"
|
10
10
|
description: Food shopping
|
11
11
|
modifiers:
|
@@ -17,6 +17,17 @@ base_config = <<~YAML
|
|
17
17
|
description: "Y2 inflation"
|
18
18
|
from: "2025-01-01"
|
19
19
|
to: "2025-12-31"
|
20
|
+
- account: "Assets:Savings"
|
21
|
+
from: "2023-05-01"
|
22
|
+
transactions:
|
23
|
+
- amount: 500
|
24
|
+
category: "Assets:Bank"
|
25
|
+
description: Savings
|
26
|
+
modifiers:
|
27
|
+
- amount: 0.1
|
28
|
+
description: "Savings uplift"
|
29
|
+
from: "2024-05-01"
|
30
|
+
to: "2025-04-30"
|
20
31
|
|
21
32
|
settings:
|
22
33
|
currency: USD
|
@@ -24,9 +35,13 @@ YAML
|
|
24
35
|
|
25
36
|
base_journal = <<~JOURNAL
|
26
37
|
~ monthly from 2023-01-01 * Food shopping
|
27
|
-
Expenses:Groceries $
|
38
|
+
Expenses:Groceries $300.00; Food shopping
|
28
39
|
Assets:Bank
|
29
40
|
|
41
|
+
~ monthly from 2023-05-01 * Savings
|
42
|
+
Assets:Bank $500.00; Savings
|
43
|
+
Assets:Savings
|
44
|
+
|
30
45
|
= Expenses:Groceries date:2024-01-01..2024-12-31
|
31
46
|
Expenses:Groceries *0.02 ; Food shopping - Y1 inflation
|
32
47
|
Assets:Bank *-0.02
|
@@ -35,6 +50,10 @@ base_journal = <<~JOURNAL
|
|
35
50
|
Expenses:Groceries *0.05 ; Food shopping - Y2 inflation
|
36
51
|
Assets:Bank *-0.05
|
37
52
|
|
53
|
+
= Assets:Bank date:2024-05-01..2025-04-30
|
54
|
+
Assets:Bank *0.1 ; Savings - Savings uplift
|
55
|
+
Assets:Savings *-0.1
|
56
|
+
|
38
57
|
JOURNAL
|
39
58
|
|
40
59
|
no_date_config = <<~YAML
|
@@ -67,20 +86,16 @@ JOURNAL
|
|
67
86
|
RSpec.describe 'Applying modifiers to transactions -' do
|
68
87
|
it 'Auto-postings should be created correctly' do
|
69
88
|
generated = HledgerForecast::Generator
|
70
|
-
generated.modified = {} # Clear modified transactions
|
71
89
|
|
72
90
|
generated_journal = generated.generate(base_config)
|
73
|
-
generated.modified = {} # Clear modified transactions
|
74
91
|
|
75
92
|
expect(generated_journal).to eq(base_journal)
|
76
93
|
end
|
77
94
|
|
78
95
|
it 'Auto-postings should be created correctly if no dates are set' do
|
79
96
|
generated = HledgerForecast::Generator
|
80
|
-
generated.modified = {} # Clear modified transactions
|
81
97
|
|
82
98
|
generated_journal = generated.generate(no_date_config)
|
83
|
-
generated.modified = {} # Clear modified transactions
|
84
99
|
|
85
100
|
expect(generated_journal).to eq(no_date_journal)
|
86
101
|
end
|
@@ -1,47 +1,32 @@
|
|
1
1
|
require_relative '../lib/hledger_forecast'
|
2
2
|
|
3
3
|
config = <<~YAML
|
4
|
-
settings:
|
5
|
-
|
4
|
+
settings:
|
5
|
+
currency: GBP
|
6
6
|
|
7
|
-
monthly:
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
amount: 200.00
|
19
|
-
- description: Food
|
20
|
-
category: "Expenses:Food"
|
21
|
-
amount: 100.00
|
22
|
-
- description: Party time
|
23
|
-
category: "Expenses:Going Out"
|
24
|
-
amount: 50.00
|
7
|
+
monthly:
|
8
|
+
- from: "2023-03-01"
|
9
|
+
to: "2023-06-01"
|
10
|
+
account: "Assets:Bank"
|
11
|
+
transactions:
|
12
|
+
- description: Mortgage
|
13
|
+
category: "Expenses:Mortgage"
|
14
|
+
amount: 2000.00
|
15
|
+
- description: Food
|
16
|
+
category: "Expenses:Food"
|
17
|
+
amount: 100.00
|
25
18
|
YAML
|
26
19
|
|
27
20
|
output = <<~JOURNAL
|
28
|
-
~ monthly from 2023-03-01 *
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
~ monthly from 2023-03-01 to 2023-06-01 * Mortgage
|
34
|
-
Expenses:Mortgage £2,000.00; Mortgage
|
35
|
-
Assets:Bank
|
36
|
-
|
37
|
-
~ monthly from 2023-03-01 to 2023-06-01 * Mortgage top up
|
38
|
-
Expenses:Mortgage Top Up £200.00 ; Mortgage top up
|
39
|
-
Assets:Bank
|
21
|
+
~ monthly from 2023-03-01 to 2023-06-01 * Mortgage, Food
|
22
|
+
Expenses:Mortgage £2,000.00; Mortgage
|
23
|
+
Expenses:Food £100.00 ; Food
|
24
|
+
Assets:Bank
|
40
25
|
|
41
26
|
JOURNAL
|
42
27
|
|
43
28
|
RSpec.describe 'generate' do
|
44
|
-
it 'generates a forecast with correct MONTHLY transactions that have an end date' do
|
29
|
+
it 'generates a forecast with correct MONTHLY transactions that have an end date, at the top level' do
|
45
30
|
expect(HledgerForecast::Generator.generate(config)).to eq(output)
|
46
31
|
end
|
47
32
|
end
|