gitquickbooks 0.0.1

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.
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ sudo: false
3
+ rvm:
4
+ - 2.2.3
5
+ script: bundle exec rake spec
6
+
7
+ addons:
8
+ code_climate:
9
+ repo_token: c2d82eb9f0e428387f401349a88ddcc21e9bbff3a91d0d2ec700c3783691da08
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "codeclimate-test-reporter", group: :test, require: nil
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Russell Osborne
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,59 @@
1
+ # GitQuickBooks
2
+
3
+ [![Build Status](https://travis-ci.org/rposborne/gitquickbooks.svg)](https://travis-ci.org/rposborne/gitquickbooks)
4
+ [![Code Climate](https://codeclimate.com/github/rposborne/gitquickbooks/badges/gpa.svg)](https://codeclimate.com/github/rposborne/gitquickbooks)
5
+ [![Test Coverage](https://codeclimate.com/github/rposborne/gitquickbooks/badges/coverage.svg)](https://codeclimate.com/github/rposborne/gitquickbooks/coverage)
6
+
7
+ This code links git wakatime per commit data to quickbooks online.
8
+ It will automatically manage API keys and storing of wakatime data locally via
9
+ GitWakatime gem.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'gitquickbooks'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install gitquickbooks
24
+
25
+ ## Usage
26
+
27
+ TODO: Write usage instructions here
28
+
29
+ ## Contributing
30
+
31
+ 1. Fork it ( https://github.com/[my-github-username]/gitquickbooks/fork )
32
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
33
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
34
+ 4. Push to the branch (`git push origin my-new-feature`)
35
+ 5. Create a new Pull Request
36
+
37
+
38
+ # Loop through TimeServices
39
+ ``` ruby
40
+ @entires = @time_service.query("Select * from TimeActivity where BillableStatus = 'Billable' and CustomerRef = '167'", :per_page => 50)
41
+
42
+ def update_entry(entry)
43
+ if entry.employee_ref
44
+ entry.hourly_rate = 75
45
+ name = entry.employee_ref.name
46
+ else #sam
47
+ entry.hourly_rate = 50
48
+ name = entry.vendor_ref.name
49
+ end
50
+ puts "#{entry.billable_status} #{entry.txn_date} #{entry.hourly_rate} for #{name}: #{entry.description}"
51
+ @time_service.update(entry)
52
+ end
53
+ entry = @entires.entries.first
54
+
55
+ update_entry(entry)
56
+ @entires.entries.each do |time_entry|
57
+ update_entry(time_entry)
58
+ end
59
+ ```
@@ -0,0 +1,19 @@
1
+ require 'bundler/gem_tasks'
2
+ begin
3
+ Bundler.setup(:default, :development)
4
+ rescue Bundler::BundlerError => e
5
+ $stderr.puts e.message
6
+ $stderr.puts 'Run `bundle install` to install missing gems'
7
+ exit e.status_code
8
+ end
9
+ require 'rake'
10
+
11
+ begin
12
+ require 'rspec/core/rake_task'
13
+
14
+ RSpec::Core::RakeTask.new(:spec)
15
+
16
+ task default: :spec
17
+ rescue LoadError
18
+ # no rspec available
19
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ require 'thor'
3
+
4
+ require_relative '../lib/gitquickbooks'
5
+
6
+ GitQuickBooks::Cli.start
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'gitquickbooks/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'gitquickbooks'
8
+ spec.version = GitQuickBooks::VERSION
9
+ spec.authors = ['Russell Osborne']
10
+ spec.email = ['russell@burningpony.com']
11
+ spec.summary = 'Bill Away'
12
+ spec.description = 'Bill Away to the clients'
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_runtime_dependency 'oauth-plugin', '>= 0.0.1'
22
+ spec.add_runtime_dependency 'dotenv', '>= 0.0.1'
23
+ spec.add_runtime_dependency 'thor', '>= 0.0.1'
24
+ spec.add_runtime_dependency 'launchy', '>= 0.0.1'
25
+ spec.add_runtime_dependency 'quickbooks-ruby', '>= 0.0.1'
26
+ spec.add_runtime_dependency 'gitwakatime', '>= 0.3.0'
27
+ spec.add_runtime_dependency 'awesome_print', '>= 0.0.1'
28
+ spec.add_development_dependency('bundler', ['>= 0'])
29
+ spec.add_development_dependency 'rake'
30
+ spec.add_development_dependency 'pry'
31
+ spec.add_development_dependency 'rspec'
32
+ end
@@ -0,0 +1,14 @@
1
+ require 'dotenv'
2
+ Dotenv.load
3
+
4
+ require 'thor'
5
+ require 'gitquickbooks/version'
6
+ require 'gitquickbooks/api'
7
+ require 'gitquickbooks/cli'
8
+ require 'gitquickbooks/cache'
9
+ require 'gitquickbooks/commit_msg_cleaner'
10
+ require 'quickbooks-ruby'
11
+ require 'gitwakatime'
12
+ # Yup it's a module :)
13
+ module GitQuickBooks
14
+ end
@@ -0,0 +1,47 @@
1
+ require 'oauth'
2
+ require 'launchy'
3
+
4
+ module GitQuickBooks
5
+ ##
6
+ # Expose and authorize primary QB account
7
+ class Api
8
+ attr_reader :realm,
9
+ :key,
10
+ :secret,
11
+ :access_token,
12
+ :callback_url,
13
+ :authorize_url
14
+
15
+ def initialize
16
+ @key = ENV['QB_KEY']
17
+ @secret = ENV['QB_SECRET']
18
+ @realm = ENV['COMPANY_ID']
19
+ @callback_url = 'https://localhost/oob'
20
+ @qb_oauth = build_oauth
21
+ @access_token = GitQuickBooks::Cache.new.fetch('key') do
22
+ setup_auth
23
+ end
24
+ end
25
+
26
+ def build_oauth
27
+ OAuth::Consumer.new(
28
+ @key,
29
+ @secret,
30
+ site: 'https://oauth.intuit.com',
31
+ request_token_path: '/oauth/v1/get_request_token',
32
+ authorize_url: 'https://appcenter.intuit.com/Connect/Begin',
33
+ access_token_path: '/oauth/v1/get_access_token'
34
+ )
35
+ end
36
+
37
+ def setup_auth
38
+ @request_token ||= @qb_oauth.get_request_token(oauth_callback: @callback_url)
39
+ @authorize_url ||= @request_token.authorize_url(oauth_callback: @callback_url)
40
+ end
41
+
42
+ def authorize(oauth)
43
+ @access_token = @request_token.get_access_token(oauth_verifier: oauth)
44
+ GitQuickBooks::Cache.new.write('key', @access_token)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,35 @@
1
+ module GitQuickBooks
2
+ ##
3
+ # Setup Marshalling cache
4
+ class Cache
5
+ # attr_accesor :base_path
6
+ def load(name)
7
+ Marshal.load(IO.read("tmp/#{name}"))
8
+ end
9
+
10
+ def present?(name)
11
+ File.file?("tmp/#{name}")
12
+ end
13
+
14
+ def write(name, data)
15
+ File.open("tmp/#{name}", 'w+') do |f|
16
+ f.write(Marshal.dump(data))
17
+ end
18
+ end
19
+
20
+ def delete(name)
21
+ File.delete("tmp/#{name}")
22
+ end
23
+
24
+ def fetch(name, &block)
25
+ if !present?(name)
26
+ @data = block.call
27
+ write(name, @data)
28
+ @data
29
+ else
30
+ puts "#{name} extracted from cache".red
31
+ @data = GitQuickBooks::Cache.new.load(name)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,161 @@
1
+ require 'active_support/core_ext/date/calculations'
2
+ require 'active_support/core_ext/date_and_time/calculations'
3
+ require 'active_support/core_ext/integer/time'
4
+ require 'active_support/core_ext/time'
5
+ require 'awesome_print'
6
+
7
+ module GitQuickBooks
8
+ # Provides two CLI actions init and tally
9
+ class Cli < Thor
10
+ include Thor::Actions
11
+
12
+ desc 'authorize', 'Login'
13
+
14
+ def authorize
15
+ api = GitQuickBooks::Api.new
16
+
17
+ say 'To use Quickbooks, you need to grant access to the app.'
18
+ say 'Press enter to launch your web browser and grant access.'
19
+
20
+ Launchy.open api.authorize_url
21
+
22
+ oauth_verifier = ask 'Now, copy the PIN below and press enter:'
23
+
24
+ api.authorize(oauth_verifier)
25
+ end
26
+
27
+ desc 'companies', 'List Company Names and IDs'
28
+ def companies
29
+ api = GitQuickBooks::Api.new
30
+
31
+ @customer_service = Quickbooks::Service::Customer.new
32
+ @customer_service.access_token = api.access_token
33
+ @customer_service.company_id = api.realm
34
+
35
+ # Called without args you get the first page of results
36
+ @customer_service.query_in_batches(nil, per_page: 20) do |batch|
37
+ batch.each do |customer|
38
+ puts "#{customer.id} : #{customer.display_name || customer.method}"
39
+ end
40
+ end
41
+ end
42
+
43
+ desc 'process', 'Produce time spend for each commit and file in each commit'
44
+ method_option :customer, aliases: '-c', type: 'numeric', required: true
45
+ method_option :file, aliases: '-f', default: '.'
46
+
47
+ method_option :start_on, aliases: '-s', default: Date.today.beginning_of_month.to_s
48
+ method_option :end_on, aliases: '-e'
49
+ method_option :output, aliases: '-o', default: 'text'
50
+ method_option :flush, default: false, type: 'boolean'
51
+ method_option :add_to_qb, default: false
52
+ method_option :rate, default: 75.0
53
+
54
+ def process
55
+ path = File.expand_path(options.file)
56
+ start_date = Date.parse(options.start_on)
57
+ end_date = Date.today
58
+
59
+ # == Setup QB
60
+ GitWakaTime.config.setup_local_db
61
+ api = GitQuickBooks::Api.new
62
+ @time_service = Quickbooks::Service::TimeActivity.new
63
+ @time_service.access_token = api.access_token
64
+ @time_service.company_id = api.realm
65
+
66
+ @customer_service = Quickbooks::Service::Customer.new
67
+ @customer_service.access_token = api.access_token
68
+ @customer_service.company_id = api.realm
69
+
70
+ customer = @customer_service.fetch_by_id(options.customer)
71
+
72
+ puts "running for #{customer.display_name} for #{start_date} #{end_date}".blue
73
+
74
+ return unless customer
75
+ @total_time = 0
76
+
77
+ controller = GitWakaTime::Controller.new(path: path, date: start_date)
78
+ @timer = controller.timer
79
+
80
+ @slips = @timer.map do |date, commits|
81
+ next unless date >= start_date
82
+ next unless date <= end_date
83
+ commits = commits.reject { |c| c.message.to_s.downcase.delete(' ').include?('nocharge') }
84
+
85
+ total_time = commits.map(&:time_in_seconds).compact.reduce(&:+) || 0
86
+
87
+ # has a local time flaw
88
+ recorded_time = GitWakaTime::Heartbeat.where(
89
+ 'DATE(time) >= ? and DATE(time) <= ? ', date.to_time.utc.to_date, date.to_time.utc.to_date
90
+ ).where(project: controller.project).sum(:duration) || 0
91
+
92
+ msgs = commits.map(&:message)
93
+ message = CommitMsgCleaner.new(msgs).call
94
+
95
+ @total_time += total_time
96
+
97
+ hours = (total_time / 60.0 / 60).floor
98
+ minutes = (total_time / 60.0).remainder(60).ceil
99
+
100
+ GitWakaTime::Log.new format(
101
+ '%-100s %-30s %-30s %30s'.purple,
102
+ date,
103
+ "Recorded #{ChronicDuration.output recorded_time}",
104
+ "Commited #{ChronicDuration.output total_time}",
105
+ seconds_to_money(total_time, rate: options.rate)
106
+ )
107
+
108
+ message.split("\n").each do |msg_line|
109
+ GitWakaTime::Log.new format(
110
+ '%-120s'.green,
111
+ msg_line
112
+ )
113
+ end
114
+
115
+ # commits.each do |commit|
116
+ # GitWakaTime::Log.new format(
117
+ # '%-120s %-30s %30s'.green,
118
+ # commit.message.split("\n").first,
119
+ # "Total #{ChronicDuration.output commit.time_in_seconds.to_f}",
120
+ # seconds_to_money(commit.time_in_seconds, rate: options.rate)
121
+ # )
122
+ # end
123
+
124
+ slip = Quickbooks::Model::TimeActivity.new
125
+ slip.txn_date = date
126
+ slip.customer_id = options.customer
127
+ slip.name_of = 'Employee'
128
+ slip.employee_id = 196
129
+
130
+ slip.billable_status = 'Billable' # or NotBillable
131
+ slip.taxable = false
132
+ slip.hours = hours
133
+ slip.hourly_rate = options.rate
134
+ slip.minutes = minutes
135
+ slip.description = message
136
+
137
+ slip
138
+ end
139
+
140
+ GitWakaTime::Log.new format(
141
+ '%-120s %-30s %30s'.red,
142
+ "#{@slips.size} slips",
143
+ "Total #{ChronicDuration.output @total_time}",
144
+ seconds_to_money(@total_time, rate: options.rate)
145
+ )
146
+
147
+ return unless options.add_to_qb
148
+ # Send slips to Quickbooks Online
149
+ @slips.compact.each do |slip|
150
+ next unless ((slip.hours * 60) + slip.minutes) > 0
151
+ puts 'Saving Slip'
152
+ @time_service.create(slip)
153
+ end
154
+ end
155
+ no_commands do
156
+ def seconds_to_money(seconds, rate: 75.0)
157
+ ((seconds.to_f / 3600.0) * rate.to_f).round(2)
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,45 @@
1
+ module GitQuickBooks
2
+ class CommitMsgCleaner
3
+ def initialize(msgs)
4
+ @msgs = msgs
5
+ end
6
+
7
+ def remove_blanks
8
+ @msgs.reject(&:blank?).compact
9
+ end
10
+
11
+ def remove_former_commits
12
+ @msgs.reject { |m| m =~ /Former-commit-id/ }
13
+ end
14
+
15
+ def remove_trail_period
16
+ @msgs.map do |msg|
17
+ # [ci-skip]
18
+ msg.strip.chomp('.')
19
+ end
20
+ end
21
+
22
+ def remove_square_brackets
23
+ @msgs.map do |msg|
24
+ # [ci-skip]
25
+ msg.gsub(/\[.*\]/, '').strip
26
+ end
27
+ end
28
+
29
+ def capitolize_first_word
30
+ @msgs.map do |msg|
31
+ msg[0] = msg[0].to_s.capitalize
32
+ msg
33
+ end
34
+ end
35
+
36
+ def call
37
+ @msgs = remove_blanks
38
+ @msgs = remove_trail_period
39
+ @msgs = remove_former_commits
40
+ @msgs = capitolize_first_word
41
+ @msgs = remove_square_brackets
42
+ @msgs.join("\n")
43
+ end
44
+ end
45
+ end