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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01faf9619f0b906b089956c1b81c16766830be412a678336848a7bbe23f78cb7
|
4
|
+
data.tar.gz: c93786627631d0f589250a8c721d8360a9feddf6f9b21954cc49b0b6b206ff29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 218ecb7ea26f4daee36981d345cf4a67052b6c3876f4f2090d608a86a10ba7e07889010fa91ab3618da60bfac98caf2c48bbdda78ab68f0290e744a580f75076
|
7
|
+
data.tar.gz: c3940bb307bbba32d374f0161ecaf5ca8bcdba2a1a0cafd0d1e4ea7f13ceea20a781dd0367eb02cd1c01fe73475f23fdcf1e520d3b76a9b7d875729971f221b7
|
data/.github/workflows/ci.yml
CHANGED
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,11 @@
|
|
1
|
-
|
1
|
+
<h1 align="center">Hledger-Forecast</h1>
|
2
2
|
|
3
|
-
|
3
|
+
<p align="center">
|
4
|
+
<a href="https://github.com/olimorris/hledger-forecast/stargazers"><img src="https://img.shields.io/github/stars/olimorris/hledger-forecast?color=c678dd&logoColor=e06c75&style=for-the-badge"></a>
|
5
|
+
<a href="https://github.com/olimorris/hledger-forecast/issues"><img src="https://img.shields.io/github/issues/olimorris/hledger-forecast?color=%23d19a66&style=for-the-badge"></a>
|
6
|
+
<a href="https://github.com/olimorris/hledger-forecast/blob/main/LICENSE"><img src="https://img.shields.io/github/license/olimorris/hledger-forecast?color=%2361afef&style=for-the-badge"></a>
|
7
|
+
<a href="https://github.com/olimorris/hledger-forecast/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/olimorris/hledger-forecast/ci.yml?branch=main&label=tests&style=for-the-badge"></a>
|
8
|
+
</p>
|
4
9
|
|
5
10
|
A wrapper which builds on [hledger's](https://github.com/simonmichael/hledger) [forecasting](https://hledger.org/dev/hledger.html#forecasting) capability. Uses a `yaml` config file to generate forecasts whilst adding functionality for future cost rises (e.g. inflation) and the automatic tracking of planned transactions.
|
6
11
|
|
@@ -42,7 +47,7 @@ The available options are:
|
|
42
47
|
|
43
48
|
### Generate command
|
44
49
|
|
45
|
-
The `hledger-forecast generate` command will generate a forecast
|
50
|
+
The `hledger-forecast generate` command will generate a forecast _from_ a `yaml` file _to_ a journal file. You can see the output of this command in the [example.journal](https://github.com/olimorris/hledger-forecast/blob/main/example.journal) file.
|
46
51
|
|
47
52
|
The available options are:
|
48
53
|
|
@@ -67,7 +72,7 @@ To work with hledger, include the forecast file and use the `--forecast` flag:
|
|
67
72
|
|
68
73
|
The command will generate a forecast up to the end of Feb 2024, showing the balance for any asset accounts, overlaying some bank transactions with the forecast journal file. Of course, refer to the [hledger](https://hledger.org/dev/hledger.html) documentation for more information on how to query your finances.
|
69
74
|
|
70
|
-
To apply any modifiers, use the `--auto` flag at the end of your command.
|
75
|
+
> **Note**: To apply any modifiers, use the `--auto` flag at the end of your command.
|
71
76
|
|
72
77
|
### Summarize command
|
73
78
|
|
@@ -84,9 +89,9 @@ The available options are:
|
|
84
89
|
|
85
90
|
## :gear: Configuration
|
86
91
|
|
87
|
-
### The
|
92
|
+
### The yaml file
|
88
93
|
|
89
|
-
> **Note**: See the [example.yml](https://github.com/olimorris/hledger-forecast/blob/main/example.yml) file for an example
|
94
|
+
> **Note**: See the [example.yml](https://github.com/olimorris/hledger-forecast/blob/main/example.yml) file for an example config and its corresponding [output](https://github.com/olimorris/hledger-forecast/blob/main/example.journal)
|
90
95
|
|
91
96
|
Firstly, create a `yaml` file which will contain the transactions you'd like to forecast:
|
92
97
|
|
@@ -175,6 +180,17 @@ monthly:
|
|
175
180
|
to: "2025-01-01"
|
176
181
|
```
|
177
182
|
|
183
|
+
It can also be useful to compute a `to` date by adding on a number of months to the `from` date. Extending the example above:
|
184
|
+
|
185
|
+
```yaml
|
186
|
+
- amount: 2000
|
187
|
+
category: "Expenses:Mortgage"
|
188
|
+
description: Mortgage
|
189
|
+
to: "=12"
|
190
|
+
```
|
191
|
+
|
192
|
+
This will take the `to` date to _2024-02-29_. This can be useful if you know a payment is due to end in _n_ months time and don't wish to use one of the many date calculators on the internet.
|
193
|
+
|
178
194
|
### Calculated amounts
|
179
195
|
|
180
196
|
> **Note**: Calculations will be determined up to two decimal places
|
@@ -191,7 +207,7 @@ monthly:
|
|
191
207
|
description: New Kitchen
|
192
208
|
```
|
193
209
|
|
194
|
-
Simply ensure that the amount starts with an `=` sign, is enclosed in quotation marks and uses standard mathematical notations.
|
210
|
+
Simply ensure that the amount starts with an `=` sign, is enclosed in quotation marks and uses standard mathematical notations. Of course, it may make sense to restrict this transaction with a `to` date in months, as per the [transaction level dates](#transaction-level) section.
|
195
211
|
|
196
212
|
### Tracking transactions
|
197
213
|
|
@@ -203,13 +219,13 @@ To mark transactions as available for tracking you may use the `track` option in
|
|
203
219
|
|
204
220
|
```yaml
|
205
221
|
once:
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
222
|
+
account: "Assets:Bank"
|
223
|
+
from: "2023-03-05"
|
224
|
+
transactions:
|
225
|
+
- amount: 3000
|
226
|
+
category: "Expenses:Shopping"
|
227
|
+
description: Refund for that damn laptop
|
228
|
+
track: true
|
213
229
|
```
|
214
230
|
|
215
231
|
> **Note**: This feature has been designed to work with one-off transactions only
|
@@ -220,7 +236,7 @@ To use this feature, ensure you pass a filepath to the `-t` flag, such as:
|
|
220
236
|
|
221
237
|
The app will use a hledger query to determine if the combination of category and amount is present in the periods between the `from` key and the current date in the journal file you've specified. If not, then the app will include it as a forecast transaction in the output file.
|
222
238
|
|
223
|
-
###
|
239
|
+
### Modifiers
|
224
240
|
|
225
241
|
> **Note**: For modifiers to be included in your hledger reporting, use the `--auto` flag
|
226
242
|
|
@@ -228,17 +244,17 @@ Within your forecasts, it can be useful to reflect future increases/decreases in
|
|
228
244
|
|
229
245
|
```yaml
|
230
246
|
monthly:
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
247
|
+
account: "Assets:Bank"
|
248
|
+
from: "2023-03-05"
|
249
|
+
transactions:
|
250
|
+
- amount: 450
|
251
|
+
category: "Expenses:Groceries"
|
252
|
+
description: Food shopping
|
253
|
+
modifiers:
|
254
|
+
- amount: 0.02
|
255
|
+
description: "Inflation"
|
256
|
+
from: "2024-01-01"
|
257
|
+
to: "2024-12-31"
|
242
258
|
```
|
243
259
|
|
244
260
|
This will generate an [auto-posting](https://hledger.org/dev/hledger.html#auto-postings) in your forecast which will
|
@@ -259,15 +275,15 @@ modifiers:
|
|
259
275
|
to: "2025-12-31"
|
260
276
|
```
|
261
277
|
|
262
|
-
### Additional settings
|
278
|
+
### Additional config settings
|
263
279
|
|
264
280
|
Additional settings in the config file to consider:
|
265
281
|
|
266
282
|
```yaml
|
267
283
|
settings:
|
268
|
-
currency: GBP
|
269
|
-
show_symbol: true
|
270
|
-
thousands_separator: true
|
284
|
+
currency: GBP # Specify the currency to use
|
285
|
+
show_symbol: true # Show the currency symbol?
|
286
|
+
thousands_separator: true # Separate thousands with a comma?
|
271
287
|
```
|
272
288
|
|
273
289
|
## :camera_flash: Screenshots
|
@@ -282,7 +298,7 @@ settings:
|
|
282
298
|
|
283
299
|
## :paintbrush: Rationale
|
284
300
|
|
285
|
-
Firstly, I've come to realise from reading countless blog and Reddit posts on [plain text accounting](https://plaintextaccounting.org), that everyone does it
|
301
|
+
Firstly, I've come to realise from reading countless blog and Reddit posts on [plain text accounting](https://plaintextaccounting.org), that everyone does it **completely** differently! There is _great_ support in hledger for [forecasting](https://hledger.org/1.29/hledger.html#forecasting) using periodic transactions. Infact, it's nearly perfect for my needs. My only wishes were to be able to sum up monthly transactions much faster (so I can see my forecasted monthly I&E), apply future cost pressures more easily (such as inflation) and to be able to track and monitor specific transactions.
|
286
302
|
|
287
303
|
Regarding the latter; I may be expecting a material amount of money to leave my account in May (perhaps for a holiday booking). But maybe, that booking ends up leaving in July instead. Whilst I would have accounted for that expense in my forecast, it will be tied to some date in May. So if that transaction doesn't appear in the "actuals" of my May bank statement (which I import into hledger), it won't be included in my forecast at all (as the latest transaction period will be greater than the forecast period). The impact is that my forecasted balance in any future month could be $X better off than reality. Being able to automatically look out for these transactions, and include them if they're not present, is a nice time saver.
|
288
304
|
|
data/hledger-forecast.gemspec
CHANGED
@@ -0,0 +1,21 @@
|
|
1
|
+
module HledgerForecast
|
2
|
+
# Calculate various
|
3
|
+
class Calculator
|
4
|
+
def initialize
|
5
|
+
@calculator = Dentaku::Calculator.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def evaluate(amount)
|
9
|
+
return amount unless amount.is_a?(String)
|
10
|
+
|
11
|
+
@calculator.evaluate(amount.slice(1..-1))
|
12
|
+
end
|
13
|
+
|
14
|
+
def evaluate_date(from, to)
|
15
|
+
return to unless to[0] == "="
|
16
|
+
|
17
|
+
# Subtract a day from the final date
|
18
|
+
(from >> @calculator.evaluate(to.slice(1..-1))) - 1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/hledger_forecast/cli.rb
CHANGED
@@ -155,8 +155,8 @@ module HledgerForecast
|
|
155
155
|
end
|
156
156
|
|
157
157
|
def self.summarize(options)
|
158
|
-
|
159
|
-
puts
|
158
|
+
config = File.read(options[:forecast_file])
|
159
|
+
puts Summarizer.summarize(config, options)
|
160
160
|
end
|
161
161
|
end
|
162
162
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module HledgerForecast
|
2
|
+
# Formats various items used throughout the application
|
3
|
+
class Formatter
|
4
|
+
def self.format_money(amount, settings)
|
5
|
+
Money.from_cents(amount.to_f * 100, (settings[:currency]) || 'USD').format(
|
6
|
+
symbol: settings[:show_symbol] || true,
|
7
|
+
sign_before_symbol: settings[:sign_before_symbol] || false,
|
8
|
+
thousands_separator: settings[:thousands_separator] ? ',' : nil
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.output_to_ledger(*compiled_data)
|
13
|
+
output = compiled_data.compact.map do |data|
|
14
|
+
data.map do |item|
|
15
|
+
next unless item[:transactions].any?
|
16
|
+
|
17
|
+
item[:header] + item[:transactions].join + item[:footer]
|
18
|
+
end.join
|
19
|
+
end.join("\n")
|
20
|
+
|
21
|
+
output.gsub(/\n{2,}/, "\n\n")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,282 +1,65 @@
|
|
1
1
|
module HledgerForecast
|
2
|
-
#
|
2
|
+
# Generate forecasts for hledger from a yaml config file
|
3
3
|
class Generator
|
4
|
-
|
5
|
-
|
4
|
+
def self.generate(config, cli_options = nil)
|
5
|
+
new.generate(config, cli_options)
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
self.tracked = {}
|
8
|
+
def generate(config, cli_options = nil)
|
9
|
+
forecast = YAML.safe_load(config)
|
10
|
+
@settings = Settings.config(forecast, cli_options)
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
output = {}
|
13
|
+
forecast.each do |period, blocks|
|
14
|
+
next if %w[settings].include?(period)
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
# @options[:sign_before_symbol] = forecast_data.fetch('settings', {}).fetch('sign_before_symbol', false)
|
20
|
-
@options[:thousands_separator] = forecast_data.fetch('settings', {}).fetch('thousands_separator', true)
|
21
|
-
end
|
22
|
-
|
23
|
-
def self.generate(yaml_file, options = nil)
|
24
|
-
forecast_data = YAML.safe_load(yaml_file)
|
25
|
-
|
26
|
-
set_options(forecast_data)
|
27
|
-
|
28
|
-
@calculator = Dentaku::Calculator.new
|
29
|
-
|
30
|
-
output = ""
|
31
|
-
|
32
|
-
# Generate regular transactions
|
33
|
-
forecast_data.each do |period, forecasts|
|
34
|
-
if period == 'custom'
|
35
|
-
output += custom_transaction(forecasts)
|
36
|
-
else
|
37
|
-
frequency = convert_period_to_frequency(period)
|
38
|
-
next unless frequency
|
39
|
-
|
40
|
-
forecasts.each do |forecast|
|
41
|
-
account = forecast['account']
|
42
|
-
from = Date.parse(forecast['from'])
|
43
|
-
to = forecast['to'] ? Date.parse(forecast['to']) : nil
|
44
|
-
transactions = forecast['transactions']
|
45
|
-
|
46
|
-
output += regular_transaction(frequency, from, to, transactions, account)
|
47
|
-
output += ending_transaction(frequency, from, transactions, account)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# Generate tracked transactions
|
53
|
-
if options && !options[:no_track] && !@tracked.empty?
|
54
|
-
if options[:transaction_file]
|
55
|
-
output += output_tracked_transaction(Tracker.track(@tracked,
|
56
|
-
options[:transaction_file]))
|
57
|
-
else
|
58
|
-
puts "\nWarning: ".yellow.bold + "You need to specify a transaction file with the `--t` flag for smart transactions to work\n"
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
output += output_modified_transaction(@modified) unless @modified.empty?
|
63
|
-
|
64
|
-
output
|
65
|
-
end
|
66
|
-
|
67
|
-
def self.regular_transaction(frequency, from, to, transactions, account)
|
68
|
-
transactions = transactions.select { |transaction| transaction['to'].nil? }
|
69
|
-
return "" if transactions.empty?
|
70
|
-
|
71
|
-
output = ""
|
72
|
-
|
73
|
-
transactions.each do |transaction|
|
74
|
-
if track_transaction?(transaction, from)
|
75
|
-
track_transaction(from, to, account, transaction)
|
76
|
-
next
|
77
|
-
end
|
78
|
-
|
79
|
-
modified_transaction(from, to, account, transaction)
|
80
|
-
|
81
|
-
output += output_transaction(transaction['category'], format_amount(calculate_amount(transaction['amount'])),
|
82
|
-
transaction['description'])
|
83
|
-
end
|
84
|
-
|
85
|
-
return "" unless output != ""
|
86
|
-
|
87
|
-
output = if to
|
88
|
-
"#{frequency} #{from} to #{to} * #{extract_descriptions(transactions,
|
89
|
-
from)}\n" << output
|
90
|
-
else
|
91
|
-
"#{frequency} #{from} * #{extract_descriptions(transactions, from)}\n" << output
|
92
|
-
end
|
93
|
-
|
94
|
-
output += " #{account}\n\n"
|
95
|
-
output
|
96
|
-
end
|
97
|
-
|
98
|
-
def self.ending_transaction(frequency, from, transactions, account)
|
99
|
-
output = ""
|
100
|
-
|
101
|
-
transactions.each do |transaction|
|
102
|
-
to = transaction['to'] ? Date.parse(transaction['to']) : nil
|
103
|
-
next unless to
|
104
|
-
|
105
|
-
if track_transaction?(transaction, from)
|
106
|
-
track_transaction(from, to, account, transaction)
|
107
|
-
next
|
108
|
-
end
|
109
|
-
|
110
|
-
modified_transaction(from, to, account, transaction)
|
111
|
-
|
112
|
-
output += "#{frequency} #{from} to #{to} * #{transaction['description']}\n"
|
113
|
-
output += output_transaction(transaction['category'], format_amount(calculate_amount(transaction['amount'])),
|
114
|
-
transaction['description'])
|
115
|
-
output += " #{account}\n\n"
|
116
|
-
end
|
117
|
-
|
118
|
-
output
|
119
|
-
end
|
120
|
-
|
121
|
-
def self.custom_transaction(forecasts)
|
122
|
-
output = ""
|
123
|
-
|
124
|
-
forecasts.each do |forecast|
|
125
|
-
account = forecast['account']
|
126
|
-
from = Date.parse(forecast['from'])
|
127
|
-
to = forecast['to'] ? Date.parse(forecast['to']) : nil
|
128
|
-
frequency = forecast['frequency']
|
129
|
-
transactions = forecast['transactions']
|
130
|
-
|
131
|
-
output += "~ #{frequency} from #{from} * #{extract_descriptions(transactions, from)}\n"
|
132
|
-
|
133
|
-
transactions.each do |transaction|
|
134
|
-
to = transaction['to'] ? Date.parse(transaction['to']) : to
|
135
|
-
|
136
|
-
if track_transaction?(transaction, from)
|
137
|
-
track_transaction(from, to, account, transaction)
|
138
|
-
next
|
139
|
-
end
|
140
|
-
|
141
|
-
modified_transaction(from, to, account, transaction)
|
142
|
-
|
143
|
-
output += output_transaction(transaction['category'], format_amount(calculate_amount(transaction['amount'])),
|
144
|
-
transaction['description'])
|
16
|
+
blocks.each do |block|
|
17
|
+
output[output.length] = process_block(period, block)
|
145
18
|
end
|
146
|
-
|
147
|
-
output += " #{account}\n\n"
|
148
|
-
end
|
149
|
-
|
150
|
-
output
|
151
|
-
end
|
152
|
-
|
153
|
-
def self.output_transaction(category, amount, description)
|
154
|
-
" #{category.ljust(@options[:max_category])} #{amount.ljust(@options[:max_amount])}; #{description}\n"
|
155
|
-
end
|
156
|
-
|
157
|
-
def self.output_modified_transaction(transactions)
|
158
|
-
output = ""
|
159
|
-
|
160
|
-
transactions.each do |_key, transaction|
|
161
|
-
date = "date:#{transaction['from']}"
|
162
|
-
date += "..#{transaction['to']}" if transaction['to']
|
163
|
-
|
164
|
-
output += "= #{transaction['category']} #{date}\n"
|
165
|
-
output += " #{transaction['category'].ljust(@options[:max_category])} *#{transaction['amount'].to_s.ljust(@options[:max_amount] - 1)}; #{transaction['description']}\n"
|
166
|
-
output += " #{transaction['account'].ljust(@options[:max_category])} *#{transaction['amount'] * -1}\n\n"
|
167
19
|
end
|
168
20
|
|
169
|
-
|
21
|
+
Formatter.output_to_ledger(
|
22
|
+
Transactions::Default.generate(output, @settings),
|
23
|
+
Transactions::Trackers.generate(output, @settings),
|
24
|
+
Transactions::Modifiers.generate(output, @settings)
|
25
|
+
)
|
170
26
|
end
|
171
27
|
|
172
|
-
|
173
|
-
output = ""
|
174
|
-
|
175
|
-
transactions.each do |_key, transaction|
|
176
|
-
next if transaction['found']
|
177
|
-
|
178
|
-
output += "~ #{transaction['from']} * [TRACKED] #{transaction['transaction']['description']}\n"
|
179
|
-
output += " #{transaction['transaction']['category'].ljust(@options[:max_category])} #{transaction['transaction']['amount'].ljust(@options[:max_amount])}; #{transaction['transaction']['description']}\n"
|
180
|
-
output += " #{transaction['account']}\n\n"
|
181
|
-
end
|
28
|
+
private
|
182
29
|
|
183
|
-
|
184
|
-
|
30
|
+
def process_block(period, block)
|
31
|
+
output = []
|
185
32
|
|
186
|
-
|
187
|
-
|
33
|
+
output << {
|
34
|
+
account: block['account'],
|
35
|
+
from: Date.parse(block['from']),
|
36
|
+
to: block['to'] ? Date.parse(block['to']) : nil,
|
37
|
+
type: period,
|
38
|
+
frequency: block['frequency'],
|
39
|
+
transactions: []
|
40
|
+
}
|
188
41
|
|
189
|
-
|
190
|
-
next if track_transaction?(transaction, from)
|
42
|
+
output = process_transactions(block, output)
|
191
43
|
|
192
|
-
|
193
|
-
|
44
|
+
output.map do |item|
|
45
|
+
transactions = item[:transactions].group_by { |t| t[:to] }
|
46
|
+
item.merge(transactions: transactions)
|
194
47
|
end
|
195
|
-
|
196
|
-
descriptions.join(', ')
|
197
48
|
end
|
198
49
|
|
199
|
-
def
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
'amount' => modifier['amount'],
|
209
|
-
'category' => transaction['category'],
|
210
|
-
'description' => description,
|
211
|
-
'from' => modifier['from'] ? Date.parse(modifier['from']) : (from || nil),
|
212
|
-
'to' => modifier['to'] ? Date.parse(modifier['to']) : (to || nil)
|
50
|
+
def process_transactions(block, output)
|
51
|
+
block['transactions'].each do |t|
|
52
|
+
output.last[:transactions] << {
|
53
|
+
category: t['category'],
|
54
|
+
amount: Formatter.format_money(Calculator.new.evaluate(t['amount']), @settings),
|
55
|
+
description: t['description'],
|
56
|
+
to: t['to'] ? Calculator.new.evaluate_date(Date.parse(block['from']), t['to']) : nil,
|
57
|
+
modifiers: t['modifiers'] ? Transactions::Modifiers.get_modifiers(t, block) : [],
|
58
|
+
track: Transactions::Trackers.track?(t, block, @settings) ? true : false
|
213
59
|
}
|
214
60
|
end
|
215
|
-
end
|
216
61
|
|
217
|
-
|
218
|
-
transaction['track'] && from <= Date.today
|
219
|
-
end
|
220
|
-
|
221
|
-
def self.track_transaction(from, to, account, transaction)
|
222
|
-
amount = calculate_amount(transaction['amount'])
|
223
|
-
transaction['amount'] = format_amount(amount)
|
224
|
-
transaction['inverse_amount'] = format_amount(amount * -1)
|
225
|
-
|
226
|
-
@tracked[@tracked.length] = {
|
227
|
-
'account' => account,
|
228
|
-
'from' => from,
|
229
|
-
'to' => to,
|
230
|
-
'transaction' => transaction
|
231
|
-
}
|
232
|
-
end
|
233
|
-
|
234
|
-
def self.convert_period_to_frequency(period)
|
235
|
-
map = {
|
236
|
-
'once' => '~',
|
237
|
-
'monthly' => '~ monthly from',
|
238
|
-
'quarterly' => '~ every 3 months from',
|
239
|
-
'half-yearly' => '~ every 6 months from',
|
240
|
-
'yearly' => '~ yearly from'
|
241
|
-
}
|
242
|
-
|
243
|
-
map[period]
|
244
|
-
end
|
245
|
-
|
246
|
-
def self.calculate_amount(amount)
|
247
|
-
return amount unless amount.is_a?(String)
|
248
|
-
|
249
|
-
@calculator.evaluate(amount.slice(1..-1))
|
250
|
-
end
|
251
|
-
|
252
|
-
def self.format_amount(amount)
|
253
|
-
Money.from_cents(amount.to_f * 100, @options[:currency]).format(
|
254
|
-
symbol: @options[:show_symbol],
|
255
|
-
sign_before_symbol: @options[:sign_before_symbol],
|
256
|
-
thousands_separator: @options[:thousands_separator] ? ',' : nil
|
257
|
-
)
|
258
|
-
end
|
259
|
-
|
260
|
-
def self.get_max_field_size(forecast_data, field)
|
261
|
-
max_size = 0
|
262
|
-
|
263
|
-
forecast_data.each do |period, forecasts|
|
264
|
-
next if period == 'settings'
|
265
|
-
|
266
|
-
forecasts.each do |forecast|
|
267
|
-
transactions = forecast['transactions']
|
268
|
-
transactions.each do |transaction|
|
269
|
-
field_value = if transaction[field].is_a?(Integer) || transaction[field].is_a?(Float)
|
270
|
-
((transaction[field] + 3) * 100).to_s
|
271
|
-
else
|
272
|
-
transaction[field].to_s
|
273
|
-
end
|
274
|
-
max_size = [max_size, field_value.length].max
|
275
|
-
end
|
276
|
-
end
|
277
|
-
end
|
278
|
-
|
279
|
-
max_size
|
62
|
+
output
|
280
63
|
end
|
281
64
|
end
|
282
65
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module HledgerForecast
|
2
|
+
# Set the options from a user's confgi
|
3
|
+
class Settings
|
4
|
+
def self.config(forecast, cli_options)
|
5
|
+
settings = {}
|
6
|
+
|
7
|
+
settings[:max_amount] = get_max_field_size(forecast, 'amount') + 1 # +1 for the negatives
|
8
|
+
settings[:max_category] = get_max_field_size(forecast, 'category')
|
9
|
+
|
10
|
+
settings[:currency] = Money::Currency.new(forecast.fetch('settings', {}).fetch('currency', 'USD'))
|
11
|
+
settings[:show_symbol] = forecast.fetch('settings', {}).fetch('show_symbol', true)
|
12
|
+
# settings[:sign_before_symbol] = forecast.fetch('settings', {}).fetch('sign_before_symbol', false)
|
13
|
+
settings[:thousands_separator] = forecast.fetch('settings', {}).fetch('thousands_separator', true)
|
14
|
+
|
15
|
+
settings.merge!(cli_options) if cli_options
|
16
|
+
|
17
|
+
settings
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.get_max_field_size(block, field)
|
21
|
+
max_size = 0
|
22
|
+
|
23
|
+
block.each do |period, items|
|
24
|
+
next if %w[settings].include?(period)
|
25
|
+
|
26
|
+
items.each do |item|
|
27
|
+
item['transactions'].each do |t|
|
28
|
+
field_value = if t[field].is_a?(Integer) || t[field].is_a?(Float)
|
29
|
+
((t[field] + 3) * 100).to_s
|
30
|
+
else
|
31
|
+
t[field].to_s
|
32
|
+
end
|
33
|
+
max_size = [max_size, field_value.length].max
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
max_size
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|