xero_gateway 2.0.13 → 2.0.14

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.
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) }