harvested2 5.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|