xeroizer 2.15.8 → 2.15.9

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