xero_gateway-n8vision 2.0.20

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/Gemfile +12 -0
  2. data/LICENSE +14 -0
  3. data/README.textile +357 -0
  4. data/Rakefile +14 -0
  5. data/examples/oauth.rb +25 -0
  6. data/examples/partner_app.rb +36 -0
  7. data/init.rb +1 -0
  8. data/lib/oauth/oauth_consumer.rb +14 -0
  9. data/lib/xero_gateway.rb +41 -0
  10. data/lib/xero_gateway/account.rb +86 -0
  11. data/lib/xero_gateway/accounts_list.rb +73 -0
  12. data/lib/xero_gateway/address.rb +96 -0
  13. data/lib/xero_gateway/bank_transaction.rb +175 -0
  14. data/lib/xero_gateway/ca-certificates.crt +2560 -0
  15. data/lib/xero_gateway/contact.rb +203 -0
  16. data/lib/xero_gateway/credit_note.rb +220 -0
  17. data/lib/xero_gateway/currency.rb +56 -0
  18. data/lib/xero_gateway/dates.rb +25 -0
  19. data/lib/xero_gateway/error.rb +18 -0
  20. data/lib/xero_gateway/exceptions.rb +51 -0
  21. data/lib/xero_gateway/gateway.rb +698 -0
  22. data/lib/xero_gateway/http.rb +135 -0
  23. data/lib/xero_gateway/http_encoding_helper.rb +49 -0
  24. data/lib/xero_gateway/invoice.rb +238 -0
  25. data/lib/xero_gateway/journal_line.rb +102 -0
  26. data/lib/xero_gateway/line_item.rb +125 -0
  27. data/lib/xero_gateway/line_item_calculations.rb +51 -0
  28. data/lib/xero_gateway/manual_journal.rb +163 -0
  29. data/lib/xero_gateway/money.rb +16 -0
  30. data/lib/xero_gateway/oauth.rb +92 -0
  31. data/lib/xero_gateway/organisation.rb +75 -0
  32. data/lib/xero_gateway/partner_app.rb +30 -0
  33. data/lib/xero_gateway/payment.rb +43 -0
  34. data/lib/xero_gateway/phone.rb +77 -0
  35. data/lib/xero_gateway/private_app.rb +17 -0
  36. data/lib/xero_gateway/response.rb +43 -0
  37. data/lib/xero_gateway/tax_rate.rb +63 -0
  38. data/lib/xero_gateway/tracking_category.rb +87 -0
  39. data/test/integration/accounts_list_test.rb +109 -0
  40. data/test/integration/create_bank_transaction_test.rb +38 -0
  41. data/test/integration/create_contact_test.rb +66 -0
  42. data/test/integration/create_credit_note_test.rb +49 -0
  43. data/test/integration/create_invoice_test.rb +49 -0
  44. data/test/integration/create_manual_journal_test.rb +35 -0
  45. data/test/integration/get_accounts_test.rb +23 -0
  46. data/test/integration/get_bank_transaction_test.rb +51 -0
  47. data/test/integration/get_bank_transactions_test.rb +88 -0
  48. data/test/integration/get_contact_test.rb +28 -0
  49. data/test/integration/get_contacts_test.rb +40 -0
  50. data/test/integration/get_credit_note_test.rb +48 -0
  51. data/test/integration/get_credit_notes_test.rb +90 -0
  52. data/test/integration/get_currencies_test.rb +25 -0
  53. data/test/integration/get_invoice_test.rb +48 -0
  54. data/test/integration/get_invoices_test.rb +92 -0
  55. data/test/integration/get_manual_journal_test.rb +50 -0
  56. data/test/integration/get_manual_journals_test.rb +88 -0
  57. data/test/integration/get_organisation_test.rb +24 -0
  58. data/test/integration/get_tax_rates_test.rb +25 -0
  59. data/test/integration/get_tracking_categories_test.rb +27 -0
  60. data/test/integration/update_bank_transaction_test.rb +31 -0
  61. data/test/integration/update_contact_test.rb +31 -0
  62. data/test/integration/update_invoice_test.rb +31 -0
  63. data/test/integration/update_manual_journal_test.rb +31 -0
  64. data/test/test_helper.rb +217 -0
  65. data/test/unit/account_test.rb +47 -0
  66. data/test/unit/bank_transaction_test.rb +126 -0
  67. data/test/unit/contact_test.rb +97 -0
  68. data/test/unit/credit_note_test.rb +284 -0
  69. data/test/unit/currency_test.rb +31 -0
  70. data/test/unit/gateway_test.rb +119 -0
  71. data/test/unit/invoice_test.rb +326 -0
  72. data/test/unit/manual_journal_test.rb +93 -0
  73. data/test/unit/oauth_test.rb +116 -0
  74. data/test/unit/organisation_test.rb +38 -0
  75. data/test/unit/tax_rate_test.rb +38 -0
  76. data/test/unit/tracking_category_test.rb +52 -0
  77. data/xero_gateway-n8vision.gemspec +15 -0
  78. metadata +178 -0
@@ -0,0 +1,25 @@
1
+ module XeroGateway
2
+ module Dates
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def format_date(time)
9
+ return time.strftime("%Y-%m-%d")
10
+ end
11
+
12
+ def format_date_time(time)
13
+ return time.strftime("%Y%m%d%H%M%S")
14
+ end
15
+
16
+ def parse_date(time)
17
+ Date.civil(time[0..3].to_i, time[5..6].to_i, time[8..9].to_i)
18
+ end
19
+
20
+ def parse_date_time(time)
21
+ Time.local(time[0..3].to_i, time[5..6].to_i, time[8..9].to_i, time[11..12].to_i, time[14..15].to_i, time[17..18].to_i)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ module XeroGateway
2
+ class Error
3
+ attr_accessor :description, :date_time, :type, :message
4
+
5
+ def initialize(params = {})
6
+ params.each do |k,v|
7
+ self.send("#{k}=", v)
8
+ end
9
+ end
10
+
11
+ def ==(other)
12
+ [:description, :date_time, :type, :message].each do |field|
13
+ return false if send(field) != other.send(field)
14
+ end
15
+ return true
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ module XeroGateway
2
+ class NoGatewayError < StandardError; end
3
+ class AccountsListNotLoadedError < StandardError; end
4
+ class InvalidLineItemError < StandardError; end
5
+
6
+ class ApiException < StandardError
7
+
8
+ def initialize(type, message, request_xml, response_xml)
9
+ @type = type
10
+ @message = message
11
+ @request_xml = request_xml
12
+ @response_xml = response_xml
13
+ end
14
+
15
+ attr_reader :request_xml, :response_xml
16
+
17
+ def message
18
+ "#{@type}: #{@message} \n Generated by the following XML: \n #{@response_xml}"
19
+ end
20
+
21
+ end
22
+
23
+ class UnparseableResponse < StandardError
24
+
25
+ def initialize(root_element_name)
26
+ @root_element_name = root_element_name
27
+ end
28
+
29
+ def message
30
+ "A root element of #{@root_element_name} was returned, and we don't understand that!"
31
+ end
32
+
33
+ end
34
+
35
+ class ObjectNotFound < StandardError
36
+
37
+ def initialize(api_endpoint)
38
+ @api_endpoint = api_endpoint
39
+ end
40
+
41
+ def message
42
+ "Couldn't find object for API Endpoint #{@api_endpoint}"
43
+ end
44
+
45
+ end
46
+
47
+ class InvoiceNotFoundError < StandardError; end
48
+ class BankTransactionNotFoundError < StandardError; end
49
+ class CreditNoteNotFoundError < StandardError; end
50
+ class ManualJournalNotFoundError < StandardError; end
51
+ end
@@ -0,0 +1,698 @@
1
+ module XeroGateway
2
+
3
+ class Gateway
4
+ include Http
5
+ include Dates
6
+
7
+ attr_accessor :client, :xero_url, :logger
8
+
9
+ extend Forwardable
10
+ def_delegators :client, :request_token, :access_token, :authorize_from_request, :authorize_from_access, :expires_at, :authorization_expires_at
11
+
12
+ #
13
+ # The consumer key and secret here correspond to those provided
14
+ # to you by Xero inside the API Previewer.
15
+ def initialize(consumer_key, consumer_secret, options = {})
16
+ @xero_url = options[:xero_url] || "https://api.xero.com/api.xro/2.0"
17
+ @client = OAuth.new(consumer_key, consumer_secret, options)
18
+ end
19
+
20
+ #
21
+ # Retrieve all contacts from Xero
22
+ #
23
+ # Usage : get_contacts(:order => :name)
24
+ # get_contacts(:modified_since => Time)
25
+ #
26
+ # Note : modified_since is in UTC format (i.e. Brisbane is UTC+10)
27
+ def get_contacts(options = {})
28
+ request_params = {}
29
+
30
+ if !options[:updated_after].nil?
31
+ warn '[warning] :updated_after is depracated in XeroGateway#get_contacts. Use :modified_since'
32
+ options[:modified_since] = options.delete(:updated_after)
33
+ end
34
+
35
+ request_params[:ContactID] = options[:contact_id] if options[:contact_id]
36
+ request_params[:ContactNumber] = options[:contact_number] if options[:contact_number]
37
+ request_params[:OrderBy] = options[:order] if options[:order]
38
+ request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
39
+ request_params[:where] = options[:where] if options[:where]
40
+
41
+ response_xml = http_get(@client, "#{@xero_url}/Contacts", request_params)
42
+
43
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/contacts'})
44
+ end
45
+
46
+ # Retrieve a contact from Xero
47
+ # Usage get_contact_by_id(contact_id)
48
+ def get_contact_by_id(contact_id)
49
+ get_contact(contact_id)
50
+ end
51
+
52
+ # Retrieve a contact from Xero
53
+ # Usage get_contact_by_id(contact_id)
54
+ def get_contact_by_number(contact_number)
55
+ get_contact(nil, contact_number)
56
+ end
57
+
58
+ # Factory method for building new Contact objects associated with this gateway.
59
+ def build_contact(contact = {})
60
+ case contact
61
+ when Contact then contact.gateway = self
62
+ when Hash then contact = Contact.new(contact.merge({:gateway => self}))
63
+ end
64
+ contact
65
+ end
66
+
67
+ #
68
+ # Creates a contact in Xero
69
+ #
70
+ # Usage :
71
+ #
72
+ # contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT #{Time.now.to_i}")
73
+ # contact.email = "whoever@something.com"
74
+ # contact.phone.number = "12345"
75
+ # contact.address.line_1 = "LINE 1 OF THE ADDRESS"
76
+ # contact.address.line_2 = "LINE 2 OF THE ADDRESS"
77
+ # contact.address.line_3 = "LINE 3 OF THE ADDRESS"
78
+ # contact.address.line_4 = "LINE 4 OF THE ADDRESS"
79
+ # contact.address.city = "WELLINGTON"
80
+ # contact.address.region = "WELLINGTON"
81
+ # contact.address.country = "NEW ZEALAND"
82
+ # contact.address.post_code = "6021"
83
+ #
84
+ # create_contact(contact)
85
+ def create_contact(contact)
86
+ save_contact(contact)
87
+ end
88
+
89
+ #
90
+ # Updates an existing Xero contact
91
+ #
92
+ # Usage :
93
+ #
94
+ # contact = xero_gateway.get_contact(some_contact_id)
95
+ # contact.email = "a_new_email_ddress"
96
+ #
97
+ # xero_gateway.update_contact(contact)
98
+ def update_contact(contact)
99
+ raise "contact_id or contact_number is required for updating contacts" if contact.contact_id.nil? and contact.contact_number.nil?
100
+ save_contact(contact)
101
+ end
102
+
103
+ #
104
+ # Updates an array of contacts in a single API operation.
105
+ #
106
+ # Usage :
107
+ # contacts = [XeroGateway::Contact.new(:name => 'Joe Bloggs'), XeroGateway::Contact.new(:name => 'Jane Doe')]
108
+ # result = gateway.update_contacts(contacts)
109
+ #
110
+ # Will update contacts with matching contact_id, contact_number or name or create if they don't exist.
111
+ #
112
+ def update_contacts(contacts)
113
+ b = Builder::XmlMarkup.new
114
+ request_xml = b.Contacts {
115
+ contacts.each do | contact |
116
+ contact.to_xml(b)
117
+ end
118
+ }
119
+
120
+ response_xml = http_post(@client, "#{@xero_url}/Contacts", request_xml, {})
121
+
122
+ response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'POST/contacts'})
123
+ response.contacts.each_with_index do | response_contact, index |
124
+ contacts[index].contact_id = response_contact.contact_id if response_contact && response_contact.contact_id
125
+ end
126
+ response
127
+ end
128
+
129
+ # Retrieves all invoices from Xero
130
+ #
131
+ # Usage : get_invoices
132
+ # get_invoices(:invoice_id => " 297c2dc5-cc47-4afd-8ec8-74990b8761e9")
133
+ #
134
+ # Note : modified_since is in UTC format (i.e. Brisbane is UTC+10)
135
+ def get_invoices(options = {})
136
+
137
+ request_params = {}
138
+
139
+ request_params[:InvoiceID] = options[:invoice_id] if options[:invoice_id]
140
+ request_params[:InvoiceNumber] = options[:invoice_number] if options[:invoice_number]
141
+ request_params[:OrderBy] = options[:order] if options[:order]
142
+ request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
143
+
144
+ request_params[:where] = options[:where] if options[:where]
145
+
146
+ response_xml = http_get(@client, "#{@xero_url}/Invoices", request_params)
147
+
148
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/Invoices'})
149
+ end
150
+
151
+ # Retrieves a single invoice
152
+ #
153
+ # Usage : get_invoice("297c2dc5-cc47-4afd-8ec8-74990b8761e9") # By ID
154
+ # get_invoice("OIT-12345") # By number
155
+ def get_invoice(invoice_id_or_number)
156
+ request_params = {}
157
+
158
+ url = "#{@xero_url}/Invoices/#{URI.escape(invoice_id_or_number)}"
159
+
160
+ response_xml = http_get(@client, url, request_params)
161
+
162
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/Invoice'})
163
+ end
164
+
165
+ # Factory method for building new Invoice objects associated with this gateway.
166
+ def build_invoice(invoice = {})
167
+ case invoice
168
+ when Invoice then invoice.gateway = self
169
+ when Hash then invoice = Invoice.new(invoice.merge(:gateway => self))
170
+ end
171
+ invoice
172
+ end
173
+
174
+ # Creates an invoice in Xero based on an invoice object.
175
+ #
176
+ # Invoice and line item totals are calculated automatically.
177
+ #
178
+ # Usage :
179
+ #
180
+ # invoice = XeroGateway::Invoice.new({
181
+ # :invoice_type => "ACCREC",
182
+ # :due_date => 1.month.from_now,
183
+ # :invoice_number => "YOUR INVOICE NUMBER",
184
+ # :reference => "YOUR REFERENCE (NOT NECESSARILY UNIQUE!)",
185
+ # :line_amount_types => "Inclusive"
186
+ # })
187
+ # invoice.contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT")
188
+ # invoice.contact.phone.number = "12345"
189
+ # invoice.contact.address.line_1 = "LINE 1 OF THE ADDRESS"
190
+ # invoice.line_items << XeroGateway::LineItem.new(
191
+ # :description => "THE DESCRIPTION OF THE LINE ITEM",
192
+ # :unit_amount => 100,
193
+ # :tax_amount => 12.5,
194
+ # :tracking_category => "THE TRACKING CATEGORY FOR THE LINE ITEM",
195
+ # :tracking_option => "THE TRACKING OPTION FOR THE LINE ITEM"
196
+ # )
197
+ #
198
+ # create_invoice(invoice)
199
+ def create_invoice(invoice)
200
+ save_invoice(invoice)
201
+ end
202
+
203
+ #
204
+ # Updates an existing Xero invoice
205
+ #
206
+ # Usage :
207
+ #
208
+ # invoice = xero_gateway.get_invoice(some_invoice_id)
209
+ # invoice.due_date = Date.today
210
+ #
211
+ # xero_gateway.update_invoice(invoice)
212
+
213
+ def update_invoice(invoice)
214
+ raise "invoice_id is required for updating invoices" if invoice.invoice_id.nil?
215
+ save_invoice(invoice)
216
+ end
217
+
218
+ #
219
+ # Creates an array of invoices with a single API request.
220
+ #
221
+ # Usage :
222
+ # invoices = [XeroGateway::Invoice.new(...), XeroGateway::Invoice.new(...)]
223
+ # result = gateway.create_invoices(invoices)
224
+ #
225
+ def create_invoices(invoices)
226
+ b = Builder::XmlMarkup.new
227
+ request_xml = b.Invoices {
228
+ invoices.each do | invoice |
229
+ invoice.to_xml(b)
230
+ end
231
+ }
232
+
233
+ response_xml = http_put(@client, "#{@xero_url}/Invoices", request_xml, {})
234
+
235
+ response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/invoices'})
236
+ response.invoices.each_with_index do | response_invoice, index |
237
+ invoices[index].invoice_id = response_invoice.invoice_id if response_invoice && response_invoice.invoice_id
238
+ end
239
+ response
240
+ end
241
+
242
+ # Retrieves all credit_notes from Xero
243
+ #
244
+ # Usage : get_credit_notes
245
+ # get_credit_notes(:credit_note_id => " 297c2dc5-cc47-4afd-8ec8-74990b8761e9")
246
+ #
247
+ # Note : modified_since is in UTC format (i.e. Brisbane is UTC+10)
248
+ def get_credit_notes(options = {})
249
+
250
+ request_params = {}
251
+
252
+ request_params[:CreditNoteID] = options[:credit_note_id] if options[:credit_note_id]
253
+ request_params[:CreditNoteNumber] = options[:credit_note_number] if options[:credit_note_number]
254
+ request_params[:OrderBy] = options[:order] if options[:order]
255
+ request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
256
+
257
+ request_params[:where] = options[:where] if options[:where]
258
+
259
+ response_xml = http_get(@client, "#{@xero_url}/CreditNotes", request_params)
260
+
261
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/CreditNotes'})
262
+ end
263
+
264
+ # Retrieves a single credit_note
265
+ #
266
+ # Usage : get_credit_note("297c2dc5-cc47-4afd-8ec8-74990b8761e9") # By ID
267
+ # get_credit_note("OIT-12345") # By number
268
+ def get_credit_note(credit_note_id_or_number)
269
+ request_params = {}
270
+
271
+ url = "#{@xero_url}/CreditNotes/#{URI.escape(credit_note_id_or_number)}"
272
+
273
+ response_xml = http_get(@client, url, request_params)
274
+
275
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/CreditNote'})
276
+ end
277
+
278
+ # Factory method for building new CreditNote objects associated with this gateway.
279
+ def build_credit_note(credit_note = {})
280
+ case credit_note
281
+ when CreditNote then credit_note.gateway = self
282
+ when Hash then credit_note = CreditNote.new(credit_note.merge(:gateway => self))
283
+ end
284
+ credit_note
285
+ end
286
+
287
+ # Creates an credit_note in Xero based on an credit_note object.
288
+ #
289
+ # CreditNote and line item totals are calculated automatically.
290
+ #
291
+ # Usage :
292
+ #
293
+ # credit_note = XeroGateway::CreditNote.new({
294
+ # :credit_note_type => "ACCREC",
295
+ # :due_date => 1.month.from_now,
296
+ # :credit_note_number => "YOUR CREDIT_NOTE NUMBER",
297
+ # :reference => "YOUR REFERENCE (NOT NECESSARILY UNIQUE!)",
298
+ # :line_amount_types => "Inclusive"
299
+ # })
300
+ # credit_note.contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT")
301
+ # credit_note.contact.phone.number = "12345"
302
+ # credit_note.contact.address.line_1 = "LINE 1 OF THE ADDRESS"
303
+ # credit_note.line_items << XeroGateway::LineItem.new(
304
+ # :description => "THE DESCRIPTION OF THE LINE ITEM",
305
+ # :unit_amount => 100,
306
+ # :tax_amount => 12.5,
307
+ # :tracking_category => "THE TRACKING CATEGORY FOR THE LINE ITEM",
308
+ # :tracking_option => "THE TRACKING OPTION FOR THE LINE ITEM"
309
+ # )
310
+ #
311
+ # create_credit_note(credit_note)
312
+ def create_credit_note(credit_note)
313
+ request_xml = credit_note.to_xml
314
+ response_xml = http_put(@client, "#{@xero_url}/CreditNotes", request_xml)
315
+ response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/credit_note'})
316
+
317
+ # Xero returns credit_notes inside an <CreditNotes> tag, even though there's only ever
318
+ # one for this request
319
+ response.response_item = response.credit_notes.first
320
+
321
+ if response.success? && response.credit_note && response.credit_note.credit_note_id
322
+ credit_note.credit_note_id = response.credit_note.credit_note_id
323
+ end
324
+
325
+ response
326
+ end
327
+
328
+ #
329
+ # Creates an array of credit_notes with a single API request.
330
+ #
331
+ # Usage :
332
+ # credit_notes = [XeroGateway::CreditNote.new(...), XeroGateway::CreditNote.new(...)]
333
+ # result = gateway.create_credit_notes(credit_notes)
334
+ #
335
+ def create_credit_notes(credit_notes)
336
+ b = Builder::XmlMarkup.new
337
+ request_xml = b.CreditNotes {
338
+ credit_notes.each do | credit_note |
339
+ credit_note.to_xml(b)
340
+ end
341
+ }
342
+
343
+ response_xml = http_put(@client, "#{@xero_url}/CreditNotes", request_xml, {})
344
+
345
+ response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/credit_notes'})
346
+ response.credit_notes.each_with_index do | response_credit_note, index |
347
+ credit_notes[index].credit_note_id = response_credit_note.credit_note_id if response_credit_note && response_credit_note.credit_note_id
348
+ end
349
+ response
350
+ end
351
+
352
+ # Creates a bank transaction in Xero based on a bank transaction object.
353
+ #
354
+ # Bank transaction and line item totals are calculated automatically.
355
+ #
356
+ # Usage :
357
+ #
358
+ # bank_transaction = XeroGateway::BankTransaction.new({
359
+ # :type => "RECEIVE",
360
+ # :date => 1.month.from_now,
361
+ # :reference => "YOUR INVOICE NUMBER",
362
+ # })
363
+ # bank_transaction.contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT")
364
+ # bank_transaction.contact.phone.number = "12345"
365
+ # bank_transaction.contact.address.line_1 = "LINE 1 OF THE ADDRESS"
366
+ # bank_transaction.line_items << XeroGateway::LineItem.new(
367
+ # :description => "THE DESCRIPTION OF THE LINE ITEM",
368
+ # :unit_amount => 100,
369
+ # :tax_amount => 12.5,
370
+ # :tracking_category => "THE TRACKING CATEGORY FOR THE LINE ITEM",
371
+ # :tracking_option => "THE TRACKING OPTION FOR THE LINE ITEM"
372
+ # )
373
+ # bank_transaction.bank_account = XeroGateway::Account.new(:code => 'BANK-ABC)
374
+ #
375
+ # create_bank_transaction(bank_transaction)
376
+ def create_bank_transaction(bank_transaction)
377
+ save_bank_transaction(bank_transaction)
378
+ end
379
+
380
+ #
381
+ # Updates an existing Xero bank transaction
382
+ #
383
+ # Usage :
384
+ #
385
+ # bank_transaction = xero_gateway.get_bank_transaction(some_bank_transaction_id)
386
+ # bank_transaction.due_date = Date.today
387
+ #
388
+ # xero_gateway.update_bank_transaction(bank_transaction)
389
+ def update_bank_transaction(bank_transaction)
390
+ raise "bank_transaction_id is required for updating bank transactions" if bank_transaction.bank_transaction_id.nil?
391
+ save_bank_transaction(bank_transaction)
392
+ end
393
+
394
+ # Retrieves all bank transactions from Xero
395
+ #
396
+ # Usage : get_bank_transactions
397
+ # get_bank_transactions(:bank_transaction_id => " 297c2dc5-cc47-4afd-8ec8-74990b8761e9")
398
+ #
399
+ # Note : modified_since is in UTC format (i.e. Brisbane is UTC+10)
400
+ def get_bank_transactions(options = {})
401
+ request_params = {}
402
+ request_params[:BankTransactionID] = options[:bank_transaction_id] if options[:bank_transaction_id]
403
+ request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
404
+
405
+ response_xml = http_get(@client, "#{@xero_url}/BankTransactions", request_params)
406
+
407
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/BankTransactions'})
408
+ end
409
+
410
+ # Retrieves a single bank transaction
411
+ #
412
+ # Usage : get_bank_transaction("297c2dc5-cc47-4afd-8ec8-74990b8761e9") # By ID
413
+ # get_bank_transaction("OIT-12345") # By number
414
+ def get_bank_transaction(bank_transaction_id)
415
+ request_params = {}
416
+ url = "#{@xero_url}/BankTransactions/#{URI.escape(bank_transaction_id)}"
417
+ response_xml = http_get(@client, url, request_params)
418
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/BankTransaction'})
419
+ end
420
+
421
+ # Creates a manual journal in Xero based on a manual journal object.
422
+ #
423
+ # Manual journal and line item totals are calculated automatically.
424
+ #
425
+ # Usage : # TODO
426
+
427
+ def create_manual_journal(manual_journal)
428
+ save_manual_journal(manual_journal)
429
+ end
430
+
431
+ #
432
+ # Updates an existing Xero manual journal
433
+ #
434
+ # Usage :
435
+ #
436
+ # manual_journal = xero_gateway.get_manual_journal(some_manual_journal_id)
437
+ #
438
+ # xero_gateway.update_manual_journal(manual_journal)
439
+ def update_manual_journal(manual_journal)
440
+ raise "manual_journal_id is required for updating manual journals" if manual_journal.manual_journal_id.nil?
441
+ save_manual_journal(manual_journal)
442
+ end
443
+
444
+ # Retrieves all manual journals from Xero
445
+ #
446
+ # Usage : get_manual_journal
447
+ # getmanual_journal(:manual_journal_id => " 297c2dc5-cc47-4afd-8ec8-74990b8761e9")
448
+ #
449
+ # Note : modified_since is in UTC format (i.e. Brisbane is UTC+10)
450
+ def get_manual_journals(options = {})
451
+ request_params = {}
452
+ request_params[:ManualJournalID] = options[:manual_journal_id] if options[:manual_journal_id]
453
+ request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
454
+
455
+ response_xml = http_get(@client, "#{@xero_url}/ManualJournals", request_params)
456
+
457
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/ManualJournals'})
458
+ end
459
+
460
+ # Retrieves a single manual journal
461
+ #
462
+ # Usage : get_manual_journal("297c2dc5-cc47-4afd-8ec8-74990b8761e9") # By ID
463
+ # get_manual_journal("OIT-12345") # By number
464
+ def get_manual_journal(manual_journal_id)
465
+ request_params = {}
466
+ url = "#{@xero_url}/ManualJournals/#{URI.escape(manual_journal_id)}"
467
+ response_xml = http_get(@client, url, request_params)
468
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/ManualJournal'})
469
+ end
470
+
471
+ #
472
+ # Gets all accounts for a specific organization in Xero.
473
+ #
474
+ def get_accounts
475
+ response_xml = http_get(@client, "#{xero_url}/Accounts")
476
+ parse_response(response_xml, {}, {:request_signature => 'GET/accounts'})
477
+ end
478
+
479
+ #
480
+ # Returns a XeroGateway::AccountsList object that makes working with
481
+ # the Xero list of accounts easier and allows caching the results.
482
+ #
483
+ def get_accounts_list(load_on_init = true)
484
+ AccountsList.new(self, load_on_init)
485
+ end
486
+
487
+ #
488
+ # Gets all tracking categories for a specific organization in Xero.
489
+ #
490
+ def get_tracking_categories
491
+ response_xml = http_get(@client, "#{xero_url}/TrackingCategories")
492
+
493
+ parse_response(response_xml, {}, {:request_signature => 'GET/TrackingCategories'})
494
+ end
495
+
496
+ #
497
+ # Gets Organisation details
498
+ #
499
+ def get_organisation
500
+ response_xml = http_get(@client, "#{xero_url}/Organisation")
501
+ parse_response(response_xml, {}, {:request_signature => 'GET/organisation'})
502
+ end
503
+
504
+ #
505
+ # Gets all currencies for a specific organisation in Xero
506
+ #
507
+ def get_currencies
508
+ response_xml = http_get(@client, "#{xero_url}/Currencies")
509
+ parse_response(response_xml, {}, {:request_signature => 'GET/currencies'})
510
+ end
511
+
512
+ #
513
+ # Gets all Tax Rates for a specific organisation in Xero
514
+ #
515
+ def get_tax_rates
516
+ response_xml = http_get(@client, "#{xero_url}/TaxRates")
517
+ parse_response(response_xml, {}, {:request_signature => 'GET/tax_rates'})
518
+ end
519
+
520
+ private
521
+
522
+ def get_contact(contact_id = nil, contact_number = nil)
523
+ request_params = contact_id ? { :contactID => contact_id } : { :contactNumber => contact_number }
524
+ response_xml = http_get(@client, "#{@xero_url}/Contacts/#{URI.escape(contact_id||contact_number)}", request_params)
525
+
526
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/contact'})
527
+ end
528
+
529
+ # Create or update a contact record based on if it has a contact_id or contact_number.
530
+ def save_contact(contact)
531
+ request_xml = contact.to_xml
532
+
533
+ response_xml = nil
534
+ create_or_save = nil
535
+ if contact.contact_id.nil? && contact.contact_number.nil?
536
+ # Create new contact record.
537
+ response_xml = http_put(@client, "#{@xero_url}/Contacts", request_xml, {})
538
+ create_or_save = :create
539
+ else
540
+ # Update existing contact record.
541
+ response_xml = http_post(@client, "#{@xero_url}/Contacts", request_xml, {})
542
+ create_or_save = :save
543
+ end
544
+
545
+ response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => "#{create_or_save == :create ? 'PUT' : 'POST'}/contact"})
546
+ contact.contact_id = response.contact.contact_id if response.contact && response.contact.contact_id
547
+ response
548
+ end
549
+
550
+ # Create or update an invoice record based on if it has an invoice_id.
551
+ def save_invoice(invoice)
552
+ request_xml = invoice.to_xml
553
+
554
+ response_xml = nil
555
+ create_or_save = nil
556
+ if invoice.invoice_id.nil?
557
+ # Create new invoice record.
558
+ response_xml = http_put(@client, "#{@xero_url}/Invoices", request_xml, {})
559
+ create_or_save = :create
560
+ else
561
+ # Update existing invoice record.
562
+ response_xml = http_post(@client, "#{@xero_url}/Invoices", request_xml, {})
563
+ create_or_save = :save
564
+ end
565
+
566
+ response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => "#{create_or_save == :create ? 'PUT' : 'POST'}/invoice"})
567
+
568
+ # Xero returns invoices inside an <Invoices> tag, even though there's only ever
569
+ # one for this request
570
+ response.response_item = response.invoices.first
571
+
572
+ if response.success? && response.invoice && response.invoice.invoice_id
573
+ invoice.invoice_id = response.invoice.invoice_id
574
+ end
575
+
576
+ response
577
+ end
578
+
579
+ # Create or update a bank transaction record based on if it has an bank_transaction_id.
580
+ def save_bank_transaction(bank_transaction)
581
+ request_xml = bank_transaction.to_xml
582
+ response_xml = nil
583
+ create_or_save = nil
584
+
585
+ if bank_transaction.bank_transaction_id.nil?
586
+ # Create new bank transaction record.
587
+ response_xml = http_put(@client, "#{@xero_url}/BankTransactions", request_xml, {})
588
+ create_or_save = :create
589
+ else
590
+ # Update existing bank transaction record.
591
+ response_xml = http_post(@client, "#{@xero_url}/BankTransactions", request_xml, {})
592
+ create_or_save = :save
593
+ end
594
+
595
+ response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => "#{create_or_save == :create ? 'PUT' : 'POST'}/BankTransactions"})
596
+
597
+ # Xero returns bank transactions inside an <BankTransactions> tag, even though there's only ever
598
+ # one for this request
599
+ response.response_item = response.bank_transactions.first
600
+
601
+ if response.success? && response.bank_transaction && response.bank_transaction.bank_transaction_id
602
+ bank_transaction.bank_transaction_id = response.bank_transaction.bank_transaction_id
603
+ end
604
+
605
+ response
606
+ end
607
+
608
+ # Create or update a manual journal record based on if it has an manual_journal_id.
609
+ def save_manual_journal(manual_journal)
610
+ request_xml = manual_journal.to_xml
611
+ response_xml = nil
612
+ create_or_save = nil
613
+
614
+ if manual_journal.manual_journal_id.nil?
615
+ # Create new manual journal record.
616
+ response_xml = http_put(@client, "#{@xero_url}/ManualJournals", request_xml, {})
617
+ create_or_save = :create
618
+ else
619
+ # Update existing manual journal record.
620
+ response_xml = http_post(@client, "#{@xero_url}/ManualJournals", request_xml, {})
621
+ create_or_save = :save
622
+ end
623
+
624
+ response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => "#{create_or_save == :create ? 'PUT' : 'POST'}/ManualJournals"})
625
+
626
+ # Xero returns manual journals inside an <ManualJournals> tag, even though there's only ever
627
+ # one for this request
628
+ response.response_item = response.manual_journals.first
629
+
630
+ manual_journal.manual_journal_id = response.manual_journal.manual_journal_id if response.success? && response.manual_journal && response.manual_journal.manual_journal_id
631
+
632
+ response
633
+ end
634
+
635
+ def parse_response(raw_response, request = {}, options = {})
636
+
637
+ response = XeroGateway::Response.new
638
+
639
+ doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
640
+
641
+ # check for responses we don't understand
642
+ raise UnparseableResponse.new(doc.root.name) unless doc.root.name == "Response"
643
+
644
+ response_element = REXML::XPath.first(doc, "/Response")
645
+
646
+ response_element.children.reject { |e| e.is_a? REXML::Text }.each do |element|
647
+ case(element.name)
648
+ when "ID" then response.response_id = element.text
649
+ when "Status" then response.status = element.text
650
+ when "ProviderName" then response.provider = element.text
651
+ when "DateTimeUTC" then response.date_time = element.text
652
+ when "Contact" then response.response_item = Contact.from_xml(element, self)
653
+ when "Invoice" then response.response_item = Invoice.from_xml(element, self, {:line_items_downloaded => options[:request_signature] != "GET/Invoices"})
654
+ when "BankTransaction"
655
+ response.response_item = BankTransaction.from_xml(element, self, {:line_items_downloaded => options[:request_signature] != "GET/BankTransactions"})
656
+ when "ManualJournal"
657
+ response.response_item = ManualJournal.from_xml(element, self, {:journal_lines_downloaded => options[:request_signature] != "GET/ManualJournals"})
658
+ when "Contacts" then element.children.each {|child| response.response_item << Contact.from_xml(child, self) }
659
+ when "Invoices" then element.children.each {|child| response.response_item << Invoice.from_xml(child, self, {:line_items_downloaded => options[:request_signature] != "GET/Invoices"}) }
660
+ when "BankTransactions"
661
+ element.children.each do |child|
662
+ response.response_item << BankTransaction.from_xml(child, self, {:line_items_downloaded => options[:request_signature] != "GET/BankTransactions"})
663
+ end
664
+ when "ManualJournals"
665
+ element.children.each do |child|
666
+ response.response_item << ManualJournal.from_xml(child, self, {:journal_lines_downloaded => options[:request_signature] != "GET/ManualJournals"})
667
+ end
668
+ when "CreditNotes" then element.children.each {|child| response.response_item << CreditNote.from_xml(child, self, {:line_items_downloaded => options[:request_signature] != "GET/CreditNotes"}) }
669
+ when "Accounts" then element.children.each {|child| response.response_item << Account.from_xml(child) }
670
+ when "TaxRates" then element.children.each {|child| response.response_item << TaxRate.from_xml(child) }
671
+ when "Currencies" then element.children.each {|child| response.response_item << Currency.from_xml(child) }
672
+ when "Organisations" then response.response_item = Organisation.from_xml(element.children.first) # Xero only returns the Authorized Organisation
673
+ when "TrackingCategories" then element.children.each {|child| response.response_item << TrackingCategory.from_xml(child) }
674
+ when "Errors" then element.children.each { |error| parse_error(error, response) }
675
+ end
676
+ end if response_element
677
+
678
+ # If a single result is returned don't put it in an array
679
+ if response.response_item.is_a?(Array) && response.response_item.size == 1
680
+ response.response_item = response.response_item.first
681
+ end
682
+
683
+ response.request_params = request[:request_params]
684
+ response.request_xml = request[:request_xml]
685
+ response.response_xml = raw_response
686
+ response
687
+ end
688
+
689
+ def parse_error(error_element, response)
690
+ response.errors << Error.new(
691
+ :description => REXML::XPath.first(error_element, "Description").text,
692
+ :date_time => REXML::XPath.first(error_element, "//DateTime").text,
693
+ :type => REXML::XPath.first(error_element, "//ExceptionType").text,
694
+ :message => REXML::XPath.first(error_element, "//Message").text
695
+ )
696
+ end
697
+ end
698
+ end