treasurer 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +3 -0
- data/VERSION +1 -1
- data/bin/treasurer +10 -0
- data/lib/treasurer/analysis.rb +194 -0
- data/lib/treasurer/commands.rb +48 -0
- data/lib/treasurer/local_customisations.rb +111 -0
- data/lib/treasurer/report.rb +570 -0
- data/lib/treasurer.rb +113 -0
- data/test/bankaccountstatement.csv +12 -0
- data/test/multiple/FirstBank.csv +2 -0
- data/test/multiple/SecondBank.csv +2 -0
- data/test/otheraccountstatement.csv +9 -0
- data/test/test_treasurer.rb +16 -3
- data/treasurer.gemspec +22 -3
- metadata +55 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7093dd3826d606b0ba1f76ae379ea9ff02d74211
|
4
|
+
data.tar.gz: 54f3cde9ccd02559074114f691dc03d48f44d55f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cbcec24e8990da5ed2131023662c5cfee4d65332f90ca1547f96d648865dbc5051a511dde77c78f27580b56832ff827b3bdc8d483efc65e351673304ccf568da
|
7
|
+
data.tar.gz: d78965e3d3b81edb9fec098aa33a2b58e17a254c71c85e1ad490ed4407434e1279cc9f187d16a6f8cec3539d1758114bb914c3751886ac56c7267bd944c59daf
|
data/Gemfile
CHANGED
@@ -2,6 +2,9 @@ source "http://rubygems.org"
|
|
2
2
|
# Add dependencies required to use your gem here.
|
3
3
|
# Example:
|
4
4
|
# gem "activesupport", ">= 2.3.5"
|
5
|
+
gem "coderunner", ">= 0.14.16"
|
6
|
+
gem "budgetcrmod", ">= 0.0.0"
|
7
|
+
gem "command-line-flunky", ">= 1.0.0"
|
5
8
|
|
6
9
|
# Add dependencies to develop your gem here.
|
7
10
|
# Include everything needed to run rake, tests, features, etc.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.1.0
|
data/bin/treasurer
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
|
2
|
+
class Treasurer::Reporter
|
3
|
+
module Analysis
|
4
|
+
# Within the range of the report, return a list
|
5
|
+
# of the dates of the beginning of each budget
|
6
|
+
# period, along with a list of the expenditures
|
7
|
+
# for each period and a list of the items within
|
8
|
+
# each period
|
9
|
+
def budget_expenditure(budget, budget_info, options={})
|
10
|
+
dates = []
|
11
|
+
expenditures = []
|
12
|
+
budget_items = []
|
13
|
+
date = budget_info[:end]||@today
|
14
|
+
start_date = [(budget_info[:start]||@start_date), @start_date].max
|
15
|
+
expenditure = 0
|
16
|
+
items_temp = []
|
17
|
+
items = @runner.component_run_list.values.find_all{|r| r.budget == budget and r.in_date(budget_info)}
|
18
|
+
#ep ['items', items]
|
19
|
+
#ep ['budget', budget]
|
20
|
+
counter = 0
|
21
|
+
if not budget_info[:period]
|
22
|
+
dates.push date
|
23
|
+
budget_items.push items
|
24
|
+
expenditures.push (items.map{|r| r.debit - r.credit}+[0]).sum
|
25
|
+
else
|
26
|
+
|
27
|
+
case budget_info[:period][1]
|
28
|
+
when :month
|
29
|
+
while date > @start_date
|
30
|
+
items_temp += items.find_all{|r| r.date == date}
|
31
|
+
if date.mday == (budget_info[:monthday] or 1)
|
32
|
+
counter +=1
|
33
|
+
if counter % budget_info[:period][0] == 0
|
34
|
+
expenditure = (items_temp.map{|r| r.debit - r.credit}+[0]).sum
|
35
|
+
dates.push date
|
36
|
+
expenditures.push expenditure
|
37
|
+
budget_items.push items_temp
|
38
|
+
items_temp = []
|
39
|
+
expenditure = 0
|
40
|
+
end
|
41
|
+
end
|
42
|
+
date-=1
|
43
|
+
end
|
44
|
+
when :day
|
45
|
+
while date > @start_date
|
46
|
+
items_temp += items.find_all{|r| r.date == date}
|
47
|
+
#expenditure += (budget_items[-1].map{|r| r.debit}+[0]).sum
|
48
|
+
counter +=1
|
49
|
+
if counter % budget_info[:period][0] == 0
|
50
|
+
expenditure = (items_temp.map{|r| r.debit - r.credit}+[0]).sum
|
51
|
+
dates.push date
|
52
|
+
expenditures.push expenditure
|
53
|
+
budget_items.push items_temp
|
54
|
+
items_temp = []
|
55
|
+
expenditure = 0
|
56
|
+
end
|
57
|
+
date-=1
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
[dates, expenditures, budget_items]
|
63
|
+
|
64
|
+
end
|
65
|
+
# Work out the average spend from the budget and include it in the budget info
|
66
|
+
def budgets_with_averages(budgets, options={})
|
67
|
+
projected_budgets = budgets.dup
|
68
|
+
projected_budgets.each{|key,v| projected_budgets[key]=projected_budgets[key].dup}
|
69
|
+
projected_budgets.each do |budget, budget_info|
|
70
|
+
#budget_info = budgets[budget]
|
71
|
+
dates, expenditures, items = budget_expenditure(budget, budget_info)
|
72
|
+
budget_info[:average] = expenditures.mean rescue 0.0
|
73
|
+
end
|
74
|
+
projected_budgets
|
75
|
+
end
|
76
|
+
# Work out the projected spend from the budget and include it in the budget info
|
77
|
+
def budgets_with_projections(budgets, options={})
|
78
|
+
projected_budgets = budgets.dup
|
79
|
+
projected_budgets.each{|key,v| projected_budgets[key]=projected_budgets[key].dup}
|
80
|
+
projected_budgets.each do |budget, budget_info|
|
81
|
+
#budget_info = budgets[budget]
|
82
|
+
dates, expenditures, items = budget_expenditure(budget, budget_info)
|
83
|
+
budget_info[:projection] = expenditures.mean rescue 0.0
|
84
|
+
end
|
85
|
+
projected_budgets
|
86
|
+
end
|
87
|
+
# Get a list of budgets to be included in the report
|
88
|
+
# i.e. budgets with non-empty expenditure
|
89
|
+
def get_actual_budgets
|
90
|
+
@actual_budgets = BUDGETS.dup
|
91
|
+
BUDGETS.keys.each do |budget|
|
92
|
+
@actual_budgets.delete(budget) if budget_expenditure(budget, BUDGETS[budget])[0].size == 0
|
93
|
+
end
|
94
|
+
end
|
95
|
+
# Find all discretionary budgets and estimate the future
|
96
|
+
# expenditure from that budget based on past
|
97
|
+
# expenditure (currently only a simple average)
|
98
|
+
def get_projected_budgets
|
99
|
+
@projected_budgets = Hash[@actual_budgets.dup.find_all{|k,v| v[:discretionary]}]
|
100
|
+
@projected_budgets = budgets_with_projections(@projected_budgets)
|
101
|
+
end
|
102
|
+
# Calculate the sum of all items within future
|
103
|
+
# items that fall before end_date
|
104
|
+
def sum_future(future_items, end_date, options={})
|
105
|
+
#end_date = @today + @days_ahead
|
106
|
+
sum = future_items.inject(0.0) do |sum, (name, item)|
|
107
|
+
item = [item] unless item.kind_of? Array
|
108
|
+
value = item.inject(0.0) do |value,info|
|
109
|
+
value += info[:size] unless ((@today||Date.today) > info[:date]) or (info[:date] > end_date) # add unless we have already passed that date
|
110
|
+
value
|
111
|
+
|
112
|
+
end
|
113
|
+
#ep ['name2223', name, item, value, end_date, @today, (@today||Date.today > item[0][:date]), (item[0][:date] > end_date)]
|
114
|
+
sum + value
|
115
|
+
#rcp.excluding.include?(name) ? sum : sum + value
|
116
|
+
end
|
117
|
+
sum
|
118
|
+
end
|
119
|
+
# Sum every future occurence of the given
|
120
|
+
# regular items that falls within the budget period
|
121
|
+
def sum_regular(regular_items, end_date, options={})
|
122
|
+
#end_date = @today + @days_ahead
|
123
|
+
sum = regular_items.inject(0) do |sum, (name, item)|
|
124
|
+
item = [item] unless item.kind_of? Array
|
125
|
+
# ep item
|
126
|
+
value = item.inject(0) do |value,info|
|
127
|
+
finish = (info[:end] and info[:end] < end_date) ? info[:end] : end_date
|
128
|
+
#today = (Time.now.to_i / (24.0*3600.0)).round
|
129
|
+
|
130
|
+
nunits = 0
|
131
|
+
counter = info[:period][0] == 1 ? 0 : nil
|
132
|
+
unless counter
|
133
|
+
date = @today
|
134
|
+
counter = 0
|
135
|
+
case info[:period][1]
|
136
|
+
when :month
|
137
|
+
while date >= (info[:start] or Date.today)
|
138
|
+
counter +=1 if date.mday == (info[:monthday] or 1)
|
139
|
+
date -= 1
|
140
|
+
end
|
141
|
+
when :year
|
142
|
+
while date >= (info[:start] or Date.today)
|
143
|
+
counter +=1 if date.yday == (info[:yearday] or 1)
|
144
|
+
date -= 1
|
145
|
+
end
|
146
|
+
when :day
|
147
|
+
while date > (info[:start] or Date.today)
|
148
|
+
counter +=1
|
149
|
+
date -= 1
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
date = @today
|
154
|
+
case info[:period][1]
|
155
|
+
when :month
|
156
|
+
#p date, info
|
157
|
+
while date <= finish
|
158
|
+
if date.mday == (info[:monthday] or 1)
|
159
|
+
nunits += 1 if counter % info[:period][0] == 0
|
160
|
+
counter +=1
|
161
|
+
end
|
162
|
+
date += 1
|
163
|
+
end
|
164
|
+
when :year
|
165
|
+
while date <= finish
|
166
|
+
if date.yday == (info[:yearday] or 1)
|
167
|
+
nunits += 1 if counter % info[:period][0] == 0
|
168
|
+
counter +=1
|
169
|
+
end
|
170
|
+
date += 1
|
171
|
+
end
|
172
|
+
when :day
|
173
|
+
while date <= finish
|
174
|
+
nunits += 1 if counter % info[:period][0] == 0
|
175
|
+
counter +=1
|
176
|
+
date += 1
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
|
182
|
+
#ep ['name2234', name, info, @projected_budget_factor] if info[:discretionary]
|
183
|
+
|
184
|
+
value + nunits * (info[:size]||info[:projection]*(@projected_budget_factor||1.0))
|
185
|
+
|
186
|
+
end
|
187
|
+
sum + value
|
188
|
+
#(rcp.excluding? and rcp.excluding.include?(name)) ? sum : sum + value
|
189
|
+
end
|
190
|
+
sum
|
191
|
+
end
|
192
|
+
end
|
193
|
+
include Analysis
|
194
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
class Treasurer
|
3
|
+
class << self
|
4
|
+
def add_file(file, account, copts={})
|
5
|
+
load_treasurer_folder
|
6
|
+
ep 'entries', Dir.entries
|
7
|
+
CodeRunner.submit(p: "{data_file: '#{File.expand_path(file)}', account: :#{account}}")
|
8
|
+
end
|
9
|
+
def add_folder(folder, copts={})
|
10
|
+
#Dir.chdir(folder) do
|
11
|
+
files = Dir.entries(folder).grep(/\.csv$/)
|
12
|
+
accounts = files.map{|f| f.sub(/\.csv/, '')}
|
13
|
+
files = files.map{|f| folder + '/' + f}
|
14
|
+
p ['files789', files, accounts]
|
15
|
+
files.zip(accounts).each{|f,a| add_file(f, a, copts)}
|
16
|
+
#end
|
17
|
+
end
|
18
|
+
def check_is_treasurer_folder
|
19
|
+
raise "This folder has not been set up to use with Treasurer; please initialise a folder with treasurer init" unless FileTest.exist? '.code_runner_script_defaults.rb' and eval(File.read('.code_runner_script_defaults.rb'))[:code] == 'budget'
|
20
|
+
end
|
21
|
+
def init_root_folder(folder, copts={})
|
22
|
+
raise "Folder already exists" if FileTest.exist? folder
|
23
|
+
FileUtils.makedirs(folder)
|
24
|
+
FileUtils.cp(SCRIPT_FOLDER + '/treasurer/local_customisations.rb', folder + '/local_customisations.rb')
|
25
|
+
CodeRunner.fetch_runner(Y: folder, C: 'budget', X: '/dev/null')
|
26
|
+
eputs "\n\n Your treasurer folder '#{folder}' has been set up. All further treasurer commands should be run from within this folder.\n"
|
27
|
+
end
|
28
|
+
def load_treasurer_folder
|
29
|
+
check_is_treasurer_folder
|
30
|
+
Treasurer.send(:remove_const, :LocalCustomisations) if defined? Treasurer::LocalCustomisations
|
31
|
+
load 'local_customisations.rb'
|
32
|
+
Treasurer::Reporter.send(:include, Treasurer::LocalCustomisations)
|
33
|
+
Treasurer::Reporter::Account.send(:include, Treasurer::LocalCustomisations)
|
34
|
+
Treasurer::Reporter::Analysis.send(:include, Treasurer::LocalCustomisations)
|
35
|
+
CodeRunner::Budget.send(:include, Treasurer::LocalCustomisations)
|
36
|
+
runner = CodeRunner.fetch_runner
|
37
|
+
end
|
38
|
+
def report(copts = {})
|
39
|
+
load_treasurer_folder
|
40
|
+
reporter = Reporter.new(CodeRunner.fetch_runner(h: :component), days_before: copts[:b]||360, days_ahead: copts[:a]||180, today: copts[:t])
|
41
|
+
reporter.report()
|
42
|
+
end
|
43
|
+
|
44
|
+
def method_missing(meth, *args)
|
45
|
+
CodeRunner.send(meth, *args)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
#class CodeRunner::Budget
|
2
|
+
module Treasurer::LocalCustomisations
|
3
|
+
|
4
|
+
|
5
|
+
REGULAR_TRANSFERS = {
|
6
|
+
[:FirstBank, :SecondBank] =>{
|
7
|
+
topup: {size: 200, period: [1, :month], monthday: 1, end: Date.parse("01/10/2014")},
|
8
|
+
},
|
9
|
+
[:FirstBank, :Rent] =>{
|
10
|
+
house: {size: 600, period: [1, :month], monthday: 20, end: Date.parse("01/07/2013")},
|
11
|
+
},
|
12
|
+
[:Income, :FirstBank] =>{
|
13
|
+
pay: {size: 1000, period: [1, :month], monthday: 1, end: Date.parse("01/07/2014")},
|
14
|
+
},
|
15
|
+
|
16
|
+
}
|
17
|
+
|
18
|
+
REGULAR_TRANSFERS.default = {}
|
19
|
+
|
20
|
+
FUTURE_TRANSFERS = {
|
21
|
+
[:Income, :SecondBank] =>{
|
22
|
+
topup: {size: 100, date: Date.parse("26/09/2010")},
|
23
|
+
},
|
24
|
+
[:FirstBank, :PersonalLoans] =>{
|
25
|
+
payfriend: {size: 640, date: Date.parse("25/09/2010")},
|
26
|
+
borrowfromfriend: {size: -840, date: Date.parse("28/09/2010")},
|
27
|
+
},
|
28
|
+
|
29
|
+
}
|
30
|
+
|
31
|
+
FUTURE_TRANSFERS.default = {}
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
BUDGETS = {
|
36
|
+
Monthly: {account: :FirstBank, period: [1, :month], monthday: 1, start: nil, end: nil, discretionary: false},
|
37
|
+
MonthlySecondBank: {account: :SecondBank, period: [1, :month], monthday: 1, start: nil, end: nil, discretionary: false},
|
38
|
+
Weekly: {account: :FirstBank, period: [7, :day], monthday: nil, start: nil, end: nil, discretionary: true},
|
39
|
+
WeeklySecondBank: {account: :SecondBank, period: [7, :day], monthday: nil, start: nil, end: nil, discretionary: true},
|
40
|
+
MyHoliday: {account: :SecondBank, period: [1, :day], monthday: nil, start: Date.parse("02/12/2013"), end: Date.parse("2/01/2014"), discretionary: false},
|
41
|
+
}
|
42
|
+
|
43
|
+
def in_date(item)
|
44
|
+
(!item[:start] or date >= item[:start]) and (!item[:end] || date <= item[:end])
|
45
|
+
end
|
46
|
+
|
47
|
+
def account_type(account)
|
48
|
+
case account
|
49
|
+
when :Food, :Phone, :Rent, :Cash, :Entertainment, :Books, :Insurance
|
50
|
+
:Expense
|
51
|
+
when :FirstBank, :SecondBank
|
52
|
+
:Asset
|
53
|
+
when :PersonalLoans
|
54
|
+
:Liability
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def red_line(account, date)
|
59
|
+
case account
|
60
|
+
when :FirstBank
|
61
|
+
300
|
62
|
+
when :SecondBank
|
63
|
+
500
|
64
|
+
else
|
65
|
+
0
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def external_account
|
71
|
+
case description
|
72
|
+
when /co-op|sainsbury/i
|
73
|
+
:Food
|
74
|
+
when /insurance/i
|
75
|
+
:Insurance
|
76
|
+
when /Vodafone/i
|
77
|
+
:Phone
|
78
|
+
when /Adams/i
|
79
|
+
:Rent
|
80
|
+
when /Carfax|Lnk/i
|
81
|
+
:Cash
|
82
|
+
when /andalus/i, /angels/i, /maggie arms/i, /barley mow/i
|
83
|
+
:Entertainment
|
84
|
+
when /blackwell/i
|
85
|
+
:Books
|
86
|
+
when /norries/i
|
87
|
+
:PersonalLoans
|
88
|
+
else
|
89
|
+
:Unknown
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def budget
|
94
|
+
case description
|
95
|
+
when /Vodafone/i
|
96
|
+
:Monthly
|
97
|
+
else
|
98
|
+
case external_account
|
99
|
+
when :Food, :Entertainment
|
100
|
+
:Weekly
|
101
|
+
when :Insurance, :Phone, :Rent
|
102
|
+
:Monthly
|
103
|
+
when :Books, :Cash
|
104
|
+
:WeeklySecondBank
|
105
|
+
else
|
106
|
+
:Unknown
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,570 @@
|
|
1
|
+
# Some thoughts on double entry accounting:
|
2
|
+
#
|
3
|
+
# assets - liabilities = equity
|
4
|
+
# where equity = equity_at_start + income - expenses
|
5
|
+
#
|
6
|
+
# so
|
7
|
+
#
|
8
|
+
# assets - liabilities = equity_at_start + income - expenses
|
9
|
+
#
|
10
|
+
# or alternatively
|
11
|
+
#
|
12
|
+
# assets + expenses = equity_at_start + income + liabilities (1)
|
13
|
+
#
|
14
|
+
# Good things:
|
15
|
+
# Positive equity_at_start, positive assets, positive income, negative liabilities, negative expenses
|
16
|
+
#
|
17
|
+
# A debit on the left of (1) must be matched by a credit on the right of (1) and
|
18
|
+
# vice versa.
|
19
|
+
#
|
20
|
+
#
|
21
|
+
# A debit to an asset account increases the value of the asset. This means buying some land
|
22
|
+
# or supplies or depositing some cash in a bank account. You can think of it as a debit because
|
23
|
+
# you are locking up your equity in a way that may not be realisable. A credit to the asset account
|
24
|
+
# means drawing down on the asset, for example selling a bit of land or taking money out of a
|
25
|
+
# bank account.
|
26
|
+
#
|
27
|
+
# Similarly, a debit to an expense account, effectively, spending money on that expense,
|
28
|
+
# increases the value of that account. Debits here are clearly negative things from
|
29
|
+
# the point of view of your wealth! (Credits to expense accounts would be something like
|
30
|
+
# travel reimbursements).
|
31
|
+
#
|
32
|
+
# A credit to income increases the value of the income account... this seems obvious. If
|
33
|
+
# you credit income you must debit assets (you have to put your income somewhere, for
|
34
|
+
# example a bank account, i.e. you must effectively spend it by buying an asset: remember
|
35
|
+
# a bank may fail... a bank account is an asset with risk just as much as a painting).
|
36
|
+
#
|
37
|
+
# A credit to liabilities increases the value of the liability, for example taking out a
|
38
|
+
# loan. Once you credit a liability you have to either buy (debit) an asset, or buy (debit)
|
39
|
+
# an expense directly (for example a loan to pay some fees).
|
40
|
+
#
|
41
|
+
# In any accounting period, the sum of all debits and credits should be 0. Also, at the end
|
42
|
+
# of the accounting period,
|
43
|
+
#
|
44
|
+
# equity_at_end = assets - liabilities = equity_at_start + income - expenses
|
45
|
+
#
|
46
|
+
# This seems obvious to me!!
|
47
|
+
class Treasurer
|
48
|
+
class Reporter
|
49
|
+
#include LocalCustomisations
|
50
|
+
attr_reader :today
|
51
|
+
attr_reader :in_limit_discretionary_budget_factor
|
52
|
+
attr_reader :stable_discretionary_budget_factor
|
53
|
+
attr_accessor :projected_budget_factor
|
54
|
+
attr_accessor :equity
|
55
|
+
def initialize(runner, options)
|
56
|
+
@runner = runner
|
57
|
+
@days_ahead = options[:days_ahead]||180
|
58
|
+
@days_before = options[:days_before]||360
|
59
|
+
@today = options[:today]||Date.today
|
60
|
+
@start_date = @today - @days_before
|
61
|
+
@runs = runner.component_run_list.values
|
62
|
+
@indateruns = @runs.find_all{|r| r.days_ago(@today) < @days_before}
|
63
|
+
p 'accounts256',@runs.size, @runs.map{|r| r.account}.uniq
|
64
|
+
|
65
|
+
end
|
66
|
+
def report
|
67
|
+
get_actual_budgets
|
68
|
+
get_projected_budgets
|
69
|
+
@accounts = @runs.map{|r| r.account}.uniq.map{|acc| Account.new(acc, self, @runner, @runs, @projected_budgets, false)} +
|
70
|
+
@runs.map{|r| r.external_account}.uniq.map{|acc| Account.new(acc, self, @runner, @runs, @projected_budgets, true)}
|
71
|
+
@accounts.unshift (@equity = Equity.new(self, @runner, @accounts))
|
72
|
+
get_in_limit_discretionary_budget_factor
|
73
|
+
get_stable_discretionary_budget_factor
|
74
|
+
report = ""
|
75
|
+
report << header
|
76
|
+
report << '\begin{multicols}{2}'
|
77
|
+
report << account_summaries
|
78
|
+
report << discretionary_budget_table
|
79
|
+
report << account_balance_graphs
|
80
|
+
report << expense_account_summary
|
81
|
+
report << budget_expenditure_graphs
|
82
|
+
report << '\end{multicols}'
|
83
|
+
report << budget_resolutions
|
84
|
+
report << budget_breakdown
|
85
|
+
report << transactions_by_account
|
86
|
+
report << footer
|
87
|
+
|
88
|
+
File.open('report.tex', 'w'){|f| f.puts report}
|
89
|
+
system "latex report.tex && latex report.tex"
|
90
|
+
end
|
91
|
+
class Account
|
92
|
+
attr_reader :name, :external, :runs
|
93
|
+
def initialize(name, reporter, runner, runs, projected_budgets, external)
|
94
|
+
@name = name
|
95
|
+
@reporter = reporter
|
96
|
+
@runner = runner
|
97
|
+
@projected_budgets =Hash[projected_budgets.find_all{|k,v| v[:account] == name}]
|
98
|
+
@external = external
|
99
|
+
@runs = runs.find_all{|r| (@external ? r.external_account : r.account) == name}
|
100
|
+
end
|
101
|
+
def type
|
102
|
+
account_type(name)
|
103
|
+
end
|
104
|
+
def red_line(date)
|
105
|
+
if Treasurer::LocalCustomisations.instance_methods.include? :red_line
|
106
|
+
super(name, date)
|
107
|
+
else
|
108
|
+
0.0
|
109
|
+
end
|
110
|
+
end
|
111
|
+
def balance(date = @reporter.today)
|
112
|
+
#if !date
|
113
|
+
#@runs.sort_by{|r| r.date}[-1].balance
|
114
|
+
if @external
|
115
|
+
@runs.find_all{|r| r.date < date}.map{|r| (r.deposit - r.withdrawal) * (@external ? -1 : 1)}.sum || 0.0
|
116
|
+
else
|
117
|
+
@runs.sort_by{|r| (r.date.to_datetime.to_time.to_i - date.to_datetime.to_time.to_i).to_f.abs}[0].balance
|
118
|
+
end
|
119
|
+
end
|
120
|
+
def expenditure(today, days_before, &block)
|
121
|
+
@runs.find_all{|r| r.days_ago(today) < days_before and (!block or yield(r)) }.map{|r| @external ? r.withdrawal : r.deposit }.sum || 0
|
122
|
+
end
|
123
|
+
def income(today, days_before)
|
124
|
+
@runs.find_all{|r| r.days_ago(today) < days_before }.map{|r| @external ? r.deposit : r.withdrawal }.sum || 0
|
125
|
+
end
|
126
|
+
def summary_table(today, days_before)
|
127
|
+
|
128
|
+
<<EOF
|
129
|
+
\\subsubsection{#{name}}
|
130
|
+
\\begin{tabulary}{0.8\\textwidth}{ r | l}
|
131
|
+
Balance & #{balance} \\\\
|
132
|
+
Deposited & #{expenditure(today, days_before)} \\\\
|
133
|
+
Withdrawn & #{income(today, days_before)} \\\\
|
134
|
+
\\end{tabulary}
|
135
|
+
EOF
|
136
|
+
end
|
137
|
+
def money_in_sign
|
138
|
+
case type
|
139
|
+
when :Liability, :Income
|
140
|
+
-1.0
|
141
|
+
else
|
142
|
+
1.0
|
143
|
+
end
|
144
|
+
end
|
145
|
+
def projected_balance(date)
|
146
|
+
non_discretionary_projected_balance(date) -
|
147
|
+
@reporter.sum_regular(@projected_budgets, date)
|
148
|
+
end
|
149
|
+
def cache
|
150
|
+
@cache ||={}
|
151
|
+
end
|
152
|
+
def non_discretionary_projected_balance(date)
|
153
|
+
#ep ['FUTURE_INCOME', FUTURE_INCOME, name] if FUTURE_INCOME.size > 0
|
154
|
+
cache[[:non_discretionary_projected_balance, date]] ||=
|
155
|
+
balance +
|
156
|
+
#@reporter.sum_regular(REGULAR_EXPENDITURE[name], date) +
|
157
|
+
#@reporter.sum_regular(REGULAR_INCOME[name], date) -
|
158
|
+
#@reporter.sum_future(FUTURE_EXPENDITURE[name], date) +
|
159
|
+
#@reporter.sum_future(FUTURE_INCOME[name], date) +
|
160
|
+
(FUTURE_TRANSFERS.keys.find_all{|from,to| to == name}.map{|key|
|
161
|
+
@reporter.sum_future(FUTURE_TRANSFERS[key], date) * money_in_sign
|
162
|
+
}.sum||0) -
|
163
|
+
(FUTURE_TRANSFERS.keys.find_all{|from,to| from == name}.map{|key|
|
164
|
+
@reporter.sum_future( FUTURE_TRANSFERS[key], date) * money_in_sign
|
165
|
+
}.sum||0) +
|
166
|
+
(REGULAR_TRANSFERS.keys.find_all{|from,to| to == name}.map{|key|
|
167
|
+
@reporter.sum_regular(REGULAR_TRANSFERS[key], date) * money_in_sign
|
168
|
+
}.sum||0) -
|
169
|
+
(REGULAR_TRANSFERS.keys.find_all{|from,to| from == name}.map{|key|
|
170
|
+
@reporter.sum_regular( REGULAR_TRANSFERS[key], date) * money_in_sign
|
171
|
+
}.sum||0)
|
172
|
+
end
|
173
|
+
# Write an eps graph to disk of past and projected
|
174
|
+
# balance of the account
|
175
|
+
def write_balance_graph(today, days_before, days_ahead)
|
176
|
+
#accshort = name.gsub(/\s/, '')
|
177
|
+
if not (@external or type == :Equity)
|
178
|
+
kit = @runner.graphkit(['date.to_time.to_i', 'balance'], {conditions: "account == #{name.inspect} and days_ago(Date.parse(#{today.to_s.inspect})) < #{days_before} and days_ago(Date.parse(#{today.to_s.inspect})) > -1", sort: '[date, id]'})
|
179
|
+
else
|
180
|
+
pastdates = (today-days_before..today).to_a
|
181
|
+
balances = pastdates.map{|date| balance(date)}
|
182
|
+
kit = GraphKit.quick_create([pastdates.map{|d| d.to_time.to_i}, balances])
|
183
|
+
end
|
184
|
+
futuredates = (today..today+days_ahead).to_a
|
185
|
+
projection = futuredates.map{|date| projected_balance(date) }
|
186
|
+
kit2 = GraphKit.quick_create([futuredates.map{|d| d.to_time.to_i}, projection])
|
187
|
+
red = futuredates.map{|date| red_line(date)}
|
188
|
+
kit3 = GraphKit.quick_create([futuredates.map{|d| d.to_time.to_i}, red])
|
189
|
+
@reporter.projected_budget_factor = @reporter.in_limit_discretionary_budget_factor
|
190
|
+
limit = futuredates.map{|date| projected_balance(date)}
|
191
|
+
kit4 = GraphKit.quick_create([futuredates.map{|d| d.to_time.to_i}, limit])
|
192
|
+
@reporter.projected_budget_factor = @reporter.stable_discretionary_budget_factor
|
193
|
+
#ep ['projected_budget_factor!!!!', @reporter.projected_budget_factor]
|
194
|
+
stable = futuredates.map{|date| projected_balance(date)}
|
195
|
+
kit5 = GraphKit.quick_create([futuredates.map{|d| d.to_time.to_i}, stable])
|
196
|
+
#exit
|
197
|
+
@projected_budget_factor = nil
|
198
|
+
kit += (kit2 + kit4 + kit5)
|
199
|
+
kit = kit3 + kit
|
200
|
+
kit.title = "Balance for #{name}"
|
201
|
+
kit.xlabel = %['Date' offset 0,-2]
|
202
|
+
kit.xlabel = nil
|
203
|
+
kit.ylabel = "Balance"
|
204
|
+
|
205
|
+
kit.data[0].gp.title = 'Limit'
|
206
|
+
kit.data[1].gp.title = 'Previous'
|
207
|
+
kit.data[2].gp.title = '0 GBP Discretionary'
|
208
|
+
kit.data[2].gp.title = 'Projection'
|
209
|
+
kit.data[3].gp.title = 'Limit'
|
210
|
+
kit.data[4].gp.title = 'Stable'
|
211
|
+
kit.data.each{|dk| dk.gp.with = "lp"}
|
212
|
+
kit.gp.key = ' bottom left '
|
213
|
+
|
214
|
+
CodeRunner::Budget.kit_time_format_x(kit)
|
215
|
+
|
216
|
+
(kit).gnuplot_write("#{name}_balance.eps", size: "4.0in,3.0in")
|
217
|
+
end
|
218
|
+
# A string to include the balance graph in the document
|
219
|
+
def balance_graph_string
|
220
|
+
#accshort = name.gsub(/\s/, '')
|
221
|
+
"\\begin{center}\\includegraphics[width=3.0in]{#{name}_balance.eps}\\end{center}"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
class Equity < Account
|
225
|
+
def initialize(reporter, runner, accounts)
|
226
|
+
@reporter = reporter
|
227
|
+
@runner = runner
|
228
|
+
@accounts = accounts #.find_all{|acc| not acc.external}
|
229
|
+
end
|
230
|
+
def type
|
231
|
+
:Equity
|
232
|
+
end
|
233
|
+
def name
|
234
|
+
:Equity
|
235
|
+
end
|
236
|
+
def red_line(date)
|
237
|
+
@accounts.map{|acc|
|
238
|
+
case acc.type
|
239
|
+
when :Asset
|
240
|
+
acc.red_line(date)
|
241
|
+
when :Liability
|
242
|
+
-acc.red_line(date)
|
243
|
+
else
|
244
|
+
0.0
|
245
|
+
end
|
246
|
+
}.sum
|
247
|
+
end
|
248
|
+
def balance(date=@reporter.today)
|
249
|
+
@accounts.map{|acc|
|
250
|
+
case acc.type
|
251
|
+
when :Asset
|
252
|
+
acc.balance(date)
|
253
|
+
when :Liability
|
254
|
+
-acc.balance(date)
|
255
|
+
else
|
256
|
+
0.0
|
257
|
+
end
|
258
|
+
}.sum
|
259
|
+
end
|
260
|
+
def projected_balance(date=@reporter.today)
|
261
|
+
@accounts.map{|acc|
|
262
|
+
case acc.type
|
263
|
+
when :Asset
|
264
|
+
acc.projected_balance(date)
|
265
|
+
when :Liability
|
266
|
+
-acc.projected_balance(date)
|
267
|
+
else
|
268
|
+
0.0
|
269
|
+
end
|
270
|
+
}.sum
|
271
|
+
end
|
272
|
+
def summary_table(today, days_before)
|
273
|
+
|
274
|
+
<<EOF
|
275
|
+
\\subsubsection{#{name}}
|
276
|
+
\\begin{tabulary}{0.8\\textwidth}{ r | l}
|
277
|
+
Balance & #{balance} \\\\
|
278
|
+
\\end{tabulary}
|
279
|
+
EOF
|
280
|
+
end
|
281
|
+
end
|
282
|
+
def account_summaries
|
283
|
+
#ep 'accounts', @accounts.map{|a| a.name}
|
284
|
+
|
285
|
+
<<EOF
|
286
|
+
\\section{Summary of Accounts}
|
287
|
+
\\subsection{Equity}
|
288
|
+
#{@accounts.find{|acc| acc.type == :Equity }.summary_table(@today, @days_before)}
|
289
|
+
\\subsection{Assets}
|
290
|
+
#{@accounts.find_all{|acc| account_type(acc.name) == :Asset }.map{|acc| acc.summary_table(@today, @days_before)}.join("\n\n") }
|
291
|
+
\\subsection{Liabilities}
|
292
|
+
#{@accounts.find_all{|acc| account_type(acc.name) == :Liability }.map{|acc| acc.summary_table(@today, @days_before)}.join("\n\n") }
|
293
|
+
\\subsection{Income}
|
294
|
+
#{@accounts.find_all{|acc| account_type(acc.name) == :Income }.map{|acc| acc.summary_table(@today, @days_before)}.join("\n\n") }
|
295
|
+
\\subsection{Expenses}
|
296
|
+
#{@accounts.find_all{|acc| account_type(acc.name) == :Expense }.map{|acc| acc.summary_table(@today, @days_before)}.join("\n\n") }
|
297
|
+
EOF
|
298
|
+
end
|
299
|
+
def account_balance_graphs
|
300
|
+
<<EOF
|
301
|
+
\\section{Graphs of Recent Balances}
|
302
|
+
#{@accounts.find_all{|acc| account_type(acc.name) != :Expense}.map{|acc|
|
303
|
+
acc.write_balance_graph(@today, @days_before, @days_ahead)
|
304
|
+
acc.balance_graph_string
|
305
|
+
}.join("\n\n")
|
306
|
+
}
|
307
|
+
EOF
|
308
|
+
end
|
309
|
+
def expense_account_summary
|
310
|
+
<<EOF
|
311
|
+
\\section{Expense Account Summary}
|
312
|
+
\\subsection{Budget Period}
|
313
|
+
#{expense_pie_chart('budgetperiod'){|r| r.days_ago(@today) < @days_before}}
|
314
|
+
\\subsection{Last Week}
|
315
|
+
#{expense_pie_chart('lastweekexpenses'){|r| p ['r.daysago', r.days_ago(@today)]; r.days_ago(@today) < 7}}
|
316
|
+
\\subsection{Last Month}
|
317
|
+
#{expense_pie_chart('lastmonthexpenses'){|r| r.days_ago(@today) < 30}}
|
318
|
+
\\subsection{Last Year}
|
319
|
+
#{expense_pie_chart('lastyearexpenses'){|r| r.days_ago(@today) < 365}}
|
320
|
+
\\section{Expense Accounts by Budget}
|
321
|
+
#{@actual_budgets.map{|budget, budget_info|
|
322
|
+
"\\subsection{#{budget}}
|
323
|
+
#{expense_pie_chart(budget + 'expenses'){|r|r.days_ago(@today) < @days_before and r.budget == budget}}"
|
324
|
+
}.join("\n\n")}
|
325
|
+
|
326
|
+
EOF
|
327
|
+
end
|
328
|
+
def expense_pie_chart(name, &block)
|
329
|
+
expaccs = @accounts.find_all{|acc| account_type(acc.name) == :Expense}
|
330
|
+
labels = expaccs.map{|acc| acc.name}
|
331
|
+
exps = expaccs.map{|acc| acc.expenditure(@today, 50000, &block)}
|
332
|
+
kit = GraphKit.quick_create([exps])
|
333
|
+
kit.data[0].gp.with = 'boxes'
|
334
|
+
kit.gp.style = "fill solid"
|
335
|
+
#pp ['kit222', kit, labels]
|
336
|
+
i = -1
|
337
|
+
kit.gp.xtics = "(#{labels.map{|l| %["#{l}" #{i+=1}]}.join(', ')}) rotate by 315"
|
338
|
+
kit.gnuplot_write("#{name}.eps")
|
339
|
+
|
340
|
+
"\\begin{center}\\includegraphics[width=3.0in]{#{name}.eps}\\vspace{1em}\\end{center}"
|
341
|
+
end
|
342
|
+
def get_in_limit_discretionary_budget_factor
|
343
|
+
@projected_budget_factor = 1.0
|
344
|
+
loop do
|
345
|
+
ok = true
|
346
|
+
date = @today
|
347
|
+
while date < @today + @days_ahead
|
348
|
+
ok = false if @equity.projected_balance(date) < @equity.red_line(date)
|
349
|
+
date += 1
|
350
|
+
#ep ['projected_budget_factor', date, @equity.projected_balance(date), @equity.red_line(date), ok]
|
351
|
+
end
|
352
|
+
@in_limit_discretionary_budget_factor = @projected_budget_factor
|
353
|
+
break if (@projected_budget_factor == 0.0 or ok == true)
|
354
|
+
@projected_budget_factor -= 0.01
|
355
|
+
@projected_budget_factor -= 0.1
|
356
|
+
end
|
357
|
+
@projected_budget_factor = nil
|
358
|
+
#exit
|
359
|
+
end
|
360
|
+
def get_stable_discretionary_budget_factor
|
361
|
+
@projected_budget_factor = 1.0
|
362
|
+
loop do
|
363
|
+
ok = true
|
364
|
+
date = @today
|
365
|
+
balances = []
|
366
|
+
while date < @today + @days_ahead
|
367
|
+
#ok = false if @equity.projected_balance(date) < @equity.red_line(date)
|
368
|
+
date += 1
|
369
|
+
balances.push @equity.projected_balance(date)
|
370
|
+
#ep ['projected_budget_factor', date, @equity.projected_balance(date), @equity.red_line(date), ok]
|
371
|
+
end
|
372
|
+
ok = false if balances.mean < @equity.balance(@today)
|
373
|
+
@stable_discretionary_budget_factor = @projected_budget_factor
|
374
|
+
break if (@projected_budget_factor == 0.0 or ok == true)
|
375
|
+
@projected_budget_factor -= 0.01
|
376
|
+
@projected_budget_factor -= 0.1
|
377
|
+
end
|
378
|
+
@projected_budget_factor = nil
|
379
|
+
#exit
|
380
|
+
end
|
381
|
+
def discretionary_budget_table
|
382
|
+
discretionary_budgets = budgets_with_averages(@projected_budgets)
|
383
|
+
|
384
|
+
<<EOF
|
385
|
+
\\section{Discretionary Budget Summary}
|
386
|
+
\\begin{tabulary}{0.5\\textwidth}{ R | c c c c }
|
387
|
+
Budget & Average & Projection & Limit & Stable \\\\
|
388
|
+
#{discretionary_budgets.map{|budget, info|
|
389
|
+
#ep info
|
390
|
+
"#{budget} & #{info[:average]} & #{info[:projection]} & #{
|
391
|
+
(info[:projection] * @in_limit_discretionary_budget_factor).round(2)} &
|
392
|
+
#{(info[:projection] * @stable_discretionary_budget_factor).round(2)} \\\\"
|
393
|
+
}.join("\n\n")
|
394
|
+
}
|
395
|
+
\\end{tabulary}
|
396
|
+
EOF
|
397
|
+
end
|
398
|
+
def budget_expenditure_graphs
|
399
|
+
<<EOF
|
400
|
+
\\section{Budget Expenditure}
|
401
|
+
#{budget_and_transfer_graphs(@actual_budgets, {})}
|
402
|
+
EOF
|
403
|
+
end
|
404
|
+
def budget_and_transfer_graphs(budgets, options)
|
405
|
+
"#{budgets.map{|budget, budget_info|
|
406
|
+
dates, expenditures, items = budget_expenditure(budget, budget_info)
|
407
|
+
#ep ['budget', budget, dates, expenditures]
|
408
|
+
kit = GraphKit.quick_create([dates.map{|d| d.to_time.to_i}, expenditures])
|
409
|
+
kit.data.each{|dk| dk.gp.with="boxes"}
|
410
|
+
kit.gp.style = "fill solid"
|
411
|
+
kit.xlabel = nil
|
412
|
+
kit.ylabel = "Expenditure"
|
413
|
+
unless options[:transfers]
|
414
|
+
kits = budgets_with_averages({budget => budget_info}).map{|budget, budget_info|
|
415
|
+
#ep 'Budget is ', budget
|
416
|
+
kit2 = GraphKit.quick_create([
|
417
|
+
[dates[0], dates[-1]].map{|d| d.to_time.to_i},
|
418
|
+
[budget_info[:average], budget_info[:average]]
|
419
|
+
])
|
420
|
+
kit2.data[0].gp.with = 'lp lw 4'
|
421
|
+
kit2
|
422
|
+
}
|
423
|
+
#$debug_gnuplot = true
|
424
|
+
#kits.sum.gnuplot
|
425
|
+
kit += kits.sum
|
426
|
+
|
427
|
+
else
|
428
|
+
kit.data[0].y.data.map!{|expen| expen*-1.0}
|
429
|
+
end
|
430
|
+
kit.title = "#{budget} Expenditure with average (Total = #{kit.data[0].y.data.sum})"
|
431
|
+
CodeRunner::Budget.kit_time_format_x(kit)
|
432
|
+
#kit.gnuplot
|
433
|
+
#ep ['kit1122', budget, kit]
|
434
|
+
kit.gnuplot_write("#{budget}.eps")
|
435
|
+
"\\begin{center}\\includegraphics[width=3.0in]{#{budget}.eps}\\vspace{1em}\\end{center}"
|
436
|
+
}.join("\n\n")
|
437
|
+
}"
|
438
|
+
end
|
439
|
+
|
440
|
+
def budget_resolutions
|
441
|
+
<<EOF
|
442
|
+
\\section{Budget Resolutions}
|
443
|
+
|
444
|
+
This section sums items from budgets drawn from an alternate account, i.e. it determines how much should be transferred from one account to another as a result of expenditure from a given budget.
|
445
|
+
|
446
|
+
#{@actual_budgets.map{|budget, budget_info|
|
447
|
+
|
448
|
+
"\\subsection{#{budget} }
|
449
|
+
\\setlength{\\parindent}{0cm}\n\n\\begin{tabulary}{0.99\\textwidth}{r l}
|
450
|
+
%\\hline
|
451
|
+
Account Owed & Amount \\\\
|
452
|
+
\\hline
|
453
|
+
\\Tstrut
|
454
|
+
#{budget_items = @indateruns.find_all{|r| r.budget == budget}
|
455
|
+
alternate_accounts = budget_items.map{|r| r.account}.uniq - [budget_info[:account]]
|
456
|
+
alternate_accounts.map{|acc|
|
457
|
+
alternate_items = budget_items.find_all{|r| r.account == acc}
|
458
|
+
total = alternate_items.map{|r| r.withdrawal - r.deposit}.sum
|
459
|
+
"#{acc} & #{total} \\\\"
|
460
|
+
}.join("\n\n")
|
461
|
+
}
|
462
|
+
|
463
|
+
\\\\
|
464
|
+
\\hline
|
465
|
+
\\end{tabulary}
|
466
|
+
\\normalsize
|
467
|
+
\\vspace{1em}\n\n
|
468
|
+
|
469
|
+
#{ alternate_accounts.map{|acc|
|
470
|
+
alternate_items = budget_items.find_all{|r| r.account == acc}
|
471
|
+
alternate_items.pieces((alternate_items.size.to_f/50.to_f).ceil).map{|piece|
|
472
|
+
"\\footnotesize\\setlength{\\parindent}{0cm}\n\n\\begin{tabulary}{0.99\\textwidth}{ #{"c " * 4 + " L " + " r " * 3 }}
|
473
|
+
#{budget}: & #{budget_info[:account]} & owes & #{acc} &&&&\\\\
|
474
|
+
\\hline
|
475
|
+
|
476
|
+
\\Tstrut
|
477
|
+
|
478
|
+
#{piece.map{|r|
|
479
|
+
([:id] + CodeRunner::Budget.rcp.component_results - [:sc]).map{|res|
|
480
|
+
r.send(res).to_s.latex_escape
|
481
|
+
#rcp.component_results.map{|res| r.send(res).to_s.gsub(/(.{20})/, '\1\\\\\\\\').latex_escape
|
482
|
+
}.join(" & ")
|
483
|
+
}.join("\\\\\n")
|
484
|
+
}
|
485
|
+
\\end{tabulary}\\normalsize"}.join("\n\n")
|
486
|
+
}.join("\n\n")}
|
487
|
+
"
|
488
|
+
}.join("\n\n")
|
489
|
+
}
|
490
|
+
EOF
|
491
|
+
end
|
492
|
+
def budget_breakdown
|
493
|
+
<<EOF
|
494
|
+
\\section{Budget and Transfer Breakdown}
|
495
|
+
#{(@actual_budgets).map{|budget, budget_info|
|
496
|
+
dates, expenditures, budget_items = budget_expenditure(budget, budget_info)
|
497
|
+
#pp budget, budget_items.map{|items| items.map{|i| i.date.to_s}}
|
498
|
+
"\\subsection{#{budget}}" +
|
499
|
+
budget_items.zip(dates, expenditures).map{|items, date, expenditure|
|
500
|
+
if items.size > 0
|
501
|
+
"
|
502
|
+
\\footnotesize
|
503
|
+
\\setlength{\\parindent}{0cm}\n\n\\begin{tabulary}{0.99\\textwidth}{ #{"c " * 4 + " L " + " r " * 2 }}
|
504
|
+
%\\hline
|
505
|
+
& #{date.to_s.latex_escape} & & & Total & #{expenditure} & \\\\
|
506
|
+
\\hline
|
507
|
+
\\Tstrut
|
508
|
+
#{items.map{|r|
|
509
|
+
([:id] + CodeRunner::Budget.rcp.component_results - [:sc, :balance]).map{|res|
|
510
|
+
r.send(res).to_s.latex_escape
|
511
|
+
}.join(" & ")
|
512
|
+
}.join("\\\\\n")
|
513
|
+
}
|
514
|
+
\\\\
|
515
|
+
\\hline
|
516
|
+
\\end{tabulary}
|
517
|
+
\\normalsize
|
518
|
+
\\vspace{1em}\n\n"
|
519
|
+
else
|
520
|
+
""
|
521
|
+
end
|
522
|
+
}.join("\n\n")
|
523
|
+
}.join("\n\n")
|
524
|
+
}
|
525
|
+
EOF
|
526
|
+
end
|
527
|
+
|
528
|
+
def transactions_by_account
|
529
|
+
<<EOF
|
530
|
+
\\section{Recent Transactions}
|
531
|
+
#{@accounts.find_all{|acc| not acc.type == :Equity}.sort_by{|acc| acc.external ? 0 : 1}.map{|acc|
|
532
|
+
"\\subsection{#{acc.name}}
|
533
|
+
\\footnotesize
|
534
|
+
#{all = acc.runs.find_all{|r| r.days_ago(@today) < @days_before}.sort_by{|r| [r.date, r.id]}.reverse
|
535
|
+
#ep ['acc', acc, 'ids', all.map{|r| r.id}, 'size', all.size]
|
536
|
+
all.pieces((all.size.to_f/50.to_f).ceil).map{|piece|
|
537
|
+
"\\setlength{\\parindent}{0cm}\n\n\\begin{tabulary}{0.99\\textwidth}{ #{"c " * 4 + " L " + " r " * 3 + "l"}}
|
538
|
+
#{piece.map{|r|
|
539
|
+
([:id] + CodeRunner::Budget.rcp.component_results - [:sc] + [:budget]).map{|res| r.send(res).to_s.latex_escape
|
540
|
+
#rcp.component_results.map{|res| r.send(res).to_s.gsub(/(.{20})/, '\1\\\\\\\\').latex_escape
|
541
|
+
}.join(" & ")
|
542
|
+
}.join("\\\\\n")}
|
543
|
+
\\end{tabulary}"}.join("\n\n")}"
|
544
|
+
}.join("\n\n")}
|
545
|
+
EOF
|
546
|
+
end
|
547
|
+
|
548
|
+
def header
|
549
|
+
<<EOF
|
550
|
+
\\documentclass{article}
|
551
|
+
\\usepackage[cm]{fullpage}
|
552
|
+
\\usepackage{tabulary}
|
553
|
+
\\usepackage{graphicx}
|
554
|
+
\\usepackage{multicol}
|
555
|
+
%\\usepackage{hyperlink}
|
556
|
+
\\newcommand\\Tstrut{\\rule{0pt}{2.8ex}}
|
557
|
+
\\begin{document}
|
558
|
+
\\title{#{@days_before}-day Budget Report}
|
559
|
+
\\maketitle
|
560
|
+
\\tableofcontents
|
561
|
+
EOF
|
562
|
+
end
|
563
|
+
def footer
|
564
|
+
<<EOF
|
565
|
+
\\end{document}
|
566
|
+
EOF
|
567
|
+
end
|
568
|
+
|
569
|
+
end
|
570
|
+
end
|
data/lib/treasurer.rb
CHANGED
@@ -0,0 +1,113 @@
|
|
1
|
+
|
2
|
+
# A tool for analysing finances and making budget projections
|
3
|
+
|
4
|
+
require 'getoptlong'
|
5
|
+
|
6
|
+
module CommandLineFlunky
|
7
|
+
|
8
|
+
STARTUP_MESSAGE = "\n------Treasurer Financial Utility (c) Edmund Highcock------"
|
9
|
+
|
10
|
+
MANUAL_HEADER = <<EOF
|
11
|
+
|
12
|
+
-------------Treasurer Financial Utility Manual---------------
|
13
|
+
|
14
|
+
Written by Edmund Highcock (2014)
|
15
|
+
|
16
|
+
NAME
|
17
|
+
|
18
|
+
treasurer
|
19
|
+
|
20
|
+
|
21
|
+
SYNOPSIS
|
22
|
+
|
23
|
+
treasurer <command> [arguments] [options]
|
24
|
+
|
25
|
+
|
26
|
+
DESCRIPTION
|
27
|
+
|
28
|
+
Generate a financial report from one or more bank accounts, credit cards
|
29
|
+
etc by analysing internet banking sheets. Simple local files can be used to
|
30
|
+
customise the reports by adding budgets and categories
|
31
|
+
|
32
|
+
EXAMPLES
|
33
|
+
|
34
|
+
treasurer init my_accounts_folder
|
35
|
+
|
36
|
+
treasurer add my_bank_statement.csv
|
37
|
+
|
38
|
+
treasurer report
|
39
|
+
|
40
|
+
treasurer add_folder folder_of_bank_statements
|
41
|
+
|
42
|
+
|
43
|
+
EOF
|
44
|
+
|
45
|
+
COMMANDS_WITH_HELP = [
|
46
|
+
['add_file', 'add', 2, 'Import a new internet banking spreadsheet for the given account.', ['csv spreadsheet filename', 'account name'], []],
|
47
|
+
['add_folder_of_files', 'addf', 1, 'Import all internet banking spreadsheets within the given folder .', ['folder'], []],
|
48
|
+
['init_root_folder', 'init', 1, 'Create a new folder and initialise it for storing treasurer data.', ['folder'], []],
|
49
|
+
['create_report', 'report', 0, 'Generate a detailed report (typeset using latex) showing account activity, spending by category, and projections.', [], [:a, :b, :t]],
|
50
|
+
|
51
|
+
]
|
52
|
+
|
53
|
+
|
54
|
+
|
55
|
+
COMMAND_LINE_FLAGS_WITH_HELP = [
|
56
|
+
['--after', '-a', GetoptLong::REQUIRED_ARGUMENT, 'Calculate projections up till given number of days after today'],
|
57
|
+
['--before', '-b', GetoptLong::REQUIRED_ARGUMENT, 'Start budget from given number of days before today'],
|
58
|
+
['--today', '-t', GetoptLong::REQUIRED_ARGUMENT, "Specify today's date, i.e. change the date on which the report is generated."],
|
59
|
+
#['--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}. "],
|
60
|
+
|
61
|
+
]
|
62
|
+
|
63
|
+
LONG_COMMAND_LINE_OPTIONS = [
|
64
|
+
#["--no-short-form", "", GetoptLong::NO_ARGUMENT, %[This boolean option has no short form]],
|
65
|
+
]
|
66
|
+
|
67
|
+
# specifying flag sets a bool to be true
|
68
|
+
|
69
|
+
CLF_BOOLS = []
|
70
|
+
|
71
|
+
CLF_INVERSE_BOOLS = [] # specifying flag sets a bool to be false
|
72
|
+
|
73
|
+
PROJECT_NAME = 'treasurer'
|
74
|
+
|
75
|
+
def self.method_missing(method, *args)
|
76
|
+
# p method, args
|
77
|
+
Treasurer.send(method, *args)
|
78
|
+
end
|
79
|
+
|
80
|
+
#def self.setup(copts)
|
81
|
+
#CommandLineFlunkyTestUtility.setup(copts)
|
82
|
+
#end
|
83
|
+
|
84
|
+
SCRIPT_FILE = __FILE__
|
85
|
+
end
|
86
|
+
|
87
|
+
class Treasurer
|
88
|
+
class << self
|
89
|
+
# This function gets called before every command
|
90
|
+
# and allows arbitrary manipulation of the command
|
91
|
+
# options (copts) hash
|
92
|
+
def setup(copts)
|
93
|
+
# None neededed
|
94
|
+
end
|
95
|
+
def verbosity
|
96
|
+
2
|
97
|
+
end
|
98
|
+
end
|
99
|
+
SCRIPT_FOLDER = folder = File.dirname(File.expand_path(__FILE__))
|
100
|
+
end
|
101
|
+
|
102
|
+
$has_put_startup_message_for_code_runner = true
|
103
|
+
require 'coderunner'
|
104
|
+
require 'treasurer/commands.rb'
|
105
|
+
require 'treasurer/report.rb'
|
106
|
+
require 'treasurer/analysis.rb'
|
107
|
+
|
108
|
+
|
109
|
+
######################################
|
110
|
+
# This must be at the end of the file
|
111
|
+
#
|
112
|
+
require 'command-line-flunky'
|
113
|
+
###############################
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Date,Type,Sort Code,Account Number,Description,In,Out,Balance,
|
2
|
+
03/09/2010,CPT,000000,99999999,LTSB CARFAX OXFORDCD 03SEP10,,20.00,156.33
|
3
|
+
03/09/2010,DD,000000,99999999,VODAFONE LIMITED,,28.61,176.33
|
4
|
+
03/09/2010,DEB,000000,99999999,ENDSLEIGH INSURANCE,,24.63,204.94
|
5
|
+
02/09/2010,DEB,000000,99999999,MIDCOUNTIES CO-OP,,32.65,229.57
|
6
|
+
01/09/2010,FPO,000000,99999999,MISS C ADAMS,,150.00,262.22
|
7
|
+
01/09/2010,DD,000000,99999999,PAYPAL PAYMENT,,5.97,412.22
|
8
|
+
01/09/2010,DEB,000000,99999999,S390-BLACKWELLCD ,,15.00,418.19
|
9
|
+
25/08/2010,DEB,000000,99999999,MIDCOUNTIES CO-OP,,52.65,229.57
|
10
|
+
15/08/2010,DEB,000000,99999999,MIDCOUNTIES CO-OP,,12.65,229.57
|
11
|
+
07/08/2010,DEB,000000,99999999,MIDCOUNTIES CO-OP,,32.65,229.57
|
12
|
+
01/08/2010,DEB,000000,99999999,MISS C ADAMS,,150.00,418.19
|
@@ -0,0 +1,9 @@
|
|
1
|
+
Date,Type,Sort Code,Account Number,Description,In,Out,Balance,
|
2
|
+
05/09/2010,CPT,222222,88888888,LNK WINDERMERE,,30.00,860.75
|
3
|
+
05/09/2010,FPO,222222,88888888,M H NORRIES,560.00,,890.75
|
4
|
+
05/09/2010,DEB,222222,88888888,SAINSBURY'S SMKT,,6.05,1450.75
|
5
|
+
05/09/2010,DEB,222222,88888888,AL-ANDALUS,,50.00,1456.80
|
6
|
+
05/09/2010,DEB,222222,88888888,ANGELS,,11.50,1506.80
|
7
|
+
01/09/2010,DEB,222222,88888888,THE MAGGIE ARMS,,48.00,1518.30
|
8
|
+
01/09/2010,DEB,222222,88888888,BARLEY MOW,,6.60,1566.30
|
9
|
+
30/08/2010,DD,222222,88888888,PAYPAL PAYMENT,,11.91,1572.90
|
data/test/test_treasurer.rb
CHANGED
@@ -1,7 +1,20 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
class TestTreasurer < Test::Unit::TestCase
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
def testfolder
|
5
|
+
'test/myaccount'
|
6
|
+
end
|
7
|
+
def test_setup
|
8
|
+
FileUtils.rm_r(testfolder) if FileTest.exist? testfolder
|
9
|
+
Treasurer.init_root_folder('test/myaccount', {})
|
10
|
+
Dir.chdir('test/myaccount') do
|
11
|
+
Treasurer.add_file('../bankaccountstatement.csv', 'FirstBank', {})
|
12
|
+
Treasurer.status
|
13
|
+
Treasurer.add_file('../otheraccountstatement.csv', 'SecondBank', {})
|
14
|
+
Treasurer.add_folder('../multiple')
|
15
|
+
Treasurer.status h: :component
|
16
|
+
Treasurer.report t: Date.parse('2010-09-07'), b: 40, a: 35
|
17
|
+
end
|
18
|
+
FileUtils.rm_r(testfolder) if FileTest.exist? testfolder
|
19
|
+
end
|
7
20
|
end
|
data/treasurer.gemspec
CHANGED
@@ -2,18 +2,19 @@
|
|
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
|
+
# stub: treasurer 0.1.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "treasurer"
|
9
|
-
s.version = "0.
|
9
|
+
s.version = "0.1.0"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
13
13
|
s.authors = ["Edmund Highcock"]
|
14
|
-
s.date = "2014-05-
|
14
|
+
s.date = "2014-05-30"
|
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."
|
16
16
|
s.email = "edmundhighcock@users.sourceforge.net"
|
17
|
+
s.executables = ["treasurer"]
|
17
18
|
s.extra_rdoc_files = [
|
18
19
|
"LICENSE.txt",
|
19
20
|
"README.rdoc"
|
@@ -25,8 +26,17 @@ Gem::Specification.new do |s|
|
|
25
26
|
"README.rdoc",
|
26
27
|
"Rakefile",
|
27
28
|
"VERSION",
|
29
|
+
"bin/treasurer",
|
28
30
|
"lib/treasurer.rb",
|
31
|
+
"lib/treasurer/analysis.rb",
|
32
|
+
"lib/treasurer/commands.rb",
|
33
|
+
"lib/treasurer/local_customisations.rb",
|
34
|
+
"lib/treasurer/report.rb",
|
35
|
+
"test/bankaccountstatement.csv",
|
29
36
|
"test/helper.rb",
|
37
|
+
"test/multiple/FirstBank.csv",
|
38
|
+
"test/multiple/SecondBank.csv",
|
39
|
+
"test/otheraccountstatement.csv",
|
30
40
|
"test/test_treasurer.rb",
|
31
41
|
"treasurer.gemspec"
|
32
42
|
]
|
@@ -39,12 +49,18 @@ Gem::Specification.new do |s|
|
|
39
49
|
s.specification_version = 4
|
40
50
|
|
41
51
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
52
|
+
s.add_runtime_dependency(%q<coderunner>, [">= 0.14.16"])
|
53
|
+
s.add_runtime_dependency(%q<budgetcrmod>, [">= 0.0.0"])
|
54
|
+
s.add_runtime_dependency(%q<command-line-flunky>, [">= 1.0.0"])
|
42
55
|
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
43
56
|
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
44
57
|
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
|
45
58
|
s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
|
46
59
|
s.add_development_dependency(%q<simplecov>, [">= 0"])
|
47
60
|
else
|
61
|
+
s.add_dependency(%q<coderunner>, [">= 0.14.16"])
|
62
|
+
s.add_dependency(%q<budgetcrmod>, [">= 0.0.0"])
|
63
|
+
s.add_dependency(%q<command-line-flunky>, [">= 1.0.0"])
|
48
64
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
49
65
|
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
50
66
|
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
@@ -52,6 +68,9 @@ Gem::Specification.new do |s|
|
|
52
68
|
s.add_dependency(%q<simplecov>, [">= 0"])
|
53
69
|
end
|
54
70
|
else
|
71
|
+
s.add_dependency(%q<coderunner>, [">= 0.14.16"])
|
72
|
+
s.add_dependency(%q<budgetcrmod>, [">= 0.0.0"])
|
73
|
+
s.add_dependency(%q<command-line-flunky>, [">= 1.0.0"])
|
55
74
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
56
75
|
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
57
76
|
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
metadata
CHANGED
@@ -1,15 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: treasurer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Edmund Highcock
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-05-
|
11
|
+
date: 2014-05-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: coderunner
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.14.16
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.14.16
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: budgetcrmod
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: command-line-flunky
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.0.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.0.0
|
13
55
|
- !ruby/object:Gem::Dependency
|
14
56
|
name: shoulda
|
15
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -83,7 +125,8 @@ dependencies:
|
|
83
125
|
description: A simple command line tool for managing accounts and finances. Easily
|
84
126
|
import internet banking spreadsheets and generate sophisticated reports and projections.
|
85
127
|
email: edmundhighcock@users.sourceforge.net
|
86
|
-
executables:
|
128
|
+
executables:
|
129
|
+
- treasurer
|
87
130
|
extensions: []
|
88
131
|
extra_rdoc_files:
|
89
132
|
- LICENSE.txt
|
@@ -95,8 +138,17 @@ files:
|
|
95
138
|
- README.rdoc
|
96
139
|
- Rakefile
|
97
140
|
- VERSION
|
141
|
+
- bin/treasurer
|
98
142
|
- lib/treasurer.rb
|
143
|
+
- lib/treasurer/analysis.rb
|
144
|
+
- lib/treasurer/commands.rb
|
145
|
+
- lib/treasurer/local_customisations.rb
|
146
|
+
- lib/treasurer/report.rb
|
147
|
+
- test/bankaccountstatement.csv
|
99
148
|
- test/helper.rb
|
149
|
+
- test/multiple/FirstBank.csv
|
150
|
+
- test/multiple/SecondBank.csv
|
151
|
+
- test/otheraccountstatement.csv
|
100
152
|
- test/test_treasurer.rb
|
101
153
|
- treasurer.gemspec
|
102
154
|
homepage: http://github.com/edmundhighcock/treasurer
|