costagent 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +12 -0
- data/lib/costagent.rb +150 -84
- metadata +4 -4
data/README.rdoc
CHANGED
@@ -12,6 +12,8 @@ This isn't meant to be an entire Ruby library for the FA API - this is simply su
|
|
12
12
|
|
13
13
|
This assumes that when calculating money earnt, you want it in GBP, and it only copes with calculating timeslips either in GBP, or in USD (which is converted using xe.com where possible).
|
14
14
|
|
15
|
+
Version 0.2.0 introduces some breaking changes to the existing API - namely moving from structs to dedicated classes backed by hashes for the FreeAgent resources. It also introduces a couple of new methods, and the ability for a third party caching provider to be plugged in - but this now means that there is NO in memory caching of anything (such as projects) at all. In short, just using this library and not supplying even a basic cache provider will mean that every single request will result in an HTTP call. Some default caching providers coming soon.
|
16
|
+
|
15
17
|
== SYNOPSIS:
|
16
18
|
|
17
19
|
To initialize:
|
@@ -20,6 +22,12 @@ costagent = CostAgent.new subdomain, username, password
|
|
20
22
|
|
21
23
|
(remember you need to enable API access from within your FreeAgent account)
|
22
24
|
|
25
|
+
To set a caching provider to limit repeat lookups to FreeAgent:
|
26
|
+
|
27
|
+
CostAgent.cache_provider = MyCacheProvider.new
|
28
|
+
|
29
|
+
(see the tests for a reference in memory cache provider)
|
30
|
+
|
23
31
|
To see all active projects:
|
24
32
|
|
25
33
|
projects = costagent.projects
|
@@ -52,6 +60,10 @@ To return your FA user ID:
|
|
52
60
|
|
53
61
|
costagent.user_id
|
54
62
|
|
63
|
+
And to return all details about the logged in user:
|
64
|
+
|
65
|
+
costagent.user
|
66
|
+
|
55
67
|
To return a total of the amount of hours worked for a specific timeframe:
|
56
68
|
|
57
69
|
hours = costagent.worked(start_date, end_date)
|
data/lib/costagent.rb
CHANGED
@@ -6,14 +6,53 @@ require "open-uri"
|
|
6
6
|
|
7
7
|
# This exposes additional billable tracking functionality around the Freeagent API
|
8
8
|
class CostAgent
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
Invoice = Struct.new(:id, :project_id, :description, :reference, :amount, :status, :date, :due, :items)
|
13
|
-
InvoiceItem = Struct.new(:id, :invoice_id, :project_id, :item_type, :description, :price, :quantity, :cost)
|
9
|
+
# This provides shared functionality for all of the wrapped FA data types
|
10
|
+
class Base
|
11
|
+
attr_accessor :data
|
14
12
|
|
13
|
+
def initialize(data = {})
|
14
|
+
@data = data
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing(name, *args)
|
18
|
+
key = name.to_s
|
19
|
+
if key[key.length - 1] == "="
|
20
|
+
@data[key[0...key.length - 1]] = args.first
|
21
|
+
end
|
22
|
+
@data[key] || @data[key.to_sym]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Our FA data types
|
27
|
+
class Project < Base; end
|
28
|
+
class Timeslip < Base; end
|
29
|
+
class Task < Base; end
|
30
|
+
class Invoice < Base; end
|
31
|
+
class InvoiceItem < Base; end
|
32
|
+
class User < Base; end
|
33
|
+
|
34
|
+
# Our configuration for FA access
|
15
35
|
attr_accessor :subdomain, :username, :password
|
16
36
|
|
37
|
+
# Our cache provider, to help to limit the amount of requests and lookups to FA
|
38
|
+
class << self
|
39
|
+
attr_accessor :cache_provider
|
40
|
+
end
|
41
|
+
|
42
|
+
# This calls out to the external third party provider for caching
|
43
|
+
def cache(resource, identifier, reload = false, &block)
|
44
|
+
if CostAgent.cache_provider.nil?
|
45
|
+
block.call
|
46
|
+
else
|
47
|
+
if (!reload && CostAgent.cache_provider.exists?(self.subdomain, resource, identifier))
|
48
|
+
CostAgent.cache_provider.get(self.subdomain, resource, identifier)
|
49
|
+
else
|
50
|
+
CostAgent.cache_provider.set(self.subdomain, resource, identifier, block.call)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Initialize and validate input data
|
17
56
|
def initialize(subdomain, username, password)
|
18
57
|
self.subdomain = subdomain
|
19
58
|
self.username = username
|
@@ -25,101 +64,117 @@ class CostAgent
|
|
25
64
|
end
|
26
65
|
|
27
66
|
# Returns all projects
|
28
|
-
def projects(filter = "active")
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
67
|
+
def projects(filter = "active", reload = false)
|
68
|
+
self.cache(CostAgent::Project, filter, reload) do
|
69
|
+
(self.api("projects", {:view => filter})/"project").collect do |project|
|
70
|
+
billing_rate = (project/"normal-billing-rate").text.to_f
|
71
|
+
hours_per_day = (project/"hours-per-day").text.to_f
|
72
|
+
billing_period = (project/"billing-period").text
|
73
|
+
hourly_rate = (billing_period == "hour" ? billing_rate : billing_rate / hours_per_day)
|
74
|
+
daily_rate = (billing_period == "hour" ? billing_rate * hours_per_day : billing_rate)
|
75
|
+
Project.new(
|
76
|
+
:id => (project/"id").text.to_i,
|
77
|
+
:name => (project/"name").text,
|
78
|
+
:currency => (project/"currency").text,
|
79
|
+
:hourly_billing_rate => hourly_rate,
|
80
|
+
:daily_billing_rate => daily_rate,
|
81
|
+
:hours_per_day => hours_per_day)
|
82
|
+
end
|
42
83
|
end
|
43
84
|
end
|
44
|
-
|
85
|
+
|
45
86
|
# This returns the specified project
|
46
87
|
def project(id)
|
47
88
|
self.projects("all").detect { |p| p.id == id }
|
48
89
|
end
|
49
90
|
|
50
91
|
# This returns all timeslips for the specified date range, with additional cost information
|
51
|
-
def timeslips(start_date = DateTime.now, end_date = start_date)
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
92
|
+
def timeslips(start_date = DateTime.now, end_date = start_date, reload = false)
|
93
|
+
self.cache(CostAgent::Timeslip, "#{start_date.strftime("%Y-%m-%d")}_#{end_date.strftime("%Y-%m-%d")}", reload) do
|
94
|
+
timeslips = (self.api("timeslips", :view => "#{start_date.strftime("%Y-%m-%d")}_#{end_date.strftime("%Y-%m-%d")}")/"timeslip").collect do |timeslip|
|
95
|
+
# Find the project and hours for this timeslip
|
96
|
+
project = self.project((timeslip/"project-id").text.to_i)
|
97
|
+
if project
|
98
|
+
task = self.tasks(project.id).detect { |t| t.id == (timeslip/"task-id").text.to_i }
|
99
|
+
hours = (timeslip/"hours").text.to_f
|
100
|
+
cost = (task.nil? ? project : task).hourly_billing_rate * hours
|
101
|
+
# Build the timeslip out using the timeslip data and the project it's tied to
|
102
|
+
Timeslip.new(
|
103
|
+
:id => (timeslip/"id").text.to_i,
|
104
|
+
:project_id => project.id,
|
105
|
+
:project => project,
|
106
|
+
:task_id => task.id,
|
107
|
+
:task => task,
|
108
|
+
:hours => hours,
|
109
|
+
:date => DateTime.parse((timeslip/"dated-on").text),
|
110
|
+
:cost => cost,
|
111
|
+
:comment => (timeslip/"comment").text,
|
112
|
+
:status => (timeslip/"status").text)
|
113
|
+
else
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
end - [nil]
|
117
|
+
end
|
72
118
|
end
|
73
119
|
|
74
120
|
# This returns all tasks for the specified project_id
|
75
|
-
def tasks(project_id)
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
121
|
+
def tasks(project_id, reload = false)
|
122
|
+
self.cache(CostAgent::Task, project_id, reload) do
|
123
|
+
(self.api("projects/#{project_id}/tasks")/"task").collect do |task|
|
124
|
+
# Find the project for this task
|
125
|
+
project = self.project((task/"project-id").text.to_i)
|
126
|
+
# Calculate rates
|
127
|
+
billing_rate = (task/"billing-rate").text.to_f
|
128
|
+
billing_period = (task/"billing-period").text
|
129
|
+
hourly_rate = (billing_period == "hour" ? billing_rate : billing_rate / project.hours_per_day)
|
130
|
+
daily_rate = (billing_period == "hour" ? billing_rate * project.hours_per_day : billing_rate)
|
131
|
+
# Build the task out using the task data and the project it's tied to
|
132
|
+
Task.new(
|
133
|
+
:id => (task/"id").text.to_i,
|
134
|
+
:name => (task/"name").text,
|
135
|
+
:project_id => project.id,
|
136
|
+
:project => project,
|
137
|
+
:hourly_billing_rate => hourly_rate,
|
138
|
+
:daily_billing_rate => daily_rate,
|
139
|
+
:billable => (task/"is-billable").text == "true")
|
140
|
+
end
|
91
141
|
end
|
92
142
|
end
|
93
143
|
|
94
144
|
# This returns all invoices
|
95
|
-
def invoices
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
(item/"id").first.inner_text.to_i
|
103
|
-
(
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
145
|
+
def invoices(reload = false)
|
146
|
+
self.cache(CostAgent::Invoice, :all, reload) do
|
147
|
+
(self.api("invoices")/"invoice").collect do |invoice|
|
148
|
+
items = (invoice/"invoice-item").collect do |item|
|
149
|
+
price = (item/"price").first.inner_text.to_f
|
150
|
+
quantity = (item/"quantity").first.inner_text.to_f
|
151
|
+
cost = price * quantity
|
152
|
+
project = self.project((item/"project-id").first.inner_text.to_i)
|
153
|
+
InvoiceItem.new(
|
154
|
+
:id => (item/"id").first.inner_text.to_i,
|
155
|
+
:invoice_id => (item/"invoice-id").first.inner_text.to_i,
|
156
|
+
:project_id => project.id,
|
157
|
+
:project => project,
|
158
|
+
:item_type => (item/"item-type").first.inner_text,
|
159
|
+
:description => (item/"description").first.inner_text,
|
160
|
+
:price => price,
|
161
|
+
:quantity => quantity,
|
162
|
+
:cost => cost)
|
163
|
+
end
|
164
|
+
project = self.project((invoice/"project-id").first.inner_text.to_i)
|
165
|
+
Invoice.new(
|
166
|
+
:id => (invoice/"id").first.inner_text.to_i,
|
167
|
+
:project_id => project.id,
|
168
|
+
:project => project,
|
169
|
+
:description => (invoice/"description").first.inner_text,
|
170
|
+
:reference => (invoice/"reference").text,
|
171
|
+
:amount => (invoice/"net-value").text.to_f,
|
172
|
+
:status => (invoice/"status").text,
|
173
|
+
:date => DateTime.parse((invoice/"dated-on").text),
|
174
|
+
:due => DateTime.parse((invoice/"due-on").text),
|
175
|
+
:items => items)
|
110
176
|
end
|
111
|
-
Invoice.new(
|
112
|
-
(invoice/"id").first.inner_text.to_i,
|
113
|
-
(invoice/"project-id").first.inner_text.to_i,
|
114
|
-
(invoice/"description").first.inner_text,
|
115
|
-
(invoice/"reference").text,
|
116
|
-
(invoice/"net-value").text.to_f,
|
117
|
-
(invoice/"status").text,
|
118
|
-
DateTime.parse((invoice/"dated-on").text),
|
119
|
-
DateTime.parse((invoice/"due-on").text),
|
120
|
-
items)
|
121
177
|
end
|
122
|
-
@invoices
|
123
178
|
end
|
124
179
|
|
125
180
|
# This returns the specific invoice by ID
|
@@ -127,9 +182,20 @@ class CostAgent
|
|
127
182
|
self.invoices.detect { |i| i.id == id }
|
128
183
|
end
|
129
184
|
|
185
|
+
# This contains the logged in user information for the configured credentials
|
186
|
+
def user(reload = false)
|
187
|
+
self.cache(CostAgent::User, self.username, reload) do
|
188
|
+
data = self.client("verify").get.headers
|
189
|
+
[User.new(
|
190
|
+
:id => data[:user_id],
|
191
|
+
:permissions => data[:user_permission_level],
|
192
|
+
:company_type => data[:company_type])]
|
193
|
+
end.first
|
194
|
+
end
|
195
|
+
|
130
196
|
# This looks up the user ID using the CostAgent credentials
|
131
197
|
def user_id
|
132
|
-
self.
|
198
|
+
self.user.id
|
133
199
|
end
|
134
200
|
|
135
201
|
# This returns the amount of hours worked
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
version: 0.2.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Elliott Draper
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-10-
|
17
|
+
date: 2010-10-13 00:00:00 +01:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|