xeroizer 2.16.5 → 2.17.1

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 (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