hledger-forecast 2.0.0 → 3.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/{test.yml → ci.yml} +18 -10
- data/.github/workflows/publish_ruby_gem.yml +24 -0
- data/.github/workflows/release.yml +12 -13
- data/.mise.toml +2 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +1 -0
- data/README.md +149 -119
- data/example.csv +15 -15
- data/example.journal +17 -18
- data/hledger-forecast.gemspec +20 -18
- data/lib/hledger_forecast/calculator.rb +7 -15
- data/lib/hledger_forecast/cli.rb +98 -71
- data/lib/hledger_forecast/comparator.rb +12 -11
- data/lib/hledger_forecast/forecast.rb +29 -0
- data/lib/hledger_forecast/formatter.rb +13 -15
- data/lib/hledger_forecast/generator.rb +32 -72
- data/lib/hledger_forecast/settings.rb +34 -47
- data/lib/hledger_forecast/summarizer.rb +34 -55
- data/lib/hledger_forecast/summarizer_formatter.rb +75 -78
- data/lib/hledger_forecast/transaction.rb +63 -0
- data/lib/hledger_forecast/transactions/default.rb +45 -72
- data/lib/hledger_forecast/version.rb +1 -1
- data/lib/hledger_forecast.rb +21 -22
- data/spec/calculator_spec.rb +45 -0
- data/spec/cli_spec.rb +19 -17
- data/spec/compare_spec.rb +16 -14
- data/spec/computed_amounts_spec.rb +7 -7
- data/spec/custom_spec.rb +9 -9
- data/spec/formatter_spec.rb +51 -0
- data/spec/half-yearly_spec.rb +5 -5
- data/spec/monthly_end_date_spec.rb +6 -6
- data/spec/monthly_end_date_transaction_spec.rb +10 -10
- data/spec/monthly_spec.rb +7 -7
- data/spec/once_spec.rb +5 -5
- data/spec/quarterly_spec.rb +5 -5
- data/spec/settings_spec.rb +101 -0
- data/spec/stubs/forecast.csv +4 -4
- data/spec/summarizer_spec.rb +28 -33
- data/spec/tags_spec.rb +92 -0
- data/spec/verbose_output_spec.rb +8 -8
- data/spec/yearly_spec.rb +5 -5
- metadata +49 -13
- data/lib/hledger_forecast/transactions/modifiers.rb +0 -90
- data/lib/hledger_forecast/transactions/trackers.rb +0 -88
- data/lib/hledger_forecast/utilities.rb +0 -14
- data/spec/track_spec.rb +0 -105
data/spec/summarizer_spec.rb
CHANGED
|
@@ -1,55 +1,50 @@
|
|
|
1
|
-
require_relative
|
|
1
|
+
require_relative "../lib/hledger_forecast"
|
|
2
2
|
|
|
3
3
|
config = <<~CSV
|
|
4
|
-
type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude
|
|
5
|
-
monthly,,Assets:Bank,01/03/2023,=24,Mortgage,Expenses:Mortgage,2000.55
|
|
6
|
-
monthly,,Assets:Bank,01/03/2023,,Food,Expenses:Food,100
|
|
7
|
-
monthly,,Assets:Savings,01/03/2023,,Savings,Assets:Bank,-1000
|
|
8
|
-
custom,every 2 weeks,[Assets:Bank],01/05/2023,,Hair and beauty,[Expenses:Personal Care],80,26
|
|
9
|
-
custom,every 2 weeks,[Assets:Checking],01/05/2023,,Extra Food,[Expenses:Groceries],50,73
|
|
4
|
+
type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude
|
|
5
|
+
monthly,,Assets:Bank,01/03/2023,=24,Mortgage,Expenses:Mortgage,2000.55,,
|
|
6
|
+
monthly,,Assets:Bank,01/03/2023,,Food,Expenses:Food,100,,
|
|
7
|
+
monthly,,Assets:Savings,01/03/2023,,Savings,Assets:Bank,-1000,,
|
|
8
|
+
custom,every 2 weeks,[Assets:Bank],01/05/2023,,Hair and beauty,[Expenses:Personal Care],80,26,
|
|
9
|
+
custom,every 2 weeks,[Assets:Checking],01/05/2023,,Extra Food,[Expenses:Groceries],50,73,
|
|
10
10
|
settings,currency,GBP,,,,,,,,
|
|
11
11
|
CSV
|
|
12
12
|
|
|
13
13
|
RSpec.describe HledgerForecast::Summarizer do
|
|
14
|
-
|
|
14
|
+
describe "#summarize with roll_up" do
|
|
15
|
+
let(:result) { described_class.summarize(config, {roll_up: "monthly"}) }
|
|
16
|
+
let(:output) { result[:output] }
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
let(:cli_options) { { roll_up: 'monthly' } }
|
|
19
|
-
|
|
20
|
-
before do
|
|
21
|
-
summarizer.summarize(config, cli_options)
|
|
18
|
+
it "includes the expected summary keys" do
|
|
19
|
+
expect(output.first).to(include(:account, :from, :to, :type, :frequency))
|
|
22
20
|
end
|
|
23
21
|
|
|
24
|
-
it
|
|
25
|
-
output
|
|
22
|
+
it "returns the raw amount on each row" do
|
|
23
|
+
expect(output.first[:amount]).to(eq(2000.55))
|
|
24
|
+
end
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
expect(output.
|
|
29
|
-
expect(output.last[:rolled_up_amount]).to eq((50.0 * 73.0) / 12.0) # ((50 * 73) / 12)
|
|
30
|
-
expect(output.length).to eq(5)
|
|
26
|
+
it "calculates rolled_up_amount for custom transactions" do
|
|
27
|
+
expect(output.last[:rolled_up_amount]).to(eq((50.0 * 73.0) / 12.0))
|
|
31
28
|
end
|
|
32
29
|
|
|
33
|
-
it
|
|
34
|
-
output
|
|
30
|
+
it "returns a row for each non-excluded transaction" do
|
|
31
|
+
expect(output.length).to(eq(5))
|
|
32
|
+
end
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
it "uses the calculated TO date from the CSV formula" do
|
|
35
|
+
expect(output.first[:to]).to(eq(Date.parse("2025-02-28")))
|
|
37
36
|
end
|
|
38
37
|
end
|
|
39
38
|
|
|
40
|
-
describe
|
|
41
|
-
let(:
|
|
42
|
-
let(:cli_options) { nil }
|
|
39
|
+
describe "#summarize without roll_up" do
|
|
40
|
+
let(:output) { described_class.summarize(config)[:output] }
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
it "returns a row for each non-excluded transaction" do
|
|
43
|
+
expect(output.length).to(eq(5))
|
|
46
44
|
end
|
|
47
45
|
|
|
48
|
-
it
|
|
49
|
-
output
|
|
50
|
-
|
|
51
|
-
# expect(output.first).to include(:account, :from, :to, :type, :frequency)
|
|
52
|
-
expect(output.length).to eq(5)
|
|
46
|
+
it "includes annualised_amount" do
|
|
47
|
+
expect(output.first).to(have_key(:annualised_amount))
|
|
53
48
|
end
|
|
54
49
|
end
|
|
55
50
|
end
|
data/spec/tags_spec.rb
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require_relative "../lib/hledger_forecast"
|
|
2
|
+
|
|
3
|
+
config = <<~CSV
|
|
4
|
+
type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,tag
|
|
5
|
+
monthly,,Assets:Bank,01/03/2023,,Salary,Income:Salary,-3500,,,fixed|essential
|
|
6
|
+
monthly,,Assets:Bank,01/03/2023,,Food,Expenses:Food,500,,,living|essential
|
|
7
|
+
monthly,,Assets:Bank,01/03/2023,,Netflix,Expenses:Subscriptions,15,,,living
|
|
8
|
+
settings,currency,GBP,,,,,,,,
|
|
9
|
+
CSV
|
|
10
|
+
|
|
11
|
+
config_without_tags = <<~CSV
|
|
12
|
+
type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,tag
|
|
13
|
+
monthly,,Assets:Bank,01/03/2023,,Salary,Income:Salary,-3500,,,
|
|
14
|
+
monthly,,Assets:Bank,01/03/2023,,Food,Expenses:Food,500,,,
|
|
15
|
+
settings,currency,GBP,,,,,,,,
|
|
16
|
+
CSV
|
|
17
|
+
|
|
18
|
+
RSpec.describe "tags" do
|
|
19
|
+
it "outputs hledger tags in posting comments" do
|
|
20
|
+
expected = <<~JOURNAL
|
|
21
|
+
~ monthly from 2023-03-01 * Salary, Food, Netflix
|
|
22
|
+
Income:Salary £-3,500.00; fixed:, essential:
|
|
23
|
+
Expenses:Food £500.00 ; living:, essential:
|
|
24
|
+
Expenses:Subscriptions £15.00 ; living:
|
|
25
|
+
Assets:Bank
|
|
26
|
+
|
|
27
|
+
JOURNAL
|
|
28
|
+
|
|
29
|
+
expect(HledgerForecast::Generator.generate(config)).to(eq(expected))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "omits comments when no tags are present" do
|
|
33
|
+
expected = <<~JOURNAL
|
|
34
|
+
~ monthly from 2023-03-01 * Salary, Food
|
|
35
|
+
Income:Salary £-3,500.00
|
|
36
|
+
Expenses:Food £500.00
|
|
37
|
+
Assets:Bank
|
|
38
|
+
|
|
39
|
+
JOURNAL
|
|
40
|
+
|
|
41
|
+
expect(HledgerForecast::Generator.generate(config_without_tags)).to(eq(expected))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "filters transactions by a single tag" do
|
|
45
|
+
expected = <<~JOURNAL
|
|
46
|
+
~ monthly from 2023-03-01 * Food, Netflix
|
|
47
|
+
Expenses:Food £500.00; living:, essential:
|
|
48
|
+
Expenses:Subscriptions £15.00 ; living:
|
|
49
|
+
Assets:Bank
|
|
50
|
+
|
|
51
|
+
JOURNAL
|
|
52
|
+
|
|
53
|
+
expect(HledgerForecast::Generator.generate(config, {tags: ["living"]})).to(eq(expected))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "filters transactions by multiple tags (OR logic)" do
|
|
57
|
+
expected = <<~JOURNAL
|
|
58
|
+
~ monthly from 2023-03-01 * Salary, Food
|
|
59
|
+
Income:Salary £-3,500.00; fixed:, essential:
|
|
60
|
+
Expenses:Food £500.00 ; living:, essential:
|
|
61
|
+
Assets:Bank
|
|
62
|
+
|
|
63
|
+
JOURNAL
|
|
64
|
+
|
|
65
|
+
expect(HledgerForecast::Generator.generate(config, {tags: ["fixed", "essential"]})).to(eq(expected))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "filters summarizer output by tags" do
|
|
69
|
+
result = HledgerForecast::Summarizer.summarize(config, {tags: ["living"]})
|
|
70
|
+
descriptions = result[:output].map { |r| r[:description] }
|
|
71
|
+
|
|
72
|
+
expect(descriptions).to(eq(["Food", "Netflix"]))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "raises an error when --tags is used on a CSV without a tag column" do
|
|
76
|
+
csv_without_tag_column = <<~CSV
|
|
77
|
+
type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude
|
|
78
|
+
monthly,,Assets:Bank,01/03/2023,,Salary,Income:Salary,-3500,,
|
|
79
|
+
settings,currency,GBP,,,,,,,,
|
|
80
|
+
CSV
|
|
81
|
+
|
|
82
|
+
expect {
|
|
83
|
+
HledgerForecast::Generator.generate(csv_without_tag_column, {tags: ["fixed"]})
|
|
84
|
+
}
|
|
85
|
+
.to(raise_error(RuntimeError, /tag.*column/i))
|
|
86
|
+
|
|
87
|
+
expect {
|
|
88
|
+
HledgerForecast::Summarizer.summarize(csv_without_tag_column, {tags: ["fixed"]})
|
|
89
|
+
}
|
|
90
|
+
.to(raise_error(RuntimeError, /tag.*column/i))
|
|
91
|
+
end
|
|
92
|
+
end
|
data/spec/verbose_output_spec.rb
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
require_relative
|
|
1
|
+
require_relative "../lib/hledger_forecast"
|
|
2
2
|
|
|
3
3
|
output = <<~JOURNAL
|
|
4
4
|
~ monthly from 2023-03-01 * Mortgage
|
|
5
|
-
Expenses:Mortgage £2,000.55
|
|
5
|
+
Expenses:Mortgage £2,000.55
|
|
6
6
|
Assets:Bank
|
|
7
7
|
|
|
8
8
|
~ monthly from 2023-03-01 * Food
|
|
9
|
-
Expenses:Food £100.00
|
|
9
|
+
Expenses:Food £100.00
|
|
10
10
|
Assets:Bank
|
|
11
11
|
|
|
12
12
|
~ monthly from 2023-03-01 * Savings
|
|
13
|
-
Assets:Bank £-1,000.00
|
|
13
|
+
Assets:Bank £-1,000.00
|
|
14
14
|
Assets:Savings
|
|
15
15
|
|
|
16
16
|
JOURNAL
|
|
17
17
|
|
|
18
|
-
RSpec.describe
|
|
19
|
-
it
|
|
20
|
-
generated_journal =
|
|
18
|
+
RSpec.describe "verbose command" do
|
|
19
|
+
it "does not group similar type transactions together in the output" do
|
|
20
|
+
generated_journal = "./test_output.journal"
|
|
21
21
|
File.delete(generated_journal) if File.exist?(generated_journal)
|
|
22
22
|
|
|
23
23
|
system("./bin/hledger-forecast generate -f ./spec/stubs/forecast.csv -o ./test_output.journal --verbose --force")
|
|
24
24
|
|
|
25
|
-
expect(File.read(generated_journal)).to
|
|
25
|
+
expect(File.read(generated_journal)).to(eq(output))
|
|
26
26
|
end
|
|
27
27
|
end
|
data/spec/yearly_spec.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require_relative
|
|
1
|
+
require_relative "../lib/hledger_forecast"
|
|
2
2
|
|
|
3
3
|
config = <<~CSV
|
|
4
4
|
type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
|
|
@@ -8,13 +8,13 @@ CSV
|
|
|
8
8
|
|
|
9
9
|
output = <<~JOURNAL
|
|
10
10
|
~ yearly from 2023-04-01 * Bonus
|
|
11
|
-
Income:Bonus £-3,000.00
|
|
11
|
+
Income:Bonus £-3,000.00
|
|
12
12
|
Assets:Bank
|
|
13
13
|
|
|
14
14
|
JOURNAL
|
|
15
15
|
|
|
16
|
-
RSpec.describe
|
|
17
|
-
it
|
|
18
|
-
expect(HledgerForecast::Generator.generate(config)).to
|
|
16
|
+
RSpec.describe "generate" do
|
|
17
|
+
it "generates a forecast with correct YEARLY transactions" do
|
|
18
|
+
expect(HledgerForecast::Generator.generate(config)).to(eq(output))
|
|
19
19
|
end
|
|
20
20
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,43 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hledger-forecast
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oli Morris
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-03-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: abbrev
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: csv
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.0'
|
|
13
41
|
- !ruby/object:Gem::Dependency
|
|
14
42
|
name: colorize
|
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -101,10 +129,13 @@ executables:
|
|
|
101
129
|
extensions: []
|
|
102
130
|
extra_rdoc_files: []
|
|
103
131
|
files:
|
|
132
|
+
- ".github/workflows/ci.yml"
|
|
133
|
+
- ".github/workflows/publish_ruby_gem.yml"
|
|
104
134
|
- ".github/workflows/release.yml"
|
|
105
|
-
- ".github/workflows/test.yml"
|
|
106
135
|
- ".gitignore"
|
|
136
|
+
- ".mise.toml"
|
|
107
137
|
- ".rubocop.yml"
|
|
138
|
+
- CHANGELOG.md
|
|
108
139
|
- Gemfile
|
|
109
140
|
- LICENSE
|
|
110
141
|
- README.md
|
|
@@ -116,26 +147,28 @@ files:
|
|
|
116
147
|
- lib/hledger_forecast/calculator.rb
|
|
117
148
|
- lib/hledger_forecast/cli.rb
|
|
118
149
|
- lib/hledger_forecast/comparator.rb
|
|
150
|
+
- lib/hledger_forecast/forecast.rb
|
|
119
151
|
- lib/hledger_forecast/formatter.rb
|
|
120
152
|
- lib/hledger_forecast/generator.rb
|
|
121
153
|
- lib/hledger_forecast/settings.rb
|
|
122
154
|
- lib/hledger_forecast/summarizer.rb
|
|
123
155
|
- lib/hledger_forecast/summarizer_formatter.rb
|
|
156
|
+
- lib/hledger_forecast/transaction.rb
|
|
124
157
|
- lib/hledger_forecast/transactions/default.rb
|
|
125
|
-
- lib/hledger_forecast/transactions/modifiers.rb
|
|
126
|
-
- lib/hledger_forecast/transactions/trackers.rb
|
|
127
|
-
- lib/hledger_forecast/utilities.rb
|
|
128
158
|
- lib/hledger_forecast/version.rb
|
|
159
|
+
- spec/calculator_spec.rb
|
|
129
160
|
- spec/cli_spec.rb
|
|
130
161
|
- spec/compare_spec.rb
|
|
131
162
|
- spec/computed_amounts_spec.rb
|
|
132
163
|
- spec/custom_spec.rb
|
|
164
|
+
- spec/formatter_spec.rb
|
|
133
165
|
- spec/half-yearly_spec.rb
|
|
134
166
|
- spec/monthly_end_date_spec.rb
|
|
135
167
|
- spec/monthly_end_date_transaction_spec.rb
|
|
136
168
|
- spec/monthly_spec.rb
|
|
137
169
|
- spec/once_spec.rb
|
|
138
170
|
- spec/quarterly_spec.rb
|
|
171
|
+
- spec/settings_spec.rb
|
|
139
172
|
- spec/stubs/forecast.csv
|
|
140
173
|
- spec/stubs/output1.csv
|
|
141
174
|
- spec/stubs/output2.csv
|
|
@@ -143,14 +176,14 @@ files:
|
|
|
143
176
|
- spec/stubs/transactions_found_inverse.journal
|
|
144
177
|
- spec/stubs/transactions_not_found.journal
|
|
145
178
|
- spec/summarizer_spec.rb
|
|
146
|
-
- spec/
|
|
179
|
+
- spec/tags_spec.rb
|
|
147
180
|
- spec/verbose_output_spec.rb
|
|
148
181
|
- spec/yearly_spec.rb
|
|
149
182
|
homepage: https://github.com/olimorris/hledger-forecast
|
|
150
183
|
licenses:
|
|
151
184
|
- MIT
|
|
152
185
|
metadata: {}
|
|
153
|
-
post_install_message:
|
|
186
|
+
post_install_message:
|
|
154
187
|
rdoc_options: []
|
|
155
188
|
require_paths:
|
|
156
189
|
- lib
|
|
@@ -158,28 +191,31 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
158
191
|
requirements:
|
|
159
192
|
- - "~>"
|
|
160
193
|
- !ruby/object:Gem::Version
|
|
161
|
-
version: '3.
|
|
194
|
+
version: '3.3'
|
|
162
195
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
163
196
|
requirements:
|
|
164
197
|
- - ">="
|
|
165
198
|
- !ruby/object:Gem::Version
|
|
166
199
|
version: '0'
|
|
167
200
|
requirements: []
|
|
168
|
-
rubygems_version: 3.
|
|
169
|
-
signing_key:
|
|
201
|
+
rubygems_version: 3.4.20
|
|
202
|
+
signing_key:
|
|
170
203
|
specification_version: 4
|
|
171
204
|
summary: An extended wrapper around hledger's forecasting functionality
|
|
172
205
|
test_files:
|
|
206
|
+
- spec/calculator_spec.rb
|
|
173
207
|
- spec/cli_spec.rb
|
|
174
208
|
- spec/compare_spec.rb
|
|
175
209
|
- spec/computed_amounts_spec.rb
|
|
176
210
|
- spec/custom_spec.rb
|
|
211
|
+
- spec/formatter_spec.rb
|
|
177
212
|
- spec/half-yearly_spec.rb
|
|
178
213
|
- spec/monthly_end_date_spec.rb
|
|
179
214
|
- spec/monthly_end_date_transaction_spec.rb
|
|
180
215
|
- spec/monthly_spec.rb
|
|
181
216
|
- spec/once_spec.rb
|
|
182
217
|
- spec/quarterly_spec.rb
|
|
218
|
+
- spec/settings_spec.rb
|
|
183
219
|
- spec/stubs/forecast.csv
|
|
184
220
|
- spec/stubs/output1.csv
|
|
185
221
|
- spec/stubs/output2.csv
|
|
@@ -187,6 +223,6 @@ test_files:
|
|
|
187
223
|
- spec/stubs/transactions_found_inverse.journal
|
|
188
224
|
- spec/stubs/transactions_not_found.journal
|
|
189
225
|
- spec/summarizer_spec.rb
|
|
190
|
-
- spec/
|
|
226
|
+
- spec/tags_spec.rb
|
|
191
227
|
- spec/verbose_output_spec.rb
|
|
192
228
|
- spec/yearly_spec.rb
|
|
@@ -1,90 +0,0 @@
|
|
|
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
|
|
@@ -1,88 +0,0 @@
|
|
|
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(forecast, options)
|
|
13
|
-
new(forecast, options).generate
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def generate
|
|
17
|
-
return if @options[:no_track]
|
|
18
|
-
return nil unless tracked?(forecast)
|
|
19
|
-
|
|
20
|
-
forecast.each do |row|
|
|
21
|
-
process_tracked(row)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
output
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def self.track?(row, options)
|
|
28
|
-
now = Date.today
|
|
29
|
-
row['track'] && Date.parse(row['from']) <= now && !exists?(row, now, options)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.exists?(row, now, options)
|
|
33
|
-
unless options[:transaction_file]
|
|
34
|
-
puts "\nWarning: ".bold.yellow + "For tracked transactions, please specify a file with the `-t` flag"
|
|
35
|
-
puts "ERROR: ".bold.red + "Tracked transactions ignored for now"
|
|
36
|
-
return
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Format the money
|
|
40
|
-
amount = Formatter.format_money(row['amount'], options)
|
|
41
|
-
inverse_amount = Formatter.format_money(row['amount'] * -1, options)
|
|
42
|
-
|
|
43
|
-
from = Date.parse(row['from'])
|
|
44
|
-
category = row['category'].gsub('[', '\\[').gsub(']', '\\]').gsub('(', '\\(').gsub(')', '\\)')
|
|
45
|
-
|
|
46
|
-
# We run two commands and check to see if category +/- amount or account +/- amount exists
|
|
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})")
|
|
49
|
-
|
|
50
|
-
system(command1) || system(command2)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
attr_reader :forecast, :options, :output
|
|
56
|
-
|
|
57
|
-
def initialize(forecast, options)
|
|
58
|
-
@forecast = forecast
|
|
59
|
-
@options = options
|
|
60
|
-
@output = []
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def tracked?(forecast)
|
|
64
|
-
forecast.any? do |row|
|
|
65
|
-
return true if row[:track] == true
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
return false
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def process_tracked(row)
|
|
72
|
-
row[:transactions].each do |t|
|
|
73
|
-
next if t[:track] == false
|
|
74
|
-
|
|
75
|
-
category = t[:category].ljust(options[:max_category])
|
|
76
|
-
amount = t[:amount].to_s.ljust(options[:max_amount])
|
|
77
|
-
|
|
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"
|
|
82
|
-
|
|
83
|
-
output << { header: header, transactions: [transactions], footer: footer }
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
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/spec/track_spec.rb
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
require_relative '../lib/hledger_forecast'
|
|
2
|
-
|
|
3
|
-
current_month = Date.new(Date.today.year, Date.today.month, 1)
|
|
4
|
-
previous_month = current_month.prev_month
|
|
5
|
-
next_month = current_month.next_month
|
|
6
|
-
|
|
7
|
-
base_config = <<~CSV
|
|
8
|
-
type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
|
|
9
|
-
once,,Assets:Bank,05/03/2023,,Tax owed,Expenses:Tax,3000,,,true
|
|
10
|
-
once,,Assets:Bank,05/03/2023,,Food expenses,Expenses:Food,100,,,
|
|
11
|
-
once,,Assets:Bank,05/03/2023,01/08/2023,Salary,Income:Salary,-1500,,,true
|
|
12
|
-
settings,currency,GBP,,,,,,,,
|
|
13
|
-
CSV
|
|
14
|
-
|
|
15
|
-
base_output = <<~JOURNAL
|
|
16
|
-
~ 2023-03-05 * Food expenses
|
|
17
|
-
Expenses:Food £100.00 ; Food expenses
|
|
18
|
-
Assets:Bank
|
|
19
|
-
|
|
20
|
-
~ #{next_month} * [TRACKED] Tax owed
|
|
21
|
-
Expenses:Tax £3,000.00; Tax owed
|
|
22
|
-
Assets:Bank
|
|
23
|
-
|
|
24
|
-
~ #{next_month} * [TRACKED] Salary
|
|
25
|
-
Income:Salary £-1,500.00; Salary
|
|
26
|
-
Assets:Bank
|
|
27
|
-
|
|
28
|
-
JOURNAL
|
|
29
|
-
|
|
30
|
-
RSpec.describe 'Tracking transactions -' do
|
|
31
|
-
it 'writes a NON-FOUND entry into a journal' do
|
|
32
|
-
options = {}
|
|
33
|
-
options[:transaction_file] = 'spec/stubs/transactions_not_found.journal'
|
|
34
|
-
|
|
35
|
-
generated_journal = HledgerForecast::Generator.generate(base_config, options)
|
|
36
|
-
|
|
37
|
-
expect(generated_journal).to eq(base_output)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
it 'writes a NON-FOUND entry for dates that are close to the current period' do
|
|
41
|
-
require 'tempfile'
|
|
42
|
-
|
|
43
|
-
forecast_config = <<~CSV
|
|
44
|
-
type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
|
|
45
|
-
once,,Assets:Bank,"#{previous_month}",,New kitchen,Expenses:House,5000,,,true
|
|
46
|
-
settings,currency,GBP,,,,,,,,
|
|
47
|
-
CSV
|
|
48
|
-
|
|
49
|
-
journal = <<~JOURNAL
|
|
50
|
-
#{previous_month} * Opening balance
|
|
51
|
-
Assets:Bank £1,000.00
|
|
52
|
-
Equity:Opening balance
|
|
53
|
-
|
|
54
|
-
#{previous_month} * Mortgage payment
|
|
55
|
-
Expenses:Mortgage £1,500.00
|
|
56
|
-
Assets:Bank
|
|
57
|
-
|
|
58
|
-
#{current_month - 10} * Groceries
|
|
59
|
-
Expenses:Groceries £1,500.00
|
|
60
|
-
Assets:Bank
|
|
61
|
-
|
|
62
|
-
JOURNAL
|
|
63
|
-
|
|
64
|
-
temp_file = Tempfile.new('journal')
|
|
65
|
-
temp_file.write(journal)
|
|
66
|
-
temp_file.close
|
|
67
|
-
|
|
68
|
-
options = {}
|
|
69
|
-
options[:transaction_file] = temp_file.path
|
|
70
|
-
|
|
71
|
-
generated_journal = HledgerForecast::Generator.generate(forecast_config, options)
|
|
72
|
-
|
|
73
|
-
expected_output = <<~JOURNAL
|
|
74
|
-
|
|
75
|
-
~ #{next_month} * [TRACKED] New kitchen
|
|
76
|
-
Expenses:House £5,000.00; New kitchen
|
|
77
|
-
Assets:Bank
|
|
78
|
-
|
|
79
|
-
JOURNAL
|
|
80
|
-
|
|
81
|
-
expect(generated_journal).to eq(expected_output)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
it 'treats a future tracked transaction as a regular transaction' do
|
|
85
|
-
forecast_config = <<~CSV
|
|
86
|
-
type,frequency,account,from,to,description,category,amount,roll-up,summary_exclude,track
|
|
87
|
-
monthly,,Assets:Bank,"#{next_month}",,Food expenses,Expenses:Food,100,,,true
|
|
88
|
-
settings,currency,GBP,,,,,,,,
|
|
89
|
-
CSV
|
|
90
|
-
|
|
91
|
-
options = {}
|
|
92
|
-
options[:transaction_file] = 'spec/stubs/transactions_not_found.journal'
|
|
93
|
-
|
|
94
|
-
generated_journal = HledgerForecast::Generator.generate(forecast_config, options)
|
|
95
|
-
|
|
96
|
-
output = <<~JOURNAL
|
|
97
|
-
~ monthly from #{next_month} * Food expenses
|
|
98
|
-
Expenses:Food £100.00 ; Food expenses
|
|
99
|
-
Assets:Bank
|
|
100
|
-
|
|
101
|
-
JOURNAL
|
|
102
|
-
|
|
103
|
-
expect(generated_journal).to eq(output)
|
|
104
|
-
end
|
|
105
|
-
end
|