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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb90937d10de71d495146036711f7028d12de7f5b7c814ccf0fb62dc7ffc5e7f
4
- data.tar.gz: 05143fad945647f27f30e49626974ec62b66074863a5f11e86898a824b182c85
3
+ metadata.gz: 01faf9619f0b906b089956c1b81c16766830be412a678336848a7bbe23f78cb7
4
+ data.tar.gz: c93786627631d0f589250a8c721d8360a9feddf6f9b21954cc49b0b6b206ff29
5
5
  SHA512:
6
- metadata.gz: 00e039f29f6f50237d4bca32d8f5e2f27173ac880661bb1445558a98380816de0718e3e748b639e7018d79b49ce019f6dac7b145b98104a687d04bbf36c8db57
7
- data.tar.gz: 233262e4e1a0612c821334f97599a0d2b00962968a18f591eb5cd96f4efdd5116c037e5b5570f2b6c3e74b1955dec140ffd774a7c04feacdb6d628303d71cb87
6
+ metadata.gz: 218ecb7ea26f4daee36981d345cf4a67052b6c3876f4f2090d608a86a10ba7e07889010fa91ab3618da60bfac98caf2c48bbdda78ab68f0290e744a580f75076
7
+ data.tar.gz: c3940bb307bbba32d374f0161ecaf5ca8bcdba2a1a0cafd0d1e4ea7f13ceea20a781dd0367eb02cd1c01fe73475f23fdcf1e520d3b76a9b7d875729971f221b7
@@ -12,7 +12,7 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  strategy:
14
14
  matrix:
15
- ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2']
15
+ ruby-version: ['3.0', '3.1', '3.2']
16
16
 
17
17
  steps:
18
18
  - uses: actions/checkout@v3
data/.gitignore CHANGED
@@ -10,6 +10,7 @@ pkg/*
10
10
  .rvmrc
11
11
  .ruby-version
12
12
  .ruby-gemset
13
+ .tool-versions
13
14
 
14
15
  # Spec artifacts
15
16
  /coverage
data/.rubocop.yml CHANGED
@@ -1,3 +1,6 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
1
4
  Layout/LineLength:
2
5
  Max: 120
3
6
 
data/README.md CHANGED
@@ -1,16 +1,22 @@
1
- # Hledger-Forecast
1
+ <h1 align="center">Hledger-Forecast</h1>
2
2
 
3
- [![Tests](https://github.com/olimorris/hledger-forecast/actions/workflows/ci.yml/badge.svg)](https://github.com/olimorris/hledger-forecast/actions/workflows/ci.yml)
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 `YAML` config file to generate forecasts whilst adding functionality for future cost rises (e.g. inflation) and the automatic tracking of planned transactions.
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 YAML files to generate forecasts which can be used with hledger
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 begin the generation of your forecast _from_ a `yaml` file _to_ a journal file.
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 YAML file
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 of a complex config file and its [output](https://github.com/olimorris/hledger-forecast/blob/main/example.journal)
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
- account: "Assets:Bank"
188
- from: "2023-03-05"
189
- transactions:
190
- - amount: 3000
191
- category: "Expenses:Shopping"
192
- description: Refund for that damn laptop
193
- track: true
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
- ### Applying modifiers
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
- account: "Assets:Bank"
213
- from: "2023-03-05"
214
- transactions:
215
- - amount: 450
216
- category: "Expenses:Groceries"
217
- description: Food shopping
218
- modifiers:
219
- - amount: 0.02
220
- description: "Inflation"
221
- from: "2024-01-01"
222
- to: "2024-12-31"
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 # Specify the currency to use
250
- show_symbol: true # Show the currency symbol?
251
- thousands_separator: true # Separate thousands with a comma?
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 __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.
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 likely be for the period of 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 is a nice time saver.
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
@@ -19,6 +19,9 @@ monthly:
19
19
  - amount: 75
20
20
  category: "Expenses:Phone"
21
21
  description: New cell phone
22
+ - amount: "=2500/12"
23
+ category: "Expenses:Holiday"
24
+ description: Holiday savings
22
25
  - amount: 1000
23
26
  category: "Expenses:Mortgage"
24
27
  description: Mortgage
@@ -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
@@ -155,8 +155,8 @@ module HledgerForecast
155
155
  end
156
156
 
157
157
  def self.summarize(options)
158
- forecast = File.read(options[:forecast_file])
159
- puts Summarize.generate(forecast)
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
- # Generates periodic transactions from a YAML file
2
+ # Generate forecasts for hledger from a yaml config file
3
3
  class Generator
4
- class << self
5
- attr_accessor :options, :modified, :tracked
4
+ def self.generate(config, cli_options = nil)
5
+ new.generate(config, cli_options)
6
6
  end
7
7
 
8
- self.options = {}
9
- self.modified = {}
10
- self.tracked = {}
8
+ def generate(config, cli_options = nil)
9
+ forecast = YAML.safe_load(config)
10
+ @settings = Settings.config(forecast, cli_options)
11
11
 
12
- def self.set_options(forecast_data)
13
- @options[:max_amount] = get_max_field_size(forecast_data, 'amount') + 1 # +1 for the negatives
14
- @options[:max_category] = get_max_field_size(forecast_data, 'category')
12
+ output = {}
13
+ forecast.each do |period, blocks|
14
+ next if %w[settings].include?(period)
15
15
 
16
- @options[:currency] = Money::Currency.new(forecast_data.fetch('settings', {}).fetch('currency', 'USD'))
17
- @options[:show_symbol] = forecast_data.fetch('settings', {}).fetch('show_symbol', true)
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
- output
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
- def self.output_tracked_transaction(transactions)
170
- output = ""
28
+ private
171
29
 
172
- transactions.each do |_key, transaction|
173
- next if transaction['found']
30
+ def process_block(period, block)
31
+ output = []
174
32
 
175
- output += "~ #{transaction['from']} * [TRACKED] #{transaction['transaction']['description']}\n"
176
- output += " #{transaction['transaction']['category'].ljust(@options[:max_category])} #{transaction['transaction']['amount'].ljust(@options[:max_amount])}; #{transaction['transaction']['description']}\n"
177
- output += " #{transaction['account']}\n\n"
178
- end
179
-
180
- output
181
- end
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
- transactions.each do |transaction|
187
- next if track_transaction?(transaction, from)
42
+ output = process_transactions(block, output)
188
43
 
189
- description = transaction['description']
190
- descriptions << description
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 self.modified_transaction(from, to, account, transaction)
197
- return unless transaction['modifiers']
198
-
199
- transaction['modifiers'].each do |modifier|
200
- description = transaction['description']
201
- description += ' - ' + modifier['description'] unless modifier['description'].empty?
202
-
203
- @modified[@modified.length] = {
204
- 'account' => account,
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
- @tracked[@tracked.length] = {
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