hledger-forecast 0.4.0 → 1.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/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
|