xero_gateway 2.0.4 → 2.0.5

Sign up to get free protection for your applications and to get access to all the features.
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