xeroizer 2.20.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +126 -185
- data/lib/xeroizer/connection.rb +49 -0
- data/lib/xeroizer/exceptions.rb +2 -0
- data/lib/xeroizer/generic_application.rb +8 -3
- data/lib/xeroizer/http.rb +5 -80
- data/lib/xeroizer/http_response.rb +154 -0
- data/lib/xeroizer/models/bank_transaction.rb +1 -0
- data/lib/xeroizer/models/batch_payment.rb +4 -1
- data/lib/xeroizer/models/contact.rb +10 -4
- data/lib/xeroizer/models/credit_note.rb +20 -20
- data/lib/xeroizer/models/history_record.rb +72 -0
- data/lib/xeroizer/models/invoice.rb +5 -1
- data/lib/xeroizer/models/line_item.rb +4 -2
- data/lib/xeroizer/models/manual_journal.rb +2 -1
- data/lib/xeroizer/models/option.rb +1 -1
- data/lib/xeroizer/models/payroll/address.rb +53 -0
- data/lib/xeroizer/models/payroll/bank_account.rb +18 -6
- data/lib/xeroizer/models/payroll/benefit_line.rb +26 -0
- data/lib/xeroizer/models/payroll/benefit_type.rb +45 -0
- data/lib/xeroizer/models/payroll/deduction_line.rb +32 -0
- data/lib/xeroizer/models/payroll/deduction_type.rb +49 -0
- data/lib/xeroizer/models/payroll/earnings_line.rb +39 -0
- data/lib/xeroizer/models/payroll/earnings_type.rb +53 -0
- data/lib/xeroizer/models/payroll/employee.rb +30 -8
- data/lib/xeroizer/models/payroll/leave_application.rb +27 -0
- data/lib/xeroizer/models/payroll/leave_line.rb +30 -0
- data/lib/xeroizer/models/payroll/leave_period.rb +15 -0
- data/lib/xeroizer/models/payroll/pay_items.rb +22 -0
- data/lib/xeroizer/models/payroll/pay_run.rb +33 -0
- data/lib/xeroizer/models/payroll/pay_schedule.rb +40 -0
- data/lib/xeroizer/models/payroll/pay_template.rb +24 -0
- data/lib/xeroizer/models/payroll/payment_method.rb +24 -0
- data/lib/xeroizer/models/payroll/paystub.rb +44 -0
- data/lib/xeroizer/models/payroll/reimbursement_line.rb +21 -0
- data/lib/xeroizer/models/payroll/reimbursement_type.rb +22 -0
- data/lib/xeroizer/models/payroll/salary_and_wage.rb +29 -0
- data/lib/xeroizer/models/payroll/super_line.rb +40 -0
- data/lib/xeroizer/models/payroll/tax_declaration.rb +50 -0
- data/lib/xeroizer/models/payroll/time_off_line.rb +20 -0
- data/lib/xeroizer/models/payroll/time_off_type.rb +32 -0
- data/lib/xeroizer/models/payroll/work_location.rb +25 -0
- data/lib/xeroizer/models/quote.rb +76 -0
- data/lib/xeroizer/models/tax_component.rb +1 -0
- data/lib/xeroizer/oauth.rb +12 -1
- data/lib/xeroizer/oauth2.rb +82 -0
- data/lib/xeroizer/oauth2_application.rb +49 -0
- data/lib/xeroizer/payroll_application.rb +8 -3
- data/lib/xeroizer/record/base_model.rb +1 -1
- data/lib/xeroizer/record/base_model_http_proxy.rb +1 -0
- data/lib/xeroizer/record/payroll_base.rb +4 -0
- data/lib/xeroizer/record/record_association_helper.rb +4 -4
- data/lib/xeroizer/record/validators/associated_validator.rb +1 -0
- data/lib/xeroizer/record/xml_helper.rb +16 -16
- data/lib/xeroizer/response.rb +22 -17
- data/lib/xeroizer/version.rb +1 -1
- data/lib/xeroizer.rb +31 -4
- data/test/acceptance/about_creating_bank_transactions_test.rb +80 -82
- data/test/acceptance/about_creating_prepayment_test.rb +25 -30
- data/test/acceptance/about_fetching_bank_transactions_test.rb +10 -10
- data/test/acceptance/about_online_invoice_test.rb +6 -10
- data/test/acceptance/acceptance_test.rb +28 -26
- data/test/acceptance/bank_transfer_test.rb +12 -17
- data/test/acceptance/bulk_operations_test.rb +18 -16
- data/test/acceptance/connections_test.rb +11 -0
- data/test/stub_responses/bad_request.json +6 -0
- data/test/stub_responses/connections.json +16 -0
- data/test/stub_responses/expired_oauth2_token.json +6 -0
- data/test/stub_responses/generic_response_error.json +6 -0
- data/test/stub_responses/invalid_oauth2_request_token.json +6 -0
- data/test/stub_responses/invalid_tenant_header.json +6 -0
- data/test/stub_responses/object_not_found.json +6 -0
- data/test/test_helper.rb +16 -11
- data/test/unit/generic_application_test.rb +21 -10
- data/test/unit/http_test.rb +281 -9
- data/test/unit/models/address_test.rb +2 -2
- data/test/unit/models/bank_transaction_model_parsing_test.rb +2 -2
- data/test/unit/models/bank_transaction_test.rb +1 -1
- data/test/unit/models/bank_transaction_validation_test.rb +1 -1
- data/test/unit/models/contact_test.rb +2 -2
- data/test/unit/models/credit_note_test.rb +8 -8
- data/test/unit/models/employee_test.rb +4 -4
- data/test/unit/models/invoice_test.rb +12 -12
- data/test/unit/models/journal_line_test.rb +6 -6
- data/test/unit/models/journal_test.rb +4 -4
- data/test/unit/models/line_item_sum_test.rb +1 -1
- data/test/unit/models/line_item_test.rb +19 -2
- data/test/unit/models/manual_journal_test.rb +3 -3
- data/test/unit/models/organisation_test.rb +2 -2
- data/test/unit/models/payment_service_test.rb +2 -2
- data/test/unit/models/phone_test.rb +7 -7
- data/test/unit/models/prepayment_test.rb +4 -4
- data/test/unit/models/repeating_invoice_test.rb +2 -2
- data/test/unit/models/tax_rate_test.rb +2 -2
- data/test/unit/oauth2_test.rb +171 -0
- data/test/unit/oauth_config_test.rb +1 -1
- data/test/unit/record/base_model_test.rb +13 -13
- data/test/unit/record/base_test.rb +5 -4
- data/test/unit/record/block_validator_test.rb +1 -1
- data/test/unit/record/connection_test.rb +60 -0
- data/test/unit/record/model_definition_test.rb +36 -36
- data/test/unit/record/parse_params_test.rb +2 -2
- data/test/unit/record/parse_where_hash_test.rb +13 -13
- data/test/unit/record/record_association_test.rb +14 -14
- data/test/unit/record/validators_test.rb +43 -43
- data/test/unit/record_definition_test.rb +7 -7
- data/test/unit/report_definition_test.rb +7 -7
- data/test/unit/report_test.rb +20 -20
- data/test/unit_test_helper.rb +16 -0
- metadata +106 -23
- data/lib/xeroizer/models/payroll/home_address.rb +0 -24
- data/lib/xeroizer/partner_application.rb +0 -51
- data/lib/xeroizer/private_application.rb +0 -25
- data/lib/xeroizer/public_application.rb +0 -21
- data/test/unit/http_tsl_12_upgrade_test.rb +0 -31
- data/test/unit/oauth_test.rb +0 -118
- data/test/unit/private_application_test.rb +0 -20
data/lib/xeroizer/http.rb
CHANGED
@@ -15,7 +15,7 @@
|
|
15
15
|
module Xeroizer
|
16
16
|
module Http
|
17
17
|
class BadResponse < XeroizerError; end
|
18
|
-
RequestInfo = Struct.new(:url, :headers, :params, :body)
|
18
|
+
RequestInfo = Struct.new(:url, :headers, :params, :body, :method)
|
19
19
|
|
20
20
|
ACCEPT_MIME_MAP = {
|
21
21
|
:pdf => 'application/pdf',
|
@@ -53,7 +53,7 @@ module Xeroizer
|
|
53
53
|
|
54
54
|
private
|
55
55
|
|
56
|
-
def http_request(client, method, url,
|
56
|
+
def http_request(client, method, url, request_body, params = {})
|
57
57
|
# headers = {'Accept-Encoding' => 'gzip, deflate'}
|
58
58
|
|
59
59
|
headers = self.default_headers.merge({ 'charset' => 'utf-8' })
|
@@ -89,14 +89,14 @@ module Xeroizer
|
|
89
89
|
|
90
90
|
attempts = 0
|
91
91
|
|
92
|
-
request_info = RequestInfo.new(url, headers, params,
|
92
|
+
request_info = RequestInfo.new(url, headers, params, request_body, method)
|
93
93
|
before_request.call(request_info) if before_request
|
94
94
|
|
95
95
|
begin
|
96
96
|
attempts += 1
|
97
97
|
logger.info("XeroGateway Request: #{method.to_s.upcase} #{uri.request_uri}") if self.logger
|
98
98
|
|
99
|
-
raw_body = params.delete(:raw_body) ?
|
99
|
+
raw_body = params.delete(:raw_body) ? request_body : {:xml => request_body}
|
100
100
|
|
101
101
|
response = with_around_request(request_info) do
|
102
102
|
case method
|
@@ -109,22 +109,7 @@ module Xeroizer
|
|
109
109
|
log_response(response, uri)
|
110
110
|
after_request.call(request_info, response) if after_request
|
111
111
|
|
112
|
-
|
113
|
-
when 200
|
114
|
-
response.plain_body
|
115
|
-
when 204
|
116
|
-
nil
|
117
|
-
when 400
|
118
|
-
handle_error!(response, body)
|
119
|
-
when 401
|
120
|
-
handle_oauth_error!(response)
|
121
|
-
when 404
|
122
|
-
handle_object_not_found!(response, url)
|
123
|
-
when 503
|
124
|
-
handle_oauth_error!(response)
|
125
|
-
else
|
126
|
-
handle_unknown_response_error!(response)
|
127
|
-
end
|
112
|
+
HttpResponse.from_response(response, request_body, url).body
|
128
113
|
rescue Xeroizer::OAuth::NonceUsed => exception
|
129
114
|
raise if attempts > nonce_used_max_attempts
|
130
115
|
logger.info("Nonce used: " + exception.to_s) if self.logger
|
@@ -159,66 +144,6 @@ module Xeroizer
|
|
159
144
|
end
|
160
145
|
end
|
161
146
|
|
162
|
-
def handle_oauth_error!(response)
|
163
|
-
error_details = CGI.parse(response.plain_body)
|
164
|
-
description = error_details["oauth_problem_advice"].first
|
165
|
-
problem = error_details["oauth_problem"].first
|
166
|
-
|
167
|
-
# see http://oauth.pbworks.com/ProblemReporting
|
168
|
-
# In addition to token_expired and token_rejected, Xero also returns
|
169
|
-
# 'rate limit exceeded' when more than 60 requests have been made in
|
170
|
-
# a second.
|
171
|
-
if problem
|
172
|
-
case problem
|
173
|
-
when "token_expired" then raise OAuth::TokenExpired.new(description)
|
174
|
-
when "token_rejected" then raise OAuth::TokenInvalid.new(description)
|
175
|
-
when "rate limit exceeded" then raise OAuth::RateLimitExceeded.new(description)
|
176
|
-
when "consumer_key_unknown" then raise OAuth::ConsumerKeyUnknown.new(description)
|
177
|
-
when "nonce_used" then raise OAuth::NonceUsed.new(description)
|
178
|
-
when "organisation offline" then raise OAuth::OrganisationOffline.new(description)
|
179
|
-
else raise OAuth::UnknownError.new(problem + ':' + description)
|
180
|
-
end
|
181
|
-
else
|
182
|
-
raise OAuth::UnknownError.new("Xero API may be down or the way OAuth errors are provided by Xero may have changed.")
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
def handle_error!(response, request_body)
|
187
|
-
|
188
|
-
raw_response = response.plain_body
|
189
|
-
|
190
|
-
# XeroGenericApplication API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
|
191
|
-
# So let's ignore that :)
|
192
|
-
raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
|
193
|
-
|
194
|
-
# doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
|
195
|
-
doc = Nokogiri::XML(raw_response)
|
196
|
-
|
197
|
-
if doc && doc.root && doc.root.name == "ApiException"
|
198
|
-
|
199
|
-
raise ApiException.new(doc.root.xpath("Type").text,
|
200
|
-
doc.root.xpath("Message").text,
|
201
|
-
raw_response,
|
202
|
-
doc,
|
203
|
-
request_body)
|
204
|
-
|
205
|
-
else
|
206
|
-
raise BadResponse.new("Unparseable 400 Response: #{raw_response}")
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
def handle_object_not_found!(response, request_url)
|
211
|
-
case request_url
|
212
|
-
when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.")
|
213
|
-
when /CreditNotes/ then raise CreditNoteNotFoundError.new("Credit Note not found in Xero.")
|
214
|
-
else raise ObjectNotFound.new(request_url)
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
def handle_unknown_response_error!(response)
|
219
|
-
raise BadResponse.new("Unknown response code: #{response.code.to_i}")
|
220
|
-
end
|
221
|
-
|
222
147
|
def sleep_for(seconds = 1)
|
223
148
|
sleep seconds
|
224
149
|
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module Xeroizer
|
2
|
+
class BadResponse < XeroizerError; end
|
3
|
+
|
4
|
+
class XmlErrorResponse
|
5
|
+
def initialize(response, request_body, url)
|
6
|
+
@response = response
|
7
|
+
@request_body = request_body
|
8
|
+
@url = url
|
9
|
+
end
|
10
|
+
|
11
|
+
def raise_error!
|
12
|
+
case response.code.to_i
|
13
|
+
when 400
|
14
|
+
raise_bad_request!
|
15
|
+
when 401
|
16
|
+
raise_error
|
17
|
+
when 403
|
18
|
+
raise_error
|
19
|
+
when 404
|
20
|
+
raise_not_found!
|
21
|
+
when 429
|
22
|
+
raise_rate_limit_exceeded!
|
23
|
+
when 503
|
24
|
+
raise_error
|
25
|
+
else
|
26
|
+
raise_unknown_response_error!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def raise_error
|
31
|
+
description, problem = parse
|
32
|
+
|
33
|
+
# see http://oauth.pbworks.com/ProblemReporting
|
34
|
+
# In addition to token_expired and token_rejected, Xero also returns
|
35
|
+
# 'rate limit exceeded' when more than 60 requests have been made in
|
36
|
+
# a second.
|
37
|
+
if problem
|
38
|
+
case problem
|
39
|
+
when "token_expired" then raise OAuth::TokenExpired.new(description)
|
40
|
+
when "token_rejected" then raise OAuth::TokenInvalid.new(description)
|
41
|
+
when "rate limit exceeded" then raise OAuth::RateLimitExceeded.new(description)
|
42
|
+
when "consumer_key_unknown" then raise OAuth::ConsumerKeyUnknown.new(description)
|
43
|
+
when "nonce_used" then raise OAuth::NonceUsed.new(description)
|
44
|
+
when "organisation offline" then raise OAuth::OrganisationOffline.new(description)
|
45
|
+
else raise OAuth::UnknownError.new(problem + ':' + description)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
raise OAuth::UnknownError.new("Xero API may be down or the way OAuth errors are provided by Xero may have changed.")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_reader :request_body, :response, :url
|
55
|
+
|
56
|
+
def parse
|
57
|
+
error_details = CGI.parse(response.plain_body)
|
58
|
+
description = error_details["oauth_problem_advice"].first
|
59
|
+
problem = error_details["oauth_problem"].first
|
60
|
+
[description, problem]
|
61
|
+
end
|
62
|
+
|
63
|
+
def raise_bad_request!
|
64
|
+
|
65
|
+
raw_response = response.plain_body
|
66
|
+
|
67
|
+
# XeroGenericApplication API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
|
68
|
+
# So let's ignore that :)
|
69
|
+
raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
|
70
|
+
|
71
|
+
# doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
|
72
|
+
doc = Nokogiri::XML(raw_response)
|
73
|
+
|
74
|
+
if doc && doc.root && (doc.root.name == "ApiException" || doc.root.name == 'Response')
|
75
|
+
|
76
|
+
raise ApiException.new(doc.root.xpath("Type").text,
|
77
|
+
doc.root.xpath("Message").text,
|
78
|
+
raw_response,
|
79
|
+
doc,
|
80
|
+
request_body)
|
81
|
+
|
82
|
+
else
|
83
|
+
raise Xeroizer::BadResponse.new("Unparseable 400 Response: #{raw_response}")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def raise_not_found!
|
88
|
+
case url
|
89
|
+
when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.")
|
90
|
+
when /CreditNotes/ then raise CreditNoteNotFoundError.new("Credit Note not found in Xero.")
|
91
|
+
else raise ObjectNotFound.new(url)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def raise_rate_limit_exceeded!
|
96
|
+
retry_after = response.response.headers["retry-after"].to_i
|
97
|
+
daily_limit_remaining = response.response.headers["x-daylimit-remaining"].to_i
|
98
|
+
|
99
|
+
description = "Rate limit exceeded: #{daily_limit_remaining} requests left for the day, #{retry_after} seconds until you can make another request"
|
100
|
+
raise OAuth::RateLimitExceeded.new(description, retry_after: retry_after, daily_limit_remaining: daily_limit_remaining)
|
101
|
+
end
|
102
|
+
|
103
|
+
def raise_unknown_response_error!
|
104
|
+
raise Xeroizer::BadResponse.new("Unknown response code: #{response.code.to_i}")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class HttpResponse
|
109
|
+
def self.from_response(response, request_body, url)
|
110
|
+
new(response, request_body, url)
|
111
|
+
end
|
112
|
+
|
113
|
+
def initialize(response, request_body, url)
|
114
|
+
@response = response
|
115
|
+
@request_body = request_body
|
116
|
+
@url = url
|
117
|
+
end
|
118
|
+
|
119
|
+
def body
|
120
|
+
response_code = response.code.to_i
|
121
|
+
return nil if response_code == 204
|
122
|
+
raise_error! unless response.code.to_i == 200
|
123
|
+
response.plain_body
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def raise_error!
|
129
|
+
begin
|
130
|
+
error_details = JSON.parse(response.plain_body)
|
131
|
+
description = error_details["Detail"]
|
132
|
+
case response.code.to_i
|
133
|
+
when 400
|
134
|
+
raise Xeroizer::BadResponse.new(description)
|
135
|
+
when 401
|
136
|
+
raise OAuth::TokenExpired.new(description) if description.include?("TokenExpired")
|
137
|
+
raise OAuth::TokenInvalid.new(description)
|
138
|
+
when 403
|
139
|
+
message = "Possible xero-tenant-id header issue. Xero Error: #{description}"
|
140
|
+
raise OAuth::Forbidden.new(message)
|
141
|
+
when 404
|
142
|
+
raise Xeroizer::ObjectNotFound.new(url)
|
143
|
+
else
|
144
|
+
raise Xeroizer::OAuth::UnknownError.new(description)
|
145
|
+
end
|
146
|
+
rescue JSON::ParserError
|
147
|
+
XmlErrorResponse.new(response, request_body, url).raise_error!
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
attr_reader :request_body, :response, :url
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
@@ -2,10 +2,13 @@ module Xeroizer
|
|
2
2
|
module Record
|
3
3
|
|
4
4
|
class BatchPaymentModel < BaseModel
|
5
|
-
set_permissions :read
|
5
|
+
set_permissions :read, :write
|
6
6
|
end
|
7
7
|
|
8
8
|
class BatchPayment < Base
|
9
|
+
set_primary_key :batch_payment_id
|
10
|
+
list_contains_summary_only false
|
11
|
+
|
9
12
|
guid :batch_payment_id
|
10
13
|
string :reference
|
11
14
|
string :details
|
@@ -2,6 +2,7 @@ require "xeroizer/models/contact_person"
|
|
2
2
|
require "xeroizer/models/balances"
|
3
3
|
require "xeroizer/models/batch_payments"
|
4
4
|
require "xeroizer/models/payment_terms"
|
5
|
+
require "xeroizer/models/history_record"
|
5
6
|
|
6
7
|
module Xeroizer
|
7
8
|
module Record
|
@@ -11,6 +12,7 @@ module Xeroizer
|
|
11
12
|
set_permissions :read, :write, :update
|
12
13
|
|
13
14
|
include AttachmentModel::Extensions
|
15
|
+
include HistoryRecordModel::Extensions
|
14
16
|
|
15
17
|
end
|
16
18
|
|
@@ -19,10 +21,12 @@ module Xeroizer
|
|
19
21
|
CONTACT_STATUS = {
|
20
22
|
'ACTIVE' => 'Active',
|
21
23
|
'DELETED' => 'Deleted',
|
22
|
-
'ARCHIVED' => 'Archived'
|
24
|
+
'ARCHIVED' => 'Archived',
|
25
|
+
'GDPRREQUEST' => 'GDPR Request'
|
23
26
|
} unless defined?(CONTACT_STATUS)
|
24
27
|
|
25
28
|
include Attachment::Extensions
|
29
|
+
include HistoryRecord::Extensions
|
26
30
|
|
27
31
|
set_primary_key :contact_id
|
28
32
|
set_possible_primary_keys :contact_id, :contact_number
|
@@ -50,18 +54,20 @@ module Xeroizer
|
|
50
54
|
string :website # read only
|
51
55
|
decimal :discount # read only
|
52
56
|
boolean :has_attachments
|
57
|
+
string :xero_network_key
|
53
58
|
|
54
59
|
has_many :addresses, :list_complete => true
|
55
60
|
has_many :phones, :list_complete => true
|
56
61
|
has_many :contact_groups, :list_complete => true
|
57
|
-
has_many :contact_persons, :internal_name => :contact_people
|
62
|
+
has_many :contact_persons, :internal_name => :contact_people, :list_complete => true
|
58
63
|
|
59
|
-
has_many :sales_tracking_categories, :model_name => 'ContactSalesTrackingCategory'
|
60
|
-
has_many :purchases_tracking_categories, :model_name => 'ContactPurchasesTrackingCategory'
|
64
|
+
has_many :sales_tracking_categories, :model_name => 'ContactSalesTrackingCategory', :list_complete => true
|
65
|
+
has_many :purchases_tracking_categories, :model_name => 'ContactPurchasesTrackingCategory', :list_complete => true
|
61
66
|
|
62
67
|
has_one :balances, :model_name => 'Balances', :list_complete => true
|
63
68
|
has_one :batch_payments, :model_name => 'BatchPayments', :list_complete => true
|
64
69
|
has_one :payment_terms, :model_name => 'PaymentTerms', :list_complete => true
|
70
|
+
has_one :branding_theme, :list_complete => true
|
65
71
|
|
66
72
|
validates_presence_of :name, :unless => Proc.new { | contact | contact.contact_id.present? || contact.contact_number.present? }
|
67
73
|
validates_inclusion_of :contact_status, :in => CONTACT_STATUS.keys, :allow_blanks => true
|
@@ -1,12 +1,12 @@
|
|
1
1
|
module Xeroizer
|
2
2
|
module Record
|
3
|
-
|
3
|
+
|
4
4
|
class CreditNoteModel < BaseModel
|
5
|
-
|
5
|
+
|
6
6
|
set_permissions :read, :write, :update
|
7
|
-
|
7
|
+
|
8
8
|
include AttachmentModel::Extensions
|
9
|
-
|
9
|
+
|
10
10
|
public
|
11
11
|
|
12
12
|
# Retrieve the PDF version of the credit matching the `id`.
|
@@ -21,11 +21,11 @@ module Xeroizer
|
|
21
21
|
pdf_data
|
22
22
|
end
|
23
23
|
end
|
24
|
-
|
24
|
+
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
class CreditNote < Base
|
28
|
-
|
28
|
+
|
29
29
|
CREDIT_NOTE_STATUS = {
|
30
30
|
'AUTHORISED' => 'Approved credit_notes awaiting payment',
|
31
31
|
'DELETED' => 'Draft credit_notes that are deleted',
|
@@ -35,7 +35,7 @@ module Xeroizer
|
|
35
35
|
'VOIDED' => 'Approved credit_notes that are voided'
|
36
36
|
} unless defined?(CREDIT_NOTE_STATUS)
|
37
37
|
CREDIT_NOTE_STATUSES = CREDIT_NOTE_STATUS.keys.sort
|
38
|
-
|
38
|
+
|
39
39
|
CREDIT_NOTE_TYPE = {
|
40
40
|
'ACCRECCREDIT' => 'Accounts Receivable',
|
41
41
|
'ACCPAYCREDIT' => 'Accounts Payable'
|
@@ -43,11 +43,11 @@ module Xeroizer
|
|
43
43
|
CREDIT_NOTE_TYPES = CREDIT_NOTE_TYPE.keys.sort
|
44
44
|
|
45
45
|
include Attachment::Extensions
|
46
|
-
|
46
|
+
|
47
47
|
set_primary_key :credit_note_id
|
48
48
|
set_possible_primary_keys :credit_note_id, :credit_note_number
|
49
49
|
list_contains_summary_only true
|
50
|
-
|
50
|
+
|
51
51
|
guid :credit_note_id
|
52
52
|
string :credit_note_number
|
53
53
|
string :reference
|
@@ -72,15 +72,15 @@ module Xeroizer
|
|
72
72
|
belongs_to :contact
|
73
73
|
has_many :line_items
|
74
74
|
has_many :allocations
|
75
|
-
|
75
|
+
|
76
76
|
validates_inclusion_of :type, :in => CREDIT_NOTE_TYPES
|
77
77
|
validates_inclusion_of :status, :in => CREDIT_NOTE_STATUSES, :allow_blanks => true
|
78
78
|
validates_associated :contact
|
79
79
|
validates_associated :line_items
|
80
80
|
validates_associated :allocations, :allow_blanks => true
|
81
|
-
|
81
|
+
|
82
82
|
public
|
83
|
-
|
83
|
+
|
84
84
|
# Access the contact name without forcing a download of
|
85
85
|
# an incomplete, summary credit note.
|
86
86
|
def contact_name
|
@@ -91,20 +91,20 @@ module Xeroizer
|
|
91
91
|
# incomplete, summary credit note.
|
92
92
|
def contact_id
|
93
93
|
attributes[:contact] && attributes[:contact][:contact_id]
|
94
|
-
end
|
95
|
-
|
94
|
+
end
|
95
|
+
|
96
96
|
# Swallow assignment of attributes that should only be calculated automatically.
|
97
97
|
def sub_total=(value); raise SettingTotalDirectlyNotSupported.new(:sub_total); end
|
98
98
|
def total_tax=(value); raise SettingTotalDirectlyNotSupported.new(:total_tax); end
|
99
99
|
def total=(value); raise SettingTotalDirectlyNotSupported.new(:total); end
|
100
|
-
|
100
|
+
|
101
101
|
# Calculate sub_total from line_items.
|
102
102
|
def sub_total(always_summary = false)
|
103
103
|
if !always_summary && (new_record? || (!new_record? && line_items && line_items.size > 0))
|
104
104
|
overall_sum = (line_items || []).inject(BigDecimal('0')) { | sum, line_item | sum + line_item.line_amount }
|
105
|
-
|
105
|
+
|
106
106
|
# If the default amount types are inclusive of 'tax' then remove the tax amount from this sub-total.
|
107
|
-
overall_sum -= total_tax if line_amount_types == 'Inclusive'
|
107
|
+
overall_sum -= total_tax if line_amount_types == 'Inclusive'
|
108
108
|
overall_sum
|
109
109
|
else
|
110
110
|
attributes[:sub_total]
|
@@ -128,14 +128,14 @@ module Xeroizer
|
|
128
128
|
attributes[:total]
|
129
129
|
end
|
130
130
|
end
|
131
|
-
|
131
|
+
|
132
132
|
# Retrieve the PDF version of this credit note.
|
133
133
|
# @param [String] filename optional filename to store the PDF in instead of returning the data.
|
134
134
|
def pdf(filename = nil)
|
135
135
|
parent.pdf(id, filename)
|
136
136
|
end
|
137
137
|
|
138
|
-
def save
|
138
|
+
def save!
|
139
139
|
# Calling parse_save_response() on the credit note will wipe out
|
140
140
|
# the allocations, so we have to manually preserve them.
|
141
141
|
allocations_backup = self.allocations
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Xeroizer
|
2
|
+
module Record
|
3
|
+
|
4
|
+
class HistoryRecordModel < BaseModel
|
5
|
+
|
6
|
+
module Extensions
|
7
|
+
def history(id)
|
8
|
+
application.HistoryRecord.history(url, id)
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_note(id, details)
|
12
|
+
application.HistoryRecord.add_note(url, id, details)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
set_permissions :read
|
17
|
+
|
18
|
+
# History Records can only be added, no update or delete is possible
|
19
|
+
def create_method
|
20
|
+
:http_put
|
21
|
+
end
|
22
|
+
|
23
|
+
def history(url, id)
|
24
|
+
response_xml = @application.http_get(@application.client, "#{url}/#{CGI.escape(id)}/history")
|
25
|
+
|
26
|
+
response = parse_response(response_xml)
|
27
|
+
|
28
|
+
response.response_items
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_note(url, id, details)
|
32
|
+
record = build(details: details)
|
33
|
+
xml = to_bulk_xml([record])
|
34
|
+
response_xml = @application.http_put(@application.client,
|
35
|
+
"#{url}/#{CGI.escape(id)}/history",
|
36
|
+
xml,
|
37
|
+
raw_body: true
|
38
|
+
)
|
39
|
+
response = parse_response(response_xml)
|
40
|
+
if (response_items = response.response_items) && response_items.size > 0
|
41
|
+
response_items.size == 1 ? response_items.first : response_items
|
42
|
+
else
|
43
|
+
response
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class HistoryRecord < Base
|
50
|
+
|
51
|
+
module Extensions
|
52
|
+
def history
|
53
|
+
parent.history(id)
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_note(details)
|
57
|
+
parent.add_note(id, details)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
datetime_utc :date_utc, :api_name => 'DateUTC'
|
62
|
+
string :date_utc_string, :api_name => 'DateUTCString'
|
63
|
+
string :changes
|
64
|
+
string :user
|
65
|
+
string :details
|
66
|
+
|
67
|
+
validates_presence_of :details
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require "xeroizer/models/attachment"
|
2
2
|
require "xeroizer/models/online_invoice"
|
3
|
+
require "xeroizer/models/history_record"
|
3
4
|
|
4
5
|
module Xeroizer
|
5
6
|
module Record
|
@@ -16,6 +17,7 @@ module Xeroizer
|
|
16
17
|
|
17
18
|
include AttachmentModel::Extensions
|
18
19
|
include OnlineInvoiceModel::Extensions
|
20
|
+
include HistoryRecordModel::Extensions
|
19
21
|
|
20
22
|
public
|
21
23
|
|
@@ -54,6 +56,7 @@ module Xeroizer
|
|
54
56
|
|
55
57
|
include Attachment::Extensions
|
56
58
|
include OnlineInvoice::Extensions
|
59
|
+
include HistoryRecord::Extensions
|
57
60
|
|
58
61
|
set_primary_key :invoice_id
|
59
62
|
set_possible_primary_keys :invoice_id, :invoice_number
|
@@ -91,7 +94,8 @@ module Xeroizer
|
|
91
94
|
has_many :credit_notes
|
92
95
|
has_many :prepayments
|
93
96
|
|
94
|
-
validates_presence_of :date, :
|
97
|
+
validates_presence_of :date, :if => :new_record?
|
98
|
+
validates_presence_of :due_date, :if => :approved?
|
95
99
|
validates_inclusion_of :type, :in => INVOICE_TYPES
|
96
100
|
validates_inclusion_of :status, :in => INVOICE_STATUSES, :unless => :new_record?
|
97
101
|
validates_inclusion_of :line_amount_types, :in => LINE_AMOUNT_TYPES, :unless => :new_record?
|
@@ -4,7 +4,7 @@ require 'xeroizer/models/line_amount_type'
|
|
4
4
|
module Xeroizer
|
5
5
|
module Record
|
6
6
|
class LineItemModel < BaseModel
|
7
|
-
|
7
|
+
set_permissions
|
8
8
|
end
|
9
9
|
|
10
10
|
class LineItem < Base
|
@@ -24,6 +24,8 @@ module Xeroizer
|
|
24
24
|
|
25
25
|
has_many :tracking, :model_name => 'TrackingCategoryChild'
|
26
26
|
|
27
|
+
validates_presence_of :description, :unless => Proc.new { |line_item| line_item.item_code.present? }
|
28
|
+
|
27
29
|
def initialize(parent)
|
28
30
|
super(parent)
|
29
31
|
@line_amount_set = false
|
@@ -42,7 +44,7 @@ module Xeroizer
|
|
42
44
|
if quantity && unit_amount
|
43
45
|
total = coerce_numeric(quantity) * coerce_numeric(unit_amount)
|
44
46
|
if discount_rate.nonzero?
|
45
|
-
BigDecimal((total * ((100 - discount_rate) / 100)).to_s).round(2)
|
47
|
+
BigDecimal((total * ((100 - discount_rate.to_f) / 100)).to_s).round(2)
|
46
48
|
elsif discount_amount
|
47
49
|
BigDecimal((total - discount_amount).to_s).round(2)
|
48
50
|
else
|
@@ -34,7 +34,8 @@ module Xeroizer
|
|
34
34
|
string :external_link_provider_name # only seems to be read-only at the moment
|
35
35
|
boolean :show_on_cash_basis_reports
|
36
36
|
datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
|
37
|
-
|
37
|
+
boolean :has_attachments
|
38
|
+
|
38
39
|
has_many :journal_lines, :model_name => 'ManualJournalLine', :complete_on_page => true
|
39
40
|
|
40
41
|
validates_presence_of :narration
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Xeroizer
|
2
|
+
module Record
|
3
|
+
module Payroll
|
4
|
+
|
5
|
+
class AddressModel < PayrollBaseModel
|
6
|
+
|
7
|
+
class_inheritable_attributes :api_controller_name
|
8
|
+
class_inheritable_attributes :permissions
|
9
|
+
class_inheritable_attributes :xml_root_name
|
10
|
+
class_inheritable_attributes :optional_xml_root_name
|
11
|
+
class_inheritable_attributes :xml_node_name
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
class Address < PayrollBase
|
16
|
+
|
17
|
+
class_inheritable_attributes :fields, :possible_primary_keys, :primary_key_name, :summary_only, :validators
|
18
|
+
|
19
|
+
|
20
|
+
string :address_line1
|
21
|
+
string :address_line2
|
22
|
+
string :city
|
23
|
+
string :region
|
24
|
+
string :postal_code
|
25
|
+
string :country
|
26
|
+
|
27
|
+
# US Payroll fields
|
28
|
+
string :street_address
|
29
|
+
string :suite_or_apt_or_unit
|
30
|
+
string :state
|
31
|
+
string :zip
|
32
|
+
decimal :latitude
|
33
|
+
decimal :longitude
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
class HomeAddressModel < AddressModel
|
38
|
+
set_xml_node_name 'HomeAddress'
|
39
|
+
end
|
40
|
+
|
41
|
+
class HomeAddress < Address
|
42
|
+
end
|
43
|
+
|
44
|
+
class MailingAddressModel < AddressModel
|
45
|
+
set_xml_node_name 'MailingAddress'
|
46
|
+
end
|
47
|
+
|
48
|
+
class MailingAddress < Address
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|