freeagent 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +17 -0
- data/Gemfile.lock +12 -10
- data/README.rdoc +13 -1
- data/TODO +22 -0
- data/freeagent.gemspec +4 -3
- data/lib/free_agent.rb +1 -0
- data/lib/free_agent/base.rb +132 -0
- data/lib/free_agent/contact.rb +68 -0
- data/lib/free_agent/invoice.rb +58 -0
- data/lib/free_agent/project.rb +20 -0
- data/lib/free_agent/task.rb +10 -0
- data/lib/free_agent/version.rb +1 -1
- data/spec/fixtures/bank_accounts/all.xml +24 -19
- data/spec/fixtures/bank_accounts/single.xml +6 -6
- data/spec/fixtures/contacts/all.xml +13 -13
- data/spec/fixtures/contacts/all_filter_all.xml +69 -0
- data/spec/fixtures/contacts/invoices.xml +75 -0
- data/spec/fixtures/contacts/single.xml +6 -6
- data/spec/fixtures/estimates/all.xml +38 -0
- data/spec/fixtures/expenses/all.xml +93 -0
- data/spec/fixtures/expenses/single.xml +31 -0
- data/spec/fixtures/expenses/single_with_project_id.xml +31 -0
- data/spec/fixtures/invoices/all.xml +112 -75
- data/spec/fixtures/invoices/single.xml +38 -37
- data/spec/fixtures/projects/invoices.xml +39 -0
- data/spec/fixtures/projects/single.xml +2 -2
- data/spec/fixtures/projects/tasks.xml +30 -0
- data/spec/fixtures/projects/timeslips.xml +25 -0
- data/spec/fixtures/tasks/single.xml +10 -0
- data/spec/fixtures/timeslips/all.xml +25 -0
- data/spec/fixtures/timeslips/single.xml +12 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/helper.rb +4 -4
- data/spec/support/mimic.rb +40 -25
- data/spec/unit/bank_account_spec.rb +1 -1
- data/spec/unit/base_spec.rb +113 -0
- data/spec/unit/contact_spec.rb +87 -1
- data/spec/unit/invoice_item_spec.rb +1 -1
- data/spec/unit/invoice_spec.rb +48 -2
- data/spec/unit/project_spec.rb +34 -0
- data/spec/unit/task_spec.rb +77 -0
- metadata +45 -7
data/CHANGELOG.rdoc
CHANGED
@@ -1,6 +1,23 @@
|
|
1
1
|
= Changelog
|
2
2
|
|
3
3
|
|
4
|
+
== Release 0.2.0
|
5
|
+
|
6
|
+
* NEW: Added support for Project#invoices.
|
7
|
+
|
8
|
+
* NEW: Added support for Task records.
|
9
|
+
|
10
|
+
* NEW: Added Contact validations.
|
11
|
+
|
12
|
+
* NEW: Added Dynamic Finders feature.
|
13
|
+
|
14
|
+
* NEW: Added support for Contact#invoices.
|
15
|
+
|
16
|
+
* NEW: Added support for Invoice status methods.
|
17
|
+
|
18
|
+
* CHANGED: Added .gemspec description and summary.
|
19
|
+
|
20
|
+
|
4
21
|
== Release 0.1.0
|
5
22
|
|
6
23
|
* Initial version
|
data/Gemfile.lock
CHANGED
@@ -6,25 +6,26 @@ PATH
|
|
6
6
|
GEM
|
7
7
|
remote: http://rubygems.org/
|
8
8
|
specs:
|
9
|
-
activemodel (3.0.
|
10
|
-
activesupport (= 3.0.
|
9
|
+
activemodel (3.0.7)
|
10
|
+
activesupport (= 3.0.7)
|
11
11
|
builder (~> 2.1.2)
|
12
12
|
i18n (~> 0.5.0)
|
13
|
-
activeresource (3.0.
|
14
|
-
activemodel (= 3.0.
|
15
|
-
activesupport (= 3.0.
|
16
|
-
activesupport (3.0.
|
13
|
+
activeresource (3.0.7)
|
14
|
+
activemodel (= 3.0.7)
|
15
|
+
activesupport (= 3.0.7)
|
16
|
+
activesupport (3.0.7)
|
17
17
|
builder (2.1.2)
|
18
18
|
diff-lcs (1.1.2)
|
19
19
|
i18n (0.5.0)
|
20
20
|
json (1.5.1)
|
21
|
-
mimic (0.4.
|
21
|
+
mimic (0.4.2)
|
22
22
|
json
|
23
23
|
plist
|
24
24
|
rack
|
25
25
|
sinatra
|
26
26
|
plist (3.1.0)
|
27
27
|
rack (1.2.2)
|
28
|
+
rr (1.0.2)
|
28
29
|
rspec (2.5.0)
|
29
30
|
rspec-core (~> 2.5.0)
|
30
31
|
rspec-expectations (~> 2.5.0)
|
@@ -33,10 +34,10 @@ GEM
|
|
33
34
|
rspec-expectations (2.5.0)
|
34
35
|
diff-lcs (~> 1.1.2)
|
35
36
|
rspec-mocks (2.5.0)
|
36
|
-
sinatra (1.2.
|
37
|
+
sinatra (1.2.3)
|
37
38
|
rack (~> 1.1)
|
38
|
-
tilt (
|
39
|
-
tilt (1.
|
39
|
+
tilt (>= 1.2.2, < 2.0)
|
40
|
+
tilt (1.3)
|
40
41
|
yard (0.6.7)
|
41
42
|
|
42
43
|
PLATFORMS
|
@@ -46,5 +47,6 @@ DEPENDENCIES
|
|
46
47
|
activeresource (>= 3.0.0)
|
47
48
|
freeagent!
|
48
49
|
mimic
|
50
|
+
rr
|
49
51
|
rspec (~> 2.5.0)
|
50
52
|
yard
|
data/README.rdoc
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
= FreeAgent Client
|
2
2
|
|
3
|
-
(Unofficial) Ruby client for the {FreeAgent
|
3
|
+
(Unofficial) Ruby client for the {FreeAgent}[http://fre.ag/4103hsny] accounting software.
|
4
|
+
|
5
|
+
|
6
|
+
== Status
|
7
|
+
|
8
|
+
This library is a work in progress.
|
9
|
+
|
10
|
+
Visit the {FreeAgent Central API}[http://www.freeagentcentral.com/developers/freeagent-api] page to learn more about FreeAgent API.
|
4
11
|
|
5
12
|
|
6
13
|
== Requirements
|
@@ -19,6 +26,11 @@
|
|
19
26
|
* {Simone Carletti}[http://www.simonecarletti.com] <weppos@weppos.net>
|
20
27
|
|
21
28
|
|
29
|
+
== Promo
|
30
|
+
|
31
|
+
Sign up with this {promo link}[http://fre.ag/4103hsny] and save 10% on the standard FreeAgent monthly price... forever!
|
32
|
+
|
33
|
+
|
22
34
|
== License
|
23
35
|
|
24
36
|
FreeAgent (the Client) is Copyright (c) 2011 Simone Carletti.
|
data/TODO
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
/verify
|
2
|
+
|
3
|
+
/company/invoice_timeline
|
4
|
+
/company/tax_timeline
|
5
|
+
|
6
|
+
/bills/types
|
7
|
+
|
8
|
+
/projects/project_id/timeslip
|
9
|
+
|
10
|
+
/timeslips
|
11
|
+
|
12
|
+
/company/users
|
13
|
+
|
14
|
+
/users/user_id/expenses
|
15
|
+
|
16
|
+
/expenses/types
|
17
|
+
|
18
|
+
?
|
19
|
+
|
20
|
+
/projects/project_id/notes
|
21
|
+
/projects/project_id/expenses
|
22
|
+
/projects/project_id/estimates
|
data/freeagent.gemspec
CHANGED
@@ -5,15 +5,15 @@ require "free_agent/version"
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = "freeagent"
|
7
7
|
s.version = FreeAgent::VERSION
|
8
|
-
s.summary = ""
|
9
|
-
s.description = ""
|
8
|
+
s.summary = "Ruby client for the FreeAgent API."
|
9
|
+
s.description = "Ruby client for the FreeAgent API."
|
10
10
|
|
11
11
|
s.platform = Gem::Platform::RUBY
|
12
12
|
s.required_ruby_version = ">= 1.8.7"
|
13
13
|
|
14
14
|
s.authors = ["Simone Carletti"]
|
15
15
|
s.email = ["weppos@weppos.net"]
|
16
|
-
s.homepage = ""
|
16
|
+
s.homepage = "https://github.com/weppos/freeagent"
|
17
17
|
|
18
18
|
s.files = `git ls-files`.split("\n")
|
19
19
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
@@ -24,4 +24,5 @@ Gem::Specification.new do |s|
|
|
24
24
|
s.add_development_dependency("yard")
|
25
25
|
s.add_development_dependency("activeresource", ">= 3.0.0")
|
26
26
|
s.add_development_dependency("mimic")
|
27
|
+
s.add_development_dependency("rr")
|
27
28
|
end
|
data/lib/free_agent.rb
CHANGED
data/lib/free_agent/base.rb
CHANGED
@@ -1,6 +1,138 @@
|
|
1
1
|
module FreeAgent
|
2
2
|
|
3
|
+
# The Base class for any FreeAgent resource.
|
4
|
+
#
|
5
|
+
# == Dynamic Finders
|
6
|
+
#
|
7
|
+
# Like ActiveRecord, this class support dynamic finders.
|
8
|
+
# You can use dynamic finders to search a resource by any of its attributes.
|
9
|
+
#
|
10
|
+
# class Contact < Base
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# Contact.find_by_email 'email@example.org'
|
14
|
+
# Contact.find_by_username_and_email 'weppos', 'email@example.org'
|
15
|
+
#
|
16
|
+
# However, there's one important caveat we should mention.
|
17
|
+
# This feature is not very efficient and the more records you have,
|
18
|
+
# the longer the query it takes.
|
19
|
+
#
|
20
|
+
# This is because record filtering is performed client-side.
|
21
|
+
# The library first loads and instantiate all records, then it filters them
|
22
|
+
# applying given criteria.
|
23
|
+
#
|
3
24
|
class Base < ActiveResource::Base
|
25
|
+
|
26
|
+
# = Active Record Dynamic Finder Match
|
27
|
+
#
|
28
|
+
# Refer to ActiveRecord::Base documentation for Dynamic attribute-based finders for detailed info.
|
29
|
+
#
|
30
|
+
class DynamicFinderMatch
|
31
|
+
def self.match(method)
|
32
|
+
finder = :first
|
33
|
+
bang = false
|
34
|
+
instantiator = nil
|
35
|
+
|
36
|
+
case method.to_s
|
37
|
+
when /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/
|
38
|
+
finder = :last if $1 == 'last_'
|
39
|
+
finder = :all if $1 == 'all_'
|
40
|
+
names = $2
|
41
|
+
when /^find_by_([_a-zA-Z]\w*)\!$/
|
42
|
+
bang = true
|
43
|
+
names = $1
|
44
|
+
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
|
45
|
+
instantiator = $1 == 'initialize' ? :new : :create
|
46
|
+
names = $2
|
47
|
+
else
|
48
|
+
return nil
|
49
|
+
end
|
50
|
+
|
51
|
+
new(finder, instantiator, bang, names.split('_and_'))
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(finder, instantiator, bang, attribute_names)
|
55
|
+
@finder = finder
|
56
|
+
@instantiator = instantiator
|
57
|
+
@bang = bang
|
58
|
+
@attribute_names = attribute_names
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_reader :finder, :attribute_names, :instantiator
|
62
|
+
|
63
|
+
def finder?
|
64
|
+
@finder && !@instantiator
|
65
|
+
end
|
66
|
+
|
67
|
+
def instantiator?
|
68
|
+
@finder == :first && @instantiator
|
69
|
+
end
|
70
|
+
|
71
|
+
def creator?
|
72
|
+
@finder == :first && @instantiator == :create
|
73
|
+
end
|
74
|
+
|
75
|
+
def bang?
|
76
|
+
@bang
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
class << self
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
def method_missing(method_id, *arguments, &block)
|
86
|
+
if match = DynamicFinderMatch.match(method_id)
|
87
|
+
attribute_names = match.attribute_names
|
88
|
+
if match.finder?
|
89
|
+
find_by_attributes(match, attribute_names, *arguments)
|
90
|
+
elsif match.instantiator?
|
91
|
+
find_or_instantiator_by_attributes(match, attribute_names, *arguments, &block)
|
92
|
+
end
|
93
|
+
else
|
94
|
+
super
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def find_by_attributes(match, attributes, *args)
|
99
|
+
conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}]
|
100
|
+
result = all.select do |record|
|
101
|
+
conditions.all? { |k,v| record.send(k) == v }
|
102
|
+
end
|
103
|
+
result = result.send(match.finder) if match.finder != :all
|
104
|
+
|
105
|
+
if match.bang? && result.blank?
|
106
|
+
raise ActiveResource::ResourceNotFound, 404, "Couldn't find #{self} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}"
|
107
|
+
else
|
108
|
+
result
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def find_or_instantiator_by_attributes(match, attributes, *args)
|
113
|
+
record = find_by_attributes(match, attributes, *args)
|
114
|
+
|
115
|
+
unless record
|
116
|
+
conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}]
|
117
|
+
record = new(conditions)
|
118
|
+
yield(record) if block_given?
|
119
|
+
record.save if match.instantiator == :create
|
120
|
+
end
|
121
|
+
|
122
|
+
record
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
def respond_to?(method_id, include_private = false)
|
129
|
+
if DynamicFinderMatch.match(method_id)
|
130
|
+
return true
|
131
|
+
end
|
132
|
+
|
133
|
+
super
|
134
|
+
end
|
135
|
+
|
4
136
|
end
|
5
137
|
|
6
138
|
end
|
data/lib/free_agent/contact.rb
CHANGED
@@ -1,7 +1,75 @@
|
|
1
1
|
module FreeAgent
|
2
2
|
|
3
3
|
# Represents a Contact in FreeAgent.
|
4
|
+
#
|
5
|
+
# @attr [String] first_name
|
6
|
+
# @attr [String] last_name
|
7
|
+
# @attr [String] organisation_name
|
8
|
+
#
|
4
9
|
class Contact < Base
|
10
|
+
|
11
|
+
# == Methods
|
12
|
+
#
|
13
|
+
# * .all
|
14
|
+
# * .first
|
15
|
+
# * .last
|
16
|
+
# * .find(id)
|
17
|
+
# * .new
|
18
|
+
# * .create
|
19
|
+
#
|
20
|
+
# * #save
|
21
|
+
# * #update
|
22
|
+
# * #destroy
|
23
|
+
#
|
24
|
+
# == Relationships
|
25
|
+
#
|
26
|
+
# - has_many :bills
|
27
|
+
# - has_many :estimates
|
28
|
+
# - has_many :projects
|
29
|
+
# * has_many :invoices
|
30
|
+
|
31
|
+
|
32
|
+
validates_presence_of :first_name, :unless => :organisation_name?
|
33
|
+
validates_presence_of :last_name, :unless => :organisation_name?
|
34
|
+
validates_presence_of :organisation_name, :unless => :name?
|
35
|
+
|
36
|
+
schema do
|
37
|
+
attribute :first_name, :string
|
38
|
+
attribute :last_name, :string
|
39
|
+
attribute :organisation_name, :string
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
# Creates the name of the contact,
|
44
|
+
# composed by the interpolation of {#first_name} and {#last_name}.
|
45
|
+
#
|
46
|
+
# @return [String,nil] The Contact name.
|
47
|
+
def name
|
48
|
+
attrs = [first_name, last_name].reject(&:blank?)
|
49
|
+
attrs.empty? ? nil : attrs.join(" ")
|
50
|
+
end
|
51
|
+
|
52
|
+
alias :name? :name
|
53
|
+
|
54
|
+
|
55
|
+
# Gets all the invoices associated to this contact.
|
56
|
+
#
|
57
|
+
# @overload invoices(options = {})
|
58
|
+
# Gets all the invoices for this contact.
|
59
|
+
# @param [Hash] options Hash of options to customize the finder behavior.
|
60
|
+
# @return [Array<FreeAgent::Invoice>]
|
61
|
+
#
|
62
|
+
# @example Simple query
|
63
|
+
# contact.invoices
|
64
|
+
# # => [...]
|
65
|
+
# @example Query with custom find params
|
66
|
+
# contact.invoices(:params => { :foo => 'bar' }
|
67
|
+
# # => [...]
|
68
|
+
#
|
69
|
+
def invoices(*args)
|
70
|
+
options = args.extract_options!
|
71
|
+
Invoice.all(options.merge!(:from => "/contacts/#{id}/invoices.xml"))
|
72
|
+
end
|
5
73
|
end
|
6
74
|
|
7
75
|
end
|
data/lib/free_agent/invoice.rb
CHANGED
@@ -2,6 +2,64 @@ module FreeAgent
|
|
2
2
|
|
3
3
|
# Represents an Invoice in FreeAgent.
|
4
4
|
class Invoice < Base
|
5
|
+
|
6
|
+
# == Methods
|
7
|
+
#
|
8
|
+
# * .all
|
9
|
+
# * .first
|
10
|
+
# * .last
|
11
|
+
# * .find(id)
|
12
|
+
# * .new
|
13
|
+
# * .create
|
14
|
+
#
|
15
|
+
# * #save
|
16
|
+
# * #update
|
17
|
+
# * #destroy
|
18
|
+
# * #mark_as_draft
|
19
|
+
# * #mark_as_sent
|
20
|
+
# * #mark_as_cancelled
|
21
|
+
|
22
|
+
# Marks the current invoice as draft.
|
23
|
+
#
|
24
|
+
# This method actually performs a new HTTP request
|
25
|
+
# to FreeAgent in order to execute the update.
|
26
|
+
#
|
27
|
+
# @return [void]
|
28
|
+
def mark_as_draft
|
29
|
+
# put(:mark_as_draft).tap do |response|
|
30
|
+
# load_attributes_from_response(response)
|
31
|
+
# end
|
32
|
+
put(:mark_as_draft) && reload
|
33
|
+
end
|
34
|
+
|
35
|
+
# Marks the current invoice as sent.
|
36
|
+
#
|
37
|
+
# This method actually performs a new HTTP request
|
38
|
+
# to FreeAgent in order to execute the update.
|
39
|
+
# It also triggers any automatic deliver if the
|
40
|
+
# {#send_new_invoice_emails} invoice attribute is set to true.
|
41
|
+
#
|
42
|
+
# @return [void]
|
43
|
+
def mark_as_sent
|
44
|
+
# put(:mark_as_sent).tap do |response|
|
45
|
+
# load_attributes_from_response(response)
|
46
|
+
# end
|
47
|
+
put(:mark_as_sent) && reload
|
48
|
+
end
|
49
|
+
|
50
|
+
# Marks the current invoice as cancelled.
|
51
|
+
#
|
52
|
+
# This method actually performs a new HTTP request
|
53
|
+
# to FreeAgent in order to execute the update.
|
54
|
+
#
|
55
|
+
# @return [void]
|
56
|
+
def mark_as_cancelled
|
57
|
+
# put(:mark_as_cancelled).tap do |response|
|
58
|
+
# load_attributes_from_response(response)
|
59
|
+
# end
|
60
|
+
put(:mark_as_cancelled) && reload
|
61
|
+
end
|
62
|
+
|
5
63
|
end
|
6
64
|
|
7
65
|
end
|
data/lib/free_agent/project.rb
CHANGED
@@ -2,6 +2,26 @@ module FreeAgent
|
|
2
2
|
|
3
3
|
# Represents a Project in FreeAgent.
|
4
4
|
class Project < Base
|
5
|
+
|
6
|
+
# Gets all the invoices associated to this project.
|
7
|
+
#
|
8
|
+
# @overload invoices(options = {})
|
9
|
+
# Gets all the invoices for this project.
|
10
|
+
# @param [Hash] options Hash of options to customize the finder behavior.
|
11
|
+
# @return [Array<FreeAgent::Invoice>]
|
12
|
+
#
|
13
|
+
# @example Simple query
|
14
|
+
# project.invoices
|
15
|
+
# # => [...]
|
16
|
+
# @example Query with custom find params
|
17
|
+
# project.invoices(:params => { :foo => 'bar' }
|
18
|
+
# # => [...]
|
19
|
+
#
|
20
|
+
def invoices(*args)
|
21
|
+
options = args.extract_options!
|
22
|
+
Invoice.all(options.merge!(:from => "/projects/#{id}/invoices.xml"))
|
23
|
+
end
|
24
|
+
|
5
25
|
end
|
6
26
|
|
7
27
|
end
|