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
@@ -0,0 +1,29 @@
1
+ module Xeroizer
2
+ module Record
3
+ module Payroll
4
+
5
+ class SalaryAndWageModel < PayrollBaseModel
6
+
7
+ end
8
+
9
+ class SalaryAndWage < PayrollBase
10
+
11
+ SALARY_AND_WAGE_TYPE = {
12
+ 'HOURLY' => '',
13
+ 'SALARY' => ''
14
+ } unless defined?(SALARY_AND_WAGE_TYPE)
15
+
16
+ guid :salary_and_wages_id
17
+ guid :earnings_type_id
18
+ string :salary_wages_type
19
+ decimal :hourly_rate
20
+ decimal :annual_salary
21
+ decimal :standard_hours_per_week
22
+ datetime :effective_date
23
+
24
+ validates_presence_of :salary_and_wage_id, :unless => :new_record?
25
+ validates_inclusion_of :salary_wages_type, :in => SALARY_AND_WAGE_TYPE
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,40 @@
1
+ module Xeroizer
2
+ module Record
3
+ module Payroll
4
+
5
+ class SuperLineModel < PayrollBaseModel
6
+
7
+ end
8
+
9
+ class SuperLine < PayrollBase
10
+
11
+ SUPERANNUATION_CONTRIBUTION_TYPE = {
12
+ 'SGC' => 'Mandatory 9% contribution',
13
+ 'SALARYSACRIFICE' => 'Pre-tax reportable employer superannuation contribution, which is displayed separately on payment summaries',
14
+ 'EMPLOYERADDITIONAL' => 'Additional employer superannuation contribution, which is displayed as RESC on payment summaries',
15
+ 'EMPLOYEE' => 'Post-tax employee superannuation contribution'
16
+ } unless defined?(SUPERANNUATION_CONTRIBUTION_TYPE)
17
+
18
+ SUPERANNUATION_CALCULATION_TYPE = {
19
+ 'FIXEDAMOUNT' => 'For voluntary superannuation, the contribution amount can be a fixed rate or a percentage of earnings. For SGC contributions it must be a percentage',
20
+ 'PERCENTAGEOFEARNINGS' => '',
21
+ 'STATUTORY' => ''
22
+ } unless defined?(SUPERANNUATION_CALCULATION_TYPE)
23
+
24
+ guid :super_membership_id, :api_name => 'SuperMembershipID'
25
+ string :contribution_type
26
+ string :calculation_type
27
+ integer :expense_account_code
28
+ integer :liability_account_code
29
+
30
+ decimal :minimum_monthly_earnings
31
+ decimal :percentage
32
+
33
+ validates_presence_of :super_membership_id, :contribution_type, :calculation_type, :expense_account_code, :liability_account_code, :unless => :new_record?
34
+ validates_inclusion_of :contribution_type, :in => SUPERANNUATION_CONTRIBUTION_TYPE
35
+ validates_inclusion_of :calculation_type, :in => SUPERANNUATION_CALCULATION_TYPE
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,50 @@
1
+ module Xeroizer
2
+ module Record
3
+ module Payroll
4
+
5
+ class TaxDeclarationModel < PayrollBaseModel
6
+ set_xml_node_name 'TaxDeclaration'
7
+ end
8
+
9
+ class TaxDeclaration < PayrollBase
10
+
11
+ EMPLOYMENT_BASIS = {
12
+ 'FULLTIME' => '',
13
+ 'PARTTIME' => '',
14
+ 'CASUAL' => '',
15
+ 'LABOURHIRE' => '',
16
+ 'SUPERINCOMESTREAM' => ''
17
+
18
+ } unless defined?(EMPLOYMENT_BASIS)
19
+
20
+ TFN_EXEMPTION_TYPE = {
21
+ 'NOTQUOTED' => 'Employee has not provided a TFN.',
22
+ 'PENDING' => 'Employee has made a separate application or Enquiry to the ATO for a new or existing TFN.',
23
+ 'PENSIONER' => 'Employee is claiming that they are in receipt of a pension, benefit or allowance.',
24
+ 'UNDER18' => 'Employee is claiming an exemption as they are under the age of 18 and do not earn enough to pay tax.'
25
+ } unless defined?(TFN_EXEMPTION_TYPE)
26
+
27
+ string :tax_file_number
28
+
29
+ string :tfn_exemption_type, :api_name => 'TFNExemptionType'
30
+ string :employment_basis
31
+
32
+ boolean :australian_resident_for_tax_purposes
33
+ boolean :tax_free_threshold_claimed
34
+ boolean :has_help_debt, :api_name => 'HasHELPDebt'
35
+ boolean :has_sfss_debt, :api_name => 'HasSFSSDebt'
36
+ boolean :eligible_to_receive_leave_loading
37
+
38
+ decimal :tax_offset_estimated_amount
39
+ decimal :upward_variation_tax_withholding_amount
40
+ decimal :approved_withholding_variation_percentage
41
+
42
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
43
+
44
+ validates_inclusion_of :employment_basis, :in => EMPLOYMENT_BASIS
45
+ validates_inclusion_of :tfn_exemption_type, :in => EMPLOYMENT_BASIS
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,20 @@
1
+ module Xeroizer
2
+ module Record
3
+ module Payroll
4
+
5
+ class TimeOffLineModel < PayrollBaseModel
6
+
7
+ end
8
+
9
+ class TimeOffLine < PayrollBase
10
+
11
+ guid :time_off_type_id, :api_name => 'TimeOffTypeID'
12
+ decimal :hours
13
+ decimal :balance
14
+
15
+ validates_presence_of :time_off_type_id, :unless => :new_record?
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ module Xeroizer
2
+ module Record
3
+ module Payroll
4
+
5
+ class TimeOffTypeModel < PayrollBaseModel
6
+
7
+ set_permissions :read
8
+
9
+ end
10
+
11
+ class TimeOffType < PayrollBase
12
+
13
+ TIME_OFF_CATEGORIES = {
14
+ 'PAID' => '',
15
+ 'UNPAID' => ''
16
+ } unless defined?(TIME_OFF_CATEGORIES)
17
+
18
+ set_primary_key :time_off_type_id
19
+
20
+ guid :time_off_type_id
21
+ string :time_off_type
22
+ string :time_off_category
23
+ string :expense_account_code
24
+ string :liability_account_code
25
+ boolean :show_balance_to_employee
26
+
27
+ validates_inclusion_of :time_off_category, :in => TIME_OFF_CATEGORIES
28
+
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ module Xeroizer
2
+ module Record
3
+ module Payroll
4
+
5
+ class WorkLocationModel < PayrollBaseModel
6
+
7
+ end
8
+
9
+ class WorkLocation < PayrollBase
10
+
11
+ guid :work_location_id
12
+ boolean :is_primary
13
+ string :street_address
14
+ string :suite_or_apt_or_unit
15
+ string :city
16
+ string :state
17
+ string :zip
18
+ decimal :latitude
19
+ decimal :longitude
20
+
21
+ validates_presence_of :work_location_id, :unless => :new_record?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,76 @@
1
+ require "xeroizer/models/attachment"
2
+
3
+ module Xeroizer
4
+ module Record
5
+
6
+ class QuoteModel < BaseModel
7
+ set_permissions :read, :write, :update
8
+
9
+ include AttachmentModel::Extensions
10
+
11
+ public
12
+
13
+ # Retrieve the PDF version of the quote matching the `id`.
14
+ # @param [String] id quote's ID.
15
+ # @param [String] filename optional filename to store the PDF in instead of returning the data.
16
+ def pdf(id, filename = nil)
17
+ pdf_data = @application.http_get(@application.client, "#{url}/#{CGI.escape(id)}", :response => :pdf)
18
+ if filename
19
+ File.open(filename, "wb") { | fp | fp.write pdf_data }
20
+ nil
21
+ else
22
+ pdf_data
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ class Quote < Base
29
+
30
+ QUOTE_STATUS = {
31
+ 'DRAFT' => 'A draft quote (default)',
32
+ 'DELETED' => 'A deleted quote',
33
+ 'SENT' => 'A quote that is marked as sent',
34
+ 'DECLINED' => 'A quote that was declined by the customer',
35
+ 'ACCEPTED' => 'A quote that was accepted by the customer',
36
+ 'INVOICED' => 'A quote that has been invoiced'
37
+ } unless defined?(QUOTE_STATUS)
38
+ QUOTE_STATUSES = QUOTE_STATUS.keys.sort
39
+
40
+ include Attachment::Extensions
41
+
42
+ set_primary_key :quote_id
43
+ set_possible_primary_keys :quote_id, :quote_number
44
+ list_contains_summary_only false
45
+
46
+ guid :quote_id
47
+ string :quote_number
48
+ string :reference
49
+ guid :branding_theme_id
50
+ date :date
51
+ date :expiry_date
52
+ string :status
53
+ string :line_amount_types
54
+ decimal :sub_total
55
+ decimal :total_tax
56
+ decimal :total
57
+ decimal :total_discount
58
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
59
+ string :currency_code
60
+ decimal :currency_rate
61
+ string :title
62
+ string :summary
63
+ string :terms
64
+ boolean :has_attachments
65
+
66
+ belongs_to :contact
67
+ has_many :line_items
68
+
69
+ # Retrieve the PDF version of this quote.
70
+ # @param [String] filename optional filename to store the PDF in instead of returning the data.
71
+ def pdf(filename = nil)
72
+ parent.pdf(id, filename)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -8,6 +8,7 @@ module Xeroizer
8
8
  string :name
9
9
  decimal :rate
10
10
  boolean :is_compound
11
+ boolean :is_non_recoverable
11
12
  end
12
13
  end
13
14
  end
@@ -26,12 +26,23 @@ module Xeroizer
26
26
  class OAuthError < XeroizerError; end
27
27
  class TokenExpired < OAuthError; end
28
28
  class TokenInvalid < OAuthError; end
29
- class RateLimitExceeded < OAuthError; end
30
29
  class ConsumerKeyUnknown < OAuthError; end
31
30
  class NonceUsed < OAuthError; end
32
31
  class OrganisationOffline < OAuthError; end
32
+ class Forbidden < OAuthError; end
33
33
  class UnknownError < OAuthError; end
34
34
 
35
+ class RateLimitExceeded < OAuthError
36
+ def initialize(description, retry_after: nil, daily_limit_remaining: nil)
37
+ super(description)
38
+
39
+ @retry_after = retry_after
40
+ @daily_limit_remaining = @daily_limit_remaining
41
+ end
42
+
43
+ attr_reader :retry_after, :daily_limit_remaining
44
+ end
45
+
35
46
  unless defined? XERO_CONSUMER_OPTIONS
36
47
  XERO_CONSUMER_OPTIONS = {
37
48
  :site => "https://api.xero.com",
@@ -0,0 +1,82 @@
1
+ module Xeroizer
2
+ class OAuth2
3
+
4
+ attr_reader :client, :access_token
5
+
6
+ attr_accessor :tenant_id
7
+
8
+ def initialize(client_key, client_secret, options = {})
9
+ @client = ::OAuth2::Client.new(client_key, client_secret, options)
10
+ end
11
+
12
+ def authorize_url(options)
13
+ @client.auth_code.authorize_url(options)
14
+ end
15
+
16
+ def authorize_from_access(access_token, options = {})
17
+ @access_token = ::OAuth2::AccessToken.new(client, access_token, options)
18
+ end
19
+
20
+ def authorize_from_code(code, options = {})
21
+ @access_token = @client.auth_code.get_token(code, options)
22
+ end
23
+
24
+ def authorize_from_client_credentials(params = {}, options = {})
25
+ @access_token = @client.client_credentials.get_token(params, options)
26
+ end
27
+
28
+ def renew_access_token
29
+ @access_token = @access_token.refresh!
30
+ end
31
+
32
+ def get(path, headers = {})
33
+ wrap_response(access_token.get(path, headers: wrap_headers(headers)))
34
+ end
35
+
36
+ def post(path, body = "", headers = {})
37
+ wrap_response(access_token.post(path, {body: body, headers: wrap_headers(headers)}))
38
+ end
39
+
40
+ def put(path, body = "", headers = {})
41
+ wrap_response(access_token.put(path, body: body, headers: wrap_headers(headers)))
42
+ end
43
+
44
+ def delete(path, headers = {})
45
+ wrap_response(access_token.delete(path, headers: wrap_headers(headers)))
46
+ end
47
+
48
+ private
49
+
50
+ def wrap_headers(headers)
51
+ if tenant_id
52
+ headers.merge("Xero-tenant-id" => tenant_id)
53
+ else
54
+ headers
55
+ end
56
+ end
57
+
58
+ def wrap_response(response)
59
+ Response.new(response)
60
+ end
61
+
62
+ class Response
63
+ attr_reader :response
64
+
65
+ def initialize(response)
66
+ @response = response
67
+ end
68
+
69
+ def code
70
+ response.status
71
+ end
72
+
73
+ def success?
74
+ (200..299).to_a.include?(code)
75
+ end
76
+
77
+ def plain_body
78
+ response.body
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,49 @@
1
+ module Xeroizer
2
+ class OAuth2Application < GenericApplication
3
+
4
+ extend Forwardable
5
+ def_delegators :client,
6
+ :authorize_from_access,
7
+ :authorize_from_client_credentials,
8
+ :authorize_from_code,
9
+ :authorize_url,
10
+ :renew_access_token,
11
+ :tenant_id,
12
+ :tenant_id=
13
+
14
+ public
15
+
16
+ # OAuth2 applications allow for connecting to Xero over OAuth2, as opposed to the
17
+ # Partner and Private applications which talk over OAuth1.
18
+ #
19
+ # @param [String] client_id client id/token from application developer (found at http://api.xero.com for your application).
20
+ # @param [String] client_secret client secret from application developer (found at http://api.xero.com for your application).
21
+ # @param [Hash] options other options to pass to the GenericApplication constructor
22
+ # @return [OAuth2Application] instance of OAuth2Application
23
+ def initialize(client_id, client_secret, options = {})
24
+ default_options = {
25
+ :xero_url => 'https://api.xero.com/api.xro/2.0',
26
+ :site => 'https://api.xero.com',
27
+ :authorize_url => 'https://login.xero.com/identity/connect/authorize',
28
+ :token_url => 'https://identity.xero.com/connect/token',
29
+ :tenets_url => 'https://api.xero.com/connections',
30
+ :raise_errors => false
31
+ }
32
+ options = default_options.merge(options)
33
+ client = OAuth2.new(client_id, client_secret, options)
34
+ super(client, options)
35
+
36
+ if options[:access_token]
37
+ authorize_from_access(options[:access_token], options)
38
+ end
39
+
40
+ if options[:tenant_id]
41
+ client.tenant_id = options[:tenant_id]
42
+ end
43
+ end
44
+
45
+ def current_connections
46
+ Connection.current_connections(client)
47
+ end
48
+ end
49
+ end
@@ -1,6 +1,6 @@
1
1
  module Xeroizer
2
2
  class PayrollApplication
3
-
3
+
4
4
  attr_reader :application
5
5
 
6
6
  # Factory for new Payroll BaseModel instances with the class name `record_type`.
@@ -15,11 +15,16 @@ module Xeroizer
15
15
  instance_variable_set(var_name, Xeroizer::Record::Payroll.const_get("#{record_type}Model".to_sym).new(self.application, record_type.to_s))
16
16
  end
17
17
  instance_variable_get(var_name)
18
- end
18
+ end
19
19
  end
20
20
 
21
21
  record :Employee
22
-
22
+ record :PayRun
23
+ record :Paystub
24
+ record :PayItems
25
+ record :PaySchedule
26
+ record :LeaveApplication
27
+
23
28
  def initialize(application)
24
29
  @application = application
25
30
  end
@@ -116,7 +116,7 @@ module Xeroizer
116
116
  build(attributes).tap { |resource| resource.save }
117
117
  end
118
118
 
119
- # Retreive full record list for this model.
119
+ # Retrieve full record list for this model.
120
120
  def all(options = {})
121
121
  raise MethodNotAllowed.new(self, :all) unless self.class.permissions[:read]
122
122
  response_xml = http_get(parse_params(options))
@@ -120,6 +120,7 @@ module Xeroizer
120
120
  when :datetime_utc then [field[:api_name], expression, "DateTime.Parse(\"#{value.utc.strftime("%Y-%m-%dT%H:%M:%S")}\")"]
121
121
  when :belongs_to then
122
122
  when :has_many then
123
+ when :has_one then
123
124
  end
124
125
  end
125
126
 
@@ -12,6 +12,10 @@ module Xeroizer
12
12
  def self.has_many(field_name, options = {})
13
13
  super(field_name, {:base_module => Xeroizer::Record::Payroll}.merge(options))
14
14
  end
15
+
16
+ def self.has_one(field_name, options = {})
17
+ super(field_name, {:base_module => Xeroizer::Record::Payroll}.merge(options))
18
+ end
15
19
 
16
20
  public
17
21
 
@@ -35,7 +35,7 @@ module Xeroizer
35
35
  def has_many(field_name, options = {})
36
36
  internal_field_name = options[:internal_name] || field_name
37
37
  internal_singular_field_name = options[:internal_name_singular] || internal_field_name.to_s.singularize
38
-
38
+
39
39
  define_association_attribute(field_name, internal_field_name, :has_many, options)
40
40
 
41
41
  # Create an #add_record_name method to build the record and add to the attributes.
@@ -80,7 +80,7 @@ module Xeroizer
80
80
  end
81
81
 
82
82
  end
83
-
83
+
84
84
  def define_association_attribute(field_name, internal_field_name, association_type, options)
85
85
  define_simple_attribute(field_name, association_type, options.merge!(:skip_writer => true), ((association_type == :has_many) ? [] : nil))
86
86
 
@@ -96,8 +96,8 @@ module Xeroizer
96
96
  when :has_many
97
97
  self.attributes[field_name] = []
98
98
  self.send("add_#{internal_singular_field_name}".to_sym, value)
99
-
100
- when :belongs_to
99
+
100
+ when :belongs_to
101
101
  self.attributes[field_name] = (options[:base_module] || Xeroizer::Record).const_get(model_name).build(value, new_model_class(model_name))
102
102
 
103
103
  end
@@ -22,6 +22,7 @@ module Xeroizer
22
22
  else
23
23
  record.errors << [attribute, "must have one or more records"]
24
24
  end
25
+
25
26
  end
26
27
  end
27
28
 
@@ -3,14 +3,14 @@ require 'active_support/time'
3
3
  module Xeroizer
4
4
  module Record
5
5
  module XmlHelper
6
-
6
+
7
7
  def self.included(base)
8
8
  base.extend(ClassMethods)
9
9
  base.send :include, InstanceMethods
10
10
  end
11
-
11
+
12
12
  module ClassMethods
13
-
13
+
14
14
  # Build a record instance from the XML node.
15
15
  def build_from_node(node, parent, base_module)
16
16
  record = new(parent)
@@ -26,10 +26,10 @@ module Xeroizer
26
26
  when :date then Date.parse(element.text)
27
27
  when :datetime then Time.parse(element.text)
28
28
  when :datetime_utc then ActiveSupport::TimeZone['UTC'].parse(element.text).utc
29
- when :belongs_to
29
+ when :belongs_to
30
30
  model_name = field[:model_name] ? field[:model_name].to_sym : element.name.to_sym
31
31
  base_module.const_get(model_name).build_from_node(element, parent, base_module)
32
-
32
+
33
33
  when :has_many
34
34
  if element.element_children.size > 0
35
35
  sub_field_name = field[:model_name] ? field[:model_name].to_sym : element.children.first.name.to_sym
@@ -51,13 +51,13 @@ module Xeroizer
51
51
  parent.mark_clean(record)
52
52
  record
53
53
  end
54
-
54
+
55
55
  end
56
-
56
+
57
57
  module InstanceMethods
58
-
58
+
59
59
  public
60
-
60
+
61
61
  # Turn a record into its XML representation.
62
62
  def to_xml(b = Builder::XmlMarkup.new(:indent => 2))
63
63
  optional_root_tag(parent.class.optional_xml_root_name, b) do |c|
@@ -70,9 +70,9 @@ module Xeroizer
70
70
  }
71
71
  end
72
72
  end
73
-
73
+
74
74
  protected
75
-
75
+
76
76
  # Add top-level root name if required.
77
77
  # E.g. Payments need specifying in the form:
78
78
  # <Payments>
@@ -87,7 +87,7 @@ module Xeroizer
87
87
  yield(b)
88
88
  end
89
89
  end
90
-
90
+
91
91
  # Format an attribute for use in the XML passed to Xero.
92
92
  def xml_value_from_field(b, field, value)
93
93
  case field[:type]
@@ -95,7 +95,7 @@ module Xeroizer
95
95
  when :string then b.tag!(field[:api_name], value)
96
96
  when :boolean then b.tag!(field[:api_name], value ? 'true' : 'false')
97
97
  when :integer then b.tag!(field[:api_name], value.to_i)
98
- when :decimal
98
+ when :decimal
99
99
  real_value = case value
100
100
  when BigDecimal then value.to_s
101
101
  when String then BigDecimal(value).to_s
@@ -111,13 +111,13 @@ module Xeroizer
111
111
  else raise ArgumentError.new("Expected Date or Time object for the #{field[:api_name]} field")
112
112
  end
113
113
  b.tag!(field[:api_name], real_value)
114
-
114
+
115
115
  when :datetime then b.tag!(field[:api_name], value.utc.strftime("%Y-%m-%dT%H:%M:%S"))
116
- when :belongs_to
116
+ when :belongs_to
117
117
  value.to_xml(b)
118
118
  nil
119
119
 
120
- when :has_many
120
+ when :has_many
121
121
  if value.size > 0
122
122
  sub_parent = value.first.parent
123
123
  b.tag!(sub_parent.class.xml_root_name || sub_parent.model_name.pluralize) {