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 = "" + 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