Support for Bank Transactions #31

Merged
merged 5 commits into from May 7, 2012
View
@@ -15,6 +15,7 @@
require File.join(File.dirname(__FILE__), 'xero_gateway', 'http')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'dates')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'money')
+require File.join(File.dirname(__FILE__), 'xero_gateway', 'line_item_calculations')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'response')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'account')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'accounts_list')
@@ -23,6 +24,7 @@
require File.join(File.dirname(__FILE__), 'xero_gateway', 'line_item')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'payment')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'invoice')
+require File.join(File.dirname(__FILE__), 'xero_gateway', 'bank_transaction')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'credit_note')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'address')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'phone')
@@ -50,10 +50,8 @@ def ==(other)
return true
end
- def to_xml
- b = Builder::XmlMarkup.new
-
- b.Account {
+ def to_xml(b = Builder::XmlMarkup.new, options={})
+ b.tag!(options[:name] ? options[:name] : 'Account') {
b.AccountID self.account_id
b.Code self.code
b.Name self.name
@@ -0,0 +1,175 @@
+module XeroGateway
+ class BankTransaction
+ include Dates
+ include LineItemCalculations
+
+ class NoGatewayError < Error; end
+
+ 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)
+
+ TYPES = {
+ 'RECEIVE' => 'Receive Bank Transaction',
+ 'SPEND' => 'Spend Bank Transaction',
+ } unless defined?(TYPES)
+
+ STATUSES = {
+ 'ACTIVE' => 'Bank Transaction is active',
+ 'DELETED' => 'Bank Transaction is deleted',
+ } unless defined?(STATUSES)
+
+ # Xero::Gateway associated with this invoice.
+ attr_accessor :gateway
+
+ # Any errors that occurred when the #valid? method called.
+ attr_reader :errors
+
+ # Represents whether the line_items have been downloaded when getting from GET /API.XRO/2.0/BankTransactions
+ attr_accessor :line_items_downloaded
+
+ # accessible fields
+ attr_accessor :bank_transaction_id, :type, :date, :reference, :status, :contact, :line_items, :bank_account, :url
+
+ def initialize(params = {})
+ @errors ||= []
+ @payments ||= []
+
+ # Check if the line items have been downloaded.
+ @line_items_downloaded = (params.delete(:line_items_downloaded) == true)
+
+ # params = {
+ # :line_amount_types => "Exclusive"
+ # }.merge(params)
+ params.each do |k,v|
+ self.send("#{k}=", v)
+ end
+
+ @line_items ||= []
+ end
+
+ def ==(other)
+ ['type', 'reference', 'status', 'contact', 'line_items', 'bank_account'].each do |field|
+ return false if send(field) != other.send(field)
+ end
+
+ ["date"].each do |field|
+ return false if send(field).to_s != other.send(field).to_s
+ end
+ return true
+ end
+
+ # Validate the BankTransaction record according to what will be valid by the gateway.
+ #
+ # Usage:
+ # bank_transaction.valid? # Returns true/false
+ #
+ # Additionally sets bank_transaction.errors array to an array of field/error.
+ def valid?
+ @errors = []
+
+ if !bank_transaction_id.nil? && bank_transaction_id !~ GUID_REGEX
+ @errors << ['bank_transaction_id', 'must be blank or a valid Xero GUID']
+ end
+
+ if type && !TYPES[type]
+ @errors << ['type', "must be one of #{TYPES.keys.join('/')}"]
+ end
+
+ if status && !STATUSES[status]
+ @errors << ['status', "must be one of #{STATUSES.keys.join('/')}"]
+ end
+
+ unless date
+ @errors << ['date', "can't be blank"]
+ end
+
+ # Make sure contact is valid.
+ unless @contact && @contact.valid?
+ @errors << ['contact', 'is invalid']
+ end
+
+ # Make sure all line_items are valid.
+ unless line_items.all? { | line_item | line_item.valid? }
+ @errors << ['line_items', "at least one line item invalid"]
+ end
+
+ @errors.size == 0
+ end
+
+
+ def line_items_downloaded?
+ @line_items_downloaded
+ end
+
+ # If line items are not downloaded, then attempt a download now (if this record was found to begin with).
+ def line_items
+ if line_items_downloaded?
+ @line_items
+
+ elsif bank_transaction_id =~ GUID_REGEX && @gateway
+ # There is a bank_transaction_id so we can assume this record was loaded from Xero.
+ # Let's attempt to download the line_item records (if there is a gateway)
+
+ response = @gateway.get_bank_transaction(bank_transaction_id)
+ raise BankTransactionNotFoundError, "Bank Transaction with ID #{bank_transaction_id} not found in Xero." unless response.success? && response.bank_transaction.is_a?(XeroGateway::BankTransaction)
+
+ @line_items = response.bank_transaction.line_items
+ @line_items_downloaded = true
+
+ @line_items
+
+ # Otherwise, this is a new bank transaction, so return the line_items reference.
+ else
+ @line_items
+ end
+ end
+
+ def to_xml(b = Builder::XmlMarkup.new)
+ b.BankTransaction {
+ b.BankTransactionID bank_transaction_id if bank_transaction_id
+ b.Type type
+ # b.CurrencyCode self.currency_code if self.currency_code
+ contact.to_xml(b)
+ bank_account.to_xml(b, :name => 'BankAccount')
+ b.Date BankTransaction.format_date(date || Date.today)
+ b.Status status if status
+ b.Reference reference if reference
+ b.LineItems {
+ self.line_items.each do |line_item|
+ line_item.to_xml(b)
+ end
+ }
+ b.Url url if url
+ }
+ end
+
+ def self.from_xml(bank_transaction_element, gateway = nil, options = {})
+ bank_transaction = BankTransaction.new(options.merge({:gateway => gateway}))
+ bank_transaction_element.children.each do |element|
+ case(element.name)
+ when "BankTransactionID" then bank_transaction.bank_transaction_id = element.text
+ when "Type" then bank_transaction.type = element.text
+ # when "CurrencyCode" then invoice.currency_code = element.text
+ when "Contact" then bank_transaction.contact = Contact.from_xml(element)
+ when "BankAccount" then bank_transaction.bank_account = Account.from_xml(element)
+ when "Date" then bank_transaction.date = parse_date(element.text)
+ when "Status" then bank_transaction.status = element.text
+ when "Reference" then bank_transaction.reference = element.text
+ when "LineItems" then element.children.each {|line_item| bank_transaction.line_items_downloaded = true; bank_transaction.line_items << LineItem.from_xml(line_item) }
+ # when "SubTotal" then invoice.sub_total = BigDecimal.new(element.text)
+ # when "TotalTax" then invoice.total_tax = BigDecimal.new(element.text)
+ # when "Total" then invoice.total = BigDecimal.new(element.text)
+ # when "InvoiceID" then invoice.invoice_id = element.text
+ # when "InvoiceNumber" then invoice.invoice_number = element.text
+ # when "Payments" then element.children.each { | payment | invoice.payments << Payment.from_xml(payment) }
+ # when "AmountDue" then invoice.amount_due = BigDecimal.new(element.text)
+ # when "AmountPaid" then invoice.amount_paid = BigDecimal.new(element.text)
+ # when "AmountCredited" then invoice.amount_credited = BigDecimal.new(element.text)
+ # when "SentToContact" then invoice.sent_to_contact = (element.text.strip.downcase == "true")
+ when "Url" then bank_transaction.url = element.text
+ end
+ end
+ bank_transaction
+ end # from_xml
+
+ end
+end
@@ -109,7 +109,7 @@ def valid?
# Make sure all phone numbers are correct.
unless phones.all? { | phone | phone.valid? }
- @errors << ['phones', 'at leaset one phone is invalid']
+ @errors << ['phones', 'at least one phone is invalid']
end
@errors.size == 0
@@ -2,10 +2,9 @@ module XeroGateway
class CreditNote
include Dates
include Money
+ include LineItemCalculations
- class Error < RuntimeError; end
class NoGatewayError < Error; end
- class InvalidLineItemError < Error; end
CREDIT_NOTE_TYPE = {
'ACCRECCREDIT' => 'Accounts Receivable',
@@ -107,58 +106,6 @@ def contact
@contact ||= build_contact
end
- # Helper method to create a new associated line_item.
- # Usage:
- # credit_note.add_line_item({:description => "Bob's Widgets", :quantity => 1, :unit_amount => 120})
- def add_line_item(params = {})
- line_item = nil
- case params
- when Hash then line_item = LineItem.new(params)
- when LineItem then line_item = params
- else raise InvalidLineItemError
- end
-
- @line_items << line_item
-
- line_item
- end
-
- # Deprecated (but API for setter remains).
- #
- # As sub_total must equal SUM(line_item.line_amount) for the API call to pass, this is now
- # automatically calculated in the sub_total method.
- def sub_total=(value)
- end
-
- # Calculate the sub_total as the SUM(line_item.line_amount).
- def sub_total
- line_items.inject(BigDecimal.new('0')) { | sum, line_item | sum + BigDecimal.new(line_item.line_amount.to_s) }
- end
-
- # Deprecated (but API for setter remains).
- #
- # As total_tax must equal SUM(line_item.tax_amount) for the API call to pass, this is now
- # automatically calculated in the total_tax method.
- def total_tax=(value)
- end
-
- # Calculate the total_tax as the SUM(line_item.tax_amount).
- def total_tax
- line_items.inject(BigDecimal.new('0')) { | sum, line_item | sum + BigDecimal.new(line_item.tax_amount.to_s) }
- end
-
- # Deprecated (but API for setter remains).
- #
- # As total must equal sub_total + total_tax for the API call to pass, this is now
- # automatically calculated in the total method.
- def total=(value)
- end
-
- # Calculate the toal as sub_total + total_tax.
- def total
- sub_total + total_tax
- end
-
# Helper method to check if the credit_note is accounts payable.
def accounts_payable?
type == 'ACCPAYCREDIT'
@@ -41,6 +41,6 @@ def message
end
class InvoiceNotFoundError < StandardError; end
-
+ class BankTransactionNotFoundError < StandardError; end
class CreditNoteNotFoundError < StandardError; end
end
Oops, something went wrong.