xero_gateway-n8vision 2.0.20

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.
Files changed (78) hide show
  1. data/Gemfile +12 -0
  2. data/LICENSE +14 -0
  3. data/README.textile +357 -0
  4. data/Rakefile +14 -0
  5. data/examples/oauth.rb +25 -0
  6. data/examples/partner_app.rb +36 -0
  7. data/init.rb +1 -0
  8. data/lib/oauth/oauth_consumer.rb +14 -0
  9. data/lib/xero_gateway.rb +41 -0
  10. data/lib/xero_gateway/account.rb +86 -0
  11. data/lib/xero_gateway/accounts_list.rb +73 -0
  12. data/lib/xero_gateway/address.rb +96 -0
  13. data/lib/xero_gateway/bank_transaction.rb +175 -0
  14. data/lib/xero_gateway/ca-certificates.crt +2560 -0
  15. data/lib/xero_gateway/contact.rb +203 -0
  16. data/lib/xero_gateway/credit_note.rb +220 -0
  17. data/lib/xero_gateway/currency.rb +56 -0
  18. data/lib/xero_gateway/dates.rb +25 -0
  19. data/lib/xero_gateway/error.rb +18 -0
  20. data/lib/xero_gateway/exceptions.rb +51 -0
  21. data/lib/xero_gateway/gateway.rb +698 -0
  22. data/lib/xero_gateway/http.rb +135 -0
  23. data/lib/xero_gateway/http_encoding_helper.rb +49 -0
  24. data/lib/xero_gateway/invoice.rb +238 -0
  25. data/lib/xero_gateway/journal_line.rb +102 -0
  26. data/lib/xero_gateway/line_item.rb +125 -0
  27. data/lib/xero_gateway/line_item_calculations.rb +51 -0
  28. data/lib/xero_gateway/manual_journal.rb +163 -0
  29. data/lib/xero_gateway/money.rb +16 -0
  30. data/lib/xero_gateway/oauth.rb +92 -0
  31. data/lib/xero_gateway/organisation.rb +75 -0
  32. data/lib/xero_gateway/partner_app.rb +30 -0
  33. data/lib/xero_gateway/payment.rb +43 -0
  34. data/lib/xero_gateway/phone.rb +77 -0
  35. data/lib/xero_gateway/private_app.rb +17 -0
  36. data/lib/xero_gateway/response.rb +43 -0
  37. data/lib/xero_gateway/tax_rate.rb +63 -0
  38. data/lib/xero_gateway/tracking_category.rb +87 -0
  39. data/test/integration/accounts_list_test.rb +109 -0
  40. data/test/integration/create_bank_transaction_test.rb +38 -0
  41. data/test/integration/create_contact_test.rb +66 -0
  42. data/test/integration/create_credit_note_test.rb +49 -0
  43. data/test/integration/create_invoice_test.rb +49 -0
  44. data/test/integration/create_manual_journal_test.rb +35 -0
  45. data/test/integration/get_accounts_test.rb +23 -0
  46. data/test/integration/get_bank_transaction_test.rb +51 -0
  47. data/test/integration/get_bank_transactions_test.rb +88 -0
  48. data/test/integration/get_contact_test.rb +28 -0
  49. data/test/integration/get_contacts_test.rb +40 -0
  50. data/test/integration/get_credit_note_test.rb +48 -0
  51. data/test/integration/get_credit_notes_test.rb +90 -0
  52. data/test/integration/get_currencies_test.rb +25 -0
  53. data/test/integration/get_invoice_test.rb +48 -0
  54. data/test/integration/get_invoices_test.rb +92 -0
  55. data/test/integration/get_manual_journal_test.rb +50 -0
  56. data/test/integration/get_manual_journals_test.rb +88 -0
  57. data/test/integration/get_organisation_test.rb +24 -0
  58. data/test/integration/get_tax_rates_test.rb +25 -0
  59. data/test/integration/get_tracking_categories_test.rb +27 -0
  60. data/test/integration/update_bank_transaction_test.rb +31 -0
  61. data/test/integration/update_contact_test.rb +31 -0
  62. data/test/integration/update_invoice_test.rb +31 -0
  63. data/test/integration/update_manual_journal_test.rb +31 -0
  64. data/test/test_helper.rb +217 -0
  65. data/test/unit/account_test.rb +47 -0
  66. data/test/unit/bank_transaction_test.rb +126 -0
  67. data/test/unit/contact_test.rb +97 -0
  68. data/test/unit/credit_note_test.rb +284 -0
  69. data/test/unit/currency_test.rb +31 -0
  70. data/test/unit/gateway_test.rb +119 -0
  71. data/test/unit/invoice_test.rb +326 -0
  72. data/test/unit/manual_journal_test.rb +93 -0
  73. data/test/unit/oauth_test.rb +116 -0
  74. data/test/unit/organisation_test.rb +38 -0
  75. data/test/unit/tax_rate_test.rb +38 -0
  76. data/test/unit/tracking_category_test.rb +52 -0
  77. data/xero_gateway-n8vision.gemspec +15 -0
  78. metadata +178 -0
@@ -0,0 +1,135 @@
1
+ module XeroGateway
2
+ module Http
3
+ OPEN_TIMEOUT = 10 unless defined? OPEN_TIMEOUT
4
+ READ_TIMEOUT = 60 unless defined? READ_TIMEOUT
5
+ ROOT_CA_FILE = File.join(File.dirname(__FILE__), 'ca-certificates.crt') unless defined? ROOT_CA_FILE
6
+
7
+ def http_get(client, url, extra_params = {})
8
+ http_request(client, :get, url, nil, extra_params)
9
+ end
10
+
11
+ def http_post(client, url, body, extra_params = {})
12
+ http_request(client, :post, url, body, extra_params)
13
+ end
14
+
15
+ def http_put(client, url, body, extra_params = {})
16
+ http_request(client, :put, url, body, extra_params)
17
+ end
18
+
19
+ private
20
+
21
+ def http_request(client, method, url, body, params = {})
22
+ # headers = {'Accept-Encoding' => 'gzip, deflate'}
23
+
24
+ headers = { 'charset' => 'utf-8' }
25
+
26
+ if method != :get
27
+ headers['Content-Type'] ||= "application/x-www-form-urlencoded"
28
+ end
29
+
30
+ # HAX. Xero completely misuse the If-Modified-Since HTTP header.
31
+ headers['If-Modified-Since'] = params.delete(:ModifiedAfter).utc.strftime("%Y-%m-%dT%H:%M:%S") if params[:ModifiedAfter]
32
+
33
+ if params.any?
34
+ url += "?" + params.map {|key,value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"}.join("&")
35
+ end
36
+
37
+ uri = URI.parse(url)
38
+
39
+ # # Only setup @cached_http once on first use as loading the CA file is quite expensive computationally.
40
+ # unless @cached_http && @cached_http.address == uri.host && @cached_http.port == uri.port
41
+ # @cached_http = Net::HTTP.new(uri.host, uri.port)
42
+ # @cached_http.open_timeout = OPEN_TIMEOUT
43
+ # @cached_http.read_timeout = READ_TIMEOUT
44
+ # @cached_http.use_ssl = true
45
+ #
46
+ # # Need to validate server's certificate against root certificate authority to prevent man-in-the-middle attacks.
47
+ # @cached_http.ca_file = ROOT_CA_FILE
48
+ # # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
49
+ # @cached_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
50
+ # @cached_http.verify_depth = 5
51
+ # end
52
+
53
+ logger.info("\n== [#{Time.now.to_s}] XeroGateway Request: #{uri.request_uri} ") if self.logger
54
+
55
+ response = case method
56
+ when :get then client.get(uri.request_uri, headers)
57
+ when :post then client.post(uri.request_uri, { :xml => body }, headers)
58
+ when :put then client.put(uri.request_uri, { :xml => body }, headers)
59
+ end
60
+
61
+ if self.logger
62
+ logger.info("== [#{Time.now.to_s}] XeroGateway Response (#{response.code})")
63
+
64
+ unless response.code.to_i == 200
65
+ logger.info("== #{uri.request_uri} Response Body \n\n #{response.plain_body} \n == End Response Body")
66
+ end
67
+ end
68
+
69
+ case response.code.to_i
70
+ when 200
71
+ response.plain_body
72
+ when 400
73
+ handle_error!(body, response)
74
+ when 401
75
+ handle_oauth_error!(response)
76
+ when 404
77
+ handle_object_not_found!(response, url)
78
+ else
79
+ raise "Unknown response code: #{response.code.to_i}"
80
+ end
81
+ end
82
+
83
+ def handle_oauth_error!(response)
84
+ error_details = CGI.parse(response.plain_body)
85
+ description = error_details["oauth_problem_advice"].first
86
+
87
+ # see http://oauth.pbworks.com/ProblemReporting
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.
91
+ case (error_details["oauth_problem"].first)
92
+ when "token_expired" then raise OAuth::TokenExpired.new(description)
93
+ when "consumer_key_unknown" then raise OAuth::TokenInvalid.new(description)
94
+ when "token_rejected" then raise OAuth::TokenInvalid.new(description)
95
+ when "rate limit exceeded" then raise OAuth::RateLimitExceeded.new(description)
96
+ else raise OAuth::UnknownError.new(error_details["oauth_problem"].first + ':' + description)
97
+ end
98
+ end
99
+
100
+ def handle_error!(request_xml, response)
101
+
102
+ raw_response = response.plain_body
103
+
104
+ # Xero Gateway API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
105
+ # So let's ignore that :)
106
+ raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
107
+
108
+ doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
109
+
110
+ if doc.root.name == "ApiException"
111
+
112
+ raise ApiException.new(doc.root.elements["Type"].text,
113
+ doc.root.elements["Message"].text,
114
+ request_xml,
115
+ raw_response)
116
+
117
+ else
118
+
119
+ raise "Unparseable 400 Response: #{raw_response}"
120
+
121
+ end
122
+
123
+ end
124
+
125
+ def handle_object_not_found!(response, request_url)
126
+ case(request_url)
127
+ when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.")
128
+ when /BankTransactions/ then raise BankTransactionNotFoundError.new("Bank Transaction not found in Xero.")
129
+ when /CreditNotes/ then raise CreditNoteNotFoundError.new("Credit Note not found in Xero.")
130
+ else raise ObjectNotFound.new(request_url)
131
+ end
132
+ end
133
+
134
+ end
135
+ end
@@ -0,0 +1,49 @@
1
+ # Intended to extend the Net::HTTP response object
2
+ # and adds support for decoding gzip and deflate encoded pages
3
+ #
4
+ # Author: Jason Stirk <http://griffin.oobleyboo.com>
5
+ # Home: http://griffin.oobleyboo.com/projects/http_encoding_helper
6
+ # Created: 5 September 2007
7
+ # Last Updated: 23 November 2007
8
+ #
9
+ # Usage:
10
+ #
11
+ # require 'net/http'
12
+ # require 'http_encoding_helper'
13
+ # headers={'Accept-Encoding' => 'gzip, deflate' }
14
+ # http = Net::HTTP.new('griffin.oobleyboo.com', 80)
15
+ # http.start do |h|
16
+ # request = Net::HTTP::Get.new('/', headers)
17
+ # response = http.request(request)
18
+ # content=response.plain_body # Method from our library
19
+ # puts "Transferred: #{response.body.length} bytes"
20
+ # puts "Compression: #{response['content-encoding']}"
21
+ # puts "Extracted: #{response.plain_body.length} bytes"
22
+ # end
23
+ #
24
+
25
+ require 'zlib'
26
+ require 'stringio'
27
+
28
+ class Net::HTTPResponse
29
+ # Return the uncompressed content
30
+ def plain_body
31
+ encoding=self['content-encoding']
32
+ content=nil
33
+ if encoding then
34
+ case encoding
35
+ when 'gzip'
36
+ i=Zlib::GzipReader.new(StringIO.new(self.body))
37
+ content=i.read
38
+ when 'deflate'
39
+ i=Zlib::Inflate.new
40
+ content=i.inflate(self.body)
41
+ else
42
+ raise "Unknown encoding - #{encoding}"
43
+ end
44
+ else
45
+ content=self.body
46
+ end
47
+ return content
48
+ end
49
+ end
@@ -0,0 +1,238 @@
1
+ module XeroGateway
2
+ class Invoice
3
+ include Dates
4
+ include Money
5
+ include LineItemCalculations
6
+
7
+ INVOICE_TYPE = {
8
+ 'ACCREC' => 'Accounts Receivable',
9
+ 'ACCPAY' => 'Accounts Payable'
10
+ } unless defined?(INVOICE_TYPE)
11
+
12
+ LINE_AMOUNT_TYPES = {
13
+ "Inclusive" => 'Invoice lines are inclusive tax',
14
+ "Exclusive" => 'Invoice lines are exclusive of tax (default)',
15
+ "NoTax" => 'Invoices lines have no tax'
16
+ } unless defined?(LINE_AMOUNT_TYPES)
17
+
18
+ INVOICE_STATUS = {
19
+ 'AUTHORISED' => 'Approved invoices awaiting payment',
20
+ 'DELETED' => 'Draft invoices that are deleted',
21
+ 'DRAFT' => 'Invoices saved as draft or entered via API',
22
+ 'PAID' => 'Invoices approved and fully paid',
23
+ 'SUBMITTED' => 'Invoices entered by an employee awaiting approval',
24
+ 'VOID' => 'Approved invoices that are voided'
25
+ } unless defined?(INVOICE_STATUS)
26
+
27
+ 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)
28
+
29
+ # Xero::Gateway associated with this invoice.
30
+ attr_accessor :gateway
31
+
32
+ # Any errors that occurred when the #valid? method called.
33
+ attr_reader :errors
34
+
35
+ # Represents whether the line_items have been downloaded when getting from GET /API.XRO/2.0/INVOICES
36
+ attr_accessor :line_items_downloaded
37
+
38
+ # All accessible fields
39
+ #NICKEDIT - added BrandingThemeID
40
+
41
+ attr_accessor :branding_theme_id, :invoice_id, :invoice_number, :invoice_type, :invoice_status, :date, :due_date, :reference, :line_amount_types, :currency_code, :line_items, :contact, :payments, :fully_paid_on, :amount_due, :amount_paid, :amount_credited, :sent_to_contact, :url
42
+
43
+
44
+ def initialize(params = {})
45
+ @errors ||= []
46
+ @payments ||= []
47
+
48
+ # Check if the line items have been downloaded.
49
+ @line_items_downloaded = (params.delete(:line_items_downloaded) == true)
50
+
51
+ params = {
52
+ :line_amount_types => "Exclusive"
53
+ }.merge(params)
54
+
55
+ params.each do |k,v|
56
+ self.send("#{k}=", v)
57
+ end
58
+
59
+ @line_items ||= []
60
+ end
61
+
62
+ # Validate the Address record according to what will be valid by the gateway.
63
+ #
64
+ # Usage:
65
+ # address.valid? # Returns true/false
66
+ #
67
+ # Additionally sets address.errors array to an array of field/error.
68
+ def valid?
69
+ @errors = []
70
+
71
+ if !invoice_id.nil? && invoice_id !~ GUID_REGEX
72
+ @errors << ['invoice_id', 'must be blank or a valid Xero GUID']
73
+ end
74
+
75
+ if invoice_status && !INVOICE_STATUS[invoice_status]
76
+ @errors << ['invoice_status', "must be one of #{INVOICE_STATUS.keys.join('/')}"]
77
+ end
78
+
79
+ if line_amount_types && !LINE_AMOUNT_TYPES[line_amount_types]
80
+ @errors << ['line_amount_types', "must be one of #{LINE_AMOUNT_TYPES.keys.join('/')}"]
81
+ end
82
+
83
+ unless date
84
+ @errors << ['invoice_date', "can't be blank"]
85
+ end
86
+
87
+ # Make sure contact is valid.
88
+ unless @contact && @contact.valid?
89
+ @errors << ['contact', 'is invalid']
90
+ end
91
+
92
+ # Make sure all line_items are valid.
93
+ unless line_items.all? { | line_item | line_item.valid? }
94
+ @errors << ['line_items', "at least one line item invalid"]
95
+ end
96
+
97
+ @errors.size == 0
98
+ end
99
+
100
+ # Helper method to create the associated contact object.
101
+ def build_contact(params = {})
102
+ self.contact = gateway ? gateway.build_contact(params) : Contact.new(params)
103
+ end
104
+
105
+ def contact
106
+ @contact ||= build_contact
107
+ end
108
+
109
+ # Helper method to check if the invoice is accounts payable.
110
+ def accounts_payable?
111
+ invoice_type == 'ACCPAY'
112
+ end
113
+
114
+ # Helper method to check if the invoice is accounts receivable.
115
+ def accounts_receivable?
116
+ invoice_type == 'ACCREC'
117
+ end
118
+
119
+ # Whether or not the line_items have been downloaded (GET/invoices does not download line items).
120
+ def line_items_downloaded?
121
+ @line_items_downloaded
122
+ end
123
+
124
+ # If line items are not downloaded, then attempt a download now (if this record was found to begin with).
125
+ def line_items
126
+ if line_items_downloaded?
127
+ @line_items
128
+
129
+ elsif invoice_id =~ GUID_REGEX && @gateway
130
+ # There is an invoice_id so we can assume this record was loaded from Xero.
131
+ # Let's attempt to download the line_item records (if there is a gateway)
132
+ response = @gateway.get_invoice(invoice_id)
133
+ raise InvoiceNotFoundError, "Invoice with ID #{invoice_id} not found in Xero." unless response.success? && response.invoice.is_a?(XeroGateway::Invoice)
134
+
135
+ @line_items = response.invoice.line_items
136
+ @line_items_downloaded = true
137
+
138
+ @line_items
139
+
140
+ # Otherwise, this is a new invoice, so return the line_items reference.
141
+ else
142
+ @line_items
143
+ end
144
+ end
145
+
146
+ def ==(other)
147
+ ["invoice_number", "invoice_type", "invoice_status", "reference", "currency_code", "line_amount_types", "contact", "line_items"].each do |field|
148
+ return false if send(field) != other.send(field)
149
+ end
150
+
151
+ ["date", "due_date"].each do |field|
152
+ return false if send(field).to_s != other.send(field).to_s
153
+ end
154
+ return true
155
+ end
156
+
157
+ # General purpose create/save method.
158
+ # If invoice_id is nil then create, otherwise, attempt to save.
159
+ def save
160
+ if invoice_id.nil?
161
+ create
162
+ else
163
+ update
164
+ end
165
+ end
166
+
167
+ # Creates this invoice record (using gateway.create_invoice) with the associated gateway.
168
+ # If no gateway set, raise a NoGatewayError exception.
169
+ def create
170
+ raise NoGatewayError unless gateway
171
+ gateway.create_invoice(self)
172
+ end
173
+
174
+ # Updates this invoice record (using gateway.update_invoice) with the associated gateway.
175
+ # If no gateway set, raise a NoGatewayError exception.
176
+ def update
177
+ raise NoGatewayError unless gateway
178
+ gateway.update_invoice(self)
179
+ end
180
+
181
+ def to_xml(b = Builder::XmlMarkup.new)
182
+ b.Invoice {
183
+ b.InvoiceID self.invoice_id if self.invoice_id
184
+ b.InvoiceNumber self.invoice_number if invoice_number
185
+ b.Type self.invoice_type
186
+ b.CurrencyCode self.currency_code if self.currency_code
187
+ contact.to_xml(b)
188
+ b.Date Invoice.format_date(self.date || Date.today)
189
+ b.DueDate Invoice.format_date(self.due_date) if self.due_date
190
+ b.Status self.invoice_status if self.invoice_status
191
+ b.Reference self.reference if self.reference
192
+ #NICKEDIT - adding BrandingThemeID
193
+ b.BrandingThemeID self.branding_theme_id if self.branding_theme_id
194
+ b.LineAmountTypes self.line_amount_types
195
+ b.LineItems {
196
+ self.line_items.each do |line_item|
197
+ line_item.to_xml(b)
198
+ end
199
+ }
200
+ b.Url url if url
201
+ }
202
+ end
203
+
204
+ #TODO UpdatedDateUTC
205
+ def self.from_xml(invoice_element, gateway = nil, options = {})
206
+ invoice = Invoice.new(options.merge({:gateway => gateway}))
207
+ invoice_element.children.each do |element|
208
+ case(element.name)
209
+ when "InvoiceID" then invoice.invoice_id = element.text
210
+ when "InvoiceNumber" then invoice.invoice_number = element.text
211
+ when "Type" then invoice.invoice_type = element.text
212
+ when "CurrencyCode" then invoice.currency_code = element.text
213
+ when "Contact" then invoice.contact = Contact.from_xml(element)
214
+ when "Date" then invoice.date = parse_date(element.text)
215
+ when "DueDate" then invoice.due_date = parse_date(element.text)
216
+ when "Status" then invoice.invoice_status = element.text
217
+ when "Reference" then invoice.reference = element.text
218
+ #NICKEDIT - adding BrandingThemeID
219
+ when "BrandingThemeID" then invoice.branding_theme_id = element.text
220
+ when "LineAmountTypes" then invoice.line_amount_types = element.text
221
+ when "LineItems" then element.children.each {|line_item| invoice.line_items_downloaded = true; invoice.line_items << LineItem.from_xml(line_item) }
222
+ when "SubTotal" then invoice.sub_total = BigDecimal.new(element.text)
223
+ when "TotalTax" then invoice.total_tax = BigDecimal.new(element.text)
224
+ when "Total" then invoice.total = BigDecimal.new(element.text)
225
+ when "InvoiceID" then invoice.invoice_id = element.text
226
+ when "InvoiceNumber" then invoice.invoice_number = element.text
227
+ when "Payments" then element.children.each { | payment | invoice.payments << Payment.from_xml(payment) }
228
+ when "AmountDue" then invoice.amount_due = BigDecimal.new(element.text)
229
+ when "AmountPaid" then invoice.amount_paid = BigDecimal.new(element.text)
230
+ when "AmountCredited" then invoice.amount_credited = BigDecimal.new(element.text)
231
+ when "SentToContact" then invoice.sent_to_contact = (element.text.strip.downcase == "true")
232
+ when "Url" then invoice.url = element.text
233
+ end
234
+ end
235
+ invoice
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,102 @@
1
+ require File.join(File.dirname(__FILE__), 'account')
2
+
3
+ module XeroGateway
4
+ class JournalLine
5
+ include Money
6
+
7
+ TAX_TYPE = Account::TAX_TYPE unless defined?(TAX_TYPE)
8
+
9
+ # Any errors that occurred when the #valid? method called.
10
+ attr_reader :errors
11
+
12
+ # All accessible fields
13
+ attr_accessor :journal_line_id, :line_amount, :account_code, :description, :tax_type, :tracking
14
+
15
+ def initialize(params = {})
16
+ @errors ||= []
17
+ @tracking ||= []
18
+
19
+ params.each do |k,v|
20
+ self.send("#{k}=", v)
21
+ end
22
+ end
23
+
24
+ # Validate the JournalLineItem record according to what will be valid by the gateway.
25
+ #
26
+ # Usage:
27
+ # journal_line_item.valid? # Returns true/false
28
+ #
29
+ # Additionally sets journal_line_item.errors array to an array of field/error.
30
+ def valid?
31
+ @errors = []
32
+
33
+ if !journal_line_id.nil? && journal_line_id !~ GUID_REGEX
34
+ @errors << ['journal_line_id', 'must be blank or a valid Xero GUID']
35
+ end
36
+
37
+ unless line_amount
38
+ @errors << ['line_amount', "can't be blank"]
39
+ end
40
+
41
+ unless account_code
42
+ @errors << ['account_code', "can't be blank"]
43
+ end
44
+
45
+ @errors.size == 0
46
+ end
47
+
48
+ def has_tracking?
49
+ return false if tracking.nil?
50
+
51
+ if tracking.is_a?(Array)
52
+ return tracking.any?
53
+ else
54
+ return tracking.is_a?(TrackingCategory)
55
+ end
56
+ end
57
+
58
+ def to_xml(b = Builder::XmlMarkup.new)
59
+ b.JournalLine {
60
+ b.LineAmount line_amount # mandatory
61
+ b.AccountCode account_code # mandatory
62
+ b.Description description if description # optional
63
+ b.TaxType tax_type if tax_type # optional
64
+ if has_tracking?
65
+ b.Tracking { # optional
66
+ # Due to strange retardness in the Xero API, the XML structure for a tracking category within
67
+ # an invoice is different to a standalone tracking category.
68
+ # This means rather than going category.to_xml we need to call the special category.to_xml_for_invoice_messages
69
+ (tracking.is_a?(TrackingCategory) ? [tracking] : tracking).each do |category|
70
+ category.to_xml_for_invoice_messages(b)
71
+ end
72
+ }
73
+ end
74
+ }
75
+ end
76
+
77
+ def self.from_xml(journal_line_element)
78
+ journal_line = JournalLine.new
79
+ journal_line_element.children.each do |element|
80
+ case(element.name)
81
+ when "LineAmount" then journal_line.line_amount = BigDecimal.new(element.text)
82
+ when "AccountCode" then journal_line.account_code = element.text
83
+ when "JournalLineID" then journal_line.journal_line_id = element.text
84
+ when "Description" then journal_line.description = element.text
85
+ when "TaxType" then journal_line.tax_type = element.text
86
+ when "Tracking" then
87
+ element.children.each do | tracking_element |
88
+ journal_line.tracking << TrackingCategory.from_xml(tracking_element)
89
+ end
90
+ end
91
+ end
92
+ journal_line
93
+ end
94
+
95
+ def ==(other)
96
+ [:description, :line_amount, :account_code, :tax_type].each do |field|
97
+ return false if send(field) != other.send(field)
98
+ end
99
+ return true
100
+ end
101
+ end
102
+ end