harvested 0.3.3 → 0.4.0
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/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
|