harvested 0.3.3 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +15 -0
- data/HISTORY +11 -0
- data/README.md +14 -6
- data/Rakefile +13 -31
- data/TODO +2 -0
- data/VERSION +1 -1
- data/examples/user_assignments.rb +1 -1
- data/harvested.gemspec +114 -126
- data/lib/ext/array.rb +52 -0
- data/lib/ext/date.rb +9 -0
- data/lib/ext/hash.rb +17 -0
- data/lib/ext/time.rb +5 -0
- data/lib/harvest/api/account.rb +8 -1
- data/lib/harvest/api/base.rb +32 -10
- data/lib/harvest/api/contacts.rb +1 -1
- data/lib/harvest/api/expenses.rb +3 -4
- data/lib/harvest/api/invoice_categories.rb +26 -0
- data/lib/harvest/api/invoices.rb +16 -0
- data/lib/harvest/api/projects.rb +2 -10
- data/lib/harvest/api/reports.rb +13 -16
- data/lib/harvest/api/task_assignments.rb +8 -6
- data/lib/harvest/api/tasks.rb +1 -1
- data/lib/harvest/api/time.rb +13 -13
- data/lib/harvest/api/user_assignments.rb +7 -5
- data/lib/harvest/base.rb +9 -1
- data/lib/harvest/behavior/activatable.rb +2 -2
- data/lib/harvest/behavior/crud.rb +15 -13
- data/lib/harvest/client.rb +18 -13
- data/lib/harvest/contact.rb +13 -11
- data/lib/harvest/errors.rb +6 -4
- data/lib/harvest/expense.rb +40 -14
- data/lib/harvest/expense_category.rb +10 -9
- data/lib/harvest/hardy_client.rb +1 -1
- data/lib/harvest/invoice.rb +103 -0
- data/lib/harvest/invoice_category.rb +18 -0
- data/lib/harvest/line_item.rb +12 -0
- data/lib/harvest/model.rb +120 -0
- data/lib/harvest/project.rb +55 -26
- data/lib/harvest/rate_limit_status.rb +9 -8
- data/lib/harvest/task.rb +17 -14
- data/lib/harvest/task_assignment.rb +27 -22
- data/lib/harvest/time_entry.rb +32 -30
- data/lib/harvest/user.rb +46 -22
- data/lib/harvest/user_assignment.rb +24 -17
- data/lib/harvested.rb +12 -5
- data/spec/functional/account_spec.rb +17 -0
- data/spec/functional/clients_spec.rb +58 -0
- data/spec/functional/errors_spec.rb +22 -0
- data/spec/functional/expenses_spec.rb +84 -0
- data/spec/functional/hardy_client_spec.rb +33 -0
- data/spec/functional/invoice_spec.rb +67 -0
- data/spec/functional/project_spec.rb +50 -0
- data/spec/functional/reporting_spec.rb +80 -0
- data/spec/functional/tasks_spec.rb +88 -0
- data/spec/functional/time_tracking_spec.rb +53 -0
- data/spec/functional/users_spec.rb +102 -0
- data/spec/harvest/base_spec.rb +1 -1
- data/spec/harvest/credentials_spec.rb +1 -1
- data/spec/harvest/expense_category_spec.rb +5 -0
- data/spec/harvest/expense_spec.rb +8 -5
- data/spec/harvest/invoice_spec.rb +47 -0
- data/spec/harvest/project_spec.rb +11 -0
- data/spec/harvest/task_assignment_spec.rb +4 -4
- data/spec/harvest/task_spec.rb +7 -0
- data/spec/harvest/time_entry_spec.rb +11 -10
- data/spec/harvest/user_assignment_spec.rb +3 -3
- data/spec/harvest/user_spec.rb +3 -1
- data/spec/spec_helper.rb +37 -6
- data/{features → spec}/support/harvest_credentials.example.yml +0 -1
- data/spec/support/harvested_helpers.rb +44 -0
- data/spec/support/json_examples.rb +11 -0
- data/spec/test_rubies +5 -0
- metadata +109 -85
- data/.gitignore +0 -28
- data/features/account.feature +0 -7
- data/features/client_contacts.feature +0 -23
- data/features/clients.feature +0 -29
- data/features/errors.feature +0 -25
- data/features/expense_categories.feature +0 -21
- data/features/expenses.feature +0 -55
- data/features/hardy_client.feature +0 -40
- data/features/projects.feature +0 -39
- data/features/reporting.feature +0 -72
- data/features/step_definitions/account_steps.rb +0 -7
- data/features/step_definitions/assignment_steps.rb +0 -100
- data/features/step_definitions/contact_steps.rb +0 -11
- data/features/step_definitions/debug_steps.rb +0 -3
- data/features/step_definitions/error_steps.rb +0 -113
- data/features/step_definitions/expenses_steps.rb +0 -46
- data/features/step_definitions/harvest_steps.rb +0 -8
- data/features/step_definitions/model_steps.rb +0 -90
- data/features/step_definitions/people_steps.rb +0 -4
- data/features/step_definitions/report_steps.rb +0 -91
- data/features/step_definitions/time_entry_steps.rb +0 -40
- data/features/support/env.rb +0 -37
- data/features/support/error_helpers.rb +0 -18
- data/features/support/fixtures/empty_clients.xml +0 -2
- data/features/support/fixtures/over_limit.xml +0 -8
- data/features/support/fixtures/receipt.png +0 -0
- data/features/support/fixtures/under_limit.xml +0 -8
- data/features/support/harvest_helpers.rb +0 -11
- data/features/support/inflections.rb +0 -9
- data/features/task_assignment.feature +0 -69
- data/features/tasks.feature +0 -25
- data/features/time_tracking.feature +0 -29
- data/features/user_assignments.feature +0 -33
- data/features/users.feature +0 -55
- data/lib/harvest/base_model.rb +0 -73
- data/spec/spec.default.opts +0 -1
data/lib/harvest/client.rb
CHANGED
@@ -10,21 +10,26 @@ module Harvest
|
|
10
10
|
# [+active?+] true|false on whether the client is active
|
11
11
|
# [+highrise_id+] (READONLY) the highrise id associated with this client
|
12
12
|
# [+update_at+] (READONLY) the last modification timestamp
|
13
|
-
class Client <
|
14
|
-
include
|
15
|
-
|
13
|
+
class Client < Hashie::Dash
|
14
|
+
include Harvest::Model
|
15
|
+
|
16
16
|
api_path '/clients'
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
17
|
+
|
18
|
+
|
19
|
+
property :id
|
20
|
+
property :active
|
21
|
+
property :name
|
22
|
+
property :details
|
23
|
+
property :currency
|
24
|
+
property :currency_symbol
|
25
|
+
property :cache_version
|
26
|
+
property :created_at
|
27
|
+
property :updated_at
|
28
|
+
property :highrise_id
|
29
|
+
property :default_invoice_timeframe
|
30
|
+
property :last_invoice_kind
|
27
31
|
|
28
32
|
alias_method :active?, :active
|
33
|
+
alias_method :is_active=, :active=
|
29
34
|
end
|
30
35
|
end
|
data/lib/harvest/contact.rb
CHANGED
@@ -11,19 +11,21 @@ module Harvest
|
|
11
11
|
# [+phone_office+] the office phone number of the contact
|
12
12
|
# [+phone_moble+] the moble phone number of the contact
|
13
13
|
# [+fax+] the fax number of the contact
|
14
|
-
class Contact <
|
15
|
-
include
|
14
|
+
class Contact < Hashie::Dash
|
15
|
+
include Harvest::Model
|
16
16
|
|
17
17
|
api_path '/contacts'
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
19
|
+
property :id
|
20
|
+
property :client_id
|
21
|
+
property :email
|
22
|
+
property :first_name
|
23
|
+
property :last_name
|
24
|
+
property :phone_office
|
25
|
+
property :phone_mobile
|
26
|
+
property :fax
|
27
|
+
property :title
|
28
|
+
property :created_at
|
29
|
+
property :updated_at
|
28
30
|
end
|
29
31
|
end
|
data/lib/harvest/errors.rb
CHANGED
@@ -3,14 +3,16 @@ module Harvest
|
|
3
3
|
|
4
4
|
class HTTPError < StandardError
|
5
5
|
attr_reader :response
|
6
|
-
|
6
|
+
attr_reader :params
|
7
|
+
|
8
|
+
def initialize(response, params = {})
|
7
9
|
@response = response
|
8
|
-
|
10
|
+
@params = params
|
11
|
+
super(response)
|
9
12
|
end
|
10
13
|
|
11
14
|
def to_s
|
12
|
-
|
13
|
-
"#{self.class.to_s} : #{response.code}#{" - #{hint}" if hint}"
|
15
|
+
"#{self.class.to_s} : #{response.code} #{response.body}"
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
data/lib/harvest/expense.rb
CHANGED
@@ -1,19 +1,45 @@
|
|
1
1
|
module Harvest
|
2
|
-
class Expense <
|
3
|
-
include
|
4
|
-
|
2
|
+
class Expense < Hashie::Dash
|
3
|
+
include Harvest::Model
|
4
|
+
|
5
5
|
api_path '/expenses'
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
6
|
+
|
7
|
+
property :id
|
8
|
+
property :notes
|
9
|
+
property :units
|
10
|
+
property :total_cost
|
11
|
+
property :project_id
|
12
|
+
property :expense_category_id
|
13
|
+
property :user_id
|
14
|
+
property :spent_at
|
15
|
+
property :created_at
|
16
|
+
property :updated_at
|
17
|
+
property :is_billed
|
18
|
+
property :is_closed
|
19
|
+
property :invoice_id
|
20
|
+
property :has_receipt
|
21
|
+
property :receipt_url
|
22
|
+
|
23
|
+
def initialize(args = {})
|
24
|
+
args = args.stringify_keys
|
25
|
+
self.spent_at = args.delete("spent_at") if args["spent_at"]
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
15
29
|
def spent_at=(date)
|
16
|
-
|
30
|
+
self["spent_at"] = (String === date ? Time.parse(date) : date)
|
31
|
+
end
|
32
|
+
|
33
|
+
def as_json(args = {})
|
34
|
+
super(args).stringify_keys.tap do |hash|
|
35
|
+
hash[json_root].update("spent_at" => (spent_at.nil? ? nil : spent_at.to_time.xmlschema))
|
36
|
+
hash[json_root].delete("has_receipt")
|
37
|
+
hash[json_root].delete("receipt_url")
|
38
|
+
end
|
17
39
|
end
|
40
|
+
|
41
|
+
alias_method :billed?, :is_billed
|
42
|
+
alias_method :closed?, :is_closed
|
43
|
+
alias_method :has_receipt?, :has_receipt
|
18
44
|
end
|
19
|
-
end
|
45
|
+
end
|
@@ -1,15 +1,16 @@
|
|
1
1
|
module Harvest
|
2
|
-
class ExpenseCategory <
|
3
|
-
include
|
4
|
-
|
5
|
-
tag 'expense-category'
|
2
|
+
class ExpenseCategory < Hashie::Dash
|
3
|
+
include Harvest::Model
|
6
4
|
api_path '/expense_categories'
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
property :id
|
7
|
+
property :name
|
8
|
+
property :unit_name
|
9
|
+
property :unit_price
|
10
|
+
property :deactivated
|
11
|
+
property :created_at
|
12
|
+
property :updated_at
|
13
|
+
property :cache_version
|
13
14
|
|
14
15
|
def active?
|
15
16
|
!deactivated
|
data/lib/harvest/hardy_client.rb
CHANGED
@@ -0,0 +1,103 @@
|
|
1
|
+
module Harvest
|
2
|
+
class Invoice < Hashie::Dash
|
3
|
+
include Harvest::Model
|
4
|
+
|
5
|
+
api_path '/invoices'
|
6
|
+
|
7
|
+
attr_reader :line_items
|
8
|
+
|
9
|
+
property :id
|
10
|
+
property :subject
|
11
|
+
property :number
|
12
|
+
property :created_at
|
13
|
+
property :updated_at
|
14
|
+
property :issued_at
|
15
|
+
property :due_at
|
16
|
+
property :due_at_human_format
|
17
|
+
property :due_amount
|
18
|
+
property :notes
|
19
|
+
property :recurring_invoice_id
|
20
|
+
property :period_start
|
21
|
+
property :period_end
|
22
|
+
property :discount
|
23
|
+
property :discount_amount
|
24
|
+
property :client_key
|
25
|
+
property :amount
|
26
|
+
property :tax
|
27
|
+
property :tax2
|
28
|
+
property :tax_amount
|
29
|
+
property :tax2_amount
|
30
|
+
property :csv_line_items
|
31
|
+
property :client_id
|
32
|
+
property :estimate_id
|
33
|
+
property :purchase_order
|
34
|
+
property :retainer_id
|
35
|
+
property :currency
|
36
|
+
property :state
|
37
|
+
property :kind
|
38
|
+
property :import_hours
|
39
|
+
property :import_expenses
|
40
|
+
|
41
|
+
def self.json_root; "doc"; end
|
42
|
+
# skip_json_root true
|
43
|
+
|
44
|
+
def initialize(args = {})
|
45
|
+
@line_items = []
|
46
|
+
args = args.stringify_keys
|
47
|
+
self.line_items = args.delete("csv_line_items")
|
48
|
+
self.line_items = args.delete("line_items")
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
def line_items=(raw_or_rich)
|
53
|
+
unless raw_or_rich.nil?
|
54
|
+
@line_items = case raw_or_rich
|
55
|
+
when String
|
56
|
+
@line_items = decode_csv(raw_or_rich).map {|row| Harvest::LineItem.new(row) }
|
57
|
+
else
|
58
|
+
raw_or_rich
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def as_json(*options)
|
64
|
+
json = super(*options)
|
65
|
+
json[json_root]["csv_line_items"] = encode_csv(@line_items)
|
66
|
+
json
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def decode_csv(string)
|
71
|
+
csv = CSV.parse(string)
|
72
|
+
headers = csv.shift
|
73
|
+
csv.map! {|row| headers.zip(row) }
|
74
|
+
csv.map {|row| row.inject({}) {|h, tuple| h.update(tuple[0] => tuple[1]) } }
|
75
|
+
end
|
76
|
+
|
77
|
+
def encode_csv(line_items)
|
78
|
+
if line_items.empty?
|
79
|
+
""
|
80
|
+
else
|
81
|
+
header = %w(kind description quantity unit_price amount taxed taxed2 project_id)
|
82
|
+
|
83
|
+
# writing this in stdlib so we don't force 1.8 users to install FasterCSV and make gem dependencies wierd
|
84
|
+
if RUBY_VERSION =~ /1.8/
|
85
|
+
csv_data = ""
|
86
|
+
CSV.generate_row(header, header.size, csv_data)
|
87
|
+
line_items.each do |item|
|
88
|
+
row_data = header.inject([]) {|row, attr| row << item[attr] }
|
89
|
+
CSV.generate_row(row_data, row_data.size, csv_data)
|
90
|
+
end
|
91
|
+
csv_data
|
92
|
+
else
|
93
|
+
CSV.generate do |csv|
|
94
|
+
csv << header
|
95
|
+
line_items.each do |item|
|
96
|
+
csv << header.inject([]) {|row, attr| row << item[attr] }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Harvest
|
2
|
+
class InvoiceCategory < Hashie::Dash
|
3
|
+
include Harvest::Model
|
4
|
+
|
5
|
+
api_path '/invoice_item_categories'
|
6
|
+
def self.json_root; "category"; end
|
7
|
+
|
8
|
+
property :id
|
9
|
+
property :name
|
10
|
+
property :created_at
|
11
|
+
property :updated_at
|
12
|
+
property :use_as_expense
|
13
|
+
property :use_as_service
|
14
|
+
|
15
|
+
alias_method :use_as_expense?, :use_as_expense
|
16
|
+
alias_method :use_as_service?, :use_as_service
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Harvest
|
2
|
+
module Model
|
3
|
+
def self.included(base)
|
4
|
+
base.send :include, InstanceMethods
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module InstanceMethods
|
9
|
+
def to_json(*args)
|
10
|
+
as_json(*args).to_json(*args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def as_json(args = {})
|
14
|
+
inner_json = self.to_hash.stringify_keys
|
15
|
+
inner_json.delete("cache_version")
|
16
|
+
if self.class.skip_json_root?
|
17
|
+
inner_json
|
18
|
+
else
|
19
|
+
{ self.class.json_root => inner_json }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_i; id; end
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
id == other.id
|
27
|
+
end
|
28
|
+
|
29
|
+
def impersonated_user_id
|
30
|
+
if respond_to?(:of_user) && respond_to?(:user_id)
|
31
|
+
of_user || user_id
|
32
|
+
elsif !respond_to?(:of_user) && respond_to?(:user_id)
|
33
|
+
user_id
|
34
|
+
elsif respond_to?(:of_user)
|
35
|
+
of_user
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def json_root
|
40
|
+
self.class.json_root
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ClassMethods
|
45
|
+
# This sets the API path so the API collections can use them in an agnostic way
|
46
|
+
# @return [void]
|
47
|
+
def api_path(path = nil)
|
48
|
+
@_api_path ||= path
|
49
|
+
end
|
50
|
+
|
51
|
+
def skip_json_root(skip = nil)
|
52
|
+
@_skip_json_root ||= skip
|
53
|
+
end
|
54
|
+
|
55
|
+
def skip_json_root?
|
56
|
+
@_skip_json_root == true
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse(json)
|
60
|
+
parsed = String === json ? JSON.parse(json) : json
|
61
|
+
Array.wrap(parsed).map {|attrs| skip_json_root? ? new(attrs) : new(attrs[json_root])}
|
62
|
+
end
|
63
|
+
|
64
|
+
def json_root
|
65
|
+
Harvest::Model::Utility.underscore(
|
66
|
+
Harvest::Model::Utility.demodulize(to_s)
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def wrap(model_or_attrs)
|
71
|
+
case model_or_attrs
|
72
|
+
when Hashie::Dash
|
73
|
+
model_or_attrs
|
74
|
+
when Hash
|
75
|
+
new(model_or_attrs)
|
76
|
+
else
|
77
|
+
model_or_attrs
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module Utility
|
83
|
+
class << self
|
84
|
+
|
85
|
+
# Both methods are shamelessly ripped from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/inflections.rb
|
86
|
+
|
87
|
+
# Removes the module part from the expression in the string.
|
88
|
+
#
|
89
|
+
# Examples:
|
90
|
+
# "ActiveRecord::CoreExtensions::String::Inflections".demodulize # => "Inflections"
|
91
|
+
# "Inflections".demodulize
|
92
|
+
def demodulize(class_name_in_module)
|
93
|
+
class_name_in_module.to_s.gsub(/^.*::/, '')
|
94
|
+
end
|
95
|
+
|
96
|
+
# Makes an underscored, lowercase form from the expression in the string.
|
97
|
+
#
|
98
|
+
# Changes '::' to '/' to convert namespaces to paths.
|
99
|
+
#
|
100
|
+
# Examples:
|
101
|
+
# "ActiveRecord".underscore # => "active_record"
|
102
|
+
# "ActiveRecord::Errors".underscore # => active_record/errors
|
103
|
+
#
|
104
|
+
# As a rule of thumb you can think of +underscore+ as the inverse of +camelize+,
|
105
|
+
# though there are cases where that does not hold:
|
106
|
+
#
|
107
|
+
# "SSLError".underscore.camelize # => "SslError"
|
108
|
+
def underscore(camel_cased_word)
|
109
|
+
word = camel_cased_word.to_s.dup
|
110
|
+
word.gsub!(/::/, '/')
|
111
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
112
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
113
|
+
word.tr!("-", "_")
|
114
|
+
word.downcase!
|
115
|
+
word
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|