module RbInvoice
Constants
- COL_CLIENT
- COL_DATE
- COL_END_TIME
- COL_MONDAY
- COL_NOTES
- COL_START_TIME
- COL_TASK
- COL_TOTAL_TIME
- VERSION
Public Class Methods
decimal_to_interval(time)
click to toggle source
# File lib/rbinvoice.rb, line 158 def self.decimal_to_interval(time) "%d:%02d" % [time.to_i, (60*time) % 60] end
earliest_task_date(hours)
click to toggle source
# File lib/rbinvoice.rb, line 31 def self.earliest_task_date(hours) row = hours.sort_by { |row| parse_date(row[0]) }.first row ? row[0] : nil end
escape_for_latex(str)
click to toggle source
# File lib/rbinvoice.rb, line 71 def self.escape_for_latex(str) (str || '').gsub('&', '\\\\&'). # tricky b/c '\&' has special meaning to gsub. gsub('"', '\texttt{"}'). gsub('$', '\$'). gsub('+', '$+$'). gsub("\n", " \\\\\\\\ \n") end
group_by_task(rows)
click to toggle source
# File lib/rbinvoice.rb, line 173 def self.group_by_task(rows) rows.group_by{|r| r[1]} end
hourly_breakdown(client, start_date, end_date, opts)
click to toggle source
# File lib/rbinvoice.rb, line 115 def self.hourly_breakdown(client, start_date, end_date, opts) hours = group_by_task(select_date_range(start_date, end_date, read_all_hours(client, opts))) end
interval_to_decimal(time)
click to toggle source
# File lib/rbinvoice.rb, line 152 def self.interval_to_decimal(time) return nil unless time d = Date._strptime(time, "%H:%M") BigDecimal.new(d[:hour] * 60 + d[:min]) / 60 end
make_pdf(tasks, start_date, end_date, filename, opts)
click to toggle source
# File lib/rbinvoice.rb, line 64 def self.make_pdf(tasks, start_date, end_date, filename, opts) write_latex(tasks, end_date, filename, opts) result = system("cd \"#{File.dirname(filename)}\" && pdflatex \"#{File.basename(filename, '.pdf')}\"") raise "Problem running LaTeX: $?" unless result RbInvoice::Options::add_invoice_to_data(tasks, start_date, end_date, filename, opts) unless opts[:no_data_file] end
open_worksheet(spreadsheet, username, password)
click to toggle source
# File lib/rbinvoice.rb, line 119 def self.open_worksheet(spreadsheet, username, password) g = Google.new(spreadsheet, username, password) g.date_format = '%m/%d/%Y' g.default_sheet = g.sheets.first return g end
parse_date(str)
click to toggle source
TODO:
- Figure out the next invoice_number. - Record the invoice & the new invoice_number. - Default dir for the tex & pdf files.
# File lib/rbinvoice.rb, line 26 def self.parse_date(str) return str if str.class == Date Date.strptime(str, "%m/%d/%Y") end
read_all_hours(client, opts)
click to toggle source
# File lib/rbinvoice.rb, line 130 def self.read_all_hours(client, opts) ss = nil begin ss = open_worksheet(opts[:spreadsheet], opts[:spreadsheet_user], opts[:spreadsheet_password]) rescue Exception => e $stderr.puts "rbinvoice: Failed to open spreadsheet #{opts[:spreadsheet]}: #{$!}" exit 1 end client = to_client_key(client) return 3.upto(ss.last_row).select { |row| to_client_key(ss.cell(row, COL_CLIENT) || '') == client }.map { |row| raise "Invalid task times: #{ss.cell(row, COL_START_TIME)}-#{ss.cell(row, COL_END_TIME)}" if ss.cell(row, COL_START_TIME) && ss.cell(row, COL_END_TIME) && ss.cell(row, COL_TOTAL_TIME) == '0:00:00' if ss.cell(row, COL_NOTES) == 'FREE' nil else [ss.cell(row, COL_DATE), ss.cell(row, COL_TASK), interval_to_decimal(ss.cell(row, COL_TOTAL_TIME))] end }.compact end
select_date_range(start_date, end_date, hours)
click to toggle source
# File lib/rbinvoice.rb, line 162 def self.select_date_range(start_date, end_date, hours) hours.select do |row| # puts "#{row[0].class}: #{row.join("\t")}" # Sometimes we get a String, sometimes a Date, # and changing the cell's format in the spreadsheet # doesn't have any effect. So do our best to support both: d = row[0].class == String ? parse_date(row[0]) : row[0] start_date <= d and d <= end_date end end
to_client_key(client)
click to toggle source
# File lib/rbinvoice.rb, line 126 def self.to_client_key(client) client.downcase.gsub(' ', '') end
write_invoices(client, start_date, end_date, filename, opts)
click to toggle source
# File lib/rbinvoice.rb, line 36 def self.write_invoices(client, start_date, end_date, filename, opts) if start_date and end_date tasks = hourly_breakdown(client, start_date, end_date, opts) make_pdf(tasks, start_date, end_date, filename, opts) else # Write all the outstanding spreadsheets freq = RbInvoice::Options::frequency_for_client(opts[:data], client) last_invoice = RbInvoice::Options::last_invoice_for_client(opts[:data], client) hours = read_all_hours(client, opts) earliest_date = if last_invoice last_invoice[:end_date] + 1 else parse_date(earliest_task_date(hours)) end start_date, end_date = RbInvoice::Options::find_invoice_bounds(earliest_date, freq) opts[:start_date] = start_date opts[:end_date] = end_date tasks = hourly_breakdown(client, start_date, end_date, opts) while tasks.size > 0 filename = RbInvoice::Options::default_out_filename(opts) make_pdf(tasks, start_date, end_date, filename, opts) start_date, end_date = RbInvoice::Options::find_invoice_bounds(end_date + 1, freq) tasks = hourly_breakdown(client, start_date, end_date, opts) opts[:invoice_number] += 1 end end end
write_latex(tasks, invoice_date, filename, opts)
click to toggle source
# File lib/rbinvoice.rb, line 79 def self.write_latex(tasks, invoice_date, filename, opts) template = File.open(opts[:template]) { |f| f.read } rate = opts[:rate] # TODO: Support per-task rates full_name = RbInvoice::Options::full_name_for_client(opts[:data], opts, opts[:client]) address = RbInvoice::Options::address_for_client(opts[:data], opts, opts[:client]) description = RbInvoice::Options::description_for_client(opts[:data], opts, opts[:client]) items = tasks.map{|task, details| task_total_hours = details.inject(0) {|t, row| t + row[2]} { 'name' => escape_for_latex(task), 'duration_decimal' => task_total_hours, 'duration' => decimal_to_interval(task_total_hours), 'price_decimal' => task_total_hours * rate, 'price' => "%0.02f" % (task_total_hours * rate) } } args = Hash[ { invoice_number: opts[:invoice_number], invoice_date: invoice_date.strftime("%d %B %Y"), line_items: items, total_duration: decimal_to_interval(items.inject(0) {|t, item| t + item['duration_decimal']}), total_price: "%0.02f" % items.inject(0) {|t, item| t + item['price_decimal']}, dba: escape_for_latex(opts[:dba]), payment_due: opts[:payment_due], client_full_name: escape_for_latex(full_name), client_address: escape_for_latex(address), client_description: escape_for_latex(description), }.map{|k, v| [k.to_s, v]} ] latex = Liquid::Template.parse(template).render args File.open("#{filename.gsub(/\.pdf$/, '')}.tex", 'w') { |f| f.write(latex) } end