xeroizer 2.16.5 → 2.17.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -27
  3. data/lib/class_level_inheritable_attributes.rb +3 -0
  4. data/lib/xeroizer.rb +13 -1
  5. data/lib/xeroizer/exceptions.rb +1 -1
  6. data/lib/xeroizer/generic_application.rb +13 -1
  7. data/lib/xeroizer/http.rb +128 -120
  8. data/lib/xeroizer/models/account.rb +1 -0
  9. data/lib/xeroizer/models/address.rb +18 -9
  10. data/lib/xeroizer/models/attachment.rb +1 -1
  11. data/lib/xeroizer/models/balances.rb +1 -0
  12. data/lib/xeroizer/models/bank_transaction.rb +2 -2
  13. data/lib/xeroizer/models/bank_transfer.rb +28 -0
  14. data/lib/xeroizer/models/contact.rb +11 -4
  15. data/lib/xeroizer/models/credit_note.rb +9 -4
  16. data/lib/xeroizer/models/from_bank_account.rb +12 -0
  17. data/lib/xeroizer/models/invoice.rb +11 -3
  18. data/lib/xeroizer/models/invoice_reminder.rb +19 -0
  19. data/lib/xeroizer/models/line_item.rb +21 -9
  20. data/lib/xeroizer/models/manual_journal.rb +6 -0
  21. data/lib/xeroizer/models/organisation.rb +4 -0
  22. data/lib/xeroizer/models/overpayment.rb +40 -0
  23. data/lib/xeroizer/models/payroll/bank_account.rb +23 -0
  24. data/lib/xeroizer/models/payroll/employee.rb +45 -0
  25. data/lib/xeroizer/models/payroll/home_address.rb +24 -0
  26. data/lib/xeroizer/models/prepayment.rb +1 -0
  27. data/lib/xeroizer/models/tax_rate.rb +5 -4
  28. data/lib/xeroizer/models/to_bank_account.rb +12 -0
  29. data/lib/xeroizer/oauth.rb +8 -8
  30. data/lib/xeroizer/partner_application.rb +4 -8
  31. data/lib/xeroizer/payroll_application.rb +28 -0
  32. data/lib/xeroizer/private_application.rb +2 -4
  33. data/lib/xeroizer/record/base_model.rb +137 -122
  34. data/lib/xeroizer/record/base_model_http_proxy.rb +1 -1
  35. data/lib/xeroizer/record/payroll_base.rb +25 -0
  36. data/lib/xeroizer/record/payroll_base_model.rb +29 -0
  37. data/lib/xeroizer/record/record_association_helper.rb +13 -14
  38. data/lib/xeroizer/record/validation_helper.rb +1 -1
  39. data/lib/xeroizer/record/xml_helper.rb +10 -8
  40. data/lib/xeroizer/report/aged_receivables_by_contact.rb +0 -1
  41. data/lib/xeroizer/report/base.rb +0 -1
  42. data/lib/xeroizer/report/factory.rb +3 -3
  43. data/lib/xeroizer/report/row/header.rb +4 -4
  44. data/lib/xeroizer/report/row/xml_helper.rb +2 -2
  45. data/lib/xeroizer/version.rb +1 -1
  46. data/test/acceptance/about_creating_prepayment_test.rb +46 -0
  47. data/test/acceptance/about_fetching_bank_transactions_test.rb +21 -21
  48. data/test/acceptance/acceptance_test.rb +4 -4
  49. data/test/acceptance/bank_transaction_reference_data.rb +3 -1
  50. data/test/acceptance/bank_transfer_test.rb +26 -0
  51. data/test/test_helper.rb +6 -4
  52. data/test/unit/models/address_test.rb +92 -0
  53. data/test/unit/models/bank_transaction_validation_test.rb +1 -1
  54. data/test/unit/models/contact_test.rb +20 -4
  55. data/test/unit/models/line_item_test.rb +20 -6
  56. data/test/unit/models/tax_rate_test.rb +52 -1
  57. data/test/unit/record/base_model_test.rb +1 -1
  58. data/test/unit/record/base_test.rb +9 -6
  59. data/test/unit/record/model_definition_test.rb +2 -2
  60. data/test/unit/report_test.rb +19 -21
  61. metadata +20 -3
@@ -0,0 +1,45 @@
1
+ module Xeroizer
2
+ module Record
3
+ module Payroll
4
+
5
+ class EmployeeModel < PayrollBaseModel
6
+
7
+ set_permissions :read, :write, :update
8
+
9
+ end
10
+
11
+ class Employee < PayrollBase
12
+
13
+ set_primary_key :employee_id
14
+
15
+ guid :employee_id
16
+ string :status
17
+ string :title
18
+ string :first_name
19
+ string :middle_names
20
+ string :last_name
21
+ date :start_date
22
+ string :email
23
+ date :date_of_birth
24
+ string :gender
25
+ string :phone
26
+ string :mobile
27
+ string :twitter_user_name
28
+ boolean :is_authorised_to_approve_leave
29
+ boolean :is_authorised_to_approve_timesheets
30
+ string :occupation
31
+ string :classification
32
+ guid :ordinary_earnings_rate_id
33
+ guid :payroll_calendar_id
34
+ string :employee_group_name
35
+ date :termination_date
36
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
37
+
38
+ belongs_to :home_address, :internal_name_singular => "home_address", :model_name => "HomeAddress"
39
+ has_many :bank_accounts
40
+
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ module Xeroizer
2
+ module Record
3
+ module Payroll
4
+
5
+ class HomeAddressModel < PayrollBaseModel
6
+
7
+ end
8
+
9
+ class HomeAddress < PayrollBase
10
+
11
+ string :address_line_1
12
+ string :address_line_2
13
+ string :address_line_3
14
+ string :address_line_4
15
+ string :city
16
+ string :region
17
+ string :postal_code
18
+ string :country
19
+
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -29,6 +29,7 @@ module Xeroizer
29
29
 
30
30
  belongs_to :contact
31
31
  has_many :line_items
32
+ has_many :payments
32
33
 
33
34
  def contact_id
34
35
  contact.id if contact
@@ -1,10 +1,10 @@
1
1
  module Xeroizer
2
2
  module Record
3
-
3
+
4
4
  class TaxRateModel < BaseModel
5
-
5
+
6
6
  set_permissions :read
7
-
7
+
8
8
  # TaxRates can be created using either POST or PUT.
9
9
  # POST will also silently update the tax, which can
10
10
  # be unexpected. PUT is only for create.
@@ -12,13 +12,14 @@ module Xeroizer
12
12
  :http_put
13
13
  end
14
14
  end
15
-
15
+
16
16
  class TaxRate < Base
17
17
  set_primary_key :name
18
18
  set_possible_primary_keys :tax_type, :name
19
19
 
20
20
  string :name
21
21
  string :tax_type
22
+ string :report_tax_type # Read-only except for AU, NZ, and UK versions
22
23
  string :status
23
24
  boolean :can_apply_to_assets
24
25
  boolean :can_apply_to_equity
@@ -0,0 +1,12 @@
1
+ module Xeroizer
2
+ module Record
3
+ class ToBankAccountModel < BaseModel
4
+ set_permissions :read
5
+ end
6
+
7
+ class ToBankAccount < Base
8
+ guid :account_id
9
+ string :code
10
+ end
11
+ end
12
+ end
@@ -23,13 +23,14 @@ module Xeroizer
23
23
  # http://github.com/jnunemaker/twitter/
24
24
 
25
25
  class OAuth
26
-
27
- class TokenExpired < StandardError; end
28
- class TokenInvalid < StandardError; end
29
- class RateLimitExceeded < StandardError; end
30
- class ConsumerKeyUnknown < StandardError; end
31
- class NonceUsed < StandardError; end
32
- class UnknownError < StandardError; end
26
+ class OAuthError < XeroizerError; end
27
+ class TokenExpired < OAuthError; end
28
+ class TokenInvalid < OAuthError; end
29
+ class RateLimitExceeded < OAuthError; end
30
+ class ConsumerKeyUnknown < OAuthError; end
31
+ class NonceUsed < OAuthError; end
32
+ class OrganisationOffline < OAuthError; end
33
+ class UnknownError < OAuthError; end
33
34
 
34
35
  unless defined? XERO_CONSUMER_OPTIONS
35
36
  XERO_CONSUMER_OPTIONS = {
@@ -132,7 +133,6 @@ module Xeroizer
132
133
  consumer.http.cert = @consumer_options[:ssl_client_cert]
133
134
  consumer.http.key = @consumer_options[:ssl_client_key]
134
135
  end
135
- consumer
136
136
 
137
137
  if @consumer_options[:http_debug_output]
138
138
  consumer.http.set_debug_output(@consumer_options[:http_debug_output])
@@ -13,21 +13,17 @@ module Xeroizer
13
13
  # @param [String] consumer_key consumer key/token from application developer (found at http://api.xero.com for your application).
14
14
  # @param [String] consumer_secret consumer secret from application developer (found at http://api.xero.com for your application).
15
15
  # @param [String] path_to_private_key application's private key for message signing (uploaded to http://api.xero.com)
16
- # @param [String] ssl_client_cert client-side SSL certificate file to use for requests
17
- # @param [String] ssl_client_key client-side SSL private key to use for requests
18
16
  # @param [Hash] options other options to pass to the GenericApplication constructor
19
17
  # @return [PartnerApplication] instance of PrivateApplication
20
- def initialize(consumer_key, consumer_secret, path_to_private_key, ssl_client_cert, ssl_client_key, options = {})
18
+ def initialize(consumer_key, consumer_secret, path_to_private_key, options = {})
21
19
  default_options = {
22
- :xero_url => 'https://api-partner.network.xero.com/api.xro/2.0',
23
- :site => 'https://api-partner.network.xero.com',
20
+ :xero_url => 'https://api.xero.com/api.xro/2.0',
21
+ :site => 'https://api.xero.com',
24
22
  :authorize_url => 'https://api.xero.com/oauth/Authorize',
25
23
  :signature_method => 'RSA-SHA1'
26
24
  }
27
25
  options = default_options.merge(options).merge(
28
- :private_key_file => path_to_private_key,
29
- :ssl_client_cert => OpenSSL::X509::Certificate.new(read_certificate(ssl_client_cert)),
30
- :ssl_client_key => OpenSSL::PKey::RSA.new(read_certificate(ssl_client_key))
26
+ :private_key_file => path_to_private_key
31
27
  )
32
28
  super(consumer_key, consumer_secret, options)
33
29
 
@@ -0,0 +1,28 @@
1
+ module Xeroizer
2
+ class PayrollApplication
3
+
4
+ attr_reader :application
5
+
6
+ # Factory for new Payroll BaseModel instances with the class name `record_type`.
7
+ # Only creates the instance if one doesn't already exist.
8
+ #
9
+ # @param [Symbol] record_type Symbol of the record type (e.g. :Invoice)
10
+ # @return [BaseModel] instance of BaseModel subclass matching `record_type`
11
+ def self.record(record_type)
12
+ define_method record_type do
13
+ var_name = "@#{record_type}_cache".to_sym
14
+ unless instance_variable_defined?(var_name)
15
+ instance_variable_set(var_name, Xeroizer::Record::Payroll.const_get("#{record_type}Model".to_sym).new(self.application, record_type.to_s))
16
+ end
17
+ instance_variable_get(var_name)
18
+ end
19
+ end
20
+
21
+ record :Employee
22
+
23
+ def initialize(application)
24
+ @application = application
25
+ end
26
+
27
+ end
28
+ end
@@ -15,10 +15,8 @@ module Xeroizer
15
15
  # @param [Hash] options other options to pass to the GenericApplication constructor
16
16
  # @return [PrivateApplication] instance of PrivateApplication
17
17
  def initialize(consumer_key, consumer_secret, path_to_private_key, options = {})
18
- options.merge!(
19
- :signature_method => 'RSA-SHA1',
20
- :private_key_file => path_to_private_key
21
- )
18
+ options[:signature_method] = 'RSA-SHA1'
19
+ options[:private_key_file] = path_to_private_key
22
20
  super(consumer_key, consumer_secret, options)
23
21
  @client.authorize_from_access(consumer_key, consumer_secret)
24
22
  end
@@ -9,7 +9,7 @@ module Xeroizer
9
9
  class_inheritable_attributes :api_controller_name
10
10
 
11
11
  module InvaidPermissionError; end
12
- class InvalidPermissionError < StandardError
12
+ class InvalidPermissionError < XeroizerError
13
13
  include InvaidPermissionError
14
14
  end
15
15
  ALLOWED_PERMISSIONS = [:read, :write, :update]
@@ -25,7 +25,7 @@ module Xeroizer
25
25
 
26
26
  attr_reader :application
27
27
  attr_reader :model_name
28
- attr :model_class
28
+ attr_writer :model_class
29
29
  attr_reader :response
30
30
 
31
31
  class << self
@@ -72,167 +72,182 @@ module Xeroizer
72
72
 
73
73
  public
74
74
 
75
- def initialize(application, model_name)
76
- @application = application
77
- @model_name = model_name
78
- end
75
+ def initialize(application, model_name)
76
+ @application = application
77
+ @model_name = model_name
78
+ @allow_batch_operations = false
79
+ @objects = {}
80
+ end
79
81
 
80
- # Retrieve the controller name.
81
- #
82
- # Default: pluaralized model name (e.g. if the controller name is
83
- # Invoice then the default is Invoices.
84
- def api_controller_name
85
- self.class.api_controller_name || model_name.pluralize
86
- end
82
+ # Retrieve the controller name.
83
+ #
84
+ # Default: pluaralized model name (e.g. if the controller name is
85
+ # Invoice then the default is Invoices.
86
+ def api_controller_name
87
+ self.class.api_controller_name || model_name.pluralize
88
+ end
87
89
 
88
- def model_class
89
- @model_class ||= Xeroizer::Record.const_get(model_name.to_sym)
90
- end
90
+ def model_class
91
+ @model_class ||= Xeroizer::Record.const_get(model_name.to_sym)
92
+ end
91
93
 
92
- # Build a record with attributes set to the value of attributes.
93
- def build(attributes = {})
94
- model_class.build(attributes, self).tap do |resource|
95
- mark_dirty(resource)
96
- end
94
+ # Build a record with attributes set to the value of attributes.
95
+ def build(attributes = {})
96
+ model_class.build(attributes, self).tap do |resource|
97
+ mark_dirty(resource)
97
98
  end
99
+ end
98
100
 
99
- def mark_dirty(resource)
100
- if @allow_batch_operations
101
- @objects[model_class] ||= {}
102
- @objects[model_class][resource.object_id] ||= resource
103
- end
101
+ def mark_dirty(resource)
102
+ if @allow_batch_operations
103
+ @objects[model_class] ||= {}
104
+ @objects[model_class][resource.object_id] ||= resource
104
105
  end
106
+ end
105
107
 
106
- def mark_clean(resource)
107
- if @objects and @objects[model_class]
108
- @objects[model_class].delete(resource.object_id)
109
- end
108
+ def mark_clean(resource)
109
+ if @objects and @objects[model_class]
110
+ @objects[model_class].delete(resource.object_id)
110
111
  end
112
+ end
111
113
 
112
- # Create (build and save) a record with attributes set to the value of attributes.
113
- def create(attributes = {})
114
- build(attributes).tap { |resource| resource.save }
115
- end
114
+ # Create (build and save) a record with attributes set to the value of attributes.
115
+ def create(attributes = {})
116
+ build(attributes).tap { |resource| resource.save }
117
+ end
116
118
 
117
- # Retreive full record list for this model.
118
- def all(options = {})
119
- raise MethodNotAllowed.new(self, :all) unless self.class.permissions[:read]
120
- response_xml = http_get(parse_params(options))
121
- response = parse_response(response_xml, options)
122
- response.response_items || []
123
- end
119
+ # Retreive full record list for this model.
120
+ def all(options = {})
121
+ raise MethodNotAllowed.new(self, :all) unless self.class.permissions[:read]
122
+ response_xml = http_get(parse_params(options))
123
+ response = parse_response(response_xml, options)
124
+ response.response_items || []
125
+ end
124
126
 
125
- # Helper method to retrieve just the first element from
126
- # the full record list.
127
- def first(options = {})
128
- raise MethodNotAllowed.new(self, :all) unless self.class.permissions[:read]
129
- result = all(options)
130
- result.first if result.is_a?(Array)
127
+ # allow invoices to be process in batches of 100 as per xero documentation
128
+ # https://developer.xero.com/documentation/api/invoices/
129
+ def find_in_batches(options = {}, &block)
130
+ options[:page] ||= 1
131
+ while results = all(options)
132
+ if results.any?
133
+ yield results
134
+ options[:page] += 1
135
+ else
136
+ break
137
+ end
131
138
  end
139
+ end
132
140
 
133
- # Retrieve record matching the passed in ID.
134
- def find(id, options = {})
135
- raise MethodNotAllowed.new(self, :all) unless self.class.permissions[:read]
136
- response_xml = @application.http_get(@application.client, "#{url}/#{CGI.escape(id)}", options)
137
- response = parse_response(response_xml, options)
138
- result = response.response_items.first if response.response_items.is_a?(Array)
139
- result.complete_record_downloaded = true if result
140
- result
141
- end
141
+ # Helper method to retrieve just the first element from
142
+ # the full record list.
143
+ def first(options = {})
144
+ raise MethodNotAllowed.new(self, :all) unless self.class.permissions[:read]
145
+ result = all(options)
146
+ result.first if result.is_a?(Array)
147
+ end
148
+
149
+ # Retrieve record matching the passed in ID.
150
+ def find(id, options = {})
151
+ raise MethodNotAllowed.new(self, :all) unless self.class.permissions[:read]
152
+ response_xml = @application.http_get(@application.client, "#{url}/#{CGI.escape(id)}", options)
153
+ response = parse_response(response_xml, options)
154
+ result = response.response_items.first if response.response_items.is_a?(Array)
155
+ result.complete_record_downloaded = true if result
156
+ result
157
+ end
142
158
 
143
- def save_records(records, chunk_size = DEFAULT_RECORDS_PER_BATCH_SAVE)
144
- no_errors = true
145
- return false unless records.all?(&:valid?)
146
-
147
- actions = records.group_by {|o| o.new_record? ? create_method : :http_post }
148
- actions.each_pair do |http_method, records_for_method|
149
- records_for_method.each_slice(chunk_size) do |some_records|
150
- request = to_bulk_xml(some_records)
151
- response = parse_response(self.send(http_method, request, {:summarizeErrors => false}))
152
- response.response_items.each_with_index do |record, i|
153
- if record and record.is_a?(model_class)
154
- some_records[i].attributes = record.non_calculated_attributes
155
- some_records[i].errors = record.errors
156
- no_errors = record.errors.nil? || record.errors.empty? if no_errors
157
- some_records[i].saved!
158
- end
159
+ def save_records(records, chunk_size = DEFAULT_RECORDS_PER_BATCH_SAVE)
160
+ no_errors = true
161
+ return false unless records.all?(&:valid?)
162
+
163
+ actions = records.group_by {|o| o.new_record? ? create_method : :http_post }
164
+ actions.each_pair do |http_method, records_for_method|
165
+ records_for_method.each_slice(chunk_size) do |some_records|
166
+ request = to_bulk_xml(some_records)
167
+ response = parse_response(self.send(http_method, request, {:summarizeErrors => false}))
168
+ response.response_items.each_with_index do |record, i|
169
+ if record and record.is_a?(model_class)
170
+ some_records[i].attributes = record.non_calculated_attributes
171
+ some_records[i].errors = record.errors
172
+ no_errors = record.errors.nil? || record.errors.empty? if no_errors
173
+ some_records[i].saved!
159
174
  end
160
175
  end
161
176
  end
162
-
163
- no_errors
164
177
  end
165
178
 
166
- def batch_save(chunk_size = DEFAULT_RECORDS_PER_BATCH_SAVE)
167
- @objects = {}
168
- @allow_batch_operations = true
179
+ no_errors
180
+ end
169
181
 
170
- begin
171
- yield
182
+ def batch_save(chunk_size = DEFAULT_RECORDS_PER_BATCH_SAVE)
183
+ @objects = {}
184
+ @allow_batch_operations = true
172
185
 
173
- if @objects[model_class]
174
- objects = @objects[model_class].values.compact
175
- save_records(objects, chunk_size)
176
- end
177
- ensure
178
- @objects = {}
179
- @allow_batch_operations = false
186
+ begin
187
+ yield
188
+
189
+ if @objects[model_class]
190
+ objects = @objects[model_class].values.compact
191
+ save_records(objects, chunk_size)
180
192
  end
193
+ ensure
194
+ @objects = {}
195
+ @allow_batch_operations = false
181
196
  end
197
+ end
182
198
 
183
- def parse_response(response_xml, options = {})
184
- Response.parse(response_xml, options) do | response, elements, response_model_name |
185
- if model_name == response_model_name
186
- @response = response
187
- parse_records(response, elements, paged_records_requested?(options))
188
- end
199
+ def parse_response(response_xml, options = {})
200
+ Response.parse(response_xml, options) do | response, elements, response_model_name |
201
+ if model_name == response_model_name
202
+ @response = response
203
+ parse_records(response, elements, paged_records_requested?(options), (options[:base_module] || Xeroizer::Record))
189
204
  end
190
205
  end
206
+ end
191
207
 
192
- def create_method
193
- :http_put
194
- end
208
+ def create_method
209
+ :http_put
210
+ end
195
211
 
196
212
  protected
197
213
 
198
214
 
199
- def paged_records_requested?(options)
200
- options.has_key?(:page) and options[:page].to_i >= 0
201
- end
202
-
215
+ def paged_records_requested?(options)
216
+ options.has_key?(:page) and options[:page].to_i >= 0
217
+ end
203
218
 
204
219
  # Parse the records part of the XML response and builds model instances as necessary.
205
- def parse_records(response, elements, paged_results)
206
- elements.each do | element |
207
- new_record = model_class.build_from_node(element, self)
208
- if element.attribute('status').try(:value) == 'ERROR'
209
- new_record.errors = []
210
- element.xpath('.//ValidationError').each do |err|
211
- new_record.errors << err.text.gsub(/^\s+/, '').gsub(/\s+$/, '')
212
- end
220
+ def parse_records(response, elements, paged_results, base_module)
221
+ elements.each do | element |
222
+ new_record = model_class.build_from_node(element, self, base_module)
223
+ if element.attribute('status').try(:value) == 'ERROR'
224
+ new_record.errors = []
225
+ element.xpath('.//ValidationError').each do |err|
226
+ new_record.errors << err.text.gsub(/^\s+/, '').gsub(/\s+$/, '')
213
227
  end
214
- new_record.paged_record_downloaded = paged_results
215
- response.response_items << new_record
216
228
  end
229
+ new_record.paged_record_downloaded = paged_results
230
+ response.response_items << new_record
217
231
  end
232
+ end
218
233
 
219
- def to_bulk_xml(records, builder = Builder::XmlMarkup.new(:indent => 2))
220
- tag = (self.class.optional_xml_root_name || model_name).pluralize
221
- builder.tag!(tag) do
222
- records.each {|r| r.to_xml(builder) }
223
- end
234
+ def to_bulk_xml(records, builder = Builder::XmlMarkup.new(:indent => 2))
235
+ tag = (self.class.optional_xml_root_name || model_name).pluralize
236
+ builder.tag!(tag) do
237
+ records.each {|r| r.to_xml(builder) }
224
238
  end
239
+ end
225
240
 
226
- # Parse the response from a create/update request.
227
- def parse_save_response(response_xml)
228
- response = parse_response(response_xml)
229
- record = response.response_items.first if response.response_items.is_a?(Array)
230
- if record && record.is_a?(self.class)
231
- @attributes = record.attributes
232
- end
233
- self
241
+ # Parse the response from a create/update request.
242
+ def parse_save_response(response_xml)
243
+ response = parse_response(response_xml)
244
+ record = response.response_items.first if response.response_items.is_a?(Array)
245
+ if record && record.is_a?(self.class)
246
+ @attributes = record.attributes
234
247
  end
248
+ self
235
249
  end
250
+ end
236
251
 
237
252
  end
238
253
  end