harvested2 5.0.3
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.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +35 -0
- data/.rspec +1 -0
- data/.rubocop.yml +34 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +20 -0
- data/HISTORY.md +118 -0
- data/MIT-LICENSE +21 -0
- data/README.md +66 -0
- data/Rakefile +24 -0
- data/harvested2.gemspec +30 -0
- 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/account.rb +13 -0
- data/lib/harvest/api/account.rb +25 -0
- data/lib/harvest/api/base.rb +72 -0
- data/lib/harvest/api/clients.rb +10 -0
- data/lib/harvest/api/company.rb +12 -0
- data/lib/harvest/api/contacts.rb +9 -0
- data/lib/harvest/api/expense_categories.rb +9 -0
- data/lib/harvest/api/expenses.rb +26 -0
- data/lib/harvest/api/invoice_categories.rb +9 -0
- data/lib/harvest/api/invoice_messages.rb +86 -0
- data/lib/harvest/api/invoice_payments.rb +41 -0
- data/lib/harvest/api/invoices.rb +9 -0
- data/lib/harvest/api/projects.rb +9 -0
- data/lib/harvest/api/task_assignments.rb +75 -0
- data/lib/harvest/api/tasks.rb +9 -0
- data/lib/harvest/api/time_entry.rb +19 -0
- data/lib/harvest/api/user_assignments.rb +75 -0
- data/lib/harvest/api/users.rb +10 -0
- data/lib/harvest/base.rb +333 -0
- data/lib/harvest/behavior/activatable.rb +31 -0
- data/lib/harvest/behavior/crud.rb +80 -0
- data/lib/harvest/client.rb +23 -0
- data/lib/harvest/company.rb +8 -0
- data/lib/harvest/contact.rb +20 -0
- data/lib/harvest/credentials.rb +34 -0
- data/lib/harvest/errors.rb +27 -0
- data/lib/harvest/expense.rb +54 -0
- data/lib/harvest/expense_category.rb +10 -0
- data/lib/harvest/hardy_client.rb +80 -0
- data/lib/harvest/invoice.rb +75 -0
- data/lib/harvest/invoice_category.rb +8 -0
- data/lib/harvest/invoice_message.rb +8 -0
- data/lib/harvest/invoice_payment.rb +8 -0
- data/lib/harvest/line_item.rb +21 -0
- data/lib/harvest/model.rb +133 -0
- data/lib/harvest/project.rb +41 -0
- data/lib/harvest/receipt.rb +12 -0
- data/lib/harvest/task.rb +21 -0
- data/lib/harvest/task_assignment.rb +27 -0
- data/lib/harvest/time_entry.rb +57 -0
- data/lib/harvest/timezones.rb +130 -0
- data/lib/harvest/user.rb +58 -0
- data/lib/harvest/user_assignment.rb +27 -0
- data/lib/harvest/version.rb +3 -0
- data/lib/harvested2.rb +96 -0
- data/spec/factories/client.rb +14 -0
- data/spec/factories/contact.rb +8 -0
- data/spec/factories/expense.rb +10 -0
- data/spec/factories/expenses_category.rb +7 -0
- data/spec/factories/invoice.rb +25 -0
- data/spec/factories/invoice_category.rb +5 -0
- data/spec/factories/invoice_message.rb +9 -0
- data/spec/factories/invoice_payment.rb +7 -0
- data/spec/factories/line_item.rb +9 -0
- data/spec/factories/project.rb +15 -0
- data/spec/factories/task.rb +8 -0
- data/spec/factories/task_assignment.rb +8 -0
- data/spec/factories/time_entry.rb +13 -0
- data/spec/factories/user.rb +19 -0
- data/spec/factories/user_assigment.rb +7 -0
- data/spec/functional/clients_spec.rb +105 -0
- data/spec/functional/errors_spec.rb +42 -0
- data/spec/functional/expenses_spec.rb +97 -0
- data/spec/functional/invoice_messages_spec.rb +48 -0
- data/spec/functional/invoice_payments_spec.rb +51 -0
- data/spec/functional/invoice_spec.rb +138 -0
- data/spec/functional/project_spec.rb +76 -0
- data/spec/functional/tasks_spec.rb +119 -0
- data/spec/functional/time_entries_spec.rb +87 -0
- data/spec/functional/users_spec.rb +72 -0
- data/spec/harvest/base_spec.rb +10 -0
- data/spec/harvest/basic_auth_credentials_spec.rb +12 -0
- data/spec/harvest/expense_category_spec.rb +5 -0
- data/spec/harvest/expense_spec.rb +18 -0
- data/spec/harvest/invoice_message_spec.rb +5 -0
- data/spec/harvest/invoice_payment_spec.rb +5 -0
- data/spec/harvest/invoice_spec.rb +5 -0
- data/spec/harvest/oauth_credentials_spec.rb +11 -0
- data/spec/harvest/project_spec.rb +5 -0
- data/spec/harvest/task_assignment_spec.rb +5 -0
- data/spec/harvest/task_spec.rb +5 -0
- data/spec/harvest/time_entry_spec.rb +23 -0
- data/spec/harvest/user_assignment_spec.rb +5 -0
- data/spec/harvest/user_spec.rb +34 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/factory_bot.rb +5 -0
- data/spec/support/harvested_helpers.rb +28 -0
- data/spec/support/json_examples.rb +9 -0
- metadata +238 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Harvest
|
|
2
|
+
module Behavior
|
|
3
|
+
|
|
4
|
+
# Activate/Deactivate behaviors that can be brought into API collections
|
|
5
|
+
module Activatable
|
|
6
|
+
# Deactivates the item. Does nothing if the item is already deactivated
|
|
7
|
+
#
|
|
8
|
+
# @param [Harvest::BaseModel] model the model you want to deactivate
|
|
9
|
+
# @return [Harvest::BaseModel] the deactivated model
|
|
10
|
+
def deactivate(model)
|
|
11
|
+
if model.active?
|
|
12
|
+
request(:post, credentials, "#{api_model.api_path}/#{model.to_i}/toggle")
|
|
13
|
+
model.is_active = false
|
|
14
|
+
end
|
|
15
|
+
model
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Activates the item. Does nothing if the item is already activated
|
|
19
|
+
#
|
|
20
|
+
# @param [Harvest::BaseModel] model the model you want to activate
|
|
21
|
+
# @return [Harvest::BaseModel] the activated model
|
|
22
|
+
def activate(model)
|
|
23
|
+
if !model.active?
|
|
24
|
+
request(:post, credentials, "#{api_model.api_path}/#{model.to_i}/toggle")
|
|
25
|
+
model.is_active = true
|
|
26
|
+
end
|
|
27
|
+
model
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Harvest
|
|
2
|
+
module Behavior
|
|
3
|
+
module Crud
|
|
4
|
+
# Retrieves all items
|
|
5
|
+
# @return [Array<Harvest::BaseModel>] an array of models depending on where you're calling it from (e.g. [Harvest::Client] from Harvest::Base#clients)
|
|
6
|
+
def all(query_options = {})
|
|
7
|
+
response = request(:get, credentials, api_model.api_path, query: query_options)
|
|
8
|
+
response_parsed = api_model.to_json(response.parsed_response)
|
|
9
|
+
|
|
10
|
+
if response_parsed['total_pages'] > 1
|
|
11
|
+
counter = response_parsed['page']
|
|
12
|
+
|
|
13
|
+
while counter <= response_parsed['total_pages'] do
|
|
14
|
+
counter += 1
|
|
15
|
+
query_options = query_options.merge!({ 'page' => counter })
|
|
16
|
+
|
|
17
|
+
response_page = request(:get, credentials, api_model.api_path,
|
|
18
|
+
query: query_options)
|
|
19
|
+
page_result = api_model.to_json(response_page.parsed_response)
|
|
20
|
+
response_parsed[api_model.json_root.pluralize.to_s]
|
|
21
|
+
.concat(page_result[api_model.json_root.pluralize.to_s])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
api_model.parse(response_parsed)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Retrieves an item by id
|
|
29
|
+
# @overload find(id)
|
|
30
|
+
# @param [Integer] the id of the item you want to retreive
|
|
31
|
+
# @overload find(id)
|
|
32
|
+
# @param [String] id the String version of the id
|
|
33
|
+
# @overload find(model)
|
|
34
|
+
# @param [Harvest::BaseModel] id you can pass a model and it will return a refreshed version
|
|
35
|
+
#
|
|
36
|
+
# @return [Harvest::BaseModel] the model depends on where you're calling it from (e.g. Harvest::Client from Harvest::Base#clients)
|
|
37
|
+
def find(id, query_options = {})
|
|
38
|
+
raise 'ID is required' unless id
|
|
39
|
+
|
|
40
|
+
response = request(:get, credentials, "#{api_model.api_path}/#{id}", query: query_options)
|
|
41
|
+
api_model.parse(response.parsed_response).first
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Creates an item
|
|
45
|
+
# @param [Harvest::BaseModel] model the item you want to create
|
|
46
|
+
# @return [Harvest::BaseModel] the created model depending on where you're calling it from (e.g. Harvest::Client from Harvest::Base#clients)
|
|
47
|
+
def create(model)
|
|
48
|
+
model = api_model.wrap(model)
|
|
49
|
+
response = request(:post, credentials, api_model.api_path, body: model.to_json)
|
|
50
|
+
model = api_model.parse(response.parsed_response).first
|
|
51
|
+
find(model.id)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Updates an item
|
|
55
|
+
# @param [Harvest::BaseModel] model the model you want to update
|
|
56
|
+
# @return [Harvest::BaseModel] the created model depending on where you're calling it from (e.g. Harvest::Client from Harvest::Base#clients)
|
|
57
|
+
def update(model)
|
|
58
|
+
model = api_model.wrap(model)
|
|
59
|
+
response = request(:put, credentials, "#{api_model.api_path}/#{model.id}", body: model.to_json)
|
|
60
|
+
model = api_model.parse(response.parsed_response).first
|
|
61
|
+
find(model.id)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Deletes an item
|
|
65
|
+
# @overload delete(model)
|
|
66
|
+
# @param [Harvest::BaseModel] model the item you want to delete
|
|
67
|
+
# @overload delete(id)
|
|
68
|
+
# @param [Integer] id the id of the item you want to delete
|
|
69
|
+
# @overload delete(id)
|
|
70
|
+
# @param [String] id the String version of the id of the item you want to delete
|
|
71
|
+
#
|
|
72
|
+
# @return [Integer] the id of the item deleted
|
|
73
|
+
def delete(model)
|
|
74
|
+
model = api_model.wrap(model)
|
|
75
|
+
response = request(:delete, credentials, "#{api_model.api_path}/#{model.id}")
|
|
76
|
+
model.id
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Harvest
|
|
2
|
+
# The model that contains information about a client
|
|
3
|
+
#
|
|
4
|
+
# == Fields
|
|
5
|
+
# [+id+] (READONLY) the id of the client
|
|
6
|
+
# [+name+] (REQUIRED) the name of the client
|
|
7
|
+
# [+details+] the details of the client
|
|
8
|
+
# [+currency+] what type of currency is associated with the client
|
|
9
|
+
# [+currency_symbol+] what currency symbol is associated with the client
|
|
10
|
+
# [+active?+] true|false on whether the client is active
|
|
11
|
+
# [+highrise_id+] (READONLY) the highrise id associated with this client
|
|
12
|
+
# [+update_at+] (READONLY) the last modification timestamp
|
|
13
|
+
class Client < Hashie::Mash
|
|
14
|
+
include Harvest::Model
|
|
15
|
+
|
|
16
|
+
skip_json_root true
|
|
17
|
+
api_path '/clients'
|
|
18
|
+
|
|
19
|
+
def is_active=(val)
|
|
20
|
+
self.active = val
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Harvest
|
|
2
|
+
# The model that contains information about a client contact
|
|
3
|
+
#
|
|
4
|
+
# == Fields
|
|
5
|
+
# [+id+] (READONLY) the id of the contact
|
|
6
|
+
# [+client_id+] (REQUIRED) the id of the client this contact is associated with
|
|
7
|
+
# [+first_name+] (REQUIRED) the first name of the contact
|
|
8
|
+
# [+last_name+] (REQUIRED) the last name of the contact
|
|
9
|
+
# [+email+] the email of the contact
|
|
10
|
+
# [+title+] the title of the contact
|
|
11
|
+
# [+phone_office+] the office phone number of the contact
|
|
12
|
+
# [+phone_moble+] the moble phone number of the contact
|
|
13
|
+
# [+fax+] the fax number of the contact
|
|
14
|
+
class Contact < Hashie::Mash
|
|
15
|
+
include Harvest::Model
|
|
16
|
+
|
|
17
|
+
skip_json_root true
|
|
18
|
+
api_path '/contacts'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Harvest
|
|
2
|
+
class BasicAuthCredentials
|
|
3
|
+
def initialize(access_token: nil, account_id: nil)
|
|
4
|
+
@access_token = access_token
|
|
5
|
+
@account_id = account_id
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def set_authentication(request_options)
|
|
9
|
+
request_options[:headers] ||= {}
|
|
10
|
+
request_options[:headers]['Authorization'] = "Bearer #{@access_token}"
|
|
11
|
+
request_options[:headers]['Harvest-Account-ID'] = @account_id
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def host
|
|
15
|
+
'https://api.harvestapp.com/v2'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class OAuthCredentials
|
|
20
|
+
def initialize(access_token: nil, client_id: nil)
|
|
21
|
+
@access_token = access_token
|
|
22
|
+
@client_id = client_id
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def set_authentication(request_options)
|
|
26
|
+
request_options[:query] ||= {}
|
|
27
|
+
request_options[:query]["access_token"] = @access_token
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def host
|
|
31
|
+
'https://api.harvestapp.com/v2'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Harvest
|
|
2
|
+
class HTTPError < StandardError
|
|
3
|
+
attr_reader :response
|
|
4
|
+
attr_reader :params
|
|
5
|
+
attr_reader :hint
|
|
6
|
+
|
|
7
|
+
def initialize(response, params = {}, hint = nil)
|
|
8
|
+
@response = response
|
|
9
|
+
@params = params
|
|
10
|
+
@hint = hint
|
|
11
|
+
super(response)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"#{self.class.to_s}: #{response.code} #{response.body}" +
|
|
16
|
+
(hint ? "\n#{hint}" : "")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class RateLimited < HTTPError; end
|
|
21
|
+
class NotFound < HTTPError; end
|
|
22
|
+
class Unavailable < HTTPError; end
|
|
23
|
+
class InformHarvest < HTTPError; end
|
|
24
|
+
class BadRequest < HTTPError; end
|
|
25
|
+
class ServerError < HTTPError; end
|
|
26
|
+
class AuthenticationFailed < HTTPError ; end
|
|
27
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Harvest
|
|
2
|
+
class Expense < Hashie::Mash
|
|
3
|
+
include Harvest::Model
|
|
4
|
+
|
|
5
|
+
skip_json_root true
|
|
6
|
+
api_path '/expenses'
|
|
7
|
+
|
|
8
|
+
delegate_methods(billed?: :is_billed,
|
|
9
|
+
closed?: :is_closed)
|
|
10
|
+
|
|
11
|
+
def initialize(args = {}, _ = nil)
|
|
12
|
+
args = args.stringify_keys
|
|
13
|
+
self.receipt = args.delete('receipt') if args['receipt']
|
|
14
|
+
self.user = args.delete('user') if args['user']
|
|
15
|
+
self.project = args.delete('project') if args['project']
|
|
16
|
+
self.client = args.delete('client') if args['client']
|
|
17
|
+
self.spent_date = args.delete('spent_date') if args['spent_date']
|
|
18
|
+
self.user_assignment = args.delete('user_assignment') if args['user_assignment']
|
|
19
|
+
self.expense_category = args.delete('expense_category') if args['expense_category']
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def receipt=(receipt)
|
|
24
|
+
self['receipt_id'] = receipt['id']
|
|
25
|
+
self['receipt_file_name'] = receipt['file_name']
|
|
26
|
+
self['receipt_file_size'] = receipt['file_size']
|
|
27
|
+
self['receipt_content_type'] = receipt['content_type']
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def user=(user)
|
|
31
|
+
self['user_id'] = user['id']
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def project=(project)
|
|
35
|
+
self['project_id'] = project['id']
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def client=(client)
|
|
39
|
+
self['client_id'] = client['id']
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def spent_date=(date)
|
|
43
|
+
self['spent_date'] = Date.parse(date.to_s)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def user_assignment=(user_assignment)
|
|
47
|
+
self['user_assignment_id'] = user_assignment['id']
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def expense_category=(expense_category)
|
|
51
|
+
self['expense_category_id'] = expense_category['id']
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Harvest
|
|
2
|
+
class HardyClient < Delegator
|
|
3
|
+
def initialize(client, max_retries)
|
|
4
|
+
super(client)
|
|
5
|
+
@_sd_obj = @client = client
|
|
6
|
+
@max_retries = max_retries
|
|
7
|
+
(@client.public_methods - Object.public_instance_methods).each do |name|
|
|
8
|
+
instance_eval <<-END
|
|
9
|
+
def #{name}(*args)
|
|
10
|
+
wrap_collection do
|
|
11
|
+
@client.send('#{name}', *args)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
END
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def __getobj__; @_sd_obj; end
|
|
19
|
+
def __setobj__(obj); @_sd_obj = obj; end
|
|
20
|
+
|
|
21
|
+
def wrap_collection
|
|
22
|
+
collection = yield
|
|
23
|
+
HardyCollection.new(collection, self, @max_retries)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class HardyCollection < Delegator
|
|
27
|
+
def initialize(collection, client, max_retries)
|
|
28
|
+
super(collection)
|
|
29
|
+
@_sd_obj = @collection = collection
|
|
30
|
+
@client = client
|
|
31
|
+
@max_retries = max_retries
|
|
32
|
+
(@collection.public_methods - Object.public_instance_methods).each do |name|
|
|
33
|
+
instance_eval <<-END
|
|
34
|
+
def #{name}(*args)
|
|
35
|
+
retry_rate_limits do
|
|
36
|
+
@collection.send('#{name}', *args)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
END
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def __getobj__; @_sd_obj; end
|
|
44
|
+
def __setobj__(obj); @_sd_obj = obj; end
|
|
45
|
+
|
|
46
|
+
def retry_rate_limits
|
|
47
|
+
retries = 0
|
|
48
|
+
|
|
49
|
+
retry_func = lambda do |e|
|
|
50
|
+
if retries < @max_retries
|
|
51
|
+
retries += 1
|
|
52
|
+
true
|
|
53
|
+
else
|
|
54
|
+
raise e
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
yield
|
|
60
|
+
rescue Harvest::RateLimited => e
|
|
61
|
+
seconds = if e.response.headers['retry-after']
|
|
62
|
+
e.response.headers['retry-after'].to_i
|
|
63
|
+
else
|
|
64
|
+
16
|
|
65
|
+
end
|
|
66
|
+
sleep(seconds)
|
|
67
|
+
retry
|
|
68
|
+
rescue Harvest::Unavailable, Harvest::InformHarvest => e
|
|
69
|
+
would_retry = retry_func.call(e)
|
|
70
|
+
sleep(16) if @client.account.rate_limit_status.over_limit?
|
|
71
|
+
retry if would_retry
|
|
72
|
+
rescue Net::HTTPError, Net::HTTPFatalError => e
|
|
73
|
+
retry if retry_func.call(e)
|
|
74
|
+
rescue SystemCallError => e
|
|
75
|
+
retry if e.is_a?(Errno::ECONNRESET) && retry_func.call(e)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Harvest
|
|
2
|
+
|
|
3
|
+
# == Fields
|
|
4
|
+
# [+due_at+] when the invoice is due
|
|
5
|
+
# [+due_at_human_format+] when the invoice is due in human representation (e.g., due upon receipt) overrides +due_at+
|
|
6
|
+
# [+client_id+] (REQUIRED) the client id of the invoice
|
|
7
|
+
# [+currency+] the invoice currency
|
|
8
|
+
# [+issued_at+] when the invoice was issued
|
|
9
|
+
# [+subject+] subject line for the invoice
|
|
10
|
+
# [+notes+] notes on the invoice
|
|
11
|
+
# [+number+] invoice number
|
|
12
|
+
# [+kind+] (REQUIRED) the type of the invoice +free_form|project|task|people|detailed+
|
|
13
|
+
# [+projects_.idnvoice+] comma separated project ids to gather data from
|
|
14
|
+
# [+import_hours+] import hours from +project|task|people|detailed+ one of +yes|no+
|
|
15
|
+
# [+import_expenses+] import expenses from +project|task|people|detailed+ one of +yes|no+
|
|
16
|
+
# [+period_start+] start of the invoice period
|
|
17
|
+
# [+period_end+] end of the invoice period
|
|
18
|
+
# [+expense_period_start+] start of the invoice expense period
|
|
19
|
+
# [+expense_period_end+] end of the invoice expense period
|
|
20
|
+
# [+created_at+] (READONLY) when the invoice was created
|
|
21
|
+
# [+updated_at+] (READONLY) when the invoice was updated
|
|
22
|
+
# [+id+] (READONLY) the id of the invoice
|
|
23
|
+
# [+amount+] (READONLY) the amount of the invoice
|
|
24
|
+
# [+due_amount+] (READONLY) the amount due on the invoice
|
|
25
|
+
# [+created_by_id+] who created the invoice
|
|
26
|
+
# [+purchase_order+] purchase order number/text
|
|
27
|
+
# [+client_key+] unique client key
|
|
28
|
+
# [+state+] (READONLY) state of the invoice
|
|
29
|
+
# [+tax+] applied tax percentage
|
|
30
|
+
# [+tax2+] applied tax 2 percentage
|
|
31
|
+
# [+tax_amount+] amount to tax
|
|
32
|
+
# [+tax_amount2+] amount to tax 2
|
|
33
|
+
# [+discount_amount+] discount amount to apply to invoice
|
|
34
|
+
# [+discount_type+] discount type
|
|
35
|
+
# [+recurring_invoice_id+] the id of the original invoice
|
|
36
|
+
# [+estimate_id+] id of the related estimate
|
|
37
|
+
# [+retainer_id+] id of the related retainer
|
|
38
|
+
class Invoice < Hashie::Mash
|
|
39
|
+
include Harvest::Model
|
|
40
|
+
|
|
41
|
+
skip_json_root true
|
|
42
|
+
api_path '/invoices'
|
|
43
|
+
|
|
44
|
+
attr_reader :line_items
|
|
45
|
+
|
|
46
|
+
def initialize(args = {}, _ = nil)
|
|
47
|
+
if args
|
|
48
|
+
args = args.stringify_keys
|
|
49
|
+
self.client = args.delete('client') if args['client']
|
|
50
|
+
self.creator = args.delete('creator') if args['creator']
|
|
51
|
+
self.line_items = args.delete('line_items') if args['line_items']
|
|
52
|
+
self.line_items = [] if self.line_items.nil?
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def client=(client)
|
|
58
|
+
self['client_id'] = client['id']
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def creator=(creator)
|
|
62
|
+
self['creator_id'] = creator['id']
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def line_items=(line_items)
|
|
66
|
+
@line_items = line_items.map do |row|
|
|
67
|
+
Harvest::LineItem.new(row)
|
|
68
|
+
end || []
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def invoice_as_json
|
|
72
|
+
{ 'invoice' => { 'id' => invoice_id } }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|