xeroizer 2.15.5 → 2.15.6

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 (49) hide show
  1. data/README.md +14 -3
  2. data/lib/xeroizer.rb +5 -0
  3. data/lib/xeroizer/generic_application.rb +15 -9
  4. data/lib/xeroizer/http.rb +44 -34
  5. data/lib/xeroizer/models/account.rb +15 -11
  6. data/lib/xeroizer/models/allocation.rb +13 -0
  7. data/lib/xeroizer/models/attachment.rb +93 -0
  8. data/lib/xeroizer/models/bank_transaction.rb +3 -2
  9. data/lib/xeroizer/models/contact.rb +20 -12
  10. data/lib/xeroizer/models/contact_person.rb +20 -0
  11. data/lib/xeroizer/models/credit_note.rb +2 -0
  12. data/lib/xeroizer/models/expense_claim.rb +29 -0
  13. data/lib/xeroizer/models/invoice.rb +42 -29
  14. data/lib/xeroizer/models/line_item.rb +1 -0
  15. data/lib/xeroizer/models/organisation.rb +6 -1
  16. data/lib/xeroizer/models/payment.rb +16 -11
  17. data/lib/xeroizer/models/receipt.rb +39 -0
  18. data/lib/xeroizer/models/tax_component.rb +13 -0
  19. data/lib/xeroizer/models/tax_rate.rb +14 -3
  20. data/lib/xeroizer/models/user.rb +26 -0
  21. data/lib/xeroizer/oauth.rb +3 -3
  22. data/lib/xeroizer/record/base.rb +40 -31
  23. data/lib/xeroizer/record/base_model.rb +31 -25
  24. data/lib/xeroizer/record/base_model_http_proxy.rb +4 -1
  25. data/lib/xeroizer/record/record_association_helper.rb +35 -35
  26. data/lib/xeroizer/record/xml_helper.rb +1 -1
  27. data/lib/xeroizer/report/factory.rb +2 -1
  28. data/lib/xeroizer/version.rb +3 -0
  29. data/test/stub_responses/organisation.xml +30 -0
  30. data/test/stub_responses/tax_rates.xml +81 -1
  31. data/test/stub_responses/users.xml +17 -0
  32. data/test/unit/generic_application_test.rb +21 -0
  33. data/test/unit/http_test.rb +18 -0
  34. data/test/unit/models/bank_transaction_test.rb +1 -4
  35. data/test/unit/models/line_item_sum_test.rb +3 -2
  36. data/test/unit/models/tax_rate_test.rb +81 -0
  37. data/test/unit/oauth_test.rb +4 -2
  38. data/test/unit/record/base_test.rb +1 -1
  39. data/test/unit/record/model_definition_test.rb +9 -3
  40. data/test/unit/record/record_association_test.rb +1 -1
  41. data/test/unit/record_definition_test.rb +1 -1
  42. metadata +540 -205
  43. data/.bundle/config +0 -2
  44. data/.gitattributes +0 -22
  45. data/Gemfile +0 -20
  46. data/Gemfile.lock +0 -59
  47. data/Rakefile +0 -55
  48. data/VERSION +0 -1
  49. data/xeroizer.gemspec +0 -409
data/README.md CHANGED
@@ -112,10 +112,10 @@ class XeroSessionController < ApplicationController
112
112
 
113
113
  session[:xero_auth] = {
114
114
  :access_token => @xero_client.access_token.token,
115
- :access_key => @xero_client.access_token.key }
115
+ :access_key => @xero_client.access_token.secret }
116
116
 
117
- session.data.delete(:request_token)
118
- session.data.delete(:request_secret)
117
+ session[:request_token] = nil
118
+ session[:request_secret] = nil
119
119
  end
120
120
 
121
121
  def destroy
@@ -549,6 +549,17 @@ client = Xeroizer::PublicApplication.new(YOUR_OAUTH_CONSUMER_KEY,
549
549
  :rate_limit_sleep => 2)
550
550
  ```
551
551
 
552
+ Logging
553
+ ---------------
554
+
555
+ You can add an optional paramater to the Xeroizer Application initialization, to pass a logger object that will need to respond_to :info. For example, in a rails app:
556
+
557
+ ```ruby
558
+ XeroLogger = Logger.new('log/xero.log', 'weekly')
559
+ client = Xeroizer::PublicApplication.new(YOUR_OAUTH_CONSUMER_KEY,
560
+ YOUR_OAUTH_CONSUMER_SECRET,
561
+ :logger => XeroLogger)
562
+ ```
552
563
 
553
564
  ### Contributors
554
565
  Xeroizer was inspired by the https://github.com/tlconnor/xero_gateway gem created by Tim Connor
data/lib/xeroizer.rb CHANGED
@@ -27,6 +27,7 @@ require 'xeroizer/configuration'
27
27
  # Include models
28
28
  require 'xeroizer/models/account'
29
29
  require 'xeroizer/models/address'
30
+ require 'xeroizer/models/allocation'
30
31
  require 'xeroizer/models/branding_theme'
31
32
  require 'xeroizer/models/bank_transaction'
32
33
  require 'xeroizer/models/bank_account'
@@ -35,6 +36,7 @@ require 'xeroizer/models/contact_group'
35
36
  require 'xeroizer/models/credit_note'
36
37
  require 'xeroizer/models/currency'
37
38
  require 'xeroizer/models/employee'
39
+ require 'xeroizer/models/expense_claim'
38
40
  require 'xeroizer/models/invoice'
39
41
  require 'xeroizer/models/item'
40
42
  require 'xeroizer/models/item_purchase_details'
@@ -48,9 +50,12 @@ require 'xeroizer/models/option'
48
50
  require 'xeroizer/models/organisation'
49
51
  require 'xeroizer/models/payment'
50
52
  require 'xeroizer/models/phone'
53
+ require 'xeroizer/models/receipt'
51
54
  require 'xeroizer/models/tax_rate'
55
+ require 'xeroizer/models/tax_component'
52
56
  require 'xeroizer/models/tracking_category'
53
57
  require 'xeroizer/models/tracking_category_child'
58
+ require 'xeroizer/models/user'
54
59
  require 'xeroizer/models/journal_line_tracking_category'
55
60
 
56
61
  require 'xeroizer/report/factory'
@@ -2,30 +2,34 @@ require 'xeroizer/record/application_helper'
2
2
 
3
3
  module Xeroizer
4
4
  class GenericApplication
5
-
5
+
6
6
  include Http
7
7
  extend Record::ApplicationHelper
8
-
9
- attr_reader :client, :xero_url, :logger, :rate_limit_sleep, :rate_limit_max_attempts
10
-
8
+
9
+ attr_reader :client, :xero_url, :logger, :rate_limit_sleep, :rate_limit_max_attempts, :default_headers
10
+
11
11
  extend Forwardable
12
12
  def_delegators :client, :access_token
13
-
13
+
14
14
  record :Account
15
+ record :Attachment
15
16
  record :BrandingTheme
16
17
  record :Contact
17
18
  record :CreditNote
18
19
  record :Currency
19
20
  record :Employee
21
+ record :ExpenseClaim
20
22
  record :Invoice
21
23
  record :Item
22
24
  record :Journal
23
25
  record :ManualJournal
24
26
  record :Organisation
25
27
  record :Payment
28
+ record :Receipt
26
29
  record :TaxRate
27
30
  record :TrackingCategory
28
31
  record :BankTransaction
32
+ record :User
29
33
 
30
34
  report :AgedPayablesByContact
31
35
  report :AgedReceivablesByContact
@@ -36,9 +40,9 @@ module Xeroizer
36
40
  report :ExecutiveSummary
37
41
  report :ProfitAndLoss
38
42
  report :TrialBalance
39
-
43
+
40
44
  public
41
-
45
+
42
46
  # Never used directly. Use sub-classes instead.
43
47
  # @see PublicApplication
44
48
  # @see PrivateApplication
@@ -47,8 +51,10 @@ module Xeroizer
47
51
  @xero_url = options[:xero_url] || "https://api.xero.com/api.xro/2.0"
48
52
  @rate_limit_sleep = options[:rate_limit_sleep] || false
49
53
  @rate_limit_max_attempts = options[:rate_limit_max_attempts] || 5
50
- @client = OAuth.new(consumer_key, consumer_secret, options)
54
+ @default_headers = options[:default_headers] || {}
55
+ @client = OAuth.new(consumer_key, consumer_secret, options.merge({default_headers: default_headers}))
56
+ @logger = options[:logger] || false
51
57
  end
52
-
58
+
53
59
  end
54
60
  end
data/lib/xeroizer/http.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # Copyright (c) 2008 Tim Connor <tlconnor@gmail.com>
2
- #
2
+ #
3
3
  # Permission to use, copy, modify, and/or distribute this software for any
4
4
  # purpose with or without fee is hereby granted, provided that the above
5
5
  # copyright notice and this permission notice appear in all copies.
6
- #
6
+ #
7
7
  # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
8
  # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
9
  # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
@@ -14,12 +14,13 @@
14
14
 
15
15
  module Xeroizer
16
16
  module Http
17
-
17
+ class BadResponse < StandardError; end
18
+
18
19
  ACCEPT_MIME_MAP = {
19
20
  :pdf => 'application/pdf',
20
21
  :json => 'application/json'
21
22
  }
22
-
23
+
23
24
  # Shortcut method for #http_request with `method` = :get.
24
25
  #
25
26
  # @param [OAuth] client OAuth client
@@ -48,21 +49,24 @@ module Xeroizer
48
49
  def http_put(client, url, body, extra_params = {})
49
50
  http_request(client, :put, url, body, extra_params)
50
51
  end
51
-
52
+
52
53
  private
53
-
54
+
54
55
  def http_request(client, method, url, body, params = {})
55
56
  # headers = {'Accept-Encoding' => 'gzip, deflate'}
56
57
 
57
- headers = { 'charset' => 'utf-8' }
58
+ headers = self.default_headers.merge({ 'charset' => 'utf-8' })
58
59
 
59
60
  if method != :get
60
61
  headers['Content-Type'] ||= "application/x-www-form-urlencoded"
61
62
  end
62
63
 
64
+ content_type = params.delete(:content_type)
65
+ headers['Content-Type'] = content_type if content_type
66
+
63
67
  # HAX. Xero completely misuse the If-Modified-Since HTTP header.
64
68
  headers['If-Modified-Since'] = params.delete(:ModifiedAfter).utc.strftime("%Y-%m-%dT%H:%M:%S") if params[:ModifiedAfter]
65
-
69
+
66
70
  # Allow 'Accept' header to be specified with :accept parameter.
67
71
  # Valid values are :pdf or :json.
68
72
  if params[:response]
@@ -72,7 +76,7 @@ module Xeroizer
72
76
  else response_type
73
77
  end
74
78
  end
75
-
79
+
76
80
  if params.any?
77
81
  url += "?" + params.map {|key,value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"}.join("&")
78
82
  end
@@ -83,19 +87,20 @@ module Xeroizer
83
87
 
84
88
  begin
85
89
  attempts += 1
86
- logger.info("\n== [#{Time.now.to_s}] XeroGateway Request: #{method.to_s.upcase} #{uri.request_uri} ") if self.logger
90
+ logger.info("XeroGateway Request: #{method.to_s.upcase} #{uri.request_uri}") if self.logger
91
+
92
+ raw_body = params.delete(:raw_body) ? body : {:xml => body}
87
93
 
88
94
  response = case method
89
95
  when :get then client.get(uri.request_uri, headers)
90
- when :post then client.post(uri.request_uri, { :xml => body }, headers)
91
- when :put then client.put(uri.request_uri, { :xml => body }, headers)
96
+ when :post then client.post(uri.request_uri, raw_body, headers)
97
+ when :put then client.put(uri.request_uri, raw_body, headers)
92
98
  end
93
99
 
94
100
  if self.logger
95
- logger.info("== [#{Time.now.to_s}] XeroGateway Response (#{response.code})")
96
-
101
+ logger.info("XeroGateway Response (#{response.code})")
97
102
  unless response.code.to_i == 200
98
- logger.info("== #{uri.request_uri} Response Body \n\n #{response.plain_body} \n == End Response Body")
103
+ logger.info("#{uri.request_uri}\n== Response Body\n\n#{response.plain_body}\n== End Response Body")
99
104
  end
100
105
  end
101
106
 
@@ -111,12 +116,12 @@ module Xeroizer
111
116
  when 503
112
117
  handle_oauth_error!(response)
113
118
  else
114
- raise "Unknown response code: #{response.code.to_i}"
119
+ handle_unknown_response_error!(response)
115
120
  end
116
121
  rescue Xeroizer::OAuth::RateLimitExceeded
117
122
  if self.rate_limit_sleep
118
123
  raise if attempts > rate_limit_max_attempts
119
- logger.info("== Rate limit exceeded, retrying") if self.logger
124
+ logger.info("Rate limit exceeded, retrying") if self.logger
120
125
  sleep_for(self.rate_limit_sleep)
121
126
  retry
122
127
  else
@@ -124,34 +129,35 @@ module Xeroizer
124
129
  end
125
130
  end
126
131
  end
127
-
132
+
128
133
  def handle_oauth_error!(response)
129
134
  error_details = CGI.parse(response.plain_body)
130
135
  description = error_details["oauth_problem_advice"].first
131
-
136
+
132
137
  # see http://oauth.pbworks.com/ProblemReporting
133
138
  # In addition to token_expired and token_rejected, Xero also returns
134
139
  # 'rate limit exceeded' when more than 60 requests have been made in
135
140
  # a second.
136
141
  case (error_details["oauth_problem"].first)
137
- when "token_expired" then raise OAuth::TokenExpired.new(description)
138
- when "token_rejected" then raise OAuth::TokenInvalid.new(description)
139
- when "rate limit exceeded" then raise OAuth::RateLimitExceeded.new(description)
140
- else raise OAuth::UnknownError.new(error_details["oauth_problem"].first + ':' + description)
142
+ when "token_expired" then raise OAuth::TokenExpired.new(description)
143
+ when "token_rejected" then raise OAuth::TokenInvalid.new(description)
144
+ when "rate limit exceeded" then raise OAuth::RateLimitExceeded.new(description)
145
+ when error_details["oauth_problem"] then raise OAuth::UnknownError.new(error_details["oauth_problem"].first + ':' + description)
146
+ else raise OAuth::UnknownError.new("Xero API may be down or the way OAuth errors are provided by Xero may have changed.")
141
147
  end
142
148
  end
143
-
149
+
144
150
  def handle_error!(response, request_body)
145
-
151
+
146
152
  raw_response = response.plain_body
147
-
153
+
148
154
  # XeroGenericApplication API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
149
155
  # So let's ignore that :)
150
156
  raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
151
-
157
+
152
158
  # doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
153
159
  doc = Nokogiri::XML(raw_response)
154
-
160
+
155
161
  if doc && doc.root && doc.root.name == "ApiException"
156
162
 
157
163
  raise ApiException.new(doc.root.xpath("Type").text,
@@ -161,13 +167,13 @@ module Xeroizer
161
167
  request_body)
162
168
 
163
169
  else
164
-
165
- raise "Unparseable 400 Response: #{raw_response}"
166
-
170
+
171
+ raise BadResponse.new("Unparseable 400 Response: #{raw_response}")
172
+
167
173
  end
168
-
174
+
169
175
  end
170
-
176
+
171
177
  def handle_object_not_found!(response, request_url)
172
178
  case(request_url)
173
179
  when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.")
@@ -176,9 +182,13 @@ module Xeroizer
176
182
  end
177
183
  end
178
184
 
185
+ def handle_unknown_response_error!(response)
186
+ raise BadResponse.new("Unknown response code: #{response.code.to_i}")
187
+ end
188
+
179
189
  def sleep_for(seconds = 1)
180
190
  sleep seconds
181
191
  end
182
-
192
+
183
193
  end
184
194
  end
@@ -1,14 +1,18 @@
1
1
  module Xeroizer
2
2
  module Record
3
-
3
+
4
4
  class AccountModel < BaseModel
5
-
6
- set_permissions :read
7
-
5
+
6
+ set_permissions :read, :write
7
+
8
+ # The Accounts endpoint doesn't support the POST method yet.
9
+ def create_method
10
+ :http_put
11
+ end
8
12
  end
9
-
13
+
10
14
  class Account < Base
11
-
15
+
12
16
  TYPE = {
13
17
  'CURRENT' => '',
14
18
  'FIXED' => '',
@@ -32,7 +36,7 @@ module Xeroizer
32
36
  'INPUT' => 'GST on expenses',
33
37
  'SRINPUT' => 'VAT on expenses',
34
38
  'ZERORATEDINPUT' => 'Expense purchased from overseas (UK only)',
35
- 'RRINPUT' => 'Reduced rate VAT on expenses (UK Only)',
39
+ 'RRINPUT' => 'Reduced rate VAT on expenses (UK Only)',
36
40
  'EXEMPTOUTPUT' => 'VAT on sales exempt from VAT (UK only)',
37
41
  'OUTPUT' => 'OUTPUT',
38
42
  'OUTPUT2' => 'OUTPUT2',
@@ -42,9 +46,9 @@ module Xeroizer
42
46
  'ZERORATED' => 'Zero-rated supplies/sales from overseas (NZ Only)',
43
47
  'ECZROUTPUT' => 'Zero-rated EC Income (UK only)'
44
48
  } unless defined?(TAX_TYPE)
45
-
49
+
46
50
  set_primary_key :account_id
47
-
51
+
48
52
  guid :account_id
49
53
  string :code
50
54
  string :name
@@ -60,8 +64,8 @@ module Xeroizer
60
64
  string :bank_account_number
61
65
  string :reporting_code
62
66
  string :reporting_code_name
63
-
67
+
64
68
  end
65
-
69
+
66
70
  end
67
71
  end
@@ -0,0 +1,13 @@
1
+ module Xeroizer
2
+ module Record
3
+ class AllocationModel < BaseModel
4
+ end
5
+
6
+ class Allocation < Base
7
+ decimal :applied_amount
8
+ belongs_to :invoice
9
+
10
+ validates_presence_of :invoice
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,93 @@
1
+ module Xeroizer
2
+ module Record
3
+
4
+ class AttachmentModel < BaseModel
5
+
6
+ module Extensions
7
+ def attach_data(id, filename, data, content_type = "application/octet-stream")
8
+ application.Attachment.attach_data(url, id, filename, data, content_type)
9
+ end
10
+
11
+ def attach_file(id, filename, path, content_type = "application/octet-stream")
12
+ application.Attachment.attach_file(url, id, filename, path, content_type)
13
+ end
14
+
15
+ def attachments(id)
16
+ application.Attachment.attachments_for(url, id)
17
+ end
18
+ end
19
+
20
+ set_permissions :read
21
+
22
+ def attach_data(url, id, filename, data, content_type)
23
+ response_xml = @application.http_put(@application.client,
24
+ "#{url}/#{CGI.escape(id)}/Attachments/#{CGI.escape(filename)}",
25
+ data,
26
+ :raw_body => true, :content_type => content_type
27
+ )
28
+ response = parse_response(response_xml)
29
+ if (response_items = response.response_items) && response_items.size > 0
30
+ response_items.size == 1 ? response_items.first : response_items
31
+ else
32
+ response
33
+ end
34
+ end
35
+
36
+ def attach_file(url, id, filename, path, content_type)
37
+ attach_data(url, id, filename, File.read(path), content_type)
38
+ end
39
+
40
+ def attachments_for(url, id)
41
+ response_xml = @application.http_get(@application.client,
42
+ "#{url}/#{CGI.escape(id)}/Attachments")
43
+
44
+ response = parse_response(response_xml)
45
+ if (response_items = response.response_items) && response_items.size > 0
46
+ response_items
47
+ else
48
+ []
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ class Attachment < Base
55
+
56
+ module Extensions
57
+ def attach_file(filename, path, content_type = "application/octet-stream")
58
+ parent.attach_file(id, filename, path, content_type)
59
+ end
60
+
61
+ def attach_data(filename, data, content_type = "application/octet-stream")
62
+ parent.attach_data(id, filename, data, content_type)
63
+ end
64
+
65
+ def attachments
66
+ parent.attachments(id)
67
+ end
68
+ end
69
+
70
+ set_primary_key :attachment_id
71
+
72
+ guid :attachment_id
73
+ string :file_name
74
+ string :url
75
+ string :mime_type
76
+ integer :content_length
77
+
78
+ # Retrieve the attachment data.
79
+ # @param [String] filename optional filename to store the attachment in instead of returning the data.
80
+ def get(filename = nil)
81
+ data = parent.application.http_get(parent.application.client, url)
82
+ if filename
83
+ File.open(filename, "w") { | fp | fp.write data }
84
+ nil
85
+ else
86
+ data
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ end
93
+ end