forecasted 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.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +39 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +16 -0
- data/Gemfile +19 -0
- data/HISTORY.md +19 -0
- data/MIT-LICENSE +20 -0
- data/README.md +109 -0
- data/Rakefile +22 -0
- data/examples/basics.rb +35 -0
- data/examples/clear_account.rb +28 -0
- data/examples/project_create_script.rb +93 -0
- data/examples/task_assignments.rb +27 -0
- data/examples/user_assignments.rb +24 -0
- data/forecasted.gemspec +25 -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/forecast/aggregate.rb +8 -0
- data/lib/forecast/api/account.rb +22 -0
- data/lib/forecast/api/aggregates.rb +20 -0
- data/lib/forecast/api/assignments.rb +63 -0
- data/lib/forecast/api/base.rb +68 -0
- data/lib/forecast/api/clients.rb +10 -0
- data/lib/forecast/api/expense_categories.rb +9 -0
- data/lib/forecast/api/expenses.rb +27 -0
- data/lib/forecast/api/invoice_categories.rb +26 -0
- data/lib/forecast/api/invoice_messages.rb +75 -0
- data/lib/forecast/api/invoice_payments.rb +31 -0
- data/lib/forecast/api/invoices.rb +35 -0
- data/lib/forecast/api/milestones.rb +21 -0
- data/lib/forecast/api/projects.rb +23 -0
- data/lib/forecast/api/reports.rb +53 -0
- data/lib/forecast/api/tasks.rb +36 -0
- data/lib/forecast/api/time.rb +48 -0
- data/lib/forecast/api/user_assignments.rb +34 -0
- data/lib/forecast/api/users.rb +21 -0
- data/lib/forecast/assignment.rb +7 -0
- data/lib/forecast/base.rb +111 -0
- data/lib/forecast/behavior/activatable.rb +31 -0
- data/lib/forecast/behavior/crud.rb +75 -0
- data/lib/forecast/client.rb +22 -0
- data/lib/forecast/credentials.rb +42 -0
- data/lib/forecast/errors.rb +26 -0
- data/lib/forecast/expense.rb +27 -0
- data/lib/forecast/expense_category.rb +10 -0
- data/lib/forecast/hardy_client.rb +80 -0
- data/lib/forecast/invoice.rb +107 -0
- data/lib/forecast/invoice_category.rb +9 -0
- data/lib/forecast/invoice_message.rb +8 -0
- data/lib/forecast/invoice_payment.rb +8 -0
- data/lib/forecast/line_item.rb +4 -0
- data/lib/forecast/model.rb +154 -0
- data/lib/forecast/project.rb +37 -0
- data/lib/forecast/rate_limit_status.rb +23 -0
- data/lib/forecast/task.rb +22 -0
- data/lib/forecast/time_entry.rb +25 -0
- data/lib/forecast/timezones.rb +130 -0
- data/lib/forecast/trackable_project.rb +38 -0
- data/lib/forecast/user_assignment.rb +30 -0
- data/lib/forecast/version.rb +3 -0
- data/lib/forecasted.rb +87 -0
- data/spec/factories.rb +17 -0
- data/spec/forecast/base_spec.rb +11 -0
- data/spec/functional/aggregates_spec.rb +64 -0
- data/spec/functional/assignments_spec.rb +131 -0
- data/spec/functional/errors_spec.rb +22 -0
- data/spec/functional/hardy_client_spec.rb +33 -0
- data/spec/functional/milestones_spec.rb +82 -0
- data/spec/functional/people_spec.rb +85 -0
- data/spec/functional/project_spec.rb +41 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/support/forecast_credentials.example.yml +6 -0
- data/spec/support/forecasted_helpers.rb +66 -0
- data/spec/support/json_examples.rb +9 -0
- metadata +189 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
module Forecast
|
2
|
+
class Base
|
3
|
+
attr_reader :request, :credentials
|
4
|
+
|
5
|
+
# @see Forecast.client
|
6
|
+
# @see Forecast.hardy_client
|
7
|
+
# def initialize({forecast_api_id: nil, access_token: nil})
|
8
|
+
def initialize(ops={})
|
9
|
+
@credentials = if ops[:forecast_account_id] and ops[:access_token]
|
10
|
+
|
11
|
+
#
|
12
|
+
# httparty stops the party (haha) if the access_token is not a String
|
13
|
+
#
|
14
|
+
string_ops = {
|
15
|
+
forecast_account_id: ops[:forecast_account_id].to_s,
|
16
|
+
access_token: ops[:access_token],
|
17
|
+
}
|
18
|
+
|
19
|
+
OAuthCredentials.new(string_ops)
|
20
|
+
else
|
21
|
+
fail 'You must provide either :forecast_account_id and :access_token'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# people - @todo
|
26
|
+
# # All API Actions surrounding Users
|
27
|
+
# #
|
28
|
+
# # == Examples
|
29
|
+
# # harvest.users.all() # Returns all users in the system
|
30
|
+
# #
|
31
|
+
# # harvest.users.find(100) # Returns the user with id = 100
|
32
|
+
# #
|
33
|
+
# # user = Harvest::User.new(:first_name => 'Edgar', :last_name => 'Ruth', :email => 'edgar@ruth.com', :password => 'mypassword', :timezone => :cst, :admin => false, :telephone => '444-4444')
|
34
|
+
# # saved_user = harvest.users.create(user) # returns a saved version of Harvest::User
|
35
|
+
# #
|
36
|
+
# # user = harvest.users.find(205)
|
37
|
+
# # user.email = 'edgar@ruth.com'
|
38
|
+
# # updated_user = harvest.users.update(user) # returns an updated version of Harvest::User
|
39
|
+
# #
|
40
|
+
# # user = harvest.users.find(205)
|
41
|
+
# # harvest.users.delete(user) # returns 205
|
42
|
+
# #
|
43
|
+
# # user = harvest.users.find(301)
|
44
|
+
# # deactivated_user = harvest.users.deactivate(user) # returns an updated deactivated user
|
45
|
+
# # activated_user = harvest.users.activate(user) # returns an updated activated user
|
46
|
+
# #
|
47
|
+
# # user = harvest.users.find(401)
|
48
|
+
# # harvest.users.reset_password(user) # will trigger the reset password feature of harvest and shoot the user an email
|
49
|
+
# #
|
50
|
+
# # @see Harvest::Behavior::Crud
|
51
|
+
# # @see Harvest::Behavior::Activatable
|
52
|
+
# # @return [Harvest::API::Users]
|
53
|
+
# def users
|
54
|
+
# @users ||= Harvest::API::Users.new(credentials)
|
55
|
+
# end
|
56
|
+
|
57
|
+
# All API Actions surrounding Projects
|
58
|
+
#
|
59
|
+
# == Examples
|
60
|
+
# forecast.projects.all() # Returns all projects in the system
|
61
|
+
#
|
62
|
+
# harvest.projects.find(100) # Returns the project with id = 100
|
63
|
+
#
|
64
|
+
# project = Harvest::Project.new(:name => 'SuprGlu' :client_id => 10)
|
65
|
+
# saved_project = harvest.projects.create(project) # returns a saved version of Harvest::Project
|
66
|
+
#
|
67
|
+
# project = harvest.projects.find(205)
|
68
|
+
# project.name = 'SuprSticky'
|
69
|
+
# updated_project = harvest.projects.update(project) # returns an updated version of Harvest::Project
|
70
|
+
#
|
71
|
+
# project = harvest.project.find(205)
|
72
|
+
# harvest.projects.delete(project) # returns 205
|
73
|
+
#
|
74
|
+
# project = harvest.projects.find(301)
|
75
|
+
# deactivated_project = harvest.projects.deactivate(project) # returns an updated deactivated project
|
76
|
+
# activated_project = harvest.projects.activate(project) # returns an updated activated project
|
77
|
+
#
|
78
|
+
# project = harvest.projects.find(401)
|
79
|
+
# harvest.projects.create_task(project, 'Bottling Glue') # creates and assigns a task to the project
|
80
|
+
#
|
81
|
+
# @see Harvest::Behavior::Crud
|
82
|
+
# @see Harvest::Behavior::Activatable
|
83
|
+
# @return [Harvest::API::Projects]
|
84
|
+
def projects
|
85
|
+
@projects ||= Forecast::API::Projects.new(credentials)
|
86
|
+
end
|
87
|
+
|
88
|
+
# All API Actions surrounding Assignments
|
89
|
+
#
|
90
|
+
# == Examples
|
91
|
+
# forecast.projects.all() # Returns all projects in the system
|
92
|
+
# forecast.projects.all({start_date: '2016-11-28', end_date: '2017-01-01', state: 'active'})
|
93
|
+
#
|
94
|
+
# forecast.projects.find(100) # Returns the project with id = 100
|
95
|
+
#
|
96
|
+
#
|
97
|
+
# @see Forecast::Behavior::Crud
|
98
|
+
# @return [Forecast::API::Projects]
|
99
|
+
def assignments
|
100
|
+
@assignments ||= Forecast::API::Assignments.new(credentials)
|
101
|
+
end
|
102
|
+
|
103
|
+
# def milestones
|
104
|
+
# @milestones ||= Forecast::API::Milestones.new(credentials)
|
105
|
+
# end
|
106
|
+
|
107
|
+
def aggregates
|
108
|
+
@aggregates ||= Forecast::API::Aggregates.new(credentials)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Forecast
|
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,75 @@
|
|
1
|
+
module Forecast
|
2
|
+
module Behavior
|
3
|
+
module Crud
|
4
|
+
|
5
|
+
# Retrieves all items
|
6
|
+
# @return [Array<Forecast::BaseModel>] an array of models depending on
|
7
|
+
# where you're calling it from (e.g. [Forecast::Client] from
|
8
|
+
# Forecast::Base#clients)
|
9
|
+
|
10
|
+
# def all(user = nil, query_options = {})
|
11
|
+
def all(query_options = {})
|
12
|
+
|
13
|
+
# query = query_options.merge!(of_user_query(user))
|
14
|
+
query = query_options
|
15
|
+
|
16
|
+
response = request(:get, credentials, api_model.api_path, :query => query)
|
17
|
+
|
18
|
+
api_model.parse(response.parsed_response)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Retrieves an item by id
|
22
|
+
# @overload find(id)
|
23
|
+
# @param [Integer] the id of the item you want to retreive
|
24
|
+
# @overload find(id)
|
25
|
+
# @param [String] id the String version of the id
|
26
|
+
# @overload find(model)
|
27
|
+
# @param [Harvest::BaseModel] id you can pass a model and it will return a refreshed version
|
28
|
+
#
|
29
|
+
# @return [Harvest::BaseModel] the model depends on where you're calling it from (e.g. Harvest::Client from Harvest::Base#clients)
|
30
|
+
def find(id, user = nil)
|
31
|
+
raise "id required" unless id
|
32
|
+
# response = request(:get, credentials, "#{api_model.api_path}/#{id}", :query => of_user_query(user))
|
33
|
+
response = request(:get, credentials, "#{api_model.api_path}/#{id}")
|
34
|
+
api_model.parse(response.parsed_response)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Creates an item
|
38
|
+
# @param [Harvest::BaseModel] model the item you want to create
|
39
|
+
# @return [Harvest::BaseModel] the created model depending on where you're calling it from (e.g. Harvest::Client from Harvest::Base#clients)
|
40
|
+
def create(model, user = nil)
|
41
|
+
model = api_model.wrap(model)
|
42
|
+
response = request(:post, credentials, "#{api_model.api_path}", :body => model.to_json, :query => of_user_query(user))
|
43
|
+
id = response.headers["location"].match(/\/.*\/(\d+)/)[1]
|
44
|
+
if user
|
45
|
+
find(id, user)
|
46
|
+
else
|
47
|
+
find(id)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Updates an item
|
52
|
+
# @param [Harvest::BaseModel] model the model you want to update
|
53
|
+
# @return [Harvest::BaseModel] the created model depending on where you're calling it from (e.g. Harvest::Client from Harvest::Base#clients)
|
54
|
+
def update(model, user = nil)
|
55
|
+
model = api_model.wrap(model)
|
56
|
+
request(:put, credentials, "#{api_model.api_path}/#{model.to_i}", :body => model.to_json, :query => of_user_query(user))
|
57
|
+
find(model.id)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Deletes an item
|
61
|
+
# @overload delete(model)
|
62
|
+
# @param [Harvest::BaseModel] model the item you want to delete
|
63
|
+
# @overload delete(id)
|
64
|
+
# @param [Integer] id the id of the item you want to delete
|
65
|
+
# @overload delete(id)
|
66
|
+
# @param [String] id the String version of the id of the item you want to delete
|
67
|
+
#
|
68
|
+
# @return [Integer] the id of the item deleted
|
69
|
+
def delete(model, user = nil)
|
70
|
+
request(:delete, credentials, "#{api_model.api_path}/#{model.to_i}", :query => of_user_query(user))
|
71
|
+
model.to_i
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Forecast
|
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 Forecast::Model
|
15
|
+
|
16
|
+
api_path '/clients'
|
17
|
+
|
18
|
+
# def is_active=(val)
|
19
|
+
# self.active = val
|
20
|
+
# end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Forecast
|
2
|
+
# class BasicAuthCredentials
|
3
|
+
# # def initialize(forecast_account_id: nil, username: nil, password: nil)
|
4
|
+
# # @forecast_account_id, @username, @password = forecast_account_id, username, password
|
5
|
+
# # end
|
6
|
+
|
7
|
+
# # def set_authentication(request_options)
|
8
|
+
# # request_options[:headers] ||= {}
|
9
|
+
# # request_options[:headers]["Authorization"] = "Basic #{basic_auth}"
|
10
|
+
# # request_options[:headers]["forecast-account-id"] = @forecast_account_id
|
11
|
+
# # end
|
12
|
+
|
13
|
+
# # @westonplatter - forecaset doesn't have subdomains at this point
|
14
|
+
# # def host
|
15
|
+
# # "https://#{@subdomain}.forecastapp.com"
|
16
|
+
# # end
|
17
|
+
|
18
|
+
# private
|
19
|
+
|
20
|
+
# # def basic_auth
|
21
|
+
# # Base64.encode64("#{@username}:#{@password}").delete("\r\n")
|
22
|
+
# # end
|
23
|
+
# end
|
24
|
+
|
25
|
+
class OAuthCredentials
|
26
|
+
def initialize(ops={})
|
27
|
+
@access_token = ops[:access_token] || nil
|
28
|
+
@forecast_account_id = ops[:forecast_account_id] || nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def set_authentication(request_options)
|
32
|
+
request_options[:headers] ||= {}
|
33
|
+
request_options[:headers]['Forecast-Account-Id'] = @forecast_account_id
|
34
|
+
request_options[:headers]['Authorization'] = "Bearer #{@access_token}"
|
35
|
+
request_options[:headers]['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'
|
36
|
+
end
|
37
|
+
|
38
|
+
def host
|
39
|
+
"https://api.forecastapp.com"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Forecast
|
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}" + (hint ? "\n#{hint}" : "")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class RateLimited < HTTPError; end
|
20
|
+
class NotFound < HTTPError; end
|
21
|
+
class Unavailable < HTTPError; end
|
22
|
+
class InformHarvest < HTTPError; end
|
23
|
+
class BadRequest < HTTPError; end
|
24
|
+
class ServerError < HTTPError; end
|
25
|
+
class AuthenticationFailed < HTTPError ; end
|
26
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# module Harvest
|
2
|
+
# class Expense < Hashie::Mash
|
3
|
+
# include Harvest::Model
|
4
|
+
|
5
|
+
# api_path '/expenses'
|
6
|
+
# delegate_methods(:billed? => :is_billed,
|
7
|
+
# :closed? => :is_closed)
|
8
|
+
|
9
|
+
# def initialize(args = {}, _ = nil)
|
10
|
+
# args = args.to_hash.stringify_keys
|
11
|
+
# self.spent_at = args.delete("spent_at") if args["spent_at"]
|
12
|
+
# super
|
13
|
+
# end
|
14
|
+
|
15
|
+
# def spent_at=(date)
|
16
|
+
# self["spent_at"] = Date.parse(date.to_s)
|
17
|
+
# end
|
18
|
+
|
19
|
+
# def as_json(args = {})
|
20
|
+
# super(args).to_hash.stringify_keys.tap do |hash|
|
21
|
+
# hash[json_root].update("spent_at" => (spent_at.nil? ? nil : spent_at.xmlschema))
|
22
|
+
# hash[json_root].delete("has_receipt")
|
23
|
+
# hash[json_root].delete("receipt_url")
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
# end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Forecast
|
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 Forecast::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 Forecast::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,107 @@
|
|
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_to_invoice+] 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
|
+
# # [+csv_line_items+] csv formatted line items for the invoice +kind,description,quantity,unit_price,amount,taxed,taxed2,project_id+
|
21
|
+
# # [+created_at+] (READONLY) when the invoice was created
|
22
|
+
# # [+updated_at+] (READONLY) when the invoice was updated
|
23
|
+
# # [+id+] (READONLY) the id of the invoice
|
24
|
+
# # [+amount+] (READONLY) the amount of the invoice
|
25
|
+
# # [+due_amount+] (READONLY) the amount due on the invoice
|
26
|
+
# # [+created_by_id+] who created the invoice
|
27
|
+
# # [+purchase_order+] purchase order number/text
|
28
|
+
# # [+client_key+] unique client key
|
29
|
+
# # [+state+] (READONLY) state of the invoice
|
30
|
+
# # [+tax+] applied tax percentage
|
31
|
+
# # [+tax2+] applied tax 2 percentage
|
32
|
+
# # [+tax_amount+] amount to tax
|
33
|
+
# # [+tax_amount2+] amount to tax 2
|
34
|
+
# # [+discount_amount+] discount amount to apply to invoice
|
35
|
+
# # [+discount_type+] discount type
|
36
|
+
# # [+recurring_invoice_id+] the id of the original invoice
|
37
|
+
# # [+estimate_id+] id of the related estimate
|
38
|
+
# # [+retainer_id+] id of the related retainer
|
39
|
+
# class Invoice < Hashie::Mash
|
40
|
+
# include Harvest::Model
|
41
|
+
|
42
|
+
# api_path '/invoices'
|
43
|
+
|
44
|
+
# attr_reader :line_items
|
45
|
+
|
46
|
+
# def self.parse(json)
|
47
|
+
# parsed = String === json ? JSON.parse(json) : json
|
48
|
+
# invoices = Array.wrap(parsed).map {|attrs| new(attrs["invoices"])}
|
49
|
+
# invoice = Array.wrap(parsed).map {|attrs| new(attrs["invoice"])}
|
50
|
+
# if invoices.first && invoices.first.length > 0
|
51
|
+
# invoices
|
52
|
+
# else
|
53
|
+
# invoice
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
|
57
|
+
# def initialize(args = {}, _ = nil)
|
58
|
+
# if args
|
59
|
+
# args = args.to_hash.stringify_keys
|
60
|
+
# self.line_items = args.delete("csv_line_items")
|
61
|
+
# self.line_items = args.delete("line_items")
|
62
|
+
# self.line_items = [] if self.line_items.nil?
|
63
|
+
# end
|
64
|
+
# super
|
65
|
+
# end
|
66
|
+
|
67
|
+
# def line_items=(raw_or_rich)
|
68
|
+
# unless raw_or_rich.nil?
|
69
|
+
# @line_items = case raw_or_rich
|
70
|
+
# when String
|
71
|
+
# @line_items = decode_csv(raw_or_rich).map {|row| Harvest::LineItem.new(row) }
|
72
|
+
# else
|
73
|
+
# raw_or_rich
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
# end
|
77
|
+
|
78
|
+
# def as_json(*options)
|
79
|
+
# json = super(*options)
|
80
|
+
# json[json_root]["csv_line_items"] = encode_csv(@line_items)
|
81
|
+
# json
|
82
|
+
# end
|
83
|
+
|
84
|
+
# private
|
85
|
+
# def decode_csv(string)
|
86
|
+
# csv = CSV.parse(string)
|
87
|
+
# headers = csv.shift
|
88
|
+
# csv.map! {|row| headers.zip(row) }
|
89
|
+
# csv.map {|row| row.inject({}) {|h, tuple| h.update(tuple[0] => tuple[1]) } }
|
90
|
+
# end
|
91
|
+
|
92
|
+
# def encode_csv(line_items)
|
93
|
+
# if line_items.empty?
|
94
|
+
# ""
|
95
|
+
# else
|
96
|
+
# header = %w(kind description quantity unit_price amount taxed taxed2 project_id)
|
97
|
+
|
98
|
+
# CSV.generate do |csv|
|
99
|
+
# csv << header
|
100
|
+
# line_items.each do |item|
|
101
|
+
# csv << header.inject([]) {|row, attr| row << item[attr] }
|
102
|
+
# end
|
103
|
+
# end
|
104
|
+
# end
|
105
|
+
# end
|
106
|
+
# end
|
107
|
+
# end
|