xero_gateway 2.0.4 → 2.0.5

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.
Files changed (41) hide show
  1. data/README.textile +57 -5
  2. data/lib/xero_gateway/account.rb +8 -2
  3. data/lib/xero_gateway/credit_note.rb +275 -0
  4. data/lib/xero_gateway/exceptions.rb +3 -1
  5. data/lib/xero_gateway/gateway.rb +119 -3
  6. data/lib/xero_gateway/http.rb +10 -6
  7. data/lib/xero_gateway/oauth.rb +3 -1
  8. data/lib/xero_gateway/response.rb +2 -0
  9. data/lib/xero_gateway/tracking_category.rb +5 -2
  10. data/lib/xero_gateway.rb +1 -0
  11. data/test/integration/create_credit_note_test.rb +49 -0
  12. data/test/integration/get_credit_note_test.rb +48 -0
  13. data/test/integration/get_credit_notes_test.rb +90 -0
  14. data/test/integration/get_tracking_categories_test.rb +3 -2
  15. data/test/test_helper.rb +24 -1
  16. data/test/unit/account_test.rb +3 -2
  17. data/test/unit/credit_note_test.rb +284 -0
  18. data/test/unit/gateway_test.rb +17 -1
  19. data/xero_gateway.gemspec +3 -76
  20. metadata +13 -29
  21. data/CHANGELOG.textile +0 -57
  22. data/test/stub_responses/accounts.xml +0 -1
  23. data/test/stub_responses/api_exception.xml +0 -153
  24. data/test/stub_responses/contact.xml +0 -1
  25. data/test/stub_responses/contacts.xml +0 -2189
  26. data/test/stub_responses/create_invoice.xml +0 -64
  27. data/test/stub_responses/currencies.xml +0 -16
  28. data/test/stub_responses/invalid_api_key_error.xml +0 -1
  29. data/test/stub_responses/invalid_consumer_key +0 -1
  30. data/test/stub_responses/invalid_request_token +0 -1
  31. data/test/stub_responses/invoice.xml +0 -1
  32. data/test/stub_responses/invoice_not_found_error.xml +0 -1
  33. data/test/stub_responses/invoices.xml +0 -1
  34. data/test/stub_responses/organisation.xml +0 -14
  35. data/test/stub_responses/tax_rates.xml +0 -52
  36. data/test/stub_responses/token_expired +0 -1
  37. data/test/stub_responses/tracking_categories.xml +0 -1
  38. data/test/stub_responses/unknown_error.xml +0 -1
  39. data/test/xsd/README +0 -2
  40. data/test/xsd/create_contact.xsd +0 -61
  41. data/test/xsd/create_invoice.xsd +0 -107
data/README.textile CHANGED
@@ -147,7 +147,7 @@ h3. GET /api.xro/2.0/contacts (get_contacts)
147
147
 
148
148
  Gets all contact records for a particular Xero customer.
149
149
  <pre><code> gateway.get_contacts(:type => :all, :sort => :name, :direction => :desc)
150
- gateway.get_contacts(:type => :all, :updated_after => 1.month.ago) # modified since 1 month ago</code></pre>
150
+ gateway.get_contacts(:type => :all, :modified_since => 1.month.ago) # modified since 1 month ago</code></pre>
151
151
 
152
152
 
153
153
 
@@ -200,7 +200,7 @@ h3. GET /api.xro/2.0/invoices (get_invoices)
200
200
 
201
201
  Gets all invoice records for a particular Xero customer.
202
202
  <pre><code> gateway.get_invoices
203
- gateway.get_invoices(1.month.ago) # modified since 1 month ago</code></pre>
203
+ gateway.get_invoices(:modified_since => 1.month.ago) # modified since 1 month ago</code></pre>
204
204
 
205
205
 
206
206
 
@@ -214,8 +214,7 @@ Invoice and line item totals are calculated automatically.
214
214
  :due_date => 1.month.from_now,
215
215
  :invoice_number => "YOUR INVOICE NUMBER",
216
216
  :reference => "YOUR REFERENCE (NOT NECESSARILY UNIQUE!)",
217
- :tax_inclusive => true,
218
- :includes_tax => false
217
+ :line_amount_types => "Inclusive" # "Inclusive", "Exclusive" or "NoTax"
219
218
  })
220
219
  invoice.contact.name = "THE NAME OF THE CONTACT"
221
220
  invoice.contact.phone.number = "12345"
@@ -238,6 +237,59 @@ This method uses only a single API request to create/update multiple contacts.
238
237
  <pre><code> invoices = [XeroGateway::Invoice.new(...), XeroGateway::Invoice.new(...)]
239
238
  result = gateway.create_invoices(invoices)</code></pre>
240
239
 
240
+ h3. GET /api.xro/2.0/credit_note (get_credit_note_by_id)
241
+
242
+ Gets an credit_note record for a specific Xero organisation
243
+ <pre><code> gateway.get_credit_note_by_id(credit_note_id)</code></pre>
244
+
245
+
246
+ h3. GET /api.xro/2.0/credit_note (get_credit_note_by_number)
247
+
248
+ Gets a credit note record for a specific Xero organisation
249
+ <pre><code> gateway.get_credit_note_by_number(credit_note_number)</code></pre>
250
+
251
+
252
+
253
+ h3. GET /api.xro/2.0/credit_notes (get_credit_notes)
254
+
255
+ Gets all credit note records for a particular Xero customer.
256
+ <pre><code> gateway.get_credit_notes
257
+ gateway.get_credit_notes(:modified_since => 1.month.ago) # modified since 1 month ago</code></pre>
258
+
259
+
260
+
261
+ h3. PUT /api.xro/2.0/credit_note
262
+
263
+ Inserts a credit note for a specific organization in Xero (Currently only adding new credit notes is allowed).
264
+
265
+ CreditNote and line item totals are calculated automatically.
266
+ <pre><code> credit_note = gateway.build_credit_note({
267
+ :credit_note_type => "ACCRECCREDIT",
268
+ :credit_note_number => "YOUR CREDIT NOTE NUMBER",
269
+ :reference => "YOUR REFERENCE (NOT NECESSARILY UNIQUE!)",
270
+ :line_amount_types => "Inclusive" # "Inclusive", "Exclusive" or "NoTax"
271
+ })
272
+ credit_note.contact.name = "THE NAME OF THE CONTACT"
273
+ credit_note.contact.phone.number = "12345"
274
+ credit_note.contact.address.line_1 = "LINE 1 OF THE ADDRESS"
275
+ credit_note.add_line_item({
276
+ :description => "THE DESCRIPTION OF THE LINE ITEM",
277
+ :unit_amount => 1000,
278
+ :tax_amount => 125,
279
+ :tracking_category => "THE TRACKING CATEGORY FOR THE LINE ITEM",
280
+ :tracking_option => "THE TRACKING OPTION FOR THE LINE ITEM"
281
+ })
282
+
283
+ credit_note.create</code></pre>
284
+
285
+
286
+ h3. PUT /api.xro/2.0/credit_notes
287
+
288
+ Inserts multiple credit notes for a specific organization in Xero (currently only adding new credit notes is allowed).
289
+ This method uses only a single API request to create/update multiple contacts.
290
+ <pre><code> credit_notes = [XeroGateway::CreditNote.new(...), XeroGateway::CreditNote.new(...)]
291
+ result = gateway.create_credit_notes(credit_notes)</code></pre>
292
+
241
293
  h3. GET /api.xro/2.0/accounts
242
294
 
243
295
  Gets all accounts for a specific organization in Xero.
@@ -286,4 +338,4 @@ You can specify a logger to use (so you can track down those tricky exceptions)
286
338
  gateway.logger = ActiveSupport::BufferedLogger.new("log_file_name.log")
287
339
  </pre>
288
340
 
289
- It doesn't have to be a buffered logger - anything that responds to "info" will do just fine.
341
+ It doesn't have to be a buffered logger - anything that responds to "info" will do just fine.
@@ -33,7 +33,7 @@ module XeroGateway
33
33
  'ZERORATED' => 'Zero-rated supplies/sales from overseas (NZ Only)'
34
34
  } unless defined?(TAX_TYPE)
35
35
 
36
- attr_accessor :code, :name, :type, :tax_type, :description
36
+ attr_accessor :account_id, :code, :name, :type, :tax_type, :description, :system_account, :enable_payments_to_account
37
37
 
38
38
  def initialize(params = {})
39
39
  params.each do |k,v|
@@ -42,7 +42,7 @@ module XeroGateway
42
42
  end
43
43
 
44
44
  def ==(other)
45
- [:code, :name, :type, :tax_type, :description].each do |field|
45
+ [:account_id, :code, :name, :type, :tax_type, :description, :system_account, :enable_payments_to_account].each do |field|
46
46
  return false if send(field) != other.send(field)
47
47
  end
48
48
  return true
@@ -52,11 +52,14 @@ module XeroGateway
52
52
  b = Builder::XmlMarkup.new
53
53
 
54
54
  b.Account {
55
+ b.AccountID self.account_id
55
56
  b.Code self.code
56
57
  b.Name self.name
57
58
  b.Type self.type
58
59
  b.TaxType self.tax_type
59
60
  b.Description self.description
61
+ b.SystemAccount self.system_account unless self.system_account.nil?
62
+ b.EnablePaymentsToAccount self.enable_payments_to_account
60
63
  }
61
64
  end
62
65
 
@@ -64,11 +67,14 @@ module XeroGateway
64
67
  account = Account.new
65
68
  account_element.children.each do |element|
66
69
  case(element.name)
70
+ when "AccountID" then account.account_id = element.text
67
71
  when "Code" then account.code = element.text
68
72
  when "Name" then account.name = element.text
69
73
  when "Type" then account.type = element.text
70
74
  when "TaxType" then account.tax_type = element.text
71
75
  when "Description" then account.description = element.text
76
+ when "SystemAccount" then account.system_account = element.text
77
+ when "EnablePaymentsToAccount" then account.enable_payments_to_account = (element.text == 'true')
72
78
  end
73
79
  end
74
80
  account
@@ -0,0 +1,275 @@
1
+ module XeroGateway
2
+ class CreditNote
3
+ include Dates
4
+ include Money
5
+
6
+ class Error < RuntimeError; end
7
+ class NoGatewayError < Error; end
8
+ class InvalidLineItemError < Error; end
9
+
10
+ CREDIT_NOTE_TYPE = {
11
+ 'ACCRECCREDIT' => 'Accounts Receivable',
12
+ 'ACCPAYCREDIT' => 'Accounts Payable'
13
+ } unless defined?(CREDIT_NOTE_TYPE)
14
+
15
+ LINE_AMOUNT_TYPES = {
16
+ "Inclusive" => 'CreditNote lines are inclusive tax',
17
+ "Exclusive" => 'CreditNote lines are exclusive of tax (default)',
18
+ "NoTax" => 'CreditNotes lines have no tax'
19
+ } unless defined?(LINE_AMOUNT_TYPES)
20
+
21
+ CREDIT_NOTE_STATUS = {
22
+ 'AUTHORISED' => 'Approved credit_notes awaiting payment',
23
+ 'DELETED' => 'Draft credit_notes that are deleted',
24
+ 'DRAFT' => 'CreditNotes saved as draft or entered via API',
25
+ 'PAID' => 'CreditNotes approved and fully paid',
26
+ 'SUBMITTED' => 'CreditNotes entered by an employee awaiting approval',
27
+ 'VOID' => 'Approved credit_notes that are voided'
28
+ } unless defined?(CREDIT_NOTE_STATUS)
29
+
30
+ 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)
31
+
32
+ # Xero::Gateway associated with this credit_note.
33
+ attr_accessor :gateway
34
+
35
+ # Any errors that occurred when the #valid? method called.
36
+ attr_reader :errors
37
+
38
+ # Represents whether the line_items have been downloaded when getting from GET /API.XRO/2.0/CreditNotes
39
+ attr_accessor :line_items_downloaded
40
+
41
+ # All accessible fields
42
+ attr_accessor :credit_note_id, :credit_note_number, :type, :status, :date, :reference, :line_amount_types, :currency_code, :line_items, :contact, :payments, :fully_paid_on, :amount_credited
43
+
44
+
45
+ def initialize(params = {})
46
+ @errors ||= []
47
+ @payments ||= []
48
+
49
+ # Check if the line items have been downloaded.
50
+ @line_items_downloaded = (params.delete(:line_items_downloaded) == true)
51
+
52
+ params = {
53
+ :line_amount_types => "Inclusive"
54
+ }.merge(params)
55
+
56
+ params.each do |k,v|
57
+ self.send("#{k}=", v)
58
+ end
59
+
60
+ @line_items ||= []
61
+ end
62
+
63
+ # Validate the Address record according to what will be valid by the gateway.
64
+ #
65
+ # Usage:
66
+ # address.valid? # Returns true/false
67
+ #
68
+ # Additionally sets address.errors array to an array of field/error.
69
+ def valid?
70
+ @errors = []
71
+
72
+ if !credit_note_id.nil? && credit_note_id !~ GUID_REGEX
73
+ @errors << ['credit_note_id', 'must be blank or a valid Xero GUID']
74
+ end
75
+
76
+ if status && !CREDIT_NOTE_STATUS[status]
77
+ @errors << ['status', "must be one of #{CREDIT_NOTE_STATUS.keys.join('/')}"]
78
+ end
79
+
80
+ if line_amount_types && !LINE_AMOUNT_TYPES[line_amount_types]
81
+ @errors << ['line_amount_types', "must be one of #{LINE_AMOUNT_TYPES.keys.join('/')}"]
82
+ end
83
+
84
+ unless date
85
+ @errors << ['credit_note_date', "can't be blank"]
86
+ end
87
+
88
+ # Make sure contact is valid.
89
+ unless @contact && @contact.valid?
90
+ @errors << ['contact', 'is invalid']
91
+ end
92
+
93
+ # Make sure all line_items are valid.
94
+ unless line_items.all? { | line_item | line_item.valid? }
95
+ @errors << ['line_items', "at least one line item invalid"]
96
+ end
97
+
98
+ @errors.size == 0
99
+ end
100
+
101
+ # Helper method to create the associated contact object.
102
+ def build_contact(params = {})
103
+ self.contact = gateway ? gateway.build_contact(params) : Contact.new(params)
104
+ end
105
+
106
+ def contact
107
+ @contact ||= build_contact
108
+ end
109
+
110
+ # Helper method to create a new associated line_item.
111
+ # Usage:
112
+ # credit_note.add_line_item({:description => "Bob's Widgets", :quantity => 1, :unit_amount => 120})
113
+ def add_line_item(params = {})
114
+ line_item = nil
115
+ case params
116
+ when Hash then line_item = LineItem.new(params)
117
+ when LineItem then line_item = params
118
+ else raise InvalidLineItemError
119
+ end
120
+
121
+ @line_items << line_item
122
+
123
+ line_item
124
+ end
125
+
126
+ # Deprecated (but API for setter remains).
127
+ #
128
+ # As sub_total must equal SUM(line_item.line_amount) for the API call to pass, this is now
129
+ # automatically calculated in the sub_total method.
130
+ def sub_total=(value)
131
+ end
132
+
133
+ # Calculate the sub_total as the SUM(line_item.line_amount).
134
+ def sub_total
135
+ line_items.inject(BigDecimal.new('0')) { | sum, line_item | sum + BigDecimal.new(line_item.line_amount.to_s) }
136
+ end
137
+
138
+ # Deprecated (but API for setter remains).
139
+ #
140
+ # As total_tax must equal SUM(line_item.tax_amount) for the API call to pass, this is now
141
+ # automatically calculated in the total_tax method.
142
+ def total_tax=(value)
143
+ end
144
+
145
+ # Calculate the total_tax as the SUM(line_item.tax_amount).
146
+ def total_tax
147
+ line_items.inject(BigDecimal.new('0')) { | sum, line_item | sum + BigDecimal.new(line_item.tax_amount.to_s) }
148
+ end
149
+
150
+ # Deprecated (but API for setter remains).
151
+ #
152
+ # As total must equal sub_total + total_tax for the API call to pass, this is now
153
+ # automatically calculated in the total method.
154
+ def total=(value)
155
+ end
156
+
157
+ # Calculate the toal as sub_total + total_tax.
158
+ def total
159
+ sub_total + total_tax
160
+ end
161
+
162
+ # Helper method to check if the credit_note is accounts payable.
163
+ def accounts_payable?
164
+ type == 'ACCPAYCREDIT'
165
+ end
166
+
167
+ # Helper method to check if the credit_note is accounts receivable.
168
+ def accounts_receivable?
169
+ type == 'ACCRECCREDIT'
170
+ end
171
+
172
+ # Whether or not the line_items have been downloaded (GET/credit_notes does not download line items).
173
+ def line_items_downloaded?
174
+ @line_items_downloaded
175
+ end
176
+
177
+ # If line items are not downloaded, then attempt a download now (if this record was found to begin with).
178
+ def line_items
179
+ if line_items_downloaded?
180
+ @line_items
181
+
182
+ # There is an credit_note_is so we can assume this record was loaded from Xero.
183
+ # attempt to download the line_item records.
184
+ elsif credit_note_id =~ GUID_REGEX
185
+ raise NoGatewayError unless @gateway
186
+
187
+ response = @gateway.get_credit_note(credit_note_id)
188
+ raise CreditNoteNotFoundError, "CreditNote with ID #{credit_note_id} not found in Xero." unless response.success? && response.credit_note.is_a?(XeroGateway::CreditNote)
189
+
190
+ @line_items = response.credit_note.line_items
191
+ @line_items_downloaded = true
192
+
193
+ @line_items
194
+
195
+ # Otherwise, this is a new credit_note, so return the line_items reference.
196
+ else
197
+ @line_items
198
+ end
199
+ end
200
+
201
+ def ==(other)
202
+ ["credit_note_number", "type", "status", "reference", "currency_code", "line_amount_types", "contact", "line_items"].each do |field|
203
+ return false if send(field) != other.send(field)
204
+ end
205
+
206
+ ["date"].each do |field|
207
+ return false if send(field).to_s != other.send(field).to_s
208
+ end
209
+ return true
210
+ end
211
+
212
+ # General purpose createsave method.
213
+ # If contact_id and contact_number are nil then create, otherwise, attempt to save.
214
+ def save
215
+ create
216
+ end
217
+
218
+ # Creates this credit_note record (using gateway.create_credit_note) with the associated gateway.
219
+ # If no gateway set, raise a Xero::CreditNote::NoGatewayError exception.
220
+ def create
221
+ raise NoGatewayError unless gateway
222
+ gateway.create_credit_note(self)
223
+ end
224
+
225
+ # Alias create as save as this is currently the only write action.
226
+ alias_method :save, :create
227
+
228
+ def to_xml(b = Builder::XmlMarkup.new)
229
+ b.CreditNote {
230
+ b.Type self.type
231
+ contact.to_xml(b)
232
+ b.Date CreditNote.format_date(self.date || Date.today)
233
+ b.Status self.status if self.status
234
+ b.CreditNoteNumber self.credit_note_number if credit_note_number
235
+ b.Reference self.reference if self.reference
236
+ b.CurrencyCode self.currency_code if self.currency_code
237
+ b.LineAmountTypes self.line_amount_types
238
+ b.LineItems {
239
+ self.line_items.each do |line_item|
240
+ line_item.to_xml(b)
241
+ end
242
+ }
243
+ }
244
+ end
245
+
246
+ #TODO UpdatedDateUTC
247
+ def self.from_xml(credit_note_element, gateway = nil, options = {})
248
+ credit_note = CreditNote.new(options.merge({:gateway => gateway}))
249
+ credit_note_element.children.each do |element|
250
+ case(element.name)
251
+ when "CreditNoteID" then credit_note.credit_note_id = element.text
252
+ when "CreditNoteNumber" then credit_note.credit_note_number = element.text
253
+ when "Type" then credit_note.type = element.text
254
+ when "CurrencyCode" then credit_note.currency_code = element.text
255
+ when "Contact" then credit_note.contact = Contact.from_xml(element)
256
+ when "Date" then credit_note.date = parse_date(element.text)
257
+ when "Status" then credit_note.status = element.text
258
+ when "Reference" then credit_note.reference = element.text
259
+ when "LineAmountTypes" then credit_note.line_amount_types = element.text
260
+ when "LineItems" then element.children.each {|line_item| credit_note.line_items_downloaded = true; credit_note.line_items << LineItem.from_xml(line_item) }
261
+ when "SubTotal" then credit_note.sub_total = BigDecimal.new(element.text)
262
+ when "TotalTax" then credit_note.total_tax = BigDecimal.new(element.text)
263
+ when "Total" then credit_note.total = BigDecimal.new(element.text)
264
+ when "CreditNoteID" then credit_note.credit_note_id = element.text
265
+ when "CreditNoteNumber" then credit_note.credit_note_number = element.text
266
+ when "Payments" then element.children.each { | payment | credit_note.payments << Payment.from_xml(payment) }
267
+ when "AmountDue" then credit_note.amount_due = BigDecimal.new(element.text)
268
+ when "AmountPaid" then credit_note.amount_paid = BigDecimal.new(element.text)
269
+ when "AmountCredited" then credit_note.amount_credited = BigDecimal.new(element.text)
270
+ end
271
+ end
272
+ credit_note
273
+ end
274
+ end
275
+ end
@@ -38,4 +38,6 @@ module XeroGateway
38
38
  end
39
39
 
40
40
  class InvoiceNotFoundError < StandardError; end
41
- end
41
+
42
+ class CreditNoteNotFoundError < StandardError; end
43
+ end
@@ -21,16 +21,21 @@ module XeroGateway
21
21
  # Retrieve all contacts from Xero
22
22
  #
23
23
  # Usage : get_contacts(:order => :name)
24
- # get_contacts(:updated_after => Time)
24
+ # get_contacts(:modified_since => Time)
25
25
  #
26
26
  # Note : modified_since is in UTC format (i.e. Brisbane is UTC+10)
27
27
  def get_contacts(options = {})
28
28
  request_params = {}
29
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
+
30
35
  request_params[:ContactID] = options[:contact_id] if options[:contact_id]
31
36
  request_params[:ContactNumber] = options[:contact_number] if options[:contact_number]
32
37
  request_params[:OrderBy] = options[:order] if options[:order]
33
- request_params[:ModifiedAfter] = Gateway.format_date_time(options[:updated_after]) if options[:updated_after]
38
+ request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
34
39
  request_params[:where] = options[:where] if options[:where]
35
40
 
36
41
  response_xml = http_get(@client, "#{@xero_url}/Contacts", request_params)
@@ -134,7 +139,7 @@ module XeroGateway
134
139
  request_params[:InvoiceID] = options[:invoice_id] if options[:invoice_id]
135
140
  request_params[:InvoiceNumber] = options[:invoice_number] if options[:invoice_number]
136
141
  request_params[:OrderBy] = options[:order] if options[:order]
137
- request_params[:ModifiedAfter] = options[:modified_since]
142
+ request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
138
143
 
139
144
  request_params[:where] = options[:where] if options[:where]
140
145
 
@@ -231,6 +236,116 @@ module XeroGateway
231
236
  response
232
237
  end
233
238
 
239
+ # Retrieves all credit_notes from Xero
240
+ #
241
+ # Usage : get_credit_notes
242
+ # get_credit_notes(:credit_note_id => " 297c2dc5-cc47-4afd-8ec8-74990b8761e9")
243
+ #
244
+ # Note : modified_since is in UTC format (i.e. Brisbane is UTC+10)
245
+ def get_credit_notes(options = {})
246
+
247
+ request_params = {}
248
+
249
+ request_params[:CreditNoteID] = options[:credit_note_id] if options[:credit_note_id]
250
+ request_params[:CreditNoteNumber] = options[:credit_note_number] if options[:credit_note_number]
251
+ request_params[:OrderBy] = options[:order] if options[:order]
252
+ request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
253
+
254
+ request_params[:where] = options[:where] if options[:where]
255
+
256
+ response_xml = http_get(@client, "#{@xero_url}/CreditNotes", request_params)
257
+
258
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/CreditNotes'})
259
+ end
260
+
261
+ # Retrieves a single credit_note
262
+ #
263
+ # Usage : get_credit_note("297c2dc5-cc47-4afd-8ec8-74990b8761e9") # By ID
264
+ # get_credit_note("OIT-12345") # By number
265
+ def get_credit_note(credit_note_id_or_number)
266
+ request_params = {}
267
+
268
+ url = "#{@xero_url}/CreditNotes/#{URI.escape(credit_note_id_or_number)}"
269
+
270
+ response_xml = http_get(@client, url, request_params)
271
+
272
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/CreditNote'})
273
+ end
274
+
275
+ # Factory method for building new CreditNote objects associated with this gateway.
276
+ def build_credit_note(credit_note = {})
277
+ case credit_note
278
+ when CreditNote then credit_note.gateway = self
279
+ when Hash then credit_note = CreditNote.new(credit_note.merge(:gateway => self))
280
+ end
281
+ credit_note
282
+ end
283
+
284
+ # Creates an credit_note in Xero based on an credit_note object.
285
+ #
286
+ # CreditNote and line item totals are calculated automatically.
287
+ #
288
+ # Usage :
289
+ #
290
+ # credit_note = XeroGateway::CreditNote.new({
291
+ # :credit_note_type => "ACCREC",
292
+ # :due_date => 1.month.from_now,
293
+ # :credit_note_number => "YOUR CREDIT_NOTE NUMBER",
294
+ # :reference => "YOUR REFERENCE (NOT NECESSARILY UNIQUE!)",
295
+ # :line_amount_types => "Inclusive"
296
+ # })
297
+ # credit_note.contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT")
298
+ # credit_note.contact.phone.number = "12345"
299
+ # credit_note.contact.address.line_1 = "LINE 1 OF THE ADDRESS"
300
+ # credit_note.line_items << XeroGateway::LineItem.new(
301
+ # :description => "THE DESCRIPTION OF THE LINE ITEM",
302
+ # :unit_amount => 100,
303
+ # :tax_amount => 12.5,
304
+ # :tracking_category => "THE TRACKING CATEGORY FOR THE LINE ITEM",
305
+ # :tracking_option => "THE TRACKING OPTION FOR THE LINE ITEM"
306
+ # )
307
+ #
308
+ # create_credit_note(credit_note)
309
+ def create_credit_note(credit_note)
310
+ request_xml = credit_note.to_xml
311
+ response_xml = http_put(@client, "#{@xero_url}/CreditNotes", request_xml)
312
+ response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/credit_note'})
313
+
314
+ # Xero returns credit_notes inside an <CreditNotes> tag, even though there's only ever
315
+ # one for this request
316
+ response.response_item = response.credit_notes.first
317
+
318
+ if response.success? && response.credit_note && response.credit_note.credit_note_id
319
+ credit_note.credit_note_id = response.credit_note.credit_note_id
320
+ end
321
+
322
+ response
323
+ end
324
+
325
+ #
326
+ # Creates an array of credit_notes with a single API request.
327
+ #
328
+ # Usage :
329
+ # credit_notes = [XeroGateway::CreditNote.new(...), XeroGateway::CreditNote.new(...)]
330
+ # result = gateway.create_credit_notes(credit_notes)
331
+ #
332
+ def create_credit_notes(credit_notes)
333
+ b = Builder::XmlMarkup.new
334
+ request_xml = b.CreditNotes {
335
+ credit_notes.each do | credit_note |
336
+ credit_note.to_xml(b)
337
+ end
338
+ }
339
+
340
+ response_xml = http_put(@client, "#{@xero_url}/CreditNotes", request_xml, {})
341
+
342
+ response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/credit_notes'})
343
+ response.credit_notes.each_with_index do | response_credit_note, index |
344
+ credit_notes[index].credit_note_id = response_credit_note.credit_note_id if response_credit_note && response_credit_note.credit_note_id
345
+ end
346
+ response
347
+ end
348
+
234
349
  #
235
350
  # Gets all accounts for a specific organization in Xero.
236
351
  #
@@ -331,6 +446,7 @@ module XeroGateway
331
446
  when "Invoice" then response.response_item = Invoice.from_xml(element, self, {:line_items_downloaded => options[:request_signature] != "GET/Invoices"})
332
447
  when "Contacts" then element.children.each {|child| response.response_item << Contact.from_xml(child, self) }
333
448
  when "Invoices" then element.children.each {|child| response.response_item << Invoice.from_xml(child, self, {:line_items_downloaded => options[:request_signature] != "GET/Invoices"}) }
449
+ when "CreditNotes" then element.children.each {|child| response.response_item << CreditNote.from_xml(child, self, {:line_items_downloaded => options[:request_signature] != "GET/CreditNotes"}) }
334
450
  when "Accounts" then element.children.each {|child| response.response_item << Account.from_xml(child) }
335
451
  when "TaxRates" then element.children.each {|child| response.response_item << TaxRate.from_xml(child) }
336
452
  when "Currencies" then element.children.each {|child| response.response_item << Currency.from_xml(child) }
@@ -85,10 +85,14 @@ module XeroGateway
85
85
  description = error_details["oauth_problem_advice"].first
86
86
 
87
87
  # see http://oauth.pbworks.com/ProblemReporting
88
- # Xero only appears to return either token_expired or token_rejected
88
+ # In addition to token_expired and token_rejected, Xero also returns
89
+ # 'rate limit exceeded' when more than 60 requests have been made in
90
+ # a second.
89
91
  case (error_details["oauth_problem"].first)
90
92
  when "token_expired" then raise OAuth::TokenExpired.new(description)
91
93
  when "token_rejected" then raise OAuth::TokenInvalid.new(description)
94
+ when "rate limit exceeded" then raise OAuth::RateLimitExceeded.new(description)
95
+ else raise OAuth::UnknownError.new(error_details["oauth_problem"].first + ':' + description)
92
96
  end
93
97
  end
94
98
 
@@ -117,12 +121,12 @@ module XeroGateway
117
121
  end
118
122
 
119
123
  def handle_object_not_found!(response, request_url)
120
- if request_url =~ /Invoices/
121
- raise InvoiceNotFoundError.new("Invoice not found in Xero.")
122
- else
123
- raise ObjectNotFound.new(request_url)
124
+ case(request_url)
125
+ when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.")
126
+ when /CreditNotes/ then raise CreditNoteNotFoundError.new("Credit Note not found in Xero.")
127
+ else raise ObjectNotFound.new(request_url)
124
128
  end
125
129
  end
126
130
 
127
131
  end
128
- end
132
+ end
@@ -10,6 +10,8 @@ module XeroGateway
10
10
 
11
11
  class TokenExpired < StandardError; end
12
12
  class TokenInvalid < StandardError; end
13
+ class RateLimitExceeded < StandardError; end
14
+ class UnknownError < StandardError; end
13
15
 
14
16
  unless defined? XERO_CONSUMER_OPTIONS
15
17
  XERO_CONSUMER_OPTIONS = {
@@ -53,4 +55,4 @@ module XeroGateway
53
55
  end
54
56
 
55
57
  end
56
- end
58
+ end
@@ -7,9 +7,11 @@ module XeroGateway
7
7
  end
8
8
 
9
9
  alias_method :invoice, :response_item
10
+ alias_method :credit_note, :response_item
10
11
  alias_method :contact, :response_item
11
12
  alias_method :organisation, :response_item
12
13
  alias_method :invoices, :array_wrapped_response_item
14
+ alias_method :credit_notes, :array_wrapped_response_item
13
15
  alias_method :contacts, :array_wrapped_response_item
14
16
  alias_method :accounts, :array_wrapped_response_item
15
17
  alias_method :tracking_categories, :array_wrapped_response_item