freeagent 0.1.0 → 0.2.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/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
|