xero_gateway-float 2.0.18 → 2.1.1
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/Rakefile +7 -1
- data/examples/partner_app.rb +2 -2
- data/lib/xero_gateway/accounts_list.rb +0 -4
- data/lib/xero_gateway/bank_transaction.rb +0 -2
- data/lib/xero_gateway/contact.rb +6 -9
- data/lib/xero_gateway/credit_note.rb +1 -3
- data/lib/xero_gateway/error.rb +16 -0
- data/lib/xero_gateway/exceptions.rb +5 -0
- data/lib/xero_gateway/gateway.rb +184 -82
- data/lib/xero_gateway/http.rb +38 -32
- data/lib/xero_gateway/invoice.rb +17 -9
- data/lib/xero_gateway/journal_line.rb +102 -0
- data/lib/xero_gateway/line_item_calculations.rb +0 -4
- data/lib/xero_gateway/manual_journal.rb +163 -0
- data/lib/xero_gateway/oauth.rb +29 -23
- data/lib/xero_gateway/partner_app.rb +6 -1
- data/lib/xero_gateway/response.rb +2 -0
- data/lib/xero_gateway/tax_rate.rb +1 -1
- data/lib/xero_gateway.rb +3 -0
- data/test/integration/accounts_list_test.rb +4 -4
- data/test/integration/create_invoice_test.rb +6 -0
- data/test/integration/create_manual_journal_test.rb +35 -0
- data/test/integration/create_payments_test.rb +35 -0
- data/test/integration/get_invoice_test.rb +27 -12
- data/test/integration/get_manual_journal_test.rb +50 -0
- data/test/integration/get_manual_journals_test.rb +88 -0
- data/test/integration/update_manual_journal_test.rb +31 -0
- data/test/test_helper.rb +39 -1
- data/test/unit/bank_transaction_test.rb +1 -1
- data/test/unit/credit_note_test.rb +1 -1
- data/test/unit/gateway_test.rb +15 -15
- data/test/unit/invoice_test.rb +3 -2
- data/test/unit/manual_journal_test.rb +93 -0
- data/test/unit/payment_test.rb +34 -0
- data/xero_gateway.gemspec +2 -2
- metadata +11 -2
data/lib/xero_gateway/http.rb
CHANGED
@@ -7,26 +7,28 @@ module XeroGateway
|
|
7
7
|
def log(str)
|
8
8
|
XeroGateway.log("HTTP : "+str)
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
|
+
def http_get(client, url, extra_params = {}, headers = {})
|
11
12
|
log "get | #{url} :: #{extra_params.inspect}"
|
12
|
-
http_request(client, :get, url, nil, extra_params)
|
13
|
+
http_request(client, :get, url, nil, extra_params, headers)
|
13
14
|
end
|
14
15
|
|
15
|
-
def http_post(client, url, body, extra_params = {})
|
16
|
+
def http_post(client, url, body, extra_params = {}, headers = {})
|
16
17
|
log "post | #{url} :: #{extra_params.inspect}"
|
17
|
-
http_request(client, :post, url, body, extra_params)
|
18
|
+
http_request(client, :post, url, body, extra_params, headers)
|
18
19
|
end
|
19
20
|
|
20
|
-
def http_put(client, url, body, extra_params = {})
|
21
|
+
def http_put(client, url, body, extra_params = {}, headers = {})
|
21
22
|
log "put | #{url} :: #{extra_params.inspect}"
|
22
|
-
http_request(client, :put, url, body, extra_params)
|
23
|
+
http_request(client, :put, url, body, extra_params, headers)
|
23
24
|
end
|
24
25
|
|
25
26
|
private
|
26
|
-
|
27
|
-
def http_request(client, method, url, body, params = {})
|
27
|
+
|
28
|
+
def http_request(client, method, url, body, params = {}, headers = {})
|
28
29
|
# headers = {'Accept-Encoding' => 'gzip, deflate'}
|
29
|
-
|
30
|
+
|
31
|
+
headers = headers.merge!('charset' => 'utf-8')
|
30
32
|
|
31
33
|
if method != :get
|
32
34
|
headers['Content-Type'] ||= "application/x-www-form-urlencoded"
|
@@ -45,39 +47,43 @@ module XeroGateway
|
|
45
47
|
|
46
48
|
# # Only setup @cached_http once on first use as loading the CA file is quite expensive computationally.
|
47
49
|
# unless @cached_http && @cached_http.address == uri.host && @cached_http.port == uri.port
|
48
|
-
# @cached_http = Net::HTTP.new(uri.host, uri.port)
|
50
|
+
# @cached_http = Net::HTTP.new(uri.host, uri.port)
|
49
51
|
# @cached_http.open_timeout = OPEN_TIMEOUT
|
50
52
|
# @cached_http.read_timeout = READ_TIMEOUT
|
51
53
|
# @cached_http.use_ssl = true
|
52
|
-
#
|
54
|
+
#
|
53
55
|
# # Need to validate server's certificate against root certificate authority to prevent man-in-the-middle attacks.
|
54
56
|
# @cached_http.ca_file = ROOT_CA_FILE
|
55
57
|
# # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
56
58
|
# @cached_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
57
59
|
# @cached_http.verify_depth = 5
|
58
60
|
# end
|
59
|
-
|
61
|
+
|
60
62
|
logger.info("\n== [#{Time.now.to_s}] XeroGateway Request: #{uri.request_uri} ") if self.logger
|
61
|
-
|
63
|
+
|
62
64
|
response = case method
|
63
65
|
when :get then client.get(uri.request_uri, headers)
|
64
66
|
when :post then client.post(uri.request_uri, { :xml => body }, headers)
|
65
67
|
when :put then client.put(uri.request_uri, { :xml => body }, headers)
|
66
68
|
end
|
67
|
-
|
69
|
+
|
68
70
|
if self.logger
|
69
71
|
logger.info("== [#{Time.now.to_s}] XeroGateway Response (#{response.code})")
|
70
|
-
|
72
|
+
|
71
73
|
unless response.code.to_i == 200
|
72
74
|
logger.info("== #{uri.request_uri} Response Body \n\n #{response.plain_body} \n == End Response Body")
|
73
75
|
end
|
74
76
|
end
|
75
|
-
|
77
|
+
|
76
78
|
case response.code.to_i
|
77
79
|
when 200
|
78
|
-
|
80
|
+
if RUBY_VERSION >= "1.9"
|
81
|
+
response.plain_body.force_encoding("UTF-8")
|
82
|
+
else
|
83
|
+
response.plain_body
|
84
|
+
end
|
79
85
|
when 400
|
80
|
-
handle_error!(body, response)
|
86
|
+
handle_error!(body, response)
|
81
87
|
when 401
|
82
88
|
handle_oauth_error!(response)
|
83
89
|
when 404
|
@@ -88,11 +94,11 @@ module XeroGateway
|
|
88
94
|
raise "Unknown response code: #{response.code.to_i}"
|
89
95
|
end
|
90
96
|
end
|
91
|
-
|
97
|
+
|
92
98
|
def handle_oauth_error!(response)
|
93
99
|
error_details = CGI.parse(response.plain_body)
|
94
100
|
description = error_details["oauth_problem_advice"].first
|
95
|
-
|
101
|
+
|
96
102
|
# see http://oauth.pbworks.com/ProblemReporting
|
97
103
|
# In addition to token_expired and token_rejected, Xero also returns
|
98
104
|
# 'rate limit exceeded' when more than 60 requests have been made in
|
@@ -105,32 +111,32 @@ module XeroGateway
|
|
105
111
|
else raise OAuth::UnknownError.new(error_details["oauth_problem"].first + ':' + description)
|
106
112
|
end
|
107
113
|
end
|
108
|
-
|
114
|
+
|
109
115
|
def handle_error!(request_xml, response)
|
110
|
-
|
116
|
+
|
111
117
|
raw_response = response.plain_body
|
112
|
-
|
118
|
+
|
113
119
|
# Xero Gateway API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
|
114
120
|
# So let's ignore that :)
|
115
121
|
raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
|
116
|
-
|
122
|
+
|
117
123
|
doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
|
118
|
-
|
124
|
+
|
119
125
|
if doc.root.name == "ApiException"
|
120
126
|
|
121
|
-
raise ApiException.new(doc.root.elements["Type"].text,
|
127
|
+
raise ApiException.new(doc.root.elements["Type"].text,
|
122
128
|
doc.root.elements["Message"].text,
|
123
|
-
request_xml,
|
129
|
+
request_xml,
|
124
130
|
raw_response)
|
125
131
|
|
126
132
|
else
|
127
|
-
|
133
|
+
|
128
134
|
raise "Unparseable 400 Response: #{raw_response}"
|
129
|
-
|
135
|
+
|
130
136
|
end
|
131
|
-
|
137
|
+
|
132
138
|
end
|
133
|
-
|
139
|
+
|
134
140
|
def handle_object_not_found!(response, request_url)
|
135
141
|
case(request_url)
|
136
142
|
when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.")
|
@@ -139,6 +145,6 @@ module XeroGateway
|
|
139
145
|
else raise ObjectNotFound.new(request_url)
|
140
146
|
end
|
141
147
|
end
|
142
|
-
|
148
|
+
|
143
149
|
end
|
144
150
|
end
|
data/lib/xero_gateway/invoice.rb
CHANGED
@@ -4,8 +4,6 @@ module XeroGateway
|
|
4
4
|
include Money
|
5
5
|
include LineItemCalculations
|
6
6
|
|
7
|
-
class NoGatewayError < Error; end
|
8
|
-
|
9
7
|
INVOICE_TYPE = {
|
10
8
|
'ACCREC' => 'Accounts Receivable',
|
11
9
|
'ACCPAY' => 'Accounts Payable'
|
@@ -32,13 +30,16 @@ module XeroGateway
|
|
32
30
|
attr_accessor :gateway
|
33
31
|
|
34
32
|
# Any errors that occurred when the #valid? method called.
|
35
|
-
|
33
|
+
# Or errors that were within the XML payload from Xero
|
34
|
+
attr_accessor :errors
|
36
35
|
|
37
36
|
# Represents whether the line_items have been downloaded when getting from GET /API.XRO/2.0/INVOICES
|
38
37
|
attr_accessor :line_items_downloaded
|
39
38
|
|
40
39
|
# All accessible fields
|
41
|
-
attr_accessor :invoice_id, :invoice_number, :invoice_type, :invoice_status, :date, :due_date, :reference, :line_amount_types, :currency_code, :line_items, :contact, :payments, :fully_paid_on, :amount_due, :amount_paid, :amount_credited, :sent_to_contact, :url, :
|
40
|
+
attr_accessor :invoice_id, :invoice_number, :invoice_type, :invoice_status, :date, :due_date, :reference, :branding_theme_id, :line_amount_types, :currency_code, :line_items, :contact, :payments, :fully_paid_on, :amount_due, :amount_paid, :amount_credited, :sent_to_contact, :url, :updated_date_utc
|
41
|
+
# Are these buggy?
|
42
|
+
#attr_accessor :sub_total, :total_tax, :total
|
42
43
|
|
43
44
|
|
44
45
|
def initialize(params = {})
|
@@ -68,6 +69,10 @@ module XeroGateway
|
|
68
69
|
def valid?
|
69
70
|
@errors = []
|
70
71
|
|
72
|
+
if !INVOICE_TYPE[invoice_type]
|
73
|
+
@errors << ['invoice_type', "must be one of #{INVOICE_TYPE.keys.join('/')}"]
|
74
|
+
end
|
75
|
+
|
71
76
|
if !invoice_id.nil? && invoice_id !~ GUID_REGEX
|
72
77
|
@errors << ['invoice_id', 'must be blank or a valid Xero GUID']
|
73
78
|
end
|
@@ -165,14 +170,14 @@ module XeroGateway
|
|
165
170
|
end
|
166
171
|
|
167
172
|
# Creates this invoice record (using gateway.create_invoice) with the associated gateway.
|
168
|
-
# If no gateway set, raise a
|
173
|
+
# If no gateway set, raise a NoGatewayError exception.
|
169
174
|
def create
|
170
175
|
raise NoGatewayError unless gateway
|
171
176
|
gateway.create_invoice(self)
|
172
177
|
end
|
173
178
|
|
174
179
|
# Updates this invoice record (using gateway.update_invoice) with the associated gateway.
|
175
|
-
# If no gateway set, raise a
|
180
|
+
# If no gateway set, raise a NoGatewayError exception.
|
176
181
|
def update
|
177
182
|
raise NoGatewayError unless gateway
|
178
183
|
gateway.update_invoice(self)
|
@@ -189,6 +194,7 @@ module XeroGateway
|
|
189
194
|
b.DueDate Invoice.format_date(self.due_date) if self.due_date
|
190
195
|
b.Status self.invoice_status if self.invoice_status
|
191
196
|
b.Reference self.reference if self.reference
|
197
|
+
b.BrandingThemeID self.branding_theme_id if self.branding_theme_id
|
192
198
|
b.LineAmountTypes self.line_amount_types
|
193
199
|
b.LineItems {
|
194
200
|
self.line_items.each do |line_item|
|
@@ -214,13 +220,14 @@ module XeroGateway
|
|
214
220
|
when "FullyPaidOnDate" then invoice.fully_paid_on = parse_date(element.text)
|
215
221
|
when "Status" then invoice.invoice_status = element.text
|
216
222
|
when "Reference" then invoice.reference = element.text
|
223
|
+
when "BrandingThemeID" then invoice.branding_theme_id = element.text
|
217
224
|
when "LineAmountTypes" then invoice.line_amount_types = element.text
|
218
225
|
when "LineItems" then element.children.each {|line_item| invoice.line_items_downloaded = true; invoice.line_items << LineItem.from_xml(line_item) }
|
219
226
|
when "SubTotal" then invoice.sub_total = BigDecimal.new(element.text)
|
220
227
|
when "TotalTax" then invoice.total_tax = BigDecimal.new(element.text)
|
221
228
|
when "Total" then invoice.total = BigDecimal.new(element.text)
|
222
229
|
when "InvoiceID" then invoice.invoice_id = element.text
|
223
|
-
when "InvoiceNumber" then invoice.invoice_number = element.text
|
230
|
+
when "InvoiceNumber" then invoice.invoice_number = element.text
|
224
231
|
when "Payments" then element.children.each { | payment | invoice.payments << Payment.from_xml(payment) }
|
225
232
|
when "AmountDue" then invoice.amount_due = BigDecimal.new(element.text)
|
226
233
|
when "AmountPaid" then invoice.amount_paid = BigDecimal.new(element.text)
|
@@ -228,9 +235,10 @@ module XeroGateway
|
|
228
235
|
when "SentToContact" then invoice.sent_to_contact = (element.text.strip.downcase == "true")
|
229
236
|
when "Url" then invoice.url = element.text
|
230
237
|
when "UpdatedDateUTC" then invoice.updated_date_utc = parse_utc_date_time(element.text)
|
238
|
+
when "ValidationErrors" then invoice.errors = element.children.map { |error| Error.parse(error) }
|
231
239
|
end
|
232
|
-
end
|
240
|
+
end
|
233
241
|
invoice
|
234
|
-
end
|
242
|
+
end
|
235
243
|
end
|
236
244
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'account')
|
2
|
+
|
3
|
+
module XeroGateway
|
4
|
+
class JournalLine
|
5
|
+
include Money
|
6
|
+
|
7
|
+
TAX_TYPE = Account::TAX_TYPE unless defined?(TAX_TYPE)
|
8
|
+
|
9
|
+
# Any errors that occurred when the #valid? method called.
|
10
|
+
attr_reader :errors
|
11
|
+
|
12
|
+
# All accessible fields
|
13
|
+
attr_accessor :journal_line_id, :line_amount, :account_code, :description, :tax_type, :tracking
|
14
|
+
|
15
|
+
def initialize(params = {})
|
16
|
+
@errors ||= []
|
17
|
+
@tracking ||= []
|
18
|
+
|
19
|
+
params.each do |k,v|
|
20
|
+
self.send("#{k}=", v)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Validate the JournalLineItem record according to what will be valid by the gateway.
|
25
|
+
#
|
26
|
+
# Usage:
|
27
|
+
# journal_line_item.valid? # Returns true/false
|
28
|
+
#
|
29
|
+
# Additionally sets journal_line_item.errors array to an array of field/error.
|
30
|
+
def valid?
|
31
|
+
@errors = []
|
32
|
+
|
33
|
+
if !journal_line_id.nil? && journal_line_id !~ GUID_REGEX
|
34
|
+
@errors << ['journal_line_id', 'must be blank or a valid Xero GUID']
|
35
|
+
end
|
36
|
+
|
37
|
+
unless line_amount
|
38
|
+
@errors << ['line_amount', "can't be blank"]
|
39
|
+
end
|
40
|
+
|
41
|
+
unless account_code
|
42
|
+
@errors << ['account_code', "can't be blank"]
|
43
|
+
end
|
44
|
+
|
45
|
+
@errors.size == 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def has_tracking?
|
49
|
+
return false if tracking.nil?
|
50
|
+
|
51
|
+
if tracking.is_a?(Array)
|
52
|
+
return tracking.any?
|
53
|
+
else
|
54
|
+
return tracking.is_a?(TrackingCategory)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_xml(b = Builder::XmlMarkup.new)
|
59
|
+
b.JournalLine {
|
60
|
+
b.LineAmount line_amount # mandatory
|
61
|
+
b.AccountCode account_code # mandatory
|
62
|
+
b.Description description if description # optional
|
63
|
+
b.TaxType tax_type if tax_type # optional
|
64
|
+
if has_tracking?
|
65
|
+
b.Tracking { # optional
|
66
|
+
# Due to strange retardness in the Xero API, the XML structure for a tracking category within
|
67
|
+
# an invoice is different to a standalone tracking category.
|
68
|
+
# This means rather than going category.to_xml we need to call the special category.to_xml_for_invoice_messages
|
69
|
+
(tracking.is_a?(TrackingCategory) ? [tracking] : tracking).each do |category|
|
70
|
+
category.to_xml_for_invoice_messages(b)
|
71
|
+
end
|
72
|
+
}
|
73
|
+
end
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.from_xml(journal_line_element)
|
78
|
+
journal_line = JournalLine.new
|
79
|
+
journal_line_element.children.each do |element|
|
80
|
+
case(element.name)
|
81
|
+
when "LineAmount" then journal_line.line_amount = BigDecimal.new(element.text)
|
82
|
+
when "AccountCode" then journal_line.account_code = element.text
|
83
|
+
when "JournalLineID" then journal_line.journal_line_id = element.text
|
84
|
+
when "Description" then journal_line.description = element.text
|
85
|
+
when "TaxType" then journal_line.tax_type = element.text
|
86
|
+
when "Tracking" then
|
87
|
+
element.children.each do | tracking_element |
|
88
|
+
journal_line.tracking << TrackingCategory.from_xml(tracking_element)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
journal_line
|
93
|
+
end
|
94
|
+
|
95
|
+
def ==(other)
|
96
|
+
[:description, :line_amount, :account_code, :tax_type].each do |field|
|
97
|
+
return false if send(field) != other.send(field)
|
98
|
+
end
|
99
|
+
return true
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module XeroGateway
|
2
|
+
class ManualJournal
|
3
|
+
include Dates
|
4
|
+
|
5
|
+
GUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ unless defined?(GUID_REGEX)
|
6
|
+
|
7
|
+
STATUSES = {
|
8
|
+
'DRAFT' => 'Draft Manual Journal',
|
9
|
+
'POSTED' => 'Posted Manual Journal',
|
10
|
+
'DELETED' => 'Deleted Draft Manual Journal',
|
11
|
+
'VOIDED' => 'Voided Posted Manual Journal'
|
12
|
+
} unless defined?(STATUSES)
|
13
|
+
|
14
|
+
# Xero::Gateway associated with this invoice.
|
15
|
+
attr_accessor :gateway
|
16
|
+
|
17
|
+
# Any errors that occurred when the #valid? method called.
|
18
|
+
attr_reader :errors
|
19
|
+
|
20
|
+
# Represents whether the journal lines have been downloaded when getting from GET /API.XRO/2.0/ManualJournals
|
21
|
+
attr_accessor :journal_lines_downloaded
|
22
|
+
|
23
|
+
# accessible fields
|
24
|
+
attr_accessor :manual_journal_id, :narration, :date, :status, :journal_lines, :url, :show_on_cash_basis_reports
|
25
|
+
|
26
|
+
def initialize(params = {})
|
27
|
+
@errors ||= []
|
28
|
+
@payments ||= []
|
29
|
+
|
30
|
+
# Check if the line items have been downloaded.
|
31
|
+
@journal_lines_downloaded = (params.delete(:journal_lines_downloaded) == true)
|
32
|
+
|
33
|
+
params.each do |k,v|
|
34
|
+
self.send("#{k}=", v)
|
35
|
+
end
|
36
|
+
|
37
|
+
@journal_lines ||= []
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(other)
|
41
|
+
['narration', 'status', 'journal_lines', 'show_on_cash_basis_reports'].each do |field|
|
42
|
+
return false if send(field) != other.send(field)
|
43
|
+
end
|
44
|
+
|
45
|
+
["date"].each do |field|
|
46
|
+
return false if send(field).to_s != other.send(field).to_s
|
47
|
+
end
|
48
|
+
return true
|
49
|
+
end
|
50
|
+
|
51
|
+
# Validate the ManualJournal record according to what will be valid by the gateway.
|
52
|
+
#
|
53
|
+
# Usage:
|
54
|
+
# manual_journal.valid? # Returns true/false
|
55
|
+
#
|
56
|
+
# Additionally sets manual_journal.errors array to an array of field/error.
|
57
|
+
def valid?
|
58
|
+
@errors = []
|
59
|
+
|
60
|
+
if !manual_journal_id.nil? && manual_journal_id !~ GUID_REGEX
|
61
|
+
@errors << ['manual_journal_id', 'must be blank or a valid Xero GUID']
|
62
|
+
end
|
63
|
+
|
64
|
+
if narration.blank?
|
65
|
+
@errors << ['narration', "can't be blank"]
|
66
|
+
end
|
67
|
+
|
68
|
+
unless date
|
69
|
+
@errors << ['date', "can't be blank"]
|
70
|
+
end
|
71
|
+
|
72
|
+
# Make sure all journal_items are valid.
|
73
|
+
unless journal_lines.all? { | journal_line | journal_line.valid? }
|
74
|
+
@errors << ['journal_lines', "at least one journal line invalid"]
|
75
|
+
end
|
76
|
+
|
77
|
+
# make sure there are at least 2 journal lines
|
78
|
+
unless journal_lines.length > 1
|
79
|
+
@errors << ['journal_lines', "journal must contain at least two individual journal lines"]
|
80
|
+
end
|
81
|
+
|
82
|
+
if journal_lines.length > 100
|
83
|
+
@errors << ['journal_lines', "journal must contain less than one hundred journal lines"]
|
84
|
+
end
|
85
|
+
|
86
|
+
unless journal_lines.sum(&:line_amount).to_f == 0.0
|
87
|
+
@errors << ['journal_lines', "the total debits must be equal to total credits"]
|
88
|
+
end
|
89
|
+
|
90
|
+
@errors.size == 0
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
def journal_lines_downloaded?
|
95
|
+
@journal_lines_downloaded
|
96
|
+
end
|
97
|
+
|
98
|
+
# If line items are not downloaded, then attempt a download now (if this record was found to begin with).
|
99
|
+
def journal_lines
|
100
|
+
if journal_lines_downloaded?
|
101
|
+
@journal_lines
|
102
|
+
|
103
|
+
elsif manual_journal_id =~ GUID_REGEX && @gateway
|
104
|
+
# There is a manual_journal_id so we can assume this record was loaded from Xero.
|
105
|
+
# Let's attempt to download the journal_line records (if there is a gateway)
|
106
|
+
|
107
|
+
response = @gateway.get_manual_journal(manual_journal_id)
|
108
|
+
raise ManualJournalNotFoundError, "Manual Journal with ID #{manual_journal_id} not found in Xero." unless response.success? && response.manual_journal.is_a?(XeroGateway::ManualJournal)
|
109
|
+
|
110
|
+
@journal_lines = response.manual_journal.journal_lines
|
111
|
+
@journal_lines_downloaded = true
|
112
|
+
|
113
|
+
@journal_lines
|
114
|
+
|
115
|
+
# Otherwise, this is a new manual journal, so return the journal_lines reference.
|
116
|
+
else
|
117
|
+
@journal_lines
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_xml(b = Builder::XmlMarkup.new)
|
122
|
+
b.ManualJournal {
|
123
|
+
b.ManualJournalID manual_journal_id if manual_journal_id
|
124
|
+
b.Narration narration
|
125
|
+
b.JournalLines {
|
126
|
+
self.journal_lines.each do |journal_line|
|
127
|
+
journal_line.to_xml(b)
|
128
|
+
end
|
129
|
+
}
|
130
|
+
b.Date ManualJournal.format_date(date || Date.today)
|
131
|
+
b.Status status if status
|
132
|
+
b.Url url if url
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.from_xml(manual_journal_element, gateway = nil, options = {})
|
137
|
+
manual_journal = ManualJournal.new(options.merge({:gateway => gateway}))
|
138
|
+
manual_journal_element.children.each do |element|
|
139
|
+
case(element.name)
|
140
|
+
when "ManualJournalID" then manual_journal.manual_journal_id = element.text
|
141
|
+
when "Date" then manual_journal.date = parse_date(element.text)
|
142
|
+
when "Status" then manual_journal.status = element.text
|
143
|
+
when "Narration" then manual_journal.narration = element.text
|
144
|
+
when "JournalLines" then element.children.each {|journal_line| manual_journal.journal_lines_downloaded = true; manual_journal.journal_lines << JournalLine.from_xml(journal_line) }
|
145
|
+
when "Url" then manual_journal.url = element.text
|
146
|
+
end
|
147
|
+
end
|
148
|
+
manual_journal
|
149
|
+
end # from_xml
|
150
|
+
|
151
|
+
def add_journal_line(params = {})
|
152
|
+
journal_line = nil
|
153
|
+
case params
|
154
|
+
when Hash then journal_line = JournalLine.new(params)
|
155
|
+
when JournalLine then journal_line = params
|
156
|
+
else raise InvalidLineItemError
|
157
|
+
end
|
158
|
+
@journal_lines << journal_line
|
159
|
+
journal_line
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
end
|
data/lib/xero_gateway/oauth.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
module XeroGateway
|
2
|
-
|
2
|
+
|
3
3
|
# Shamelessly based on the Twitter Gem's OAuth implementation by John Nunemaker
|
4
4
|
# Thanks!
|
5
|
-
#
|
5
|
+
#
|
6
6
|
# http://twitter.rubyforge.org/
|
7
7
|
# http://github.com/jnunemaker/twitter/
|
8
|
-
|
8
|
+
|
9
9
|
class OAuth
|
10
|
-
|
10
|
+
|
11
11
|
class TokenExpired < StandardError; end
|
12
12
|
class TokenInvalid < StandardError; end
|
13
13
|
class RateLimitExceeded < StandardError; end
|
14
14
|
class UnknownError < StandardError; end
|
15
|
-
|
15
|
+
|
16
16
|
unless defined? XERO_CONSUMER_OPTIONS
|
17
17
|
XERO_CONSUMER_OPTIONS = {
|
18
18
|
:site => "https://api.xero.com",
|
@@ -21,59 +21,65 @@ module XeroGateway
|
|
21
21
|
:authorize_path => "/oauth/Authorize"
|
22
22
|
}.freeze
|
23
23
|
end
|
24
|
-
|
24
|
+
|
25
25
|
extend Forwardable
|
26
26
|
def_delegators :access_token, :get, :post, :put, :delete
|
27
|
-
|
28
|
-
attr_reader
|
29
|
-
|
27
|
+
|
28
|
+
attr_reader :ctoken, :csecret, :consumer_options, :authorization_expires_at
|
29
|
+
attr_accessor :session_handle
|
30
|
+
|
30
31
|
def initialize(ctoken, csecret, options = {})
|
31
32
|
@ctoken, @csecret = ctoken, csecret
|
32
33
|
@consumer_options = XERO_CONSUMER_OPTIONS.merge(options)
|
33
34
|
end
|
34
|
-
|
35
|
+
|
35
36
|
def consumer
|
36
37
|
@consumer ||= ::OAuth::Consumer.new(@ctoken, @csecret, consumer_options)
|
37
38
|
end
|
38
|
-
|
39
|
+
|
39
40
|
def request_token(params = {})
|
40
41
|
@request_token ||= consumer.get_request_token(params)
|
41
42
|
end
|
42
|
-
|
43
|
+
|
43
44
|
def authorize_from_request(rtoken, rsecret, params = {})
|
44
45
|
request_token = ::OAuth::RequestToken.new(consumer, rtoken, rsecret)
|
45
46
|
access_token = request_token.get_access_token(params)
|
46
47
|
@atoken, @asecret = access_token.token, access_token.secret
|
47
|
-
|
48
|
+
|
48
49
|
update_attributes_from_token(access_token)
|
49
50
|
end
|
50
|
-
|
51
|
+
|
51
52
|
def access_token
|
52
53
|
@access_token ||= ::OAuth::AccessToken.new(consumer, @atoken, @asecret)
|
53
54
|
end
|
54
|
-
|
55
|
+
|
55
56
|
def authorize_from_access(atoken, asecret)
|
56
57
|
@atoken, @asecret = atoken, asecret
|
57
58
|
end
|
58
|
-
|
59
|
+
|
59
60
|
# Renewing access tokens only works for Partner applications
|
60
61
|
def renew_access_token(access_token = nil, access_secret = nil, session_handle = nil)
|
61
62
|
access_token ||= @atoken
|
62
|
-
access_secret ||= @asecret
|
63
|
+
access_secret ||= @asecret
|
63
64
|
session_handle ||= @session_handle
|
64
|
-
|
65
|
+
|
65
66
|
old_token = ::OAuth::RequestToken.new(consumer, access_token, access_secret)
|
66
|
-
|
67
|
+
|
67
68
|
access_token = old_token.get_access_token({
|
68
69
|
:oauth_session_handle => session_handle,
|
69
70
|
:token => old_token
|
70
71
|
})
|
71
|
-
|
72
|
+
|
72
73
|
update_attributes_from_token(access_token)
|
74
|
+
rescue ::OAuth::Unauthorized => e
|
75
|
+
# If the original access token is for some reason invalid an OAuth::Unauthorized could be raised.
|
76
|
+
# In this case raise a XeroGateway::OAuth::TokenInvalid which can be captured by the caller. In this
|
77
|
+
# situation the end user will need to re-authorize the application via the request token authorization URL
|
78
|
+
raise XeroGateway::OAuth::TokenInvalid.new(e.message)
|
73
79
|
end
|
74
|
-
|
80
|
+
|
75
81
|
private
|
76
|
-
|
82
|
+
|
77
83
|
# Update instance variables with those from the AccessToken.
|
78
84
|
def update_attributes_from_token(access_token)
|
79
85
|
@expires_at = Time.now + access_token.params[:oauth_expires_in].to_i
|
@@ -82,6 +88,6 @@ module XeroGateway
|
|
82
88
|
@atoken, @asecret = access_token.token, access_token.secret
|
83
89
|
@access_token = nil
|
84
90
|
end
|
85
|
-
|
91
|
+
|
86
92
|
end
|
87
93
|
end
|
@@ -6,7 +6,7 @@ module XeroGateway
|
|
6
6
|
NO_SSL_CLIENT_CERT_MESSAGE = "You need to provide a client ssl certificate and key pair (these are the ones you got from Entrust and should not be password protected) as :ssl_client_cert and :ssl_client_key (should be .crt or .pem files)"
|
7
7
|
NO_PRIVATE_KEY_ERROR_MESSAGE = "You need to provide your private key (corresponds to the public key you uploaded at api.xero.com) as :private_key_file (should be .crt or .pem files)"
|
8
8
|
|
9
|
-
def_delegators :client, :session_handle, :renew_access_token
|
9
|
+
def_delegators :client, :session_handle, :renew_access_token, :authorization_expires_at
|
10
10
|
|
11
11
|
def initialize(consumer_key, consumer_secret, options = {})
|
12
12
|
|
@@ -26,5 +26,10 @@ module XeroGateway
|
|
26
26
|
@xero_url = options[:xero_url] || "https://api-partner.xero.com/api.xro/2.0"
|
27
27
|
@client = OAuth.new(consumer_key, consumer_secret, options)
|
28
28
|
end
|
29
|
+
|
30
|
+
def set_session_handle(handle)
|
31
|
+
client.session_handle = handle
|
32
|
+
end
|
33
|
+
|
29
34
|
end
|
30
35
|
end
|
@@ -9,11 +9,13 @@ module XeroGateway
|
|
9
9
|
alias_method :invoice, :response_item
|
10
10
|
alias_method :credit_note, :response_item
|
11
11
|
alias_method :bank_transaction, :response_item
|
12
|
+
alias_method :manual_journal, :response_item
|
12
13
|
alias_method :contact, :response_item
|
13
14
|
alias_method :organisation, :response_item
|
14
15
|
alias_method :invoices, :array_wrapped_response_item
|
15
16
|
alias_method :credit_notes, :array_wrapped_response_item
|
16
17
|
alias_method :bank_transactions, :array_wrapped_response_item
|
18
|
+
alias_method :manual_journals, :array_wrapped_response_item
|
17
19
|
alias_method :contacts, :array_wrapped_response_item
|
18
20
|
alias_method :accounts, :array_wrapped_response_item
|
19
21
|
alias_method :tracking_categories, :array_wrapped_response_item
|
@@ -47,7 +47,7 @@ module XeroGateway
|
|
47
47
|
attribute = element.name
|
48
48
|
underscored_attribute = element.name.underscore
|
49
49
|
|
50
|
-
|
50
|
+
next if !ATTRS.keys.include?(attribute)
|
51
51
|
|
52
52
|
case (ATTRS[attribute])
|
53
53
|
when :boolean then tax_rate.send("#{underscored_attribute}=", (element.text == "true"))
|