xeroizer 2.15.5 → 2.15.6

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