hledger-forecast 0.4.0 → 1.1.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 +111 -41
- data/example.journal +33 -22
- data/example.yml +40 -26
- data/hledger-forecast.gemspec +1 -0
- data/lib/hledger_forecast/calculator.rb +21 -0
- data/lib/hledger_forecast/cli.rb +27 -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/summarizer.rb +106 -0
- data/lib/hledger_forecast/summarizer_formatter.rb +115 -0
- 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 +12 -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/monthly_spec.rb +7 -7
- data/spec/summarizer_spec.rb +86 -0
- data/spec/track_spec.rb +5 -67
- metadata +15 -7
- data/lib/hledger_forecast/summarize.rb +0 -134
- data/lib/hledger_forecast/tracker.rb +0 -37
@@ -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,16 @@ 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/summarizer_formatter'
|
22
|
+
require_relative 'hledger_forecast/version'
|
23
|
+
|
24
|
+
require_relative 'hledger_forecast/transactions/default'
|
25
|
+
require_relative 'hledger_forecast/transactions/modifiers'
|
26
|
+
require_relative 'hledger_forecast/transactions/trackers'
|
27
|
+
|
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
|
@@ -1,32 +1,69 @@
|
|
1
1
|
require_relative '../lib/hledger_forecast'
|
2
2
|
|
3
|
-
|
3
|
+
base_config = <<~YAML
|
4
4
|
settings:
|
5
5
|
currency: GBP
|
6
6
|
|
7
7
|
monthly:
|
8
8
|
- from: "2023-03-01"
|
9
|
-
to: "2023-06-01"
|
10
9
|
account: "Assets:Bank"
|
11
10
|
transactions:
|
12
11
|
- description: Mortgage
|
12
|
+
to: "2023-06-01"
|
13
13
|
category: "Expenses:Mortgage"
|
14
14
|
amount: 2000.00
|
15
|
+
- description: Mortgage top up
|
16
|
+
to: "2023-06-01"
|
17
|
+
category: "Expenses:Mortgage Top Up"
|
18
|
+
amount: 200.00
|
15
19
|
- description: Food
|
16
20
|
category: "Expenses:Food"
|
17
21
|
amount: 100.00
|
22
|
+
- description: Party time
|
23
|
+
category: "Expenses:Going Out"
|
24
|
+
amount: 50.00
|
18
25
|
YAML
|
19
26
|
|
20
|
-
|
21
|
-
~ monthly from 2023-03-01 to 2023-06-01 * Mortgage,
|
27
|
+
base_output = <<~JOURNAL
|
28
|
+
~ monthly from 2023-03-01 to 2023-06-01 * Mortgage, Mortgage top up
|
29
|
+
Expenses:Mortgage £2,000.00; Mortgage
|
30
|
+
Expenses:Mortgage Top Up £200.00 ; Mortgage top up
|
31
|
+
Assets:Bank
|
32
|
+
|
33
|
+
~ monthly from 2023-03-01 * Food, Party time
|
34
|
+
Expenses:Food £100.00 ; Food
|
35
|
+
Expenses:Going Out £50.00 ; Party time
|
36
|
+
Assets:Bank
|
37
|
+
|
38
|
+
JOURNAL
|
39
|
+
|
40
|
+
computed_config = <<~YAML
|
41
|
+
settings:
|
42
|
+
currency: GBP
|
43
|
+
|
44
|
+
monthly:
|
45
|
+
- from: "2023-03-01"
|
46
|
+
account: "Assets:Bank"
|
47
|
+
transactions:
|
48
|
+
- description: Mortgage
|
49
|
+
category: "Expenses:Mortgage"
|
50
|
+
to: "=12"
|
51
|
+
amount: 2000.00
|
52
|
+
YAML
|
53
|
+
|
54
|
+
computed_output = <<~JOURNAL
|
55
|
+
~ monthly from 2023-03-01 to 2024-02-29 * Mortgage
|
22
56
|
Expenses:Mortgage £2,000.00; Mortgage
|
23
|
-
Expenses:Food £100.00 ; Food
|
24
57
|
Assets:Bank
|
25
58
|
|
26
59
|
JOURNAL
|
27
60
|
|
28
61
|
RSpec.describe 'generate' do
|
29
|
-
it 'generates a forecast with correct MONTHLY transactions that have an end date
|
30
|
-
expect(HledgerForecast::Generator.generate(
|
62
|
+
it 'generates a forecast with correct MONTHLY transactions that have an end date' do
|
63
|
+
expect(HledgerForecast::Generator.generate(base_config)).to eq(base_output)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'generates a forecast with correct MONTHLY transactions that have a COMPUTED end date' do
|
67
|
+
expect(HledgerForecast::Generator.generate(computed_config)).to eq(computed_output)
|
31
68
|
end
|
32
69
|
end
|
data/spec/monthly_spec.rb
CHANGED
@@ -23,14 +23,14 @@ config = <<~YAML
|
|
23
23
|
YAML
|
24
24
|
|
25
25
|
output = <<~JOURNAL
|
26
|
-
~ monthly from 2023-03-01 * Mortgage, Food
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
~ monthly from 2023-03-01 * Mortgage, Food
|
27
|
+
Expenses:Mortgage £2,000.55; Mortgage
|
28
|
+
Expenses:Food £100.00 ; Food
|
29
|
+
Assets:Bank
|
30
30
|
|
31
|
-
~ monthly from 2023-03-01 * Savings
|
32
|
-
|
33
|
-
|
31
|
+
~ monthly from 2023-03-01 * Savings
|
32
|
+
Assets:Bank £-1,000.00; Savings
|
33
|
+
Assets:Savings
|
34
34
|
|
35
35
|
JOURNAL
|
36
36
|
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require_relative '../lib/hledger_forecast'
|
2
|
+
|
3
|
+
config = <<~YAML
|
4
|
+
monthly:
|
5
|
+
- account: "Assets:Bank"
|
6
|
+
from: "2023-03-01"
|
7
|
+
transactions:
|
8
|
+
- amount: 2000.55
|
9
|
+
category: "Expenses:Mortgage"
|
10
|
+
description: Mortgage
|
11
|
+
to: "=24"
|
12
|
+
- amount: 100
|
13
|
+
category: "Expenses:Food"
|
14
|
+
description: Food
|
15
|
+
- account: "Assets:Savings"
|
16
|
+
from: "2023-03-01"
|
17
|
+
transactions:
|
18
|
+
- amount: -1000
|
19
|
+
category: "Assets:Bank"
|
20
|
+
description: Savings
|
21
|
+
|
22
|
+
custom:
|
23
|
+
- frequency: "every 2 weeks"
|
24
|
+
from: "2023-05-01"
|
25
|
+
account: "[Assets:Bank]"
|
26
|
+
roll-up: 26
|
27
|
+
transactions:
|
28
|
+
- amount: 80
|
29
|
+
category: "[Expenses:Personal Care]"
|
30
|
+
description: Hair and beauty
|
31
|
+
- frequency: "every 5 days"
|
32
|
+
from: "2023-05-01"
|
33
|
+
account: "[Assets:Checking]"
|
34
|
+
roll-up: 73
|
35
|
+
transactions:
|
36
|
+
- amount: 50
|
37
|
+
category: "[Expenses:Groceries]"
|
38
|
+
description: Gotta feed that stomach
|
39
|
+
|
40
|
+
settings:
|
41
|
+
currency: GBP
|
42
|
+
YAML
|
43
|
+
|
44
|
+
RSpec.describe HledgerForecast::Summarizer do
|
45
|
+
let(:summarizer) { described_class.new }
|
46
|
+
|
47
|
+
describe '#generate with roll_up' do
|
48
|
+
let(:forecast) { YAML.safe_load(config) }
|
49
|
+
let(:cli_options) { { roll_up: 'monthly' } }
|
50
|
+
|
51
|
+
before do
|
52
|
+
summarizer.summarize(config, cli_options)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'generates the correct output' do
|
56
|
+
output = summarizer.send(:generate, forecast)
|
57
|
+
|
58
|
+
expect(output.first).to include(:account, :from, :to, :type, :frequency)
|
59
|
+
expect(output.first[:amount]).to eq(2000.55)
|
60
|
+
expect(output.last[:rolled_up_amount]).to eq(304) # ((50 * 73) / 12)
|
61
|
+
expect(output.length).to eq(5)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'transaction TO date take precedence over block TO date' do
|
65
|
+
output = summarizer.send(:generate, forecast)
|
66
|
+
|
67
|
+
expect(output.first[:to]).to eq(Date.parse("2025-02-28"))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '#generate' do
|
72
|
+
let(:forecast) { YAML.safe_load(config) }
|
73
|
+
let(:cli_options) { nil }
|
74
|
+
|
75
|
+
before do
|
76
|
+
summarizer.summarize(config, cli_options)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'generates the correct output' do
|
80
|
+
output = summarizer.send(:generate, forecast)
|
81
|
+
|
82
|
+
# expect(output.first).to include(:account, :from, :to, :type, :frequency)
|
83
|
+
expect(output.length).to eq(5)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/spec/track_spec.rb
CHANGED
@@ -42,70 +42,13 @@ base_output = <<~JOURNAL
|
|
42
42
|
JOURNAL
|
43
43
|
|
44
44
|
RSpec.describe 'Tracking transactions -' do
|
45
|
-
it 'Determines which transactions should be tracked' do
|
46
|
-
generated = HledgerForecast::Generator
|
47
|
-
generated.generate(base_config)
|
48
|
-
tracked = generated.tracked
|
49
|
-
|
50
|
-
expect(tracked[0]['transaction']).to eq(
|
51
|
-
{ "amount" => "£3,000.00", "category" => "Expenses:Tax", "description" => "Tax owed",
|
52
|
-
"inverse_amount" => "£-3,000.00", "track" => true }
|
53
|
-
)
|
54
|
-
expect(tracked[0]['account']).to eq("Assets:Bank")
|
55
|
-
|
56
|
-
expect(tracked[1]['transaction']).to eq(
|
57
|
-
{ "amount" => "£-1,500.00", "category" => "Income:Salary", "description" => "Salary", "to" => "2023-08-01",
|
58
|
-
"inverse_amount" => "£1,500.00", "track" => true }
|
59
|
-
)
|
60
|
-
expect(tracked[1]['account']).to eq("Assets:Bank")
|
61
|
-
end
|
62
|
-
|
63
|
-
it 'marks a transaction as NOT FOUND if it doesnt exist' do
|
64
|
-
generated = HledgerForecast::Generator
|
65
|
-
generated.tracked = {} # Clear tracked transactions
|
66
|
-
generated.generate(base_config)
|
67
|
-
transactions_to_track = generated.tracked
|
68
|
-
|
69
|
-
track = HledgerForecast::Tracker.track(transactions_to_track, 'spec/stubs/transactions_not_found.journal')
|
70
|
-
|
71
|
-
expect(track[0]['found']).to eq(false)
|
72
|
-
expect(track[1]['found']).to eq(false)
|
73
|
-
end
|
74
|
-
|
75
|
-
it 'marks a transaction as FOUND if it exists' do
|
76
|
-
generated = HledgerForecast::Generator
|
77
|
-
generated.tracked = {} # Clear tracked transactions
|
78
|
-
generated.generate(base_config)
|
79
|
-
transactions_to_track = generated.tracked
|
80
|
-
|
81
|
-
track = HledgerForecast::Tracker.track(transactions_to_track, 'spec/stubs/transactions_found.journal')
|
82
|
-
|
83
|
-
expect(track[0]['found']).to eq(true)
|
84
|
-
expect(track[1]['found']).to eq(true)
|
85
|
-
end
|
86
|
-
|
87
|
-
it 'marks a transaction as FOUND if it exists, even if the category/amount are inversed' do
|
88
|
-
generated = HledgerForecast::Generator
|
89
|
-
generated.tracked = {} # Clear tracked transactions
|
90
|
-
generated.generate(base_config)
|
91
|
-
transactions_to_track = generated.tracked
|
92
|
-
|
93
|
-
track = HledgerForecast::Tracker.track(transactions_to_track, 'spec/stubs/transactions_found_inverse.journal')
|
94
|
-
|
95
|
-
expect(track[0]['found']).to eq(true)
|
96
|
-
end
|
97
|
-
|
98
45
|
it 'writes a NON-FOUND entry into a journal' do
|
99
46
|
options = {}
|
100
47
|
options[:transaction_file] = 'spec/stubs/transactions_not_found.journal'
|
101
48
|
|
102
|
-
|
103
|
-
generated.tracked = {} # Clear tracked transactions
|
104
|
-
|
105
|
-
generated_journal = generated.generate(base_config, options)
|
49
|
+
generated_journal = HledgerForecast::Generator.generate(base_config, options)
|
106
50
|
|
107
|
-
|
108
|
-
expect(generated_journal).to eq(expected_output)
|
51
|
+
expect(generated_journal).to eq(base_output)
|
109
52
|
end
|
110
53
|
|
111
54
|
it 'writes a NON-FOUND entry for dates that are close to the current period' do
|
@@ -147,12 +90,10 @@ RSpec.describe 'Tracking transactions -' do
|
|
147
90
|
options = {}
|
148
91
|
options[:transaction_file] = temp_file.path
|
149
92
|
|
150
|
-
|
151
|
-
generated.tracked = {} # Clear tracked transactions
|
152
|
-
|
153
|
-
generated_journal = generated.generate(forecast_config, options)
|
93
|
+
generated_journal = HledgerForecast::Generator.generate(forecast_config, options)
|
154
94
|
|
155
95
|
expected_output = <<~JOURNAL
|
96
|
+
|
156
97
|
~ #{next_month} * [TRACKED] New kitchen
|
157
98
|
Expenses:House £5,000.00; New kitchen
|
158
99
|
Assets:Bank
|
@@ -180,10 +121,7 @@ RSpec.describe 'Tracking transactions -' do
|
|
180
121
|
options = {}
|
181
122
|
options[:transaction_file] = 'spec/stubs/transactions_not_found.journal'
|
182
123
|
|
183
|
-
|
184
|
-
generated.tracked = {} # Clear tracked transactions
|
185
|
-
|
186
|
-
generated_journal = generated.generate(forecast_config, options)
|
124
|
+
generated_journal = HledgerForecast::Generator.generate(forecast_config, options)
|
187
125
|
|
188
126
|
output = <<~JOURNAL
|
189
127
|
~ monthly from #{next_month} * Food expenses
|