xero_gateway-float 2.0.18 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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"))
|