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.
- data/.gitignore +2 -0
- data/Pratt.gemspec +38 -12
- data/README.html +85 -0
- data/README.txt +6 -3
- data/TODO +6 -3
- data/VERSION +1 -1
- data/config.rb +3 -17
- data/db/zips.csv.zip +0 -0
- data/lib/models.rb +8 -0
- data/lib/pratt.rb +58 -293
- data/lib/pratt.rb.orig +626 -0
- data/lib/pratt/core_ext.rb +6 -0
- data/lib/pratt/{array.rb → core_ext/array.rb} +5 -0
- data/lib/pratt/core_ext/float.rb +28 -0
- data/lib/pratt/core_ext/nil.rb +5 -0
- data/lib/pratt/core_ext/numeric.rb +9 -0
- data/lib/pratt/{string.rb → core_ext/string.rb} +13 -0
- data/lib/pratt/core_ext/time.rb +20 -0
- data/lib/pratt/dialogs.rb +81 -0
- data/lib/pratt/formatting.rb +24 -0
- data/lib/pratt/project_actions.rb +25 -0
- data/lib/pratt/reports.rb +127 -0
- data/models/app.rb +1 -2
- data/models/customer.rb +22 -0
- data/models/invoice.rb +28 -0
- data/models/invoice_whence.rb +18 -0
- data/models/payment.rb +1 -1
- data/models/pratt.rb +0 -9
- data/models/project.rb +16 -22
- data/models/whence.rb +10 -1
- data/models/zip.rb +27 -0
- data/spec/fixtures/graph.expectation +0 -5
- data/spec/fixtures/proportions.expectation +4 -0
- data/spec/float_spec.rb +24 -0
- data/spec/numeric_spec.rb +30 -0
- data/spec/pratt_spec.rb +5 -4
- data/spec/project_spec.rb +8 -2
- data/spec/spec.opts +1 -1
- data/spec/spec_helper.rb +31 -1
- data/spec/string_ext_spec.rb +33 -0
- data/views/current.eruby +5 -0
- data/views/general-invoice.eruby +538 -0
- data/views/graph.eruby +2 -7
- data/views/invoice.eruby +3 -3
- data/views/main.rb +8 -6
- data/views/proportions.eruby +4 -0
- data/views/raw.eruby +1 -1
- metadata +86 -33
- data/lib/pratt/time.rb +0 -12
- data/views/env.rb +0 -22
@@ -1,6 +1,11 @@
|
|
1
1
|
class Pratt
|
2
2
|
class Formats
|
3
3
|
module Array
|
4
|
+
##
|
5
|
+
# Turns an array into a sentence
|
6
|
+
#
|
7
|
+
# @param [String] conjunction
|
8
|
+
# @return [String]
|
4
9
|
def to_sentence conjunction = 'and'
|
5
10
|
self[0..-2].join(", ") << (self.size > 2 ? ',' : '') << " #{conjunction} #{self.last}"
|
6
11
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Float
|
2
|
+
def pretty_print
|
3
|
+
"%0.2f"% self
|
4
|
+
end
|
5
|
+
|
6
|
+
def percentage of_total = 1
|
7
|
+
( (self/of_total)*100.0 ).pretty_print << "%"
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_money
|
11
|
+
Money.new(self)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Money
|
16
|
+
include Comparable
|
17
|
+
|
18
|
+
def <=> other
|
19
|
+
@f <=> other
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize f
|
23
|
+
@f = Float(f)
|
24
|
+
end
|
25
|
+
def pretty_print
|
26
|
+
"$#{@f.pretty_print}"
|
27
|
+
end
|
28
|
+
end
|
@@ -16,3 +16,16 @@ end
|
|
16
16
|
|
17
17
|
String.send :include, Pratt::ColorString
|
18
18
|
String.send :include, Pratt::Colored
|
19
|
+
|
20
|
+
class String
|
21
|
+
def with_label label
|
22
|
+
"#{padded_to_max(label)} #{self}"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
# Pad the output string to the maximum Project name
|
27
|
+
def padded_to_max string
|
28
|
+
project_padding = Project.longest_project_name
|
29
|
+
"%#{project_padding}.#{project_padding}s"% string
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Date
|
2
|
+
##
|
3
|
+
# Return "zero hour" of the first day of the, possibly shifted, week.
|
4
|
+
#
|
5
|
+
# @return [Date]
|
6
|
+
def beginning_of_week
|
7
|
+
(self - wday_offset).beginning_of_day
|
8
|
+
end
|
9
|
+
|
10
|
+
##
|
11
|
+
# Return "last second" of the last day of the, possibly shifted, week.
|
12
|
+
#
|
13
|
+
# @return [Date]
|
14
|
+
def end_of_week
|
15
|
+
(beginning_of_week+6).end_of_day
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class DateTime < Date
|
20
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class Pratt
|
2
|
+
def daemonized?
|
3
|
+
!app.pid.blank? and ( cpid.to_i == app.pid )
|
4
|
+
end
|
5
|
+
|
6
|
+
def daemonize!
|
7
|
+
defork {
|
8
|
+
puts "pratt (#{Process.pid.to_s.yellow})"
|
9
|
+
app.pid = Process.pid
|
10
|
+
app.save!
|
11
|
+
|
12
|
+
gui
|
13
|
+
while(daemonized?)
|
14
|
+
sleep(app.interval)
|
15
|
+
gui
|
16
|
+
end
|
17
|
+
quit
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def gui
|
22
|
+
if Whence.last_unended
|
23
|
+
pop
|
24
|
+
else
|
25
|
+
main
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def detect
|
30
|
+
if self.daemonized?
|
31
|
+
gui
|
32
|
+
else
|
33
|
+
daemonize!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def main
|
39
|
+
reload_and_detect_lock 'main'
|
40
|
+
projects = ([Project.primary, Project.off] | Project.rest).collect(&:name)
|
41
|
+
if Whence.count == 0
|
42
|
+
# first run
|
43
|
+
project = Whence.new(:project => Project.new)
|
44
|
+
current_project_name = project.project.name
|
45
|
+
else
|
46
|
+
project = Whence.last_unended || Whence.last
|
47
|
+
current_project_name = ''
|
48
|
+
end
|
49
|
+
|
50
|
+
if Project.count > 0
|
51
|
+
projects = ([Project.primary, Project.off] | Project.rest).compact.collect(&:name)
|
52
|
+
else
|
53
|
+
projects = []
|
54
|
+
end
|
55
|
+
defork {
|
56
|
+
command = "ruby views/main.rb --projects '#{projects*"','"}' --current '#{current_project_name}'"
|
57
|
+
system command
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def pop
|
62
|
+
reload_and_detect_lock 'pop'
|
63
|
+
self.project = Whence.last_unended.project
|
64
|
+
defork do
|
65
|
+
command = "ruby views/pop.rb --project '#{project.name}' --start '#{project.whences.last_unended.start_at}' --project_time '#{Pratt.totals(project.time_spent)}'"
|
66
|
+
system command
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def reload_and_detect_lock of
|
71
|
+
self.app.reload
|
72
|
+
return if self.app.gui? of
|
73
|
+
self.app.log of
|
74
|
+
end
|
75
|
+
|
76
|
+
def defork &block
|
77
|
+
Process.detach(
|
78
|
+
fork &block
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Pratt
|
2
|
+
|
3
|
+
@@color = true
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def color
|
8
|
+
@@color
|
9
|
+
end
|
10
|
+
|
11
|
+
def color= color_bool
|
12
|
+
@@color = color_bool
|
13
|
+
end
|
14
|
+
|
15
|
+
def color?
|
16
|
+
@@color == true
|
17
|
+
end
|
18
|
+
|
19
|
+
# Calculate totals. I think this should be an instance method on Projects/?/Whences
|
20
|
+
def totals hr, fmt = false
|
21
|
+
"#{(hr / 24).format_integer.cyan} day #{(hr % 24).format_integer.yellow} hour #{(60*(hr -= hr.to_i)).format_integer.green} min"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Pratt
|
2
|
+
def begin
|
3
|
+
self.project.start! when_to
|
4
|
+
end
|
5
|
+
def restart
|
6
|
+
if project?
|
7
|
+
project.restart! when_to
|
8
|
+
else
|
9
|
+
Whence.last_unended.project.restart! when_to
|
10
|
+
end
|
11
|
+
end
|
12
|
+
def end
|
13
|
+
if project?
|
14
|
+
project.stop! when_to
|
15
|
+
else
|
16
|
+
Whence.last_unended.stop! when_to
|
17
|
+
end
|
18
|
+
end
|
19
|
+
def change
|
20
|
+
Whence.last.change! project.name
|
21
|
+
end
|
22
|
+
def destroy
|
23
|
+
project.destroy
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
class Pratt
|
2
|
+
|
3
|
+
# TODO Rename
|
4
|
+
def graph
|
5
|
+
self.template = 'graph'
|
6
|
+
|
7
|
+
if project?
|
8
|
+
@projects = [project]
|
9
|
+
else
|
10
|
+
@projects = Project.all
|
11
|
+
end
|
12
|
+
|
13
|
+
process_template!
|
14
|
+
end
|
15
|
+
|
16
|
+
def proportions
|
17
|
+
@primary = @off_total = @rest_total = 0.0
|
18
|
+
self.template = 'proportions'
|
19
|
+
|
20
|
+
if project?
|
21
|
+
@projects = [project]
|
22
|
+
|
23
|
+
@primary = project.time_spent(scale, when_to)
|
24
|
+
@scaled_total = project.whences.time_spent(scale, when_to)
|
25
|
+
else
|
26
|
+
@projects = Project.all
|
27
|
+
|
28
|
+
@projects.each do |proj|
|
29
|
+
@primary = proj.time_spent(scale, when_to) if proj.name == Project.primary.name
|
30
|
+
@off_total = proj.time_spent(scale, when_to) if proj.name == Project.off.name
|
31
|
+
@rest_total += proj.time_spent(scale, when_to) if Project.rest.collect(&:name).include?(proj.name)
|
32
|
+
end
|
33
|
+
@scaled_total = Whence.time_spent(scale, when_to)-@off_total
|
34
|
+
end
|
35
|
+
|
36
|
+
if @primary + @off_total + @rest_total > 0.0
|
37
|
+
process_template!
|
38
|
+
else
|
39
|
+
"No data to report"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Generate an invoice for a given time period
|
44
|
+
def invoice
|
45
|
+
self.template ||= 'invoice'
|
46
|
+
|
47
|
+
if project?
|
48
|
+
@projects = [project]
|
49
|
+
|
50
|
+
@total = project.amount(scale, when_to)
|
51
|
+
else
|
52
|
+
@projects = (Project.all - [Project.primary, Project.off])
|
53
|
+
@projects.select! {|proj| show_all or ( !show_all and proj.time_spent(scale, when_to) != 0.0 ) }
|
54
|
+
|
55
|
+
@total = @projects.inject 0.0 do |total, proj|
|
56
|
+
total += proj.amount(scale, when_to)
|
57
|
+
total
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
@projects.each do |project|
|
62
|
+
puts "<!-- #{project.name} #{project.payment.inspect} -->"
|
63
|
+
end
|
64
|
+
if @total > 0.0
|
65
|
+
process_template!
|
66
|
+
else
|
67
|
+
puts "No data to report"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def current
|
72
|
+
self.template = 'current'
|
73
|
+
@last_whence = Whence.last_unended || Whence.last || Whence.new( :end_at => nil, :project => Project.new )
|
74
|
+
|
75
|
+
projects = ([Project.primary, Project.off] | Project.rest).compact
|
76
|
+
@project_names = projects.collect do |proj|
|
77
|
+
|
78
|
+
if @last_whence.end_at.nil? && @last_whence.project.name == proj.name
|
79
|
+
colored_name = proj.name.green
|
80
|
+
else
|
81
|
+
colored_name = proj.name.magenta
|
82
|
+
end
|
83
|
+
"'" << colored_name << "'"
|
84
|
+
end
|
85
|
+
@time_til = ( app.interval - ( Time.now - @last_whence.start_at ) ) if @last_whence.end_at.nil?
|
86
|
+
|
87
|
+
process_template!
|
88
|
+
end
|
89
|
+
|
90
|
+
def raw
|
91
|
+
self.template = 'raw'
|
92
|
+
|
93
|
+
if project?
|
94
|
+
@whences = project.whences.all
|
95
|
+
else
|
96
|
+
case raw_conditions
|
97
|
+
when 'all'
|
98
|
+
@whences = Whence.find raw_conditions.to_sym
|
99
|
+
when /^last$/, 'first'
|
100
|
+
@whences = [Whence.find raw_conditions.to_sym]
|
101
|
+
when /last[\(\s]?(\d+)[\)\s]?/
|
102
|
+
@whences = Whence.all.last($1.to_i)
|
103
|
+
else
|
104
|
+
@whences = Whence.all :conditions => ["start_at > ?", Chronic.parse("today 00:00")]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
@whences.sort_by(&:id)
|
108
|
+
process_template!
|
109
|
+
end
|
110
|
+
|
111
|
+
def cpid
|
112
|
+
`pgrep -fl 'bin/pratt'`.chomp.split(' ').first
|
113
|
+
end
|
114
|
+
|
115
|
+
def pid
|
116
|
+
self.template = 'pid'
|
117
|
+
process_template!
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
def process_template!
|
122
|
+
input = File.open(Pratt.root("views", "#{template}.eruby").first).read
|
123
|
+
erubis = Erubis::Eruby.new(input)
|
124
|
+
puts erubis.evaluate(self)
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
data/models/app.rb
CHANGED
data/models/customer.rb
CHANGED
@@ -1,10 +1,29 @@
|
|
1
1
|
class Customer < ActiveRecord::Base
|
2
2
|
has_many :projects
|
3
|
+
has_many :invoices
|
3
4
|
has_one :payment, :as => :billable
|
4
5
|
|
6
|
+
validates_presence_of :name
|
7
|
+
|
5
8
|
def amount
|
6
9
|
payment.rate / 100.0
|
7
10
|
end
|
11
|
+
|
12
|
+
def phone
|
13
|
+
phone = read_attribute(:phone)
|
14
|
+
class << phone
|
15
|
+
def pretty_print sep = '.'
|
16
|
+
self.split(/(\d{3})(\d{3})(\d{4})/)[1,3] * sep
|
17
|
+
end
|
18
|
+
end
|
19
|
+
phone
|
20
|
+
end
|
21
|
+
|
22
|
+
def city_state_zip
|
23
|
+
a_zip = Zip.find_by_zip(self.zip)
|
24
|
+
return "#{a_zip.city}, #{a_zip.ST} #{a_zip.zip}" if a_zip
|
25
|
+
return self.zip
|
26
|
+
end
|
8
27
|
|
9
28
|
class << self
|
10
29
|
def migrate which = :up
|
@@ -12,7 +31,10 @@ class Customer < ActiveRecord::Base
|
|
12
31
|
if which == :up
|
13
32
|
create_table :customers do |t|
|
14
33
|
t.string :name, :null => false
|
34
|
+
t.string :company_name
|
15
35
|
t.string :address
|
36
|
+
t.string :phone
|
37
|
+
t.string :email
|
16
38
|
t.string :zip, :limit => 5
|
17
39
|
end
|
18
40
|
elsif which == :down
|
data/models/invoice.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
class Invoice < ActiveRecord::Base
|
2
|
+
belongs_to :customer
|
3
|
+
has_many :invoice_whences
|
4
|
+
has_many :whences, :through => :invoice_whences
|
5
|
+
|
6
|
+
validates_presence_of :customer_id, :weekday_start
|
7
|
+
validates_format_of :invoice_number, :with => /(0?[1-9]+[0-9]*)-([0-4][0-9]|5[0-4])-(0?[1-9]+[0-9]*)/, :on => :create
|
8
|
+
|
9
|
+
def before_validation_on_create
|
10
|
+
self.invoice_number = "%s-%s-%s"% [customer.id, weekday_start.week, customer.invoices.count+1 ]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.migrate which = :up
|
14
|
+
ActiveRecord::Schema.define do
|
15
|
+
if which == :up
|
16
|
+
create_table :invoices do |t|
|
17
|
+
t.references :customer
|
18
|
+
t.datetime :weekday_start
|
19
|
+
t.datetime :weekday_end
|
20
|
+
t.string 'invoice_number'
|
21
|
+
t.integer :total
|
22
|
+
end
|
23
|
+
elsif which == :down
|
24
|
+
drop_table :invoices
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|