xeroizer 2.15.8 → 2.15.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -552,7 +552,7 @@ client = Xeroizer::PublicApplication.new(YOUR_OAUTH_CONSUMER_KEY,
552
552
  Logging
553
553
  ---------------
554
554
 
555
- You can add an optional paramater to the Xeroizer Application initialization, to pass a logger object that will need to respond_to :info. For example, in a rails app:
555
+ You can add an optional parameter to the Xeroizer Application initialization, to pass a logger object that will need to respond_to :info. For example, in a rails app:
556
556
 
557
557
  ```ruby
558
558
  XeroLogger = Logger.new('log/xero.log', 'weekly')
@@ -561,6 +561,20 @@ client = Xeroizer::PublicApplication.new(YOUR_OAUTH_CONSUMER_KEY,
561
561
  :logger => XeroLogger)
562
562
  ```
563
563
 
564
+ Unit Price Precision
565
+ --------------------
566
+
567
+ By default, the API accepts unit prices (UnitAmount) to two decimals places. If you require greater precision, you can opt-in to 4 decimal places by setting an optional parameter when initializing an application:
568
+
569
+
570
+ ```ruby
571
+ client = Xeroizer::PublicApplication.new(YOUR_OAUTH_CONSUMER_KEY,
572
+ YOUR_OAUTH_CONSUMER_SECRET,
573
+ :unitdp => 4)
574
+ ```
575
+
576
+ This option adds the unitdp=4 query string parameter to all requests for models with line items - invoices, credit notes, bank transactions and receipts.
577
+
564
578
  ### Contributors
565
579
  Xeroizer was inspired by the https://github.com/tlconnor/xero_gateway gem created by Tim Connor
566
580
  and Nik Wakelin and portions of the networking and authentication code are based completely off
@@ -28,6 +28,16 @@ module Xeroizer
28
28
  def message
29
29
  "#{@type}: #{@message} \n Generated by the following XML: \n #{@xml}"
30
30
  end
31
+
32
+ def validation_errors
33
+ errors = []
34
+ @parsed_xml.xpath("//ValidationError").each do |err|
35
+ errors << err.text.gsub(/^\s+/, '').gsub(/\s+$/, '')
36
+ end
37
+ errors
38
+ rescue
39
+ []
40
+ end
31
41
 
32
42
  end
33
43
 
@@ -6,7 +6,7 @@ module Xeroizer
6
6
  include Http
7
7
  extend Record::ApplicationHelper
8
8
 
9
- attr_reader :client, :xero_url, :logger, :rate_limit_sleep, :rate_limit_max_attempts, :default_headers
9
+ attr_reader :client, :xero_url, :logger, :rate_limit_sleep, :rate_limit_max_attempts, :default_headers, :unitdp
10
10
 
11
11
  extend Forwardable
12
12
  def_delegators :client, :access_token
@@ -15,6 +15,7 @@ module Xeroizer
15
15
  record :Attachment
16
16
  record :BrandingTheme
17
17
  record :Contact
18
+ record :ContactGroup
18
19
  record :CreditNote
19
20
  record :Currency
20
21
  record :Employee
@@ -28,6 +29,7 @@ module Xeroizer
28
29
  record :Receipt
29
30
  record :TaxRate
30
31
  record :TrackingCategory
32
+ record :TrackingCategoryChild
31
33
  record :BankTransaction
32
34
  record :User
33
35
 
@@ -54,6 +56,7 @@ module Xeroizer
54
56
  @default_headers = options[:default_headers] || {}
55
57
  @client = OAuth.new(consumer_key, consumer_secret, options.merge({default_headers: default_headers}))
56
58
  @logger = options[:logger] || false
59
+ @unitdp = options[:unitdp] || 2
57
60
  end
58
61
 
59
62
  end
data/lib/xeroizer/http.rb CHANGED
@@ -57,6 +57,9 @@ module Xeroizer
57
57
 
58
58
  headers = self.default_headers.merge({ 'charset' => 'utf-8' })
59
59
 
60
+ # include the unitdp query string parameter
61
+ params.merge!(unitdp_param(url))
62
+
60
63
  if method != :get
61
64
  headers['Content-Type'] ||= "application/x-www-form-urlencoded"
62
65
  end
@@ -195,5 +198,13 @@ module Xeroizer
195
198
  sleep seconds
196
199
  end
197
200
 
201
+ # unitdp query string parameter to be added to request params
202
+ # when the application option has been set and the model has line items
203
+ # http://developer.xero.com/documentation/advanced-docs/rounding-in-xero/#unitamount
204
+ def unitdp_param(request_url)
205
+ models = [/Invoices/, /CreditNotes/, /BankTransactions/, /Receipts/]
206
+ self.unitdp == 4 && models.any?{ |m| request_url =~ m } ? {:unitdp => 4} : {}
207
+ end
208
+
198
209
  end
199
210
  end
@@ -13,7 +13,7 @@ module Xeroizer
13
13
  class BankTransaction < Base
14
14
 
15
15
  BANK_TRANSACTION_STATUS = {
16
- 'ACTIVE' => 'Active bank transactions',
16
+ 'AUTHORISED' => 'Active bank transactions',
17
17
  'DELETED' => 'Deleted bank transactions',
18
18
  } unless defined?(BANK_TRANSACTION_STATUS)
19
19
  BANK_TRANSACTION_STATUSES = BANK_TRANSACTION_STATUS.keys.sort
@@ -52,7 +52,7 @@ module Xeroizer
52
52
  validates_inclusion_of :type,
53
53
  :in => %w{SPEND RECEIVE RECEIVE-PREPAYMENT RECEIVE-OVERPAYMENT}, :allow_blanks => false,
54
54
  :message => "Invalid type. Expected either SPEND, RECEIVE, RECEIVE-PREPAYMENT or RECEIVE-OVERPAYMENT."
55
- validates_inclusion_of :status, :in => BANK_TRANSACTION_STATUSES, :unless => :new_record?
55
+ validates_inclusion_of :status, :in => BANK_TRANSACTION_STATUSES, :allow_blanks => true
56
56
 
57
57
  validates_presence_of :contact, :bank_account, :allow_blanks => false
58
58
 
@@ -24,6 +24,7 @@ module Xeroizer
24
24
  guid :contact_id
25
25
  string :contact_number
26
26
  string :contact_status
27
+ string :account_number
27
28
  string :name
28
29
  string :tax_number
29
30
  string :bank_account_details
@@ -44,6 +45,9 @@ module Xeroizer
44
45
  has_many :contact_groups
45
46
  has_many :contact_persons, :internal_name => :contact_people
46
47
 
48
+ has_many :sales_tracking_categories, :model_name => 'ContactSalesTrackingCategory'
49
+ has_many :purchases_tracking_categories, :model_name => 'ContactPurchasesTrackingCategory'
50
+
47
51
  validates_presence_of :name
48
52
  validates_inclusion_of :contact_status, :in => CONTACT_STATUS.keys, :allow_blanks => true
49
53
 
@@ -1,16 +1,19 @@
1
1
  module Xeroizer
2
2
  module Record
3
-
4
- class ContactGroupModel < BaseModel
3
+
4
+ class ContactGroupModel < BaseModel
5
+ set_permissions :read
5
6
  end
6
-
7
+
7
8
  class ContactGroup < Base
8
-
9
+
9
10
  guid :contact_group_id
10
11
  string :name
11
12
  string :status
12
-
13
+
14
+ has_many :contacts, :list_complete => true
15
+
13
16
  end
14
-
17
+
15
18
  end
16
- end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Xeroizer
2
+ module Record
3
+
4
+ class ContactPurchasesTrackingCategoryModel < BaseModel
5
+
6
+ set_xml_root_name 'PurchasesTrackingCategories'
7
+ set_xml_node_name 'PurchasesTrackingCategory'
8
+
9
+ end
10
+
11
+ class ContactPurchasesTrackingCategory < Base
12
+
13
+ string :tracking_category_name
14
+ string :tracking_option_name
15
+
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Xeroizer
2
+ module Record
3
+
4
+ class ContactSalesTrackingCategoryModel < BaseModel
5
+
6
+ set_xml_root_name 'SalesTrackingCategories'
7
+ set_xml_node_name 'SalesTrackingCategory'
8
+
9
+ end
10
+
11
+ class ContactSalesTrackingCategory < Base
12
+
13
+ string :tracking_category_name
14
+ string :tracking_option_name
15
+
16
+ end
17
+
18
+ end
19
+ end
@@ -57,6 +57,7 @@ module Xeroizer
57
57
  decimal :total, :calculated => true
58
58
  datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
59
59
  string :currency_code
60
+ decimal :currency_rate
60
61
  datetime :fully_paid_on_date
61
62
  boolean :sent_to_contact
62
63
 
@@ -73,6 +73,7 @@ module Xeroizer
73
73
  string :currency_code
74
74
  decimal :currency_rate
75
75
  datetime :fully_paid_on_date
76
+ datetime :expected_payment_date
76
77
  boolean :sent_to_contact
77
78
  boolean :has_attachments
78
79
 
@@ -20,6 +20,7 @@ module Xeroizer
20
20
  string :status
21
21
  string :reference
22
22
  datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
23
+ boolean :is_reconciled
23
24
 
24
25
  belongs_to :account
25
26
  belongs_to :invoice
@@ -1,46 +1,55 @@
1
1
  module Xeroizer
2
2
  class PartnerApplication < GenericApplication
3
-
3
+
4
4
  extend Forwardable
5
5
  def_delegators :client, :request_token, :authorize_from_request, :renew_access_token, :expires_at, :authorization_expires_at, :session_handle, :authorize_from_access
6
-
6
+
7
7
  public
8
-
8
+
9
9
  # Partner applications allow for public AccessToken's received via the stanard OAuth
10
10
  # authentication process to be renewed up until the session's expiry. The default session
11
11
  # expiry for Xero is 365 days and the default AccessToken expiry is 30 minutes.
12
- #
12
+ #
13
13
  # @param [String] consumer_key consumer key/token from application developer (found at http://api.xero.com for your application).
14
14
  # @param [String] consumer_secret consumer secret from application developer (found at http://api.xero.com for your application).
15
15
  # @param [String] path_to_private_key application's private key for message signing (uploaded to http://api.xero.com)
16
- # @param [String] path_to_ssl_client_cert client-side SSL certificate file to use for requests
17
- # @param [String] path_to_ssl_client_key client-side SSL private key to use for requests
16
+ # @param [String] ssl_client_cert client-side SSL certificate file to use for requests
17
+ # @param [String] ssl_client_key client-side SSL private key to use for requests
18
18
  # @param [Hash] options other options to pass to the GenericApplication constructor
19
19
  # @return [PartnerApplication] instance of PrivateApplication
20
- def initialize(consumer_key, consumer_secret, path_to_private_key, path_to_ssl_client_cert, path_to_ssl_client_key, options = {})
20
+ def initialize(consumer_key, consumer_secret, path_to_private_key, ssl_client_cert, ssl_client_key, options = {})
21
21
  default_options = {
22
22
  :xero_url => 'https://api-partner.network.xero.com/api.xro/2.0',
23
23
  :site => 'https://api-partner.network.xero.com',
24
- :authorize_url => 'https://api.xero.com/oauth/Authorize',
24
+ :authorize_url => 'https://api.xero.com/oauth/Authorize',
25
25
  :signature_method => 'RSA-SHA1'
26
26
  }
27
27
  options = default_options.merge(options).merge(
28
28
  :private_key_file => path_to_private_key,
29
- :ssl_client_cert => OpenSSL::X509::Certificate.new(File.read(path_to_ssl_client_cert)),
30
- :ssl_client_key => OpenSSL::PKey::RSA.new(File.read(path_to_ssl_client_key))
29
+ :ssl_client_cert => OpenSSL::X509::Certificate.new(read_certificate(ssl_client_cert)),
30
+ :ssl_client_key => OpenSSL::PKey::RSA.new(read_certificate(ssl_client_key))
31
31
  )
32
32
  super(consumer_key, consumer_secret, options)
33
-
33
+
34
34
  # Load up an existing token if passed the token/key.
35
35
  if options[:access_token] && options[:access_key]
36
36
  authorize_from_access(options[:access_token], options[:access_key])
37
37
  end
38
-
38
+
39
39
  # Save the session_handle if passed in so we can renew the token.
40
40
  if options[:session_handle]
41
41
  client.session_handle = options[:session_handle]
42
42
  end
43
43
  end
44
-
44
+
45
+ private
46
+
47
+ def read_certificate(cert)
48
+ if File.exists?(cert)
49
+ File.read(cert)
50
+ else
51
+ cert
52
+ end
53
+ end
45
54
  end
46
55
  end
@@ -1,3 +1,3 @@
1
1
  module Xeroizer
2
- VERSION = "2.15.8".freeze
2
+ VERSION = "2.15.9".freeze
3
3
  end
data/lib/xeroizer.rb CHANGED
@@ -57,6 +57,8 @@ require 'xeroizer/models/tracking_category'
57
57
  require 'xeroizer/models/tracking_category_child'
58
58
  require 'xeroizer/models/user'
59
59
  require 'xeroizer/models/journal_line_tracking_category'
60
+ require 'xeroizer/models/contact_sales_tracking_category'
61
+ require 'xeroizer/models/contact_purchases_tracking_category'
60
62
 
61
63
  require 'xeroizer/report/factory'
62
64
 
@@ -5,7 +5,8 @@ class GenericApplicationTest < Test::Unit::TestCase
5
5
 
6
6
  def setup
7
7
  @headers = {"User-Agent" => "Xeroizer/2.15.5"}
8
- @client = Xeroizer::GenericApplication.new(CONSUMER_KEY, CONSUMER_SECRET, :default_headers => @headers)
8
+ @unitdp = 4
9
+ @client = Xeroizer::GenericApplication.new(CONSUMER_KEY, CONSUMER_SECRET, :default_headers => @headers, :unitdp => @unitdp)
9
10
  end
10
11
 
11
12
  context "initialization" do
@@ -14,6 +15,10 @@ class GenericApplicationTest < Test::Unit::TestCase
14
15
  assert_equal(@headers, @client.default_headers)
15
16
  end
16
17
 
18
+ should "pass unitdp value" do
19
+ assert_equal(@unitdp, @client.unitdp)
20
+ end
21
+
17
22
  end
18
23
 
19
24
  end
@@ -5,32 +5,60 @@ class BankTransactionValidationTest < Test::Unit::TestCase
5
5
 
6
6
  must "supply either SPEND or RECEIVE as the type" do
7
7
  instance = BankTransaction.build({:type => "xxx"}, nil)
8
-
8
+
9
9
  assert false == instance.valid?, "Expected invalid because of invalid type"
10
-
10
+
11
11
  expected_error = "Invalid type. Expected either SPEND, RECEIVE, RECEIVE-PREPAYMENT or RECEIVE-OVERPAYMENT."
12
12
 
13
13
  assert_equal expected_error, instance.errors_for(:type).first, "Expected an error about type"
14
14
 
15
15
  instance = BankTransaction.build({:type => "SPEND"}, nil)
16
-
16
+
17
17
  instance.valid?
18
-
18
+
19
19
  assert_empty instance.errors_for(:type), "Expected no error about type"
20
20
 
21
21
  instance = BankTransaction.build({:type => "RECEIVE"}, nil)
22
-
22
+
23
23
  instance.valid?
24
-
24
+
25
25
  assert_empty instance.errors_for(:type), "Expected no error about type"
26
26
  end
27
27
 
28
+ can "omit the status attribute" do
29
+ instance = BankTransaction.build({}, nil)
30
+ instance.valid?
31
+ assert_empty instance.errors_for(:status), "Expected no error about status"
32
+ end
33
+
34
+ must "supply either AUTHORISED or DELETED as the status" do
35
+ instance = BankTransaction.build({:status => "xxx"}, nil)
36
+
37
+ assert false == instance.valid?, "Expected invalid because of invalid status"
38
+
39
+ expected_error = "not one of AUTHORISED, DELETED"
40
+
41
+ assert_equal expected_error, instance.errors_for(:status).first, "Expected an error about status"
42
+
43
+ instance = BankTransaction.build({:status => "AUTHORISED"}, nil)
44
+
45
+ instance.valid?
46
+
47
+ assert_empty instance.errors_for(:status), "Expected no error about status"
48
+
49
+ instance = BankTransaction.build({:status => "DELETED"}, nil)
50
+
51
+ instance.valid?
52
+
53
+ assert_empty instance.errors_for(:status), "Expected no error about status"
54
+ end
55
+
28
56
  must "supply a non-blank contact" do
29
57
  instance = BankTransaction.build({}, nil)
30
-
58
+
31
59
  assert false == instance.valid?, "Expected invalid because of missing contact"
32
60
 
33
- assert_equal "can't be blank", instance.errors_for(:contact).first,
61
+ assert_equal "can't be blank", instance.errors_for(:contact).first,
34
62
  "Expected an error about blank contact"
35
63
  end
36
64
 
@@ -64,18 +92,18 @@ class BankTransactionValidationTest < Test::Unit::TestCase
64
92
 
65
93
  assert false == instance.valid?, "Expected invalid because of missing bank account"
66
94
 
67
- assert_equal "can't be blank", instance.errors_for(:bank_account).first,
95
+ assert_equal "can't be blank", instance.errors_for(:bank_account).first,
68
96
  "Expected an error about blank contact"
69
97
  end
70
98
 
71
- must "supply valid line_amount_types value" do
99
+ must "supply valid line_amount_types value" do
72
100
  instance = BankTransaction.build({
73
101
  :line_amount_types => "XXX_ANYTHING_INVALID_XXX"
74
102
  }, nil)
75
103
 
76
104
  assert false == instance.valid?, "Expected invalid because of missing bank account"
77
105
 
78
- assert_equal "not one of Exclusive, Inclusive, NoTax", instance.errors_for(:line_amount_types).first,
106
+ assert_equal "not one of Exclusive, Inclusive, NoTax", instance.errors_for(:line_amount_types).first,
79
107
  "Expected an error about blank contact"
80
108
  end
81
109
 
@@ -7,32 +7,32 @@ class RecordBaseTest < Test::Unit::TestCase
7
7
  @client = Xeroizer::PublicApplication.new(CONSUMER_KEY, CONSUMER_SECRET)
8
8
  @contact = @client.Contact.build(:name => 'Test Contact Name ABC')
9
9
  end
10
-
10
+
11
11
  context "base record" do
12
-
12
+
13
13
  should "create new model instance from #new_model_class" do
14
14
  model_class = @contact.new_model_class("Invoice")
15
15
  assert_kind_of(Xeroizer::Record::InvoiceModel, model_class)
16
16
  assert_equal(@contact.parent.application, model_class.application)
17
17
  assert_equal('Invoice', model_class.model_name)
18
18
  end
19
-
19
+
20
20
  should "new_record? should be new on create" do
21
21
  assert_equal(true, @contact.new_record?)
22
22
  end
23
-
23
+
24
24
  end
25
-
25
+
26
26
  context "new_record? states" do
27
-
27
+
28
28
  should "new_record? should be false when loading data" do
29
29
  Xeroizer::OAuth.any_instance.stubs(:get).returns(stub(:plain_body => get_record_xml(:contact), :code => '200'))
30
30
  contact = @client.Contact.find('TESTID')
31
31
  assert_kind_of(Xeroizer::Record::Contact, contact)
32
32
  assert_equal(false, contact.new_record?)
33
33
  end
34
-
35
- should "new_record? should be false after successfully creating a record" do
34
+
35
+ should "new_record? should be false after successfully creating a record" do
36
36
  Xeroizer::OAuth.any_instance.stubs(:put).returns(stub(:plain_body => get_record_xml(:contact), :code => '200'))
37
37
  assert_equal(true, @contact.new_record?)
38
38
  assert_nil(@contact.contact_id)
@@ -40,27 +40,29 @@ class RecordBaseTest < Test::Unit::TestCase
40
40
  assert_equal(false, @contact.new_record?)
41
41
  assert(@contact.contact_id =~ GUID_REGEX, "@contact.contact_id is not a GUID, it is '#{@contact.contact_id}'")
42
42
  end
43
-
43
+
44
44
  should "new_record? should be false if we have specified a primary key" do
45
45
  contact = @client.Contact.build(:contact_id => 'ABC')
46
46
  assert_equal(false, contact.new_record?)
47
-
47
+
48
48
  contact = @client.Contact.build(:contact_number => 'CDE')
49
49
  assert_equal(true, contact.new_record?)
50
-
50
+
51
51
  contact = @client.Contact.build(:name => 'TEST NAME')
52
52
  assert_equal(true, contact.new_record?)
53
53
  end
54
-
54
+
55
55
  end
56
-
56
+
57
57
  context "about logging" do
58
58
  setup do
59
- @example_class = Class.new(Xeroizer::Record::Base) do
59
+ class ExampleRecordClass < Xeroizer::Record::Base
60
60
  def valid?; true; end
61
61
  def to_xml(b = nil); "<FakeRequest />" end
62
62
  string :id
63
63
  end
64
+ class Xeroizer::Record::ExampleRecordClassModel < Xeroizer::Record::BaseModel ; end
65
+ @example_class = ExampleRecordClass
64
66
  end
65
67
 
66
68
  must "log the request and response xml when saving a new record" do
@@ -69,7 +71,10 @@ class RecordBaseTest < Test::Unit::TestCase
69
71
 
70
72
  a_fake_parent = mock "Mock parent",
71
73
  :http_put => "<FakeResponse />",
72
- :parse_response => stub("Stub response", :response_items => [])
74
+ :parse_response => stub("Stub response", :response_items => []),
75
+ :mark_dirty => nil,
76
+ :create_method => :http_put,
77
+ :mark_clean => nil
73
78
 
74
79
  an_example_instance = @example_class.new(a_fake_parent)
75
80
 
@@ -83,7 +88,9 @@ class RecordBaseTest < Test::Unit::TestCase
83
88
 
84
89
  a_fake_parent = mock "Mock parent",
85
90
  :http_post => "<FakeResponse />",
86
- :parse_response => stub("Stub response", :response_items => [])
91
+ :parse_response => stub("Stub response", :response_items => []),
92
+ :mark_dirty => nil,
93
+ :mark_clean => nil
87
94
 
88
95
  an_example_instance = @example_class.new(a_fake_parent)
89
96
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xeroizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.15.8
4
+ version: 2.15.9
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -295,6 +295,8 @@ files:
295
295
  - lib/xeroizer/models/contact.rb
296
296
  - lib/xeroizer/models/contact_group.rb
297
297
  - lib/xeroizer/models/contact_person.rb
298
+ - lib/xeroizer/models/contact_purchases_tracking_category.rb
299
+ - lib/xeroizer/models/contact_sales_tracking_category.rb
298
300
  - lib/xeroizer/models/credit_note.rb
299
301
  - lib/xeroizer/models/currency.rb
300
302
  - lib/xeroizer/models/employee.rb
@@ -628,18 +630,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
628
630
  - - ! '>='
629
631
  - !ruby/object:Gem::Version
630
632
  version: '0'
631
- segments:
632
- - 0
633
- hash: -1071589211669174391
634
633
  required_rubygems_version: !ruby/object:Gem::Requirement
635
634
  none: false
636
635
  requirements:
637
636
  - - ! '>='
638
637
  - !ruby/object:Gem::Version
639
638
  version: '0'
640
- segments:
641
- - 0
642
- hash: -1071589211669174391
643
639
  requirements: []
644
640
  rubyforge_project:
645
641
  rubygems_version: 1.8.29