diff --git a/README.md b/README.md
index 98bbc7b1..2624dd48 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,22 @@ There are three methods of authentication detailed below:
All methods of authentication require your OAuth consumer key and secret. This can be found for your application
in the API management console at [http://api.xero.com](http://api.xero.com).
+##### Payroll Applications
+
+Applications accessing the [Payroll API](http://developer.xero.com/payroll-api/) need to do things slightly differently. To change a client into a payroll client, call `.payroll` on the client. For example, for a public application, change:
+
+```ruby
+client = Xeroizer::PublicApplication.new(YOUR_OAUTH_CONSUMER_KEY, YOUR_OAUTH_CONSUMER_SECRET)
+```
+
+to:
+
+```ruby
+client = Xeroizer::PublicApplication.new(YOUR_OAUTH_CONSUMER_KEY, YOUR_OAUTH_CONSUMER_SECRET).payroll
+```
+
+Some kinds of applications will also need to provide scopes when navigating to the request URL. See below for more details.
+
### Public Applications
Public applications use a 3-legged authorisation process. A user will need to authorise your
@@ -87,6 +103,23 @@ You can now use the client to access the Xero API methods, e.g.
contacts = client.Contact.all
```
+#### Payroll Applications
+
+Payroll applications need to get permission to the appropriate API endpoints. Do this by providing a `scope` parameter when calling `request_token.authorize_url`.
+
+
+```ruby
+client = Xeroizer::PublicApplication.new(YOUR_OAUTH_CONSUMER_KEY, YOUR_OAUTH_CONSUMER_SECRET).payroll
+request_token = client.request_token(:oauth_callback => 'http://yourapp.com/oauth/callback')
+redirect_to request_token.authorize_url(scope: Xeroizer::Scopes.all_payroll)
+```
+
+`Xeroizer::Scopes.all_payroll` requests access to all payroll endpoints. If you prefer, you can specify only specific endpoints:
+
+```ruby
+redirect_to request_token.authorize_url(scope: 'payroll.employees,payroll.payitems')
+```
+
#### Example Rails Controller
```ruby
@@ -389,6 +422,8 @@ in the resulting response, including all nested XML elements.
contacts = xero.Contact.all(:where => 'Name.StartsWith("Pet")')
contacts = xero.Contact.all(:where => 'Name.EndsWith("er")')
+See [Xero's documentation on filters](http://developer.xero.com/api-overview/http-requests-and-responses/#get-filtered) for more information.
+
Associations
------------
diff --git a/lib/xeroizer.rb b/lib/xeroizer.rb
index 240dce19..b8e36653 100644
--- a/lib/xeroizer.rb
+++ b/lib/xeroizer.rb
@@ -18,6 +18,7 @@
require 'class_level_inheritable_attributes'
require 'xeroizer/exceptions'
require 'xeroizer/oauth'
+require 'xeroizer/scopes'
require 'xeroizer/oauth2'
require 'xeroizer/http_encoding_helper'
require 'xeroizer/http'
@@ -25,85 +26,40 @@
require 'xeroizer/record/base_model'
require 'xeroizer/record/payroll_base_model'
+require 'xeroizer/record/payroll_array_base_model'
require 'xeroizer/record/base'
require 'xeroizer/record/payroll_base'
+require 'xeroizer/record/payroll_array_base'
require 'xeroizer/configuration'
require 'xeroizer/http_response'
# Include models
-require 'xeroizer/models/account'
-require 'xeroizer/models/address'
-require 'xeroizer/models/allocation'
-require 'xeroizer/models/branding_theme'
-require 'xeroizer/models/bank_transaction'
-require 'xeroizer/models/bank_account'
-require 'xeroizer/models/batch_payment'
-require 'xeroizer/models/from_bank_account'
-require 'xeroizer/models/to_bank_account'
-require 'xeroizer/models/bank_transfer'
-require 'xeroizer/models/contact'
-require 'xeroizer/models/contact_group'
-require 'xeroizer/models/credit_note'
-require 'xeroizer/models/currency'
-require 'xeroizer/models/employee'
-require 'xeroizer/models/expense_claim'
-require 'xeroizer/models/invoice'
-require 'xeroizer/models/invoice_reminder'
-require 'xeroizer/models/online_invoice'
-require 'xeroizer/models/item'
-require 'xeroizer/models/item_purchase_details'
-require 'xeroizer/models/item_sales_details'
-require 'xeroizer/models/journal'
-require 'xeroizer/models/journal_line'
-require 'xeroizer/models/line_item'
-require 'xeroizer/models/manual_journal'
-require 'xeroizer/models/manual_journal_line'
-require 'xeroizer/models/option'
-require 'xeroizer/models/organisation'
-require 'xeroizer/models/payment'
-require 'xeroizer/models/payment_service'
-require 'xeroizer/models/prepayment'
-require 'xeroizer/models/overpayment'
-require 'xeroizer/models/phone'
-require 'xeroizer/models/purchase_order'
-require 'xeroizer/models/receipt'
-require 'xeroizer/models/repeating_invoice'
-require 'xeroizer/models/schedule'
-require 'xeroizer/models/tax_rate'
-require 'xeroizer/models/tax_component'
-require 'xeroizer/models/tracking_category'
-require 'xeroizer/models/tracking_category_child'
-require 'xeroizer/models/user'
-require 'xeroizer/models/journal_line_tracking_category'
-require 'xeroizer/models/contact_sales_tracking_category'
-require 'xeroizer/models/contact_purchases_tracking_category'
+['account','address','allocation','branding_theme','bank_transaction','bank_account','contact','contact_group',
+ 'credit_note','currency','employee','invoice','item','item_purchase_details','item_sales_details',
+ 'journal','journal_line','line_item','manual_journal','manual_journal_line','option','organisation',
+ 'payment','phone','tax_rate','tracking_category','tracking_category_child',
+ 'journal_line_tracking_category', 'user', 'batch_payment', 'from_bank_account',
+ 'to_bank_account', 'bank_transfer', 'expense_claim', 'invoice_reminder',
+ 'online_invoice', 'payment_service', 'prepayment', 'overpayment',
+ 'purchase_order', 'receipt', 'repeating_invoice',
+ 'schedule', 'tax_component', 'contact_sales_tracking_category',
+ 'contact_purchases_tracking_category'].each do |model|
+ require "xeroizer/models/#{model}"
+end
+
+# Include payroll models
+['home_address', 'bank_account', 'employee', 'employee_leave_type', 'timesheet', 'timesheet_line', 'number_of_unit',
+ 'leave_application', 'leave', 'leave_period', 'period', 'pay_items', 'deduction_type', 'earnings_rate',
+ 'reimbursement_type', 'leave_type', 'payroll_calendar', 'pay_template', 'super_membership',
+ 'leave_line', 'reimbursement_line', 'super_line', 'deduction_line', 'earnings_line', 'opening_balance',
+ 'pay_run', 'settings', 'tracking_categories', 'employee_groups', 'timesheet_categories', 'account',
+ 'tax_declaration', 'payslip', 'timesheet_earnings_line', 'tax_line', 'leave_accrual_line', 'superannuation_line',
+ 'leave_balance', 'time_off_balance', 'earnings_type', 'super_fund', 'earning_template', 'salary_and_wages',
+ 'benefit_line', 'benefit_type', 'earnings_type', 'address', 'payment_method', 'pay_schedule', 'paystub', 'salary_and_wage',
+ 'time_off_line', 'time_off_type', 'work_location'].each do |payroll_model|
+ require "xeroizer/models/payroll/#{payroll_model}"
+end
-require 'xeroizer/models/payroll/bank_account'
-require 'xeroizer/models/payroll/benefit_line'
-require 'xeroizer/models/payroll/benefit_type'
-require 'xeroizer/models/payroll/deduction_line'
-require 'xeroizer/models/payroll/deduction_type'
-require 'xeroizer/models/payroll/earnings_line'
-require 'xeroizer/models/payroll/earnings_type'
-require 'xeroizer/models/payroll/employee'
-require 'xeroizer/models/payroll/address'
-require 'xeroizer/models/payroll/leave_line'
-require 'xeroizer/models/payroll/pay_items'
-require 'xeroizer/models/payroll/pay_run'
-require 'xeroizer/models/payroll/pay_template'
-require 'xeroizer/models/payroll/payment_method'
-require 'xeroizer/models/payroll/pay_schedule'
-require 'xeroizer/models/payroll/paystub'
-require 'xeroizer/models/payroll/reimbursement_line'
-require 'xeroizer/models/payroll/reimbursement_type'
-require 'xeroizer/models/payroll/salary_and_wage'
-require 'xeroizer/models/payroll/super_line'
-require 'xeroizer/models/payroll/tax_declaration'
-require 'xeroizer/models/payroll/time_off_line'
-require 'xeroizer/models/payroll/time_off_type'
-require 'xeroizer/models/payroll/work_location'
-require 'xeroizer/models/payroll/leave_application'
-require 'xeroizer/models/payroll/leave_period'
require 'xeroizer/report/factory'
diff --git a/lib/xeroizer/application_http_proxy.rb b/lib/xeroizer/application_http_proxy.rb
index a758c0a0..74684630 100644
--- a/lib/xeroizer/application_http_proxy.rb
+++ b/lib/xeroizer/application_http_proxy.rb
@@ -1,30 +1,33 @@
module Xeroizer
module ApplicationHttpProxy
-
+
def self.included(base)
base.send :include, InstanceMethods
end
-
+
module InstanceMethods
-
+
# URL end-point for this model.
- def url
- @application.xero_url + '/' + api_controller_name
+ def url(suffix = nil)
+ @application.xero_url + '/' + (suffix || api_controller_name)
end
def http_get(extra_params = {})
- application.http_get(application.client, url, extra_params)
+ extra_params.reverse_merge!(response: application.api_format)
+ application.http_get(application.client, url(extra_params.delete(:url)), extra_params)
end
def http_put(xml, extra_params = {})
- application.http_put(application.client, url, xml, extra_params)
+ extra_params.reverse_merge!(response: application.api_format)
+ application.http_put(application.client, url(extra_params.delete(:url)), xml, extra_params)
end
def http_post(xml, extra_params = {})
- application.http_post(application.client, url, xml, extra_params)
+ extra_params.reverse_merge!(response: application.api_format)
+ application.http_post(application.client, url(extra_params.delete(:url)), xml, extra_params)
end
-
+
end
-
+
end
-end
\ No newline at end of file
+end
diff --git a/lib/xeroizer/exceptions.rb b/lib/xeroizer/exceptions.rb
index 78cff7a8..2a691991 100644
--- a/lib/xeroizer/exceptions.rb
+++ b/lib/xeroizer/exceptions.rb
@@ -70,6 +70,8 @@ def message
class InvoiceNotFoundError < XeroizerError; end
+ class LackingPermissionToAccessRecord < XeroizerError; end
+
class CreditNoteNotFoundError < XeroizerError; end
class MethodNotAllowed < XeroizerError
diff --git a/lib/xeroizer/generic_application.rb b/lib/xeroizer/generic_application.rb
index b14ccec1..7cc90cde 100644
--- a/lib/xeroizer/generic_application.rb
+++ b/lib/xeroizer/generic_application.rb
@@ -6,8 +6,9 @@ class GenericApplication
include Http
extend Record::ApplicationHelper
- attr_reader :client, :logger, :rate_limit_sleep, :rate_limit_max_attempts,
+ attr_reader :client, :xero_url, :logger, :rate_limit_sleep, :rate_limit_max_attempts,
:default_headers, :unitdp, :before_request, :after_request, :around_request, :nonce_used_max_attempts
+ attr_accessor :logger
attr_accessor :xero_url
@@ -34,6 +35,7 @@ class GenericApplication
record :LineItem
record :ManualJournal
record :Organisation
+ record :User
record :Payment
record :PaymentService
record :Prepayment
@@ -82,9 +84,13 @@ def initialize(client, options = {})
def payroll(options = {})
xero_client = self.clone
- xero_client.xero_url = options[:xero_url] || "https://api.xero.com/payroll.xro/1.0"
+ xero_client.xero_url_suffix = options[:xero_url_suffix] || "payroll.xro/1.0"
+ xero_client.api_format = options[:api_format] || :xml
@payroll ||= PayrollApplication.new(xero_client)
end
+ def xero_url
+ @xero_url_prefix + '/' + @xero_url_suffix
+ end
end
end
diff --git a/lib/xeroizer/http.rb b/lib/xeroizer/http.rb
index b1aca702..4e7c7276 100644
--- a/lib/xeroizer/http.rb
+++ b/lib/xeroizer/http.rb
@@ -19,7 +19,8 @@ class BadResponse < XeroizerError; end
ACCEPT_MIME_MAP = {
:pdf => 'application/pdf',
- :json => 'application/json'
+ :json => 'application/json',
+ :xml => 'application/xml',
}
# Shortcut method for #http_request with `method` = :get.
@@ -79,6 +80,8 @@ def http_request(client, method, url, request_body, params = {})
when Symbol then ACCEPT_MIME_MAP[response_type]
else response_type
end
+ else
+ headers['Accept'] = "application/xml"
end
if params.any?
diff --git a/lib/xeroizer/models/credit_note.rb b/lib/xeroizer/models/credit_note.rb
index 568e7cd8..a821338e 100644
--- a/lib/xeroizer/models/credit_note.rb
+++ b/lib/xeroizer/models/credit_note.rb
@@ -1,12 +1,11 @@
module Xeroizer
module Record
-
+
class CreditNoteModel < BaseModel
-
- set_permissions :read, :write, :update
-
+
+ set_permissions :read, :write, :update
include AttachmentModel::Extensions
-
+
public
# Retrieve the PDF version of the credit matching the `id`.
@@ -21,11 +20,11 @@ def pdf(id, filename = nil)
pdf_data
end
end
-
+
end
-
+
class CreditNote < Base
-
+
CREDIT_NOTE_STATUS = {
'AUTHORISED' => 'Approved credit_notes awaiting payment',
'DELETED' => 'Draft credit_notes that are deleted',
@@ -35,7 +34,7 @@ class CreditNote < Base
'VOIDED' => 'Approved credit_notes that are voided'
} unless defined?(CREDIT_NOTE_STATUS)
CREDIT_NOTE_STATUSES = CREDIT_NOTE_STATUS.keys.sort
-
+
CREDIT_NOTE_TYPE = {
'ACCRECCREDIT' => 'Accounts Receivable',
'ACCPAYCREDIT' => 'Accounts Payable'
@@ -47,7 +46,7 @@ class CreditNote < Base
set_primary_key :credit_note_id
set_possible_primary_keys :credit_note_id, :credit_note_number
list_contains_summary_only true
-
+
guid :credit_note_id
string :credit_note_number
string :reference
@@ -72,15 +71,15 @@ class CreditNote < Base
belongs_to :contact
has_many :line_items
has_many :allocations
-
+
validates_inclusion_of :type, :in => CREDIT_NOTE_TYPES
validates_inclusion_of :status, :in => CREDIT_NOTE_STATUSES, :allow_blanks => true
validates_associated :contact
validates_associated :line_items
validates_associated :allocations, :allow_blanks => true
-
+
public
-
+
# Access the contact name without forcing a download of
# an incomplete, summary credit note.
def contact_name
@@ -91,13 +90,13 @@ def contact_name
# incomplete, summary credit note.
def contact_id
attributes[:contact] && attributes[:contact][:contact_id]
- end
-
+ end
+
# Swallow assignment of attributes that should only be calculated automatically.
def sub_total=(value); raise SettingTotalDirectlyNotSupported.new(:sub_total); end
def total_tax=(value); raise SettingTotalDirectlyNotSupported.new(:total_tax); end
def total=(value); raise SettingTotalDirectlyNotSupported.new(:total); end
-
+
# Calculate sub_total from line_items.
def sub_total(always_summary = false)
if !always_summary && (new_record? || (!new_record? && line_items && line_items.size > 0))
@@ -128,7 +127,7 @@ def total(always_summary = false)
attributes[:total]
end
end
-
+
# Retrieve the PDF version of this credit note.
# @param [String] filename optional filename to store the PDF in instead of returning the data.
def pdf(filename = nil)
diff --git a/lib/xeroizer/models/employee.rb b/lib/xeroizer/models/employee.rb
index 02743c6f..2568835c 100644
--- a/lib/xeroizer/models/employee.rb
+++ b/lib/xeroizer/models/employee.rb
@@ -2,17 +2,17 @@
module Xeroizer
module Record
-
+
class EmployeeModel < BaseModel
-
+
set_permissions :read, :write, :update
-
+
end
-
+
class Employee < Base
-
+
set_primary_key :employee_id
-
+
guid :employee_id
string :status
string :first_name
@@ -29,8 +29,9 @@ class Employee < Base
boolean :is_authorised_to_approve_timesheets
string :employee_group_name
- datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
-
+ datetime_utc :updated_date_utc, api_name: 'UpdatedDateUTC'
+ date :end_date, api_name: 'EndDate' # UK - null when employee is active
+
belongs_to :external_link
validates_presence_of :first_name, :last_name, :date_of_birth
diff --git a/lib/xeroizer/models/payroll/account.rb b/lib/xeroizer/models/payroll/account.rb
new file mode 100644
index 00000000..d377fc95
--- /dev/null
+++ b/lib/xeroizer/models/payroll/account.rb
@@ -0,0 +1,21 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class AccountModel < PayrollBaseModel
+
+ set_permissions :read
+
+ end
+
+ class Account < PayrollBase
+
+ string :type
+ string :code
+ string :name
+ guid :account_id
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/bank_account.rb b/lib/xeroizer/models/payroll/bank_account.rb
index a222760b..5a91d858 100644
--- a/lib/xeroizer/models/payroll/bank_account.rb
+++ b/lib/xeroizer/models/payroll/bank_account.rb
@@ -13,7 +13,7 @@ class BankAccount < PayrollBase
string :bsb, :api_name => 'BSB'
string :account_number
boolean :remainder
- string :percentage
+ decimal :percentage
decimal :amount
# US Payroll fields
diff --git a/lib/xeroizer/models/payroll/deduction_line.rb b/lib/xeroizer/models/payroll/deduction_line.rb
index 600ef2ac..975c7d17 100644
--- a/lib/xeroizer/models/payroll/deduction_line.rb
+++ b/lib/xeroizer/models/payroll/deduction_line.rb
@@ -1,11 +1,11 @@
module Xeroizer
module Record
module Payroll
-
class DeductionLineModel < PayrollBaseModel
end
+ # child of PayTemplate
class DeductionLine < PayrollBase
DEDUCTION_TYPE_CALCULATION_TYPE = {
@@ -15,7 +15,7 @@ class DeductionLine < PayrollBase
} unless defined?(DEDUCTION_TYPE_CALCULATION_TYPE)
guid :deduction_type_id, :api_name => 'DeductionTypeID'
- string :calculation_type
+ string :calculation_type # http://developer.xero.com/payroll-api/types-and-codes#DeductionTypeCalculationType
decimal :percentage
decimal :amount
diff --git a/lib/xeroizer/models/payroll/deduction_type.rb b/lib/xeroizer/models/payroll/deduction_type.rb
index eb96c749..b92a4afd 100644
--- a/lib/xeroizer/models/payroll/deduction_type.rb
+++ b/lib/xeroizer/models/payroll/deduction_type.rb
@@ -32,7 +32,15 @@ class DeductionType < PayrollBase
set_primary_key :deduction_type_id
- guid :deduction_type_id
+ string :name
+ string :account_code # http://developer.xero.com/api/Accounts
+ boolean :reduces_super
+ boolean :reduces_tax
+
+ guid :deduction_type_id
+
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
+
string :deduction_type
string :deduction_category
string :calculation_type
@@ -43,7 +51,9 @@ class DeductionType < PayrollBase
validates_inclusion_of :deduction_category, :in => DEDUCTION_CATEGORIES
validates_inclusion_of :calculation_type, :in => CALCULATION_TYPES
+ validates_presence_of :name, :account_code, :reduces_tax, :reduces_super
end
- end
+
+ end
end
-end
\ No newline at end of file
+end
diff --git a/lib/xeroizer/models/payroll/earning_template.rb b/lib/xeroizer/models/payroll/earning_template.rb
new file mode 100644
index 00000000..5426da6b
--- /dev/null
+++ b/lib/xeroizer/models/payroll/earning_template.rb
@@ -0,0 +1,26 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class EarningTemplateModel < PayrollBaseModel
+
+ set_permissions :read, :write, :update
+
+ end
+ # https://developer.xero.com/documentation/payroll-api-uk/employeepaytemplates
+ class EarningTemplate < PayrollBase
+
+ guid :pay_template_earning_id
+ guid :earnings_rate_id
+
+ string :name
+
+ decimal :rate_per_unit
+ decimal :number_of_units
+ decimal :fixed_amount
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/earnings_line.rb b/lib/xeroizer/models/payroll/earnings_line.rb
index 55887a29..91559264 100644
--- a/lib/xeroizer/models/payroll/earnings_line.rb
+++ b/lib/xeroizer/models/payroll/earnings_line.rb
@@ -1,11 +1,11 @@
module Xeroizer
module Record
module Payroll
-
class EarningsLineModel < PayrollBaseModel
end
-
+
+ # child of PayTemplate, Payslip, OpeningBalance
class EarningsLine < PayrollBase
EARNINGS_RATE_CALCULATION_TYPE = {
@@ -15,12 +15,14 @@ class EarningsLine < PayrollBase
} unless defined?(EARNINGS_RATE_CALCULATION_TYPE)
guid :earning_rate_id, :api_name => 'EarningsRateID'
- string :calculation_type
+ string :calculation_type # http://developer.xero.com/payroll-api/types-and-codes/#EarningsRateCalculationType
decimal :number_of_units_per_week
+ decimal :number_of_units
decimal :annual_salary
decimal :rate_per_unit
decimal :normal_number_of_units
+ decimal :amount
# US Payroll fields
guid :earnings_type_id
@@ -36,4 +38,4 @@ class EarningsLine < PayrollBase
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/xeroizer/models/payroll/earnings_rate.rb b/lib/xeroizer/models/payroll/earnings_rate.rb
new file mode 100644
index 00000000..8207c9d9
--- /dev/null
+++ b/lib/xeroizer/models/payroll/earnings_rate.rb
@@ -0,0 +1,41 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class EarningsRateModel < PayrollBaseModel
+
+ set_permissions :read, :write, :update
+
+ end
+
+ class EarningsRate < PayrollBase
+
+ string :name
+ string :account_code # http://developer.xero.com/api/Accounts
+ string :type_of_units
+ boolean :is_exempt_from_tax
+ boolean :is_exempt_from_super
+ string :earnings_type # http://developer.xero.com/payroll-api/types-and-codes/#EarningsTypes
+
+ guid :earnings_rate_id
+ string :rate_type # http://developer.xero.com/payroll-api/types-and-codes/#EarningsRateTypes
+ decimal :rate_per_unit
+ decimal :multiplier
+ boolean :accrue_leave
+ decimal :amount
+ decimal :fixed_amount # UK
+ decimal :multiple_of_ordinary_earnings_rate # UK
+ string :expense_account_id # UK
+ boolean :current_record # UK
+ boolean :is_reportable_as_w1
+ string :employment_termination_payment_type
+
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
+
+ validates_presence_of :name, :earnings_type # UK has lesser validation requirements
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/earnings_type.rb b/lib/xeroizer/models/payroll/earnings_type.rb
index a6472d9e..96404068 100644
--- a/lib/xeroizer/models/payroll/earnings_type.rb
+++ b/lib/xeroizer/models/payroll/earnings_type.rb
@@ -8,6 +8,7 @@ class EarningsTypeModel < PayrollBaseModel
end
+ # https://developer.xero.com/documentation/payroll-api-us/pay-items/#EarningsTypes
class EarningsType < PayrollBase
EARNINGS_CATEGORIES = {
@@ -34,9 +35,9 @@ class EarningsType < PayrollBase
guid :earnings_type_id
guid :earnings_rate_id
- string :earnings_type
- string :expense_account_code
- string :earnings_category
+ string :earnings_type # http://developer.xero.com/payroll-api/types-and-codes/#EarningsTypes
+ string :expense_account_code # http://developer.xero.com/api/Accounts
+ string :earnings_category # https://developer.xero.com/documentation/payroll-api-us/types-codes/#EarningsCategory
string :rate_type
string :type_of_units
decimal :multiple
@@ -47,7 +48,11 @@ class EarningsType < PayrollBase
validates_inclusion_of :earnings_category, :in => EARNINGS_CATEGORIES
validates_inclusion_of :rate_type, :in => RATE_TYPES
+
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
+
+ validates_presence_of :earnings_type, :expense_account_code, :earnings_category
end
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/xeroizer/models/payroll/employee.rb b/lib/xeroizer/models/payroll/employee.rb
index 829605b1..e8e6266f 100644
--- a/lib/xeroizer/models/payroll/employee.rb
+++ b/lib/xeroizer/models/payroll/employee.rb
@@ -36,12 +36,21 @@ class Employee < PayrollBase
guid :payroll_calendar_id
string :employee_group_name
date :termination_date
- datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
+ string :national_insurance_number # UK
+ guid :pay_run_calendar_id # UK
+ datetime_utc :updated_date_utc, api_name: 'UpdatedDateUTC'
+ date :end_date, api_name: 'EndDate' # UK - null when employee is active
+
+ belongs_to :home_address, :internal_name_singular => "home_address", :model_name => "HomeAddress"
+ belongs_to :tax_declaration, :internal_name_singular => "tax_declaration", :model_name => "TaxDeclaration"
- has_one :home_address, :internal_name_singular => "home_address", :model_name => "HomeAddress"
- has_one :tax_declaration, :internal_name_singular => "tax_declaration", :model_name => "TaxDeclaration"
- has_one :pay_template, :internal_name_singular => "pay_template", :model_name => "PayTemplate"
has_many :bank_accounts
+ belongs_to :pay_template, :internal_name_singular => "pay_template", :model_name => "PayTemplate"
+ belongs_to :opening_balances, :internal_name_singular => "opening_balance", :model_name => "OpeningBalances"
+ has_many :super_memberships, :internal_name_singular => "super_membership", :model_name => "SuperMembership"
+ has_many :leave_balances, :internal_name_singular => "leave_balance", model_name: "LeaveBalance"
+ has_many :leave_types, :internal_name_singular => "leave_type", model_name: "LeaveType"
+ has_many :time_off_balances, :internal_name_singular => "time_off_balance", model_name: "TimeOffBalance"
# US Payroll fields
string :job_title
diff --git a/lib/xeroizer/models/payroll/employee_groups.rb b/lib/xeroizer/models/payroll/employee_groups.rb
new file mode 100644
index 00000000..a4b5397f
--- /dev/null
+++ b/lib/xeroizer/models/payroll/employee_groups.rb
@@ -0,0 +1,24 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class EmployeeGroupsModel < PayrollBaseModel
+
+ set_standalone_model true
+ set_xml_root_name 'EmployeeGroups'
+ set_xml_node_name 'EmployeeGroups'
+ end
+
+ # child of TrackingCategories
+ class EmployeeGroups < PayrollBase
+
+ set_primary_key false
+
+ guid :tracking_category_id
+ string :tracking_category_name
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/employee_leave_type.rb b/lib/xeroizer/models/payroll/employee_leave_type.rb
new file mode 100644
index 00000000..f2146e03
--- /dev/null
+++ b/lib/xeroizer/models/payroll/employee_leave_type.rb
@@ -0,0 +1,39 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class EmployeeLeaveTypeModel < PayrollBaseModel
+ set_permissions :read, :write, :update
+
+ def api_url(options)
+ if options.keys.include?(:employee_id)
+ "employees/#{options.delete(:employee_id)}/leaveTypes"
+ end
+ end
+
+ def model_name_to_parse
+ # HACK so we can use LeaveType to be the iterable
+ "LeaveType"
+ end
+ end
+
+ class EmployeeLeaveType < PayrollBase
+ guid :leave_type_id
+ guid :employee_id
+
+ string :schedule_of_accrual
+ decimal :hours_accrued_annually
+ decimal :maximum_to_accrue
+ decimal :opening_balance
+ decimal :rate_accrued_hourly
+
+ validates_presence_of :leave_type_id, :schedule_of_accrual
+
+ def api_url
+ "employees/#{employee_id}/leaveTypes"
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/home_address.rb b/lib/xeroizer/models/payroll/home_address.rb
new file mode 100644
index 00000000..9d5de35c
--- /dev/null
+++ b/lib/xeroizer/models/payroll/home_address.rb
@@ -0,0 +1,24 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class HomeAddressModel < PayrollBaseModel
+
+ end
+
+ class HomeAddress < PayrollBase
+
+ string :address_line1
+ string :address_line2
+ string :address_line3
+ string :address_line4
+ string :city
+ string :region
+ string :postal_code
+ string :country
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/leave.rb b/lib/xeroizer/models/payroll/leave.rb
new file mode 100644
index 00000000..f0f1a65e
--- /dev/null
+++ b/lib/xeroizer/models/payroll/leave.rb
@@ -0,0 +1,35 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class LeaveModel < PayrollBaseModel
+
+ set_permissions :read, :write, :update
+
+ def api_url(options = {})
+ "employees/#{options[:employee_id]}/leave"
+ end
+ end
+
+ class Leave < PayrollBase
+
+ set_primary_key :leave_id
+
+ guid :employee_id
+ guid :leave_id
+ guid :leave_type_id
+ string :description
+ date :start_date
+ date :end_date
+ datetime_utc :updated_date_utc
+
+ has_many :periods
+
+ def api_url
+ "employees/#{employee_id}/leave"
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/leave_accrual_line.rb b/lib/xeroizer/models/payroll/leave_accrual_line.rb
new file mode 100644
index 00000000..29a4e090
--- /dev/null
+++ b/lib/xeroizer/models/payroll/leave_accrual_line.rb
@@ -0,0 +1,21 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class LeaveAccrualLineModel < PayrollBaseModel
+
+ end
+
+ # http://developer.xero.com/documentation/payroll-api/payslip/#LeaveAccrualLine
+ class LeaveAccrualLine < PayrollBase
+ decimal :number_of_units
+
+ boolean :auto_calculate
+
+ guid :leave_type_id
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/leave_application.rb b/lib/xeroizer/models/payroll/leave_application.rb
index f1933344..88df4046 100644
--- a/lib/xeroizer/models/payroll/leave_application.rb
+++ b/lib/xeroizer/models/payroll/leave_application.rb
@@ -4,7 +4,7 @@ module Payroll
class LeaveApplicationModel < PayrollBaseModel
set_permissions :read, :write, :update
end
-
+
class LeaveApplication < PayrollBase
set_primary_key :leave_application_id
@@ -21,7 +21,9 @@ class LeaveApplication < PayrollBase
has_many :leave_periods
validates_presence_of :employee_id, :leave_type_id, :title, :start_date, :end_date
+
end
- end
+
+ end
end
end
diff --git a/lib/xeroizer/models/payroll/leave_balance.rb b/lib/xeroizer/models/payroll/leave_balance.rb
new file mode 100644
index 00000000..3b5184ad
--- /dev/null
+++ b/lib/xeroizer/models/payroll/leave_balance.rb
@@ -0,0 +1,32 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class LeaveBalanceModel < PayrollBaseModel
+
+ set_permissions :read, :write, :update
+
+ def api_url(options = {})
+ "employees/#{options.delete(:employee_id)}/leavebalances"
+ end
+ end
+
+ # child of Employee
+ class LeaveBalance < PayrollBase
+
+ # AU: # https://developer.xero.com/documentation/payroll-api/leavebalances/
+ string :leave_name
+ guid :leave_type_id
+ decimal :number_of_units
+ string :type_of_units
+
+ # UK: https://developer.xero.com/documentation/payroll-api-uk/employeeleavebalances
+ # NZ: https://developer.xero.com/documentation/payroll-api-nz/leavebalances
+ string :name
+ decimal :balance
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/leave_line.rb b/lib/xeroizer/models/payroll/leave_line.rb
index 5bd57754..9b018562 100644
--- a/lib/xeroizer/models/payroll/leave_line.rb
+++ b/lib/xeroizer/models/payroll/leave_line.rb
@@ -6,6 +6,7 @@ class LeaveLineModel < PayrollBaseModel
end
+ # child of PayTemplate
class LeaveLine < PayrollBase
LEAVE_TYPE_CALCULATION_TYPE = {
@@ -15,7 +16,8 @@ class LeaveLine < PayrollBase
} unless defined?(LEAVE_TYPE_CALCULATION_TYPE)
guid :leave_type_id, :api_name => 'LeaveTypeID'
- string :calculation_type
+ guid :leave_line_id
+ string :calculation_type # http://developer.xero.com/payroll-api/types-and-codes#LeaveTypeCalculationType
decimal :annual_number_of_units
decimal :full_time_number_of_units_per_period
@@ -27,4 +29,4 @@ class LeaveLine < PayrollBase
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/xeroizer/models/payroll/leave_type.rb b/lib/xeroizer/models/payroll/leave_type.rb
new file mode 100644
index 00000000..bd3c42fc
--- /dev/null
+++ b/lib/xeroizer/models/payroll/leave_type.rb
@@ -0,0 +1,28 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class LeaveTypeModel < PayrollBaseModel
+ set_permissions :read, :write, :update
+ end
+
+ class LeaveType < PayrollBase
+
+ string :name
+ string :type_of_units
+ boolean :is_paid_leave
+ boolean :show_on_payslip
+
+ guid :leave_type_id
+ decimal :normal_entitlement
+ decimal :leave_loading_rate
+
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
+
+ validates_presence_of :name, :type_of_units, :is_paid_leave, :show_on_payslip
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/number_of_unit.rb b/lib/xeroizer/models/payroll/number_of_unit.rb
new file mode 100644
index 00000000..0c54a546
--- /dev/null
+++ b/lib/xeroizer/models/payroll/number_of_unit.rb
@@ -0,0 +1,17 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class NumberOfUnitModel < PayrollArrayBaseModel
+
+ end
+
+ class NumberOfUnit < PayrollArrayBase
+
+ decimal :value
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/opening_balance.rb b/lib/xeroizer/models/payroll/opening_balance.rb
new file mode 100644
index 00000000..90fb7fb2
--- /dev/null
+++ b/lib/xeroizer/models/payroll/opening_balance.rb
@@ -0,0 +1,28 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class OpeningBalancesModel < PayrollBaseModel
+
+ set_standalone_model true
+ set_xml_root_name 'OpeningBalances'
+ set_xml_node_name 'OpeningBalances'
+ end
+
+ # child of Employee
+ class OpeningBalances < PayrollBase
+
+ set_primary_key false
+
+ date :opening_balance_date
+ has_many :earnings_lines
+ has_many :deduction_lines
+ has_many :super_lines
+ has_many :reimbursement_lines
+ has_many :leave_lines
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/pay_items.rb b/lib/xeroizer/models/payroll/pay_items.rb
index e9cd9aa3..eef9e62d 100644
--- a/lib/xeroizer/models/payroll/pay_items.rb
+++ b/lib/xeroizer/models/payroll/pay_items.rb
@@ -1,22 +1,27 @@
module Xeroizer
module Record
module Payroll
+ class PayItemModel < PayrollBaseModel
- class PayItemsModel < PayrollBaseModel
-
- set_permissions :read
+ set_permissions :read, :write, :update
+ set_standalone_model true
+ set_xml_root_name 'PayItems'
+ set_xml_node_name 'PayItems'
end
- class PayItems < PayrollBase
+ class PayItem < PayrollBase
+ set_primary_key false
- has_many :earnings_types
+ has_many :earnings_rates # AU
+ has_many :earnings_types # US
has_many :benefit_types
has_many :deduction_types
+ has_many :leave_types
has_many :reimbursement_types
has_many :time_off_types
-
end
+
end
end
end
diff --git a/lib/xeroizer/models/payroll/pay_run.rb b/lib/xeroizer/models/payroll/pay_run.rb
index d6fa3c73..0c7c9c6c 100644
--- a/lib/xeroizer/models/payroll/pay_run.rb
+++ b/lib/xeroizer/models/payroll/pay_run.rb
@@ -8,6 +8,7 @@ class PayRunModel < PayrollBaseModel
end
+ # http://developer.xero.com/documentation/payroll-api/payruns/
class PayRun < PayrollBase
set_primary_key :pay_run_id
@@ -27,6 +28,14 @@ class PayRun < PayrollBase
has_many :paystubs
+ guid :payroll_calendar_id
+
+
+ decimal :wages
+ decimal :super
+
+ string :payslip_message
+ has_many :payslips
end
end
end
diff --git a/lib/xeroizer/models/payroll/pay_template.rb b/lib/xeroizer/models/payroll/pay_template.rb
index 79d2e144..cf4e96cc 100644
--- a/lib/xeroizer/models/payroll/pay_template.rb
+++ b/lib/xeroizer/models/payroll/pay_template.rb
@@ -4,10 +4,22 @@ module Payroll
class PayTemplateModel < PayrollBaseModel
+ set_permissions :read, :write, :update
+
+ set_standalone_model true
+
+ set_xml_root_name 'PayTemplate'
+ set_xml_node_name 'PayTemplate'
+
+ def api_url(options = {})
+ "employees/#{options.delete(:employee_id)}/paytemplates"
+ end
end
class PayTemplate < PayrollBase
+ set_primary_key false
+
has_many :earnings_lines
has_many :deduction_lines
has_many :super_lines
@@ -16,9 +28,12 @@ class PayTemplate < PayrollBase
# US Payroll fields
has_many :benefit_lines
+ # UK: https://developer.xero.com/documentation/payroll-api-uk/employeepaytemplates
+ has_many :earning_templates
+ guid :employee_id
end
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/xeroizer/models/payroll/payroll_calendar.rb b/lib/xeroizer/models/payroll/payroll_calendar.rb
new file mode 100644
index 00000000..930e9f5a
--- /dev/null
+++ b/lib/xeroizer/models/payroll/payroll_calendar.rb
@@ -0,0 +1,28 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class PayrollCalendarModel < PayrollBaseModel
+
+ set_permissions :read, :write
+
+ end
+
+ class PayrollCalendar < PayrollBase
+
+ set_primary_key :payroll_calendar_id
+
+ guid :payroll_calendar_id
+
+ string :name
+ string :calendar_type # http://developer.xero.com/payroll-api/types-and-codes/#CalendarTypes
+ date :start_date
+ date :payment_date
+
+ validates_presence_of :name, :calendar_type, :start_date, :payment_date
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/payslip.rb b/lib/xeroizer/models/payroll/payslip.rb
new file mode 100644
index 00000000..6774a4ca
--- /dev/null
+++ b/lib/xeroizer/models/payroll/payslip.rb
@@ -0,0 +1,46 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class PayslipModel < PayrollBaseModel
+
+ set_api_controller_name 'Payslip'
+
+ set_api_response_padding 'Payslip'
+
+ set_permissions :read, :update
+
+ end
+
+ # http://developer.xero.com/documentation/payroll-api/payslip/
+ class Payslip < PayrollBase
+
+ set_primary_key :payslip_id
+
+ guid :payslip_id
+ guid :employee_id
+ guid :pay_run_id
+
+ string :first_name
+ string :last_name
+
+ decimal :wages
+ decimal :deductions
+ decimal :net_pay
+ decimal :tax
+ decimal :super
+ decimal :reimubrsements
+
+ has_many :earnings_lines, model_name: "EarningsLine"
+ has_many :tax_lines, model_name: "TaxLine"
+ has_many :timesheet_earnings_lines, model_name: "TimesheetEarningsLine"
+ has_many :deduction_lines, model_name: "DeductionLine"
+ has_many :leave_accrual_lines, model_name: "LeaveAccrualLine"
+ has_many :superannuation_lines, :model_name => "SuperannuationLine"
+ has_many :reimbursement_lines, model_name: "ReimbursementLine"
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/period.rb b/lib/xeroizer/models/payroll/period.rb
new file mode 100644
index 00000000..b17c9b01
--- /dev/null
+++ b/lib/xeroizer/models/payroll/period.rb
@@ -0,0 +1,27 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class PeriodModel < PayrollBaseModel
+ set_permissions :read
+
+ def api_url(options)
+ "employees/#{options.delete(:employee_id)}/leavePeriods?startDate=#{options[:start_date]}&endDate=#{options[:end_date]}"
+ end
+ end
+
+ class Period < PayrollBase
+
+ decimal :number_of_units
+ date :period_start_date
+ date :period_end_date
+ string :period_status
+
+ def to_api_json
+ JSON.parse(super)
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/reimbursement_line.rb b/lib/xeroizer/models/payroll/reimbursement_line.rb
index 5281574c..e50ec71c 100644
--- a/lib/xeroizer/models/payroll/reimbursement_line.rb
+++ b/lib/xeroizer/models/payroll/reimbursement_line.rb
@@ -6,6 +6,7 @@ class ReimbursementLineModel < PayrollBaseModel
end
+ # child of PayTemplate
class ReimbursementLine < PayrollBase
guid :reimbursement_type_id, :api_name => 'ReimbursementTypeID'
@@ -18,4 +19,4 @@ class ReimbursementLine < PayrollBase
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/xeroizer/models/payroll/reimbursement_type.rb b/lib/xeroizer/models/payroll/reimbursement_type.rb
index 63d250ae..89e2bd8b 100644
--- a/lib/xeroizer/models/payroll/reimbursement_type.rb
+++ b/lib/xeroizer/models/payroll/reimbursement_type.rb
@@ -12,10 +12,16 @@ class ReimbursementType < PayrollBase
set_primary_key :reimbursement_type_id
+ string :name
+ string :account_code # http://developer.xero.com/api/Accounts
+
guid :reimbursement_type_id
string :reimbursement_type
string :expense_or_liability_account_code
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
+
+ validates_presence_of :name, :account_code
end
end
end
diff --git a/lib/xeroizer/models/payroll/salary_and_wages.rb b/lib/xeroizer/models/payroll/salary_and_wages.rb
new file mode 100644
index 00000000..cacf6d42
--- /dev/null
+++ b/lib/xeroizer/models/payroll/salary_and_wages.rb
@@ -0,0 +1,32 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class SalaryAndWagesModel < PayrollBaseModel
+
+ set_permissions :read, :write, :update
+
+ def api_url(options = {})
+ "employees/#{options.delete(:employee_id)}/salaryAndWages"
+ end
+ end
+
+ class SalaryAndWages < PayrollBase
+
+ guid :salary_and_wages_id
+ guid :earnings_rate_id
+
+ decimal :number_of_units_per_week
+ decimal :rate_per_unit
+ decimal :number_of_units_per_day
+ decimal :annual_salary
+
+ datetime_utc :effective_from
+
+ string :status
+ string :payment_type
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/settings.rb b/lib/xeroizer/models/payroll/settings.rb
new file mode 100644
index 00000000..568d51cf
--- /dev/null
+++ b/lib/xeroizer/models/payroll/settings.rb
@@ -0,0 +1,27 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class SettingModel < PayrollBaseModel
+
+ set_permissions :read
+
+ set_standalone_model true
+ set_xml_root_name 'Settings'
+ set_xml_node_name 'Settings'
+ end
+
+ class Setting < PayrollBase
+
+ set_primary_key false
+
+ has_many :accounts
+ has_many :timesheet_categories, :model_name => 'TimesheetCategories'
+ has_many :employee_groups, :model_name => 'EmployeeGroups'
+ integer :days_in_payroll_year
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/super_fund.rb b/lib/xeroizer/models/payroll/super_fund.rb
new file mode 100644
index 00000000..5baf098a
--- /dev/null
+++ b/lib/xeroizer/models/payroll/super_fund.rb
@@ -0,0 +1,29 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class SuperFundModel < PayrollBaseModel
+ set_permissions :read, :write, :update
+ end
+
+ # https://developer.xero.com/documentation/payroll-api/superfunds
+ class SuperFund < PayrollBase
+
+ guid :super_fund_id
+ string :type # http://developer.xero.com/documentation/payroll-api/types-and-codes#SuperFundsTypes
+ string :abn, api_name: "ABN"
+
+ # this is for Regulated funds
+ string :usi, api_name: "USI"
+
+ # this is for self-managed funds
+ string :name
+ string :bsb, api_name: "BSB"
+ string :account_number
+ string :account_name
+ string :electronic_service_address
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/super_line.rb b/lib/xeroizer/models/payroll/super_line.rb
index 2e6835dc..27f72722 100644
--- a/lib/xeroizer/models/payroll/super_line.rb
+++ b/lib/xeroizer/models/payroll/super_line.rb
@@ -6,6 +6,7 @@ class SuperLineModel < PayrollBaseModel
end
+ # child of PayTemplate
class SuperLine < PayrollBase
SUPERANNUATION_CONTRIBUTION_TYPE = {
@@ -22,13 +23,15 @@ class SuperLine < PayrollBase
} unless defined?(SUPERANNUATION_CALCULATION_TYPE)
guid :super_membership_id, :api_name => 'SuperMembershipID'
- string :contribution_type
- string :calculation_type
+ string :contribution_type # http://developer.xero.com/payroll-api/types-and-codes#SuperannuationContributionType
+ string :calculation_type # http://developer.xero.com/payroll-api/types-and-codes#SuperannuationCalculationType
integer :expense_account_code
integer :liability_account_code
decimal :minimum_monthly_earnings
decimal :percentage
+ date :payment_date_for_this_period
+ decimal :amount
validates_presence_of :super_membership_id, :contribution_type, :calculation_type, :expense_account_code, :liability_account_code, :unless => :new_record?
validates_inclusion_of :contribution_type, :in => SUPERANNUATION_CONTRIBUTION_TYPE
@@ -37,4 +40,4 @@ class SuperLine < PayrollBase
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/xeroizer/models/payroll/super_membership.rb b/lib/xeroizer/models/payroll/super_membership.rb
new file mode 100644
index 00000000..4b30edf3
--- /dev/null
+++ b/lib/xeroizer/models/payroll/super_membership.rb
@@ -0,0 +1,20 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class SuperMembershipModel < PayrollBaseModel
+
+ end
+
+ # child of Employee
+ class SuperMembership < PayrollBase
+
+ guid :super_membership_id
+ guid :super_fund_id
+ string :employee_number
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/superannuation_line.rb b/lib/xeroizer/models/payroll/superannuation_line.rb
new file mode 100644
index 00000000..af826603
--- /dev/null
+++ b/lib/xeroizer/models/payroll/superannuation_line.rb
@@ -0,0 +1,28 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class SuperannuationLineModel < PayrollBaseModel
+
+ end
+
+ # child of Payslip. Same as SuperLine.
+ class SuperannuationLine < PayrollBase
+
+ guid :super_membership_id
+ string :contribution_type # http://developer.xero.com/payroll-api/types-and-codes#SuperannuationContributionType
+ string :calculation_type # http://developer.xero.com/payroll-api/types-and-codes#SuperannuationCalculationType
+ string :expense_account_code
+ string :liability_account_code
+
+ date :payment_date_for_this_period
+
+ decimal :minimum_monthly_earnings
+ decimal :percentage
+ decimal :amount
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/tax_declaration.rb b/lib/xeroizer/models/payroll/tax_declaration.rb
index 783b6f89..f4fc0f30 100644
--- a/lib/xeroizer/models/payroll/tax_declaration.rb
+++ b/lib/xeroizer/models/payroll/tax_declaration.rb
@@ -1,13 +1,12 @@
module Xeroizer
module Record
module Payroll
-
+
class TaxDeclarationModel < PayrollBaseModel
set_xml_node_name 'TaxDeclaration'
end
-
+
class TaxDeclaration < PayrollBase
-
EMPLOYMENT_BASIS = {
'FULLTIME' => '',
'PARTTIME' => '',
@@ -27,6 +26,8 @@ class TaxDeclaration < PayrollBase
string :tax_file_number
string :tfn_exemption_type, :api_name => 'TFNExemptionType'
+
+ guid :employee_id
string :employment_basis
boolean :australian_resident_for_tax_purposes
@@ -45,6 +46,6 @@ class TaxDeclaration < PayrollBase
validates_inclusion_of :tfn_exemption_type, :in => EMPLOYMENT_BASIS
end
- end
+ end
end
end
\ No newline at end of file
diff --git a/lib/xeroizer/models/payroll/tax_line.rb b/lib/xeroizer/models/payroll/tax_line.rb
new file mode 100644
index 00000000..c785ec1a
--- /dev/null
+++ b/lib/xeroizer/models/payroll/tax_line.rb
@@ -0,0 +1,24 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class TaxLineModel < PayrollBaseModel
+
+ end
+
+ # http://developer.xero.com/documentation/payroll-api/payslip/#TaxLine
+ class TaxLine < PayrollBase
+
+ string :tax_type_name
+ string :description
+ string :liability_account
+
+ decimal :amount
+
+ guid :payslip_tax_line_id
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/time_off_balance.rb b/lib/xeroizer/models/payroll/time_off_balance.rb
new file mode 100644
index 00000000..84616e04
--- /dev/null
+++ b/lib/xeroizer/models/payroll/time_off_balance.rb
@@ -0,0 +1,22 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class TimeOffBalanceModel < PayrollBaseModel
+
+ end
+
+ # child of Employee
+ # https://developer.xero.com/documentation/payroll-api-us/timeoff-balances/
+ class TimeOffBalance < PayrollBase
+
+ string :time_off_name
+ guid :time_off_type_id
+ decimal :number_of_units
+ string :type_of_units
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/timesheet.rb b/lib/xeroizer/models/payroll/timesheet.rb
new file mode 100644
index 00000000..2c9f103a
--- /dev/null
+++ b/lib/xeroizer/models/payroll/timesheet.rb
@@ -0,0 +1,40 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class TimesheetModel < PayrollBaseModel
+
+ set_permissions :read, :write, :update
+
+ end
+
+ class Timesheet < PayrollBase
+
+ set_primary_key :timesheet_id
+
+ guid :timesheet_id
+ guid :employee_id
+ guid :payroll_calendar_id # UK
+ date :start_date
+ date :end_date
+ decimal :hours
+ string :status
+
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
+
+ has_many :timesheet_lines
+
+ validates_presence_of :start_date, :end_date, :employee_id
+
+ def approve
+ params = extra_params_for_create_or_update
+ params[:url] = "Timesheets/#{attributes[:timesheet_id]}/approve"
+ response = parent.send(:http_post, {}, params)
+ parse_save_response(response)
+ end
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/timesheet_categories.rb b/lib/xeroizer/models/payroll/timesheet_categories.rb
new file mode 100644
index 00000000..fb1572ba
--- /dev/null
+++ b/lib/xeroizer/models/payroll/timesheet_categories.rb
@@ -0,0 +1,24 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class TimesheetCategoriesModel < PayrollBaseModel
+
+ set_standalone_model true
+ set_xml_root_name 'TimesheetCategories'
+ set_xml_node_name 'TimesheetCategories'
+ end
+
+ # child of TrackingCategories
+ class TimesheetCategories < PayrollBase
+
+ set_primary_key false
+
+ guid :tracking_category_id
+ string :tracking_category_name
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/timesheet_earnings_line.rb b/lib/xeroizer/models/payroll/timesheet_earnings_line.rb
new file mode 100644
index 00000000..cab521f7
--- /dev/null
+++ b/lib/xeroizer/models/payroll/timesheet_earnings_line.rb
@@ -0,0 +1,20 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class TimesheetEarningsLineModel < PayrollBaseModel
+
+ end
+
+ # child of Payslip
+ class TimesheetEarningsLine < PayrollBase
+
+ guid :earnings_rate_id
+ decimal :number_of_units
+ decimal :rate_per_unit
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/timesheet_line.rb b/lib/xeroizer/models/payroll/timesheet_line.rb
new file mode 100644
index 00000000..e6fbdeaa
--- /dev/null
+++ b/lib/xeroizer/models/payroll/timesheet_line.rb
@@ -0,0 +1,37 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class TimesheetLineModel < PayrollBaseModel
+
+ set_permissions :read, :write, :update # UK
+
+ end
+
+ class TimesheetLine < PayrollBase
+
+ # AU
+ guid :earnings_rate_id
+ guid :tracking_item_id
+
+ # USA
+ guid :earnings_type_id
+ guid :work_location_id
+
+ has_array :number_of_units, :api_child_name => 'NumberOfUnit'
+
+ # UK
+ datetime :date
+ guid :timesheet_id # used to make the URL
+
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
+
+ def api_url
+ "Timesheets/#{attributes[:timesheet_id]}/lines"
+ end
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/payroll/tracking_categories.rb b/lib/xeroizer/models/payroll/tracking_categories.rb
new file mode 100644
index 00000000..fbaa9a22
--- /dev/null
+++ b/lib/xeroizer/models/payroll/tracking_categories.rb
@@ -0,0 +1,35 @@
+module Xeroizer
+ module Record
+ module Payroll
+
+ class TrackingCategoriesModel < PayrollBaseModel
+
+ set_permissions :read
+
+ set_standalone_model true
+ set_xml_root_name 'TrackingCategories'
+ set_xml_node_name 'TrackingCategories'
+
+ def api_url(options = {})
+ if options.has_key?(:use_json_api) && options[:use_json_api]
+ "settings/trackingCategories"
+ end
+ end
+ end
+
+ # child of Settings
+ class TrackingCategories < PayrollBase
+
+ set_primary_key false
+
+ guid :employee_groups_tracking_category_id
+ guid :timesheet_tracking_category_id
+
+ has_many :employee_groups
+ has_many :timesheet_categories
+
+ end
+
+ end
+ end
+end
diff --git a/lib/xeroizer/models/user.rb b/lib/xeroizer/models/user.rb
index a4f2dc53..c71c67ac 100644
--- a/lib/xeroizer/models/user.rb
+++ b/lib/xeroizer/models/user.rb
@@ -1,6 +1,5 @@
module Xeroizer
module Record
-
class UserModel < BaseModel
set_api_controller_name 'User'
@@ -21,6 +20,5 @@ class User < Base
string :organisation_role
end
-
end
-end
+end
\ No newline at end of file
diff --git a/lib/xeroizer/partner_application.rb b/lib/xeroizer/partner_application.rb
index 28b891ce..b6416e39 100644
--- a/lib/xeroizer/partner_application.rb
+++ b/lib/xeroizer/partner_application.rb
@@ -17,6 +17,7 @@ class PartnerApplication < GenericApplication
# @return [PartnerApplication] instance of PrivateApplication
def initialize(consumer_key, consumer_secret, path_to_private_key, options = {})
default_options = {
+ :xero_url_prefix => 'https://api.xero.com',
:xero_url => 'https://api.xero.com/api.xro/2.0',
:site => 'https://api.xero.com',
:authorize_url => 'https://api.xero.com/oauth/Authorize',
diff --git a/lib/xeroizer/payroll_application.rb b/lib/xeroizer/payroll_application.rb
index e21a0913..01f14c1c 100644
--- a/lib/xeroizer/payroll_application.rb
+++ b/lib/xeroizer/payroll_application.rb
@@ -3,6 +3,12 @@ class PayrollApplication
attr_reader :application
+ extend Forwardable
+ def_delegators :application, :client, :request_token, :access_token, :authorize_from_request,
+ :authorize_from_access, :logger, :xero_url, :logger=, :xero_url=, :client,
+ :rate_limit_sleep, :rate_limit_max_attempts, # all from generic_application.rb
+ :renew_access_token, :expires_at, :authorization_expires_at, :session_handle # partner_application.rb
+
# Factory for new Payroll BaseModel instances with the class name `record_type`.
# Only creates the instance if one doesn't already exist.
#
@@ -23,7 +29,23 @@ def self.record(record_type)
record :Paystub
record :PayItems
record :PaySchedule
+ record :Timesheet
+ record :PayItem
+ record :PayrollCalendar
record :LeaveApplication
+ record :PayRun
+ record :Payslip
+ record :Setting
+ record :SuperFund
+ record :EarningsRate # UK
+ record :Leave # UK
+ record :Period # UK
+ record :LeaveType # UK
+ record :LeaveBalance # UK, NZ
+ record :TrackingCategories # UK
+ record :EmployeeLeaveType # UK
+ record :PayTemplate # UK
+ record :SalaryAndWages # UK
def initialize(application)
@application = application
diff --git a/lib/xeroizer/record/base.rb b/lib/xeroizer/record/base.rb
index a0534a5f..74a6cee3 100644
--- a/lib/xeroizer/record/base.rb
+++ b/lib/xeroizer/record/base.rb
@@ -3,6 +3,7 @@
require 'xeroizer/record/validation_helper'
require 'xeroizer/record/xml_helper'
require 'xeroizer/logging'
+require 'active_support/inflector'
module Xeroizer
module Record
@@ -17,7 +18,12 @@ class Base
attr_reader :model
attr_accessor :errors
attr_accessor :complete_record_downloaded
+
+ attr_writer :api_method_for_creating
+ attr_writer :api_method_for_updating
+
attr_accessor :paged_record_downloaded
+ attr_accessor :after_initialize
include ModelDefinitionHelper
include RecordAssociationHelper
@@ -30,8 +36,12 @@ class << self
def build(attributes, parent)
record = new(parent)
attributes.each do | key, value |
- attr = record.respond_to?("#{key}=") || record.class.fields[key].nil? ? key : record.class.fields[key][:internal_name]
- record.send("#{attr}=", value)
+ attr_to_set = if record.respond_to?("#{key}=")
+ key
+ elsif record.class.fields.key?(key)
+ record.class.fields[key][:internal_name]
+ end
+ record.send("#{attr_to_set}=", value) if attr_to_set
end
record
end
@@ -152,15 +162,22 @@ def inspect
"#<#{self.class} #{attribute_string}>"
end
- protected
+ # TODO Is this necessary with parent.create?
+ def api_method_for_creating
+ @api_method_for_creating || parent.create || :http_put
+ end
+ def api_method_for_updating
+ @api_method_for_updating || :http_post
+ end
+ protected
+
# Attempt to create a new record.
def create
- request = to_xml
+ request = json? ? to_api_json : to_xml
log "[CREATE SENT] (#{__FILE__}:#{__LINE__}) #{request}"
- response = parent.send(parent.create_method, request)
-
+ response = parent.send(api_method_for_creating, request, extra_params_for_create_or_update)
log "[CREATE RECEIVED] (#{__FILE__}:#{__LINE__}) #{response}"
parse_save_response(response)
@@ -172,17 +189,42 @@ def update
raise RecordKeyMustBeDefined.new(self.class.possible_primary_keys)
end
- request = to_xml
-
+ request = json? ? to_api_json : to_xml
log "[UPDATE SENT] (#{__FILE__}:#{__LINE__}) \r\n#{request}"
- response = parent.http_post(request)
+ response = parent.send(api_method_for_updating, request, extra_params_for_create_or_update)
log "[UPDATE RECEIVED] (#{__FILE__}:#{__LINE__}) \r\n#{response}"
parse_save_response(response)
end
+ def extra_params_for_create_or_update
+ json? ? {raw_body: true, content_type: "application/json", url: api_url} : {}
+ end
+
+ def api_url; end # individual models can override this
+
+ def to_api_json
+ attrs = self.attributes.reject {|k, v| k == :parent }.map do |k, v|
+ value = if v.respond_to?(:to_api_json)
+ v.to_api_json
+ elsif k == :periods # hack for leave request periods for xero uk
+ v.map(&:to_api_json)
+ elsif v.is_a?(Array) && [0, 1].include?(v.count)
+ v.first # hack for timesheet line has_array values
+ else
+ v
+ end
+ [k.to_s.camelize(:lower), value]
+ end
+ Hash[attrs].to_json
+ end
+
+ def json?
+ parent.application.api_format == :json
+ end
+
# Parse the response from a create/update request.
def parse_save_response(response_xml)
response = parent.parse_response(response_xml)
diff --git a/lib/xeroizer/record/base_model.rb b/lib/xeroizer/record/base_model.rb
index a0fd0545..2f8837c0 100644
--- a/lib/xeroizer/record/base_model.rb
+++ b/lib/xeroizer/record/base_model.rb
@@ -1,4 +1,5 @@
require 'xeroizer/record/base_model_http_proxy'
+require 'active_support/inflector'
module Xeroizer
module Record
@@ -21,6 +22,10 @@ class InvalidPermissionError < XeroizerError
DEFAULT_RECORDS_PER_BATCH_SAVE = 50
+ class_inheritable_attributes :standalone_model
+ class_inheritable_attributes :before_padding
+ class_inheritable_attributes :after_padding
+
include BaseModelHttpProxy
attr_reader :application
@@ -68,6 +73,25 @@ def set_optional_xml_root_name(optional_root_name)
self.optional_xml_root_name = optional_root_name
end
+ # Usually the xml structure will be
+ # If this is true, the tag isn't expected. So it would be
+ #
+ # Example: http://developer.xero.com/payroll-api/PayItems/#GET
+ def set_standalone_model(boolean)
+ self.standalone_model = boolean
+ end
+
+ # Usually the xml structure will be
+ # Provide wrapping if the response is
+ def set_api_response_padding(padding)
+ self.before_padding = "<#{padding.pluralize}><#{padding}>"
+ self.after_padding = "#{padding}>#{padding.pluralize}>"
+ end
+
+ def pad_api_response?
+ self.before_padding && self.after_padding
+ end
+
end
public
@@ -141,14 +165,14 @@ def find_in_batches(options = {}, &block)
# Helper method to retrieve just the first element from
# the full record list.
def first(options = {})
- raise MethodNotAllowed.new(self, :all) unless self.class.permissions[:read]
+ raise MethodNotAllowed.new(self, :all) unless self.class.permissions && self.class.permissions[:read]
result = all(options)
result.first if result.is_a?(Array)
end
# Retrieve record matching the passed in ID.
def find(id, options = {})
- raise MethodNotAllowed.new(self, :all) unless self.class.permissions[:read]
+ raise MethodNotAllowed.new(self, :all) unless self.class.permissions && self.class.permissions[:read]
response_xml = @application.http_get(@application.client, "#{url}/#{CGI.escape(id)}", options)
response = parse_response(response_xml, options)
result = response.response_items.first if response.response_items.is_a?(Array)
@@ -160,9 +184,9 @@ def save_records(records, chunk_size = DEFAULT_RECORDS_PER_BATCH_SAVE)
no_errors = true
return false unless records.all?(&:valid?)
- actions = records.group_by {|o| o.new_record? ? create_method : :http_post }
- actions.each_pair do |http_method, records_for_method|
- records_for_method.each_slice(chunk_size) do |some_records|
+ actions = records.group_by {|o| o.new_record? ? o.api_method_for_creating : o.api_method_for_updating }
+ actions.each_pair do |http_method, records_for_method|
+ records_for_method.each_slice(chunk_size) do |some_records|
request = to_bulk_xml(some_records)
response = parse_response(self.send(http_method, request, {:summarizeErrors => false}))
response.response_items.each_with_index do |record, i|
@@ -197,38 +221,108 @@ def batch_save(chunk_size = DEFAULT_RECORDS_PER_BATCH_SAVE)
end
def parse_response(response_xml, options = {})
- Response.parse(response_xml, options) do | response, elements, response_model_name |
- if model_name == response_model_name
- @response = response
- parse_records(response, elements, paged_records_requested?(options), (options[:base_module] || Xeroizer::Record))
+ format = options[:api_format] || @application.api_format
+ case format
+ when :json
+ parse_json_response(response_body, options)
+ else
+ parse_xml_response(response_body, options)
+ end
+ end
+
+ protected
+ def parse_json_response(response_body, options = {})
+ json = ::JSON.parse(response_body)
+ response = Response.new
+
+ model_name_to_parse =
+ if self.respond_to?(:model_name_to_parse)
+ self.model_name_to_parse
+ else
+ self.model_name
+ end
+
+ iterable = json[model_name_to_parse.camelize(:lower).pluralize] || json[model_name_to_parse.camelize(:lower)]
+ iterable = [iterable] if iterable.is_a?(Hash)
+
+ iterable.each {|object|
+ object = object.map {|key, value| [key.underscore.to_sym, value]}.to_h
+ response_object = self.model_class.build(object, self)
+ self.model_class.fields.each {|field, field_props|
+ if field_props[:type] == :has_many && object[field]
+ response_object[field] = []
+ object[field].each {|child_object|
+ model_class_name = field_props[:api_child_name].to_sym # TimesheetLine
+ model_klass_obj = response_object.new_model_class(model_class_name)
+ response_object[field] << model_klass_obj.model_class.build(child_object.map {|key, value| [key.underscore.to_sym, value]}.to_h, model_klass_obj)
+ }
+ end
+ }
+ response.response_items << response_object
+ }
+
+ response
+ end
+
+ def parse_xml_response(response_body, options = {})
+ Response.parse(response_body, options) do | response, elements, response_model_name |
+ if self.class.pad_api_response?
+ @response = response
+ parse_records(response, Nokogiri::XML("#{self.class.before_padding}#{elements.to_xml}#{self.class.after_padding}").root.elements, paged_records_requested?(options), (options[:base_module] || Xeroizer::Record))
+ elsif model_name == response_model_name
+ @response = response
+ parse_records(response, elements, paged_records_requested?(options), (options[:base_module] || Xeroizer::Record))
+ elsif self.class.standalone_model && self.class.xml_root_name == elements.first.parent.name
+ @response = response
+ parse_records(response, elements, paged_records_requested?(options), (options[:base_module] || Xeroizer::Record), true)
+ end
end
end
end
+ protected
def create_method
:http_put
end
- protected
-
-
def paged_records_requested?(options)
options.has_key?(:page) and options[:page].to_i >= 0
end
# Parse the records part of the XML response and builds model instances as necessary.
- def parse_records(response, elements, paged_results, base_module)
+ def parse_records(response, elements, paged_results, base_module, standalone_model = false)
elements.each do | element |
- new_record = model_class.build_from_node(element, self, base_module)
+ new_record = model_class.build_from_node(element, self, base_module, standalone_model)
if element.attribute('status').try(:value) == 'ERROR'
new_record.errors = []
element.xpath('.//ValidationError').each do |err|
new_record.errors << err.text.gsub(/^\s+/, '').gsub(/\s+$/, '')
end
end
- new_record.paged_record_downloaded = paged_results
+
+ if standalone_model
+ if response.response_items.count == 0
+ new_record.paged_record_downloaded = paged_results
+ else
+ # http://developer.xero.com/documentation/payroll-api/settings/
+ # tracking categories have subcategories of timesheet categoires and employee groups
+ # which we group together here as it's much easier to model
+ fields_to_fill = model_class.fields.find_all do |f|
+ new_record_field = new_record[f[0]]
+ if new_record_field.respond_to?(:count)
+ new_record_field.count > 0
+ else
+ !new_record_field.nil?
+ end
+ end
+ fields_to_fill.each {|field| response.response_items.first[field[0]] = new_record[field[0]]}
+ end
+ else
+ new_record.paged_record_downloaded = paged_results
+ end
response.response_items << new_record
end
+ response.response_items
end
def to_bulk_xml(records, builder = Builder::XmlMarkup.new(:indent => 2))
diff --git a/lib/xeroizer/record/base_model_http_proxy.rb b/lib/xeroizer/record/base_model_http_proxy.rb
index daf8c689..77d90d6a 100644
--- a/lib/xeroizer/record/base_model_http_proxy.rb
+++ b/lib/xeroizer/record/base_model_http_proxy.rb
@@ -37,6 +37,8 @@ def parse_params(options)
params[:DateFrom] = options[:date_from] if options[:date_from]
params[:DateTo] = options[:date_to] if options[:date_to]
params[:page] = options[:page] if options[:page]
+ params[:response] = options[:api_format] || @application.api_format
+ params[:url] = self.send(:api_url, options) if self.respond_to?(:api_url)
params
end
@@ -115,9 +117,9 @@ def where_condition_part(field, expression, value)
when :boolean then [field[:api_name], expression, value ? 'true' : 'false']
when :integer then [field[:api_name], expression, value.to_s]
when :decimal then [field[:api_name], expression, value.to_s]
- when :date then [field[:api_name], expression, "DateTime.Parse(\"#{value.strftime("%Y-%m-%d")}\")"]
- when :datetime then [field[:api_name], expression, "DateTime.Parse(\"#{value.utc.strftime("%Y-%m-%dT%H:%M:%S")}\")"]
- when :datetime_utc then [field[:api_name], expression, "DateTime.Parse(\"#{value.utc.strftime("%Y-%m-%dT%H:%M:%S")}\")"]
+ when :date then [field[:api_name], expression, "DateTime(#{value.strftime("%Y,%m,%d")})"]
+ when :datetime then [field[:api_name], expression, "DateTime(#{value.utc.strftime("%Y,%m,%d,%H,%M,%S")})"]
+ when :datetime_utc then [field[:api_name], expression, "DateTime(#{value.utc.strftime("%Y-%m-%dT%H:%M:%S")})"]
when :belongs_to then
when :has_many then
when :has_one then
diff --git a/lib/xeroizer/record/model_definition_helper.rb b/lib/xeroizer/record/model_definition_helper.rb
index e2da46bc..cc48a9be 100644
--- a/lib/xeroizer/record/model_definition_helper.rb
+++ b/lib/xeroizer/record/model_definition_helper.rb
@@ -16,8 +16,9 @@ def set_possible_primary_keys(*args)
end
# Set the actual Xero primary key for this record.
+ # Set this to `false` if this type has no primary key
def set_primary_key(primary_key_name)
- self.primary_key_name = primary_key_name
+ self.primary_key_name = primary_key_name unless primary_key_name == false
end
# Whether this record type's list results contain summary data only.
@@ -65,6 +66,7 @@ def define_simple_attribute(field_name, field_type, options, value_if_nil = nil)
self.fields[field_name] = options.merge({
:internal_name => internal_field_name,
:api_name => options[:api_name] || field_name.to_s.camelize,
+ :api_child_name => options[:api_child_name] || (options[:api_name] || field_name.to_s.camelize).singularize,
:type => field_type
})
define_method internal_field_name do
@@ -85,7 +87,7 @@ module InstanceMethods
# Returns the value of the Xero primary key for this record if it exists.
def id
- self[self.class.primary_key_name]
+ self[self.class.primary_key_name] unless self.class.primary_key_name.nil?
end
# Sets the value of the Xero primary key for this record if it exists.
diff --git a/lib/xeroizer/record/payroll_array_base.rb b/lib/xeroizer/record/payroll_array_base.rb
new file mode 100644
index 00000000..fe54a3c8
--- /dev/null
+++ b/lib/xeroizer/record/payroll_array_base.rb
@@ -0,0 +1,17 @@
+module Xeroizer
+ module Record
+
+ class PayrollArrayBase < PayrollBase
+
+ class_inheritable_attributes :fields, :possible_primary_keys, :primary_key_name, :summary_only, :validators
+
+ def self.build(value, parent)
+ record = new(parent)
+ record.value = value
+ record
+ end
+
+ end
+
+ end
+end
diff --git a/lib/xeroizer/record/payroll_array_base_model.rb b/lib/xeroizer/record/payroll_array_base_model.rb
new file mode 100644
index 00000000..bf654c72
--- /dev/null
+++ b/lib/xeroizer/record/payroll_array_base_model.rb
@@ -0,0 +1,21 @@
+module Xeroizer
+ module Record
+
+ class PayrollArrayBaseModel < PayrollBaseModel
+
+ class_inheritable_attributes :api_controller_name
+ class_inheritable_attributes :permissions
+ class_inheritable_attributes :xml_root_name
+ class_inheritable_attributes :optional_xml_root_name
+ class_inheritable_attributes :xml_node_name
+
+ attr_accessor :value
+
+ def to_s
+ value
+ end
+
+ end
+
+ end
+end
diff --git a/lib/xeroizer/record/payroll_base.rb b/lib/xeroizer/record/payroll_base.rb
index ddbc4984..05d1bc35 100644
--- a/lib/xeroizer/record/payroll_base.rb
+++ b/lib/xeroizer/record/payroll_base.rb
@@ -9,6 +9,10 @@ def self.belongs_to(field_name, options = {})
super(field_name, {:base_module => Xeroizer::Record::Payroll}.merge(options))
end
+ def self.has_array(field_name, options = {})
+ super(field_name, {:base_module => Xeroizer::Record::Payroll}.merge(options))
+ end
+
def self.has_many(field_name, options = {})
super(field_name, {:base_module => Xeroizer::Record::Payroll}.merge(options))
end
@@ -19,6 +23,12 @@ def self.has_one(field_name, options = {})
public
+ def initialize(parent)
+ super(parent)
+ self.api_method_for_creating = :http_post
+ self.api_method_for_updating = :http_post
+ end
+
def new_model_class(model_name)
Xeroizer::Record::Payroll.const_get("#{model_name}Model".to_sym).new(parent.application, model_name.to_s)
end
diff --git a/lib/xeroizer/record/record_association_helper.rb b/lib/xeroizer/record/record_association_helper.rb
index f0a9b7c1..dc34358c 100644
--- a/lib/xeroizer/record/record_association_helper.rb
+++ b/lib/xeroizer/record/record_association_helper.rb
@@ -16,6 +16,7 @@ def belongs_to(field_name, options = {})
# Create a #build_record_name method to build the record.
define_method "build_#{internal_singular_field_name}" do | *args |
+
attributes = args.size == 1 ? args.first : {}
# The name of the record model.
@@ -27,9 +28,56 @@ def belongs_to(field_name, options = {})
# Create a new record, binding it to it's parent instance.
record = (options[:base_module] || Xeroizer::Record).const_get(model_name).build(attributes, model_parent)
self.attributes[field_name] = record
+ self.parent.mark_dirty(self) if self.parent
+ end
+ end
+
+ def has_array(field_name, options = {})
+ internal_field_name = options[:internal_name] || field_name
+ internal_plural_field_name = options[:internal_name_plural] || internal_field_name.to_s
+
+ define_association_attribute(field_name, internal_field_name, :has_array, options)
+
+ define_method "add_to_#{internal_plural_field_name}" do | *args |
+ # The name of the record model.
+ model_name = options[:model_name] ? options[:model_name].to_sym : field_name.to_s.singularize.camelize.to_sym
+
+ # The record's parent instance for this current application.
+ model_parent = new_model_class(model_name)
+
+ # The class of this record.
+ record_class = (options[:base_module] || Xeroizer::Record).const_get(model_name)
+
+ # Parse the *args variable so that we can use this method like:
+ # add_to_records([value, value, value])
+ # add_to_records(value)
+ values = []
+ if args.size == 1 && args.first.is_a?(Array)
+ values = args.first
+ elsif args.size > 0
+ values = args
+ else
+ raise StandardError.new("Invalid arguments for #{self.class.name}#add_#{internal_singular_field_name}(#{args.inspect}).")
+ end
+
+ # Ensure that complete record is downloaded before adding new records
+ self.send(field_name)
+
+ # Add each value.
+ last_record = nil
+ values.each do |value|
+ record = record_class.build(value, model_parent)
+ raise StandardError.new("Record #{record.class.name} is not a #{record_class.name}.") unless record.is_a?(record_class)
+ self.attributes[field_name] ||= []
+ self.attributes[field_name] << record
+ self.parent.mark_dirty(self) if self.parent
+ last_record = record
+ end
+
+ last_record
end
end
-
+
alias_method :has_one, :belongs_to
def has_many(field_name, options = {})
@@ -73,6 +121,7 @@ def has_many(field_name, options = {})
raise XeroizerError.new("Record #{record.class.name} is not a #{record_class.name}.") unless record.is_a?(record_class)
self.attributes[field_name] ||= []
self.attributes[field_name] << record
+ self.parent.mark_dirty(self) if self.parent
last_record = record
end
@@ -82,16 +131,16 @@ def has_many(field_name, options = {})
end
def define_association_attribute(field_name, internal_field_name, association_type, options)
- define_simple_attribute(field_name, association_type, options.merge!(:skip_writer => true), ((association_type == :has_many) ? [] : nil))
-
+ define_simple_attribute(field_name, association_type, options.merge!(:skip_writer => true), value_if_nil(association_type))
+
internal_field_name = options[:internal_name] || field_name
internal_singular_field_name = options[:internal_name_singular] || internal_field_name.to_s.singularize
model_name = options[:model_name] ? options[:model_name].to_sym : field_name.to_s.singularize.camelize.to_sym
define_method "#{internal_field_name}=".to_sym do | value |
record_class = (options[:base_module] || Xeroizer::Record).const_get(model_name)
case value
- when Hash
- self.attributes[field_name] = ((association_type == :has_many) ? [] : nil)
+ when Hash
+ self.attributes[field_name] = self.class.value_if_nil(association_type)
case association_type
when :has_many
self.attributes[field_name] = []
@@ -99,11 +148,10 @@ def define_association_attribute(field_name, internal_field_name, association_ty
when :belongs_to
self.attributes[field_name] = (options[:base_module] || Xeroizer::Record).const_get(model_name).build(value, new_model_class(model_name))
-
end
when Array
- self.attributes[field_name] = ((association_type == :has_many) ? [] : nil)
+ self.attributes[field_name] = self.class.value_if_nil(association_type)
value.each do | single_value |
case single_value
when Hash then send("add_#{internal_singular_field_name}".to_sym, single_value)
@@ -113,11 +161,18 @@ def define_association_attribute(field_name, internal_field_name, association_ty
end
when record_class
- self.attributes[field_name] = ((association_type == :has_many) ? [value] : value)
+ self.attributes[field_name] = self.class.value_if_nil(association_type, value)
when NilClass
self.attributes[field_name] = []
+ when Float, Fixnum
+ if record_class.fields.count == 1 && record_class.fields.keys.first == :value
+ self.attributes[field_name] = self.class.value_if_nil(association_type, value)
+ else
+ raise AssociationTypeMismatch.new(record_class, value.class)
+ end
+
else
raise AssociationTypeMismatch.new(record_class, value.class)
end
@@ -129,11 +184,22 @@ def define_association_attribute(field_name, internal_field_name, association_ty
if list_contains_summary_only?
define_method internal_field_name do
download_complete_record! unless new_record? || options[:list_complete] || options[:complete_on_page] && paged_record_downloaded? || complete_record_downloaded?
- self.attributes[field_name] || ((association_type == :has_many) ? [] : nil)
+ self.attributes[field_name] || self.class.value_if_nil(association_type)
end
end
end
+ def value_if_nil(association_type, boxed_value = nil)
+ case association_type
+ when :has_many
+ [boxed_value].compact
+ when :has_array
+ [boxed_value].compact
+ when :belongs_to
+ boxed_value
+ end
+ end
+
end
end
diff --git a/lib/xeroizer/record/xml_helper.rb b/lib/xeroizer/record/xml_helper.rb
index 54d40339..6874b465 100644
--- a/lib/xeroizer/record/xml_helper.rb
+++ b/lib/xeroizer/record/xml_helper.rb
@@ -12,10 +12,11 @@ def self.included(base)
module ClassMethods
# Build a record instance from the XML node.
- def build_from_node(node, parent, base_module)
+ def build_from_node(node, parent, base_module, standalone_model = false)
record = new(parent)
node.elements.each do | element |
- field = self.fields[element.name.to_s.underscore.to_sym]
+ element_name = standalone_model ? element.name.to_s.pluralize : element.name.to_s
+ field = self.fields[element_name.underscore.to_sym]
if field
value = case field[:type]
when :guid then element.text
@@ -31,27 +32,71 @@ def build_from_node(node, parent, base_module)
base_module.const_get(model_name).build_from_node(element, parent, base_module)
when :has_many
+ if element.element_children.size > 0
+ sub_field_name = field[:model_name] ? field[:model_name].to_sym : (standalone_model ? element.name : element.children.first.name).to_sym
+ sub_parent = record.new_model_class(sub_field_name)
+
+ if standalone_model
+ base_module.const_get(sub_field_name).build_from_node(element, sub_parent, base_module)
+ else
+ remove_empty_text_nodes(element.children).inject([]) do | list, inner_element |
+ list << base_module.const_get(sub_field_name).build_from_node(inner_element, sub_parent, base_module)
+ end
+ end
+ else
+ []
+ end
+
+ when :has_array
if element.element_children.size > 0
sub_field_name = field[:model_name] ? field[:model_name].to_sym : element.children.first.name.to_sym
sub_parent = record.new_model_class(sub_field_name)
- element.children.inject([]) do | list, inner_element |
- list << base_module.const_get(sub_field_name).build_from_node(inner_element, sub_parent, base_module)
+ element.element_children.inject([]) do |list, child|
+ list << base_module.const_get(sub_field_name).build_from_node(child, sub_parent, base_module)
end
+ else
+ []
end
end
if field[:calculated]
record.attributes[field[:internal_name]] = value
+ elsif standalone_model
+ record.send("add_#{field[:internal_name].to_s.singularize}".to_sym, value)
else
record.send("#{field[:internal_name]}=".to_sym, value)
end
end
end
+ # special case for array models eg. NumberOfUnit(s) in http://developer.xero.com/documentation/payroll-api/timesheets/
+ if node.elements.empty? && parent.is_a?(Xeroizer::Record::PayrollArrayBaseModel)
+ field = self.fields[:value]
+ if field
+ element = node.children.first
+ value = case field[:type]
+ when :guid then element.text
+ when :string then element.text
+ when :boolean then (element.text == 'true')
+ when :integer then element.text.to_i
+ when :decimal then BigDecimal(element.text)
+ when :date then Date.parse(element.text)
+ when :datetime then Time.parse(element.text)
+ when :datetime_utc then ActiveSupport::TimeZone['UTC'].parse(element.text).utc
+ end
+
+ record.value = value
+ end
+ end
+
parent.mark_clean(record)
record
end
+ private
+ def remove_empty_text_nodes(children)
+ children.find_all {|c| !c.respond_to?(:text) || !c.text.strip.empty?}
+ end
end
module InstanceMethods
@@ -114,8 +159,15 @@ def xml_value_from_field(b, field, value)
when :datetime then b.tag!(field[:api_name], value.utc.strftime("%Y-%m-%dT%H:%M:%S"))
when :belongs_to
- value.to_xml(b)
- nil
+ value_is_present = (value.respond_to?(:blank?) && !value.blank?) || (!value.nil? && (!value.respond_to?(:length) || (value.respond_to?(:length) && value.length != 0)))
+ # you may need to set a belongs_to to nil, at which point it defaults back to an array
+ # but in that case we don't want to include it in the XML response
+ if value_is_present
+ value.to_xml(b)
+ nil
+ else
+ nil
+ end
when :has_many
if value.size > 0
@@ -126,6 +178,16 @@ def xml_value_from_field(b, field, value)
nil
end
+ when :has_array
+ if value.size > 0
+ b.tag!(field[:api_name]) do
+ value.each do |v|
+ b.tag!(field[:api_child_name], v.value)
+ end
+ end
+ nil
+ end
+
end
end
diff --git a/lib/xeroizer/report/aged_receivables_by_contact.rb b/lib/xeroizer/report/aged_receivables_by_contact.rb
index e622527b..37aa8f59 100644
--- a/lib/xeroizer/report/aged_receivables_by_contact.rb
+++ b/lib/xeroizer/report/aged_receivables_by_contact.rb
@@ -5,7 +5,7 @@ class AgedReceivablesByContact < Base
public
def total
- @_total_cache ||= summary.cell(:Total).value
+ @_total_cache ||= summary.cell(:Total).value
end
def total_paid
@@ -22,9 +22,9 @@ def total_due
def total_overdue
return @_total_due_cache if @_total_due_cache
-
+
now = Time.now
- @_total_due_cache = sum(:Due) do | row |
+ @_total_due_cache = sum(:Due) do | row |
due_date = row.cell('Due Date').value
due_date && due_date < now
end
@@ -36,7 +36,7 @@ def sum(column_name, &block)
sum
end
end
-
+
end
end
end
diff --git a/lib/xeroizer/scopes.rb b/lib/xeroizer/scopes.rb
new file mode 100644
index 00000000..9de3e0d9
--- /dev/null
+++ b/lib/xeroizer/scopes.rb
@@ -0,0 +1,15 @@
+module Xeroizer
+ class Scopes
+ def self.all_payroll
+ ['Employees', 'LeaveApplications', 'PayItems', 'PayrollCalendars', 'PayRuns', 'Payslip', 'SuperFunds', 'Settings', 'Timesheets'].map {|s| "payroll.#{s.downcase}"}.join(',')
+ end
+
+ def self.au_payroll
+ all_payroll
+ end
+
+ def self.us_payroll
+ ['Employees', 'PayItems', 'PaySchedules', 'PayRuns', 'Paystubs', 'Worklocations', 'Settings', 'Timesheets'].map {|s| "payroll.#{s.downcase}"}.join(',')
+ end
+ end
+end
\ No newline at end of file
diff --git a/privatekey.pem b/privatekey.pem
new file mode 100644
index 00000000..e11a8af1
--- /dev/null
+++ b/privatekey.pem
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQDsJmYfpxau21Z1/7khZsiB6ZKX/BeurXUtX4LAEUNDjZe7Ytv1
+Vi+iWTkig+QXDO0YRiKqX6rLpQWlJDjIt6rzjSa8FEr6KBC+U2PBEv98FjQ8ZfvL
+ty9vCewIrjk0iCifzgv/K14pA+aoJRE8IRmnoVVkDiGNj2Ds/YGkImgjYQIDAQAB
+AoGALDsI97bBDeExMYrDLxlkRsjr1yG1gdclUmlIQRc6pQV5PPTIRAxvgZX6mJdh
+elvGcLx6M7UVdW0kQIknRZj5IKbR9HP8SbuUtaKeofDAK2UFtVbnYB9qkWeYR0+q
+IjDI1l+m7gK+AEg6/WVHC6woE94GTgSK1tBgN7HjjjEZkA0CQQD9qzvWf/9Sy2zP
+KLq/5iEVMNHX5PmiqMqA7sPTtaQ4ij55snnlDHMKf8QxHXSMDlhmkU7vimQiKnto
+UEh6KALfAkEA7lHziAB1fIWXGqerbcVTeJ3tI4YlbDVfesBvi7aEN3JZ5Q8YP9/n
+BXNeqRfRnAPdFfkzR5MGZ+osjJhcdPjhvwJAI2KdiEB2p2AFH6i41EgP2VrkCs/A
+GvacuPuViZTPAawXJvbEljT0X0SPY6KOPXNK1ZPzhOqzKSjv6g847QFj1QJAcRtz
++Zg+Kls82+m38uE0PIq3gaSpHjI2nou2ZRi6p5YeFBiV6braajvXMWmcke9DfqpH
+LDEbWTZK7m9hciKtAQJBALBcn/xLbuP7xnn8SVdqEfxgK8L2rVJvrSl+yepohLHG
+QGOpLqvJUjG4mU8Go5YPwrkyDdKpqsbxKB7ax1OGLoc=
+-----END RSA PRIVATE KEY-----
diff --git a/public_privatekey.pfx b/public_privatekey.pfx
new file mode 100644
index 00000000..0b2a6168
Binary files /dev/null and b/public_privatekey.pfx differ
diff --git a/publickey.cer b/publickey.cer
new file mode 100644
index 00000000..1b9c1fea
--- /dev/null
+++ b/publickey.cer
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDYDCCAsmgAwIBAgIJAMWGDPGFjZcwMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpRdWVlbnNsYW5kMQ8wDQYDVQQHEwZOdW5kYWgxDzAN
+BgNVBAoTBlBheUF1czEYMBYGA1UEAxMPQWxleCBHaGljdWxlc2N1MR4wHAYJKoZI
+hvcNAQkBFg9hbGV4QHBheWF1cy5jb20wHhcNMTMwNTMwMDUyMjE0WhcNMTQwNTMw
+MDUyMjE0WjB+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKUXVlZW5zbGFuZDEPMA0G
+A1UEBxMGTnVuZGFoMQ8wDQYDVQQKEwZQYXlBdXMxGDAWBgNVBAMTD0FsZXggR2hp
+Y3VsZXNjdTEeMBwGCSqGSIb3DQEJARYPYWxleEBwYXlhdXMuY29tMIGfMA0GCSqG
+SIb3DQEBAQUAA4GNADCBiQKBgQDsJmYfpxau21Z1/7khZsiB6ZKX/BeurXUtX4LA
+EUNDjZe7Ytv1Vi+iWTkig+QXDO0YRiKqX6rLpQWlJDjIt6rzjSa8FEr6KBC+U2PB
+Ev98FjQ8ZfvLty9vCewIrjk0iCifzgv/K14pA+aoJRE8IRmnoVVkDiGNj2Ds/YGk
+ImgjYQIDAQABo4HlMIHiMB0GA1UdDgQWBBTAfUPCLmW+/3WCAHmN3yZWeDDypjCB
+sgYDVR0jBIGqMIGngBTAfUPCLmW+/3WCAHmN3yZWeDDypqGBg6SBgDB+MQswCQYD
+VQQGEwJBVTETMBEGA1UECBMKUXVlZW5zbGFuZDEPMA0GA1UEBxMGTnVuZGFoMQ8w
+DQYDVQQKEwZQYXlBdXMxGDAWBgNVBAMTD0FsZXggR2hpY3VsZXNjdTEeMBwGCSqG
+SIb3DQEJARYPYWxleEBwYXlhdXMuY29tggkAxYYM8YWNlzAwDAYDVR0TBAUwAwEB
+/zANBgkqhkiG9w0BAQUFAAOBgQDcecLBQTiyfrtqZHXNdpJj0BzAvyFO4D9PuenM
+CYTe/hYncSaapngNrFUuakUpN/zVag/PoV0Ag0HNBsyexq12gCkbdhhtAYD/hXYV
+4/i2Tgy1H5ct6kBv2u/e5GtSGrVKiIHWcTF5tPGlXg75w49yi9iyxBNDjNFFyYpM
+hCpGmg==
+-----END CERTIFICATE-----
diff --git a/test/dummy_script.rb b/test/dummy_script.rb
new file mode 100644
index 00000000..fe341a51
--- /dev/null
+++ b/test/dummy_script.rb
@@ -0,0 +1,26 @@
+require "pp"
+require "logger"
+require 'rubygems'
+require 'debugger'
+require 'net-http-spy'
+
+Net::HTTP.http_logger_options = {verbose: true, body: true, trace: true}
+
+require File.dirname(__FILE__) + "/../lib/xeroizer"
+
+xero = Xeroizer::PrivateApplication.new(
+ 'JRKZQGSECLTD6PZBLVRDKZGGELNRI1',
+ 'QXJLLZK63AM29DO0GPRIR3ABPNZYRM',
+ File.dirname(__FILE__) + "/../privatekey.pem"
+ ).payroll
+
+#xero = Xeroizer::PublicApplication.new(
+# 'JRKZQGSECLTD6PZBLVRDKZGGELNRI1',
+# 'QXJLLZK63AM29DO0GPRIR3ABPNZYRM'
+# ).payroll
+#url = xero.request_token(:oauth_callback => 'http://developer.xero.com/payroll-api/')
+
+xero.logger = Logger.new(STDOUT)
+
+puts xero.Employee.url
+puts xero.Timesheet.url
\ No newline at end of file
diff --git a/test/stub_responses/payroll_employees.xml b/test/stub_responses/payroll_employees.xml
new file mode 100644
index 00000000..ae8713c9
--- /dev/null
+++ b/test/stub_responses/payroll_employees.xml
@@ -0,0 +1,98 @@
+
+ a1b8088e-4166-48e9-9e29-5b5919cac0a0
+ OK
+ Xeroizer test
+ 2013-05-30T05:25:03.0599507Z
+
+
+ fb4ebd68-6568-41eb-96ab-628a0f54b4b8
+ James
+ Lebron
+ ACTIVE
+ JL@madeup.email.com
+ 1978-08-13T00:00:00
+ M
+ 0400-000-123
+ 408-230-9732
+ 2012-01-30T00:00:00
+ 72e962d1-fcac-4083-8a71-742bb3e7ae14
+ cb8e4706-2fdc-4170-aebd-0ffb855557f5
+ 2013-04-01T23:02:36
+
+
+ 007ebe02-4696-4453-a37d-fcca764a35a2
+ John
+ Smith
+ ACTIVE
+ 2013-04-01T23:27:35
+
+
+ de8599f6-04da-429a-a0cd-74c0da042c2b
+ Odette
+ Garrison
+ ACTIVE
+ ogg@madeup.email.com
+ 1975-05-18T00:00:00
+ F
+ 9000 1234
+ 2012-04-04T00:00:00
+ 72e962d1-fcac-4083-8a71-742bb3e7ae14
+ 97e0784f-6c3e-4371-a177-f93fbada3c58
+ 2013-04-01T23:02:36
+
+
+ 33c0dccf-cc2d-44ef-97d7-64041ca914cc
+ Oliver
+ Gray
+ ACTIVE
+ og@madeup.email.com
+ 1965-12-26T00:00:00
+ M
+ 0401 123 456
+ 2012-07-02T00:00:00
+ 72e962d1-fcac-4083-8a71-742bb3e7ae14
+ cb8e4706-2fdc-4170-aebd-0ffb855557f5
+ 2013-04-01T23:02:36
+
+
+ 8a1618c6-306c-4f9b-b9cd-d637cc1b3da0
+ Sally
+ Martin
+ ACTIVE
+ sm@madeup3993.com
+ 1983-11-26T00:00:00
+ F
+ 0400 123 456
+ 2012-12-30T00:00:00
+ 72e962d1-fcac-4083-8a71-742bb3e7ae14
+ cb8e4706-2fdc-4170-aebd-0ffb855557f5
+ 2013-04-01T23:02:36
+
+
+ ff135c75-0121-48e3-8cac-128cfcc69b99
+ Sonia
+ Michaels
+ ACTIVE
+ som@madeup3993.com
+ 1986-01-22T00:00:00
+ F
+ 02 2345 6789
+
+ 2013-04-01T23:02:36
+
+
+ e8645dca-46ab-4026-a8d1-1c389378de50
+ Tracy
+ Green
+ ACTIVE
+ tg@madeup.email.com
+ 1985-08-04T00:00:00
+ F
+ 0402 123 456
+ 2012-05-01T00:00:00
+ 72e962d1-fcac-4083-8a71-742bb3e7ae14
+ 97e0784f-6c3e-4371-a177-f93fbada3c58
+ 2013-04-01T23:02:36
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/payroll_leave_applications.xml b/test/stub_responses/payroll_leave_applications.xml
new file mode 100644
index 00000000..88dda278
--- /dev/null
+++ b/test/stub_responses/payroll_leave_applications.xml
@@ -0,0 +1,15 @@
+
+ a1b8088e-4166-48e9-9e29-5b5919cac0a0
+ OK
+ Xeroizer test
+ 2013-05-30T05:25:03.0599507Z
+
+
+ fb4ebd68-6568-41eb-96ab-628a0f54b4b8
+ ACTIVE
+ LeBron
+ James
+ 2013-04-18T01:29:53
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/payroll_pay_items.xml b/test/stub_responses/payroll_pay_items.xml
new file mode 100644
index 00000000..c8b35c88
--- /dev/null
+++ b/test/stub_responses/payroll_pay_items.xml
@@ -0,0 +1,203 @@
+
+ a1b8088e-4166-48e9-9e29-5b5919cac0a0
+ OK
+ Xeroizer test
+ 2013-05-30T05:25:03.0599507Z
+
+
+
+ eca71b79-edab-4c3f-967f-a405453bac08
+ Ordinary Hours
+ ORDINARYTIMEEARNINGS
+ RATEPERUNIT
+ 477
+ Hours
+ false
+ false
+ true
+ 2013-04-09T23:45:25
+
+
+ f8eb8381-7db0-4de3-ada0-e03bbd1d811f
+ Overtime Hours (exempt from super)
+ OVERTIMEEARNINGS
+ MULTIPLE
+ 477
+ 1.5000
+ false
+ true
+ true
+ false
+ 2013-04-09T23:45:25
+
+
+ 694d0065-f594-48f7-89c0-5cfd7a0c4c92
+ Allowances subject to tax withholding and super
+ ALLOWANCE
+ RATEPERUNIT
+ 477
+ Fixed Amount
+ false
+ false
+ true
+ 2013-04-09T23:45:25
+
+
+ 92d6ae67-4302-4561-9000-90163cac8eb8
+ Allowances exempt from tax withholding and super
+ ALLOWANCE
+ RATEPERUNIT
+ 477
+ Fixed Amount
+ true
+ true
+ true
+ 2013-04-09T23:45:25
+
+
+ 683f87af-50f7-4584-9b0b-93d943537945
+ ETP Type O Payments Subject To Tax
+ EMPLOYMENTTERMINATIONPAYMENT
+ FIXEDAMOUNT
+ 477
+ false
+ true
+ false
+ 2018-02-07T01:01:25
+ O
+
+
+
+
+ f9be43ee-1876-4e67-a295-2a30eadd1942
+ Union Fees/Subscriptions
+ 850
+ false
+ false
+ 2013-04-09T23:45:25
+
+
+ f70fcea1-157c-4a1a-8724-369160e5cb5c
+ Lease Payments
+ 850
+ true
+ true
+ 2013-04-09T23:45:25
+
+
+ 8d6e580f-37e1-48cf-80fe-e591008a251f
+ FBT
+ 850
+ true
+ true
+ 2013-04-09T23:45:25
+
+
+ eabff3b0-2fdc-4e3b-955c-30e113829c74
+ Other Pre-Tax Deductions
+ 850
+ true
+ true
+ 2013-04-09T23:45:25
+
+
+ c0b2f074-883e-451b-8863-185fd2403cef
+ Other Post-Tax Deductions
+ 850
+ false
+ false
+ 2013-04-09T23:45:25
+
+
+
+
+ d4263307-f387-4c69-9525-7482d742ef50
+ Travel National Costs
+ 493
+ 2013-04-09T23:45:25
+
+
+ 7ecdc711-ea65-469a-ab9d-2a63f0753129
+ Other Reimbursable Costs
+ 850
+ 2013-04-09T23:45:25
+
+
+
+
+ bd2ead8d-f8f9-480a-8a71-260222ed1a34
+ Annual Leave
+ Hours
+ 152.0000
+ true
+ true
+ 2013-04-09T23:45:25
+
+
+ cd61fd41-671b-4d1f-ad6e-fbcb1eca3fc3
+ Long Service Leave
+ Hours
+ true
+ false
+ 2013-04-09T23:45:25
+
+
+ c06359d4-ee61-4f74-9c74-f94ef6a850e3
+ Parental Leave (unpaid)
+ Hours
+ false
+ false
+ 2013-04-09T23:45:25
+
+
+ 271814d6-1087-45f2-923a-8301113a298b
+ Personal/Carer's Leave
+ Hours
+ 76.0000
+ true
+ false
+ 2013-04-09T23:45:25
+
+
+ f3a5f2f8-c14e-4223-ab58-4e779e0f6f2c
+ Carer's Leave (unpaid)
+ Hours
+ false
+ false
+ 2013-04-09T23:45:25
+
+
+ 30ca48d0-fe04-40c2-b590-f5191159259f
+ Compassionate Leave (paid)
+ Hours
+ true
+ false
+ 2013-04-09T23:45:25
+
+
+ 1e1765bc-1dbc-4ac9-b6cd-9f9f3290d8a4
+ Compassionate Leave (unpaid)
+ Hours
+ false
+ false
+ 2013-04-09T23:45:25
+
+
+ 42d46756-9f2b-4456-ad44-8c889ff4a5bd
+ Community Service Leave
+ Hours
+ false
+ false
+ 2013-04-09T23:45:25
+
+
+ 7ea96a9f-d7c2-4de1-a3ac-bf89980f1890
+ Other Unpaid Leave
+ Hours
+ false
+ false
+ 2013-04-09T23:45:25
+
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/payroll_pay_runs.xml b/test/stub_responses/payroll_pay_runs.xml
new file mode 100644
index 00000000..b26cd55d
--- /dev/null
+++ b/test/stub_responses/payroll_pay_runs.xml
@@ -0,0 +1,34 @@
+
+ a1b8088e-4166-48e9-9e29-5b5919cac0a0
+ OK
+ Xeroizer test
+ 2013-05-30T05:25:03.0599507Z
+
+
+ 260.04
+ 18831.25
+ e3bdb2f7-2b20-45e6-ac8d-ec67d17de9f4
+ 2012-01-07T00:00:00
+ 2012-01-01T00:00:00
+ Posted
+ 2012-01-08T00:00:00
+ bfac31bd-ea62-4fc8-a5e7-7965d9504b15
+ 2539.97
+ 6651.00
+ 25742.29
+
+
+ 260.04
+ 22463.25
+ 7c998e04-1cee-4a19-bfe6-3cbfd5cb9cea
+ 2012-01-14T00:00:00
+ 2012-01-08T00:00:00
+ Posted
+ 2012-01-15T00:00:00
+ bfac31bd-ea62-4fc8-a5e7-7965d9504b15
+ 2892.78
+ 6939.00
+ 29662.29
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/payroll_payroll_calendars.xml b/test/stub_responses/payroll_payroll_calendars.xml
new file mode 100644
index 00000000..f3d4b418
--- /dev/null
+++ b/test/stub_responses/payroll_payroll_calendars.xml
@@ -0,0 +1,29 @@
+
+ a1b8088e-4166-48e9-9e29-5b5919cac0a0
+ OK
+ Xeroizer test
+ 2013-05-30T05:25:03.0599507Z
+
+
+ FORTNIGHTLY
+ Fortnightly Calendar
+ 2012-08-17T00:00:00Z
+ a17394fe-fa23-4d4a-8e2f-a19217bc6b4f
+ 2012-08-01T00:00:00
+
+
+ WEEKLY
+ Weekly Calendar
+ 2012-05-20T00:00:00Z
+ bfac31bd-ea62-4fc8-a5e7-7965d9504b15
+ 2012-05-13T00:00:00
+
+
+ WEEKLY
+ What
+ 2012-11-16T00:00:00Z
+ 49713875-ad73-492c-b6ac-2d265a5fe862
+ 2012-11-08T00:00:00
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/payroll_timesheets.xml b/test/stub_responses/payroll_timesheets.xml
new file mode 100644
index 00000000..41b17f5d
--- /dev/null
+++ b/test/stub_responses/payroll_timesheets.xml
@@ -0,0 +1,69 @@
+
+ a1b8088e-4166-48e9-9e29-5b5919cac0a0
+ OK
+ Xeroizer test
+ 2013-05-30T05:25:03.0599507Z
+
+
+ 049765fc-4506-48fb-bf88-3578dec0ec47
+ 72a0d0c2-0cf8-4f0b-ade1-33231f47b41b
+ 2013-02-01T00:00:00Z
+ 2013-02-07T00:00:00Z
+ Processed
+ 31.0000
+
+
+ 966c5c77-2ef0-4320-b6a9-6c27b080ecc5
+
+ 7.00
+ 6.00
+ 6.00
+ 5.00
+ 0.00
+ 0.00
+ 0.00
+
+ 2013-03-21T20:14:28
+
+
+ 677b6175-dc1c-4255-99e2-157117ae5bcc
+
+ 3.00
+ 4.00
+ 0.00
+ 0.00
+ 0.00
+ 0.00
+ 0.00
+
+ 2013-03-21T20:14:28
+
+
+ 2013-03-21T20:14:28
+
+
+ 6ab1c708-a958-4788-8aa6-9a1d1a660900
+ 72a0d0c2-0cf8-4f0b-ade1-33231f47b41b
+ 2013-02-08T00:00:00Z
+ 2013-02-14T00:00:00Z
+ Processed
+ 30.0000
+
+
+ 966c5c77-2ef0-4320-b6a9-6c27b080ecc5
+
+ 8.00
+ 8.00
+ 8.00
+ 6.00
+ 0.00
+ 0.00
+ 0.00
+
+ 2013-03-21T20:14:28
+
+
+ 2013-03-21T20:14:28
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_employee-007ebe02-4696-4453-a37d-fcca764a35a2.xml b/test/stub_responses/records/payroll_employee-007ebe02-4696-4453-a37d-fcca764a35a2.xml
new file mode 100644
index 00000000..419db7b1
--- /dev/null
+++ b/test/stub_responses/records/payroll_employee-007ebe02-4696-4453-a37d-fcca764a35a2.xml
@@ -0,0 +1,15 @@
+
+ 007ebe02-4696-4453-a37d-fcca764a35a2
+ OK
+ Demo AU
+ 2011-05-31T01:18:05.1105867Z
+
+
+ 007ebe02-4696-4453-a37d-fcca764a35a2
+ John
+ Smith
+ ACTIVE
+ 2013-04-01T23:27:35
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_employee-33c0dccf-cc2d-44ef-97d7-64041ca914cc.xml b/test/stub_responses/records/payroll_employee-33c0dccf-cc2d-44ef-97d7-64041ca914cc.xml
new file mode 100644
index 00000000..c9cce18a
--- /dev/null
+++ b/test/stub_responses/records/payroll_employee-33c0dccf-cc2d-44ef-97d7-64041ca914cc.xml
@@ -0,0 +1,22 @@
+
+ 33c0dccf-cc2d-44ef-97d7-64041ca914cc
+ OK
+ Demo AU
+ 2011-05-31T01:18:05.1105867Z
+
+
+ 33c0dccf-cc2d-44ef-97d7-64041ca914cc
+ Oliver
+ Gray
+ ACTIVE
+ og@madeup.email.com
+ 1965-12-26T00:00:00
+ M
+ 0401 123 456
+ 2012-07-02T00:00:00
+ 72e962d1-fcac-4083-8a71-742bb3e7ae14
+ cb8e4706-2fdc-4170-aebd-0ffb855557f5
+ 2013-04-01T23:02:36
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_employee-8a1618c6-306c-4f9b-b9cd-d637cc1b3da0.xml b/test/stub_responses/records/payroll_employee-8a1618c6-306c-4f9b-b9cd-d637cc1b3da0.xml
new file mode 100644
index 00000000..d8a2fda7
--- /dev/null
+++ b/test/stub_responses/records/payroll_employee-8a1618c6-306c-4f9b-b9cd-d637cc1b3da0.xml
@@ -0,0 +1,22 @@
+
+ 8a1618c6-306c-4f9b-b9cd-d637cc1b3da0
+ OK
+ Demo AU
+ 2011-05-31T01:18:05.1105867Z
+
+
+ 8a1618c6-306c-4f9b-b9cd-d637cc1b3da0
+ Sally
+ Martin
+ ACTIVE
+ sm@madeup3993.com
+ 1983-11-26T00:00:00
+ F
+ 0400 123 456
+ 2012-12-30T00:00:00
+ 72e962d1-fcac-4083-8a71-742bb3e7ae14
+ cb8e4706-2fdc-4170-aebd-0ffb855557f5
+ 2013-04-01T23:02:36
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_employee-de8599f6-04da-429a-a0cd-74c0da042c2b.xml b/test/stub_responses/records/payroll_employee-de8599f6-04da-429a-a0cd-74c0da042c2b.xml
new file mode 100644
index 00000000..1ae0c97e
--- /dev/null
+++ b/test/stub_responses/records/payroll_employee-de8599f6-04da-429a-a0cd-74c0da042c2b.xml
@@ -0,0 +1,22 @@
+
+ de8599f6-04da-429a-a0cd-74c0da042c2b
+ OK
+ Demo AU
+ 2011-05-31T01:18:05.1105867Z
+
+
+ de8599f6-04da-429a-a0cd-74c0da042c2b
+ Odette
+ Garrison
+ ACTIVE
+ ogg@madeup.email.com
+ 1975-05-18T00:00:00
+ F
+ 9000 1234
+ 2012-04-04T00:00:00
+ 72e962d1-fcac-4083-8a71-742bb3e7ae14
+ 97e0784f-6c3e-4371-a177-f93fbada3c58
+ 2013-04-01T23:02:36
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_employee-e8645dca-46ab-4026-a8d1-1c389378de50.xml b/test/stub_responses/records/payroll_employee-e8645dca-46ab-4026-a8d1-1c389378de50.xml
new file mode 100644
index 00000000..f37a4064
--- /dev/null
+++ b/test/stub_responses/records/payroll_employee-e8645dca-46ab-4026-a8d1-1c389378de50.xml
@@ -0,0 +1,22 @@
+
+ e8645dca-46ab-4026-a8d1-1c389378de50
+ OK
+ Demo AU
+ 2011-05-31T01:18:05.1105867Z
+
+
+ e8645dca-46ab-4026-a8d1-1c389378de50
+ Tracy
+ Green
+ ACTIVE
+ tg@madeup.email.com
+ 1985-08-04T00:00:00
+ F
+ 0402 123 456
+ 2012-05-01T00:00:00
+ 72e962d1-fcac-4083-8a71-742bb3e7ae14
+ 97e0784f-6c3e-4371-a177-f93fbada3c58
+ 2013-04-01T23:02:36
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_employee-fb4ebd68-6568-41eb-96ab-628a0f54b4b8.xml b/test/stub_responses/records/payroll_employee-fb4ebd68-6568-41eb-96ab-628a0f54b4b8.xml
new file mode 100644
index 00000000..33ea6b6c
--- /dev/null
+++ b/test/stub_responses/records/payroll_employee-fb4ebd68-6568-41eb-96ab-628a0f54b4b8.xml
@@ -0,0 +1,161 @@
+
+ fb4ebd68-6568-41eb-96ab-628a0f54b4b8
+ OK
+ Demo AU
+ 2011-05-31T01:18:05.1105867Z
+
+
+ fb4ebd68-6568-41eb-96ab-628a0f54b4b8
+ James
+ Lebron
+ ACTIVE
+ JL@madeup.email.com
+ 1978-08-13T00:00:00
+ M
+
+ 123 Main St
+ St. Kilda
+ VIC
+ 3182
+ AUSTRALIA
+
+ 0400-000-123
+ 408-230-9732
+ 2012-01-30T00:00:00
+ 72e962d1-fcac-4083-8a71-742bb3e7ae14
+ cb8e4706-2fdc-4170-aebd-0ffb855557f5
+ 2013-04-01T23:02:36
+ false
+
+ false
+ true
+ true
+ false
+ false
+ false
+ 2013-04-26T00:04:48
+
+
+
+ Salary
+ James Lebron Savings
+ 122344
+ 345678
+ false
+ 20.0000
+
+
+ Salary
+ James Lebron
+ 123443
+ 2345678
+ true
+
+
+
+ 2012-07-01T00:00:00
+
+
+ 72e962d1-fcac-4083-8a71-742bb3e7ae14
+ 13333.33
+
+
+
+
+ 59cd9d04-4521-4cc3-93ac-7841651ff407
+ 40.00
+
+
+ 4000.00
+
+
+ 4333d5cd-53a5-4c31-98e5-a8b4e5676b0b
+ SGC
+ 3999.99
+
+
+
+
+
+ 742998cb-7584-4ecf-aa88-d694f59c50f9
+ 100.0000
+
+
+ 3bc7b584-a49f-40c6-ac5e-891f382785c9
+ 40.0000
+
+
+
+
+
+
+ 72e962d1-fcac-4083-8a71-742bb3e7ae14
+ ANNUALSALARY
+ 40000.00
+ 38.0000
+
+
+
+
+ 59cd9d04-4521-4cc3-93ac-7841651ff407
+ FIXEDAMOUNT
+ 10.00
+
+
+
+
+ 4333d5cd-53a5-4c31-98e5-a8b4e5676b0b
+ SGC
+ PERCENTAGEOFEARNINGS
+ 450.00
+ 478
+ 826
+ 9.0000
+
+
+ 4333d5cd-53a5-4c31-98e5-a8b4e5676b0b
+ SALARYSACRIFICE
+ FIXEDAMOUNT
+ 478
+ 826
+ 50.0000
+
+
+
+
+
+ 742998cb-7584-4ecf-aa88-d694f59c50f9
+ FIXEDAMOUNTEACHPERIOD
+ 152.0000
+
+
+ 3bc7b584-a49f-40c6-ac5e-891f382785c9
+ FIXEDAMOUNTEACHPERIOD
+ 76.0000
+
+
+
+
+
+ 4333d5cd-53a5-4c31-98e5-a8b4e5676b0b
+ 2187a42b-639a-45cb-9eed-cd4ae488306a
+ 1234
+
+
+
+
+ Annual Leave
+ 544d9292-4329-4512-bfff-a9f15236d776
+ 81.2602
+ Hours
+
+
+ Personal/Carer's Leave
+ 5fbec239-131c-4ebc-87c4-47db887a85d1
+ 45.8302
+ Hours
+
+
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_employee-ff135c75-0121-48e3-8cac-128cfcc69b99.xml b/test/stub_responses/records/payroll_employee-ff135c75-0121-48e3-8cac-128cfcc69b99.xml
new file mode 100644
index 00000000..c9d5b6ef
--- /dev/null
+++ b/test/stub_responses/records/payroll_employee-ff135c75-0121-48e3-8cac-128cfcc69b99.xml
@@ -0,0 +1,20 @@
+
+ ff135c75-0121-48e3-8cac-128cfcc69b99
+ OK
+ Demo AU
+ 2011-05-31T01:18:05.1105867Z
+
+
+ ff135c75-0121-48e3-8cac-128cfcc69b99
+ Sonia
+ Michaels
+ ACTIVE
+ som@madeup3993.com
+ 1986-01-22T00:00:00
+ F
+ 02 2345 6789
+
+ 2013-04-01T23:02:36
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_leave_application-fb4ebd68-6568-41eb-96ab-628a0f54b4b8.xml b/test/stub_responses/records/payroll_leave_application-fb4ebd68-6568-41eb-96ab-628a0f54b4b8.xml
new file mode 100644
index 00000000..770adc67
--- /dev/null
+++ b/test/stub_responses/records/payroll_leave_application-fb4ebd68-6568-41eb-96ab-628a0f54b4b8.xml
@@ -0,0 +1,25 @@
+
+ a1b8088e-4166-48e9-9e29-5b5919cac0a0
+ OK
+ Xeroizer test
+ 2013-05-30T05:25:03.0599507Z
+
+
+ e0eb6747-7c17-4075-b804-989f8d4e5d39
+ fb4ebd68-6568-41eb-96ab-628a0f54b4b8
+ 742998cb-7584-4ecf-aa88-d694f59c50f9
+
+
+ 2013-04-29T00:00:00
+ 2013-05-12T00:00:00
+ SCHEDULED
+ 22.8000
+
+
+ Annual Leave
+ 2013-05-08T00:00:00
+ 2013-05-10T00:00:00
+ 2013-04-01T23:02:36
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_pay_run-7c998e04-1cee-4a19-bfe6-3cbfd5cb9cea.xml b/test/stub_responses/records/payroll_pay_run-7c998e04-1cee-4a19-bfe6-3cbfd5cb9cea.xml
new file mode 100644
index 00000000..50d463e8
--- /dev/null
+++ b/test/stub_responses/records/payroll_pay_run-7c998e04-1cee-4a19-bfe6-3cbfd5cb9cea.xml
@@ -0,0 +1,62 @@
+
+ 49713875-ad73-492c-b6ac-2d265a5fe862
+ OK
+ Demo AU
+ 2011-05-31T01:18:05.1105867Z
+
+
+ 260.04
+ 18831.25
+ 7c998e04-1cee-4a19-bfe6-3cbfd5cb9cea
+ 2012-01-07T00:00:00
+ 2012-01-01T00:00:00
+ Posted
+ 2012-01-08T00:00:00
+ bfac31bd-ea62-4fc8-a5e7-7965d9504b15
+
+
+ 0.00
+ f3f29c96-77d7-44f0-80fc-0e87dea2320d
+ Bucky
+ 2012-03-27T23:33:35
+ Lasek
+ 441.00
+ 2d3a0cf2-6b1c-499d-a2bb-dda0cdad7634
+ 0.00
+ 43.56
+ 43.00
+ 484.00
+
+
+ 0.00
+ dcdb554d-2dbf-4a4f-99dd-c3fc6fafb7f3
+ Chet
+ 2012-03-27T23:33:39
+ Taylor
+ 1261.20
+ 3586f5b4-9a04-4c2d-8ab6-3e1b34de0b62
+ 0.00
+ 151.31
+ 420.00
+ 1681.20
+
+
+ 0.00
+ c3896c4a-da46-489c-89c6-e48bcef655ef
+ Ray
+ 2012-03-27T23:41:28
+ Barbee
+ 889.33
+ c7196baa-3248-4801-ad7b-ae353b7b2f8c
+ 0.00
+ 100.11
+ 223.00
+ 1112.33
+
+
+ 2539.97
+ 6651.00
+ 25742.29
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_pay_run-e3bdb2f7-2b20-45e6-ac8d-ec67d17de9f4.xml b/test/stub_responses/records/payroll_pay_run-e3bdb2f7-2b20-45e6-ac8d-ec67d17de9f4.xml
new file mode 100644
index 00000000..47b7ca55
--- /dev/null
+++ b/test/stub_responses/records/payroll_pay_run-e3bdb2f7-2b20-45e6-ac8d-ec67d17de9f4.xml
@@ -0,0 +1,62 @@
+
+ 49713875-ad73-492c-b6ac-2d265a5fe862
+ OK
+ Demo AU
+ 2011-05-31T01:18:05.1105867Z
+
+
+ 260.04
+ 18831.25
+ e3bdb2f7-2b20-45e6-ac8d-ec67d17de9f4
+ 2012-01-07T00:00:00
+ 2012-01-01T00:00:00
+ Posted
+ 2012-01-08T00:00:00
+ bfac31bd-ea62-4fc8-a5e7-7965d9504b15
+
+
+ 0.00
+ f3f29c96-77d7-44f0-80fc-0e87dea2320d
+ Bucky
+ 2012-03-27T23:33:35
+ Lasek
+ 441.00
+ 2d3a0cf2-6b1c-499d-a2bb-dda0cdad7634
+ 0.00
+ 43.56
+ 43.00
+ 484.00
+
+
+ 0.00
+ dcdb554d-2dbf-4a4f-99dd-c3fc6fafb7f3
+ Chet
+ 2012-03-27T23:33:39
+ Taylor
+ 1261.20
+ 3586f5b4-9a04-4c2d-8ab6-3e1b34de0b62
+ 0.00
+ 151.31
+ 420.00
+ 1681.20
+
+
+ 0.00
+ c3896c4a-da46-489c-89c6-e48bcef655ef
+ Ray
+ 2012-03-27T23:41:28
+ Barbee
+ 889.33
+ c7196baa-3248-4801-ad7b-ae353b7b2f8c
+ 0.00
+ 100.11
+ 223.00
+ 1112.33
+
+
+ 2539.97
+ 6651.00
+ 25742.29
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_payroll_calendar-49713875-ad73-492c-b6ac-2d265a5fe862.xml b/test/stub_responses/records/payroll_payroll_calendar-49713875-ad73-492c-b6ac-2d265a5fe862.xml
new file mode 100644
index 00000000..4376f7c0
--- /dev/null
+++ b/test/stub_responses/records/payroll_payroll_calendar-49713875-ad73-492c-b6ac-2d265a5fe862.xml
@@ -0,0 +1,15 @@
+
+ 49713875-ad73-492c-b6ac-2d265a5fe862
+ OK
+ Demo AU
+ 2011-05-31T01:18:05.1105867Z
+
+
+ WEEKLY
+ What
+ 2012-11-16T00:00:00Z
+ 49713875-ad73-492c-b6ac-2d265a5fe862
+ 2012-11-08T00:00:00
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_payroll_calendar-a17394fe-fa23-4d4a-8e2f-a19217bc6b4f.xml b/test/stub_responses/records/payroll_payroll_calendar-a17394fe-fa23-4d4a-8e2f-a19217bc6b4f.xml
new file mode 100644
index 00000000..5572b624
--- /dev/null
+++ b/test/stub_responses/records/payroll_payroll_calendar-a17394fe-fa23-4d4a-8e2f-a19217bc6b4f.xml
@@ -0,0 +1,15 @@
+
+ a17394fe-fa23-4d4a-8e2f-a19217bc6b4f
+ OK
+ Demo AU
+ 2011-05-31T01:18:05.1105867Z
+
+
+ FORTNIGHTLY
+ Fortnightly Calendar
+ 2012-08-17T00:00:00Z
+ a17394fe-fa23-4d4a-8e2f-a19217bc6b4f
+ 2012-08-01T00:00:00
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_payroll_calendar-bfac31bd-ea62-4fc8-a5e7-7965d9504b15.xml b/test/stub_responses/records/payroll_payroll_calendar-bfac31bd-ea62-4fc8-a5e7-7965d9504b15.xml
new file mode 100644
index 00000000..c82e695e
--- /dev/null
+++ b/test/stub_responses/records/payroll_payroll_calendar-bfac31bd-ea62-4fc8-a5e7-7965d9504b15.xml
@@ -0,0 +1,15 @@
+
+ bfac31bd-ea62-4fc8-a5e7-7965d9504b15
+ OK
+ Demo AU
+ 2011-05-31T01:18:05.1105867Z
+
+
+ WEEKLY
+ Weekly Calendar
+ 2012-05-20T00:00:00Z
+ bfac31bd-ea62-4fc8-a5e7-7965d9504b15
+ 2012-05-13T00:00:00
+
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_timesheet-049765fc-4506-48fb-bf88-3578dec0ec47.xml b/test/stub_responses/records/payroll_timesheet-049765fc-4506-48fb-bf88-3578dec0ec47.xml
new file mode 100644
index 00000000..ee513045
--- /dev/null
+++ b/test/stub_responses/records/payroll_timesheet-049765fc-4506-48fb-bf88-3578dec0ec47.xml
@@ -0,0 +1,43 @@
+
+ 049765fc-4506-48fb-bf88-3578dec0ec47
+ OK
+ Xeroizer test
+ 2013-05-30T05:25:03.0599507Z
+
+ 049765fc-4506-48fb-bf88-3578dec0ec47
+ 72a0d0c2-0cf8-4f0b-ade1-33231f47b41b
+ 2013-02-01T00:00:00
+ 2013-02-07T00:00:00
+ Processed
+ 31.0000
+
+
+ 966c5c77-2ef0-4320-b6a9-6c27b080ecc5
+
+ 7.00
+ 6.00
+ 6.00
+ 5.00
+ 0.00
+ 0.00
+ 0.00
+
+ 2013-03-21T20:14:28
+
+
+ 677b6175-dc1c-4255-99e2-157117ae5bcc
+
+ 3.00
+ 4.00
+ 0.00
+ 0.00
+ 0.00
+ 0.00
+ 0.00
+
+ 2013-03-21T20:14:28
+
+
+ 2013-03-21T20:14:28
+
+
\ No newline at end of file
diff --git a/test/stub_responses/records/payroll_timesheet-6ab1c708-a958-4788-8aa6-9a1d1a660900.xml b/test/stub_responses/records/payroll_timesheet-6ab1c708-a958-4788-8aa6-9a1d1a660900.xml
new file mode 100644
index 00000000..0d787329
--- /dev/null
+++ b/test/stub_responses/records/payroll_timesheet-6ab1c708-a958-4788-8aa6-9a1d1a660900.xml
@@ -0,0 +1,30 @@
+
+ 6ab1c708-a958-4788-8aa6-9a1d1a660900
+ OK
+ Xeroizer test
+ 2013-05-30T05:25:03.0599507Z
+
+ 6ab1c708-a958-4788-8aa6-9a1d1a660900
+ 72a0d0c2-0cf8-4f0b-ade1-33231f47b41b
+ 2013-02-08T00:00:00Z
+ 2013-02-14T00:00:00Z
+ Processed
+ 30.0000
+
+
+ 966c5c77-2ef0-4320-b6a9-6c27b080ecc5
+
+ 8.00
+ 8.00
+ 8.00
+ 6.00
+ 0.00
+ 0.00
+ 0.00
+
+ 2013-03-21T20:14:28
+
+
+ 2013-03-21T20:14:28
+
+
\ No newline at end of file
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 88a1048f..3f59c70f 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,5 +1,4 @@
require "rubygems"
-
require 'test/unit'
require 'mocha'
require 'shoulda'
@@ -36,7 +35,12 @@ module TestHelper
GUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ unless defined?(GUID_REGEX)
def get_file_as_string(filename)
- File.read(File.dirname(__FILE__) + "/stub_responses/" + filename)
+ if File.exist?(File.dirname(__FILE__) + "/stub_responses/" + filename)
+ File.read(File.dirname(__FILE__) + "/stub_responses/" + filename)
+ else
+ puts "WARNING: File does not exist: #{filename}"
+ nil
+ end
end
def get_record_xml(type, id = nil)
@@ -52,14 +56,45 @@ def get_report_xml(report_type)
end
def mock_api(model_name)
- @client.stubs(:http_get).with {|client, url, params| url =~ /#{model_name}$/ }.returns(get_record_xml(model_name.underscore.pluralize.to_s.to_sym))
- @client.send(model_name.singularize.to_s.to_sym).all.each do | record |
- @client.stubs(:http_get).with {|client, url, params| url =~ /#{model_name}\/#{record.id}$/ }.returns(get_record_xml(model_name.underscore.singularize.to_s.to_sym, record.id))
+ client_for_stubbing.stubs(:http_get).with {|client, url, params| url =~ /#{model_name}$/ }.returns(get_record_xml("#{model_name_for_file(model_name).underscore.pluralize}".to_sym))
+
+ @client.send("#{model_name.singularize}".to_sym).all.each do | record |
+ next if record.id.nil?
+
+ client_for_stubbing.stubs(:http_get).with {|client, url, params| url =~ /#{model_name}\/#{record.id}$/ }.returns(get_record_xml("#{model_name_for_file(model_name).underscore.singularize}".to_sym, record.id))
+ end
+ end
+
+ # some models have a parent-child relationship, where you should call:
+ # Child.find(parent.id) to find items of type child belonging to the parent
+ # eg. http://developer.xero.com/documentation/payroll-api/leaveapplications/
+ def mock_child_relationship_api(child, parent)
+ mock_api(child)
+ mock_api(parent)
+ # grab the ID of each parent record
+ # if we call api/child/parent_id, return the appropriate child xml
+ @client.send("#{parent.singularize}".to_sym).all.each do | record |
+ next if record.id.nil?
+ client_for_stubbing.stubs(:http_get).with {|client, url, params|
+ url =~ /#{child}\/#{record.id}$/
+ }.returns(get_record_xml("#{model_name_for_file(child).underscore.singularize}".to_sym, record.id))
end
end
def mock_report_api(report_type)
- @client.stubs(:http_get).with { | client, url, params | url =~ /Reports\/#{report_type}$/ }.returns(get_report_xml(report_type))
+ client_for_stubbing.stubs(:http_get).with { | client, url, params | url =~ /Reports\/#{report_type}$/ }.returns(get_report_xml(report_type))
+ end
+
+ def client_for_stubbing
+ payroll_application? ? @client.application : @client
+ end
+
+ def model_name_for_file(model_name)
+ payroll_application? ? "payroll_#{model_name}" : model_name
+ end
+
+ def payroll_application?
+ @client.is_a? Xeroizer::PayrollApplication
end
end
diff --git a/test/unit/models/payroll/employees_test.rb b/test/unit/models/payroll/employees_test.rb
new file mode 100644
index 00000000..adfc330e
--- /dev/null
+++ b/test/unit/models/payroll/employees_test.rb
@@ -0,0 +1,34 @@
+require 'test_helper'
+
+class EmployeesTest < Test::Unit::TestCase
+ include TestHelper
+
+ def setup
+ @client = Xeroizer::PublicApplication.new(CONSUMER_KEY, CONSUMER_SECRET).payroll
+ mock_api('Employees')
+ end
+
+ test "get all" do
+ employees = @client.Employee.all
+ assert_equal 7, employees.length
+
+ employee = @client.Employee.find(employees.first.id)
+ assert_not_nil employee.home_address
+ assert_equal 2, employee.bank_accounts.length
+ assert_equal 1, employee.super_memberships.length
+
+ assert_not_nil employee.pay_template
+ assert_equal 1, employee.pay_template.earnings_lines.length
+
+ assert_not_nil employee.opening_balances
+ assert_not_nil employee.opening_balances.opening_balance_date
+ assert_equal 1, employee.opening_balances.earnings_lines.length
+
+ assert_not_nil employee.leave_balances
+ assert_equal 2, employee.leave_balances.length
+ assert_not_nil employee.leave_balances.first.leave_name
+ assert_not_nil employee.leave_balances.first.leave_type_id
+ assert_not_nil employee.leave_balances.first.number_of_units
+ assert_not_nil employee.leave_balances.first.type_of_units
+ end
+end
diff --git a/test/unit/models/payroll/leave_application_test.rb b/test/unit/models/payroll/leave_application_test.rb
new file mode 100644
index 00000000..561db9ab
--- /dev/null
+++ b/test/unit/models/payroll/leave_application_test.rb
@@ -0,0 +1,18 @@
+require 'test_helper'
+
+class LeaveApplicationTest < Test::Unit::TestCase
+ include TestHelper
+
+ def setup
+ @client = Xeroizer::PublicApplication.new(CONSUMER_KEY, CONSUMER_SECRET).payroll
+ mock_child_relationship_api('LeaveApplications', 'Employees')
+ end
+
+ test "can get leave applications using employee ID" do
+ application = @client.LeaveApplication.find(@client.Employee.first.id)
+ assert_not_nil application
+ assert_equal 1, application.leave_periods.size
+ application.add_leave_period(number_of_units: 48.00, pay_period_end_date: Date.today)
+ assert_equal 2, application.leave_periods.size
+ end
+end
diff --git a/test/unit/models/payroll/pay_items_test.rb b/test/unit/models/payroll/pay_items_test.rb
new file mode 100644
index 00000000..781dfa3e
--- /dev/null
+++ b/test/unit/models/payroll/pay_items_test.rb
@@ -0,0 +1,24 @@
+require 'test_helper'
+
+class PayItemsTest < Test::Unit::TestCase
+ include TestHelper
+
+ def setup
+ @client = Xeroizer::PublicApplication.new(CONSUMER_KEY, CONSUMER_SECRET).payroll
+ mock_api('PayItems')
+ end
+
+ test "get all" do
+ pay_items = @client.PayItem.first
+ assert_equal 4, pay_items.earnings_rates.size
+ assert_equal 5, pay_items.deduction_types.size
+ assert_equal 2, pay_items.reimbursement_types.size
+ assert_equal 9, pay_items.leave_types.size
+
+ doc = Nokogiri::XML pay_items.to_xml
+ assert_equal 4, doc.xpath("/PayItems/EarningsRates/EarningsRate").size
+ assert_equal 5, doc.xpath("/PayItems/DeductionTypes/DeductionType").size
+ assert_equal 2, doc.xpath("/PayItems/ReimbursementTypes/ReimbursementType").size
+ assert_equal 9, doc.xpath("/PayItems/LeaveTypes/LeaveType").size
+ end
+end
diff --git a/test/unit/models/payroll/pay_runs_test.rb b/test/unit/models/payroll/pay_runs_test.rb
new file mode 100644
index 00000000..e04a614a
--- /dev/null
+++ b/test/unit/models/payroll/pay_runs_test.rb
@@ -0,0 +1,15 @@
+require 'test_helper'
+
+class PayRunsTest < Test::Unit::TestCase
+ include TestHelper
+
+ def setup
+ @client = Xeroizer::PublicApplication.new(CONSUMER_KEY, CONSUMER_SECRET).payroll
+ mock_api('PayRuns')
+ end
+
+ test "get all" do
+ runs = @client.PayRun.all
+ assert_equal 2, runs.length
+ end
+end
diff --git a/test/unit/models/payroll/payroll_calendar_test.rb b/test/unit/models/payroll/payroll_calendar_test.rb
new file mode 100644
index 00000000..6f5df81e
--- /dev/null
+++ b/test/unit/models/payroll/payroll_calendar_test.rb
@@ -0,0 +1,23 @@
+require 'test_helper'
+
+class PayrollCalendarTest < Test::Unit::TestCase
+ include TestHelper
+
+ def setup
+ @client = Xeroizer::PublicApplication.new(CONSUMER_KEY, CONSUMER_SECRET).payroll
+ mock_api('PayrollCalendars')
+ end
+
+ test "get all" do
+ calendars = @client.PayrollCalendar.all
+ assert_equal 3, calendars.size
+ end
+
+ test "get one" do
+ calendar = @client.PayrollCalendar.first
+ assert_not_nil calendar
+
+ doc = Nokogiri::XML calendar.to_xml
+ assert_equal 1, doc.xpath("/PayrollCalendar/PayrollCalendarID").size
+ end
+end
diff --git a/test/unit/models/payroll/timesheet_test.rb b/test/unit/models/payroll/timesheet_test.rb
new file mode 100644
index 00000000..2b42b32f
--- /dev/null
+++ b/test/unit/models/payroll/timesheet_test.rb
@@ -0,0 +1,53 @@
+require 'test_helper'
+
+class TimesheetTest < Test::Unit::TestCase
+ include TestHelper
+
+ def setup
+ @client = Xeroizer::PublicApplication.new(CONSUMER_KEY, CONSUMER_SECRET).payroll
+ mock_api('Employees')
+ mock_api('Timesheets')
+ @employee = @client.Employee.first
+ end
+
+ test "get all" do
+ timesheets = @client.Timesheet.all
+ assert_equal 2, timesheets.count
+ timesheets.each do |timesheet|
+ timesheet.timesheet_lines.each do |line|
+ assert_equal 7, line.number_of_units.count
+ line.number_of_units.each do |unit|
+ assert_not_nil unit.value
+ end
+ end
+ end
+ end
+
+ test "get one" do
+ timesheet = @client.Timesheet.first
+ assert_equal 2, timesheet.timesheet_lines.count
+ assert_equal 7, timesheet.timesheet_lines.sample.number_of_units.count
+ end
+
+ test "create invalid timesheet" do
+ timesheet = @client.Timesheet.build
+ assert_equal(false, timesheet.valid?)
+ end
+
+ test "create valid timesheet" do
+ timesheet = @client.Timesheet.build(employee_id: @employee.id, start_date: Date.today - 1.week, end_date: Date.today)
+ assert_equal(true, timesheet.valid?)
+ end
+
+ test "create timesheet with units" do
+ timesheet = @client.Timesheet.build(status: 'DRAFT', employee_id: @employee.id, start_date: Date.today - 1.week, end_date: Date.today)
+
+ timesheet_line = timesheet.add_timesheet_line(earnings_rate_id: @employee.ordinary_earnings_rate_id)
+ timesheet_line.add_to_number_of_units([8.00, 8.00, 8.00, 8.00, 8.00, 0.00, 0.00])
+
+ doc = Nokogiri::XML timesheet.to_xml
+ assert_equal 1, doc.xpath("/Timesheet/TimesheetLines/TimesheetLine").size
+ assert_equal 7, doc.xpath("/Timesheet/TimesheetLines/TimesheetLine/NumberOfUnits/NumberOfUnit").size
+ end
+
+end