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,125 @@
1
+ require File.join(File.dirname(__FILE__), 'account')
2
+
3
+ module XeroGateway
4
+ class LineItem
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 :line_item_id, :description, :quantity, :unit_amount, :item_code, :tax_type, :tax_amount, :account_code, :tracking
14
+
15
+ def initialize(params = {})
16
+ @errors ||= []
17
+ @tracking ||= []
18
+ @quantity = 1
19
+ @unit_amount = BigDecimal.new('0')
20
+
21
+ params.each do |k,v|
22
+ self.send("#{k}=", v)
23
+ end
24
+ end
25
+
26
+ # Validate the LineItem record according to what will be valid by the gateway.
27
+ #
28
+ # Usage:
29
+ # line_item.valid? # Returns true/false
30
+ #
31
+ # Additionally sets line_item.errors array to an array of field/error.
32
+ def valid?
33
+ @errors = []
34
+
35
+ if !line_item_id.nil? && line_item_id !~ GUID_REGEX
36
+ @errors << ['line_item_id', 'must be blank or a valid Xero GUID']
37
+ end
38
+
39
+ unless description
40
+ @errors << ['description', "can't be blank"]
41
+ end
42
+
43
+ if tax_type && !TAX_TYPE[tax_type]
44
+ @errors << ['tax_type', "must be one of #{TAX_TYPE.keys.join('/')}"]
45
+ end
46
+
47
+ @errors.size == 0
48
+ end
49
+
50
+ def has_tracking?
51
+ return false if tracking.nil?
52
+
53
+ if tracking.is_a?(Array)
54
+ return tracking.any?
55
+ else
56
+ return tracking.is_a?(TrackingCategory)
57
+ end
58
+ end
59
+
60
+ # Deprecated (but API for setter remains).
61
+ #
62
+ # As line_amount must equal quantity * unit_amount for the API call to pass, this is now
63
+ # automatically calculated in the line_amount method.
64
+ def line_amount=(value)
65
+ end
66
+
67
+ # Calculate the line_amount as quantity * unit_amount as this value must be correct
68
+ # for the API call to succeed.
69
+ def line_amount
70
+ quantity * unit_amount
71
+ end
72
+
73
+ def to_xml(b = Builder::XmlMarkup.new)
74
+ b.LineItem {
75
+ b.Description description
76
+ b.Quantity quantity if quantity
77
+ b.UnitAmount LineItem.format_money(unit_amount)
78
+ b.ItemCode item_code if item_code
79
+ b.TaxType tax_type if tax_type
80
+ b.TaxAmount tax_amount if tax_amount
81
+ b.LineAmount line_amount if line_amount
82
+ b.AccountCode account_code if account_code
83
+ if has_tracking?
84
+ b.Tracking {
85
+ # Due to strange retardness in the Xero API, the XML structure for a tracking category within
86
+ # an invoice is different to a standalone tracking category.
87
+ # This means rather than going category.to_xml we need to call the special category.to_xml_for_invoice_messages
88
+ (tracking.is_a?(TrackingCategory) ? [tracking] : tracking).each do |category|
89
+ category.to_xml_for_invoice_messages(b)
90
+ end
91
+ }
92
+ end
93
+ }
94
+ end
95
+
96
+ def self.from_xml(line_item_element)
97
+ line_item = LineItem.new
98
+ line_item_element.children.each do |element|
99
+ case(element.name)
100
+ when "LineItemID" then line_item.line_item_id = element.text
101
+ when "Description" then line_item.description = element.text
102
+ when "Quantity" then line_item.quantity = BigDecimal(element.text)
103
+ when "UnitAmount" then line_item.unit_amount = BigDecimal.new(element.text)
104
+ when "ItemCode" then line_item.item_code = element.text
105
+ when "TaxType" then line_item.tax_type = element.text
106
+ when "TaxAmount" then line_item.tax_amount = BigDecimal.new(element.text)
107
+ when "LineAmount" then line_item.line_amount = BigDecimal.new(element.text)
108
+ when "AccountCode" then line_item.account_code = element.text
109
+ when "Tracking" then
110
+ element.children.each do | tracking_element |
111
+ line_item.tracking << TrackingCategory.from_xml(tracking_element)
112
+ end
113
+ end
114
+ end
115
+ line_item
116
+ end
117
+
118
+ def ==(other)
119
+ [:description, :quantity, :unit_amount, :tax_type, :tax_amount, :line_amount, :account_code, :item_code].each do |field|
120
+ return false if send(field) != other.send(field)
121
+ end
122
+ return true
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,51 @@
1
+ module XeroGateway
2
+ module LineItemCalculations
3
+ def add_line_item(params = {})
4
+ line_item = nil
5
+ case params
6
+ when Hash then line_item = LineItem.new(params)
7
+ when LineItem then line_item = params
8
+ else raise InvalidLineItemError
9
+ end
10
+ @line_items << line_item
11
+ line_item
12
+ end
13
+
14
+ # Deprecated (but API for setter remains).
15
+ #
16
+ # As sub_total must equal SUM(line_item.line_amount) for the API call to pass, this is now
17
+ # automatically calculated in the sub_total method.
18
+ def sub_total=(value)
19
+ end
20
+
21
+ # Calculate the sub_total as the SUM(line_item.line_amount).
22
+ def sub_total
23
+ line_items.inject(BigDecimal.new('0')) { | sum, line_item | sum + BigDecimal.new(line_item.line_amount.to_s) }
24
+ end
25
+
26
+ # Deprecated (but API for setter remains).
27
+ #
28
+ # As total_tax must equal SUM(line_item.tax_amount) for the API call to pass, this is now
29
+ # automatically calculated in the total_tax method.
30
+ def total_tax=(value)
31
+ end
32
+
33
+ # Calculate the total_tax as the SUM(line_item.tax_amount).
34
+ def total_tax
35
+ line_items.inject(BigDecimal.new('0')) { | sum, line_item | sum + BigDecimal.new(line_item.tax_amount.to_s) }
36
+ end
37
+
38
+ # Deprecated (but API for setter remains).
39
+ #
40
+ # As total must equal sub_total + total_tax for the API call to pass, this is now
41
+ # automatically calculated in the total method.
42
+ def total=(value)
43
+ end
44
+
45
+ # Calculate the toal as sub_total + total_tax.
46
+ def total
47
+ sub_total + total_tax
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,163 @@
1
+ module XeroGateway
2
+ class ManualJournal
3
+ include Dates
4
+
5
+ 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)
6
+
7
+ STATUSES = {
8
+ 'DRAFT' => 'Draft Manual Journal',
9
+ 'POSTED' => 'Posted Manual Journal',
10
+ 'DELETED' => 'Deleted Draft Manual Journal',
11
+ 'VOIDED' => 'Voided Posted Manual Journal'
12
+ } unless defined?(STATUSES)
13
+
14
+ # Xero::Gateway associated with this invoice.
15
+ attr_accessor :gateway
16
+
17
+ # Any errors that occurred when the #valid? method called.
18
+ attr_reader :errors
19
+
20
+ # Represents whether the journal lines have been downloaded when getting from GET /API.XRO/2.0/ManualJournals
21
+ attr_accessor :journal_lines_downloaded
22
+
23
+ # accessible fields
24
+ attr_accessor :manual_journal_id, :narration, :date, :status, :journal_lines, :url, :show_on_cash_basis_reports
25
+
26
+ def initialize(params = {})
27
+ @errors ||= []
28
+ @payments ||= []
29
+
30
+ # Check if the line items have been downloaded.
31
+ @journal_lines_downloaded = (params.delete(:journal_lines_downloaded) == true)
32
+
33
+ params.each do |k,v|
34
+ self.send("#{k}=", v)
35
+ end
36
+
37
+ @journal_lines ||= []
38
+ end
39
+
40
+ def ==(other)
41
+ ['narration', 'status', 'journal_lines', 'show_on_cash_basis_reports'].each do |field|
42
+ return false if send(field) != other.send(field)
43
+ end
44
+
45
+ ["date"].each do |field|
46
+ return false if send(field).to_s != other.send(field).to_s
47
+ end
48
+ return true
49
+ end
50
+
51
+ # Validate the ManualJournal record according to what will be valid by the gateway.
52
+ #
53
+ # Usage:
54
+ # manual_journal.valid? # Returns true/false
55
+ #
56
+ # Additionally sets manual_journal.errors array to an array of field/error.
57
+ def valid?
58
+ @errors = []
59
+
60
+ if !manual_journal_id.nil? && manual_journal_id !~ GUID_REGEX
61
+ @errors << ['manual_journal_id', 'must be blank or a valid Xero GUID']
62
+ end
63
+
64
+ if narration.blank?
65
+ @errors << ['narration', "can't be blank"]
66
+ end
67
+
68
+ unless date
69
+ @errors << ['date', "can't be blank"]
70
+ end
71
+
72
+ # Make sure all journal_items are valid.
73
+ unless journal_lines.all? { | journal_line | journal_line.valid? }
74
+ @errors << ['journal_lines', "at least one journal line invalid"]
75
+ end
76
+
77
+ # make sure there are at least 2 journal lines
78
+ unless journal_lines.length > 1
79
+ @errors << ['journal_lines', "journal must contain at least two individual journal lines"]
80
+ end
81
+
82
+ if journal_lines.length > 100
83
+ @errors << ['journal_lines', "journal must contain less than one hundred journal lines"]
84
+ end
85
+
86
+ unless journal_lines.sum(&:line_amount).to_f == 0.0
87
+ @errors << ['journal_lines', "the total debits must be equal to total credits"]
88
+ end
89
+
90
+ @errors.size == 0
91
+ end
92
+
93
+
94
+ def journal_lines_downloaded?
95
+ @journal_lines_downloaded
96
+ end
97
+
98
+ # If line items are not downloaded, then attempt a download now (if this record was found to begin with).
99
+ def journal_lines
100
+ if journal_lines_downloaded?
101
+ @journal_lines
102
+
103
+ elsif manual_journal_id =~ GUID_REGEX && @gateway
104
+ # There is a manual_journal_id so we can assume this record was loaded from Xero.
105
+ # Let's attempt to download the journal_line records (if there is a gateway)
106
+
107
+ response = @gateway.get_manual_journal(manual_journal_id)
108
+ raise ManualJournalNotFoundError, "Manual Journal with ID #{manual_journal_id} not found in Xero." unless response.success? && response.manual_journal.is_a?(XeroGateway::ManualJournal)
109
+
110
+ @journal_lines = response.manual_journal.journal_lines
111
+ @journal_lines_downloaded = true
112
+
113
+ @journal_lines
114
+
115
+ # Otherwise, this is a new manual journal, so return the journal_lines reference.
116
+ else
117
+ @journal_lines
118
+ end
119
+ end
120
+
121
+ def to_xml(b = Builder::XmlMarkup.new)
122
+ b.ManualJournal {
123
+ b.ManualJournalID manual_journal_id if manual_journal_id
124
+ b.Narration narration
125
+ b.JournalLines {
126
+ self.journal_lines.each do |journal_line|
127
+ journal_line.to_xml(b)
128
+ end
129
+ }
130
+ b.Date ManualJournal.format_date(date || Date.today)
131
+ b.Status status if status
132
+ b.Url url if url
133
+ }
134
+ end
135
+
136
+ def self.from_xml(manual_journal_element, gateway = nil, options = {})
137
+ manual_journal = ManualJournal.new(options.merge({:gateway => gateway}))
138
+ manual_journal_element.children.each do |element|
139
+ case(element.name)
140
+ when "ManualJournalID" then manual_journal.manual_journal_id = element.text
141
+ when "Date" then manual_journal.date = parse_date(element.text)
142
+ when "Status" then manual_journal.status = element.text
143
+ when "Narration" then manual_journal.narration = element.text
144
+ when "JournalLines" then element.children.each {|journal_line| manual_journal.journal_lines_downloaded = true; manual_journal.journal_lines << JournalLine.from_xml(journal_line) }
145
+ when "Url" then manual_journal.url = element.text
146
+ end
147
+ end
148
+ manual_journal
149
+ end # from_xml
150
+
151
+ def add_journal_line(params = {})
152
+ journal_line = nil
153
+ case params
154
+ when Hash then journal_line = JournalLine.new(params)
155
+ when JournalLine then journal_line = params
156
+ else raise InvalidLineItemError
157
+ end
158
+ @journal_lines << journal_line
159
+ journal_line
160
+ end
161
+
162
+ end
163
+ end
@@ -0,0 +1,16 @@
1
+ module XeroGateway
2
+ module Money
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def format_money(amount)
9
+ if amount.class == BigDecimal
10
+ return amount.to_s("F")
11
+ end
12
+ return amount
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,92 @@
1
+ module XeroGateway
2
+
3
+ # Shamelessly based on the Twitter Gem's OAuth implementation by John Nunemaker
4
+ # Thanks!
5
+ #
6
+ # http://twitter.rubyforge.org/
7
+ # http://github.com/jnunemaker/twitter/
8
+
9
+ class OAuth
10
+
11
+ class TokenExpired < StandardError; end
12
+ class TokenInvalid < StandardError; end
13
+ class RateLimitExceeded < StandardError; end
14
+ class UnknownError < StandardError; end
15
+
16
+ unless defined? XERO_CONSUMER_OPTIONS
17
+ XERO_CONSUMER_OPTIONS = {
18
+ :site => "https://api.xero.com",
19
+ :request_token_path => "/oauth/RequestToken",
20
+ :access_token_path => "/oauth/AccessToken",
21
+ :authorize_path => "/oauth/Authorize"
22
+ }.freeze
23
+ end
24
+
25
+ extend Forwardable
26
+ def_delegators :access_token, :get, :post, :put, :delete
27
+
28
+ attr_reader :ctoken, :csecret, :consumer_options, :session_handle, :expires_at, :authorization_expires_at
29
+
30
+ def initialize(ctoken, csecret, options = {})
31
+ @ctoken, @csecret = ctoken, csecret
32
+ @consumer_options = XERO_CONSUMER_OPTIONS.merge(options)
33
+ end
34
+
35
+ def consumer
36
+ @consumer ||= ::OAuth::Consumer.new(@ctoken, @csecret, consumer_options)
37
+ end
38
+
39
+ def request_token(params = {})
40
+ @request_token ||= consumer.get_request_token(params)
41
+ end
42
+
43
+ def authorize_from_request(rtoken, rsecret, params = {})
44
+ request_token = ::OAuth::RequestToken.new(consumer, rtoken, rsecret)
45
+ access_token = request_token.get_access_token(params)
46
+ @atoken, @asecret = access_token.token, access_token.secret
47
+
48
+ update_attributes_from_token(access_token)
49
+ end
50
+
51
+ def access_token
52
+ @access_token ||= ::OAuth::AccessToken.new(consumer, @atoken, @asecret)
53
+ end
54
+
55
+ def authorize_from_access(atoken, asecret)
56
+ @atoken, @asecret = atoken, asecret
57
+ end
58
+
59
+ # Renewing access tokens only works for Partner applications
60
+ def renew_access_token(access_token = nil, access_secret = nil, session_handle = nil)
61
+ access_token ||= @atoken
62
+ access_secret ||= @asecret
63
+ session_handle ||= @session_handle
64
+
65
+ old_token = ::OAuth::RequestToken.new(consumer, access_token, access_secret)
66
+
67
+ access_token = old_token.get_access_token({
68
+ :oauth_session_handle => session_handle,
69
+ :token => old_token
70
+ })
71
+
72
+ update_attributes_from_token(access_token)
73
+ rescue ::OAuth::Unauthorized => e
74
+ # If the original access token is for some reason invalid an OAuth::Unauthorized could be raised.
75
+ # In this case raise a XeroGateway::OAuth::TokenInvalid which can be captured by the caller. In this
76
+ # situation the end user will need to re-authorize the application via the request token authorization URL
77
+ raise XeroGateway::OAuth::TokenInvalid.new(e.message)
78
+ end
79
+
80
+ private
81
+
82
+ # Update instance variables with those from the AccessToken.
83
+ def update_attributes_from_token(access_token)
84
+ @expires_at = Time.now + access_token.params[:oauth_expires_in].to_i
85
+ @authorization_expires_at = Time.now + access_token.params[:oauth_authorization_expires_in].to_i
86
+ @session_handle = access_token.params[:oauth_session_handle]
87
+ @atoken, @asecret = access_token.token, access_token.secret
88
+ @access_token = nil
89
+ end
90
+
91
+ end
92
+ end