big_bucks_no_whammies 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Jim Jones
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,88 @@
1
+ = Description
2
+ Big Bucks No Whammies (BBNW) will download your time usage from {Timesink}[http://manytricks.com/timesink/], Chrometa, or any other service/app that tracks application time usage, cross reference it with your commits to a git repository, and generate an invoice. This is useful for developers that are independent contractors.
3
+
4
+ If you don't have time usage statistics, BBNW can estimate how much each commit took by looking at the number of source changes per commit.
5
+
6
+ == Contributing to big_bucks_no_whammies
7
+ I'm assuming that you're a programmer. You're working for a client making the big bucks. You spend a ton of time in your favorite editor or maybe there's a couple of terminal sessions open.
8
+
9
+ So, I assume that you're tracking all of your activity on the computer. I'm currently using {Timesink.}[http://manytricks.com/timesink/]
10
+
11
+ With all of this hard work, eventually you commit these changes to a repository. I personally use git.
12
+
13
+ BBNW will consult {Timesink}[http://manytricks.com/timesink/] / Chrometa / etc, gather all of the time that you've accumulated using your favorite development applications, and then it will cross reference that activity with your git commits.
14
+ Finally, it will generate an invoice listing for each commit you've made along with the time used for each issue.
15
+
16
+ BBNW makes the assumption that all of the work done before the commit was specifically for that commit.
17
+
18
+ E.g.
19
+
20
+ Timesink csv report :
21
+
22
+ Pool,Application,Window,Foreground Begin (Unix Time),Foreground End (Unix Time)
23
+ ,Finder,,1305084898.21,1305084902.27
24
+ ,Firefox,"Screen_shot_2011-05-10_at_11.42.19_AM.png (PNG Image, 405x336 pixels)",1305084938.45,1305084939.47
25
+ ,Firefox,localhost,1305084943.00,1305084946.04
26
+ ,Microsoft Outlook,,1305084946.55,1305085092.49
27
+ ,Microsoft Outlook,Inbox • MyCompany,1305084946.55,1305085092.49
28
+ ,RubyMine,,1305085092.99,1305085279.06
29
+ ,RubyMine,myproject - [/Users/jimjones/dev/myproject] - .../db/seeds.rb - JetBrains RubyMine 3.1.1,1305085092.99,1305085097.04
30
+ ,RubyMine,myproject - [/Users/jimjones/dev/myproject] - .../app/views/mailers/_form.html.haml - JetBrains RubyMine 3.1.1,1305085097.55,1305085100.09
31
+ ,RubyMine,myproject - [/Users/jimjones/dev/myproject] - .../app/views/mailers/edit.html.haml - JetBrains RubyMine 3.1.1,1305085100.59,1305085279.06
32
+ ,Microsoft Outlook,,1305085279.56,1305085289.21
33
+
34
+ And then there's a commit in your git repository with the following :
35
+
36
+ commit 1b132d58af90af8c65738f9189eb7c8d7d29173c
37
+ Author: Jim Jones <jjones@aantix.com>
38
+ Date: Sun Apr 5 09:17:51 2011 -0700
39
+
40
+ Added facial recognition. No big deal.
41
+
42
+
43
+ In your invoice, you would see the following line item :
44
+
45
+ Description Unit Price Quantity Amount
46
+ Added facial recognition. No big deal. $100 1.5 hours $150
47
+
48
+ = Meetings, Activities, Phonecalls, Anything that isn't recorded by Timesink
49
+ You can programmatically adds meetings/tasks to your invoice.
50
+
51
+ invoice.meeting("Progress Review", "#{meeting_date.strftime("%d/%m/%Y")} 8:30:00","#{meeting_date.strftime("%d/%m/%Y")} 9:30:00")
52
+ invoice.task("Answered support phone call", "04/05/2011 8:30:00","04/05/2011 8:37:00")
53
+
54
+
55
+ = Extending BBNW
56
+ For activity data, BBNW supports Chrometa, TimeSink and a generic CSV adapter. There's a Git repository adapater. But if you use another time tracker (e.g. Time Sink, Freshbooks, Freckle, etc), nothing would prevent you from writing your own activity adapter.
57
+
58
+
59
+ == Sample Usages
60
+ If you want to cross reference your recorded activities with your Git commits, do the following :
61
+
62
+ company = ['123 Easy St', 'San Francisco, CA 94132','','(505) 555-5575','jjones@aantix.com']
63
+ bill_to = ['The Software Corp','100 Microsoft Drive, Suite 150','San Francisco, CA 94105']
64
+
65
+ start_date = Date.today - 365
66
+ end_date = Date.today
67
+
68
+ activities = BBNW::TimeSink.new('~/Dropbox/time_reports/2011-07-29.csv', start_date, end_date, true)
69
+ #activities = BBNW::CsvFile.new('~/Dropbox/time_sink_report.csv', [1,3,4], true)
70
+ #activities = BBNW::Chrometa.new('jjones@example.com', '643e011b0532dfD1gjhuytyu0bc96f4058rffa', nil, nil, start_date, end_date, true)
71
+ git = BBNW::GitRepos.new('~/dev/turkee', nil, start_date, end_date, true)
72
+
73
+ invoice = BBNW::Invoice.new
74
+ invoice.line_items(activities, git, BBNW::RecordedActivity, false) # No grouping of line items
75
+ invoice.generate(invoice_num, invoice_path, company_logo, company_name, company_details, rate_per_hour)
76
+
77
+ Or if you don't have your activities recorded and would just like to generate a line itemed invoice from Git commits with the time estimated for each commit you can do the following :
78
+
79
+ git = BBNW::GitRepos.new('~/Projects/turkee', nil, start_date, end_date, true)
80
+ invoice = BBNW::Invoice.new
81
+ invoice.line_items(80, git, BBNW::NumberOfSourceCommits)
82
+ invoice.generate(invoice_num, './logo.png', 'Jim Jones', company, bill_to, 250, start_date, end_date)
83
+
84
+ == Copyright
85
+
86
+ Copyright (c) 2011 Jim Jones. See LICENSE.txt for
87
+ further details.
88
+
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
9
+ gem.name = "big_bucks_no_whammies"
10
+ gem.homepage = "http://github.com/aantix/big_bucks_no_whammies"
11
+ gem.license = "MIT"
12
+ gem.summary = %Q{Big Bucks No Whammies allows software development contractors to generate a line-item invoice based on git commits. Activity}
13
+ gem.description = %Q{Auto-generate a pdf invoice utilizing your git commit activity. Time is automatically accounted for utilizing TimeSink and is proportionately applied towards each line item entry. }
14
+ gem.email = "jjones@aantix.com"
15
+ gem.authors = ["Jim Jones"]
16
+ gem.require_paths = ["lib","lib/adapters","lib/adapters/base","lib/adapters/time_estimators","lib/adapters/utils"]
17
+ gem.add_dependency(%q<grit>, [">= 2.4.1"])
18
+ gem.add_dependency(%q<payday>, [">= 1.0.2"])
19
+ gem.add_dependency(%q<jeweler>, [">= 1.6.0"])
20
+ gem.add_dependency(%<rspec>, [">= 2.3.0"])
21
+ # dependencies defined in Gemfile
22
+ end
23
+ Jeweler::GemcutterTasks.new
24
+
25
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,80 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{big_bucks_no_whammies}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = [%q{Jim Jones}]
12
+ s.date = %q{2011-08-22}
13
+ s.description = %q{Auto-generate a pdf invoice utilizing your git commit activity. Time is automatically accounted for utilizing TimeSink and is proportionately applied towards each line item entry. }
14
+ s.email = %q{jjones@aantix.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rspec",
22
+ "Gemfile",
23
+ "LICENSE.txt",
24
+ "README.rdoc",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "big_bucks_no_whammies.gemspec",
28
+ "lib/adapters/base/activities.rb",
29
+ "lib/adapters/base/activity.rb",
30
+ "lib/adapters/base/billable.rb",
31
+ "lib/adapters/base/commit.rb",
32
+ "lib/adapters/base/commits.rb",
33
+ "lib/adapters/base/meeting.rb",
34
+ "lib/adapters/chrometa.rb",
35
+ "lib/adapters/csv_file.rb",
36
+ "lib/adapters/git.rb",
37
+ "lib/adapters/time_estimators/number_of_source_commits.rb",
38
+ "lib/adapters/time_estimators/number_of_sources_changes.rb",
39
+ "lib/adapters/time_estimators/recorded_activity.rb",
40
+ "lib/adapters/time_sink.rb",
41
+ "lib/big_bucks_no_whammies.rb",
42
+ "lib/utils/date_time_utils.rb",
43
+ "lib/utils/invoice_display_utils.rb",
44
+ "lib/utils/line_item_utils.rb",
45
+ "sample_report.pdf",
46
+ "spec/big_bucks_no_whammies_spec.rb",
47
+ "spec/csv_file_spec.rb",
48
+ "spec/spec_helper.rb"
49
+ ]
50
+ s.homepage = %q{http://github.com/aantix/big_bucks_no_whammies}
51
+ s.licenses = [%q{MIT}]
52
+ s.require_paths = [%q{lib}, %q{lib/adapters}, %q{lib/adapters/base}, %q{lib/adapters/time_estimators}, %q{lib/adapters/utils}]
53
+ s.rubygems_version = %q{1.8.5}
54
+ s.summary = %q{Big Bucks No Whammies allows software development contractors to generate a line-item invoice based on git commits. Activity}
55
+
56
+ if s.respond_to? :specification_version then
57
+ s.specification_version = 3
58
+
59
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
60
+ s.add_runtime_dependency(%q<big_bucks_no_whammies>, [">= 0"])
61
+ s.add_runtime_dependency(%q<grit>, [">= 2.4.1"])
62
+ s.add_runtime_dependency(%q<payday>, [">= 1.0.2"])
63
+ s.add_runtime_dependency(%q<jeweler>, [">= 1.6.0"])
64
+ s.add_runtime_dependency(%q<rspec>, [">= 2.3.0"])
65
+ else
66
+ s.add_dependency(%q<big_bucks_no_whammies>, [">= 0"])
67
+ s.add_dependency(%q<grit>, [">= 2.4.1"])
68
+ s.add_dependency(%q<payday>, [">= 1.0.2"])
69
+ s.add_dependency(%q<jeweler>, [">= 1.6.0"])
70
+ s.add_dependency(%q<rspec>, [">= 2.3.0"])
71
+ end
72
+ else
73
+ s.add_dependency(%q<big_bucks_no_whammies>, [">= 0"])
74
+ s.add_dependency(%q<grit>, [">= 2.4.1"])
75
+ s.add_dependency(%q<payday>, [">= 1.0.2"])
76
+ s.add_dependency(%q<jeweler>, [">= 1.6.0"])
77
+ s.add_dependency(%q<rspec>, [">= 2.3.0"])
78
+ end
79
+ end
80
+
@@ -0,0 +1,9 @@
1
+ module BBNW
2
+ module Activities
3
+ attr_accessor :activities
4
+
5
+ def activities
6
+ @activities||=get_activities
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ require 'utils/invoice_display_utils'
2
+
3
+ module BBNW
4
+ class Activity
5
+ include BBNW::InvoiceDisplayUtils
6
+
7
+ attr_accessor :activity, :start_time, :end_time, :billable
8
+
9
+ def initialize(activity, start_time, end_time, billable = true)
10
+ @activity = activity
11
+ @start_time = start_time
12
+ @end_time = end_time
13
+ @billable = billable
14
+ end
15
+
16
+ def total_seconds
17
+ (@end_time - @start_time) rescue 0
18
+ end
19
+
20
+ def self.roundup(time_in_seconds, nearest = 15) # nearest 15 minutes/900 seconds
21
+ nearest_in_seconds = nearest * 60
22
+ time_in_seconds % nearest_in_seconds == 0 ? time_in_seconds : time_in_seconds + nearest_in_seconds - (time_in_seconds % nearest_in_seconds)
23
+ end
24
+
25
+ def self.rounddown(time_in_seconds, nearest = 15) # nearest 15 minutes/900 seconds
26
+ nearest_in_seconds = nearest * 60
27
+ time_in_seconds % nearest_in_seconds == 0 ? time_in_seconds : time_in_seconds - (time_in_seconds % nearest_in_seconds)
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ module BBNW
2
+ class Billable
3
+ attr_accessor :descriptions, :total_time
4
+
5
+ def initialize
6
+ @descriptions = []
7
+ @total_time = 0
8
+ end
9
+
10
+ def all_descriptions(header = "")
11
+ descriptions = "#{header}\n\n"
12
+ descriptions+= @descriptions.join("\n\n")
13
+ descriptions
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ require 'time'
2
+
3
+ module BBNW
4
+ class Commit
5
+ attr_reader :commit_id, :message, :committer, :timestamp, :total_changes
6
+
7
+ def initialize(commit_id, message, committer, timestamp, total_changes = 0)
8
+ @commit_id = commit_id
9
+ @message = message
10
+ @committer = committer
11
+ @timestamp = timestamp.is_a?(String) ? Time.parse(timestamp) : timestamp
12
+ @total_changes = total_changes
13
+ end
14
+
15
+ def start_time
16
+ @timestamp
17
+ end
18
+ def end_time
19
+ @timestamp
20
+ end
21
+ def activity
22
+ @message
23
+ end
24
+ # Looks for an integer > 5 chars as the task_id
25
+ def task_id
26
+ if message =~ /(\d{6,})/
27
+ return $1.to_i
28
+ end
29
+ nil
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module BBNW
2
+ module Commits
3
+ attr_accessor :commits
4
+
5
+ def self.included(base)
6
+ @commits = []
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ require 'time'
2
+
3
+ module BBNW
4
+ class Meeting
5
+ attr_reader :message, :start_time, :end_time
6
+
7
+ def initialize(message, start_time, end_time, account_for = true)
8
+ @message = message
9
+ @start_time = start_time
10
+ @end_time = end_time
11
+ @account_for = account_for
12
+ end
13
+
14
+ def elapsed
15
+ @account_for ? (@end_time - @start_time) : 0
16
+ end
17
+
18
+ def total_seconds
19
+ elapsed
20
+ end
21
+
22
+ def timestamp
23
+ start_time
24
+ end
25
+
26
+ def task_id
27
+ nil
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,98 @@
1
+ require "uri"
2
+ require 'cgi'
3
+ require "net/https"
4
+ require "rexml/document"
5
+ require 'base/activities'
6
+ require 'base/activity'
7
+ require 'date'
8
+
9
+ module BBNW
10
+
11
+ class Chrometa
12
+ include BBNW::Activities
13
+
14
+ def initialize(email, token, clients, projects, start_date, end_date, debug = false)
15
+ @email = email
16
+ @token = token
17
+ @start_date = start_date.is_a?(String) ? Date.parse(start_date) : start_date
18
+ @end_date = end_date.is_a?(String) ? Date.parse(end_date) : end_date
19
+ @debug = debug
20
+
21
+ @clients = clients.is_a?(String) ? [clients] : clients
22
+ @projects = projects.is_a?(String) ? [projects] : projects
23
+
24
+ end
25
+
26
+ def log(message)
27
+ puts message if @debug
28
+ end
29
+
30
+ def get_activities
31
+ activities = []
32
+ current_date = @start_date
33
+ while current_date <= @end_date
34
+ url = URI.parse("https://api.chrometa.com/api/report")
35
+ params = {"date" => current_date.strftime("%m/%d/%Y")}
36
+ req = Net::HTTP::Post.new(url.path)
37
+ req.basic_auth @email, @token
38
+ req.set_form_data(params)
39
+ http = Net::HTTP.new(url.host, url.port)
40
+ http.use_ssl = true
41
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
42
+ resp = http.start { |http| http.request(req) }
43
+
44
+ body = resp.body.clone
45
+
46
+ xml = REXML::Document.new(body)
47
+ log "==================="
48
+ log "date : #{current_date}"
49
+ log "Body : #{body}"
50
+ log "==================="
51
+
52
+ root = xml.elements["chrometa-info"]
53
+
54
+ log "Email : #{root.attributes["email"]}"
55
+ log "Date : #{root.attributes["date"]}"
56
+
57
+ root.elements.each do |client|
58
+ log " Client Name : #{client.attributes["name"]}"
59
+
60
+ next unless (@clients.blank? || @clients.include?(client.attributes["name"]))
61
+
62
+ client.elements.each do |project|
63
+ log " Project Name : #{project.attributes["name"]}"
64
+ log " Project Rate : #{project.attributes["rate"]}"
65
+
66
+ next unless (@projects.blank? || @projects.include?(project.attributes["name"]))
67
+
68
+ project.elements.each do |time_entry|
69
+ start_time = Time.parse(time_entry.attributes["timestamp"])
70
+ end_time = start_time + time_entry.attributes["seconds"].to_i
71
+
72
+ activity_entry = BBNW::Activity.new(time_entry.attributes["activity"],
73
+ start_time, end_time)
74
+ activities << activity_entry
75
+ log " Application : #{time_entry.attributes['application']}"
76
+ log " Timestamp : #{time_entry.attributes['timestamp']}"
77
+ log " Activity : #{time_entry.attributes['activity']}"
78
+ log " URL : #{time_entry.attributes['url']}"
79
+ log " Seconds : #{time_entry.attributes['seconds']}"
80
+ log " Billable : #{time_entry.attributes['billable']}"
81
+ end
82
+
83
+ end
84
+ end
85
+ current_date+=1
86
+ end
87
+
88
+ File.open('chrometa.csv', 'w') do |f|
89
+ activities.each do |activity|
90
+ f.puts("#{CGI.escapeHTML(activity.activity)},#{activity.start_time},#{activity.end_time}")
91
+ end
92
+ end
93
+
94
+ activities
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'base/activities'
3
+ require 'base/activity'
4
+ require 'utils/date_time_utils'
5
+ require 'csv'
6
+
7
+ module BBNW
8
+
9
+ class CsvFile
10
+ include BBNW::Activities
11
+ include BBNW::DateTimeUtils
12
+
13
+ def initialize(file_path, columns = [0,1,2], debug = false)
14
+ @files = Dir.glob(file_path)
15
+ @columns = columns
16
+ end
17
+
18
+ def log(message)
19
+ puts message if @debug
20
+ end
21
+
22
+ def get_activities
23
+ activities = []
24
+ blank_time = Time.at(0.0)
25
+
26
+ @files.each do |file|
27
+ csv = CSV::parse(File.open(file) { |f| f.read })
28
+ headers = csv.shift
29
+
30
+ csv.each_with_index do |row, i|
31
+ activity = row[@columns[0]]
32
+ s = row[@columns[1]]
33
+ e = row[@columns[2]]
34
+
35
+ start_time = parse_time(s)
36
+ next if start_time == blank_time
37
+
38
+ end_time = parse_time(e)
39
+ next if end_time == blank_time
40
+
41
+ time_entry = BBNW::Activity.new(activity, start_time, end_time)
42
+ activities << time_entry
43
+ end
44
+ end
45
+ activities
46
+ end
47
+ end
48
+
49
+ end
@@ -0,0 +1,51 @@
1
+ require 'grit'
2
+ require 'base/commit'
3
+ require 'base/commits'
4
+ require 'cgi'
5
+
6
+ module BBNW
7
+ class GitRepos
8
+ include BBNW::Commits
9
+
10
+ attr_accessor :commits
11
+
12
+ def initialize(working_dir, committer = nil, start_date = nil, end_date = nil, debug = false)
13
+ @git = Grit::Repo.new(working_dir)
14
+
15
+ @committer = committer
16
+ @start_date = start_date
17
+ @end_date = end_date
18
+ end
19
+
20
+ def commits
21
+ @commits||=get_commits
22
+ end
23
+
24
+ def total_changes(commit)
25
+ total = 0
26
+ stats = commit.stats
27
+ stats.files.each do |f|
28
+ total+=f[3]
29
+ end
30
+ total
31
+ end
32
+
33
+ def get_commits
34
+ user_commits = []
35
+ commits = @git.commits_since('master',@start_date.strftime("%Y-%m-%d"))
36
+ start_date = Time.new(@start_date.year, @start_date.month, @start_date.day, 0, 0, 0)
37
+ end_date = Time.new(@end_date.year, @end_date.month, @end_date.day, 23, 59, 59)
38
+
39
+ commits.each do |commit|
40
+ if (commit.author.to_s =~ /#{@committer}/i || @committer.nil?) &&
41
+ (start_date.nil? || commit.committed_date >= start_date) &&
42
+ (end_date.nil? || commit.committed_date <= end_date)
43
+ user_commits << BBNW::Commit.new(commit.id, commit.message, commit.committer.to_s, commit.committed_date, total_changes(commit))
44
+ end
45
+ end
46
+
47
+ user_commits
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,34 @@
1
+ require 'utils/invoice_display_utils'
2
+
3
+ module BBNW
4
+ class NumberOfSourceCommits
5
+ extend BBNW::InvoiceDisplayUtils
6
+
7
+ def self.line_items(activities_span, repository_adapter, meetings = {})
8
+ line_items = {}
9
+ task_ids_commits = {}
10
+
11
+ activities_span_in_seconds = activities_span * 3600
12
+ meetings.each {|m, time_elapsed| activities_span_in_seconds-=time_elapsed}
13
+
14
+ commits = repository_adapter.commits
15
+ return line_items if commits.size == 0
16
+
17
+ commits.each do |commit|
18
+ task_id = commit.task_id
19
+ task_id = short_date(commit.timestamp) if task_id.nil?
20
+ task_ids_commits[task_id]||=[]
21
+ task_ids_commits[task_id] << commit
22
+ end
23
+
24
+ fraction_of_work = activities_span_in_seconds / commits.size
25
+
26
+ task_ids_commits.each do |task_id, task_commits|
27
+ task_commits.each {|tc| line_items[tc] = fraction_of_work }
28
+ end
29
+
30
+ line_items
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ require 'utils/invoice_display_utils'
2
+
3
+ module BBNW
4
+ class NumberOfSourceChanges
5
+ extend BBNW::InvoiceDisplayUtils
6
+
7
+ def self.line_items(activities_span, repository_adapter, meetings = {})
8
+ line_items = {}
9
+ task_ids_commits = {}
10
+
11
+ activities_span_in_seconds = activities_span * 3600
12
+
13
+ commits = repository_adapter.commits
14
+ return line_items if commits.size == 0
15
+
16
+ changes_count = 0
17
+ meetings.each {|m, time_elapsed| activities_span_in_seconds-=time_elapsed}
18
+
19
+ commits.each do |commit|
20
+ task_id = commit.task_id
21
+ task_id = short_date(commit.timestamp) if task_id.nil?
22
+ task_ids_commits[task_id]||=[]
23
+ task_ids_commits[task_id] << commit
24
+ changes_count+=commit.total_changes
25
+ end
26
+
27
+ meetings.each {|m,duration| activities_span_in_seconds-=duration }
28
+
29
+ fraction_of_work = activities_span_in_seconds / changes_count
30
+
31
+ task_ids_commits.each do |task_id, task_commits|
32
+ task_commits.each {|tc| line_items[tc] = (fraction_of_work * tc.total_changes)}
33
+ end
34
+
35
+ line_items
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,138 @@
1
+ require 'utils/invoice_display_utils'
2
+
3
+ module BBNW
4
+ class RecordedActivity
5
+ extend BBNW::InvoiceDisplayUtils
6
+
7
+ def self.line_items(activities_span, repository_adapter, meetings = {})
8
+ line_items = {}
9
+ activities = activities_span.activities
10
+ commits = repository_adapter.commits
11
+
12
+ all_activity = activities.concat(commits)
13
+ all_activity.sort! { |a, b| a.end_time <=> b.end_time }
14
+
15
+ activity_block = []
16
+ current_time = all_activity.first.end_time
17
+
18
+ all_activity.each_with_index do |activity, index|
19
+ if day_changed?(activity, current_time)
20
+ record_daily_activity(activity_block, line_items, all_activity, current_time, index)
21
+ current_time = activity.end_time
22
+ end
23
+
24
+ if activity.is_a?(BBNW::Activity)
25
+ activity_block << activity
26
+ elsif activity.is_a?(BBNW::Commit)
27
+ record_activities(activity_block, line_items, activity)
28
+ end
29
+
30
+ debug_output(activity)
31
+ end
32
+
33
+ line_items
34
+ end
35
+
36
+ private
37
+ def self.record_daily_activity(activity_block, line_items, all_activity, current_time, index)
38
+ message = ""
39
+ commit_id = ""
40
+ committer = nil
41
+
42
+ if activity_block.size == 0
43
+ message = "No activity."
44
+ else
45
+ upcoming_commit = upcoming_commit(all_activity, index)
46
+ if upcoming_commit.nil?
47
+ message = "Work in progress.."
48
+ else
49
+ task_id = upcoming_commit.task_id
50
+ commit_id = upcoming_commit.commit_id
51
+
52
+ message = "Work towards story # #{task_id} (#{upcoming_commit.commit_id})" unless task_id.nil?
53
+ message = "Work towards commit #{upcoming_commit.commit_id}" if task_id.nil?
54
+ committer = upcoming_commit.committer
55
+ end
56
+ end
57
+ daily_incomplete_commit = BBNW::Commit.new(commit_id, message, committer, current_time)
58
+
59
+ line_items[daily_incomplete_commit]||=[]
60
+ line_items[daily_incomplete_commit].concat(activity_block)
61
+ activity_block.clear
62
+
63
+ daily_incomplete_commit
64
+ end
65
+
66
+ def self.upcoming_commit(all_activity, index)
67
+ start_index = index + 1
68
+ while (start_index < all_activity.size) do
69
+ return all_activity[start_index] if all_activity[start_index].is_a?(BBNW::Commit)
70
+ start_index+=1
71
+ end
72
+ nil
73
+ end
74
+
75
+ def self.record_activities(activity_block, line_items, commit)
76
+ line_items[commit]||=[]
77
+ line_items[commit].concat(activity_block)
78
+ activity_block.clear
79
+ commit
80
+ end
81
+
82
+ def self.day_changed?(activity, current_time)
83
+ end_time = activity.end_time
84
+ !(current_time.year == end_time.year && current_time.day == end_time.day && current_time.month == end_time.month)
85
+ end
86
+
87
+ def self.debug_output(activity)
88
+ if activity.is_a?(BBNW::Activity)
89
+ puts "#{activity.start_time},#{activity.end_time},#{activity.activity}"
90
+ elsif activity.is_a?(BBNW::Commit)
91
+ puts "---------------------------------------------"
92
+ puts "Commit : #{activity.start_time},#{activity.end_time},#{activity.activity}"
93
+ puts "---------------------------------------------"
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+
101
+
102
+ # Example data :
103
+ #
104
+ #2011-05-26 04:38:47 -0700,2011-05-26 04:38:52 -0700,1. Cypress Server (cc1)
105
+ #2011-05-26 04:38:47 -0700,2011-05-26 04:38:49 -0700,https://www.pivotaltracker.com/signin
106
+ #2011-05-26 04:38:47 -0700,2011-05-26 04:38:52 -0700,1. Cypress Server (git)
107
+ #2011-05-26 04:38:47 -0700,2011-05-26 04:42:48 -0700,Searching "Inbox"
108
+ #2011-05-26 04:38:47 -0700,2011-05-26 04:39:22 -0700,db/schema.rb at master from manilla/redwood - GitHub
109
+ #2011-05-26 04:38:47 -0700,2011-05-26 04:38:52 -0700,manilla/redwood at master - GitHub
110
+ #2011-05-26 04:38:47 -0700,2011-05-26 04:39:51 -0700,1. Cypress Server (bash)
111
+ #2011-05-26 04:38:47 -0700,2011-05-26 04:40:22 -0700,1. Cypress Server (ruby)
112
+ #2011-05-26 04:38:47 -0700,2011-05-26 04:39:01 -0700,app/models/cypress_mailer.rb at master from manilla/redwood - GitHub
113
+ #2011-05-26 04:38:47 -0700,2011-05-26 04:38:57 -0700,1. Cypress Worker (ruby)
114
+ #2011-05-26 04:38:47 -0700,2011-05-26 04:39:40 -0700,2. nano
115
+ #2011-05-26 04:38:47 -0700,2011-05-26 04:39:02 -0700,manilla/redwood - GitHub
116
+ #2011-05-26 03:39:12 -0700,2011-05-26 03:39:17 -0700,2. sh
117
+ #2011-05-26 03:39:12 -0700,2011-05-26 03:40:25 -0700,Git Cherry Picking: Move small code patches across branches | TechnoSophos
118
+ #2011-05-26 03:39:12 -0700,2011-05-26 03:39:17 -0700,2. gnumake
119
+ #2011-05-26 03:39:12 -0700,2011-05-26 03:39:14 -0700,Warning: Unresponsive script
120
+ #2011-05-26 03:39:12 -0700,2011-05-26 03:39:17 -0700,Problem loading page
121
+ #2011-05-26 03:39:12 -0700,2011-05-26 03:39:32 -0700,Re: Remote Redwood branch clean up
122
+ #2011-05-26 03:39:12 -0700,2011-05-26 03:39:17 -0700,2. gcc-4.2
123
+ #---------------------------------------------
124
+ #2011-05-26 01:23:03 -0700,2011-05-26 01:23:03 -0700,Merge branch 'feature-13269583-require_comments_on_robot_revisions' [13269583]
125
+ #---------------------------------------------
126
+ #---------------------------------------------
127
+ #2011-05-26 01:19:50 -0700,2011-05-26 01:19:50 -0700,User is now prompted for a comment before publishing to production. Comment is displayed in the show fields along with an icon next to the entry in the similar robots table. [13269583]
128
+ #---------------------------------------------
129
+ #---------------------------------------------
130
+ #2011-05-25 21:33:31 -0700,2011-05-25 21:33:31 -0700,Merge branch 'master' of github.com:manilla/redwood [13269479]
131
+ #---------------------------------------------
132
+ #2011-05-25 16:13:28 -0700,2011-05-25 16:23:33 -0700,git_merge_fails.txt — jimjones
133
+ #2011-05-25 16:13:28 -0700,2011-05-25 16:13:33 -0700,git cherry pick - Google Search
134
+ #2011-05-25 16:13:28 -0700,2011-05-25 16:16:33 -0700,index.html — jquery.fancybox-1.3.4
135
+ #2011-05-25 16:13:28 -0700,2011-05-25 16:21:59 -0700,
136
+ #2011-05-25 16:13:28 -0700,2011-05-25 17:13:37 -0700,RubyMine
137
+ #2011-05-25 16:13:28 -0700,2011-05-25 16:13:54 -0700,Action Controller: Exception caught
138
+ #2011-05-25 16:13:28 -0700,2011-05-25 16:36:06 -0700,Redwood
@@ -0,0 +1,83 @@
1
+ require 'rubygems'
2
+ require 'base/activities'
3
+ require 'base/activity'
4
+ require 'utils/date_time_utils'
5
+ require 'csv'
6
+
7
+ module BBNW
8
+
9
+ class TimeSink
10
+ include BBNW::Activities
11
+ include BBNW::DateTimeUtils
12
+
13
+ APPLICATION = 0
14
+ START_TIME = 1
15
+ END_TIME = 2
16
+ TIME_SINK_HEADERS = {'Application' => APPLICATION,
17
+ 'Foreground Begin (Unix Time)' => START_TIME,
18
+ 'Foreground End (Unix Time)' => END_TIME}
19
+
20
+
21
+ def initialize(file_path, start_date, end_date, debug = false)
22
+ @files = Dir.glob(file_path)
23
+ @debug = debug
24
+ @start_date = Time.new(start_date.year, start_date.month, start_date.day, 0, 0, 0)
25
+ @end_date = Time.new(end_date.year, end_date.month, end_date.day, 23, 59, 59)
26
+ end
27
+
28
+ def log(message)
29
+ puts message if @debug
30
+ end
31
+
32
+ def columns(header)
33
+ hs = {}
34
+ TIME_SINK_HEADERS.each do |k,v|
35
+ header.each_with_index do |header_item,i|
36
+ if header_item == k
37
+ hs[v] = i
38
+ break
39
+ end
40
+ end
41
+ end
42
+ hs
43
+ end
44
+
45
+ def get_activities
46
+ activities = []
47
+ prev_e = nil
48
+ blank_time = Time.at(0.0)
49
+
50
+ @files.each do |file|
51
+ csv = CSV::parse(File.open(file) { |f| f.read })
52
+ headers = csv.shift
53
+ cols = columns(headers)
54
+
55
+ csv.each_with_index do |row, i|
56
+ activity = row[cols[APPLICATION]]
57
+ s = row[cols[START_TIME]]
58
+ e = row[cols[END_TIME]]
59
+
60
+ next unless prev_e.nil? || s >= prev_e
61
+
62
+ start_time = parse_time(s)
63
+ #next if start_time == blank_time
64
+
65
+ end_time = parse_time(e)
66
+ #next if end_time == blank_time
67
+
68
+ next if end_time < @start_date
69
+ next if start_time > @end_date
70
+ #start_time = @start_date if start_time < @start_date && end_time < @end_date
71
+ #end_time = @end_date if start_time > @start_date && end_time > @end_date
72
+
73
+ time_entry = BBNW::Activity.new(activity, start_time, end_time)
74
+ activities << time_entry
75
+
76
+ prev_e = e
77
+ end
78
+ end
79
+ activities
80
+ end
81
+ end
82
+
83
+ end
@@ -0,0 +1,95 @@
1
+ Dir.glob(File.join(File.dirname(__FILE__), 'adapters', '*.rb')).each {|f| require f }
2
+ Dir.glob(File.join(File.dirname(__FILE__), 'adapters', 'base', '*.rb')).each {|f| require f }
3
+ Dir.glob(File.join(File.dirname(__FILE__), 'adapters', 'time_estimators', '*.rb')).each {|f| require f }
4
+ Dir.glob(File.join(File.dirname(__FILE__), 'utils', '*.rb')).each {|f| require f }
5
+
6
+ require 'payday'
7
+
8
+ module BBNW
9
+ class Invoice
10
+ include InvoiceDisplayUtils
11
+
12
+ attr_accessor :line_items
13
+
14
+ def initialize
15
+ @meetings = {}
16
+ end
17
+
18
+ # Usage :
19
+ # Compute time usage % over a 20 hour span
20
+ #
21
+ # line_items(git_commits, 20)
22
+ #
23
+ # Compute time usage using recorded time activities
24
+ # and cross reference that with git commits.
25
+ #
26
+ # line_items(git_commits, time_sink_activities)
27
+ #
28
+ def line_items(repository_adapter, activities_span, time_estimate_adapter, group_by_task_id = true)
29
+ @line_items = time_estimate_adapter.line_items(repository_adapter, activities_span, @meetings)
30
+
31
+ @line_items = BBNW::LineItemUtils.group_line_items(@line_items, {}, group_by_task_id)
32
+ @line_items = BBNW::LineItemUtils.group_line_items(@meetings, @line_items, group_by_task_id)
33
+ end
34
+
35
+ def generate(invoice_num, company_logo, company_name,
36
+ company_details, bill_to, rate_per_hour, start_date = nil, end_date = nil, hours = nil)
37
+
38
+ raise "There are not line items to generate a report with." if @line_items.empty?
39
+
40
+ rate_per_second = rate_per_hour / 3600.to_f
41
+ Payday::Config.default.invoice_logo = company_logo
42
+ Payday::Config.default.company_name = company_name
43
+ Payday::Config.default.company_details = company(company_details, start_date, end_date, hours)
44
+
45
+ due_date = Date.today + 21
46
+ invoice = Payday::Invoice.new(:invoice_number => invoice_num,
47
+ :bill_to => bill_to.join("\n"),
48
+ :due_at => due_date)
49
+
50
+ display_price = display_price(rate_per_hour)
51
+
52
+ build_line_item_report(display_price, invoice, rate_per_second, Fixnum)
53
+ build_line_item_report(display_price, invoice, rate_per_second, String)
54
+
55
+ invoice.render_pdf_to_file(invoice_path(invoice_num, bill_to[0]))
56
+ end
57
+
58
+ def meeting(label, start_time, end_time, account_for)
59
+ s = Time.parse(start_time)
60
+ e = Time.parse(end_time)
61
+ meeting_label = "Meeting (#{short_time(s)} - #{short_time(e)}): #{label}"
62
+ time_span = account_for ? (e - s) : 0
63
+ @meetings[BBNW::Meeting.new(meeting_label, s, e, account_for)] = time_span
64
+ end
65
+
66
+ def task(label, start_time, end_time)
67
+ s = Time.parse(start_time)
68
+ e = Time.parse(end_time)
69
+ label = "Task : #{label}"
70
+ @meetings[BBNW::Meeting.new(label, s, e)] = e - s
71
+ end
72
+
73
+ private
74
+
75
+ def build_line_item_report(display_price, invoice, rate_per_second, typ = nil)
76
+ @line_items.each do |task_id, billable_item|
77
+ next unless typ.nil? || task_id.is_a?(typ)
78
+ time_elapsed, minutes, hours = time_description(billable_item.total_time)
79
+ head = header(task_id)
80
+
81
+ invoice.line_items << Payday::LineItem.new(:price => rate_per_second,
82
+ :quantity => billable_item.total_time,
83
+ :display_quantity => time_elapsed,
84
+ :display_price => display_price,
85
+ :description => billable_item.all_descriptions(head))
86
+ end
87
+ end
88
+
89
+ def invoice_path(invoice_num, company_name)
90
+ "./#{company_name.gsub(' ','_').gsub("'",'').gsub(',','').downcase}_#{invoice_num}.pdf"
91
+ end
92
+
93
+ end
94
+ end
95
+
@@ -0,0 +1,42 @@
1
+ module BBNW
2
+ module DateTimeUtils
3
+ def short_date(time)
4
+ "#{Date::DAYNAMES[time.wday]}, #{Date::ABBR_MONTHNAMES[time.month]} #{time.mday}"
5
+ end
6
+
7
+ def long_date(time)
8
+ "#{Date::DAYNAMES[time.wday]}, #{Date::ABBR_MONTHNAMES[time.month]} #{time.mday}, #{time.year}"
9
+ end
10
+
11
+ def short_time(time)
12
+ time.strftime("%I:%M %P")
13
+ end
14
+
15
+ def long_time(time)
16
+ time.strftime("%I:%M.%S %P")
17
+ end
18
+
19
+ def long_date_time(time)
20
+ "#{long_date(time)} #{long_time(time)}"
21
+ end
22
+
23
+ def is_number?(v)
24
+ true if Float(v) rescue false
25
+ end
26
+
27
+ def parse_time(s)
28
+ # If it's a number, it's probably a UNIX style timestamp, otherwise parse the string
29
+ is_number?(s) ? Time.at(s.to_f) : Time.parse(s)
30
+ end
31
+
32
+ def time_description(time)
33
+ hours = (time / 3600).to_i
34
+ minutes = (time / 60 - hours * 60).to_i
35
+ seconds = (time - (minutes * 60 + hours * 3600))
36
+
37
+ display = "#{minutes} minutes"
38
+ display = "#{hours} hours, #{display}" if hours > 0
39
+ return display, minutes, hours
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,27 @@
1
+ require File.join(File.join(File.dirname(__FILE__),'date_time_utils'))
2
+
3
+ module BBNW
4
+ module InvoiceDisplayUtils
5
+ include BBNW::DateTimeUtils
6
+
7
+ def header(task_id)
8
+ task_id.is_a?(Integer) ? "Story #: #{task_id}" : task_id
9
+ end
10
+
11
+ def display_price(price)
12
+ display_price = "$#{price}"
13
+ "#{display_price}.00" unless price =~ /./
14
+ end
15
+
16
+ def message_display(billed_commit)
17
+ "( #{billed_commit.timestamp} )\t#{billed_commit.message}"
18
+ end
19
+
20
+ def company(details, start_date, end_date, hours)
21
+ details << "\n\n#{long_date(start_date)} - #{long_date(end_date)}" unless start_date.nil? || end_date.nil?
22
+ details << "\nBillable : #{hours} hours" unless hours.nil?
23
+ details.join("\n")
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,40 @@
1
+ require 'adapters/base/billable'
2
+ require 'utils/invoice_display_utils'
3
+
4
+ module BBNW
5
+ class LineItemUtils
6
+ MAX_IDLE = 900
7
+ extend BBNW::InvoiceDisplayUtils
8
+
9
+ def self.group_line_items(line_items, prev_line_items = {}, group_by_task_id = true)
10
+ grouped_line_items = prev_line_items.clone
11
+
12
+ line_items.each do |billed_commit, activities|
13
+
14
+ # Let's look for a long numeric ID and group by that; if it doesn't exist
15
+ # Add the commit item as it's own line item (hence the random id).
16
+ task_id = "== #{short_date(billed_commit.timestamp)} =="
17
+ task_id = billed_commit.task_id if group_by_task_id
18
+
19
+ billable_item = grouped_line_items.has_key?(task_id) ? grouped_line_items[task_id] : BBNW::Billable.new
20
+
21
+ billable_item.total_time+=(activities.is_a?(Array) ? billed_seconds(activities) : activities)
22
+ billable_item.descriptions << billed_commit.message
23
+ grouped_line_items[task_id] = billable_item
24
+ end
25
+
26
+ grouped_line_items
27
+ end
28
+
29
+ def self.billed_seconds(activities)
30
+ seconds = 0
31
+ activities.each do |activity|
32
+ time_elapsed = activity.total_seconds
33
+ time_elapsed = MAX_IDLE if time_elapsed > MAX_IDLE
34
+ seconds += time_elapsed
35
+ end
36
+ seconds
37
+ end
38
+
39
+ end
40
+ end
Binary file
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "BigBucksNoWhammies" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe BBNW::CsvFile do
4
+ describe "#get_activities" do
5
+ it "should convert each line in the CSV into an Activity" do
6
+ end_date = Time.now
7
+ start_date = end_date + 10
8
+
9
+ puts start_date.to_s
10
+
11
+ fake_row = ['rubymine', start_date.to_s, end_date.to_s]
12
+ CSV.stub!(:open).with("foo", 'r').and_yield(fake_row)
13
+
14
+ csv_file = BBNW::CsvFile.new("foo", nil, nil)
15
+ # csv_file.get_activities.should == [
16
+ # BBNW::Activity.new('rubymine', start_date, end_date)
17
+ # ]
18
+ activities = csv_file.get_activities
19
+ activities.should be_an(Array)
20
+ activities.should have(1).activity
21
+ activity = activities.first
22
+ activity.start_time.to_s.should == start_date.to_s
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
4
+ require 'rspec'
5
+ require 'big_bucks_no_whammies'
6
+
7
+ # Requires supporting files with custom matchers and macros, etc,
8
+ # in ./support/ and its subdirectories.
9
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
10
+
11
+ RSpec.configure do |config|
12
+
13
+ end
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: big_bucks_no_whammies
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 1.0.0
6
+ platform: ruby
7
+ authors:
8
+ - Jim Jones
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-08-22 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: big_bucks_no_whammies
17
+ requirement: &id001 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0"
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: grit
28
+ requirement: &id002 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.4.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: payday
39
+ requirement: &id003 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 1.0.2
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
49
+ name: jeweler
50
+ requirement: &id004 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 1.6.0
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: *id004
59
+ - !ruby/object:Gem::Dependency
60
+ name: rspec
61
+ requirement: &id005 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 2.3.0
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: *id005
70
+ description: "Auto-generate a pdf invoice utilizing your git commit activity. Time is automatically accounted for utilizing TimeSink and is proportionately applied towards each line item entry. "
71
+ email: jjones@aantix.com
72
+ executables: []
73
+
74
+ extensions: []
75
+
76
+ extra_rdoc_files:
77
+ - LICENSE.txt
78
+ - README.rdoc
79
+ files:
80
+ - .document
81
+ - .rspec
82
+ - Gemfile
83
+ - LICENSE.txt
84
+ - README.rdoc
85
+ - Rakefile
86
+ - VERSION
87
+ - big_bucks_no_whammies.gemspec
88
+ - lib/adapters/base/activities.rb
89
+ - lib/adapters/base/activity.rb
90
+ - lib/adapters/base/billable.rb
91
+ - lib/adapters/base/commit.rb
92
+ - lib/adapters/base/commits.rb
93
+ - lib/adapters/base/meeting.rb
94
+ - lib/adapters/chrometa.rb
95
+ - lib/adapters/csv_file.rb
96
+ - lib/adapters/git.rb
97
+ - lib/adapters/time_estimators/number_of_source_commits.rb
98
+ - lib/adapters/time_estimators/number_of_sources_changes.rb
99
+ - lib/adapters/time_estimators/recorded_activity.rb
100
+ - lib/adapters/time_sink.rb
101
+ - lib/big_bucks_no_whammies.rb
102
+ - lib/utils/date_time_utils.rb
103
+ - lib/utils/invoice_display_utils.rb
104
+ - lib/utils/line_item_utils.rb
105
+ - sample_report.pdf
106
+ - spec/big_bucks_no_whammies_spec.rb
107
+ - spec/csv_file_spec.rb
108
+ - spec/spec_helper.rb
109
+ homepage: http://github.com/aantix/big_bucks_no_whammies
110
+ licenses:
111
+ - MIT
112
+ post_install_message:
113
+ rdoc_options: []
114
+
115
+ require_paths:
116
+ - lib
117
+ - lib/adapters
118
+ - lib/adapters/base
119
+ - lib/adapters/time_estimators
120
+ - lib/adapters/utils
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: "0"
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: "0"
133
+ requirements: []
134
+
135
+ rubyforge_project:
136
+ rubygems_version: 1.8.5
137
+ signing_key:
138
+ specification_version: 3
139
+ summary: Big Bucks No Whammies allows software development contractors to generate a line-item invoice based on git commits. Activity
140
+ test_files: []
141
+