hledger-forecast 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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]
|