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 +43 -0
- data/README.md +52 -1
- data/Rakefile +3 -0
- data/TODO +1 -0
- data/VERSION +1 -1
- data/lib/rbinvoice.rb +21 -6
- data/lib/rbinvoice/options.rb +17 -0
- data/rbinvoice.gemspec +3 -2
- data/spec/options_spec.rb +8 -8
- data/templates/invoice.tex.liquid +7 -7
- metadata +5 -3
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
|
-
|
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
data/TODO
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
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
|
-
|
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)
|
data/lib/rbinvoice/options.rb
CHANGED
@@ -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.
|
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 = "
|
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,
|
60
|
-
RbInvoice::Options::semimonth_start(Date.new(2011, 3,31)).should == Date.new(2011, 3,
|
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,
|
64
|
-
RbInvoice::Options::semimonth_start(Date.new(2012, 2,28)).should == Date.new(2012, 2,
|
65
|
-
RbInvoice::Options::semimonth_start(Date.new(2012, 2,29)).should == Date.new(2012, 2,
|
66
|
-
RbInvoice::Options::semimonth_start(Date.new(2011, 2,19)).should == Date.new(2011, 2,
|
67
|
-
RbInvoice::Options::semimonth_start(Date.new(2012, 2,19)).should == Date.new(2012, 2,
|
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,
|
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
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
28
|
+
{{ client_full_name }} \\
|
29
|
+
{% if client_address %}{{ client_address }} \\
|
30
|
+
{% endif %}
|
31
31
|
\smallskip
|
32
32
|
|
33
33
|
\noindent {\sc for}: \\
|
34
|
-
|
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
|
49
|
-
Payment due
|
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.
|
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:
|
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:
|
186
|
+
hash: 2995707456041748597
|
185
187
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
186
188
|
none: false
|
187
189
|
requirements:
|