xero_gateway 2.0.13 → 2.0.14

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -5,7 +5,8 @@ gem 'oauth', '>= 0.3.6'
5
5
  gem 'activesupport'
6
6
 
7
7
  group :test do
8
+ gem 'i18n' # For fixing undocumented active_support dependency
8
9
  gem 'mocha'
9
10
  gem 'shoulda'
10
11
  gem 'libxml-ruby'
11
- end
12
+ end
data/README.textile CHANGED
@@ -222,14 +222,17 @@ Invoice and line item totals are calculated automatically.
222
222
  })
223
223
  invoice.contact.name = "THE NAME OF THE CONTACT"
224
224
  invoice.contact.phone.number = "12345"
225
- invoice.contact.address.line_1 = "LINE 1 OF THE ADDRESS"
226
- invoice.add_line_item({
225
+ invoice.contact.address.line_1 = "LINE 1 OF THE ADDRESS"
226
+
227
+ line_item = XeroGateway::LineItem.new(
227
228
  :description => "THE DESCRIPTION OF THE LINE ITEM",
228
- :unit_amount => 1000,
229
- :tax_amount => 125,
230
- :tracking_category => "THE TRACKING CATEGORY FOR THE LINE ITEM",
231
- :tracking_option => "THE TRACKING OPTION FOR THE LINE ITEM"
232
- })
229
+ :account_code => 200,
230
+ :unit_amount => 1000
231
+ )
232
+
233
+ line_item.tracking << XeroGateway::TrackingCategory.new(:name => "tracking category", :options => "tracking option")
234
+
235
+ invoice.line_items << line_item
233
236
 
234
237
  invoice.create</code></pre>
235
238
 
data/Rakefile CHANGED
@@ -7,7 +7,7 @@ task :default => :test
7
7
 
8
8
  desc 'Test the xero gateway.'
9
9
  Rake::TestTask.new(:test) do |t|
10
- t.libs << 'lib'
10
+ t.libs << '.'
11
11
  t.pattern = 'test/**/*_test.rb'
12
12
  t.verbose = true
13
13
  end
data/lib/xero_gateway.rb CHANGED
@@ -15,6 +15,7 @@ require File.join(File.dirname(__FILE__), 'xero_gateway', 'http_encoding_helper'
15
15
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'http')
16
16
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'dates')
17
17
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'money')
18
+ require File.join(File.dirname(__FILE__), 'xero_gateway', 'line_item_calculations')
18
19
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'response')
19
20
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'account')
20
21
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'accounts_list')
@@ -23,6 +24,7 @@ require File.join(File.dirname(__FILE__), 'xero_gateway', 'contact')
23
24
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'line_item')
24
25
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'payment')
25
26
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'invoice')
27
+ require File.join(File.dirname(__FILE__), 'xero_gateway', 'bank_transaction')
26
28
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'credit_note')
27
29
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'address')
28
30
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'phone')
@@ -35,7 +35,7 @@ module XeroGateway
35
35
  'ZERORATED' => 'Zero-rated supplies/sales from overseas (NZ Only)'
36
36
  } unless defined?(TAX_TYPE)
37
37
 
38
- attr_accessor :account_id, :code, :name, :type, :tax_type, :description, :system_account, :enable_payments_to_account
38
+ attr_accessor :account_id, :code, :name, :type, :tax_type, :description, :system_account, :enable_payments_to_account, :currency_code
39
39
 
40
40
  def initialize(params = {})
41
41
  params.each do |k,v|
@@ -50,10 +50,8 @@ module XeroGateway
50
50
  return true
51
51
  end
52
52
 
53
- def to_xml
54
- b = Builder::XmlMarkup.new
55
-
56
- b.Account {
53
+ def to_xml(b = Builder::XmlMarkup.new, options={})
54
+ b.tag!(options[:name] ? options[:name] : 'Account') {
57
55
  b.AccountID self.account_id
58
56
  b.Code self.code
59
57
  b.Name self.name
@@ -62,6 +60,7 @@ module XeroGateway
62
60
  b.Description self.description
63
61
  b.SystemAccount self.system_account unless self.system_account.nil?
64
62
  b.EnablePaymentsToAccount self.enable_payments_to_account
63
+ b.CurrencyCode currency_code if currency_code
65
64
  }
66
65
  end
67
66
 
@@ -77,6 +76,7 @@ module XeroGateway
77
76
  when "Description" then account.description = element.text
78
77
  when "SystemAccount" then account.system_account = element.text
79
78
  when "EnablePaymentsToAccount" then account.enable_payments_to_account = (element.text == 'true')
79
+ when "CurrencyCode" then account.currency_code = element.text
80
80
  end
81
81
  end
82
82
  account
@@ -3,8 +3,7 @@ module XeroGateway
3
3
 
4
4
  ADDRESS_TYPE = {
5
5
  'STREET' => 'Street',
6
- 'POBOX' => 'PO Box',
7
- 'DEFAULT' => 'Default address type'
6
+ 'POBOX' => 'PO Box'
8
7
  } unless defined?(ADDRESS_TYPE)
9
8
 
10
9
  # Any errors that occurred when the #valid? method called.
@@ -16,7 +15,7 @@ module XeroGateway
16
15
  @errors ||= []
17
16
 
18
17
  params = {
19
- :address_type => "DEFAULT"
18
+ :address_type => "POBOX"
20
19
  }.merge(params)
21
20
 
22
21
  params.each do |k,v|
@@ -0,0 +1,175 @@
1
+ module XeroGateway
2
+ class BankTransaction
3
+ include Dates
4
+ include LineItemCalculations
5
+
6
+ class NoGatewayError < Error; end
7
+
8
+ 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)
9
+
10
+ TYPES = {
11
+ 'RECEIVE' => 'Receive Bank Transaction',
12
+ 'SPEND' => 'Spend Bank Transaction',
13
+ } unless defined?(TYPES)
14
+
15
+ STATUSES = {
16
+ 'ACTIVE' => 'Bank Transaction is active',
17
+ 'DELETED' => 'Bank Transaction is deleted',
18
+ } unless defined?(STATUSES)
19
+
20
+ # Xero::Gateway associated with this invoice.
21
+ attr_accessor :gateway
22
+
23
+ # Any errors that occurred when the #valid? method called.
24
+ attr_reader :errors
25
+
26
+ # Represents whether the line_items have been downloaded when getting from GET /API.XRO/2.0/BankTransactions
27
+ attr_accessor :line_items_downloaded
28
+
29
+ # accessible fields
30
+ attr_accessor :bank_transaction_id, :type, :date, :reference, :status, :contact, :line_items, :bank_account, :url
31
+
32
+ def initialize(params = {})
33
+ @errors ||= []
34
+ @payments ||= []
35
+
36
+ # Check if the line items have been downloaded.
37
+ @line_items_downloaded = (params.delete(:line_items_downloaded) == true)
38
+
39
+ # params = {
40
+ # :line_amount_types => "Exclusive"
41
+ # }.merge(params)
42
+ params.each do |k,v|
43
+ self.send("#{k}=", v)
44
+ end
45
+
46
+ @line_items ||= []
47
+ end
48
+
49
+ def ==(other)
50
+ ['type', 'reference', 'status', 'contact', 'line_items', 'bank_account'].each do |field|
51
+ return false if send(field) != other.send(field)
52
+ end
53
+
54
+ ["date"].each do |field|
55
+ return false if send(field).to_s != other.send(field).to_s
56
+ end
57
+ return true
58
+ end
59
+
60
+ # Validate the BankTransaction record according to what will be valid by the gateway.
61
+ #
62
+ # Usage:
63
+ # bank_transaction.valid? # Returns true/false
64
+ #
65
+ # Additionally sets bank_transaction.errors array to an array of field/error.
66
+ def valid?
67
+ @errors = []
68
+
69
+ if !bank_transaction_id.nil? && bank_transaction_id !~ GUID_REGEX
70
+ @errors << ['bank_transaction_id', 'must be blank or a valid Xero GUID']
71
+ end
72
+
73
+ if type && !TYPES[type]
74
+ @errors << ['type', "must be one of #{TYPES.keys.join('/')}"]
75
+ end
76
+
77
+ if status && !STATUSES[status]
78
+ @errors << ['status', "must be one of #{STATUSES.keys.join('/')}"]
79
+ end
80
+
81
+ unless date
82
+ @errors << ['date', "can't be blank"]
83
+ end
84
+
85
+ # Make sure contact is valid.
86
+ unless @contact && @contact.valid?
87
+ @errors << ['contact', 'is invalid']
88
+ end
89
+
90
+ # Make sure all line_items are valid.
91
+ unless line_items.all? { | line_item | line_item.valid? }
92
+ @errors << ['line_items', "at least one line item invalid"]
93
+ end
94
+
95
+ @errors.size == 0
96
+ end
97
+
98
+
99
+ def line_items_downloaded?
100
+ @line_items_downloaded
101
+ end
102
+
103
+ # If line items are not downloaded, then attempt a download now (if this record was found to begin with).
104
+ def line_items
105
+ if line_items_downloaded?
106
+ @line_items
107
+
108
+ elsif bank_transaction_id =~ GUID_REGEX && @gateway
109
+ # There is a bank_transaction_id so we can assume this record was loaded from Xero.
110
+ # Let's attempt to download the line_item records (if there is a gateway)
111
+
112
+ response = @gateway.get_bank_transaction(bank_transaction_id)
113
+ raise BankTransactionNotFoundError, "Bank Transaction with ID #{bank_transaction_id} not found in Xero." unless response.success? && response.bank_transaction.is_a?(XeroGateway::BankTransaction)
114
+
115
+ @line_items = response.bank_transaction.line_items
116
+ @line_items_downloaded = true
117
+
118
+ @line_items
119
+
120
+ # Otherwise, this is a new bank transaction, so return the line_items reference.
121
+ else
122
+ @line_items
123
+ end
124
+ end
125
+
126
+ def to_xml(b = Builder::XmlMarkup.new)
127
+ b.BankTransaction {
128
+ b.BankTransactionID bank_transaction_id if bank_transaction_id
129
+ b.Type type
130
+ # b.CurrencyCode self.currency_code if self.currency_code
131
+ contact.to_xml(b)
132
+ bank_account.to_xml(b, :name => 'BankAccount')
133
+ b.Date BankTransaction.format_date(date || Date.today)
134
+ b.Status status if status
135
+ b.Reference reference if reference
136
+ b.LineItems {
137
+ self.line_items.each do |line_item|
138
+ line_item.to_xml(b)
139
+ end
140
+ }
141
+ b.Url url if url
142
+ }
143
+ end
144
+
145
+ def self.from_xml(bank_transaction_element, gateway = nil, options = {})
146
+ bank_transaction = BankTransaction.new(options.merge({:gateway => gateway}))
147
+ bank_transaction_element.children.each do |element|
148
+ case(element.name)
149
+ when "BankTransactionID" then bank_transaction.bank_transaction_id = element.text
150
+ when "Type" then bank_transaction.type = element.text
151
+ # when "CurrencyCode" then invoice.currency_code = element.text
152
+ when "Contact" then bank_transaction.contact = Contact.from_xml(element)
153
+ when "BankAccount" then bank_transaction.bank_account = Account.from_xml(element)
154
+ when "Date" then bank_transaction.date = parse_date(element.text)
155
+ when "Status" then bank_transaction.status = element.text
156
+ when "Reference" then bank_transaction.reference = element.text
157
+ when "LineItems" then element.children.each {|line_item| bank_transaction.line_items_downloaded = true; bank_transaction.line_items << LineItem.from_xml(line_item) }
158
+ # when "SubTotal" then invoice.sub_total = BigDecimal.new(element.text)
159
+ # when "TotalTax" then invoice.total_tax = BigDecimal.new(element.text)
160
+ # when "Total" then invoice.total = BigDecimal.new(element.text)
161
+ # when "InvoiceID" then invoice.invoice_id = element.text
162
+ # when "InvoiceNumber" then invoice.invoice_number = element.text
163
+ # when "Payments" then element.children.each { | payment | invoice.payments << Payment.from_xml(payment) }
164
+ # when "AmountDue" then invoice.amount_due = BigDecimal.new(element.text)
165
+ # when "AmountPaid" then invoice.amount_paid = BigDecimal.new(element.text)
166
+ # when "AmountCredited" then invoice.amount_credited = BigDecimal.new(element.text)
167
+ # when "SentToContact" then invoice.sent_to_contact = (element.text.strip.downcase == "true")
168
+ when "Url" then bank_transaction.url = element.text
169
+ end
170
+ end
171
+ bank_transaction
172
+ end # from_xml
173
+
174
+ end
175
+ end
@@ -109,7 +109,7 @@ module XeroGateway
109
109
 
110
110
  # Make sure all phone numbers are correct.
111
111
  unless phones.all? { | phone | phone.valid? }
112
- @errors << ['phones', 'at leaset one phone is invalid']
112
+ @errors << ['phones', 'at least one phone is invalid']
113
113
  end
114
114
 
115
115
  @errors.size == 0
@@ -2,10 +2,9 @@ module XeroGateway
2
2
  class CreditNote
3
3
  include Dates
4
4
  include Money
5
+ include LineItemCalculations
5
6
 
6
- class Error < RuntimeError; end
7
7
  class NoGatewayError < Error; end
8
- class InvalidLineItemError < Error; end
9
8
 
10
9
  CREDIT_NOTE_TYPE = {
11
10
  'ACCRECCREDIT' => 'Accounts Receivable',
@@ -107,58 +106,6 @@ module XeroGateway
107
106
  @contact ||= build_contact
108
107
  end
109
108
 
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
109
  # Helper method to check if the credit_note is accounts payable.
163
110
  def accounts_payable?
164
111
  type == 'ACCPAYCREDIT'
@@ -41,6 +41,6 @@ module XeroGateway
41
41
  end
42
42
 
43
43
  class InvoiceNotFoundError < StandardError; end
44
-
44
+ class BankTransactionNotFoundError < StandardError; end
45
45
  class CreditNoteNotFoundError < StandardError; end
46
46
  end
@@ -349,6 +349,75 @@ module XeroGateway
349
349
  response
350
350
  end
351
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
+
352
421
  #
353
422
  # Gets all accounts for a specific organization in Xero.
354
423
  #
@@ -457,6 +526,35 @@ module XeroGateway
457
526
  response
458
527
  end
459
528
 
529
+ # Create or update a bank transaction record based on if it has an bank_transaction_id.
530
+ def save_bank_transaction(bank_transaction)
531
+ request_xml = bank_transaction.to_xml
532
+ response_xml = nil
533
+ create_or_save = nil
534
+
535
+ if bank_transaction.bank_transaction_id.nil?
536
+ # Create new bank transaction record.
537
+ response_xml = http_put(@client, "#{@xero_url}/BankTransactions", request_xml, {})
538
+ create_or_save = :create
539
+ else
540
+ # Update existing bank transaction record.
541
+ response_xml = http_post(@client, "#{@xero_url}/BankTransactions", 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'}/BankTransactions"})
546
+
547
+ # Xero returns bank transactions inside an <BankTransactions> tag, even though there's only ever
548
+ # one for this request
549
+ response.response_item = response.bank_transactions.first
550
+
551
+ if response.success? && response.bank_transaction && response.bank_transaction.bank_transaction_id
552
+ bank_transaction.bank_transaction_id = response.bank_transaction.bank_transaction_id
553
+ end
554
+
555
+ response
556
+ end
557
+
460
558
  def parse_response(raw_response, request = {}, options = {})
461
559
 
462
560
  response = XeroGateway::Response.new
@@ -476,8 +574,14 @@ module XeroGateway
476
574
  when "DateTimeUTC" then response.date_time = element.text
477
575
  when "Contact" then response.response_item = Contact.from_xml(element, self)
478
576
  when "Invoice" then response.response_item = Invoice.from_xml(element, self, {:line_items_downloaded => options[:request_signature] != "GET/Invoices"})
577
+ when "BankTransaction"
578
+ response.response_item = BankTransaction.from_xml(element, self, {:line_items_downloaded => options[:request_signature] != "GET/BankTransactions"})
479
579
  when "Contacts" then element.children.each {|child| response.response_item << Contact.from_xml(child, self) }
480
580
  when "Invoices" then element.children.each {|child| response.response_item << Invoice.from_xml(child, self, {:line_items_downloaded => options[:request_signature] != "GET/Invoices"}) }
581
+ when "BankTransactions"
582
+ element.children.each do |child|
583
+ response.response_item << BankTransaction.from_xml(child, self, {:line_items_downloaded => options[:request_signature] != "GET/BankTransactions"})
584
+ end
481
585
  when "CreditNotes" then element.children.each {|child| response.response_item << CreditNote.from_xml(child, self, {:line_items_downloaded => options[:request_signature] != "GET/CreditNotes"}) }
482
586
  when "Accounts" then element.children.each {|child| response.response_item << Account.from_xml(child) }
483
587
  when "TaxRates" then element.children.each {|child| response.response_item << TaxRate.from_xml(child) }