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.
- data/README.textile +57 -5
- data/lib/xero_gateway/account.rb +8 -2
- data/lib/xero_gateway/credit_note.rb +275 -0
- data/lib/xero_gateway/exceptions.rb +3 -1
- data/lib/xero_gateway/gateway.rb +119 -3
- data/lib/xero_gateway/http.rb +10 -6
- data/lib/xero_gateway/oauth.rb +3 -1
- data/lib/xero_gateway/response.rb +2 -0
- data/lib/xero_gateway/tracking_category.rb +5 -2
- data/lib/xero_gateway.rb +1 -0
- data/test/integration/create_credit_note_test.rb +49 -0
- data/test/integration/get_credit_note_test.rb +48 -0
- data/test/integration/get_credit_notes_test.rb +90 -0
- data/test/integration/get_tracking_categories_test.rb +3 -2
- data/test/test_helper.rb +24 -1
- data/test/unit/account_test.rb +3 -2
- data/test/unit/credit_note_test.rb +284 -0
- data/test/unit/gateway_test.rb +17 -1
- data/xero_gateway.gemspec +3 -76
- metadata +13 -29
- data/CHANGELOG.textile +0 -57
- data/test/stub_responses/accounts.xml +0 -1
- data/test/stub_responses/api_exception.xml +0 -153
- data/test/stub_responses/contact.xml +0 -1
- data/test/stub_responses/contacts.xml +0 -2189
- data/test/stub_responses/create_invoice.xml +0 -64
- data/test/stub_responses/currencies.xml +0 -16
- data/test/stub_responses/invalid_api_key_error.xml +0 -1
- data/test/stub_responses/invalid_consumer_key +0 -1
- data/test/stub_responses/invalid_request_token +0 -1
- data/test/stub_responses/invoice.xml +0 -1
- data/test/stub_responses/invoice_not_found_error.xml +0 -1
- data/test/stub_responses/invoices.xml +0 -1
- data/test/stub_responses/organisation.xml +0 -14
- data/test/stub_responses/tax_rates.xml +0 -52
- data/test/stub_responses/token_expired +0 -1
- data/test/stub_responses/tracking_categories.xml +0 -1
- data/test/stub_responses/unknown_error.xml +0 -1
- data/test/xsd/README +0 -2
- data/test/xsd/create_contact.xsd +0 -61
- 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, :
|
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
|
-
:
|
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.
|
data/lib/xero_gateway/account.rb
CHANGED
@@ -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
|
data/lib/xero_gateway/gateway.rb
CHANGED
@@ -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(:
|
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] =
|
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) }
|
data/lib/xero_gateway/http.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
|
121
|
-
raise InvoiceNotFoundError.new("Invoice not found in Xero.")
|
122
|
-
|
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
|
data/lib/xero_gateway/oauth.rb
CHANGED
@@ -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
|