Pratt 1.6.2 → 1.6.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.
Files changed (50) hide show
  1. data/.gitignore +2 -0
  2. data/Pratt.gemspec +38 -12
  3. data/README.html +85 -0
  4. data/README.txt +6 -3
  5. data/TODO +6 -3
  6. data/VERSION +1 -1
  7. data/config.rb +3 -17
  8. data/db/zips.csv.zip +0 -0
  9. data/lib/models.rb +8 -0
  10. data/lib/pratt.rb +58 -293
  11. data/lib/pratt.rb.orig +626 -0
  12. data/lib/pratt/core_ext.rb +6 -0
  13. data/lib/pratt/{array.rb → core_ext/array.rb} +5 -0
  14. data/lib/pratt/core_ext/float.rb +28 -0
  15. data/lib/pratt/core_ext/nil.rb +5 -0
  16. data/lib/pratt/core_ext/numeric.rb +9 -0
  17. data/lib/pratt/{string.rb → core_ext/string.rb} +13 -0
  18. data/lib/pratt/core_ext/time.rb +20 -0
  19. data/lib/pratt/dialogs.rb +81 -0
  20. data/lib/pratt/formatting.rb +24 -0
  21. data/lib/pratt/project_actions.rb +25 -0
  22. data/lib/pratt/reports.rb +127 -0
  23. data/models/app.rb +1 -2
  24. data/models/customer.rb +22 -0
  25. data/models/invoice.rb +28 -0
  26. data/models/invoice_whence.rb +18 -0
  27. data/models/payment.rb +1 -1
  28. data/models/pratt.rb +0 -9
  29. data/models/project.rb +16 -22
  30. data/models/whence.rb +10 -1
  31. data/models/zip.rb +27 -0
  32. data/spec/fixtures/graph.expectation +0 -5
  33. data/spec/fixtures/proportions.expectation +4 -0
  34. data/spec/float_spec.rb +24 -0
  35. data/spec/numeric_spec.rb +30 -0
  36. data/spec/pratt_spec.rb +5 -4
  37. data/spec/project_spec.rb +8 -2
  38. data/spec/spec.opts +1 -1
  39. data/spec/spec_helper.rb +31 -1
  40. data/spec/string_ext_spec.rb +33 -0
  41. data/views/current.eruby +5 -0
  42. data/views/general-invoice.eruby +538 -0
  43. data/views/graph.eruby +2 -7
  44. data/views/invoice.eruby +3 -3
  45. data/views/main.rb +8 -6
  46. data/views/proportions.eruby +4 -0
  47. data/views/raw.eruby +1 -1
  48. metadata +86 -33
  49. data/lib/pratt/time.rb +0 -12
  50. data/views/env.rb +0 -22
@@ -0,0 +1,18 @@
1
+ class InvoiceWhence < ActiveRecord::Base
2
+ belongs_to :invoice
3
+ belongs_to :whence
4
+
5
+ def self.migrate which = :up
6
+ ActiveRecord::Schema.define do
7
+ if which == :up
8
+ create_table :invoice_whences do |t|
9
+ t.references :invoice
10
+ t.references :whence
11
+ t.datetime :created_at
12
+ end
13
+ elsif which == :down
14
+ drop_table :invoice_whences
15
+ end
16
+ end
17
+ end
18
+ end
@@ -2,7 +2,7 @@ class Payment < ActiveRecord::Base
2
2
  belongs_to :billable, :polymorphic => true
3
3
 
4
4
  def pretty_print
5
- "$%0.2f"% (rate/100)
5
+ "$%0.2f"% (rate/100.0)
6
6
  end
7
7
 
8
8
  class << self
@@ -6,14 +6,5 @@ class Pratt
6
6
  cond = [(cond << "start_at BETWEEN ? AND ?").join(' AND ')] | [when_to.send("beginning_of_#{scale}"), when_to.send("end_of_#{scale}")] unless scale.nil?
7
7
  cond
8
8
  end
9
-
10
- def spent what
11
- Proc.new {|*scale_when|
12
- scale, when_to = scale_when
13
- what.send(:all, :conditions => conditions_for_time_spent(scale, when_to) ).inject(0.0) {|total, whence|
14
- total += ( whence.end_at - whence.start_at )
15
- } / 3600
16
- }
17
- end
18
9
  end
19
10
  end
@@ -26,11 +26,16 @@ class Project < ActiveRecord::Base
26
26
  end
27
27
 
28
28
  def time_spent scale = nil, when_to = DateTime.now
29
- spent(self.whences).call(scale, when_to)
29
+ whences_since = self.whences.find :all, :conditions => conditions_for_time_spent(scale, when_to)
30
+ whences_since.inject(0.0) {|total, whence|
31
+ total += ( whence.end_at - whence.start_at )
32
+ } / 3600
30
33
  end
31
34
 
32
35
  def amount scale = nil, when_to = Time.now
33
- spent(self.whences).call(scale, when_to) * (payment.rate / 100.0)
36
+ amount = time_spent(scale, when_to)
37
+ amount *= payment.rate / 100.0
38
+ amount.to_money
34
39
  end
35
40
 
36
41
  class << self
@@ -47,6 +52,15 @@ class Project < ActiveRecord::Base
47
52
  all - [primary, off]
48
53
  end
49
54
 
55
+ def longest_project_name
56
+ project_names = all.collect(&:name)
57
+ longest = project_names.inject(0) do |max, next_name|
58
+ max = next_name.length if next_name.length > max
59
+ max
60
+ end
61
+ longest
62
+ end
63
+
50
64
  def migrate which = :up
51
65
  ActiveRecord::Schema.define do
52
66
  if which == :up
@@ -55,26 +69,6 @@ class Project < ActiveRecord::Base
55
69
  t.integer :weight, :default => -1
56
70
  t.references :customer, :null => false
57
71
  end
58
-
59
- Project.create(
60
- [
61
- {
62
- :name => 'Refactor',
63
- :weight => 1,
64
- :customer_id => 0
65
- },
66
- {
67
- :name => 'Lunch/Break',
68
- :weight => 0,
69
- :customer_id => 0
70
- },
71
- {
72
- :name => 'Other',
73
- :weight => -1,
74
- :customer_id => 0
75
- }
76
- ]
77
- )
78
72
  elsif which == :down
79
73
  drop_table :projects
80
74
  end
@@ -2,6 +2,8 @@ require 'models/pratt'
2
2
 
3
3
  class Whence < ActiveRecord::Base
4
4
  belongs_to :project
5
+ has_many :invoice_whences
6
+ has_many :invoices, :through => :invoice_whences
5
7
  validates_associated :project
6
8
 
7
9
  def stop! when_to
@@ -33,12 +35,19 @@ class Whence < ActiveRecord::Base
33
35
  def inspect
34
36
  "#<Whence id: '#{id || ''}', project: '#{project ? project.name : ''}', start_at: '#{start_at? ? start_at.strftime(Pratt::FMT) : ''}', end_at: '#{end_at? ? end_at.strftime(Pratt::FMT) : ''}', invoiced: #{invoiced ? 'true' : 'false'}>"
35
37
  end
38
+
39
+ def invoiced?
40
+ read_attribute(:invoiced) == true
41
+ end
36
42
 
37
43
  class << self
38
44
  include Pratt::TimeSpent
39
45
 
40
46
  def time_spent scale = nil, when_to = Time.now
41
- spent(self).call(scale, when_to)
47
+ whences_since = Whence.find :all, :conditions => conditions_for_time_spent(scale, when_to)
48
+ whences_since.inject(0.0) {|total, whence|
49
+ total += ( whence.end_at - whence.start_at )
50
+ } / 3600
42
51
  end
43
52
 
44
53
  def last_unended
@@ -0,0 +1,27 @@
1
+ class Zip < ActiveRecord::Base
2
+
3
+ validates_presence_of :zip, :city, :ST, :state, :longitude, :latitude
4
+
5
+ def before_validation_on_create
6
+ self.ST = self.state[0,2].upcase
7
+ end
8
+
9
+ class << self
10
+ def migrate which = :up
11
+ ActiveRecord::Schema.define do
12
+ if which == :up
13
+ create_table :zips do |t|
14
+ t.string :zip, :null => false
15
+ t.string :city, :null => false
16
+ t.string :ST, :null => false, :limit => 2
17
+ t.string :state, :null => false
18
+ t.float :longitude, :null => false
19
+ t.float :latitude, :null => false
20
+ end
21
+ elsif which == :down
22
+ drop_table :zips
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -11,8 +11,3 @@ Project detail
11
11
  Task3| 01 day 17 hour 32 min
12
12
 
13
13
  ------------------------------------------------------------
14
-
15
- Total 43.69 hrs
16
- **** ******** 0.00%
17
- Lunch/Break 3.11%
18
- Other 100.00%
@@ -0,0 +1,4 @@
1
+ Total 43.69 hrs
2
+ **** ******** 0.00%
3
+ Lunch/Break 3.11%
4
+ Other 100.00%
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+ require 'pratt'
3
+
4
+ describe Float do
5
+ it { 2.0.should respond_to(:format_integer) }
6
+ it { 2.0.should respond_to(:percentage) }
7
+
8
+ it "should format correctly" do
9
+ 2.0.format_integer.should eql("02")
10
+ end
11
+
12
+ it "shows percentage correctly with no total" do
13
+ 2.3.percentage.should eql("230.00%")
14
+ end
15
+
16
+ it "shows percentage correctly with total" do
17
+ 2.5.percentage(5).should eql("50.00%")
18
+ end
19
+
20
+ it "handles with_label as expected" do
21
+ Project.expects(:longest_project_name).returns 21
22
+ 3.5.with_label("label").should eql(" label 3.5")
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+ require 'pratt'
3
+
4
+ describe Numeric do
5
+ it { 1.should respond_to(:format_integer) }
6
+ it { 1.should respond_to(:with_label) }
7
+
8
+ it "should format correctly" do
9
+ Pratt.color = false
10
+ 1.format_integer.should eql("01")
11
+ end
12
+
13
+ it '"format" with with_label' do
14
+ Pratt.color = false
15
+ Project.expects(:longest_project_name).returns(11)
16
+ 1.with_label(:blue).should eql(" blue 1")
17
+ end
18
+
19
+ it "recursively applies formatting without color" do
20
+ Pratt.color = false
21
+ Project.expects(:longest_project_name).returns(11)
22
+ 1.format_integer.blue.with_label("#").should eql(" # 01")
23
+ end
24
+
25
+ it "recursively applies formatting with color" do
26
+ Pratt.color = true
27
+ Project.expects(:longest_project_name).returns(11)
28
+ 1.format_integer.blue.with_label("#").should eql(" # \e[34m01\e[0m")
29
+ end
30
+ end
@@ -55,8 +55,8 @@ describe Pratt do
55
55
 
56
56
  describe "\b#root" do
57
57
  before :each do
58
- Dir.stubs(:pwd).returns("/home/scott/git/pratt")
59
- @expected_root = "/home/scott/git/pratt"
58
+ @expected_root = File.dirname( File.expand_path( '..', __FILE__ ) ).to_s
59
+ Dir.stubs(:pwd).returns(@expected_root)
60
60
  end
61
61
 
62
62
  it "is correct without arguments" do
@@ -75,8 +75,9 @@ describe Pratt do
75
75
 
76
76
  it "is correct with an argument and block" do
77
77
  received = []
78
+ expected = %w(app.rb customer.rb project.rb payment.rb pratt.rb whence.rb invoice.rb invoice_whence.rb zip.rb)
78
79
  Pratt.root('models', '*.rb') {|model| received << model }
79
- received.to_set.should == %w(app.rb customer.rb project.rb payment.rb pratt.rb whence.rb).collect {|model| Pathname.new File.join(@expected_root, "models", model) }.to_set
80
+ received.to_set.should == expected.collect {|model| Pathname.new File.join(@expected_root, "models", model) }.to_set
80
81
  end
81
82
  end
82
83
 
@@ -125,7 +126,7 @@ describe Pratt do
125
126
 
126
127
  it "should look right with data" do
127
128
  populate_with_data
128
- # @pratt.expects(:process_template!)
129
+ @pratt.expects(:process_template!)
129
130
 
130
131
  @pratt.graph.should == get_expected_display
131
132
  end
@@ -10,6 +10,11 @@ end
10
10
  describe Project do
11
11
  it_should_behave_like "being a billable item"
12
12
 
13
+ it{ Project.should respond_to(:primary) }
14
+ it{ Project.should respond_to(:off) }
15
+ it{ Project.should respond_to(:rest) }
16
+ it{ Project.should respond_to(:longest_project_name) }
17
+
13
18
  context "scopes" do
14
19
  it "named primary" do
15
20
  Project.expects(:first).with(:conditions => { :weight => 1 }).returns(mock('Refactor'))
@@ -96,7 +101,7 @@ describe Project do
96
101
 
97
102
  context "amount calculation" do
98
103
  before :each do
99
- Payment.create :rate => '315', :billable => @project
104
+ Payment.create :rate => '315.0', :billable => @project
100
105
 
101
106
  @now = Time.parse("2009-10-04 17:53:58")
102
107
  Time.stubs(:now).returns(@now.beginning_of_week)
@@ -132,7 +137,7 @@ describe Project do
132
137
  end
133
138
 
134
139
  it "correctly calculates with no data" do
135
- Whence.expects(:find).with(:all, :conditions => ["end_at IS NOT NULL"]).returns []
140
+ @project.expects(:time_spent).returns 0
136
141
  @project.time_spent.should == 0.0
137
142
  end
138
143
 
@@ -160,4 +165,5 @@ describe Project do
160
165
  @project.time_spent('day', Chronic.parse('yesterday')).should == 18.0/60
161
166
  end
162
167
  end
168
+
163
169
  end
@@ -1 +1 @@
1
- color=true color: true color colour c --colour --color -c
1
+ --color
@@ -1,7 +1,9 @@
1
1
  # This file is copied to ~/spec when you run 'ruby script/generate rspec'
2
2
  # from the project root directory.
3
+ require 'rubygems'
3
4
  require 'spec'
4
- require 'config'
5
+ require 'mocha'
6
+ require 'lib/pratt'
5
7
  require 'ruby-debug'
6
8
 
7
9
  Spec::Runner.configure do |config|
@@ -9,6 +11,34 @@ Spec::Runner.configure do |config|
9
11
  config.mock_with :mocha
10
12
 
11
13
  Pratt.connect! 'test'
14
+
15
+ Customer.create(
16
+ :name => 'Scott Noel-Hemming',
17
+ :company_name => 'Frogstarr78 Software',
18
+ :address => '312 NW 7th',
19
+ :phone => '509.730.5401',
20
+ :zip => '97862'
21
+ ) unless Customer.count > 0
22
+
23
+ Project.create(
24
+ [
25
+ {
26
+ :name => 'Refactor',
27
+ :weight => 1,
28
+ :customer_id => 0
29
+ },
30
+ {
31
+ :name => 'Lunch/Break',
32
+ :weight => 0,
33
+ :customer_id => 0
34
+ },
35
+ {
36
+ :name => 'Other',
37
+ :weight => -1,
38
+ :customer_id => 0
39
+ }
40
+ ]
41
+ ) unless Project.count > 0
12
42
  end
13
43
 
14
44
  shared_examples_for "Time spent on a project" do
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'pratt'
3
+
4
+ describe String do
5
+ it { subject.should respond_to(:colorize) }
6
+ it { subject.should respond_to(:or) }
7
+ it { subject.should respond_to(:with_label) }
8
+
9
+ it "doesn't process alternate format when Pratt.color? == false" do
10
+ Pratt.color = false
11
+ "abc".or("xyz").should eql("abc")
12
+ end
13
+
14
+ it "processes alternate format when Pratt.color? == true" do
15
+ Pratt.color = true
16
+ "abc".or("xyz").should eql("xyz")
17
+ end
18
+
19
+ it "doesn't colorize when Pratt.color? == false" do
20
+ Pratt.color = false
21
+ "abc".blue.should eql("abc")
22
+ end
23
+
24
+ it "colorizes when Pratt.color? == true" do
25
+ Pratt.color = true
26
+ "abc".blue.should eql("\e[34mabc\e[0m")
27
+ end
28
+
29
+ it "handles with_label as expected" do
30
+ Project.expects(:longest_project_name).returns 11
31
+ "abc".with_label("label").should eql(" label abc")
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ projects: <%= @project_names.join ' ' %>
2
+ <% if @last_whence.end_at.nil? %>
3
+ started: <%= @last_whence.start_at.strftime(Pratt::FMT).send(:blue) %>
4
+ next prompt: <%= ( @time_til / 60.0 ).format_integer.yellow %> min <%= ( @time_til % 60 ).format_integer.yellow %> sec
5
+ <% end %>
@@ -0,0 +1,538 @@
1
+ <?xml version="1.0" encoding="UTF-8" ?>
2
+ <html xmlns="http://www.w3.org/1999/xhtml">
3
+ <!--This file was converted to xhtml by OpenOffice.org - see http://xml.openoffice.org/odf2xhtml for more info.-->
4
+ <head>
5
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6
+ <title xml:lang="en-US">Frogstarr78 Software Invoice</title>
7
+
8
+ <style type="text/css">
9
+ @page { }
10
+ body {
11
+ padding: 15px;
12
+ }
13
+ table {
14
+ border-collapse: collapse;
15
+ border-spacing: 0;
16
+ empty-cells: show
17
+ }
18
+ td, th {
19
+ vertical-align: top;
20
+ font-size: 10pt;
21
+ }
22
+ * {
23
+ margin: 0;
24
+ }
25
+ .Default {
26
+ font-family: Arial;
27
+ }
28
+ .ce1 {
29
+ color: #4CBB17;
30
+ font-family: sans-serif;
31
+ font-size: 16pt;
32
+ font-variant: small-caps;
33
+ }
34
+ .ce11 {
35
+ font-family: Arial;
36
+ border: 1px solid #666666;
37
+ border-top: none;
38
+ vertical-align: top;
39
+ text-align: left ! important;
40
+ margin-left: 0in;
41
+ }
42
+ .ce12 {
43
+ font-family: Arial;
44
+ border-style: none;
45
+ vertical-align: top;
46
+ text-align: center ! important;
47
+ margin-left: 0in;
48
+ font-size: 10pt;
49
+ font-style: normal;
50
+ text-shadow: none;
51
+ text-decoration: none ! important;
52
+ font-weight: normal;
53
+ }
54
+ .ce13 {
55
+ font-family: Arial;
56
+ border-style: none;
57
+ vertical-align: top;
58
+ text-align: center ! important;
59
+ margin-left: 0in;
60
+ font-size: 10pt;
61
+ font-style: normal;
62
+ text-shadow: none;
63
+ text-decoration: none ! important;
64
+ font-weight: normal;
65
+ }
66
+ .ce14 {
67
+ font-family: Trebuchet MS;
68
+ border-style: none;
69
+ vertical-align: top;
70
+ text-align: center ! important;
71
+ margin-left: 0in;
72
+ font-size: 12pt;
73
+ font-style: italic;
74
+ text-shadow: none;
75
+ text-decoration: none ! important;
76
+ font-weight: bold;
77
+ }
78
+ .ce15 {
79
+ font-family: Trebuchet MS;
80
+ border-style: none;
81
+ vertical-align: top;
82
+ text-align: right ! important;
83
+ margin-left: 0in;
84
+ color: #4CBB17;
85
+ font-size: 28pt;
86
+ font-style: normal;
87
+ text-shadow: none;
88
+ text-decoration: none ! important;
89
+ font-weight: bold;
90
+ }
91
+ .ce16 {
92
+ font-family: Arial;
93
+ border-style: none;
94
+ vertical-align: top;
95
+ text-align: center ! important;
96
+ margin-left: 0in;
97
+ }
98
+ .ce18 {
99
+ font-family: Arial;
100
+ border: none;
101
+ border-bottom: 1px solid #000000;
102
+ vertical-align: top;
103
+ text-align: center ! important;
104
+ margin-left: 0in;
105
+ }
106
+ .ce20 {
107
+ font-family: Trebuchet MS;
108
+ background-color: transparent;
109
+ vertical-align: top;
110
+ text-align: left ! important;
111
+ margin-left: 0in;
112
+ font-size: 10pt;
113
+ font-style: normal;
114
+ text-shadow: none;
115
+ text-decoration: none ! important;
116
+ font-weight: bold;
117
+ }
118
+ .ce21 {
119
+ font-family: Trebuchet MS;
120
+ font-size: 10pt;
121
+ font-style: normal;
122
+ text-shadow: none;
123
+ text-decoration: none ! important;
124
+ font-weight: bold;
125
+ }
126
+ .ce22 {
127
+ font-family: Arial;
128
+ border: none;
129
+ border-right: 1px solid #000000;
130
+ vertical-align: top;
131
+ text-align: center ! important;
132
+ margin-left: 0in;
133
+ }
134
+ .ce25 {
135
+ font-family: Arial;
136
+ border: none;
137
+ vertical-align: top;
138
+ text-align: center ! important;
139
+ margin-left: 0in;
140
+ font-size: 10pt;
141
+ font-style: normal;
142
+ text-shadow: none;
143
+ text-decoration: none ! important;
144
+ font-weight: normal;
145
+ }
146
+ .ce26 {
147
+ font-family: Trebuchet MS;
148
+ border-style: none;
149
+ vertical-align: top;
150
+ text-align: center ! important;
151
+ margin-left: 0in;
152
+ font-size: 10pt;
153
+ font-style: normal;
154
+ text-shadow: none;
155
+ text-decoration: none ! important;
156
+ font-weight: bold;
157
+ }
158
+ .ce28 {
159
+ font-size: 10pt;
160
+ font-style: normal;
161
+ text-shadow: none;
162
+ text-decoration: none ! important;
163
+ font-weight: normal;
164
+ }
165
+ .ce29 {
166
+ font-family: Arial;
167
+ border: 1px solid #000000;
168
+ vertical-align: top;
169
+ text-align: center ! important;
170
+ margin-left: 0in;
171
+ }
172
+ .ce3 {
173
+ font-family: Arial;
174
+ font-size: 10pt;
175
+ font-style: normal;
176
+ text-shadow: none;
177
+ text-decoration: none ! important;
178
+ font-weight: normal;
179
+ }
180
+ .ce31 {
181
+ font-family: Arial;
182
+ border-bottom: none;
183
+ border-left: 0.0879cm solid #000000;
184
+ border-right: 0.0879cm solid #000000;
185
+ border-top: none;
186
+ }
187
+ .ce32 {
188
+ font-family: Arial;
189
+ border: 0.0879cm solid #000000;
190
+ border-top: none;
191
+ }
192
+ .ce34 {
193
+ font-family: Arial;
194
+ border: none;
195
+ }
196
+ .ce35 {
197
+ font-family: Arial;
198
+ border: none;
199
+ border-bottom: 0.1049cm double #000000;
200
+ background-color: transparent;
201
+ }
202
+ .ce36 {
203
+ font-family: Trebuchet MS;
204
+ border-bottom: 0.1049cm double #000000;
205
+ background-color: transparent;
206
+ font-size: 10pt;
207
+ font-style: normal;
208
+ text-shadow: none;
209
+ text-decoration: none
210
+ }
211
+ .ce5 {
212
+ background-color: #4CBB17;
213
+ border: none;
214
+ color: #ffffff;
215
+ font-family: Trebuchet MS;
216
+ font-size: 10pt;
217
+ font-style: normal;
218
+ font-weight: bold;
219
+ margin-left: 0in;
220
+ text-align: center ! important;
221
+ text-decoration: none ! important;
222
+ text-shadow: none;
223
+ vertical-align: top;
224
+ }
225
+ .ce30 {
226
+ border: 0.0879cm solid #000000;
227
+ border-bottom: none;
228
+ }
229
+ .ceN {
230
+ vertical-align: top;
231
+ text-align: left ! important;
232
+ margin-left: 0in;
233
+ font-size: 10pt;
234
+ font-style: normal;
235
+ text-shadow: none;
236
+ text-decoration: none ! important;
237
+ font-weight: normal;
238
+ }
239
+ .ce6 {
240
+ padding-left: 7px;
241
+ font-family: Arial;
242
+ border-bottom: none;
243
+ border-left: 1px solid #000000;
244
+ border-right: 1px solid #000000;
245
+ border-top: none;
246
+
247
+ }
248
+ .ce8 {
249
+ font-family: Trebuchet MS;
250
+ border: none;
251
+ border-bottom: 1px solid #666666;
252
+ background-color: #c0c0c0;
253
+ font-weight: bold;
254
+ }
255
+ .ce10 {
256
+ font-family: Arial;
257
+ border: 1px solid #666666;
258
+ border-bottom: none;
259
+ border-top: none;
260
+ }
261
+ .ro1 {
262
+ height: 0.4736in;
263
+ }
264
+ .ro2 {
265
+ height: 0.1736in;
266
+ }
267
+ .ro3 {
268
+ height: 0.1807in;
269
+ }
270
+ .ro4 {
271
+ height: 0.2071in;
272
+ }
273
+
274
+ .TN {
275
+ font-style: normal;
276
+ text-shadow: none;
277
+ font-weight: normal;
278
+ text-decoration: none;
279
+ font-family: Trebuchet MS;
280
+ }
281
+
282
+ .T1 {
283
+ float: left;
284
+ font-size: 38pt;
285
+ text-decoration: underline;
286
+ }
287
+ .T2 {
288
+ font-size: 16pt;
289
+ }
290
+ .T3 {
291
+ font-family: Arial;
292
+ font-size: 10pt;
293
+ }
294
+ </style>
295
+
296
+ </head>
297
+
298
+ <body dir="ltr">
299
+ <table border="0" cellspacing="0" cellpadding="0" class="ta1">
300
+ <colgroup>
301
+ <col width="224"/>
302
+ <col width="181"/>
303
+ <col width="100"/>
304
+ <col width="100"/>
305
+ <col width="140"/>
306
+ </colgroup>
307
+
308
+ <tr class="ro1">
309
+ <td colspan="3" class="ce1">
310
+ <p>
311
+ <span style="float:left;">
312
+ <span style="float: left; height: 25px; padding-top: 11px;">
313
+ Frogstarr
314
+ </span>
315
+
316
+ <span style="float: left; clear: left;">
317
+ Software
318
+ </span>
319
+ </span>
320
+ <span class="TN T1">78</span>
321
+ </p>
322
+ </td>
323
+ <td colspan="2" class="ce15">
324
+ <p>INVOICE</p>
325
+ </td>
326
+ </tr>
327
+
328
+ <tr class="ro2">
329
+ <td colspan="2" class="ce3">
330
+ <p>312 NW 7<span class="TN T3">th</span></p>
331
+ </td>
332
+ <td class="Default"> </td>
333
+ <td class="ce20">
334
+ <p>DATE:</p>
335
+ </td>
336
+ <td style="text-align: right; background-color: #AAE09A;" class="ce29">
337
+ <p><%= when_to.send("beginning_of_#{scale}").strftime(Pratt::INVOICE_FMT) %> to <%= when_to.send("end_of_#{scale}").strftime(Pratt::INVOICE_FMT) %></p>
338
+ </td>
339
+ </tr>
340
+ <tr class="ro2">
341
+ <td colspan="2" class="ce3">
342
+ <p>Milton-Freewater, OR 97862</p>
343
+ </td>
344
+ <td class="Default"> </td>
345
+ <td class="ce20">
346
+ <p>INVOICE #</p>
347
+ </td>
348
+ <td class="ce29 ce28">
349
+ <p><%= project.customer.id %>-<%= when_to.week %>-01</p>
350
+ </td>
351
+ </tr>
352
+ <tr class="ro3">
353
+ <td colspan="2" class="ce3">
354
+ <p>Phone: 509-730-5401</p>
355
+ </td>
356
+ <td class="Default"> </td>
357
+ <td class="ce21">
358
+ <p>Customer ID</p>
359
+ </td>
360
+ <td style="text-align: right;" class="ce29">
361
+ <p><%= project.customer.id %></p>
362
+ </td>
363
+ </tr>
364
+
365
+ <tr class="ro2">
366
+ <td colspan="5" class="Default"> </td>
367
+ </tr>
368
+ <tr class="ro2">
369
+ <td colspan="5" class="Default"> </td>
370
+ </tr>
371
+
372
+ <tr class="ro2">
373
+ <td style="border-bottom: 1px solid #000000; text-align: left;" class="ce5">
374
+ <p>BILL TO:</p>
375
+ </td>
376
+ </tr>
377
+ <tr class="ro2">
378
+ <td class="ce3">
379
+ <p><%= project.customer.name %></p>
380
+ </td>
381
+ </tr>
382
+ <%
383
+ if project.customer.address
384
+ %>
385
+ <tr class="ro2">
386
+ <td class="ce3">
387
+ <p><%= project.customer.address %></p>
388
+ </td>
389
+ </tr>
390
+ <%
391
+ end
392
+
393
+ if project.customer.company_name
394
+ %>
395
+ <tr class="ro2">
396
+ <td class="ce3">
397
+ <p><%= project.customer.company_name %></p>
398
+ </td>
399
+ </tr>
400
+ <%
401
+ end
402
+
403
+ if project.customer.zip
404
+ %>
405
+ <tr class="ro2">
406
+ <td class="ce3">
407
+ <p><%= project.customer.city_state_zip %></p>
408
+ </td>
409
+ </tr>
410
+ <%
411
+ end
412
+
413
+ if project.customer.phone
414
+ %>
415
+ <tr class="ro2">
416
+ <td class="ce3">
417
+ <p><%= project.customer.phone.pretty_print %></p>
418
+ </td>
419
+ </tr>
420
+ <%
421
+ end
422
+ %>
423
+ <tr class="ro2">
424
+ <td class="Default"> </td>
425
+ </tr>
426
+ <tr class="ro2">
427
+ <td colspan="2" class="ce5">
428
+ <p>DESCRIPTION</p>
429
+ </td>
430
+ <td class="ce5">
431
+ <p>HOURS</p>
432
+ </td>
433
+ <td class="ce5">
434
+ <p>RATE</p>
435
+ </td>
436
+ <td class="ce5 ce30">
437
+ <p>AMOUNT</p>
438
+ </td>
439
+ </tr>
440
+
441
+ <% @projects.each do |project| %>
442
+ <tr class="ro2">
443
+ <td colspan="2" class="ceN ce6">
444
+ <p><%= project.name %></p>
445
+ </td>
446
+ <td style="text-align:right; " class="ce16">
447
+ <p><%= project.time_spent(scale, when_to).pretty_print %></p>
448
+ </td>
449
+ <td style="text-align:right; " class="ce22">
450
+ <p><%= project.payment.pretty_print %></p>
451
+ </td>
452
+ <td style="text-align:right; " class="ce31">
453
+ <p><%= project.amount(scale, when_to).pretty_print %></p>
454
+ </td>
455
+ </tr>
456
+ <% end %>
457
+ <tr class="ro2">
458
+ <td colspan="2" style="border: 1px solid black; border-top: none;"> </td>
459
+ <td class="ce18"> </td>
460
+ <td style="border-right: 1px solid #000000;" class="ce18"> </td>
461
+ <td class="ce32"> </td>
462
+ </tr>
463
+
464
+ <tr class="ro2">
465
+ <td colspan="2" class="Default"> </td>
466
+ <td class="Default"> </td>
467
+ <td class="Default ce35">
468
+ <!--p>SUBTOTAL</p-->
469
+ </td>
470
+ <td style="text-align:right; " class="ce36">
471
+ <!--p><%= project.amount(scale, when_to).pretty_print %></p-->
472
+ </td>
473
+ </tr>
474
+ <tr class="ro2">
475
+ <td colspan="2" class="ceN ce8">
476
+ <p>OTHER COMMENTS</p>
477
+ </td>
478
+ <td class="Default"> </td>
479
+ <td class="ce21">
480
+ <p>TOTAL</p>
481
+ </td>
482
+ <td style="text-align:right; " class="ce21 ce34">
483
+ <p><%= project.amount(scale, when_to).pretty_print %></p>
484
+ </td>
485
+ </tr>
486
+ <tr class="ro2">
487
+ <td colspan="2" class="ceN ce10">
488
+ <p>1. Total payment due in 30 days</p>
489
+ </td>
490
+ </tr>
491
+ <tr class="ro2">
492
+ <td colspan="2" class="ceN ce10">
493
+ <p>2. Please include the invoice number on your check</p>
494
+ </td>
495
+ <td class="Default"> </td>
496
+ <td colspan="2" class="ce25">
497
+ <p>Make all checks payable to</p>
498
+ </td>
499
+ </tr>
500
+
501
+ <tr class="ro2">
502
+ <td colspan="2" class="ceN ce10"> </td>
503
+ <td class="Default"> </td>
504
+ <td colspan="2" class="ce26">
505
+ <p>Frogstarr78 Software</p>
506
+ </td>
507
+ </tr>
508
+
509
+ <tr class="ro2">
510
+ <td colspan="2" class="ce11"> </td>
511
+ </tr>
512
+ <tr class="ro2">
513
+ <td colspan="5" class="Default"> </td>
514
+ </tr>
515
+
516
+ <tr class="ro2">
517
+ <td colspan="5" class="ce12">
518
+ <p>If you have any questions about this invoice, please contact</p>
519
+ </td>
520
+ </tr>
521
+ <tr class="ro3">
522
+ <td colspan="5" class="ce13">
523
+ <p>
524
+ Scott Noel-Hemming, 509.301.6893, <a href="mailto:frogstarr78@gmail.com">frogstarr78@gmail.com</a>
525
+ </p>
526
+ </td>
527
+ </tr>
528
+ <tr class="ro2">
529
+ <td colspan="5" class="Default"> </td>
530
+ </tr>
531
+ <tr class="ro4">
532
+ <td colspan="5" class="ce14">
533
+ <p>Thank You For Your Business!</p>
534
+ </td>
535
+ </tr>
536
+ </table>
537
+ </body>
538
+ </html>