hledger-forecast 0.2.1 → 0.3.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/.gitignore +0 -2
- data/README.md +150 -71
- data/example.journal +35 -0
- data/example.yml +35 -36
- data/hledger-forecast.gemspec +5 -4
- data/lib/hledger_forecast/cli.rb +35 -93
- data/lib/hledger_forecast/generator.rb +233 -90
- data/lib/hledger_forecast/summarize.rb +4 -5
- data/lib/hledger_forecast/tracker.rb +37 -0
- data/lib/hledger_forecast/version.rb +1 -1
- data/lib/hledger_forecast.rb +1 -1
- data/spec/command_spec.rb +14 -4
- data/spec/custom_spec.rb +35 -42
- data/spec/half-yearly_spec.rb +22 -6
- data/spec/modifier_spec.rb +87 -0
- data/spec/monthly_end_date_spec.rb +47 -0
- data/spec/monthly_end_date_transaction_spec.rb +32 -0
- data/spec/monthly_spec.rb +35 -27
- data/spec/once_spec.rb +22 -7
- data/spec/quarterly_spec.rb +21 -7
- data/spec/stubs/{monthly/forecast_monthly.yml → forecast.yml} +7 -7
- data/spec/stubs/transactions_found.journal +24 -0
- data/spec/stubs/transactions_found_inverse.journal +24 -0
- data/spec/stubs/transactions_not_found.journal +16 -0
- data/spec/track_spec.rb +197 -0
- data/spec/yearly_spec.rb +21 -7
- metadata +32 -74
- data/spec/start_date_spec.rb +0 -12
- data/spec/stubs/custom/forecast_custom_days.yml +0 -14
- data/spec/stubs/custom/forecast_custom_months.yml +0 -14
- data/spec/stubs/custom/forecast_custom_weeks.yml +0 -14
- data/spec/stubs/custom/forecast_custom_weeks_twice.yml +0 -24
- data/spec/stubs/custom/output_custom_days.journal +0 -24
- data/spec/stubs/custom/output_custom_months.journal +0 -20
- data/spec/stubs/custom/output_custom_weeks.journal +0 -28
- data/spec/stubs/custom/output_custom_weeks_twice.journal +0 -44
- data/spec/stubs/half-yearly/forecast_half-yearly.yml +0 -10
- data/spec/stubs/half-yearly/output_half-yearly.journal +0 -20
- data/spec/stubs/modifiers/forecast_modifiers.yml +0 -13
- data/spec/stubs/modifiers/output_modifiers.journal +0 -44
- data/spec/stubs/monthly/forecast_monthly_enddate.yml +0 -14
- data/spec/stubs/monthly/forecast_monthly_enddate_top.yml +0 -14
- data/spec/stubs/monthly/forecast_monthly_modifier.yml +0 -11
- data/spec/stubs/monthly/output_monthly.journal +0 -44
- data/spec/stubs/monthly/output_monthly_enddate.journal +0 -48
- data/spec/stubs/monthly/output_monthly_enddate_top.journal +0 -40
- data/spec/stubs/monthly/output_monthly_modifier.journal +0 -20
- data/spec/stubs/once/forecast_once.yml +0 -10
- data/spec/stubs/once/output_once.journal +0 -12
- data/spec/stubs/quarterly/forecast_quarterly.yml +0 -10
- data/spec/stubs/quarterly/output_quarterly.journal +0 -20
- data/spec/stubs/start_date/forecast_startdate.yml +0 -26
- data/spec/stubs/start_date/output_startdate.journal +0 -56
- data/spec/stubs/transactions.journal +0 -8
- data/spec/stubs/yearly/forecast_yearly.yml +0 -10
- data/spec/stubs/yearly/output_yearly.journal +0 -16
data/lib/hledger_forecast/cli.rb
CHANGED
@@ -2,27 +2,26 @@ module HledgerForecast
|
|
2
2
|
# The Command Line Interface for the application
|
3
3
|
# Takes user arguments and translates them into actions
|
4
4
|
class Cli
|
5
|
-
|
6
|
-
|
7
|
-
case subcommand
|
5
|
+
def self.run(command, options)
|
6
|
+
case command
|
8
7
|
when 'generate'
|
9
8
|
generate(options)
|
10
9
|
when 'summarize'
|
11
10
|
summarize(options)
|
12
11
|
else
|
13
|
-
puts "Unknown command: #{
|
12
|
+
puts "Unknown command: #{command}"
|
14
13
|
exit(1)
|
15
14
|
end
|
16
15
|
end
|
17
16
|
|
18
17
|
def self.parse_commands(args = ARGV, _stdin = $stdin)
|
19
|
-
|
18
|
+
command = nil
|
20
19
|
options = {}
|
21
20
|
|
22
21
|
global = OptionParser.new do |opts|
|
23
|
-
opts.banner = "Usage: hledger-forecast [
|
22
|
+
opts.banner = "Usage: hledger-forecast [command] [options]"
|
24
23
|
opts.separator ""
|
25
|
-
opts.separator "
|
24
|
+
opts.separator "Commands:"
|
26
25
|
opts.separator " generate Generate the forecast file"
|
27
26
|
opts.separator " summarize Summarize the forecast file and output to the terminal"
|
28
27
|
opts.separator ""
|
@@ -33,7 +32,7 @@ module HledgerForecast
|
|
33
32
|
exit
|
34
33
|
end
|
35
34
|
|
36
|
-
opts.on_tail("-v", "--version", "Show version") do
|
35
|
+
opts.on_tail("-v", "--version", "Show the installed version") do
|
37
36
|
puts VERSION
|
38
37
|
exit
|
39
38
|
end
|
@@ -41,63 +40,58 @@ module HledgerForecast
|
|
41
40
|
|
42
41
|
begin
|
43
42
|
global.order!(args)
|
44
|
-
|
43
|
+
command = args.shift || 'generate'
|
45
44
|
rescue OptionParser::InvalidOption => e
|
46
45
|
puts e
|
47
46
|
puts global
|
48
47
|
exit(1)
|
49
48
|
end
|
50
49
|
|
51
|
-
case
|
50
|
+
case command
|
52
51
|
when 'generate'
|
53
52
|
options = parse_generate_options(args)
|
54
53
|
when 'summarize'
|
55
54
|
options = parse_summarize_options(args)
|
56
55
|
else
|
57
|
-
puts "Unknown
|
56
|
+
puts "Unknown command: #{command}"
|
58
57
|
puts global
|
59
58
|
exit(1)
|
60
59
|
end
|
61
60
|
|
62
|
-
return
|
61
|
+
return command, options
|
63
62
|
end
|
64
63
|
|
65
64
|
def self.parse_generate_options(args)
|
66
65
|
options = {}
|
67
66
|
|
68
67
|
OptionParser.new do |opts|
|
69
|
-
opts.banner = "Usage:
|
68
|
+
opts.banner = "Usage: hledger-forecast generate [options]"
|
70
69
|
opts.separator ""
|
71
70
|
|
72
|
-
opts.on("-t", "--transaction FILE",
|
73
|
-
"The base TRANSACTIONS file to extend from") do |file|
|
74
|
-
options[:transactions_file] = file if file && !file.empty?
|
75
|
-
end
|
76
|
-
|
77
71
|
opts.on("-f", "--forecast FILE",
|
78
|
-
"The FORECAST yaml file to generate from") do |file|
|
72
|
+
"The path to the FORECAST yaml file to generate from") do |file|
|
79
73
|
options[:forecast_file] = file
|
80
74
|
options[:output_file] ||= file.sub(/\.yml$/, '.journal')
|
81
75
|
end
|
82
76
|
|
83
77
|
opts.on("-o", "--output-file FILE",
|
84
|
-
"The OUTPUT file to create") do |file|
|
78
|
+
"The path to the OUTPUT file to create") do |file|
|
85
79
|
options[:output_file] = file
|
86
80
|
end
|
87
81
|
|
88
|
-
opts.on("-
|
89
|
-
"The
|
90
|
-
options[:
|
82
|
+
opts.on("-t", "--transaction FILE",
|
83
|
+
"The path to the TRANSACTION journal file") do |file|
|
84
|
+
options[:transaction_file] = file
|
91
85
|
end
|
92
86
|
|
93
|
-
opts.on("
|
94
|
-
"
|
95
|
-
options[:
|
87
|
+
opts.on("--force",
|
88
|
+
"Force an overwrite of the output file") do
|
89
|
+
options[:force] = true
|
96
90
|
end
|
97
91
|
|
98
|
-
opts.on("--
|
99
|
-
"
|
100
|
-
options[:
|
92
|
+
opts.on("--no-track",
|
93
|
+
"Don't track any transactions") do
|
94
|
+
options[:no_track] = true
|
101
95
|
end
|
102
96
|
|
103
97
|
opts.on_tail("-h", "--help", "Show this help message") do
|
@@ -109,18 +103,6 @@ module HledgerForecast
|
|
109
103
|
options[:forecast_file] = "forecast.yml" unless options[:forecast_file]
|
110
104
|
options[:output_file] = "forecast.journal" unless options[:output_file]
|
111
105
|
|
112
|
-
today = Date.today
|
113
|
-
|
114
|
-
unless options[:start_date]
|
115
|
-
options[:default_dates] = true
|
116
|
-
options[:start_date] =
|
117
|
-
Date.new(today.year, today.month, 1).next_month.to_s
|
118
|
-
end
|
119
|
-
unless options[:end_date]
|
120
|
-
options[:default_dates] = true
|
121
|
-
options[:end_date] = Date.new(today.year + 3, 12, 31).to_s
|
122
|
-
end
|
123
|
-
|
124
106
|
options
|
125
107
|
end
|
126
108
|
|
@@ -128,11 +110,11 @@ module HledgerForecast
|
|
128
110
|
options = {}
|
129
111
|
|
130
112
|
OptionParser.new do |opts|
|
131
|
-
opts.banner = "Usage:
|
113
|
+
opts.banner = "Usage: hledger-forecast summarize [options]"
|
132
114
|
opts.separator ""
|
133
115
|
|
134
116
|
opts.on("-f", "--forecast FILE",
|
135
|
-
"The FORECAST yaml file to summarize") do |file|
|
117
|
+
"The path to the FORECAST yaml file to summarize") do |file|
|
136
118
|
options[:forecast_file] = file
|
137
119
|
end
|
138
120
|
|
@@ -146,30 +128,29 @@ module HledgerForecast
|
|
146
128
|
end
|
147
129
|
|
148
130
|
def self.generate(options)
|
149
|
-
end_date = options[:end_date]
|
150
|
-
start_date = options[:start_date]
|
151
131
|
forecast = File.read(options[:forecast_file])
|
152
|
-
transactions = options[:transactions_file] ? File.read(options[:transactions_file]) : nil
|
153
|
-
|
154
|
-
# Generate the forecast
|
155
|
-
puts "[Using default dates: #{start_date} to #{end_date}]" if options[:default_dates]
|
156
132
|
|
157
|
-
|
133
|
+
begin
|
134
|
+
transactions = Generator.generate(forecast, options)
|
135
|
+
rescue StandardError => e
|
136
|
+
puts "An error occurred while generating transactions: #{e.message}"
|
137
|
+
exit(1)
|
138
|
+
end
|
158
139
|
|
159
140
|
output_file = options[:output_file]
|
160
141
|
if File.exist?(output_file) && !options[:force]
|
161
|
-
print "
|
142
|
+
print "\nFile '#{output_file}' already exists. Overwrite? (y/n): "
|
162
143
|
overwrite = gets.chomp.downcase
|
163
144
|
|
164
145
|
if overwrite == 'y'
|
165
146
|
File.write(output_file, transactions)
|
166
|
-
puts "File '#{output_file}' has been overwritten."
|
147
|
+
puts "\nSuccess: ".bold.green + "File '#{output_file}' has been overwritten."
|
167
148
|
else
|
168
|
-
puts "Operation aborted. File '#{output_file}' was not overwritten."
|
149
|
+
puts "\nInfo: ".bold.blue + "Operation aborted. File '#{output_file}' was not overwritten."
|
169
150
|
end
|
170
151
|
else
|
171
152
|
File.write(output_file, transactions)
|
172
|
-
puts "File '#{output_file}' has been created
|
153
|
+
puts "\nSuccess: ".bold.green + "File '#{output_file}' has been created"
|
173
154
|
end
|
174
155
|
end
|
175
156
|
|
@@ -177,44 +158,5 @@ module HledgerForecast
|
|
177
158
|
forecast = File.read(options[:forecast_file])
|
178
159
|
puts Summarize.generate(forecast)
|
179
160
|
end
|
180
|
-
|
181
|
-
# def self.run(args)
|
182
|
-
# end_date = args[:end_date]
|
183
|
-
# start_date = args[:start_date]
|
184
|
-
# forecast = File.read(args[:forecast_file])
|
185
|
-
# transactions = args[:transactions_file] ? File.read(args[:transactions_file]) : nil
|
186
|
-
#
|
187
|
-
# # Output the summary
|
188
|
-
# return HledgerForecast::Summarize.generate(forecast) if args[:summarize]
|
189
|
-
#
|
190
|
-
# # Generate the forecast
|
191
|
-
# unless args[:skip]
|
192
|
-
#
|
193
|
-
# puts "[Using default dates: #{start_date} to #{end_date}]" if args[:default_dates]
|
194
|
-
#
|
195
|
-
# transactions = Generator.generate(transactions, forecast, start_date, end_date)
|
196
|
-
#
|
197
|
-
# output_file = args[:output_file]
|
198
|
-
# if File.exist?(output_file) && !args[:force]
|
199
|
-
# print "File '#{output_file}' already exists. Overwrite? (y/n): "
|
200
|
-
# overwrite = gets.chomp.downcase
|
201
|
-
#
|
202
|
-
# if overwrite == 'y'
|
203
|
-
# File.write(output_file, transactions)
|
204
|
-
# puts "File '#{output_file}' has been overwritten."
|
205
|
-
# else
|
206
|
-
# puts "Operation aborted. File '#{output_file}' was not overwritten."
|
207
|
-
# end
|
208
|
-
# else
|
209
|
-
# File.write(output_file, transactions)
|
210
|
-
# puts "File '#{output_file}' has been created."
|
211
|
-
# end
|
212
|
-
# end
|
213
|
-
#
|
214
|
-
# # Check for missing transactions
|
215
|
-
# return unless args[:check] && args[:transactions_file] && args[:forecast_file]
|
216
|
-
#
|
217
|
-
# HledgerForecast::Checker.check(args)
|
218
|
-
# end
|
219
161
|
end
|
220
162
|
end
|
@@ -1,130 +1,273 @@
|
|
1
1
|
module HledgerForecast
|
2
|
-
# Generates
|
3
|
-
# on forecast data and optional existing transactions.
|
2
|
+
# Generates periodic transactions from a YAML file
|
4
3
|
class Generator
|
5
4
|
class << self
|
6
|
-
attr_accessor :
|
5
|
+
attr_accessor :options, :modified, :tracked
|
7
6
|
end
|
8
7
|
|
9
|
-
self.
|
8
|
+
self.options = {}
|
9
|
+
self.modified = {}
|
10
|
+
self.tracked = {}
|
10
11
|
|
11
|
-
def self.
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@settings[:sign_before_symbol] = forecast_data.fetch('settings', {}).fetch('sign_before_symbol', true)
|
15
|
-
@settings[:thousands_separator] = forecast_data.fetch('settings', {}).fetch('thousands_separator', true)
|
16
|
-
end
|
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')
|
17
15
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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)
|
22
20
|
end
|
23
21
|
|
24
|
-
def self.
|
25
|
-
|
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
|
26
58
|
|
27
|
-
|
28
|
-
Money.from_cents(formatted_transaction['amount'].to_f * 100, @settings[:currency]).format(
|
29
|
-
symbol: @settings[:show_symbol],
|
30
|
-
sign_before_symbol: @settings[:sign_before_symbol],
|
31
|
-
thousands_separator: @settings[:thousands_separator] ? ',' : nil
|
32
|
-
)
|
59
|
+
output += output_modified_transaction(@modified) unless @modified.empty?
|
33
60
|
|
34
|
-
|
61
|
+
output
|
35
62
|
end
|
36
63
|
|
37
|
-
def self.
|
38
|
-
|
39
|
-
|
40
|
-
end_date = forecast['end'] ? Date.parse(forecast['end']) : nil
|
41
|
-
account = forecast['account']
|
42
|
-
period = forecast['recurrence']['period']
|
43
|
-
quantity = forecast['recurrence']['quantity']
|
64
|
+
def self.regular_transaction(frequency, from, to, transactions, account)
|
65
|
+
transactions = transactions.select { |transaction| transaction['to'].nil? }
|
66
|
+
return "" if transactions.empty?
|
44
67
|
|
45
|
-
|
68
|
+
output = ""
|
46
69
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
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)
|
55
77
|
|
56
|
-
|
57
|
-
|
58
|
-
|
78
|
+
output += output_transaction(transaction['category'], format_amount(transaction['amount']),
|
79
|
+
transaction['description'])
|
80
|
+
end
|
59
81
|
|
60
|
-
|
82
|
+
return "" unless output != ""
|
61
83
|
|
62
|
-
|
63
|
-
|
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
|
64
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"
|
65
113
|
end
|
114
|
+
|
115
|
+
output
|
66
116
|
end
|
67
117
|
|
68
|
-
def self.
|
69
|
-
|
70
|
-
|
71
|
-
|
118
|
+
def self.custom_transaction(forecasts)
|
119
|
+
output = ""
|
120
|
+
|
121
|
+
forecasts.each do |forecast|
|
72
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"
|
73
129
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
date.day == start_date.day && date.month % 3 == start_date.month % 3
|
81
|
-
when 'half-yearly'
|
82
|
-
date.day == start_date.day && (date.month - start_date.month) % 6 == 0
|
83
|
-
when 'yearly'
|
84
|
-
date.day == start_date.day && date.month == start_date.month
|
85
|
-
when 'once'
|
86
|
-
date == start_date
|
87
|
-
end
|
88
|
-
|
89
|
-
if date_matches
|
90
|
-
forecast['transactions'].each do |transaction|
|
91
|
-
transaction_start_date = transaction['start'] ? Date.parse(transaction['start']) : nil
|
92
|
-
transaction_end_date = transaction['end'] ? Date.parse(transaction['end']) : nil
|
93
|
-
|
94
|
-
if (transaction_start_date && date < transaction_start_date) || (transaction_end_date && date > transaction_end_date)
|
95
|
-
next
|
96
|
-
end
|
97
|
-
|
98
|
-
write_transactions(output_file, date, account, format_transaction(transaction))
|
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
|
99
136
|
end
|
137
|
+
|
138
|
+
modified_transaction(from, to, account, transaction)
|
139
|
+
|
140
|
+
output += output_transaction(transaction['category'], format_amount(transaction['amount']),
|
141
|
+
transaction['description'])
|
100
142
|
end
|
143
|
+
|
144
|
+
output += " #{account}\n\n"
|
101
145
|
end
|
146
|
+
|
147
|
+
output
|
102
148
|
end
|
103
149
|
|
104
|
-
def self.
|
105
|
-
|
106
|
-
|
107
|
-
|
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 = ""
|
108
156
|
|
109
|
-
|
157
|
+
transactions.each do |_key, transaction|
|
158
|
+
date = "date:#{transaction['from']}"
|
159
|
+
date += "..#{transaction['to']}" if transaction['to']
|
110
160
|
|
111
|
-
|
112
|
-
|
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
|
+
end
|
113
165
|
|
114
|
-
|
166
|
+
output
|
167
|
+
end
|
115
168
|
|
116
|
-
|
117
|
-
|
118
|
-
process_forecast(output, forecast_data, 'quarterly', date)
|
119
|
-
process_forecast(output, forecast_data, 'half-yearly', date)
|
120
|
-
process_forecast(output, forecast_data, 'yearly', date)
|
121
|
-
process_forecast(output, forecast_data, 'once', date)
|
122
|
-
process_custom(output, forecast_data, date)
|
169
|
+
def self.output_tracked_transaction(transactions)
|
170
|
+
output = ""
|
123
171
|
|
124
|
-
|
172
|
+
transactions.each do |_key, transaction|
|
173
|
+
next if transaction['found']
|
174
|
+
|
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"
|
125
178
|
end
|
126
179
|
|
127
180
|
output
|
128
181
|
end
|
182
|
+
|
183
|
+
def self.extract_descriptions(transactions, from)
|
184
|
+
descriptions = []
|
185
|
+
|
186
|
+
transactions.each do |transaction|
|
187
|
+
next if track_transaction?(transaction, from)
|
188
|
+
|
189
|
+
description = transaction['description']
|
190
|
+
descriptions << description
|
191
|
+
end
|
192
|
+
|
193
|
+
descriptions.join(', ')
|
194
|
+
end
|
195
|
+
|
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)
|
210
|
+
}
|
211
|
+
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
|
+
|
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
|
271
|
+
end
|
129
272
|
end
|
130
273
|
end
|
@@ -15,7 +15,7 @@ module HledgerForecast
|
|
15
15
|
|
16
16
|
def self.init_generator(forecast_data)
|
17
17
|
generator = HledgerForecast::Generator
|
18
|
-
generator.
|
18
|
+
generator.set_options(forecast_data)
|
19
19
|
|
20
20
|
@generator = generator
|
21
21
|
end
|
@@ -37,8 +37,7 @@ module HledgerForecast
|
|
37
37
|
|
38
38
|
forecast_data['custom']&.each do |entry|
|
39
39
|
period_data = {}
|
40
|
-
period_data[:
|
41
|
-
period_data[:period] = entry['recurrence']['period']
|
40
|
+
period_data[:frequency] = entry['frequency']
|
42
41
|
period_data[:category] = entry['transactions'].first['category']
|
43
42
|
period_data[:amount] = entry['transactions'].first['amount']
|
44
43
|
|
@@ -53,7 +52,7 @@ module HledgerForecast
|
|
53
52
|
end
|
54
53
|
|
55
54
|
def self.format_amount(amount)
|
56
|
-
formatted_amount = @generator.
|
55
|
+
formatted_amount = @generator.format_amount(amount)
|
57
56
|
amount.to_f < 0 ? formatted_amount.green : formatted_amount.red
|
58
57
|
end
|
59
58
|
|
@@ -61,7 +60,7 @@ module HledgerForecast
|
|
61
60
|
if custom
|
62
61
|
row_data[:periods].each do |period|
|
63
62
|
@table.add_row [{ value: period[:category], alignment: :left },
|
64
|
-
{ value:
|
63
|
+
{ value: period[:frequency], alignment: :right },
|
65
64
|
{ value: format_amount(period[:amount]), alignment: :right }]
|
66
65
|
|
67
66
|
period_total += period[:amount]
|