xero_gateway-n8vision 2.0.20

Sign up to get free protection for your applications and to get access to all the features.
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