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.
- checksums.yaml +4 -4
- data/README.md +21 -27
- data/lib/class_level_inheritable_attributes.rb +3 -0
- data/lib/xeroizer.rb +13 -1
- data/lib/xeroizer/exceptions.rb +1 -1
- data/lib/xeroizer/generic_application.rb +13 -1
- data/lib/xeroizer/http.rb +128 -120
- data/lib/xeroizer/models/account.rb +1 -0
- data/lib/xeroizer/models/address.rb +18 -9
- data/lib/xeroizer/models/attachment.rb +1 -1
- data/lib/xeroizer/models/balances.rb +1 -0
- data/lib/xeroizer/models/bank_transaction.rb +2 -2
- data/lib/xeroizer/models/bank_transfer.rb +28 -0
- data/lib/xeroizer/models/contact.rb +11 -4
- data/lib/xeroizer/models/credit_note.rb +9 -4
- data/lib/xeroizer/models/from_bank_account.rb +12 -0
- data/lib/xeroizer/models/invoice.rb +11 -3
- data/lib/xeroizer/models/invoice_reminder.rb +19 -0
- data/lib/xeroizer/models/line_item.rb +21 -9
- data/lib/xeroizer/models/manual_journal.rb +6 -0
- data/lib/xeroizer/models/organisation.rb +4 -0
- data/lib/xeroizer/models/overpayment.rb +40 -0
- data/lib/xeroizer/models/payroll/bank_account.rb +23 -0
- data/lib/xeroizer/models/payroll/employee.rb +45 -0
- data/lib/xeroizer/models/payroll/home_address.rb +24 -0
- data/lib/xeroizer/models/prepayment.rb +1 -0
- data/lib/xeroizer/models/tax_rate.rb +5 -4
- data/lib/xeroizer/models/to_bank_account.rb +12 -0
- data/lib/xeroizer/oauth.rb +8 -8
- data/lib/xeroizer/partner_application.rb +4 -8
- data/lib/xeroizer/payroll_application.rb +28 -0
- data/lib/xeroizer/private_application.rb +2 -4
- data/lib/xeroizer/record/base_model.rb +137 -122
- data/lib/xeroizer/record/base_model_http_proxy.rb +1 -1
- data/lib/xeroizer/record/payroll_base.rb +25 -0
- data/lib/xeroizer/record/payroll_base_model.rb +29 -0
- data/lib/xeroizer/record/record_association_helper.rb +13 -14
- data/lib/xeroizer/record/validation_helper.rb +1 -1
- data/lib/xeroizer/record/xml_helper.rb +10 -8
- data/lib/xeroizer/report/aged_receivables_by_contact.rb +0 -1
- data/lib/xeroizer/report/base.rb +0 -1
- data/lib/xeroizer/report/factory.rb +3 -3
- data/lib/xeroizer/report/row/header.rb +4 -4
- data/lib/xeroizer/report/row/xml_helper.rb +2 -2
- data/lib/xeroizer/version.rb +1 -1
- data/test/acceptance/about_creating_prepayment_test.rb +46 -0
- data/test/acceptance/about_fetching_bank_transactions_test.rb +21 -21
- data/test/acceptance/acceptance_test.rb +4 -4
- data/test/acceptance/bank_transaction_reference_data.rb +3 -1
- data/test/acceptance/bank_transfer_test.rb +26 -0
- data/test/test_helper.rb +6 -4
- data/test/unit/models/address_test.rb +92 -0
- data/test/unit/models/bank_transaction_validation_test.rb +1 -1
- data/test/unit/models/contact_test.rb +20 -4
- data/test/unit/models/line_item_test.rb +20 -6
- data/test/unit/models/tax_rate_test.rb +52 -1
- data/test/unit/record/base_model_test.rb +1 -1
- data/test/unit/record/base_test.rb +9 -6
- data/test/unit/record/model_definition_test.rb +2 -2
- data/test/unit/report_test.rb +19 -21
- 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
|
@@ -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
|
data/lib/xeroizer/oauth.rb
CHANGED
@@ -23,13 +23,14 @@ module Xeroizer
|
|
23
23
|
# http://github.com/jnunemaker/twitter/
|
24
24
|
|
25
25
|
class OAuth
|
26
|
-
|
27
|
-
class TokenExpired <
|
28
|
-
class TokenInvalid <
|
29
|
-
class RateLimitExceeded <
|
30
|
-
class ConsumerKeyUnknown <
|
31
|
-
class NonceUsed <
|
32
|
-
class
|
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,
|
18
|
+
def initialize(consumer_key, consumer_secret, path_to_private_key, options = {})
|
21
19
|
default_options = {
|
22
|
-
:xero_url => 'https://api
|
23
|
-
:site => 'https://api
|
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
|
19
|
-
|
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 <
|
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
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
90
|
+
def model_class
|
91
|
+
@model_class ||= Xeroizer::Record.const_get(model_name.to_sym)
|
92
|
+
end
|
91
93
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
167
|
-
|
168
|
-
@allow_batch_operations = true
|
179
|
+
no_errors
|
180
|
+
end
|
169
181
|
|
170
|
-
|
171
|
-
|
182
|
+
def batch_save(chunk_size = DEFAULT_RECORDS_PER_BATCH_SAVE)
|
183
|
+
@objects = {}
|
184
|
+
@allow_batch_operations = true
|
172
185
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
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
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
208
|
+
def create_method
|
209
|
+
:http_put
|
210
|
+
end
|
195
211
|
|
196
212
|
protected
|
197
213
|
|
198
214
|
|
199
|
-
|
200
|
-
|
201
|
-
|
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
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|