xeroizer 2.15.5 → 2.15.6
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/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
|