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.
Files changed (117) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +126 -185
  3. data/lib/xeroizer/connection.rb +49 -0
  4. data/lib/xeroizer/exceptions.rb +2 -0
  5. data/lib/xeroizer/generic_application.rb +8 -3
  6. data/lib/xeroizer/http.rb +5 -80
  7. data/lib/xeroizer/http_response.rb +154 -0
  8. data/lib/xeroizer/models/bank_transaction.rb +1 -0
  9. data/lib/xeroizer/models/batch_payment.rb +4 -1
  10. data/lib/xeroizer/models/contact.rb +10 -4
  11. data/lib/xeroizer/models/credit_note.rb +20 -20
  12. data/lib/xeroizer/models/history_record.rb +72 -0
  13. data/lib/xeroizer/models/invoice.rb +5 -1
  14. data/lib/xeroizer/models/line_item.rb +4 -2
  15. data/lib/xeroizer/models/manual_journal.rb +2 -1
  16. data/lib/xeroizer/models/option.rb +1 -1
  17. data/lib/xeroizer/models/payroll/address.rb +53 -0
  18. data/lib/xeroizer/models/payroll/bank_account.rb +18 -6
  19. data/lib/xeroizer/models/payroll/benefit_line.rb +26 -0
  20. data/lib/xeroizer/models/payroll/benefit_type.rb +45 -0
  21. data/lib/xeroizer/models/payroll/deduction_line.rb +32 -0
  22. data/lib/xeroizer/models/payroll/deduction_type.rb +49 -0
  23. data/lib/xeroizer/models/payroll/earnings_line.rb +39 -0
  24. data/lib/xeroizer/models/payroll/earnings_type.rb +53 -0
  25. data/lib/xeroizer/models/payroll/employee.rb +30 -8
  26. data/lib/xeroizer/models/payroll/leave_application.rb +27 -0
  27. data/lib/xeroizer/models/payroll/leave_line.rb +30 -0
  28. data/lib/xeroizer/models/payroll/leave_period.rb +15 -0
  29. data/lib/xeroizer/models/payroll/pay_items.rb +22 -0
  30. data/lib/xeroizer/models/payroll/pay_run.rb +33 -0
  31. data/lib/xeroizer/models/payroll/pay_schedule.rb +40 -0
  32. data/lib/xeroizer/models/payroll/pay_template.rb +24 -0
  33. data/lib/xeroizer/models/payroll/payment_method.rb +24 -0
  34. data/lib/xeroizer/models/payroll/paystub.rb +44 -0
  35. data/lib/xeroizer/models/payroll/reimbursement_line.rb +21 -0
  36. data/lib/xeroizer/models/payroll/reimbursement_type.rb +22 -0
  37. data/lib/xeroizer/models/payroll/salary_and_wage.rb +29 -0
  38. data/lib/xeroizer/models/payroll/super_line.rb +40 -0
  39. data/lib/xeroizer/models/payroll/tax_declaration.rb +50 -0
  40. data/lib/xeroizer/models/payroll/time_off_line.rb +20 -0
  41. data/lib/xeroizer/models/payroll/time_off_type.rb +32 -0
  42. data/lib/xeroizer/models/payroll/work_location.rb +25 -0
  43. data/lib/xeroizer/models/quote.rb +76 -0
  44. data/lib/xeroizer/models/tax_component.rb +1 -0
  45. data/lib/xeroizer/oauth.rb +12 -1
  46. data/lib/xeroizer/oauth2.rb +82 -0
  47. data/lib/xeroizer/oauth2_application.rb +49 -0
  48. data/lib/xeroizer/payroll_application.rb +8 -3
  49. data/lib/xeroizer/record/base_model.rb +1 -1
  50. data/lib/xeroizer/record/base_model_http_proxy.rb +1 -0
  51. data/lib/xeroizer/record/payroll_base.rb +4 -0
  52. data/lib/xeroizer/record/record_association_helper.rb +4 -4
  53. data/lib/xeroizer/record/validators/associated_validator.rb +1 -0
  54. data/lib/xeroizer/record/xml_helper.rb +16 -16
  55. data/lib/xeroizer/response.rb +22 -17
  56. data/lib/xeroizer/version.rb +1 -1
  57. data/lib/xeroizer.rb +31 -4
  58. data/test/acceptance/about_creating_bank_transactions_test.rb +80 -82
  59. data/test/acceptance/about_creating_prepayment_test.rb +25 -30
  60. data/test/acceptance/about_fetching_bank_transactions_test.rb +10 -10
  61. data/test/acceptance/about_online_invoice_test.rb +6 -10
  62. data/test/acceptance/acceptance_test.rb +28 -26
  63. data/test/acceptance/bank_transfer_test.rb +12 -17
  64. data/test/acceptance/bulk_operations_test.rb +18 -16
  65. data/test/acceptance/connections_test.rb +11 -0
  66. data/test/stub_responses/bad_request.json +6 -0
  67. data/test/stub_responses/connections.json +16 -0
  68. data/test/stub_responses/expired_oauth2_token.json +6 -0
  69. data/test/stub_responses/generic_response_error.json +6 -0
  70. data/test/stub_responses/invalid_oauth2_request_token.json +6 -0
  71. data/test/stub_responses/invalid_tenant_header.json +6 -0
  72. data/test/stub_responses/object_not_found.json +6 -0
  73. data/test/test_helper.rb +16 -11
  74. data/test/unit/generic_application_test.rb +21 -10
  75. data/test/unit/http_test.rb +281 -9
  76. data/test/unit/models/address_test.rb +2 -2
  77. data/test/unit/models/bank_transaction_model_parsing_test.rb +2 -2
  78. data/test/unit/models/bank_transaction_test.rb +1 -1
  79. data/test/unit/models/bank_transaction_validation_test.rb +1 -1
  80. data/test/unit/models/contact_test.rb +2 -2
  81. data/test/unit/models/credit_note_test.rb +8 -8
  82. data/test/unit/models/employee_test.rb +4 -4
  83. data/test/unit/models/invoice_test.rb +12 -12
  84. data/test/unit/models/journal_line_test.rb +6 -6
  85. data/test/unit/models/journal_test.rb +4 -4
  86. data/test/unit/models/line_item_sum_test.rb +1 -1
  87. data/test/unit/models/line_item_test.rb +19 -2
  88. data/test/unit/models/manual_journal_test.rb +3 -3
  89. data/test/unit/models/organisation_test.rb +2 -2
  90. data/test/unit/models/payment_service_test.rb +2 -2
  91. data/test/unit/models/phone_test.rb +7 -7
  92. data/test/unit/models/prepayment_test.rb +4 -4
  93. data/test/unit/models/repeating_invoice_test.rb +2 -2
  94. data/test/unit/models/tax_rate_test.rb +2 -2
  95. data/test/unit/oauth2_test.rb +171 -0
  96. data/test/unit/oauth_config_test.rb +1 -1
  97. data/test/unit/record/base_model_test.rb +13 -13
  98. data/test/unit/record/base_test.rb +5 -4
  99. data/test/unit/record/block_validator_test.rb +1 -1
  100. data/test/unit/record/connection_test.rb +60 -0
  101. data/test/unit/record/model_definition_test.rb +36 -36
  102. data/test/unit/record/parse_params_test.rb +2 -2
  103. data/test/unit/record/parse_where_hash_test.rb +13 -13
  104. data/test/unit/record/record_association_test.rb +14 -14
  105. data/test/unit/record/validators_test.rb +43 -43
  106. data/test/unit/record_definition_test.rb +7 -7
  107. data/test/unit/report_definition_test.rb +7 -7
  108. data/test/unit/report_test.rb +20 -20
  109. data/test/unit_test_helper.rb +16 -0
  110. metadata +106 -23
  111. data/lib/xeroizer/models/payroll/home_address.rb +0 -24
  112. data/lib/xeroizer/partner_application.rb +0 -51
  113. data/lib/xeroizer/private_application.rb +0 -25
  114. data/lib/xeroizer/public_application.rb +0 -21
  115. data/test/unit/http_tsl_12_upgrade_test.rb +0 -31
  116. data/test/unit/oauth_test.rb +0 -118
  117. 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, body, params = {})
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, body)
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) ? body : {:xml => 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
- case response.code.to_i
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
+
@@ -40,6 +40,7 @@ module Xeroizer
40
40
  string :status
41
41
  string :currency_code
42
42
  decimal :currency_rate
43
+ boolean :has_attachments
43
44
 
44
45
  alias_method :reconciled?, :is_reconciled
45
46
 
@@ -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, :due_date, :unless => :new_record?
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
@@ -11,7 +11,7 @@ module Xeroizer
11
11
 
12
12
  guid :tracking_option_id
13
13
  string :name
14
-
14
+ string :status
15
15
  end
16
16
 
17
17
  end
@@ -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