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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.document +3 -0
  3. data/.gitignore +39 -0
  4. data/.rspec +2 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +16 -0
  7. data/Gemfile +19 -0
  8. data/HISTORY.md +19 -0
  9. data/MIT-LICENSE +20 -0
  10. data/README.md +109 -0
  11. data/Rakefile +22 -0
  12. data/examples/basics.rb +35 -0
  13. data/examples/clear_account.rb +28 -0
  14. data/examples/project_create_script.rb +93 -0
  15. data/examples/task_assignments.rb +27 -0
  16. data/examples/user_assignments.rb +24 -0
  17. data/forecasted.gemspec +25 -0
  18. data/lib/ext/array.rb +52 -0
  19. data/lib/ext/date.rb +9 -0
  20. data/lib/ext/hash.rb +17 -0
  21. data/lib/ext/time.rb +5 -0
  22. data/lib/forecast/aggregate.rb +8 -0
  23. data/lib/forecast/api/account.rb +22 -0
  24. data/lib/forecast/api/aggregates.rb +20 -0
  25. data/lib/forecast/api/assignments.rb +63 -0
  26. data/lib/forecast/api/base.rb +68 -0
  27. data/lib/forecast/api/clients.rb +10 -0
  28. data/lib/forecast/api/expense_categories.rb +9 -0
  29. data/lib/forecast/api/expenses.rb +27 -0
  30. data/lib/forecast/api/invoice_categories.rb +26 -0
  31. data/lib/forecast/api/invoice_messages.rb +75 -0
  32. data/lib/forecast/api/invoice_payments.rb +31 -0
  33. data/lib/forecast/api/invoices.rb +35 -0
  34. data/lib/forecast/api/milestones.rb +21 -0
  35. data/lib/forecast/api/projects.rb +23 -0
  36. data/lib/forecast/api/reports.rb +53 -0
  37. data/lib/forecast/api/tasks.rb +36 -0
  38. data/lib/forecast/api/time.rb +48 -0
  39. data/lib/forecast/api/user_assignments.rb +34 -0
  40. data/lib/forecast/api/users.rb +21 -0
  41. data/lib/forecast/assignment.rb +7 -0
  42. data/lib/forecast/base.rb +111 -0
  43. data/lib/forecast/behavior/activatable.rb +31 -0
  44. data/lib/forecast/behavior/crud.rb +75 -0
  45. data/lib/forecast/client.rb +22 -0
  46. data/lib/forecast/credentials.rb +42 -0
  47. data/lib/forecast/errors.rb +26 -0
  48. data/lib/forecast/expense.rb +27 -0
  49. data/lib/forecast/expense_category.rb +10 -0
  50. data/lib/forecast/hardy_client.rb +80 -0
  51. data/lib/forecast/invoice.rb +107 -0
  52. data/lib/forecast/invoice_category.rb +9 -0
  53. data/lib/forecast/invoice_message.rb +8 -0
  54. data/lib/forecast/invoice_payment.rb +8 -0
  55. data/lib/forecast/line_item.rb +4 -0
  56. data/lib/forecast/model.rb +154 -0
  57. data/lib/forecast/project.rb +37 -0
  58. data/lib/forecast/rate_limit_status.rb +23 -0
  59. data/lib/forecast/task.rb +22 -0
  60. data/lib/forecast/time_entry.rb +25 -0
  61. data/lib/forecast/timezones.rb +130 -0
  62. data/lib/forecast/trackable_project.rb +38 -0
  63. data/lib/forecast/user_assignment.rb +30 -0
  64. data/lib/forecast/version.rb +3 -0
  65. data/lib/forecasted.rb +87 -0
  66. data/spec/factories.rb +17 -0
  67. data/spec/forecast/base_spec.rb +11 -0
  68. data/spec/functional/aggregates_spec.rb +64 -0
  69. data/spec/functional/assignments_spec.rb +131 -0
  70. data/spec/functional/errors_spec.rb +22 -0
  71. data/spec/functional/hardy_client_spec.rb +33 -0
  72. data/spec/functional/milestones_spec.rb +82 -0
  73. data/spec/functional/people_spec.rb +85 -0
  74. data/spec/functional/project_spec.rb +41 -0
  75. data/spec/spec_helper.rb +41 -0
  76. data/spec/support/forecast_credentials.example.yml +6 -0
  77. data/spec/support/forecasted_helpers.rb +66 -0
  78. data/spec/support/json_examples.rb +9 -0
  79. 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,10 @@
1
+ # module Harvest
2
+ # class ExpenseCategory < Hashie::Mash
3
+ # include Harvest::Model
4
+ # api_path '/expense_categories'
5
+
6
+ # def active?
7
+ # !deactivated
8
+ # end
9
+ # end
10
+ # 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
@@ -0,0 +1,9 @@
1
+ # module Harvest
2
+ # class InvoiceCategory < Hashie::Mash
3
+ # include Harvest::Model
4
+
5
+ # api_path '/invoice_item_categories'
6
+ # def self.json_root; "category"; end
7
+
8
+ # end
9
+ # end
@@ -0,0 +1,8 @@
1
+ # module Harvest
2
+ # class InvoiceMessage < Hashie::Mash
3
+ # include Harvest::Model
4
+
5
+ # api_path '/messages'
6
+ # def self.json_root; 'message'; end
7
+ # end
8
+ # end
@@ -0,0 +1,8 @@
1
+ # module Harvest
2
+ # class InvoicePayment < Hashie::Mash
3
+ # include Harvest::Model
4
+
5
+ # api_path '/payments'
6
+ # def self.json_root; 'payment'; end
7
+ # end
8
+ # end
@@ -0,0 +1,4 @@
1
+ # module Harvest
2
+ # class LineItem < Hashie::Mash
3
+ # end
4
+ # end