require 'active_support/core_ext/integer/inflections' class Array def median self.sort[size/2 + size%2] end end class Float def to_s sprintf("%.2f", self) end end class Numeric def to_tex sprintf("%.2f", self).reverse.gsub(/(\d{3})(?=\d)/){"#$1,\\"}.reverse end end class Date def inspect "Date.parse('#{to_s}')" end end # Some thoughts on double entry accounting: # # assets - liabilities = equity # where equity = equity_at_start + income - expenses # # so # # assets - liabilities = equity_at_start + income - expenses # # or alternatively # # assets + expenses = equity_at_start + income + liabilities (1) # # Good things: # Positive equity_at_start, positive assets, positive income, negative liabilities, negative expenses # # A debit on the left of (1) must be matched by a credit on the right of (1) and # vice versa. # # # A debit to an asset account increases the value of the asset. This means buying some land # or supplies or depositing some cash in a bank account. You can think of it as a debit because # you are locking up your equity in a way that may not be realisable. A credit to the asset account # means drawing down on the asset, for example selling a bit of land or taking money out of a # bank account. # # Similarly, a debit to an expense account, effectively, spending money on that expense, # increases the value of that account. Debits here are clearly negative things from # the point of view of your wealth! (Credits to expense accounts would be something like # travel reimbursements). # # A credit to income increases the value of the income account... this seems obvious. If # you credit income you must debit assets (you have to put your income somewhere, for # example a bank account, i.e. you must effectively spend it by buying an asset: remember # a bank may fail... a bank account is an asset with risk just as much as a painting). # # A credit to liabilities increases the value of the liability, for example taking out a # loan. Once you credit a liability you have to either buy (debit) an asset, or buy (debit) # an expense directly (for example a loan to pay some fees). # # In any accounting period, the sum of all debits and credits should be 0. Also, at the end # of the accounting period, # # equity_at_end = assets - liabilities = equity_at_start + income - expenses # # This seems obvious to me!! class Treasurer class Reporter #include LocalCustomisations attr_reader :today, :start_date, :end_date attr_reader :in_limit_discretionary_account_factors attr_reader :stable_discretionary_account_factors attr_accessor :projected_account_factor attr_reader :accounts attr_reader :equity attr_reader :projected_accounts_info attr_reader :days_before attr_reader :report_currency attr_reader :accounts_hash def initialize(runner, options) @runner = runner @days_ahead = options[:days_ahead]||180 @days_before = options[:days_before]||360 @today = options[:today]||Date.today @start_date = @today - @days_before @end_date = @today + @days_ahead @runs = runner.component_run_list.values @currencies = ACCOUNT_INFO.map{|k,v| v[:currencies]}.flatten.uniq @report_currency = options[:report_currency] if run = @runs.find{|r| not r.external_account} raise "External_account not specified for #{run.data_line}" end @indateruns = @runs.find_all{|r| r.days_ago(@today) < @days_before} @stable_discretionary_account_factors = {} @in_limit_discretionary_account_factors = {} #p 'accounts256',@runs.size, @runs.map{|r| r.account}.uniq end def generate_accounts accounts = @runs.map{|r| r.account}.uniq.map{|acc| Account.new(acc, self, @runner, @runs, false)} external_accounts = (@runs.map{|r| r.external_account}.uniq - accounts.map{|acc| acc.name}).map{|acc| Account.new(acc, self, @runner, @runs, true)} #if not @report_currency external_accounts = external_accounts.map do |acc| if acc_inf = ACCOUNT_INFO[acc.name] and currencies = acc_inf[:currencies] and currencies.size > 1 raise "Only expense accounts can have multiple currencies: #{acc.name} has type #{acc.type}" unless acc.type == :Expense new_accounts = currencies.map do |curr| Account.new(acc.name, self, @runner, @runs, true, currency: curr) end new_accounts.delete_if{|a| a.runs.size == 0} new_accounts else acc end end #end external_accounts = external_accounts.flatten @accounts = accounts + external_accounts @expense_accounts = @accounts.find_all{|acc| acc.type == :Expense} @accounts_hash = @accounts.map{|acc| [acc.name, acc]}.to_h if @report_currency @runs.each do |r| if (curr = @accounts_hash[r.account].currency) != @report_currency er = EXCHANGE_RATES[[curr, @report_currency]] r.deposit *= er r.withdrawal *= er r.balance *= er if r.has_balance? end end ASSETS.each do |name, details| details[:size] *= EXCHANGE_RATES[[details[:currency], @report_currency]] if details[:currency]!=@report_currency details[:currency] = @report_currency end [REGULAR_TRANSFERS, FUTURE_TRANSFERS].each do |transfers| transfers.each do |accs, trans| #acc = accs.find{|a| p a, @accounts_hash.keys, @accounts.map{|ac| ac.name}; not @accounts_hash[a].external} trans.each do |item, details| if details[:currency] != @report_currency #p item, acc, curr, @report_currency details[:size] *= EXCHANGE_RATES[[details[:currency], @report_currency]] details[:currency] = @report_currency end end end end @accounts.each do |acc| if acc.info[:opening_balance] if acc.currency != @report_currency acc.info[:opening_balance] *= EXCHANGE_RATES[[acc.currency, @report_currency]] end end if acc.should_report? #p acc.name_c acc.generate_report_account end acc.instance_variable_set(:@original_currency, acc.currency) acc.instance_variable_set(:@currency, @report_currency) acc.info[:currencies] = [@report_currency] end end get_projected_accounts #p ['projected_accounts_info', @projected_accounts_info] #exit @equities = currency_list.map do |currency| equity = Equity.new(self, @runner, @accounts.find_all{|acc| acc.currency == currency}, currency: currency) @accounts.unshift (equity) [currency, equity] end @equities = @equities.to_h end def report generate_accounts #get_actual_accounts currency_list.each do |currency| get_in_limit_discretionary_account_factor(currency) get_stable_discretionary_account_factor(currency) end report = "" report << header #report << '\begin{multicols}{2}' report << account_summaries currency_list.each do |currency| report << discretionary_account_table(currency) end report << available_balances_table report << account_balance_graphs report << expense_account_summary report << account_expenditure_graphs #report << '\end{multicols}' ##report << account_resolutions #report << account_breakdown report << assumptions report << transactions_by_account report << footer File.open('report.tex', 'w'){|f| f.puts report} Process.waitall system "lualatex report.tex && lualatex report.tex" end def account_summaries #ep 'accounts', @accounts.map{|a| a.name} < labels.size==0 k = GraphKit.quick_create([labels.size.times.to_a, exps]) k.data[0].gp.title = "Cumulative over budget period. Total = #{exps.sum}" k end #sum = exps.sum #angles = exps.map{|ex| ex/sum * 360.0} #start_angles = angles.dup #inject(-angles[0]){|o,n| o+n} ##start_angles.map!{|a| a+(start_angles #end_angles = angles.inject(-angles[0]){|o,n| o+n} kit.data.each{|dk| dk.gp.with = 'boxes'} kit.gp.boxwidth = "#{0.8/kit.data.size} absolute" kit.gp.style = ["fill solid", "textbox opaque noborder fillcolor rgb 'white'"] kit.gp.yrange = "[#{[kit.data[0].y.data.min,0].min}:]" #kit.gp.xrange = "[-1:#{labels.size+1}]" kit.gp.xrange = "[-1.5:1]" if labels.size==1 kit.gp.mytics = "5" kit.gp.ytics = "autofreq rotate by 45" kit.gp.grid = "ytics mytics lw 2,lw 1" kit.xlabel = nil kit.ylabel = nil kit.gp.decimalsign = 'locale "en_GB.UTF-8"' kit.gp.format = [%["%'.2f"]] i = -1 kit.gp.xtics = "(#{labels.map{|l| %["#{l}" #{i+=1}]}.join(', ')}) rotate by 90 right" #pp ['kit222', kit, labels] fork do kit.gp.key = "off" kit.gnuplot_write("#{name}2.eps", size: "#{[[labels.size.to_f/4.2, 5.0].min, 1.0].max}in,4.5in") system %[ps2epsi #{name}2.eps #{name}.eps] system %[epstopdf #{name}.eps] end fork do kit.gp.key = "tmargin left Left reverse" kit.gp.border = "unset" kit.gp.xtics = "unset" kit.gp.ytics = "unset" kit.gp.title = "unset" kit.gp.xlabel = "unset" kit.gp.ylabel = "unset" kit.gp.label = "unset" #kit.gp.xrange = "[-100:-10]" kit.gp.boxwidth = "#{0.0/kit.data.size} absolute" kit.gp.object = " rect from screen 0, screen 0 to screen 1, graph 1 front fc rgb 'white' fillstyle solid noborder" #kit.gp.style = "fill empty noborder" #kit.gp.yrange = "[-10:10]" kit.gnuplot_write("#{name}_key.eps", size: "4.0in,1.5in") system %[convert -density 500 #{name}_key.eps -resize 4000 -bordercolor white -border 20x20 -background white -flatten -trim +repage #{name}_key.pdf] #%x[convert -density 500 #{name}.eps -resize 4000 -bordercolor white -border 20x20 -background white -flatten -trim +repage #{name}.pdf] end #"\\begin{center}\\includegraphics[width=3.0in]{#{name}.eps}\\vspace{1em}\\end{center}" #"\\begin{center}\\includegraphics[width=0.9\\textwidth]{#{name}.eps}\\vspace{1em}\\end{center}" "\\myfigurerot{#{name}.pdf}{#{name}_key.pdf}{270}" end def get_in_limit_discretionary_account_factor(currency) @projected_account_factor = 1.0 loop do ok = true date = @today while date < @today + @days_ahead ok = false if @equities[currency].projected_balance(date) < @equities[currency].red_line(date) date += 1 #ep ['projected_account_factor', date, @equity.projected_balance(date), @equity.red_line(date), ok] end @in_limit_discretionary_account_factors[currency] = @projected_account_factor ep ['projected_account_factor', @projected_account_factor, @equities[currency].projected_balance(date), currency, ok] break if (@projected_account_factor <= 0.0 or ok == true) @projected_account_factor -= 0.01 @projected_account_factor -= 0.04 end @projected_account_factor = nil #exit end def get_stable_discretionary_account_factor(currency) @projected_account_factor = 1.0 loop do ok = true date = @today balances = [] while date < @today + @days_ahead #ok = false if @equity.projected_balance(date) < @equity.red_line(date) date += 1 balances.push @equities[currency].projected_balance(date) #ep ['projected_account_factor', date, balances.mean, @projected_account_factor, @equities[currency].balance(@today), ok] end ok = false if balances.mean < @equities[currency].balance(@today) - 0.001 #ok = false if balances.median < @equities[currency].balance(@today) - 0.001 @stable_discretionary_account_factors[currency] = @projected_account_factor break if (@projected_account_factor <= 0.0 or ok == true) @projected_account_factor -= 0.01 #@projected_account_factor -= 0.1 end @projected_account_factor = nil #exit end def discretionary_account_table(currency) discretionary_accounts = accounts_with_averages( @projected_accounts_info.find_all{|acc,inf| acc.currency == currency and acc.should_report?}.map{|acc,inf| [acc.report_account,inf]}.to_h) accounts_with_projections(discretionary_accounts.keys) < account_info}).map{|acco, acco_info| #ep 'Budget is ', account kit2 = GraphKit.quick_create([ [dates[0], dates[-1]].map{|d| d.to_time.to_i - barsize}, [acco.average, acco.average] ]) kit2.data[0].gp.with = 'l lw 5' kit2 } #$debug_gnuplot = true #kits.sum.gnuplot kit += kits.sum else kit.data[0].y.data.map!{|expen| expen*-1.0} end kit.title = "#{account.name_c} Expenditure with average (Total = #{kit.data[0].y.data.sum})" CodeRunner::Budget.kit_time_format_x(kit) kit.gp.decimalsign = 'locale "en_GB.UTF-8"' kit.gp.format.push %[y "%'.2f"] #kit.gnuplot #ep ['kit1122', account, kit] fork do kit.gnuplot_write("#{account.name_c_file}2.eps", size: "4.0in,2.0in") system "ps2epsi #{account.name_c_file}2.eps #{account.name_c_file}.eps" exec "epstopdf #{account.name_c_file}.eps" end #%x[ps2eps #{account}.ps] #"\\begin{center}\\includegraphics[width=3.0in]{#{account}.eps}\\vspace{1em}\\end{center}" #"\\begin{center}\\includegraphics[width=0.9\\textwidth]{#{account}.eps}\\vspace{1em}\\end{center}" "\\myfigure{#{account.name_c_file}.pdf}" end }.join("\n\n") }" end def account_resolutions < 0 " \\small \\setlength{\\parindent}{0cm}\n\n\\begin{tabulary}{0.99\\textwidth}{ #{"c " * 3 + " L " + " r " * 2 + " c " }} %\\hline #{date.to_s.latex_escape} & & & Total & #{expenditure} & \\\\ \\hline \\Tstrut #{items.map{|r| ( CodeRunner::Budget.rcp.component_results + [:external_account] - [:sc, :balance ]).map{|res| r.send(res).to_s.latex_escape }.join(" & ") }.join("\\\\\n") } \\\\ \\hline \\end{tabulary} \\normalsize \\vspace{1em}\n\n" else "" end }.join("\n\n") }.join("\n\n") } EOF end def transactions_by_account < 25 #if entry.length > 40 #entry = entry.split(/.{40}/).join(" \\newline ") #end entry #rcp.component_results.map{|res| r.send(res).to_s.gsub(/(.{20})/, '\1\\\\\\\\').latex_escape }.join(" & ") }.join("\\\\\n")} \\end{tabulary}"}.join("\n\n")}" }.join("\n\n")} EOF end def header <