rbinvoice 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.html ADDED
@@ -0,0 +1,43 @@
1
+ <h1>rbinvoice</h1>
2
+
3
+ <p>rbinvoice lets you generate PDF invoices from a Google Spreadsheet.
4
+ It's pretty obscure; you probably haven't heard of it.</p>
5
+
6
+ <h2>Input</h2>
7
+
8
+ <p>RbInvoice reads your hours from a Google Spreadsheet, which should be formatted like this:</p>
9
+
10
+ <table>
11
+ <tr>
12
+ <td colspan="7"><b style="font-size:120%">My Time Tracking</b></td>
13
+ </tr>
14
+ <tr>
15
+ <td><b>Weekday</b></td>
16
+ <td><b>Day</b></td>
17
+ <td><b>Task</b></td>
18
+ <td><b>Notes</b></td>
19
+ <td><b>Start</b></td>
20
+ <td><b>Stop</b></td>
21
+ <td><b>Total</b></td>
22
+ </tr>
23
+ <tr>
24
+ <td>T</td>
25
+ <td>3/20/2012</td>
26
+ <td>BigCorp</td>
27
+ <td>API</td>
28
+ <td>8:00</td>
29
+ <td>12:15</td>
30
+ <td>4:15</td>
31
+ </tr>
32
+ <tr>
33
+ <td>T</td>
34
+ <td>3/20/2012</td>
35
+ <td>SmallCorp</td>
36
+ <td>Shopping Cart</td>
37
+ <td>13:00</td>
38
+ <td>17:15</td>
39
+ <td>4:00</td>
40
+ </tr>
41
+ </table>
42
+
43
+ <p>Columns B, E, F, and G should have a Date format. </p>
data/README.md CHANGED
@@ -1,6 +1,57 @@
1
1
  rbinvoice
2
2
  =========
3
3
 
4
- rbinvoice lets you generate PDF invoices from a Google Spreadsheet.
4
+ RbInvoice lets you generate PDF invoices from a Google Spreadsheet.
5
5
  It's pretty obscure; you probably haven't heard of it.
6
6
 
7
+
8
+
9
+ Disclaimer
10
+ ----------
11
+
12
+ RbInvoice is not production-ready code! I keep it on Github more for my own convenience than anything else. I do use it myself to bill clients, but lots of things are hard-coded, like my company address. If you use it, you do so at your own risk! I don't guarantee anything, and I don't promise any support. If you tell your clients to write checks to Paul Jungwirth and send them to my address, that's too bad for you. :-)
13
+
14
+ Perhaps someday I'll get this code into a shareable state; it's inching there a little bit each month. But right now you should find a real invoicing solution somewhere else. All documentation here is purely in expectation of an eventual release. Things may be broken and may change, so please don't take it as a promise of anything.
15
+
16
+
17
+
18
+ Input
19
+ -----
20
+
21
+ RbInvoice reads your hours from a Google Spreadsheet, which should be formatted like this:
22
+
23
+ <table>
24
+ <tr>
25
+ <td colspan="7"><b style="font-size:120%">My Time Tracking</b></td>
26
+ </tr>
27
+ <tr>
28
+ <td><b>Weekday</b></td>
29
+ <td><b>Day</b></td>
30
+ <td><b>Task</b></td>
31
+ <td><b>Notes</b></td>
32
+ <td><b>Start</b></td>
33
+ <td><b>Stop</b></td>
34
+ <td><b>Total</b></td>
35
+ </tr>
36
+ <tr>
37
+ <td>T</td>
38
+ <td>3/20/2012</td>
39
+ <td>BigCorp</td>
40
+ <td>API</td>
41
+ <td>8:00</td>
42
+ <td>12:15</td>
43
+ <td>4:15</td>
44
+ </tr>
45
+ <tr>
46
+ <td>T</td>
47
+ <td>3/20/2012</td>
48
+ <td>SmallCorp</td>
49
+ <td>Shopping Cart</td>
50
+ <td>13:00</td>
51
+ <td>17:15</td>
52
+ <td>4:00</td>
53
+ </tr>
54
+ </table>
55
+
56
+ Columns B, E, F, and G should have a Date format. I calculate G automatically by saying `=max(0, F3 - E3)`, but if you do that, make sure you enter times in 24-hour format, because if you work through lunch (e.g. 11:00 to 1:30) your total column will be 0:00.
57
+
data/Rakefile CHANGED
@@ -58,3 +58,6 @@ task :run, [] => [] do |t, args|
58
58
  RbInvoice::write_invoice(*RbInvoice::Options::parse_command_line(%w{okvenue}))
59
59
  end
60
60
 
61
+ task :readme => [] do |task|
62
+ `markdown README.md >README.html`
63
+ end
data/TODO CHANGED
@@ -8,5 +8,6 @@
8
8
  - more tests
9
9
  - print the pdflatex results to std{out,err} only if the user gives a --verbose flag.
10
10
  - Fix LaTeX escaping for double quotes.
11
+ - Add an option to redo the last invoice for the given client.
11
12
 
12
13
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.3
1
+ 0.2.4
data/lib/rbinvoice.rb CHANGED
@@ -15,6 +15,8 @@ module RbInvoice
15
15
  COL_START_TIME = 'E'
16
16
  COL_END_TIME = 'F'
17
17
  COL_TOTAL_TIME = 'G'
18
+ COL_MONDAY = 'H'
19
+ COL_NOTES = 'I'
18
20
 
19
21
  # TODO:
20
22
  # - Figure out the next invoice_number.
@@ -43,7 +45,7 @@ module RbInvoice
43
45
  earliest_date = if last_invoice
44
46
  last_invoice[:end_date] + 1
45
47
  else
46
- parse_date(earliest_task_date)
48
+ parse_date(earliest_task_date(hours))
47
49
  end
48
50
  start_date, end_date = RbInvoice::Options::find_invoice_bounds(earliest_date, freq)
49
51
  tasks = hourly_breakdown(client, start_date, end_date, opts)
@@ -59,20 +61,25 @@ module RbInvoice
59
61
 
60
62
  def self.make_pdf(tasks, start_date, end_date, filename, opts)
61
63
  write_latex(tasks, end_date, filename, opts)
62
- system("cd \"#{File.dirname(filename)}\" && pdflatex \"#{File.basename(filename, '.pdf')}\"")
64
+ result = system("cd \"#{File.dirname(filename)}\" && pdflatex \"#{File.basename(filename, '.pdf')}\"")
65
+ raise "Problem running LaTeX: $?" unless result
63
66
  RbInvoice::Options::add_invoice_to_data(tasks, start_date, end_date, filename, opts) unless opts[:no_data_file]
64
67
  end
65
68
 
66
69
  def self.escape_for_latex(str)
67
- str.gsub('&', '\\\\&'). # tricky b/c '\&' has special meaning to gsub.
70
+ (str || '').gsub('&', '\\\\&'). # tricky b/c '\&' has special meaning to gsub.
68
71
  gsub('"', '\texttt{"}').
69
72
  gsub('$', '\$').
70
- gsub('+', '$+$')
73
+ gsub('+', '$+$').
74
+ gsub("\n", " \\\\\\\\ \n")
71
75
  end
72
76
 
73
77
  def self.write_latex(tasks, invoice_date, filename, opts)
74
78
  template = File.open(opts[:template]) { |f| f.read }
75
79
  rate = opts[:rate] # TODO: Support per-task rates
80
+ full_name = RbInvoice::Options::full_name_for_client(opts[:data], opts, opts[:client])
81
+ address = RbInvoice::Options::address_for_client(opts[:data], opts, opts[:client])
82
+ description = RbInvoice::Options::description_for_client(opts[:data], opts, opts[:client])
76
83
  items = tasks.map{|task, details|
77
84
  task_total_hours = details.inject(0) {|t, row| t + row[2]}
78
85
  {
@@ -92,6 +99,10 @@ module RbInvoice
92
99
  line_items: items,
93
100
  total_duration: decimal_to_interval(items.inject(0) {|t, item| t + item['duration_decimal']}),
94
101
  total_price: "%0.02f" % items.inject(0) {|t, item| t + item['price_decimal']},
102
+ dba: escape_for_latex(opts[:dba]),
103
+ client_full_name: escape_for_latex(full_name),
104
+ client_address: escape_for_latex(address),
105
+ client_description: escape_for_latex(description),
95
106
  }.map{|k, v| [k.to_s, v]}
96
107
  ]
97
108
  latex = Liquid::Template.parse(template).render args
@@ -127,8 +138,12 @@ module RbInvoice
127
138
  to_client_key(ss.cell(row, COL_CLIENT) || '') == client
128
139
  }.map { |row|
129
140
  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'
130
- [ss.cell(row, COL_DATE), ss.cell(row, COL_TASK), interval_to_decimal(ss.cell(row, COL_TOTAL_TIME))]
131
- }
141
+ if ss.cell(row, COL_NOTES) == 'FREE'
142
+ nil
143
+ else
144
+ [ss.cell(row, COL_DATE), ss.cell(row, COL_TASK), interval_to_decimal(ss.cell(row, COL_TOTAL_TIME))]
145
+ end
146
+ }.compact
132
147
  end
133
148
 
134
149
  def self.interval_to_decimal(time)
@@ -195,6 +195,22 @@ module RbInvoice
195
195
  key_for_client(data, client, :frequency)
196
196
  end
197
197
 
198
+ def self.dba_for_client(data, opts, client)
199
+ key_for_client(data, client, :dba) || opts[:dba] || 'Illuminated Computing Inc.'
200
+ end
201
+
202
+ def self.full_name_for_client(data, opts, client)
203
+ key_for_client(data, client, :full_name)
204
+ end
205
+
206
+ def self.address_for_client(data, opts, client)
207
+ key_for_client(data, client, :address)
208
+ end
209
+
210
+ def self.description_for_client(data, opts, client)
211
+ key_for_client(data, client, :description)
212
+ end
213
+
198
214
  def self.parse_command_line(argv)
199
215
  opts = Trollop::options(argv) do
200
216
  version "rbinvoice 0.1.0 (c) 2012 Paul A. Jungwirth"
@@ -243,6 +259,7 @@ module RbInvoice
243
259
  opts[:start_date] = Date.strptime(opts[:start_date], "%Y-%m-%d") if opts[:start_date]
244
260
  opts[:end_date] = Date.strptime(opts[:end_date], "%Y-%m-%d") if opts[:end_date]
245
261
 
262
+ opts[:dba] = dba_for_client(opts[:data], opts, opts[:client])
246
263
  # Read the list of past invoices.
247
264
  # If there are none, assume there is only one invoice to do.
248
265
 
data/rbinvoice.gemspec CHANGED
@@ -5,16 +5,17 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "rbinvoice"
8
- s.version = "0.2.3"
8
+ s.version = "0.2.4"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Paul A. Jungwirth"]
12
- s.date = "2012-08-23"
12
+ s.date = "2013-03-15"
13
13
  s.description = " Reads hours from a Google Spreadsheet and generates a PDF invoice.\n"
14
14
  s.email = "pj@illuminatedcomputing.com"
15
15
  s.executables = ["rbinvoice", "rbinvoice"]
16
16
  s.extra_rdoc_files = [
17
17
  "LICENSE.txt",
18
+ "README.html",
18
19
  "README.md",
19
20
  "TODO"
20
21
  ]
data/spec/options_spec.rb CHANGED
@@ -56,20 +56,20 @@ describe RbInvoice::Options do
56
56
 
57
57
  it "should compute the semimonth start date" do
58
58
  RbInvoice::Options::semimonth_start(Date.new(2011, 3, 5)).should == Date.new(2011, 3, 1)
59
- RbInvoice::Options::semimonth_start(Date.new(2011, 3,21)).should == Date.new(2011, 3,15)
60
- RbInvoice::Options::semimonth_start(Date.new(2011, 3,31)).should == Date.new(2011, 3,15)
59
+ RbInvoice::Options::semimonth_start(Date.new(2011, 3,21)).should == Date.new(2011, 3,16)
60
+ RbInvoice::Options::semimonth_start(Date.new(2011, 3,31)).should == Date.new(2011, 3,16)
61
61
  RbInvoice::Options::semimonth_start(Date.new(2011, 3, 1)).should == Date.new(2011, 3, 1)
62
62
  RbInvoice::Options::semimonth_start(Date.new(2011, 4, 8)).should == Date.new(2011, 4, 1)
63
- RbInvoice::Options::semimonth_start(Date.new(2011, 2,28)).should == Date.new(2011, 2,15)
64
- RbInvoice::Options::semimonth_start(Date.new(2012, 2,28)).should == Date.new(2012, 2,15)
65
- RbInvoice::Options::semimonth_start(Date.new(2012, 2,29)).should == Date.new(2012, 2,15)
66
- RbInvoice::Options::semimonth_start(Date.new(2011, 2,19)).should == Date.new(2011, 2,15)
67
- RbInvoice::Options::semimonth_start(Date.new(2012, 2,19)).should == Date.new(2012, 2,15)
63
+ RbInvoice::Options::semimonth_start(Date.new(2011, 2,28)).should == Date.new(2011, 2,16)
64
+ RbInvoice::Options::semimonth_start(Date.new(2012, 2,28)).should == Date.new(2012, 2,16)
65
+ RbInvoice::Options::semimonth_start(Date.new(2012, 2,29)).should == Date.new(2012, 2,16)
66
+ RbInvoice::Options::semimonth_start(Date.new(2011, 2,19)).should == Date.new(2011, 2,16)
67
+ RbInvoice::Options::semimonth_start(Date.new(2012, 2,19)).should == Date.new(2012, 2,16)
68
68
  RbInvoice::Options::semimonth_start(Date.new(2012, 1, 5)).should == Date.new(2012, 1, 1)
69
69
  RbInvoice::Options::semimonth_start(Date.new(2011,12, 1)).should == Date.new(2011,12, 1)
70
70
  RbInvoice::Options::semimonth_start(Date.new(2011,12, 5)).should == Date.new(2011,12, 1)
71
71
  RbInvoice::Options::semimonth_start(Date.new(2011,12,15)).should == Date.new(2011,12, 1)
72
- RbInvoice::Options::semimonth_start(Date.new(2011,12,25)).should == Date.new(2011,12,15)
72
+ RbInvoice::Options::semimonth_start(Date.new(2011,12,25)).should == Date.new(2011,12,16)
73
73
  end
74
74
 
75
75
  it "should compute the previous semimonth" do
@@ -8,7 +8,7 @@
8
8
 
9
9
  \noindent
10
10
  \begin{minipage}[t]{3in}
11
- Paul Jungwirth \\
11
+ {{ dba }} \\
12
12
  2520 SW Edgemoor Ave. \\
13
13
  Beaverton, OR 97005 \\
14
14
  909 557-0421 \\
@@ -25,13 +25,13 @@ Beaverton, OR 97005 \\
25
25
  {% endraw %}
26
26
 
27
27
  \noindent {\sc to}: \\
28
- OK Venue LLC \\
29
- 116 W 23rd St. \\
30
- New York, NY 10011 \\
28
+ {{ client_full_name }} \\
29
+ {% if client_address %}{{ client_address }} \\
30
+ {% endif %}
31
31
  \smallskip
32
32
 
33
33
  \noindent {\sc for}: \\
34
- Consulting and programming for the okvenue.com website and related projects. \\
34
+ {{ client_description }} \\
35
35
  \linebreak[4]
36
36
 
37
37
  \begin{tabular}{lrr}
@@ -45,8 +45,8 @@ Consulting and programming for the okvenue.com website and related projects. \\
45
45
  \bigskip
46
46
 
47
47
  \noindent
48
- Please make all checks payable to Paul Jungwirth. \\
49
- Payment due net 30 days. \\
48
+ Please make all checks payable to {{ dba }}. \\
49
+ Payment due upon receipt. \\
50
50
 
51
51
  \begin{center}
52
52
  \textbf{Thank you for your business!}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbinvoice
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-23 00:00:00.000000000 Z
12
+ date: 2013-03-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: trollop
@@ -148,6 +148,7 @@ executables:
148
148
  extensions: []
149
149
  extra_rdoc_files:
150
150
  - LICENSE.txt
151
+ - README.html
151
152
  - README.md
152
153
  - TODO
153
154
  files:
@@ -166,6 +167,7 @@ files:
166
167
  - rbinvoice.gemspec
167
168
  - spec/options_spec.rb
168
169
  - templates/invoice.tex.liquid
170
+ - README.html
169
171
  homepage: http://github.com/pjungwir/rbinvoice
170
172
  licenses:
171
173
  - MIT
@@ -181,7 +183,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
181
183
  version: '0'
182
184
  segments:
183
185
  - 0
184
- hash: 1152283729066738972
186
+ hash: 2995707456041748597
185
187
  required_rubygems_version: !ruby/object:Gem::Requirement
186
188
  none: false
187
189
  requirements: