harvested 0.3.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/.gitignore +23 -0
- data/HISTORY +16 -0
- data/MIT-LICENSE +20 -0
- data/README.md +67 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/examples/basics.rb +35 -0
- data/examples/clear_account.rb +28 -0
- data/examples/task_assignments.rb +27 -0
- data/examples/user_assignments.rb +24 -0
- data/features/account.feature +7 -0
- data/features/client_contacts.feature +23 -0
- data/features/clients.feature +29 -0
- data/features/errors.feature +25 -0
- data/features/expense_categories.feature +21 -0
- data/features/expenses.feature +55 -0
- data/features/hardy_client.feature +40 -0
- data/features/projects.feature +39 -0
- data/features/reporting.feature +72 -0
- data/features/step_definitions/account_steps.rb +7 -0
- data/features/step_definitions/assignment_steps.rb +100 -0
- data/features/step_definitions/contact_steps.rb +11 -0
- data/features/step_definitions/debug_steps.rb +3 -0
- data/features/step_definitions/error_steps.rb +113 -0
- data/features/step_definitions/expenses_steps.rb +46 -0
- data/features/step_definitions/harvest_steps.rb +8 -0
- data/features/step_definitions/model_steps.rb +90 -0
- data/features/step_definitions/people_steps.rb +4 -0
- data/features/step_definitions/report_steps.rb +91 -0
- data/features/step_definitions/time_entry_steps.rb +40 -0
- data/features/support/env.rb +37 -0
- data/features/support/error_helpers.rb +18 -0
- data/features/support/fixtures/empty_clients.xml +2 -0
- data/features/support/fixtures/over_limit.xml +8 -0
- data/features/support/fixtures/receipt.png +0 -0
- data/features/support/fixtures/under_limit.xml +8 -0
- data/features/support/harvest_credentials.example.yml +4 -0
- data/features/support/harvest_helpers.rb +11 -0
- data/features/support/inflections.rb +9 -0
- data/features/task_assignment.feature +69 -0
- data/features/tasks.feature +25 -0
- data/features/time_tracking.feature +29 -0
- data/features/user_assignments.feature +33 -0
- data/features/users.feature +55 -0
- data/lib/harvest/api/account.rb +10 -0
- data/lib/harvest/api/base.rb +42 -0
- data/lib/harvest/api/clients.rb +10 -0
- data/lib/harvest/api/contacts.rb +19 -0
- data/lib/harvest/api/expense_categories.rb +9 -0
- data/lib/harvest/api/expenses.rb +28 -0
- data/lib/harvest/api/projects.rb +39 -0
- data/lib/harvest/api/reports.rb +39 -0
- data/lib/harvest/api/task_assignments.rb +32 -0
- data/lib/harvest/api/tasks.rb +9 -0
- data/lib/harvest/api/time.rb +32 -0
- data/lib/harvest/api/user_assignments.rb +32 -0
- data/lib/harvest/api/users.rb +15 -0
- data/lib/harvest/base.rb +59 -0
- data/lib/harvest/base_model.rb +34 -0
- data/lib/harvest/behavior/activatable.rb +21 -0
- data/lib/harvest/behavior/crud.rb +31 -0
- data/lib/harvest/client.rb +18 -0
- data/lib/harvest/contact.rb +16 -0
- data/lib/harvest/credentials.rb +21 -0
- data/lib/harvest/errors.rb +23 -0
- data/lib/harvest/expense.rb +19 -0
- data/lib/harvest/expense_category.rb +18 -0
- data/lib/harvest/hardy_client.rb +80 -0
- data/lib/harvest/project.rb +22 -0
- data/lib/harvest/rate_limit_status.rb +16 -0
- data/lib/harvest/task.rb +20 -0
- data/lib/harvest/task_assignment.rb +34 -0
- data/lib/harvest/time_entry.rb +41 -0
- data/lib/harvest/timezones.rb +150 -0
- data/lib/harvest/user.rb +40 -0
- data/lib/harvest/user_assignment.rb +34 -0
- data/lib/harvested.rb +31 -0
- data/spec/harvest/base_spec.rb +9 -0
- data/spec/harvest/credentials_spec.rb +22 -0
- data/spec/harvest/expense_spec.rb +15 -0
- data/spec/harvest/task_assignment_spec.rb +10 -0
- data/spec/harvest/time_entry_spec.rb +22 -0
- data/spec/harvest/user_assignment_spec.rb +10 -0
- data/spec/harvest/user_spec.rb +32 -0
- data/spec/spec.default.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- metadata +243 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
@clean
|
2
|
+
Feature: User Assignments
|
3
|
+
|
4
|
+
Scenario: Adding and Updating a User on a Project
|
5
|
+
Given I am using the credentials from "./support/harvest_credentials.yml"
|
6
|
+
When I create a client with the following:
|
7
|
+
| name | Client Projects |
|
8
|
+
| details | Building API Widgets across the country |
|
9
|
+
When I create a project for the client "Client Projects" with the following:
|
10
|
+
| name | Test Project |
|
11
|
+
| active | true |
|
12
|
+
| notes | project to test the api |
|
13
|
+
Then there should be a project "Test Project"
|
14
|
+
When I create a user with the following:
|
15
|
+
| first_name | Edgar |
|
16
|
+
| last_name | Ruth |
|
17
|
+
| email | edgar@ruth.com |
|
18
|
+
| password | mypassword |
|
19
|
+
| password_confirmation | mypassword |
|
20
|
+
Then there should be a user "edgar@ruth.com"
|
21
|
+
When I assign the user "edgar@ruth.com" to the project "Test Project"
|
22
|
+
Then the user "edgar@ruth.com" should be assigned to the project "Test Project"
|
23
|
+
When I update the user "edgar@ruth.com" on the project "Test Project" with the following:
|
24
|
+
| hourly_rate | 50.0 |
|
25
|
+
| project_manager | true |
|
26
|
+
Then the user "edgar@ruth.com" on the project "Test Project" should have the following attributes:
|
27
|
+
| active? | true |
|
28
|
+
| hourly_rate | 50.0 |
|
29
|
+
| project_manager | true |
|
30
|
+
When I remove the user "edgar@ruth.com" from the project "Test Project"
|
31
|
+
Then the user "edgar@ruth.com" should not be assigned to the project "Test Project"
|
32
|
+
|
33
|
+
Scenario: Removing a user from a project that has recorded hours
|
@@ -0,0 +1,55 @@
|
|
1
|
+
@clean
|
2
|
+
Feature: Managing People
|
3
|
+
|
4
|
+
Scenario: Adding and Removing a User
|
5
|
+
Given I am using the credentials from "./support/harvest_credentials.yml"
|
6
|
+
When I create a user with the following:
|
7
|
+
| first_name | Edgar |
|
8
|
+
| last_name | Ruth |
|
9
|
+
| email | edgar@ruth.com |
|
10
|
+
| password | mypassword |
|
11
|
+
| password_confirmation | mypassword |
|
12
|
+
| timezone | cst |
|
13
|
+
| admin | false |
|
14
|
+
| telephone | 444-4444 |
|
15
|
+
Then there should be a user "edgar@ruth.com"
|
16
|
+
When I update the user "edgar@ruth.com" with the following:
|
17
|
+
| first_name | Jonah |
|
18
|
+
| timezone | pst |
|
19
|
+
Then the user "edgar@ruth.com" should have the following attributes:
|
20
|
+
| first_name | Jonah |
|
21
|
+
| timezone | Pacific Time (US & Canada) |
|
22
|
+
When I delete the user "edgar@ruth.com"
|
23
|
+
Then there should not be a user "edgar@ruth.com"
|
24
|
+
|
25
|
+
Scenario: Activating and Deactivating a User
|
26
|
+
Given I am using the credentials from "./support/harvest_credentials.yml"
|
27
|
+
Then I create a user with the following:
|
28
|
+
| first_name | Simon |
|
29
|
+
| last_name | Steel |
|
30
|
+
| email | simon@steel.com |
|
31
|
+
| password | mypassword |
|
32
|
+
| password_confirmation | mypassword |
|
33
|
+
| timezone | cst |
|
34
|
+
| admin | false |
|
35
|
+
| telephone | 444-4444 |
|
36
|
+
Then the user "simon@steel.com" should be activated
|
37
|
+
When I deactivate the user "simon@steel.com"
|
38
|
+
Then the user "simon@steel.com" should be deactivated
|
39
|
+
When I activate the user "simon@steel.com"
|
40
|
+
Then the user "simon@steel.com" should be activated
|
41
|
+
Then I delete the user "simon@steel.com"
|
42
|
+
|
43
|
+
Scenario: Resetting a User's password
|
44
|
+
Given I am using the credentials from "./support/harvest_credentials.yml"
|
45
|
+
Then I create a user with the following:
|
46
|
+
| first_name | Edgar |
|
47
|
+
| last_name | Ruth |
|
48
|
+
| email | edgar@ruth.com |
|
49
|
+
| password | mypassword |
|
50
|
+
| password_confirmation | mypassword |
|
51
|
+
| timezone | cst |
|
52
|
+
| admin | false |
|
53
|
+
| telephone | 444-4444 |
|
54
|
+
Then there should be a user "edgar@ruth.com"
|
55
|
+
Then I reset the password of "edgar@ruth.com"
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Harvest
|
2
|
+
module API
|
3
|
+
class Base
|
4
|
+
attr_reader :credentials
|
5
|
+
|
6
|
+
def initialize(credentials)
|
7
|
+
@credentials = credentials
|
8
|
+
end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def api_model(klass)
|
12
|
+
class_eval <<-END
|
13
|
+
def api_model
|
14
|
+
#{klass}
|
15
|
+
end
|
16
|
+
END
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
def request(method, credentials, path, options = {})
|
22
|
+
response = HTTParty.send(method, "#{credentials.host}#{path}", :query => options[:query], :body => options[:body], :headers => {"Accept" => "application/xml", "Content-Type" => "application/xml; charset=utf-8", "Authorization" => "Basic #{credentials.basic_auth}", "User-Agent" => "Harvestable/#{Harvest::VERSION}"}.update(options[:headers] || {}), :format => :plain)
|
23
|
+
case response.code
|
24
|
+
when 200..201
|
25
|
+
response
|
26
|
+
when 400
|
27
|
+
raise Harvest::BadRequest.new(response)
|
28
|
+
when 404
|
29
|
+
raise Harvest::NotFound.new(response)
|
30
|
+
when 500
|
31
|
+
raise Harvest::ServerError.new(response)
|
32
|
+
when 502
|
33
|
+
raise Harvest::Unavailable.new(response)
|
34
|
+
when 503
|
35
|
+
raise Harvest::RateLimited.new(response)
|
36
|
+
else
|
37
|
+
raise Harvest::InformHarvest.new(response)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Harvest
|
2
|
+
module API
|
3
|
+
class Contacts < Base
|
4
|
+
api_model Harvest::Contact
|
5
|
+
|
6
|
+
include Harvest::Behavior::Crud
|
7
|
+
|
8
|
+
def all(client_id = nil)
|
9
|
+
response = if client_id
|
10
|
+
request(:get, credentials, "/clients/#{client_id}/contacts")
|
11
|
+
else
|
12
|
+
request(:get, credentials, "/contacts")
|
13
|
+
end
|
14
|
+
|
15
|
+
api_model.parse(response.body)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Harvest
|
2
|
+
module API
|
3
|
+
class Expenses < Base
|
4
|
+
api_model Harvest::Expense
|
5
|
+
|
6
|
+
include Harvest::Behavior::Crud
|
7
|
+
|
8
|
+
def all(date = ::Time.now)
|
9
|
+
date = ::Time.parse(date) if String === date
|
10
|
+
response = request(:get, credentials, "#{api_model.api_path}/#{date.yday}/#{date.year}")
|
11
|
+
api_model.parse(response.body)
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
# This is currently broken, but will come back to it
|
16
|
+
def attach(expense, filename, receipt)
|
17
|
+
body = ""
|
18
|
+
body << "------------------------------b7edea381b46\r\n"
|
19
|
+
body << %Q{Content-Disposition: form-data; name="expense[receipt]"; filename="#{filename}"\r\n}
|
20
|
+
body << "Content-Type: image/png\r\n"
|
21
|
+
body << "\r\n#{receipt.read}\r\n"
|
22
|
+
body << "------------------------------b7edea381b46\r\n"
|
23
|
+
|
24
|
+
request(:post, credentials, "#{api_model.api_path}/#{expense.to_i}/receipt", :headers => {'Content-Type' => 'multipart/form-data; boundary=------------------------------b7edea381b46'}, :body => body)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Harvest
|
2
|
+
module API
|
3
|
+
class Projects < Base
|
4
|
+
api_model Harvest::Project
|
5
|
+
|
6
|
+
include Harvest::Behavior::Crud
|
7
|
+
|
8
|
+
def create_task(project, task_name)
|
9
|
+
response = request(:post, credentials, "/projects/#{project.to_i}/task_assignments/add_with_create_new_task", :body => task_xml(task_name))
|
10
|
+
id = response.headers["location"].first.match(/\/.*\/(\d+)\/.*\/(\d+)/)[1]
|
11
|
+
find(id)
|
12
|
+
end
|
13
|
+
|
14
|
+
def deactivate(project)
|
15
|
+
if project.active?
|
16
|
+
request(:put, credentials, "#{api_model.api_path}/#{project.to_i}/toggle", :headers => {'Content-Length' => '0'})
|
17
|
+
project.active = false
|
18
|
+
end
|
19
|
+
project
|
20
|
+
end
|
21
|
+
|
22
|
+
def activate(project)
|
23
|
+
if !project.active?
|
24
|
+
request(:put, credentials, "#{api_model.api_path}/#{project.to_i}/toggle", :headers => {'Content-Length' => '0'})
|
25
|
+
project.active = true
|
26
|
+
end
|
27
|
+
project
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def task_xml(name)
|
32
|
+
builder = Builder::XmlMarkup.new
|
33
|
+
builder.task do |t|
|
34
|
+
t.name(name)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Harvest
|
2
|
+
module API
|
3
|
+
class Reports < Base
|
4
|
+
|
5
|
+
def time_by_project(project, start_date, end_date, user = nil)
|
6
|
+
query = {:from => start_date.strftime("%Y%m%d"), :to => end_date.strftime("%Y%m%d")}
|
7
|
+
query[:user_id] = user.to_i if user
|
8
|
+
|
9
|
+
response = request(:get, credentials, "/projects/#{project.to_i}/entries", :query => query)
|
10
|
+
Harvest::TimeEntry.parse(massage_xml(response.body))
|
11
|
+
end
|
12
|
+
|
13
|
+
def time_by_user(user, start_date, end_date, project = nil)
|
14
|
+
query = {:from => start_date.strftime("%Y%m%d"), :to => end_date.strftime("%Y%m%d")}
|
15
|
+
query[:project_id] = project.to_i if project
|
16
|
+
|
17
|
+
response = request(:get, credentials, "/people/#{user.to_i}/entries", :query => query)
|
18
|
+
Harvest::TimeEntry.parse(massage_xml(response.body))
|
19
|
+
end
|
20
|
+
|
21
|
+
def expenses_by_user(user, start_date, end_date)
|
22
|
+
query = {:from => start_date.strftime("%Y%m%d"), :to => end_date.strftime("%Y%m%d")}
|
23
|
+
|
24
|
+
response = request(:get, credentials, "/people/#{user.to_i}/expenses", :query => query)
|
25
|
+
Harvest::Expense.parse(response.body)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def massage_xml(original_xml)
|
30
|
+
# this needs to be done because of the differences in dashes and underscores in the harvest api
|
31
|
+
xml = original_xml
|
32
|
+
%w(day-entry adjustment-record created-at project-id spent-at task-id timer-started-at updated-at user-id).each do |dash_field|
|
33
|
+
xml = xml.gsub(dash_field, dash_field.gsub("-", "_"))
|
34
|
+
end
|
35
|
+
xml
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Harvest
|
2
|
+
module API
|
3
|
+
class TaskAssignments < Base
|
4
|
+
|
5
|
+
def all(project)
|
6
|
+
response = request(:get, credentials, "/projects/#{project.to_i}/task_assignments")
|
7
|
+
Harvest::TaskAssignment.parse(response.body)
|
8
|
+
end
|
9
|
+
|
10
|
+
def find(project, id)
|
11
|
+
response = request(:get, credentials, "/projects/#{project.to_i}/task_assignments/#{id}")
|
12
|
+
Harvest::TaskAssignment.parse(response.body, :single => true)
|
13
|
+
end
|
14
|
+
|
15
|
+
def create(task_assignment)
|
16
|
+
response = request(:post, credentials, "/projects/#{task_assignment.project_id}/task_assignments", :body => task_assignment.task_xml)
|
17
|
+
id = response.headers["location"].first.match(/\/.*\/(\d+)\/.*\/(\d+)/)[2]
|
18
|
+
find(task_assignment.project_id, id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def update(task_assignment)
|
22
|
+
request(:put, credentials, "/projects/#{task_assignment.project_id}/task_assignments/#{task_assignment.to_i}", :body => task_assignment.to_xml)
|
23
|
+
find(task_assignment.project_id, task_assignment.id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(task_assignment)
|
27
|
+
request(:delete, credentials, "/projects/#{task_assignment.project_id}/task_assignments/#{task_assignment.to_i}")
|
28
|
+
task_assignment.id
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Harvest
|
2
|
+
module API
|
3
|
+
class Time < Base
|
4
|
+
|
5
|
+
def find(id)
|
6
|
+
response = request(:get, credentials, "/daily/show/#{id}")
|
7
|
+
Harvest::TimeEntry.parse(response.body, :single => true)
|
8
|
+
end
|
9
|
+
|
10
|
+
def all(date = ::Time.now)
|
11
|
+
date = ::Time.parse(date) if String === date
|
12
|
+
response = request(:get, credentials, "/daily/#{date.yday}/#{date.year}")
|
13
|
+
Harvest::TimeEntry.parse(response.body)
|
14
|
+
end
|
15
|
+
|
16
|
+
def create(entry)
|
17
|
+
response = request(:post, credentials, '/daily/add', :body => entry.to_xml)
|
18
|
+
Harvest::TimeEntry.parse(response.body).first
|
19
|
+
end
|
20
|
+
|
21
|
+
def update(entry)
|
22
|
+
request(:put, credentials, "/daily/update/#{entry.to_i}", :body => entry.to_xml)
|
23
|
+
find(entry.id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(entry)
|
27
|
+
request(:delete, credentials, "/daily/delete/#{entry.to_i}")
|
28
|
+
entry.id
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Harvest
|
2
|
+
module API
|
3
|
+
class UserAssignments < Base
|
4
|
+
|
5
|
+
def all(project)
|
6
|
+
response = request(:get, credentials, "/projects/#{project.to_i}/user_assignments")
|
7
|
+
Harvest::UserAssignment.parse(response.body)
|
8
|
+
end
|
9
|
+
|
10
|
+
def find(project, id)
|
11
|
+
response = request(:get, credentials, "/projects/#{project.to_i}/user_assignments/#{id}")
|
12
|
+
Harvest::UserAssignment.parse(response.body, :single => true)
|
13
|
+
end
|
14
|
+
|
15
|
+
def create(user_assignment)
|
16
|
+
response = request(:post, credentials, "/projects/#{user_assignment.project_id}/user_assignments", :body => user_assignment.user_xml)
|
17
|
+
id = response.headers["location"].first.match(/\/.*\/(\d+)\/.*\/(\d+)/)[2]
|
18
|
+
find(user_assignment.project_id, id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def update(user_assignment)
|
22
|
+
request(:put, credentials, "/projects/#{user_assignment.project_id}/user_assignments/#{user_assignment.id}", :body => user_assignment.to_xml)
|
23
|
+
find(user_assignment.project_id, user_assignment.id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(user_assignment)
|
27
|
+
request(:delete, credentials, "/projects/#{user_assignment.project_id}/user_assignments/#{user_assignment.to_i}")
|
28
|
+
user_assignment.id
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Harvest
|
2
|
+
module API
|
3
|
+
class Users < Base
|
4
|
+
api_model Harvest::User
|
5
|
+
|
6
|
+
include Harvest::Behavior::Crud
|
7
|
+
include Harvest::Behavior::Activatable
|
8
|
+
|
9
|
+
def reset_password(user)
|
10
|
+
request(:post, credentials, "#{api_model.api_path}/#{user.to_i}/reset_password")
|
11
|
+
user
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/harvest/base.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Harvest
|
2
|
+
class Base
|
3
|
+
attr_reader :request, :credentials
|
4
|
+
|
5
|
+
def initialize(subdomain, username, password, options = {})
|
6
|
+
options[:ssl] = true if options[:ssl].nil?
|
7
|
+
@credentials = Credentials.new(subdomain, username, password, options[:ssl])
|
8
|
+
raise InvalidCredentials unless credentials.valid?
|
9
|
+
end
|
10
|
+
|
11
|
+
def account
|
12
|
+
@account ||= Harvest::API::Account.new(credentials)
|
13
|
+
end
|
14
|
+
|
15
|
+
def clients
|
16
|
+
@clients ||= Harvest::API::Clients.new(credentials)
|
17
|
+
end
|
18
|
+
|
19
|
+
def contacts
|
20
|
+
@contacts ||= Harvest::API::Contacts.new(credentials)
|
21
|
+
end
|
22
|
+
|
23
|
+
def projects
|
24
|
+
@projects ||= Harvest::API::Projects.new(credentials)
|
25
|
+
end
|
26
|
+
|
27
|
+
def tasks
|
28
|
+
@tasks ||= Harvest::API::Tasks.new(credentials)
|
29
|
+
end
|
30
|
+
|
31
|
+
def users
|
32
|
+
@users ||= Harvest::API::Users.new(credentials)
|
33
|
+
end
|
34
|
+
|
35
|
+
def task_assignments
|
36
|
+
@task_assignments ||= Harvest::API::TaskAssignments.new(credentials)
|
37
|
+
end
|
38
|
+
|
39
|
+
def user_assignments
|
40
|
+
@user_assignments ||= Harvest::API::UserAssignments.new(credentials)
|
41
|
+
end
|
42
|
+
|
43
|
+
def expense_categories
|
44
|
+
@expense_categories ||= Harvest::API::ExpenseCategories.new(credentials)
|
45
|
+
end
|
46
|
+
|
47
|
+
def expenses
|
48
|
+
@expenses ||= Harvest::API::Expenses.new(credentials)
|
49
|
+
end
|
50
|
+
|
51
|
+
def time
|
52
|
+
@time ||= Harvest::API::Time.new(credentials)
|
53
|
+
end
|
54
|
+
|
55
|
+
def reports
|
56
|
+
@reports ||= Harvest::API::Reports.new(credentials)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|