treasurer 0.5.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/treasurer/accounts.rb +64 -28
- data/lib/treasurer/analysis.rb +58 -33
- data/lib/treasurer/commands.rb +1 -1
- data/lib/treasurer/report.rb +122 -41
- data/lib/treasurer.rb +3 -2
- data/treasurer.gemspec +3 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b5eb9890a54a614720f287ec3cd65d77478b8225
|
4
|
+
data.tar.gz: 7341d4cb3c37507bdacf15b7d49b53d83de79749
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3e86953149b37629f61297e90beeb3ebc7e41b70816f38c45f332a3f333fd40b5bd3b40a696c69b6dc33d2a103e590653ac13e9926280c5a95e3bdc6d9d0b252
|
7
|
+
data.tar.gz: c58e49206abb4f80e48a98af57659374aa14d38841c110ac1bc73bb1b6825b2961e82f978addf28273683e43310e4756e65c451d88d0554442f2784d8d47fa65
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.5.
|
1
|
+
0.5.1
|
data/lib/treasurer/accounts.rb
CHANGED
@@ -15,7 +15,7 @@ class Treasurer::Reporter
|
|
15
15
|
end
|
16
16
|
class Account
|
17
17
|
attr_reader :name, :external, :runs, :currency
|
18
|
-
attr_accessor :projection, :average
|
18
|
+
attr_accessor :projection, :average, :original_currency
|
19
19
|
def initialize(name, reporter, runner, runs, external, options={})
|
20
20
|
@name = name
|
21
21
|
@reporter = reporter
|
@@ -28,7 +28,7 @@ class Treasurer::Reporter
|
|
28
28
|
#@external ? r.external_account : r.account) == name}
|
29
29
|
if not @external
|
30
30
|
r.account == name
|
31
|
-
elsif info and cur = info[:currencies] and cur.size > 1
|
31
|
+
elsif @currency and info and cur = info[:currencies] and cur.size > 1
|
32
32
|
#p ['checking11', name, @currency, ACCOUNT_INFO[r.account]] if name == r.external_account and @currency
|
33
33
|
r.external_account == name and acinfo = ACCOUNT_INFO[r.account] and acinfo[:currencies] == [@currency]
|
34
34
|
else
|
@@ -51,7 +51,12 @@ class Treasurer::Reporter
|
|
51
51
|
end
|
52
52
|
def red_line(date)
|
53
53
|
if Treasurer::LocalCustomisations.instance_methods.include? :red_line
|
54
|
-
super(name, date)
|
54
|
+
val = super(name, date)
|
55
|
+
if rc = @report_currency
|
56
|
+
er = EXCHANGE_RATES[[@original_currency,rc]]
|
57
|
+
val *= er
|
58
|
+
end
|
59
|
+
val
|
55
60
|
else
|
56
61
|
0.0
|
57
62
|
end
|
@@ -75,9 +80,14 @@ class Treasurer::Reporter
|
|
75
80
|
if @external or not has_balance?
|
76
81
|
#p ['name is ', name, type]
|
77
82
|
#
|
78
|
-
|
79
|
-
|
80
|
-
|
83
|
+
if type == :Expense
|
84
|
+
balance = (@runs.find_all{|r| r.date <= date and r.date >= @reporter.start_date }.map{|r| money_in_sign * (r.deposit - r.withdrawal) * (@external ? -1 : 1)}.sum || 0.0)
|
85
|
+
|
86
|
+
else
|
87
|
+
balance = (@runs.find_all{|r| r.date <= date and r.date >= opening_date }.map{|r| money_in_sign * (r.deposit - r.withdrawal) * (@external ? -1 : 1)}.sum || 0.0)
|
88
|
+
balance += info[:opening_balance] if info[:opening_balance]
|
89
|
+
balance
|
90
|
+
end
|
81
91
|
#Temporary....
|
82
92
|
#0.0
|
83
93
|
else
|
@@ -87,20 +97,20 @@ class Treasurer::Reporter
|
|
87
97
|
end
|
88
98
|
end
|
89
99
|
def deposited(today, days_before, &block)
|
90
|
-
|
100
|
+
p ['name223344 is ', name_c, today, days_before]
|
91
101
|
#@runs.find_all{|r| r.days_ago(today) < days_before and (!block or yield(r)) }.map{|r| (@external and not ([:Liability, :Income].include?(type))) ? r.withdrawal : r.deposit }.sum || 0
|
92
|
-
@runs.find_all{|r| r.days_ago(today) < days_before and (!block or yield(r)) }.map{|r| (@external) ? r.withdrawal : r.deposit }.sum || 0
|
102
|
+
@runs.find_all{|r| r.days_ago(today) < days_before and r.date <= today and (!block or yield(r)) }.map{|r| (@external) ? r.withdrawal : r.deposit }.sum || 0
|
93
103
|
end
|
94
104
|
def withdrawn(today, days_before)
|
95
105
|
#@runs.find_all{|r| r.days_ago(today) < days_before }.map{|r| (@external and not ([:Liability, :Income].include?(type))) ? r.deposit : r.withdrawal }.sum || 0
|
96
|
-
@runs.find_all{|r| r.days_ago(today) < days_before }.map{|r| (@external) ? r.deposit : r.withdrawal }.sum || 0
|
106
|
+
@runs.find_all{|r| r.days_ago(today) < days_before and r.date <= today }.map{|r| (@external) ? r.deposit : r.withdrawal }.sum || 0
|
97
107
|
end
|
98
108
|
def currency
|
99
109
|
@currency || (info[:currencies] && info[:currencies][0])
|
100
110
|
end
|
101
111
|
def currency_label
|
102
|
-
if
|
103
|
-
" (
|
112
|
+
if currency
|
113
|
+
" (#{currency}#{@original_currency ? "<-#@original_currency" : ""})"
|
104
114
|
else
|
105
115
|
''
|
106
116
|
end
|
@@ -110,7 +120,7 @@ class Treasurer::Reporter
|
|
110
120
|
name + currency_label
|
111
121
|
end
|
112
122
|
def name_c_file
|
113
|
-
name_c.to_s.gsub(/[: ()]/, '_')
|
123
|
+
name_c.to_s.gsub(/[: ()<-]/, '_')
|
114
124
|
end
|
115
125
|
|
116
126
|
def summary_table(today, days_before)
|
@@ -154,30 +164,49 @@ EOF
|
|
154
164
|
#(discretionary ? @reporter.sum_regular({name => info}, date) : 0.0)
|
155
165
|
end
|
156
166
|
def linked_projected_account_info
|
157
|
-
Hash[@reporter.projected_accounts_info.find_all{|ext_ac,inf| inf[:
|
167
|
+
#Hash[@reporter.projected_accounts_info.find_all{|ext_ac,inf| inf[:linked_accounts] == name and ext_ac.currency == currency}]
|
168
|
+
Hash[@reporter.projected_accounts_info.find_all{|ext_ac,inf| inf[:linked_accounts][original_currency] and inf[:linked_accounts][original_currency] == name and ext_ac.original_currency == original_currency}]
|
158
169
|
end
|
159
170
|
def cache
|
160
171
|
@cache ||={}
|
161
172
|
end
|
162
173
|
def non_discretionary_projected_balance(date)
|
163
174
|
#ep ['FUTURE_INCOME', FUTURE_INCOME, name] if FUTURE_INCOME.size > 0
|
175
|
+
if not (@futures and @regulars)
|
176
|
+
@futures = Marshal.load(Marshal.dump(FUTURE_TRANSFERS))
|
177
|
+
@regulars = Marshal.load(Marshal.dump(REGULAR_TRANSFERS))
|
178
|
+
[@regulars, @futures].each do |transfers|
|
179
|
+
@accounts_hash = @reporter.accounts_hash
|
180
|
+
transfers.each do |accs, trans|
|
181
|
+
next unless accs.include? name
|
182
|
+
trans.each do |item, details|
|
183
|
+
if details[:currency] != currency
|
184
|
+
#p ['LAGT(O', details[:currency], currency, details, name_c, item]
|
185
|
+
details[:size] *= EXCHANGE_RATES[[details[:currency], currency]]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
|
164
193
|
cache[[:non_discretionary_projected_balance, date]] ||=
|
165
194
|
balance +
|
166
195
|
#@reporter.sum_regular(REGULAR_EXPENDITURE[name], date) +
|
167
196
|
#@reporter.sum_regular(REGULAR_INCOME[name], date) -
|
168
197
|
#@reporter.sum_future(FUTURE_EXPENDITURE[name], date) +
|
169
198
|
#@reporter.sum_future(FUTURE_INCOME[name], date) +
|
170
|
-
(
|
171
|
-
@reporter.sum_future(
|
199
|
+
(@futures.keys.find_all{|from,to| to == name}.map{|key|
|
200
|
+
@reporter.sum_future(@futures[key], date) * money_in_sign
|
172
201
|
}.sum||0) -
|
173
|
-
(
|
174
|
-
@reporter.sum_future(
|
202
|
+
(@futures.keys.find_all{|from,to| from == name}.map{|key|
|
203
|
+
@reporter.sum_future( @futures[key], date) * money_in_sign
|
175
204
|
}.sum||0) +
|
176
|
-
(
|
177
|
-
@reporter.sum_regular(
|
205
|
+
(@regulars.keys.find_all{|from,to| to == name}.map{|key|
|
206
|
+
@reporter.sum_regular(@regulars[key], date) * money_in_sign
|
178
207
|
}.sum||0) -
|
179
|
-
(
|
180
|
-
@reporter.sum_regular(
|
208
|
+
(@regulars.keys.find_all{|from,to| from == name}.map{|key|
|
209
|
+
@reporter.sum_regular( @regulars[key], date) * money_in_sign
|
181
210
|
}.sum||0)
|
182
211
|
end
|
183
212
|
# Write an eps graph to disk of past and projected
|
@@ -203,9 +232,12 @@ EOF
|
|
203
232
|
#ep ['projected_account_factor!!!!', @reporter.projected_account_factor]
|
204
233
|
stable = futuredates.map{|date| projected_balance(date)}
|
205
234
|
kit5 = GraphKit.quick_create([futuredates.map{|d| d.to_time.to_i}, stable])
|
235
|
+
|
236
|
+
[kit2,kit4,kit5].each{|k| k.data[0].y.data[0] = balance(today)}
|
206
237
|
#exit
|
207
238
|
@reporter.projected_account_factor = nil
|
208
|
-
kit += (
|
239
|
+
kit += ( kit4 + kit5 + kit2)
|
240
|
+
kit.yrange = [kit.data.map{|dk| dk.y.data.min}.min, kit.data.map{|dk| dk.y.data.max}.max]
|
209
241
|
#kit += (kit2)
|
210
242
|
kit = kit3 + kit
|
211
243
|
kit.title = "Balance for #{name_c}"
|
@@ -216,11 +248,12 @@ EOF
|
|
216
248
|
|
217
249
|
kit.data[0].gp.title = 'Limit'
|
218
250
|
kit.data[1].gp.title = 'Previous'
|
219
|
-
kit.data[2].gp.title = '0 GBP Discretionary'
|
220
|
-
kit.data[2].gp.title = '
|
221
|
-
kit.data[3].gp.title = '
|
222
|
-
kit.data[4].gp.title = '
|
251
|
+
#kit.data[2].gp.title = '0 GBP Discretionary'
|
252
|
+
kit.data[2].gp.title = 'Avoid Limit'
|
253
|
+
kit.data[3].gp.title = 'Stable'
|
254
|
+
kit.data[4].gp.title = 'Projection'
|
223
255
|
kit.data.each{|dk| dk.gp.with = "l lw 5"}
|
256
|
+
kit.data[4].gp.with = "l lw 5 dt 2 lc rgb 'black' "
|
224
257
|
kit.gp.key = ' bottom left '
|
225
258
|
kit.gp.key = ' rmargin '
|
226
259
|
|
@@ -276,7 +309,7 @@ EOF
|
|
276
309
|
else
|
277
310
|
0.0
|
278
311
|
end
|
279
|
-
}.sum
|
312
|
+
}.sum + sum_of_assets
|
280
313
|
end
|
281
314
|
def projected_balance(date=@reporter.today)
|
282
315
|
@accounts.map{|acc|
|
@@ -288,7 +321,10 @@ EOF
|
|
288
321
|
else
|
289
322
|
0.0
|
290
323
|
end
|
291
|
-
}.sum
|
324
|
+
}.sum + sum_of_assets
|
325
|
+
end
|
326
|
+
def sum_of_assets
|
327
|
+
ASSETS.find_all{|name,details| details[:currency] == currency}.map{|name,details| details[:size]}.sum or 0.0
|
292
328
|
end
|
293
329
|
def summary_table(today, days_before)
|
294
330
|
|
data/lib/treasurer/analysis.rb
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
|
1
|
+
class Date
|
2
|
+
def days_in_month
|
3
|
+
self.class.new(year, month, -1).mday
|
4
|
+
end
|
5
|
+
def days_in_year
|
6
|
+
self.class.new(year, 12, -1).yday
|
7
|
+
end
|
8
|
+
end
|
2
9
|
class Treasurer::Reporter
|
3
10
|
module Analysis
|
4
11
|
# Within the range of the report, return a list
|
@@ -7,38 +14,46 @@ module Analysis
|
|
7
14
|
# for each period and a list of the items within
|
8
15
|
# each period
|
9
16
|
def account_expenditure(account, options={})
|
10
|
-
|
17
|
+
start_dates = []
|
18
|
+
end_dates = []
|
11
19
|
expenditures = []
|
12
20
|
account_items = []
|
13
21
|
date = account.info[:end]||@today
|
14
22
|
#start_date = [(account.info[:start]||@start_date), @start_date].max
|
15
23
|
expenditure = 0
|
16
24
|
items_temp = []
|
17
|
-
items = @runner.component_run_list.values.find_all{|r| r.external_account == account.name and r.in_date(account.info) and @accounts_hash[r.account].
|
25
|
+
items = @runner.component_run_list.values.find_all{|r| r.external_account == account.name and r.in_date(account.info) and @accounts_hash[r.account].original_currency == account.original_currency}
|
18
26
|
#ep ['items', items.map{|i| i.date}]
|
19
27
|
#ep ['account', account.name_c]
|
20
28
|
counter = 0
|
21
29
|
if not account.info[:period]
|
22
|
-
|
30
|
+
start_dates.push date
|
31
|
+
end_dates.push date
|
23
32
|
account_items.push items
|
24
33
|
expenditures.push (items.map{|r| (r.deposit - r.withdrawal) * (account.info[:external] ? -1 : 1)}+[0]).sum
|
25
34
|
else
|
26
|
-
|
35
|
+
end_date = nil
|
27
36
|
case account.info[:period][1]
|
28
37
|
when :month
|
38
|
+
crossed_boundary = false
|
29
39
|
while date > @start_date
|
30
|
-
|
31
|
-
if date.mday == (account.info[:monthday] or 1)
|
40
|
+
if (date+1).mday == (account.info[:monthday] or 1)
|
32
41
|
counter +=1
|
33
42
|
if counter % account.info[:period][0] == 0
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
43
|
+
if crossed_boundary # We only calcate expenditures for whole periods.
|
44
|
+
expenditure = (items_temp.map{|r| (r.deposit - r.withdrawal) * (account.info[:external] ? -1 : 1)}+[0]).sum
|
45
|
+
end_dates.push end_date
|
46
|
+
start_dates.push date+1
|
47
|
+
expenditures.push expenditure
|
48
|
+
account_items.push items_temp
|
49
|
+
end
|
50
|
+
end_date = date
|
51
|
+
crossed_boundary = true
|
52
|
+
items_temp = []
|
53
|
+
expenditure = 0
|
40
54
|
end
|
41
55
|
end
|
56
|
+
items_temp += items.find_all{|r| r.date == date}
|
42
57
|
date-=1
|
43
58
|
end
|
44
59
|
when :day
|
@@ -48,7 +63,7 @@ module Analysis
|
|
48
63
|
counter +=1
|
49
64
|
if counter % account.info[:period][0] == 0
|
50
65
|
expenditure = (items_temp.map{|r| (r.deposit - r.withdrawal) * (account.info[:external] ? -1 : 1)}+[0]).sum
|
51
|
-
|
66
|
+
end_dates.push date
|
52
67
|
expenditures.push expenditure
|
53
68
|
account_items.push items_temp
|
54
69
|
items_temp = []
|
@@ -59,7 +74,7 @@ module Analysis
|
|
59
74
|
end
|
60
75
|
end
|
61
76
|
|
62
|
-
[
|
77
|
+
[start_dates, end_dates, expenditures, account_items]
|
63
78
|
|
64
79
|
end
|
65
80
|
# Work out the average spend from the account and include it in the account info
|
@@ -68,7 +83,8 @@ module Analysis
|
|
68
83
|
projected_accounts_info.each{|key,v| projected_accounts_info[key]=projected_accounts_info[key].dup}
|
69
84
|
projected_accounts_info.each do |account, account_info|
|
70
85
|
#account_info = accounts[account]
|
71
|
-
_dates, expenditures, _items = account_expenditure(account, account_info)
|
86
|
+
#_dates, expenditures, _items = account_expenditure(account, account_info)
|
87
|
+
_start_dates, _dates, expenditures, _items = account_expenditure(account)
|
72
88
|
account.average = expenditures.mean rescue 0.0
|
73
89
|
end
|
74
90
|
projected_accounts_info
|
@@ -79,7 +95,7 @@ module Analysis
|
|
79
95
|
#projected_accounts_info.each{|key,v| projected_accounts_info[key]=projected_accounts_info[key].dup}
|
80
96
|
projected_accounts.each do |account|
|
81
97
|
#account_info = accounts[account]
|
82
|
-
_dates, expenditures, _items = account_expenditure(account)
|
98
|
+
_start_dates, _dates, expenditures, _items = account_expenditure(account)
|
83
99
|
account.projection = expenditures.mean rescue 0.0
|
84
100
|
end
|
85
101
|
projected_accounts.map{|acc| [acc, acc.info]}.to_h
|
@@ -106,18 +122,18 @@ module Analysis
|
|
106
122
|
# items that fall before end_date
|
107
123
|
def sum_future(future_items, end_date, options={})
|
108
124
|
#end_date = @today + @days_ahead
|
109
|
-
|
125
|
+
sum_out = future_items.inject(0.0) do |sum, (_name, item)|
|
110
126
|
item = [item] unless item.kind_of? Array
|
111
|
-
|
127
|
+
value_out = item.inject(0.0) do |value,info|
|
112
128
|
value += info[:size] unless ((@today||Date.today) > info[:date]) or (info[:date] > end_date) # add unless we have already passed that date
|
113
129
|
value
|
114
130
|
|
115
131
|
end
|
116
132
|
#ep ['name2223', name, item, value, end_date, @today, (@today||Date.today > item[0][:date]), (item[0][:date] > end_date)]
|
117
|
-
sum +
|
133
|
+
sum + value_out
|
118
134
|
#rcp.excluding.include?(name) ? sum : sum + value
|
119
135
|
end
|
120
|
-
|
136
|
+
sum_out
|
121
137
|
end
|
122
138
|
# Sum every future occurence of the given
|
123
139
|
# regular items that falls within the account period
|
@@ -130,46 +146,55 @@ module Analysis
|
|
130
146
|
finish = (info[:end] and info[:end] < end_date) ? info[:end] : end_date
|
131
147
|
#today = (Time.now.to_i / (24.0*3600.0)).round
|
132
148
|
|
133
|
-
|
149
|
+
nunits = 0.0
|
134
150
|
counter = info[:period][0] == 1 ? 0 : nil
|
135
151
|
unless counter
|
136
152
|
date = @today
|
137
153
|
counter = 0
|
138
154
|
case info[:period][1]
|
139
155
|
when :month
|
140
|
-
while date >= (info[:start] or
|
156
|
+
while date >= (info[:start] or @today)
|
141
157
|
counter +=1 if date.mday == (info[:monthday] or 1)
|
142
158
|
date -= 1
|
143
159
|
end
|
144
160
|
when :year
|
145
|
-
while date >= (info[:start] or
|
161
|
+
while date >= (info[:start] or @today)
|
146
162
|
counter +=1 if date.yday == (info[:yearday] or 1)
|
147
163
|
date -= 1
|
148
164
|
end
|
149
165
|
when :day
|
150
|
-
while date > (info[:start] or
|
166
|
+
while date > (info[:start] or @today)
|
151
167
|
counter +=1
|
152
168
|
date -= 1
|
153
169
|
end
|
154
170
|
end
|
155
171
|
end
|
172
|
+
delta_units = account.kind_of?(Account) && account.projection
|
156
173
|
date = @today
|
157
174
|
case info[:period][1]
|
158
175
|
when :month
|
159
176
|
#p date, info
|
160
177
|
while date <= finish
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
178
|
+
if delta_units
|
179
|
+
nunits += 1.0/date.days_in_month
|
180
|
+
else
|
181
|
+
if date.mday == (info[:monthday] or 1)
|
182
|
+
nunits += 1 if counter % info[:period][0] == 0
|
183
|
+
counter +=1
|
184
|
+
end
|
185
|
+
end
|
165
186
|
date += 1
|
166
187
|
end
|
167
188
|
when :year
|
168
189
|
while date <= finish
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
190
|
+
if delta_units
|
191
|
+
nunits += 1.0/date.days_in_year
|
192
|
+
else
|
193
|
+
if date.yday == (info[:yearday] or 1)
|
194
|
+
nunits += 1 if counter % info[:period][0] == 0
|
195
|
+
counter +=1
|
196
|
+
end
|
197
|
+
end
|
173
198
|
date += 1
|
174
199
|
end
|
175
200
|
when :day
|
data/lib/treasurer/commands.rb
CHANGED
@@ -24,7 +24,7 @@ class << self
|
|
24
24
|
end
|
25
25
|
def fetch_reporter(copts = {})
|
26
26
|
load_treasurer_folder(copts)
|
27
|
-
reporter = Reporter.new(CodeRunner.fetch_runner(h: :component, A: true), days_before: copts[:b]||360, days_ahead: copts[:a]||180, today: copts[:t])
|
27
|
+
reporter = Reporter.new(CodeRunner.fetch_runner(h: :component, A: true), days_before: copts[:b]||360, days_ahead: copts[:a]||180, today: copts[:t], report_currency: copts[:r])
|
28
28
|
end
|
29
29
|
def status(copts={})
|
30
30
|
load_treasurer_folder(copts)
|
data/lib/treasurer/report.rb
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
require 'active_support/core_ext/integer/inflections'
|
2
2
|
|
3
|
+
class Array
|
4
|
+
def median
|
5
|
+
self.sort[size/2 + size%2]
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
3
9
|
class Float
|
4
10
|
def to_s
|
5
11
|
sprintf("%.2f", self)
|
@@ -59,7 +65,7 @@ end
|
|
59
65
|
class Treasurer
|
60
66
|
class Reporter
|
61
67
|
#include LocalCustomisations
|
62
|
-
attr_reader :today
|
68
|
+
attr_reader :today, :start_date, :end_date
|
63
69
|
attr_reader :in_limit_discretionary_account_factors
|
64
70
|
attr_reader :stable_discretionary_account_factors
|
65
71
|
attr_accessor :projected_account_factor
|
@@ -67,6 +73,8 @@ class Treasurer
|
|
67
73
|
attr_reader :equity
|
68
74
|
attr_reader :projected_accounts_info
|
69
75
|
attr_reader :days_before
|
76
|
+
attr_reader :report_currency
|
77
|
+
attr_reader :accounts_hash
|
70
78
|
def initialize(runner, options)
|
71
79
|
@runner = runner
|
72
80
|
@days_ahead = options[:days_ahead]||180
|
@@ -76,6 +84,7 @@ class Treasurer
|
|
76
84
|
@end_date = @today + @days_ahead
|
77
85
|
@runs = runner.component_run_list.values
|
78
86
|
@currencies = ACCOUNT_INFO.map{|k,v| v[:currencies]}.flatten.uniq
|
87
|
+
@report_currency = options[:report_currency]
|
79
88
|
|
80
89
|
if run = @runs.find{|r| not r.external_account}
|
81
90
|
raise "External_account not specified for #{run.data_line}"
|
@@ -89,22 +98,62 @@ class Treasurer
|
|
89
98
|
def generate_accounts
|
90
99
|
accounts = @runs.map{|r| r.account}.uniq.map{|acc| Account.new(acc, self, @runner, @runs, false)}
|
91
100
|
external_accounts = (@runs.map{|r| r.external_account}.uniq - accounts.map{|acc| acc.name}).map{|acc| Account.new(acc, self, @runner, @runs, true)}
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
101
|
+
#if not @report_currency
|
102
|
+
external_accounts = external_accounts.map do |acc|
|
103
|
+
if acc_inf = ACCOUNT_INFO[acc.name] and currencies = acc_inf[:currencies] and currencies.size > 1
|
104
|
+
raise "Only expense accounts can have multiple currencies: #{acc.name} has type #{acc.type}" unless acc.type == :Expense
|
105
|
+
new_accounts = currencies.map do |curr|
|
106
|
+
Account.new(acc.name, self, @runner, @runs, true, currency: curr)
|
107
|
+
end
|
108
|
+
new_accounts.delete_if{|a| a.runs.size == 0}
|
109
|
+
new_accounts
|
110
|
+
else
|
111
|
+
acc
|
97
112
|
end
|
98
|
-
new_accounts.delete_if{|a| a.runs.size == 0}
|
99
|
-
new_accounts
|
100
|
-
else
|
101
|
-
acc
|
102
113
|
end
|
103
|
-
end
|
114
|
+
#end
|
104
115
|
external_accounts = external_accounts.flatten
|
105
116
|
@accounts = accounts + external_accounts
|
106
117
|
@expense_accounts = @accounts.find_all{|acc| acc.type == :Expense}
|
107
|
-
@accounts_hash = accounts.map{|acc| [acc.name, acc]}.to_h
|
118
|
+
@accounts_hash = @accounts.map{|acc| [acc.name, acc]}.to_h
|
119
|
+
|
120
|
+
if @report_currency
|
121
|
+
@runs.each do |r|
|
122
|
+
if (curr = @accounts_hash[r.account].currency) != @report_currency
|
123
|
+
er = EXCHANGE_RATES[[curr, @report_currency]]
|
124
|
+
r.deposit *= er
|
125
|
+
r.withdrawal *= er
|
126
|
+
r.balance *= er if r.has_balance?
|
127
|
+
end
|
128
|
+
end
|
129
|
+
ASSETS.each do |name, details|
|
130
|
+
details[:size] *= EXCHANGE_RATES[[details[:currency], @report_currency]] if details[:currency]!=@report_currency
|
131
|
+
details[:currency] = @report_currency
|
132
|
+
end
|
133
|
+
[REGULAR_TRANSFERS, FUTURE_TRANSFERS].each do |transfers|
|
134
|
+
transfers.each do |accs, trans|
|
135
|
+
#acc = accs.find{|a| p a, @accounts_hash.keys, @accounts.map{|ac| ac.name}; not @accounts_hash[a].external}
|
136
|
+
trans.each do |item, details|
|
137
|
+
if details[:currency] != @report_currency
|
138
|
+
#p item, acc, curr, @report_currency
|
139
|
+
details[:size] *= EXCHANGE_RATES[[details[:currency], @report_currency]]
|
140
|
+
details[:currency] = @report_currency
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
@accounts.each do |acc|
|
146
|
+
if acc.info[:opening_balance]
|
147
|
+
if acc.currency != @report_currency
|
148
|
+
acc.info[:opening_balance] *= EXCHANGE_RATES[[acc.currency, @report_currency]]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
acc.instance_variable_set(:@original_currency, acc.currency)
|
152
|
+
acc.instance_variable_set(:@currency, @report_currency)
|
153
|
+
acc.info[:currencies] = [@report_currency]
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
108
157
|
get_projected_accounts
|
109
158
|
#p ['projected_accounts_info', @projected_accounts_info]
|
110
159
|
#exit
|
@@ -115,6 +164,7 @@ class Treasurer
|
|
115
164
|
end
|
116
165
|
@equities = @equities.to_h
|
117
166
|
end
|
167
|
+
|
118
168
|
def report
|
119
169
|
generate_accounts
|
120
170
|
#get_actual_accounts
|
@@ -149,12 +199,18 @@ class Treasurer
|
|
149
199
|
<<EOF
|
150
200
|
\\section{Summary of Accounts}
|
151
201
|
#{[:Equity, :Asset, :Liability, :Income, :Expense].map{|type|
|
202
|
+
accs = @accounts.find_all{|acc| acc.type == type }
|
152
203
|
"\\subsection{#{type}}
|
153
204
|
\\begin{tabulary}{0.9\\textwidth}{ R | c | c | c}
|
154
205
|
Account & Balance & Deposited & Withdrawn \\\\
|
155
206
|
\\hline
|
156
207
|
\\Tstrut
|
157
|
-
#{
|
208
|
+
#{(accs.map{|acc| acc.summary_line(@today, @days_before)} +
|
209
|
+
(type == :Asset ? ASSETS.map{|n,details| "#{n} (#{details[:currency]}) & #{details[:size]} & & "} : [])).join("\\\\\n")}
|
210
|
+
#{type!=:Equity&&false ? "
|
211
|
+
\\\\ \\hline
|
212
|
+
\\Tstrut
|
213
|
+
Totals & #{accs.map{|a| a.balance}.sum} & #{accs.map{|a| a.deposited(@today, @days_before)}.sum} & #{accs.map{|a| a.withdrawn(@today, @days_before)}.sum} \\\\ " : "\\\\"}
|
158
214
|
\\end{tabulary}"
|
159
215
|
}.join("\n\n")}
|
160
216
|
EOF
|
@@ -175,21 +231,13 @@ EOF
|
|
175
231
|
def expense_account_summary
|
176
232
|
<<EOF
|
177
233
|
\\section{Expense Account Summary}
|
178
|
-
\\subsection{Budget Period}
|
234
|
+
\\subsection{Totals for #@days_before-day Budget Period}
|
179
235
|
#{expense_pie_charts_by_currency('accountperiod', @expense_accounts){|r| r.days_ago(@today) < @days_before}}
|
180
|
-
\\subsection{
|
181
|
-
#{expense_pie_charts_by_currency('lastweekexpenses', @expense_accounts){|r|
|
182
|
-
#p ['r.daysago', r.days_ago(@today)];
|
183
|
-
r.days_ago(@today) < 7}}
|
184
|
-
\\subsection{Last Month}
|
185
|
-
#{expense_pie_charts_by_currency('lastmonthexpenses', @expense_accounts){|r| r.days_ago(@today) < 30}}
|
186
|
-
\\subsection{Last Year}
|
187
|
-
#{expense_pie_charts_by_currency('lastyearexpenses', @expense_accounts){|r| r.days_ago(@today) < 365}}
|
188
|
-
\\section{Expense Account Breakdown}
|
236
|
+
\\subsection{Expense Account Breakdown}
|
189
237
|
#{@expense_accounts.map{|account|
|
190
238
|
#ep ['sub_accounts2124', account.sub_accounts.map{|sa| sa.name}]
|
191
239
|
"\\subsection{#{account.name_c}}
|
192
|
-
#{expense_pie_chart(account.name_c_file + 'breakdown', account.sub_accounts){|r|r.days_ago(@today) < @days_before }}"
|
240
|
+
#{expense_pie_chart(account.name_c_file + 'breakdown', account.sub_accounts, account){|r|r.days_ago(@today) < @days_before }}"
|
193
241
|
}.join("\n\n")}
|
194
242
|
EOF
|
195
243
|
|
@@ -215,13 +263,33 @@ EOF
|
|
215
263
|
end
|
216
264
|
).join("\n\n")
|
217
265
|
end
|
218
|
-
def expense_pie_chart(name, accounts, &block)
|
266
|
+
def expense_pie_chart(name, accounts, subacc=nil, &block)
|
219
267
|
#expaccs = accounts.find_all{|acc| acc.type == :Expense}
|
220
268
|
labels = accounts.map{|acc| acc.name}
|
221
|
-
exps = accounts.map{|acc| acc.deposited(@today, 50000, &block)}
|
222
|
-
labels, exps = [labels, exps].transpose.find_all{|l, e| e != 0.0}.transpose
|
223
269
|
#ep ['labels22539', name, labels, exps]
|
224
|
-
|
270
|
+
|
271
|
+
kit = if subacc
|
272
|
+
start_dates, end_dates, _exps, _items = account_expenditure(subacc)
|
273
|
+
end_dates = end_dates.reverse #Now from earliest to latest
|
274
|
+
start_dates = start_dates.reverse
|
275
|
+
pp ['DATES', start_dates, end_dates, subacc.name]
|
276
|
+
return "No expenditure in account period." if end_dates.size==0
|
277
|
+
k = (
|
278
|
+
end_dates.size.times.map do |i|
|
279
|
+
exps = accounts.map{|acc| acc.deposited(end_dates[i], end_dates[i] - start_dates[i], &block)}
|
280
|
+
kt = GraphKit.quick_create([labels.size.times.to_a.map{|l| l.to_f + i.to_f/end_dates.size.to_f}, exps])
|
281
|
+
kt.data[0].gp.title = "Ending #{end_dates[i].strftime("#{end_dates[i].mday.ordinalize} %B")}; total = #{exps.sum}"
|
282
|
+
kt.gp.key = "tmargin"
|
283
|
+
kt
|
284
|
+
end
|
285
|
+
).sum
|
286
|
+
k
|
287
|
+
else
|
288
|
+
exps = accounts.map{|acc| acc.deposited(@today, 50000, &block)}
|
289
|
+
labels, exps = [labels, exps].transpose.find_all{|l, e| e != 0.0}.transpose
|
290
|
+
return "No expenditure in account period." if not labels #<F8> labels.size==0
|
291
|
+
GraphKit.quick_create([labels.size.times.to_a, exps])
|
292
|
+
end
|
225
293
|
|
226
294
|
|
227
295
|
#sum = exps.sum
|
@@ -231,21 +299,20 @@ EOF
|
|
231
299
|
#end_angles = angles.inject(-angles[0]){|o,n| o+n}
|
232
300
|
|
233
301
|
|
234
|
-
kit
|
235
|
-
kit.
|
236
|
-
kit.gp.boxwidth = "#{0.8} absolute"
|
302
|
+
kit.data.each{|dk| dk.gp.with = 'boxes'}
|
303
|
+
kit.gp.boxwidth = "#{0.8/kit.data.size} absolute"
|
237
304
|
kit.gp.style = "fill solid"
|
238
|
-
kit.gp.yrange = "[#{[kit.data[0].
|
305
|
+
kit.gp.yrange = "[#{[kit.data[0].y.data.min,0].min}:]"
|
239
306
|
#kit.gp.xrange = "[-1:#{labels.size+1}]"
|
240
307
|
kit.gp.xrange = "[-1:1]" if labels.size==1
|
241
308
|
kit.gp.grid = "ytics"
|
242
309
|
kit.xlabel = nil
|
243
310
|
kit.ylabel = nil
|
244
|
-
#pp ['kit222', kit, labels]
|
245
311
|
i = -1
|
246
312
|
kit.gp.xtics = "(#{labels.map{|l| %["#{l}" #{i+=1}]}.join(', ')}) rotate by 315"
|
313
|
+
pp ['kit222', kit, labels]
|
247
314
|
fork do
|
248
|
-
kit.gnuplot_write("#{name}.eps", size: "4.0in,
|
315
|
+
kit.gnuplot_write("#{name}.eps", size: "4.0in,2.0in")
|
249
316
|
%x[epspdf #{name}.eps]
|
250
317
|
end
|
251
318
|
|
@@ -285,10 +352,11 @@ EOF
|
|
285
352
|
#ep ['projected_account_factor', date, balances.mean, @projected_account_factor, @equities[currency].balance(@today), ok]
|
286
353
|
end
|
287
354
|
ok = false if balances.mean < @equities[currency].balance(@today) - 0.001
|
355
|
+
#ok = false if balances.median < @equities[currency].balance(@today) - 0.001
|
288
356
|
@stable_discretionary_account_factors[currency] = @projected_account_factor
|
289
357
|
break if (@projected_account_factor <= 0.0 or ok == true)
|
290
358
|
@projected_account_factor -= 0.01
|
291
|
-
|
359
|
+
#@projected_account_factor -= 0.1
|
292
360
|
end
|
293
361
|
@projected_account_factor = nil
|
294
362
|
#exit
|
@@ -324,20 +392,23 @@ EOF
|
|
324
392
|
"#{accounts.map{|account|
|
325
393
|
account_info = account.info
|
326
394
|
#ep ['accountbadf', account, account_info]
|
327
|
-
dates, expenditures, _items = account_expenditure(account)
|
395
|
+
start_dates, dates, expenditures, _items = account_expenditure(account)
|
328
396
|
ep ['accountbadf', account.name_c, account_info, expenditures]
|
329
397
|
if dates.size == 0
|
330
398
|
""
|
331
399
|
else
|
332
400
|
#ep ['account', account, dates, expenditures]
|
333
|
-
|
401
|
+
plotdates = dates.zip(start_dates).map{|d, s| (d.to_time.to_i + s.to_time.to_i)/2.0}
|
402
|
+
kit = GraphKit.quick_create([plotdates, expenditures])
|
334
403
|
kit.data.each{|dk| dk.gp.with="boxes"}
|
335
404
|
kit.gp.style = "fill solid"
|
336
405
|
kit.xlabel = nil
|
337
406
|
kit.ylabel = "Expenditure"
|
338
407
|
kit.data[0].gp.with = 'boxes'
|
339
408
|
dat = kit.data[0].x.data
|
340
|
-
|
409
|
+
barsize = (dat.max.to_f - dat.min.to_f)/dat.size * 0.8
|
410
|
+
kit.gp.boxwidth = "#{barsize} absolute"
|
411
|
+
dat.map!{|d| d - barsize*1.1}
|
341
412
|
kit.gp.yrange = "[#{[kit.data[0].y.data.min,0].min}:]"
|
342
413
|
#kit.gp.xrange = "[-1:#{labels.size+1}]"
|
343
414
|
#kit.gp.xrange = "[-1:1]" if labels.size==1
|
@@ -348,7 +419,7 @@ EOF
|
|
348
419
|
kits = accounts_with_averages({account => account_info}).map{|account, account_info|
|
349
420
|
#ep 'Budget is ', account
|
350
421
|
kit2 = GraphKit.quick_create([
|
351
|
-
[dates[0], dates[-1]].map{|d| d.to_time.to_i},
|
422
|
+
[dates[0], dates[-1]].map{|d| d.to_time.to_i - barsize},
|
352
423
|
[account.average, account.average]
|
353
424
|
])
|
354
425
|
kit2.data[0].gp.with = 'l lw 5'
|
@@ -447,7 +518,7 @@ EOF
|
|
447
518
|
<<EOF
|
448
519
|
\\section{SubAccount Breakdown}
|
449
520
|
#{(@actual_accounts).map{|account, account_info|
|
450
|
-
dates, expenditures, account_items = account_expenditure(account, account_info)
|
521
|
+
_start_dates, dates, expenditures, account_items = account_expenditure(account, account_info)
|
451
522
|
#pp account, account_items.map{|items| items.map{|i| i.date.to_s}}
|
452
523
|
"\\subsection{#{account}}" +
|
453
524
|
account_items.zip(dates, expenditures).map{|items, date, expenditure|
|
@@ -549,7 +620,9 @@ stringstyle=\\color{orange}}
|
|
549
620
|
\\setlength{\\topsep}{0pt}
|
550
621
|
\\setlength{\\partopsep}{0pt}
|
551
622
|
\\begin{document}
|
552
|
-
\\title{Budget Report from #{@start_date.strftime("%A #{@start_date.day.ordinalize} of %B %Y")} to #{@
|
623
|
+
\\title{Budget Report from #{@start_date.strftime("%A #{@start_date.day.ordinalize} of %B %Y")} to #{@today.strftime("%A #{@today.day.ordinalize} of %B %Y")}}
|
624
|
+
\\author{With projections to #{@end_date.strftime("%A #{@end_date.day.ordinalize} of %B %Y")}}
|
625
|
+
\\date{\\today}
|
553
626
|
\\maketitle
|
554
627
|
\\tableofcontents
|
555
628
|
EOF
|
@@ -562,3 +635,11 @@ EOF
|
|
562
635
|
|
563
636
|
end
|
564
637
|
end
|
638
|
+
#\\subsection{Last Week}
|
639
|
+
##{expense_pie_charts_by_currency('lastweekexpenses', @expense_accounts){|r|
|
640
|
+
##p ['r.daysago', r.days_ago(@today)];
|
641
|
+
#r.days_ago(@today) < 7}}
|
642
|
+
#\\subsection{Last Month}
|
643
|
+
##{expense_pie_charts_by_currency('lastmonthexpenses', @expense_accounts){|r| r.days_ago(@today) < 30}}
|
644
|
+
#\\subsection{Last Year}
|
645
|
+
##{expense_pie_charts_by_currency('lastyearexpenses', @expense_accounts){|r| r.days_ago(@today) < 365}}
|
data/lib/treasurer.rb
CHANGED
@@ -59,6 +59,7 @@ EOF
|
|
59
59
|
['--today', '-t', GetoptLong::REQUIRED_ARGUMENT, "Specify today's date, i.e. change the date on which the report is generated."],
|
60
60
|
['--coderunner', '-C', GetoptLong::REQUIRED_ARGUMENT, "Options to pass to CodeRunner, the engine which manages the transaction data."],
|
61
61
|
['--month', '-m', GetoptLong::REQUIRED_ARGUMENT, "Overrides -a, -b and -t and produces a report for a given month"],
|
62
|
+
['--report-currency', '-r', GetoptLong::REQUIRED_ARGUMENT, "Convert all amounts to the given currency (e.g. GBP) and amalgamate different currency data to form a unified overview."],
|
62
63
|
#['--formats', '-f', GetoptLong::REQUIRED_ARGUMENT, "A list of formats pertaining to the various input and output files (in the order which they appear), separated by commas. If they are all the same, only one value may be given. If a value is left empty (i.e. there are two commas in a row) then the previous value will be used. Currently supported formats are #{SUPPORTED_FORMATS.inspect}. "],
|
63
64
|
|
64
65
|
]
|
@@ -94,8 +95,8 @@ class Treasurer
|
|
94
95
|
# options (copts) hash
|
95
96
|
def setup(copts)
|
96
97
|
# None neededed
|
97
|
-
copts[:b] = copts[:b].to_i
|
98
|
-
copts[:a] = copts[:a].to_i
|
98
|
+
copts[:b] = copts[:b].to_i if copts[:b]
|
99
|
+
copts[:a] = copts[:a].to_i if copts[:a]
|
99
100
|
copts[:t] = Date.parse(copts[:t]) if copts[:t]
|
100
101
|
end
|
101
102
|
def verbosity
|
data/treasurer.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: treasurer 0.5.
|
5
|
+
# stub: treasurer 0.5.1 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "treasurer".freeze
|
9
|
-
s.version = "0.5.
|
9
|
+
s.version = "0.5.1"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
13
13
|
s.authors = ["Edmund Highcock".freeze]
|
14
|
-
s.date = "2018-03-
|
14
|
+
s.date = "2018-03-07"
|
15
15
|
s.description = "A simple command line tool for managing accounts and finances. Easily import internet banking spreadsheets and generate sophisticated reports and projections.".freeze
|
16
16
|
s.email = "edmundhighcock@users.sourceforge.net".freeze
|
17
17
|
s.executables = ["treasurer".freeze]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: treasurer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Edmund Highcock
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-03-
|
11
|
+
date: 2018-03-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|