xeroizer 2.20.0 → 3.0.0
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 +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
|