hledger-forecast 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -0
- data/README.md +68 -33
- data/example.journal +6 -1
- data/example.yml +3 -0
- data/hledger-forecast.gemspec +2 -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 -249
- 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 +11 -4
- data/spec/computed_amounts_spec.rb +35 -0
- 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 +28 -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,16 +1,22 @@
|
|
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
|
-
A wrapper which builds on [hledger's](https://github.com/simonmichael/hledger) [forecasting](https://hledger.org/dev/hledger.html#forecasting) capability. Uses a `
|
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
|
|
7
12
|
See the [rationale](#brain-rationale) section for why this gem may be useful to you.
|
8
13
|
|
9
14
|
## :sparkles: Features
|
10
15
|
|
11
|
-
- :book: Uses simple
|
16
|
+
- :book: Uses a simple yaml file to generate forecasts which can be used with hledger
|
12
17
|
- :date: Can smartly track forecasted transactions against actuals
|
13
18
|
- :moneybag: Can automatically apply modifiers such as inflation/deflation to forecasts
|
19
|
+
- :abacus: Supports calculated amounts in forecasts (uses the [Dentaku](https://github.com/rubysolo/dentaku) gem)
|
14
20
|
- :heavy_dollar_sign: Full currency support (uses the [RubyMoney](https://github.com/RubyMoney/money) gem)
|
15
21
|
- :computer: Simple and easy to use CLI
|
16
22
|
- :chart_with_upwards_trend: Summarize your forecasts by period and category and output to the CLI
|
@@ -41,7 +47,7 @@ The available options are:
|
|
41
47
|
|
42
48
|
### Generate command
|
43
49
|
|
44
|
-
The `hledger-forecast generate` command will
|
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.
|
45
51
|
|
46
52
|
The available options are:
|
47
53
|
|
@@ -66,7 +72,7 @@ To work with hledger, include the forecast file and use the `--forecast` flag:
|
|
66
72
|
|
67
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.
|
68
74
|
|
69
|
-
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.
|
70
76
|
|
71
77
|
### Summarize command
|
72
78
|
|
@@ -83,9 +89,9 @@ The available options are:
|
|
83
89
|
|
84
90
|
## :gear: Configuration
|
85
91
|
|
86
|
-
### The
|
92
|
+
### The yaml file
|
87
93
|
|
88
|
-
> **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)
|
89
95
|
|
90
96
|
Firstly, create a `yaml` file which will contain the transactions you'd like to forecast:
|
91
97
|
|
@@ -174,6 +180,35 @@ monthly:
|
|
174
180
|
to: "2025-01-01"
|
175
181
|
```
|
176
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
|
+
|
194
|
+
### Calculated amounts
|
195
|
+
|
196
|
+
> **Note**: Calculations will be determined up to two decimal places
|
197
|
+
|
198
|
+
It may be helpful to let the app calculate the forecasted amount in your transactions on your behalf. This can be especially useful if you're spreading a payment out over a number of months:
|
199
|
+
|
200
|
+
```yaml
|
201
|
+
monthly:
|
202
|
+
- account: "Liabilities:Amex"
|
203
|
+
from: "2023-05-01"
|
204
|
+
transactions:
|
205
|
+
- amount: "=5000/24"
|
206
|
+
category: "Expenses:House"
|
207
|
+
description: New Kitchen
|
208
|
+
```
|
209
|
+
|
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.
|
211
|
+
|
177
212
|
### Tracking transactions
|
178
213
|
|
179
214
|
> **Note**: Marking a transaction for tracking will ensure that it is only written into the forecast if it isn't found within a specified transaction file
|
@@ -184,13 +219,13 @@ To mark transactions as available for tracking you may use the `track` option in
|
|
184
219
|
|
185
220
|
```yaml
|
186
221
|
once:
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
194
229
|
```
|
195
230
|
|
196
231
|
> **Note**: This feature has been designed to work with one-off transactions only
|
@@ -201,7 +236,7 @@ To use this feature, ensure you pass a filepath to the `-t` flag, such as:
|
|
201
236
|
|
202
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.
|
203
238
|
|
204
|
-
###
|
239
|
+
### Modifiers
|
205
240
|
|
206
241
|
> **Note**: For modifiers to be included in your hledger reporting, use the `--auto` flag
|
207
242
|
|
@@ -209,17 +244,17 @@ Within your forecasts, it can be useful to reflect future increases/decreases in
|
|
209
244
|
|
210
245
|
```yaml
|
211
246
|
monthly:
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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"
|
223
258
|
```
|
224
259
|
|
225
260
|
This will generate an [auto-posting](https://hledger.org/dev/hledger.html#auto-postings) in your forecast which will
|
@@ -240,15 +275,15 @@ modifiers:
|
|
240
275
|
to: "2025-12-31"
|
241
276
|
```
|
242
277
|
|
243
|
-
### Additional settings
|
278
|
+
### Additional config settings
|
244
279
|
|
245
280
|
Additional settings in the config file to consider:
|
246
281
|
|
247
282
|
```yaml
|
248
283
|
settings:
|
249
|
-
currency: GBP
|
250
|
-
show_symbol: true
|
251
|
-
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?
|
252
287
|
```
|
253
288
|
|
254
289
|
## :camera_flash: Screenshots
|
@@ -263,9 +298,9 @@ settings:
|
|
263
298
|
|
264
299
|
## :paintbrush: Rationale
|
265
300
|
|
266
|
-
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.
|
267
302
|
|
268
|
-
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
|
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.
|
269
304
|
|
270
305
|
Also, I like to look ahead up to 3 years at a time and understand what my bank balances might look like. For this to be really accurate, factors such as inflation and salary expectations should be included. This is where the idea for modifiers came in. Being able to apply a percentage to a given category between two dates and automatically have the impact included any extended forecasts.
|
271
306
|
|
data/example.journal
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
~ monthly from 2023-03-01 * Bonus, Salary, Food, New cell phone
|
1
|
+
~ monthly from 2023-03-01 * Bonus, Salary, Food, New cell phone, Holiday savings
|
2
2
|
Income:Bonus £-100.00 ; Bonus
|
3
3
|
Income:Salary £-2,000.00; Salary
|
4
4
|
Expenses:Food £500.00 ; Food
|
5
5
|
Expenses:Phone £75.00 ; New cell phone
|
6
|
+
Expenses:Holiday £208.33 ; Holiday savings
|
6
7
|
Assets:Bank
|
7
8
|
|
8
9
|
~ monthly from 2023-03-01 to 2024-01-01 * Mortgage
|
@@ -29,6 +30,10 @@
|
|
29
30
|
Expenses:Car:Fuel £150.00 ; Car fuel
|
30
31
|
Assets:Bank
|
31
32
|
|
33
|
+
~ 2023-06-01 * [TRACKED] Forecast new car cost
|
34
|
+
Expenses:Car £5,000.00 ; Forecast new car cost
|
35
|
+
Assets:Bank
|
36
|
+
|
32
37
|
= Expenses:Food date:2024-01-01..2024-12-31
|
33
38
|
Expenses:Food *0.1 ; Food - Inflation
|
34
39
|
Assets:Bank *-0.1
|
data/example.yml
CHANGED
data/hledger-forecast.gemspec
CHANGED
@@ -4,6 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'hledger_forecast/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
|
+
s.required_ruby_version = '~> 3.0'
|
7
8
|
s.name = 'hledger-forecast'
|
8
9
|
s.version = HledgerForecast::VERSION
|
9
10
|
s.authors = ['Oli Morris']
|
@@ -14,6 +15,7 @@ Gem::Specification.new do |s|
|
|
14
15
|
s.license = 'MIT'
|
15
16
|
|
16
17
|
s.add_dependency "colorize", "~> 0.8.1"
|
18
|
+
s.add_dependency "dentaku", "~> 3.5.1"
|
17
19
|
s.add_dependency "highline", "~> 2.1.0"
|
18
20
|
s.add_dependency "money", "~> 6.16.0"
|
19
21
|
s.add_dependency "terminal-table", "~> 3.0.2"
|
@@ -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,273 +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
|
-
|
8
|
+
def generate(config, cli_options = nil)
|
9
|
+
forecast = YAML.safe_load(config)
|
10
|
+
@settings = Settings.config(forecast, cli_options)
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
output = {}
|
13
|
+
forecast.each do |period, blocks|
|
14
|
+
next if %w[settings].include?(period)
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
# @options[:sign_before_symbol] = forecast_data.fetch('settings', {}).fetch('sign_before_symbol', false)
|
19
|
-
@options[:thousands_separator] = forecast_data.fetch('settings', {}).fetch('thousands_separator', true)
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.generate(yaml_file, options = nil)
|
23
|
-
forecast_data = YAML.safe_load(yaml_file)
|
24
|
-
|
25
|
-
set_options(forecast_data)
|
26
|
-
|
27
|
-
output = ""
|
28
|
-
|
29
|
-
# Generate regular transactions
|
30
|
-
forecast_data.each do |period, forecasts|
|
31
|
-
if period == 'custom'
|
32
|
-
output += custom_transaction(forecasts)
|
33
|
-
else
|
34
|
-
frequency = convert_period_to_frequency(period)
|
35
|
-
next unless frequency
|
36
|
-
|
37
|
-
forecasts.each do |forecast|
|
38
|
-
account = forecast['account']
|
39
|
-
from = Date.parse(forecast['from'])
|
40
|
-
to = forecast['to'] ? Date.parse(forecast['to']) : nil
|
41
|
-
transactions = forecast['transactions']
|
42
|
-
|
43
|
-
output += regular_transaction(frequency, from, to, transactions, account)
|
44
|
-
output += ending_transaction(frequency, from, transactions, account)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# Generate tracked transactions
|
50
|
-
if options && !options[:no_track] && !@tracked.empty?
|
51
|
-
if options[:transaction_file]
|
52
|
-
output += output_tracked_transaction(Tracker.track(@tracked,
|
53
|
-
options[:transaction_file]))
|
54
|
-
else
|
55
|
-
puts "\nWarning: ".yellow.bold + "You need to specify a transaction file with the `--t` flag for smart transactions to work\n"
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
output += output_modified_transaction(@modified) unless @modified.empty?
|
60
|
-
|
61
|
-
output
|
62
|
-
end
|
63
|
-
|
64
|
-
def self.regular_transaction(frequency, from, to, transactions, account)
|
65
|
-
transactions = transactions.select { |transaction| transaction['to'].nil? }
|
66
|
-
return "" if transactions.empty?
|
67
|
-
|
68
|
-
output = ""
|
69
|
-
|
70
|
-
transactions.each do |transaction|
|
71
|
-
if track_transaction?(transaction, from)
|
72
|
-
track_transaction(from, to, account, transaction)
|
73
|
-
next
|
74
|
-
end
|
75
|
-
|
76
|
-
modified_transaction(from, to, account, transaction)
|
77
|
-
|
78
|
-
output += output_transaction(transaction['category'], format_amount(transaction['amount']),
|
79
|
-
transaction['description'])
|
80
|
-
end
|
81
|
-
|
82
|
-
return "" unless output != ""
|
83
|
-
|
84
|
-
output = if to
|
85
|
-
"#{frequency} #{from} to #{to} * #{extract_descriptions(transactions,
|
86
|
-
from)}\n" << output
|
87
|
-
else
|
88
|
-
"#{frequency} #{from} * #{extract_descriptions(transactions, from)}\n" << output
|
89
|
-
end
|
90
|
-
|
91
|
-
output += " #{account}\n\n"
|
92
|
-
output
|
93
|
-
end
|
94
|
-
|
95
|
-
def self.ending_transaction(frequency, from, transactions, account)
|
96
|
-
output = ""
|
97
|
-
|
98
|
-
transactions.each do |transaction|
|
99
|
-
to = transaction['to'] ? Date.parse(transaction['to']) : nil
|
100
|
-
next unless to
|
101
|
-
|
102
|
-
if track_transaction?(transaction, from)
|
103
|
-
track_transaction(from, to, account, transaction)
|
104
|
-
next
|
105
|
-
end
|
106
|
-
|
107
|
-
modified_transaction(from, to, account, transaction)
|
108
|
-
|
109
|
-
output += "#{frequency} #{from} to #{to} * #{transaction['description']}\n"
|
110
|
-
output += output_transaction(transaction['category'], format_amount(transaction['amount']),
|
111
|
-
transaction['description'])
|
112
|
-
output += " #{account}\n\n"
|
113
|
-
end
|
114
|
-
|
115
|
-
output
|
116
|
-
end
|
117
|
-
|
118
|
-
def self.custom_transaction(forecasts)
|
119
|
-
output = ""
|
120
|
-
|
121
|
-
forecasts.each do |forecast|
|
122
|
-
account = forecast['account']
|
123
|
-
from = Date.parse(forecast['from'])
|
124
|
-
to = forecast['to'] ? Date.parse(forecast['to']) : nil
|
125
|
-
frequency = forecast['frequency']
|
126
|
-
transactions = forecast['transactions']
|
127
|
-
|
128
|
-
output += "~ #{frequency} from #{from} * #{extract_descriptions(transactions, from)}\n"
|
129
|
-
|
130
|
-
transactions.each do |transaction|
|
131
|
-
to = transaction['to'] ? Date.parse(transaction['to']) : to
|
132
|
-
|
133
|
-
if track_transaction?(transaction, from)
|
134
|
-
track_transaction(from, to, account, transaction)
|
135
|
-
next
|
136
|
-
end
|
137
|
-
|
138
|
-
modified_transaction(from, to, account, transaction)
|
139
|
-
|
140
|
-
output += output_transaction(transaction['category'], format_amount(transaction['amount']),
|
141
|
-
transaction['description'])
|
16
|
+
blocks.each do |block|
|
17
|
+
output[output.length] = process_block(period, block)
|
142
18
|
end
|
143
|
-
|
144
|
-
output += " #{account}\n\n"
|
145
|
-
end
|
146
|
-
|
147
|
-
output
|
148
|
-
end
|
149
|
-
|
150
|
-
def self.output_transaction(category, amount, description)
|
151
|
-
" #{category.ljust(@options[:max_category])} #{amount.ljust(@options[:max_amount])}; #{description}\n"
|
152
|
-
end
|
153
|
-
|
154
|
-
def self.output_modified_transaction(transactions)
|
155
|
-
output = ""
|
156
|
-
|
157
|
-
transactions.each do |_key, transaction|
|
158
|
-
date = "date:#{transaction['from']}"
|
159
|
-
date += "..#{transaction['to']}" if transaction['to']
|
160
|
-
|
161
|
-
output += "= #{transaction['category']} #{date}\n"
|
162
|
-
output += " #{transaction['category'].ljust(@options[:max_category])} *#{transaction['amount'].to_s.ljust(@options[:max_amount] - 1)}; #{transaction['description']}\n"
|
163
|
-
output += " #{transaction['account'].ljust(@options[:max_category])} *#{transaction['amount'] * -1}\n\n"
|
164
19
|
end
|
165
20
|
|
166
|
-
|
21
|
+
Formatter.output_to_ledger(
|
22
|
+
Transactions::Default.generate(output, @settings),
|
23
|
+
Transactions::Trackers.generate(output, @settings),
|
24
|
+
Transactions::Modifiers.generate(output, @settings)
|
25
|
+
)
|
167
26
|
end
|
168
27
|
|
169
|
-
|
170
|
-
output = ""
|
28
|
+
private
|
171
29
|
|
172
|
-
|
173
|
-
|
30
|
+
def process_block(period, block)
|
31
|
+
output = []
|
174
32
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
def self.extract_descriptions(transactions, from)
|
184
|
-
descriptions = []
|
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
|
+
}
|
185
41
|
|
186
|
-
|
187
|
-
next if track_transaction?(transaction, from)
|
42
|
+
output = process_transactions(block, output)
|
188
43
|
|
189
|
-
|
190
|
-
|
44
|
+
output.map do |item|
|
45
|
+
transactions = item[:transactions].group_by { |t| t[:to] }
|
46
|
+
item.merge(transactions: transactions)
|
191
47
|
end
|
192
|
-
|
193
|
-
descriptions.join(', ')
|
194
48
|
end
|
195
49
|
|
196
|
-
def
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
'amount' => modifier['amount'],
|
206
|
-
'category' => transaction['category'],
|
207
|
-
'description' => description,
|
208
|
-
'from' => modifier['from'] ? Date.parse(modifier['from']) : (from || nil),
|
209
|
-
'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
|
210
59
|
}
|
211
60
|
end
|
212
|
-
end
|
213
|
-
|
214
|
-
def self.track_transaction?(transaction, from)
|
215
|
-
transaction['track'] && from <= Date.today
|
216
|
-
end
|
217
|
-
|
218
|
-
def self.track_transaction(from, to, account, transaction)
|
219
|
-
amount = transaction['amount']
|
220
|
-
transaction['amount'] = format_amount(amount)
|
221
|
-
transaction['inverse_amount'] = format_amount(amount * -1)
|
222
61
|
|
223
|
-
|
224
|
-
'account' => account,
|
225
|
-
'from' => from,
|
226
|
-
'to' => to,
|
227
|
-
'transaction' => transaction
|
228
|
-
}
|
229
|
-
end
|
230
|
-
|
231
|
-
def self.convert_period_to_frequency(period)
|
232
|
-
map = {
|
233
|
-
'once' => '~',
|
234
|
-
'monthly' => '~ monthly from',
|
235
|
-
'quarterly' => '~ every 3 months from',
|
236
|
-
'half-yearly' => '~ every 6 months from',
|
237
|
-
'yearly' => '~ yearly from'
|
238
|
-
}
|
239
|
-
|
240
|
-
map[period]
|
241
|
-
end
|
242
|
-
|
243
|
-
def self.format_amount(amount)
|
244
|
-
Money.from_cents(amount.to_f * 100, @options[:currency]).format(
|
245
|
-
symbol: @options[:show_symbol],
|
246
|
-
sign_before_symbol: @options[:sign_before_symbol],
|
247
|
-
thousands_separator: @options[:thousands_separator] ? ',' : nil
|
248
|
-
)
|
249
|
-
end
|
250
|
-
|
251
|
-
def self.get_max_field_size(forecast_data, field)
|
252
|
-
max_size = 0
|
253
|
-
|
254
|
-
forecast_data.each do |period, forecasts|
|
255
|
-
next if period == 'settings'
|
256
|
-
|
257
|
-
forecasts.each do |forecast|
|
258
|
-
transactions = forecast['transactions']
|
259
|
-
transactions.each do |transaction|
|
260
|
-
field_value = if transaction[field].is_a?(Integer) || transaction[field].is_a?(Float)
|
261
|
-
((transaction[field] + 3) * 100).to_s
|
262
|
-
else
|
263
|
-
transaction[field].to_s
|
264
|
-
end
|
265
|
-
max_size = [max_size, field_value.length].max
|
266
|
-
end
|
267
|
-
end
|
268
|
-
end
|
269
|
-
|
270
|
-
max_size
|
62
|
+
output
|
271
63
|
end
|
272
64
|
end
|
273
65
|
end
|