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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 931217424d6cdc37f7c8dcc760fd08bc9d5d2a3d
4
- data.tar.gz: a48e8e11f67f770600ff93f6506a4eed78eb2c8d
3
+ metadata.gz: b5eb9890a54a614720f287ec3cd65d77478b8225
4
+ data.tar.gz: 7341d4cb3c37507bdacf15b7d49b53d83de79749
5
5
  SHA512:
6
- metadata.gz: abb2e38f758b4737b9084479661e274a2dcc5fb536b01327d032374991ab02da419fb33d412fdd26799da0ebe53b018bf73fe77cff90a361dfe3668384e232c1
7
- data.tar.gz: 9abf97d6b4166d1e71b636d0486ab69369f7645644882b0d212541d19f37ae90e35cc4f8a823cdb3f39221ab9d99dd76ba9763c39b6cfa909d71082525f7edbe
6
+ metadata.gz: 3e86953149b37629f61297e90beeb3ebc7e41b70816f38c45f332a3f333fd40b5bd3b40a696c69b6dc33d2a103e590653ac13e9926280c5a95e3bdc6d9d0b252
7
+ data.tar.gz: c58e49206abb4f80e48a98af57659374aa14d38841c110ac1bc73bb1b6825b2961e82f978addf28273683e43310e4756e65c451d88d0554442f2784d8d47fa65
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.0
1
+ 0.5.1
@@ -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
- 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)
79
- balance += info[:opening_balance] if info[:opening_balance]
80
- balance
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
- #p ['name22 is ', name, type, @runs.size]
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 @currency
103
- " (#@currency)"
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[:linked_account] == name and ext_ac.currency == currency}]
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
- (FUTURE_TRANSFERS.keys.find_all{|from,to| to == name}.map{|key|
171
- @reporter.sum_future(FUTURE_TRANSFERS[key], date) * money_in_sign
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
- (FUTURE_TRANSFERS.keys.find_all{|from,to| from == name}.map{|key|
174
- @reporter.sum_future( FUTURE_TRANSFERS[key], date) * money_in_sign
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
- (REGULAR_TRANSFERS.keys.find_all{|from,to| to == name}.map{|key|
177
- @reporter.sum_regular(REGULAR_TRANSFERS[key], date) * money_in_sign
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
- (REGULAR_TRANSFERS.keys.find_all{|from,to| from == name}.map{|key|
180
- @reporter.sum_regular( REGULAR_TRANSFERS[key], date) * money_in_sign
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 += (kit2 + kit4 + kit5)
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 = 'Projection'
221
- kit.data[3].gp.title = 'Limit'
222
- kit.data[4].gp.title = 'Stable'
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
 
@@ -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
- dates = []
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].currency == account.currency}
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
- dates.push date
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
- items_temp += items.find_all{|r| r.date == date}
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
- expenditure = (items_temp.map{|r| (r.deposit - r.withdrawal) * (account.info[:external] ? -1 : 1)}+[0]).sum
35
- dates.push date
36
- expenditures.push expenditure
37
- account_items.push items_temp
38
- items_temp = []
39
- expenditure = 0
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
- dates.push date
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
- [dates, expenditures, account_items]
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
- sum = future_items.inject(0.0) do |sum, (name, item)|
125
+ sum_out = future_items.inject(0.0) do |sum, (_name, item)|
110
126
  item = [item] unless item.kind_of? Array
111
- value = item.inject(0.0) do |value,info|
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 + value
133
+ sum + value_out
118
134
  #rcp.excluding.include?(name) ? sum : sum + value
119
135
  end
120
- sum
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
- nunits = 0
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 Date.today)
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 Date.today)
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 Date.today)
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
- if date.mday == (info[:monthday] or 1)
162
- nunits += 1 if counter % info[:period][0] == 0
163
- counter +=1
164
- end
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
- if date.yday == (info[:yearday] or 1)
170
- nunits += 1 if counter % info[:period][0] == 0
171
- counter +=1
172
- end
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
@@ -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)
@@ -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
- external_accounts = external_accounts.map do |acc|
93
- if acc_inf = ACCOUNT_INFO[acc.name] and currencies = acc_inf[:currencies] and currencies.size > 1
94
- raise "Only expense accounts can have multiple currencies: #{acc.name} has type #{acc.type}" unless acc.type == :Expense
95
- new_accounts = currencies.map do |curr|
96
- Account.new(acc.name, self, @runner, @runs, true, currency: curr)
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
- #{@accounts.find_all{|acc| acc.type == type }.map{|acc| acc.summary_line(@today, @days_before)}.join("\\\\\n")}
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{Last Week}
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
- return "No expenditure in account period." if labels == nil
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 = GraphKit.quick_create([labels.size.times.to_a, exps])
235
- kit.data[0].gp.with = 'boxes'
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].x.data.min,0].min}:]"
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,1.8in")
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
- @projected_account_factor -= 0.1
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
- kit = GraphKit.quick_create([dates.map{|d| d.to_time.to_i}, expenditures])
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
- kit.gp.boxwidth = "#{(dat.max.to_f - dat.min.to_f)/dat.size * 0.8} absolute"
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 #{@end_date.strftime("%A #{@end_date.day.ordinalize} of %B %Y")}}
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.0 ruby lib
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.0"
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-04"
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.0
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-04 00:00:00.000000000 Z
11
+ date: 2018-03-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport