xeroizer 2.20.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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