xeroizer 2.15.5 → 2.15.6
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +14 -3
- data/lib/xeroizer.rb +5 -0
- data/lib/xeroizer/generic_application.rb +15 -9
- data/lib/xeroizer/http.rb +44 -34
- data/lib/xeroizer/models/account.rb +15 -11
- data/lib/xeroizer/models/allocation.rb +13 -0
- data/lib/xeroizer/models/attachment.rb +93 -0
- data/lib/xeroizer/models/bank_transaction.rb +3 -2
- data/lib/xeroizer/models/contact.rb +20 -12
- data/lib/xeroizer/models/contact_person.rb +20 -0
- data/lib/xeroizer/models/credit_note.rb +2 -0
- data/lib/xeroizer/models/expense_claim.rb +29 -0
- data/lib/xeroizer/models/invoice.rb +42 -29
- data/lib/xeroizer/models/line_item.rb +1 -0
- data/lib/xeroizer/models/organisation.rb +6 -1
- data/lib/xeroizer/models/payment.rb +16 -11
- data/lib/xeroizer/models/receipt.rb +39 -0
- data/lib/xeroizer/models/tax_component.rb +13 -0
- data/lib/xeroizer/models/tax_rate.rb +14 -3
- data/lib/xeroizer/models/user.rb +26 -0
- data/lib/xeroizer/oauth.rb +3 -3
- data/lib/xeroizer/record/base.rb +40 -31
- data/lib/xeroizer/record/base_model.rb +31 -25
- data/lib/xeroizer/record/base_model_http_proxy.rb +4 -1
- data/lib/xeroizer/record/record_association_helper.rb +35 -35
- data/lib/xeroizer/record/xml_helper.rb +1 -1
- data/lib/xeroizer/report/factory.rb +2 -1
- data/lib/xeroizer/version.rb +3 -0
- data/test/stub_responses/organisation.xml +30 -0
- data/test/stub_responses/tax_rates.xml +81 -1
- data/test/stub_responses/users.xml +17 -0
- data/test/unit/generic_application_test.rb +21 -0
- data/test/unit/http_test.rb +18 -0
- data/test/unit/models/bank_transaction_test.rb +1 -4
- data/test/unit/models/line_item_sum_test.rb +3 -2
- data/test/unit/models/tax_rate_test.rb +81 -0
- data/test/unit/oauth_test.rb +4 -2
- data/test/unit/record/base_test.rb +1 -1
- data/test/unit/record/model_definition_test.rb +9 -3
- data/test/unit/record/record_association_test.rb +1 -1
- data/test/unit/record_definition_test.rb +1 -1
- metadata +540 -205
- data/.bundle/config +0 -2
- data/.gitattributes +0 -22
- data/Gemfile +0 -20
- data/Gemfile.lock +0 -59
- data/Rakefile +0 -55
- data/VERSION +0 -1
- data/xeroizer.gemspec +0 -409
data/README.md
CHANGED
@@ -112,10 +112,10 @@ class XeroSessionController < ApplicationController
|
|
112
112
|
|
113
113
|
session[:xero_auth] = {
|
114
114
|
:access_token => @xero_client.access_token.token,
|
115
|
-
:access_key => @xero_client.access_token.
|
115
|
+
:access_key => @xero_client.access_token.secret }
|
116
116
|
|
117
|
-
session
|
118
|
-
|
117
|
+
session[:request_token] = nil
|
118
|
+
session[:request_secret] = nil
|
119
119
|
end
|
120
120
|
|
121
121
|
def destroy
|
@@ -549,6 +549,17 @@ client = Xeroizer::PublicApplication.new(YOUR_OAUTH_CONSUMER_KEY,
|
|
549
549
|
:rate_limit_sleep => 2)
|
550
550
|
```
|
551
551
|
|
552
|
+
Logging
|
553
|
+
---------------
|
554
|
+
|
555
|
+
You can add an optional paramater to the Xeroizer Application initialization, to pass a logger object that will need to respond_to :info. For example, in a rails app:
|
556
|
+
|
557
|
+
```ruby
|
558
|
+
XeroLogger = Logger.new('log/xero.log', 'weekly')
|
559
|
+
client = Xeroizer::PublicApplication.new(YOUR_OAUTH_CONSUMER_KEY,
|
560
|
+
YOUR_OAUTH_CONSUMER_SECRET,
|
561
|
+
:logger => XeroLogger)
|
562
|
+
```
|
552
563
|
|
553
564
|
### Contributors
|
554
565
|
Xeroizer was inspired by the https://github.com/tlconnor/xero_gateway gem created by Tim Connor
|
data/lib/xeroizer.rb
CHANGED
@@ -27,6 +27,7 @@ require 'xeroizer/configuration'
|
|
27
27
|
# Include models
|
28
28
|
require 'xeroizer/models/account'
|
29
29
|
require 'xeroizer/models/address'
|
30
|
+
require 'xeroizer/models/allocation'
|
30
31
|
require 'xeroizer/models/branding_theme'
|
31
32
|
require 'xeroizer/models/bank_transaction'
|
32
33
|
require 'xeroizer/models/bank_account'
|
@@ -35,6 +36,7 @@ require 'xeroizer/models/contact_group'
|
|
35
36
|
require 'xeroizer/models/credit_note'
|
36
37
|
require 'xeroizer/models/currency'
|
37
38
|
require 'xeroizer/models/employee'
|
39
|
+
require 'xeroizer/models/expense_claim'
|
38
40
|
require 'xeroizer/models/invoice'
|
39
41
|
require 'xeroizer/models/item'
|
40
42
|
require 'xeroizer/models/item_purchase_details'
|
@@ -48,9 +50,12 @@ require 'xeroizer/models/option'
|
|
48
50
|
require 'xeroizer/models/organisation'
|
49
51
|
require 'xeroizer/models/payment'
|
50
52
|
require 'xeroizer/models/phone'
|
53
|
+
require 'xeroizer/models/receipt'
|
51
54
|
require 'xeroizer/models/tax_rate'
|
55
|
+
require 'xeroizer/models/tax_component'
|
52
56
|
require 'xeroizer/models/tracking_category'
|
53
57
|
require 'xeroizer/models/tracking_category_child'
|
58
|
+
require 'xeroizer/models/user'
|
54
59
|
require 'xeroizer/models/journal_line_tracking_category'
|
55
60
|
|
56
61
|
require 'xeroizer/report/factory'
|
@@ -2,30 +2,34 @@ require 'xeroizer/record/application_helper'
|
|
2
2
|
|
3
3
|
module Xeroizer
|
4
4
|
class GenericApplication
|
5
|
-
|
5
|
+
|
6
6
|
include Http
|
7
7
|
extend Record::ApplicationHelper
|
8
|
-
|
9
|
-
attr_reader :client, :xero_url, :logger, :rate_limit_sleep, :rate_limit_max_attempts
|
10
|
-
|
8
|
+
|
9
|
+
attr_reader :client, :xero_url, :logger, :rate_limit_sleep, :rate_limit_max_attempts, :default_headers
|
10
|
+
|
11
11
|
extend Forwardable
|
12
12
|
def_delegators :client, :access_token
|
13
|
-
|
13
|
+
|
14
14
|
record :Account
|
15
|
+
record :Attachment
|
15
16
|
record :BrandingTheme
|
16
17
|
record :Contact
|
17
18
|
record :CreditNote
|
18
19
|
record :Currency
|
19
20
|
record :Employee
|
21
|
+
record :ExpenseClaim
|
20
22
|
record :Invoice
|
21
23
|
record :Item
|
22
24
|
record :Journal
|
23
25
|
record :ManualJournal
|
24
26
|
record :Organisation
|
25
27
|
record :Payment
|
28
|
+
record :Receipt
|
26
29
|
record :TaxRate
|
27
30
|
record :TrackingCategory
|
28
31
|
record :BankTransaction
|
32
|
+
record :User
|
29
33
|
|
30
34
|
report :AgedPayablesByContact
|
31
35
|
report :AgedReceivablesByContact
|
@@ -36,9 +40,9 @@ module Xeroizer
|
|
36
40
|
report :ExecutiveSummary
|
37
41
|
report :ProfitAndLoss
|
38
42
|
report :TrialBalance
|
39
|
-
|
43
|
+
|
40
44
|
public
|
41
|
-
|
45
|
+
|
42
46
|
# Never used directly. Use sub-classes instead.
|
43
47
|
# @see PublicApplication
|
44
48
|
# @see PrivateApplication
|
@@ -47,8 +51,10 @@ module Xeroizer
|
|
47
51
|
@xero_url = options[:xero_url] || "https://api.xero.com/api.xro/2.0"
|
48
52
|
@rate_limit_sleep = options[:rate_limit_sleep] || false
|
49
53
|
@rate_limit_max_attempts = options[:rate_limit_max_attempts] || 5
|
50
|
-
@
|
54
|
+
@default_headers = options[:default_headers] || {}
|
55
|
+
@client = OAuth.new(consumer_key, consumer_secret, options.merge({default_headers: default_headers}))
|
56
|
+
@logger = options[:logger] || false
|
51
57
|
end
|
52
|
-
|
58
|
+
|
53
59
|
end
|
54
60
|
end
|
data/lib/xeroizer/http.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# Copyright (c) 2008 Tim Connor <tlconnor@gmail.com>
|
2
|
-
#
|
2
|
+
#
|
3
3
|
# Permission to use, copy, modify, and/or distribute this software for any
|
4
4
|
# purpose with or without fee is hereby granted, provided that the above
|
5
5
|
# copyright notice and this permission notice appear in all copies.
|
6
|
-
#
|
6
|
+
#
|
7
7
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
8
8
|
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
9
9
|
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
@@ -14,12 +14,13 @@
|
|
14
14
|
|
15
15
|
module Xeroizer
|
16
16
|
module Http
|
17
|
-
|
17
|
+
class BadResponse < StandardError; end
|
18
|
+
|
18
19
|
ACCEPT_MIME_MAP = {
|
19
20
|
:pdf => 'application/pdf',
|
20
21
|
:json => 'application/json'
|
21
22
|
}
|
22
|
-
|
23
|
+
|
23
24
|
# Shortcut method for #http_request with `method` = :get.
|
24
25
|
#
|
25
26
|
# @param [OAuth] client OAuth client
|
@@ -48,21 +49,24 @@ module Xeroizer
|
|
48
49
|
def http_put(client, url, body, extra_params = {})
|
49
50
|
http_request(client, :put, url, body, extra_params)
|
50
51
|
end
|
51
|
-
|
52
|
+
|
52
53
|
private
|
53
|
-
|
54
|
+
|
54
55
|
def http_request(client, method, url, body, params = {})
|
55
56
|
# headers = {'Accept-Encoding' => 'gzip, deflate'}
|
56
57
|
|
57
|
-
headers = { 'charset' => 'utf-8' }
|
58
|
+
headers = self.default_headers.merge({ 'charset' => 'utf-8' })
|
58
59
|
|
59
60
|
if method != :get
|
60
61
|
headers['Content-Type'] ||= "application/x-www-form-urlencoded"
|
61
62
|
end
|
62
63
|
|
64
|
+
content_type = params.delete(:content_type)
|
65
|
+
headers['Content-Type'] = content_type if content_type
|
66
|
+
|
63
67
|
# HAX. Xero completely misuse the If-Modified-Since HTTP header.
|
64
68
|
headers['If-Modified-Since'] = params.delete(:ModifiedAfter).utc.strftime("%Y-%m-%dT%H:%M:%S") if params[:ModifiedAfter]
|
65
|
-
|
69
|
+
|
66
70
|
# Allow 'Accept' header to be specified with :accept parameter.
|
67
71
|
# Valid values are :pdf or :json.
|
68
72
|
if params[:response]
|
@@ -72,7 +76,7 @@ module Xeroizer
|
|
72
76
|
else response_type
|
73
77
|
end
|
74
78
|
end
|
75
|
-
|
79
|
+
|
76
80
|
if params.any?
|
77
81
|
url += "?" + params.map {|key,value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"}.join("&")
|
78
82
|
end
|
@@ -83,19 +87,20 @@ module Xeroizer
|
|
83
87
|
|
84
88
|
begin
|
85
89
|
attempts += 1
|
86
|
-
logger.info("
|
90
|
+
logger.info("XeroGateway Request: #{method.to_s.upcase} #{uri.request_uri}") if self.logger
|
91
|
+
|
92
|
+
raw_body = params.delete(:raw_body) ? body : {:xml => body}
|
87
93
|
|
88
94
|
response = case method
|
89
95
|
when :get then client.get(uri.request_uri, headers)
|
90
|
-
when :post then client.post(uri.request_uri,
|
91
|
-
when :put then client.put(uri.request_uri,
|
96
|
+
when :post then client.post(uri.request_uri, raw_body, headers)
|
97
|
+
when :put then client.put(uri.request_uri, raw_body, headers)
|
92
98
|
end
|
93
99
|
|
94
100
|
if self.logger
|
95
|
-
logger.info("
|
96
|
-
|
101
|
+
logger.info("XeroGateway Response (#{response.code})")
|
97
102
|
unless response.code.to_i == 200
|
98
|
-
logger.info("
|
103
|
+
logger.info("#{uri.request_uri}\n== Response Body\n\n#{response.plain_body}\n== End Response Body")
|
99
104
|
end
|
100
105
|
end
|
101
106
|
|
@@ -111,12 +116,12 @@ module Xeroizer
|
|
111
116
|
when 503
|
112
117
|
handle_oauth_error!(response)
|
113
118
|
else
|
114
|
-
|
119
|
+
handle_unknown_response_error!(response)
|
115
120
|
end
|
116
121
|
rescue Xeroizer::OAuth::RateLimitExceeded
|
117
122
|
if self.rate_limit_sleep
|
118
123
|
raise if attempts > rate_limit_max_attempts
|
119
|
-
logger.info("
|
124
|
+
logger.info("Rate limit exceeded, retrying") if self.logger
|
120
125
|
sleep_for(self.rate_limit_sleep)
|
121
126
|
retry
|
122
127
|
else
|
@@ -124,34 +129,35 @@ module Xeroizer
|
|
124
129
|
end
|
125
130
|
end
|
126
131
|
end
|
127
|
-
|
132
|
+
|
128
133
|
def handle_oauth_error!(response)
|
129
134
|
error_details = CGI.parse(response.plain_body)
|
130
135
|
description = error_details["oauth_problem_advice"].first
|
131
|
-
|
136
|
+
|
132
137
|
# see http://oauth.pbworks.com/ProblemReporting
|
133
138
|
# In addition to token_expired and token_rejected, Xero also returns
|
134
139
|
# 'rate limit exceeded' when more than 60 requests have been made in
|
135
140
|
# a second.
|
136
141
|
case (error_details["oauth_problem"].first)
|
137
|
-
when "token_expired"
|
138
|
-
when "token_rejected"
|
139
|
-
when "rate limit exceeded"
|
140
|
-
|
142
|
+
when "token_expired" then raise OAuth::TokenExpired.new(description)
|
143
|
+
when "token_rejected" then raise OAuth::TokenInvalid.new(description)
|
144
|
+
when "rate limit exceeded" then raise OAuth::RateLimitExceeded.new(description)
|
145
|
+
when error_details["oauth_problem"] then raise OAuth::UnknownError.new(error_details["oauth_problem"].first + ':' + description)
|
146
|
+
else raise OAuth::UnknownError.new("Xero API may be down or the way OAuth errors are provided by Xero may have changed.")
|
141
147
|
end
|
142
148
|
end
|
143
|
-
|
149
|
+
|
144
150
|
def handle_error!(response, request_body)
|
145
|
-
|
151
|
+
|
146
152
|
raw_response = response.plain_body
|
147
|
-
|
153
|
+
|
148
154
|
# XeroGenericApplication API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
|
149
155
|
# So let's ignore that :)
|
150
156
|
raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
|
151
|
-
|
157
|
+
|
152
158
|
# doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
|
153
159
|
doc = Nokogiri::XML(raw_response)
|
154
|
-
|
160
|
+
|
155
161
|
if doc && doc.root && doc.root.name == "ApiException"
|
156
162
|
|
157
163
|
raise ApiException.new(doc.root.xpath("Type").text,
|
@@ -161,13 +167,13 @@ module Xeroizer
|
|
161
167
|
request_body)
|
162
168
|
|
163
169
|
else
|
164
|
-
|
165
|
-
raise "Unparseable 400 Response: #{raw_response}"
|
166
|
-
|
170
|
+
|
171
|
+
raise BadResponse.new("Unparseable 400 Response: #{raw_response}")
|
172
|
+
|
167
173
|
end
|
168
|
-
|
174
|
+
|
169
175
|
end
|
170
|
-
|
176
|
+
|
171
177
|
def handle_object_not_found!(response, request_url)
|
172
178
|
case(request_url)
|
173
179
|
when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.")
|
@@ -176,9 +182,13 @@ module Xeroizer
|
|
176
182
|
end
|
177
183
|
end
|
178
184
|
|
185
|
+
def handle_unknown_response_error!(response)
|
186
|
+
raise BadResponse.new("Unknown response code: #{response.code.to_i}")
|
187
|
+
end
|
188
|
+
|
179
189
|
def sleep_for(seconds = 1)
|
180
190
|
sleep seconds
|
181
191
|
end
|
182
|
-
|
192
|
+
|
183
193
|
end
|
184
194
|
end
|
@@ -1,14 +1,18 @@
|
|
1
1
|
module Xeroizer
|
2
2
|
module Record
|
3
|
-
|
3
|
+
|
4
4
|
class AccountModel < BaseModel
|
5
|
-
|
6
|
-
set_permissions :read
|
7
|
-
|
5
|
+
|
6
|
+
set_permissions :read, :write
|
7
|
+
|
8
|
+
# The Accounts endpoint doesn't support the POST method yet.
|
9
|
+
def create_method
|
10
|
+
:http_put
|
11
|
+
end
|
8
12
|
end
|
9
|
-
|
13
|
+
|
10
14
|
class Account < Base
|
11
|
-
|
15
|
+
|
12
16
|
TYPE = {
|
13
17
|
'CURRENT' => '',
|
14
18
|
'FIXED' => '',
|
@@ -32,7 +36,7 @@ module Xeroizer
|
|
32
36
|
'INPUT' => 'GST on expenses',
|
33
37
|
'SRINPUT' => 'VAT on expenses',
|
34
38
|
'ZERORATEDINPUT' => 'Expense purchased from overseas (UK only)',
|
35
|
-
'RRINPUT' => 'Reduced rate VAT on expenses (UK Only)',
|
39
|
+
'RRINPUT' => 'Reduced rate VAT on expenses (UK Only)',
|
36
40
|
'EXEMPTOUTPUT' => 'VAT on sales exempt from VAT (UK only)',
|
37
41
|
'OUTPUT' => 'OUTPUT',
|
38
42
|
'OUTPUT2' => 'OUTPUT2',
|
@@ -42,9 +46,9 @@ module Xeroizer
|
|
42
46
|
'ZERORATED' => 'Zero-rated supplies/sales from overseas (NZ Only)',
|
43
47
|
'ECZROUTPUT' => 'Zero-rated EC Income (UK only)'
|
44
48
|
} unless defined?(TAX_TYPE)
|
45
|
-
|
49
|
+
|
46
50
|
set_primary_key :account_id
|
47
|
-
|
51
|
+
|
48
52
|
guid :account_id
|
49
53
|
string :code
|
50
54
|
string :name
|
@@ -60,8 +64,8 @@ module Xeroizer
|
|
60
64
|
string :bank_account_number
|
61
65
|
string :reporting_code
|
62
66
|
string :reporting_code_name
|
63
|
-
|
67
|
+
|
64
68
|
end
|
65
|
-
|
69
|
+
|
66
70
|
end
|
67
71
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Xeroizer
|
2
|
+
module Record
|
3
|
+
|
4
|
+
class AttachmentModel < BaseModel
|
5
|
+
|
6
|
+
module Extensions
|
7
|
+
def attach_data(id, filename, data, content_type = "application/octet-stream")
|
8
|
+
application.Attachment.attach_data(url, id, filename, data, content_type)
|
9
|
+
end
|
10
|
+
|
11
|
+
def attach_file(id, filename, path, content_type = "application/octet-stream")
|
12
|
+
application.Attachment.attach_file(url, id, filename, path, content_type)
|
13
|
+
end
|
14
|
+
|
15
|
+
def attachments(id)
|
16
|
+
application.Attachment.attachments_for(url, id)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
set_permissions :read
|
21
|
+
|
22
|
+
def attach_data(url, id, filename, data, content_type)
|
23
|
+
response_xml = @application.http_put(@application.client,
|
24
|
+
"#{url}/#{CGI.escape(id)}/Attachments/#{CGI.escape(filename)}",
|
25
|
+
data,
|
26
|
+
:raw_body => true, :content_type => content_type
|
27
|
+
)
|
28
|
+
response = parse_response(response_xml)
|
29
|
+
if (response_items = response.response_items) && response_items.size > 0
|
30
|
+
response_items.size == 1 ? response_items.first : response_items
|
31
|
+
else
|
32
|
+
response
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def attach_file(url, id, filename, path, content_type)
|
37
|
+
attach_data(url, id, filename, File.read(path), content_type)
|
38
|
+
end
|
39
|
+
|
40
|
+
def attachments_for(url, id)
|
41
|
+
response_xml = @application.http_get(@application.client,
|
42
|
+
"#{url}/#{CGI.escape(id)}/Attachments")
|
43
|
+
|
44
|
+
response = parse_response(response_xml)
|
45
|
+
if (response_items = response.response_items) && response_items.size > 0
|
46
|
+
response_items
|
47
|
+
else
|
48
|
+
[]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
class Attachment < Base
|
55
|
+
|
56
|
+
module Extensions
|
57
|
+
def attach_file(filename, path, content_type = "application/octet-stream")
|
58
|
+
parent.attach_file(id, filename, path, content_type)
|
59
|
+
end
|
60
|
+
|
61
|
+
def attach_data(filename, data, content_type = "application/octet-stream")
|
62
|
+
parent.attach_data(id, filename, data, content_type)
|
63
|
+
end
|
64
|
+
|
65
|
+
def attachments
|
66
|
+
parent.attachments(id)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
set_primary_key :attachment_id
|
71
|
+
|
72
|
+
guid :attachment_id
|
73
|
+
string :file_name
|
74
|
+
string :url
|
75
|
+
string :mime_type
|
76
|
+
integer :content_length
|
77
|
+
|
78
|
+
# Retrieve the attachment data.
|
79
|
+
# @param [String] filename optional filename to store the attachment in instead of returning the data.
|
80
|
+
def get(filename = nil)
|
81
|
+
data = parent.application.http_get(parent.application.client, url)
|
82
|
+
if filename
|
83
|
+
File.open(filename, "w") { | fp | fp.write data }
|
84
|
+
nil
|
85
|
+
else
|
86
|
+
data
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|