xero_gateway 2.5.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9c288d4d30be0c8ce139ffd5b3c714f077835e47
4
- data.tar.gz: b09d959c33ebacbf10a98509e59cf8e19d53b1f1
3
+ metadata.gz: 15bce269e603d39203dbafb5858843c8e8fe8b3e
4
+ data.tar.gz: a996fa84e4ca2471b3efc06eb68c080fbe4ed8cb
5
5
  SHA512:
6
- metadata.gz: 56dc6a1fc73e3ad5118caa9c10662a64dafeef89e902dc9080053953042cd571895379622be61126c4ecfaeb79c99b5712e14230948aac614ae04dfc0dcc3800
7
- data.tar.gz: d3906629a8c6acc8856ac23f3fb1e7f91fca06b9ad22d51d3feb6d455b69ef61cec9381deb8861530c9d860d65b8c5054c30638432e3efa895d3871a7aa20986
6
+ metadata.gz: 57f8ec61dbe74dcd7fb6fbdb4a2f65087079e2f4808bc8a08696e445d6f91b2b9ec4661ca557029041b1ffe35a11e3b2f49f42faf458c9b42c4bacad77b02cca
7
+ data.tar.gz: 4a203e5a9a76a0146c758249126bc595946422960d4cb37a056fb9607e0e5ad1493d336f5b24ad02a87076875a41b5dfd2ed09c3cc51f5d14b27caccb9158045
data/README.md CHANGED
@@ -26,19 +26,19 @@ The Xero Gateway uses [OAuth 1.0a](https://oauth.net/core/1.0a/) for authenticat
26
26
  implements OAuth in a very similar manner to the [Twitter gem by John Nunemaker](http://github.com/jnunemaker/twitter)
27
27
  , so if you've used that before this will all seem familiar.
28
28
 
29
- ### Authenticating: Public/Partner Applications
29
+ ### Authenticating: Public Applications
30
30
 
31
- Public (or Partner, if you've been through the process to be approved) are traditional three-legged OAuth apps that can be used to access many different Xero accounts.
31
+ Public are traditional three-legged OAuth apps that can be used to access many different Xero accounts.
32
32
 
33
33
  1. **Get a Consumer Key & Secret**
34
34
 
35
35
  First off, you'll need to get a Consumer Key/Secret pair for your application from Xero.
36
36
 
37
- Head to <https://api.xero.com>, log in and then click My Applications &gt; Add Application.
37
+ Head to <https://developer.xero.com/myapps>, log in and then click New Application.
38
38
 
39
- Part of the process for this will ask you for an "OAuth Redirect URL". This is where customers will be redirected once they complete logging in with Xero.
39
+ Part of the process for this will ask you for an "OAuth Callback Domain". This is the domain where customers will be redirected once they complete logging in with Xero.
40
40
 
41
- On the right-hand-side of your application's page there's a box titled "OAuth Credentials". Use the Key and Secret from this box in order to set up a new Gateway instance.
41
+ Further down in your application's page there's a box titled "App Credentials". Use the Key and Secret from this box in order to set up a new Gateway instance.
42
42
 
43
43
  2. **Create a Xero Gateway in your App**
44
44
 
@@ -106,7 +106,7 @@ Public (or Partner, if you've been through the process to be approved) are tradi
106
106
  gateway.authorize_from_access(your_stored_token.access_token, your_stored_token.access_secret)
107
107
  ```
108
108
 
109
- ### Authenticating: Private Applications
109
+ ### Authenticating: Private Applications
110
110
 
111
111
  Private applications are used to access a single Xero account.
112
112
 
@@ -117,8 +117,8 @@ Private applications are used to access a single Xero account.
117
117
  You'll need to generate an RSA keypair and an X509 certificate. This can be done with OpenSSL as below:
118
118
 
119
119
  ```bash
120
- openssl genrsa -out privatekey.pem
121
- openssl req -newkey rsa:1024 -x509 -days 365 -in privatekey.pem -out publickey.cer
120
+ openssl genrsa -out privatekey.pem 1024
121
+ openssl req -new -x509 -key privatekey.pem -out publickey.cer -days 1825
122
122
  ```
123
123
 
124
124
  You can then copy `publickey.cer` and paste it into the certificate box (`cat publickey.cer | pbcopy` on a Mac :apple:)
@@ -138,13 +138,28 @@ Private applications are used to access a single Xero account.
138
138
 
139
139
  Note that for private apps, your consumer key and secret do double duty as your access token and secret pair :)
140
140
 
141
+ ### Authenticating: Partner Applications
142
+
143
+ Partner applications are public applications that have been upgraded to support long-term access tokens.
144
+
145
+ Use the same three-legged authentication process as for public applications, but with an RSA keypair and an X509 certificate as for private applications:
146
+
147
+ ```ruby
148
+ require 'xero_gateway'
149
+ gateway = XeroGateway::PartnerApp.new(YOUR_OAUTH_CONSUMER_KEY, YOUR_OAUTH_CONSUMER_SECRET, PATH_TO_YOUR_PRIVATE_KEY)
150
+
151
+ pp gateway.get_contacts
152
+ ```
153
+
154
+ For more information on partner applications see the Xero documentation: <https://developer.xero.com/documentation/auth-and-limits/partner-applications>
155
+
141
156
  ## Examples
142
157
 
143
158
  Open `examples/oauth.rb` and change `CONSUMER_KEY` and `CONSUMER_SECRET` to
144
159
  the values for a Test OAuth Public Application in order to see an example of
145
160
  OAuth at work.
146
161
 
147
- There's also `examples/private_app.rb` if you'd like to see an example private app.
162
+ See also `examples/private_app.rb` for an example private app or `examples/partner_app.rb` for an example partner app.
148
163
 
149
164
  If you're working with Rails, a controller similar to this might come in
150
165
  handy:
data/lib/xero_gateway.rb CHANGED
@@ -24,8 +24,11 @@ require File.join(File.dirname(__FILE__), 'xero_gateway', 'accounts_list')
24
24
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'tracking_category')
25
25
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'contact')
26
26
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'contact_group')
27
+ require File.join(File.dirname(__FILE__), 'xero_gateway', 'contact_person')
27
28
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'line_item')
28
29
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'payment')
30
+ require File.join(File.dirname(__FILE__), 'xero_gateway', 'payroll_calendar')
31
+ require File.join(File.dirname(__FILE__), 'xero_gateway', 'pay_run')
29
32
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'invoice')
30
33
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'bank_transaction')
31
34
  require File.join(File.dirname(__FILE__), 'xero_gateway', 'credit_note')
@@ -76,9 +76,13 @@ module XeroGateway
76
76
  end
77
77
 
78
78
  value = case attr_definition
79
- when :boolean then element.text == "true"
80
- when :float then element.text.to_f
81
- when :integer then element.text.to_i
79
+ when :boolean then element.text == "true"
80
+ when :float then element.text.to_f
81
+ when :integer then element.text.to_i
82
+ when :currency then BigDecimal.new(element.text)
83
+ when :date then Date.strptime(element.text, "%Y-%m-%d")
84
+ when :datetime then Date.strptime(element.text, "%Y-%m-%dT%H:%M:%S")
85
+ when :datetime_utc then Date.strptime(element.text + "Z", "%Y-%m-%dT%H:%M:%S%Z")
82
86
  when Array then array_from_xml(element, attr_definition)
83
87
  else element.text
84
88
  end if element.text.present? || element.children.present?
@@ -17,7 +17,7 @@ module XeroGateway
17
17
 
18
18
  attr_accessor :contact_id, :contact_number, :account_number, :status, :name, :first_name, :last_name, :email, :addresses, :phones, :updated_at,
19
19
  :bank_account_details, :tax_number, :accounts_receivable_tax_type, :accounts_payable_tax_type, :is_customer, :is_supplier,
20
- :default_currency, :contact_groups
20
+ :default_currency, :contact_groups, :contact_persons
21
21
 
22
22
 
23
23
  def initialize(params = {})
@@ -79,6 +79,11 @@ module XeroGateway
79
79
  self.phones << Phone.new(phone_params)
80
80
  end
81
81
 
82
+ def add_contact_person(contact_person_params = {})
83
+ self.contact_persons ||= []
84
+ self.contact_persons << ContactPerson.new(contact_person_params)
85
+ end
86
+
82
87
  # Validate the Contact record according to what will be valid by the gateway.
83
88
  #
84
89
  # Usage:
@@ -160,6 +165,9 @@ module XeroGateway
160
165
  b.Phones {
161
166
  phones.each { |phone| phone.to_xml(b) }
162
167
  } if self.phones.any?
168
+ b.ContactPersons {
169
+ contact_persons.each { |contact_person| contact_person.to_xml(b) }
170
+ } unless contact_persons.nil?
163
171
  }
164
172
  end
165
173
 
@@ -187,6 +195,7 @@ module XeroGateway
187
195
  when "IsSupplier" then contact.is_supplier = (element.text == "true")
188
196
  when "DefaultCurrency" then contact.default_currency = element.text
189
197
  when "UpdatedDateUTC" then contact.updated_at = parse_date_time(element.text)
198
+ when "ContactPersons" then element.children.each { |contact_person_element| contact.contact_persons ||= []; contact.contact_persons << ContactPerson.from_xml(contact_person_element) }
190
199
  end
191
200
  end
192
201
  contact
@@ -0,0 +1,33 @@
1
+ module XeroGateway
2
+ class ContactPerson
3
+ attr_accessor :first_name, :last_name, :email_address, :include_in_emails
4
+
5
+ def initialize(params = {})
6
+ params.each do |k,v|
7
+ self.send("#{k}=", v)
8
+ end
9
+ end
10
+
11
+ def to_xml(b = Builder::XmlMarkup.new)
12
+ b.ContactPerson {
13
+ b.FirstName first_name if first_name
14
+ b.LastName last_name if last_name
15
+ b.EmailAddress email_address if email_address
16
+ b.IncludeInEmails include_in_emails if include_in_emails
17
+ }
18
+ end
19
+
20
+ def self.from_xml(contact_person_element)
21
+ contact_person = ContactPerson.new
22
+ contact_person_element.children.each do |element|
23
+ case(element.name)
24
+ when "FirstName" then contact_person.first_name = element.text
25
+ when "LastName" then contact_person.last_name = element.text
26
+ when "EmailAddress" then contact_person.email_address = element.text
27
+ when "IncludeInEmails" then contact_person.include_in_emails = (element.text == "true")
28
+ end
29
+ end
30
+ contact_person
31
+ end
32
+ end
33
+ end
@@ -4,7 +4,7 @@ module XeroGateway
4
4
  include Http
5
5
  include Dates
6
6
 
7
- attr_accessor :client, :xero_url, :logger
7
+ attr_accessor :client, :xero_url, :payroll_url, :logger
8
8
 
9
9
  extend Forwardable
10
10
  def_delegators :client, :request_token, :access_token, :authorize_from_request, :authorize_from_access, :expires_at, :authorization_expires_at
@@ -14,6 +14,7 @@ module XeroGateway
14
14
  # to you by Xero inside the API Previewer.
15
15
  def initialize(consumer_key, consumer_secret, options = {})
16
16
  @xero_url = options[:xero_url] || "https://api.xero.com/api.xro/2.0"
17
+ @payroll_url = options[:payroll_url] || "https://api.xero.com/payroll.xro/1.0"
17
18
  @client = OAuth.new(consumer_key, consumer_secret, options)
18
19
  end
19
20
 
@@ -445,6 +446,7 @@ module XeroGateway
445
446
  request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
446
447
  request_params[:order] = options[:order] if options[:order]
447
448
  request_params[:where] = options[:where] if options[:where]
449
+ request_params[:page] = options[:page] if options[:page]
448
450
 
449
451
  response_xml = http_get(@client, "#{@xero_url}/BankTransactions", request_params)
450
452
 
@@ -603,10 +605,28 @@ module XeroGateway
603
605
  #
604
606
  def get_payment(payment_id, options = {})
605
607
  request_params = {}
606
- response_xml = http_get(client, "#{xero_url}/Payments/#{payment_id}", request_params)
608
+ response_xml = http_get(client, "#{@xero_url}/Payments/#{payment_id}", request_params)
607
609
  parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/payments'})
608
610
  end
609
611
 
612
+ #
613
+ # Get the Payroll calendars for a specific organization in Xero
614
+ #
615
+ def get_payroll_calendars(options = {})
616
+ request_params = {}
617
+ response_xml = http_get(client, "#{@payroll_url}/PayrollCalendars", request_params)
618
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/payroll_calendars'})
619
+ end
620
+
621
+ #
622
+ # Get the Pay Runs for a specific organization in Xero
623
+ #
624
+ def get_pay_runs(options = {})
625
+ request_params = {}
626
+ response_xml = http_get(client, "#{@payroll_url}/PayRuns", request_params)
627
+ parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/pay_runs'})
628
+ end
629
+
610
630
  # Retrieves reports from Xero
611
631
  #
612
632
  # Usage : get_report("BankStatement", bank_account_id: "AC993F75-035B-433C-82E0-7B7A2D40802C")
@@ -782,6 +802,8 @@ module XeroGateway
782
802
  element.children.each do |child|
783
803
  response.response_item << Payment.from_xml(child)
784
804
  end
805
+ when "PayrollCalendars" then element.children.each {|child| response.response_item << PayrollCalendar.from_xml(child) }
806
+ when "PayRuns" then element.children.each {|child| response.response_item << PayRun.from_xml(child) }
785
807
  when "Reports"
786
808
  element.children.each do |child|
787
809
  response.response_item << Report.from_xml(child)
@@ -185,6 +185,7 @@ module XeroGateway
185
185
  contact.to_xml(b)
186
186
  b.Date Invoice.format_date(self.date || Date.today)
187
187
  b.DueDate Invoice.format_date(self.due_date) if self.due_date
188
+ b.FullyPaidOnDate Invoice.format_date(self.fully_paid_on) if self.fully_paid_on
188
189
  b.Status self.invoice_status if self.invoice_status
189
190
  b.Reference self.reference if self.reference
190
191
  b.BrandingThemeID self.branding_theme_id if self.branding_theme_id
@@ -212,6 +213,7 @@ module XeroGateway
212
213
  when "Contact" then invoice.contact = Contact.from_xml(element)
213
214
  when "Date" then invoice.date = parse_date(element.text)
214
215
  when "DueDate" then invoice.due_date = parse_date(element.text)
216
+ when "FullyPaidOnDate" then invoice.fully_paid_on = parse_date(element.text)
215
217
  when "UpdatedDateUTC" then invoice.updated_date_utc = parse_date(element.text)
216
218
  when "Status" then invoice.invoice_status = element.text
217
219
  when "Reference" then invoice.reference = element.text
@@ -3,73 +3,75 @@ require File.join(File.dirname(__FILE__), 'account')
3
3
  module XeroGateway
4
4
  class LineItem
5
5
  include Money
6
-
6
+
7
7
  TAX_TYPE = Account::TAX_TYPE unless defined?(TAX_TYPE)
8
8
 
9
9
  # Any errors that occurred when the #valid? method called.
10
10
  attr_reader :errors
11
11
 
12
12
  # All accessible fields
13
- attr_accessor :line_item_id, :description, :quantity, :unit_amount, :item_code, :tax_type, :tax_amount, :account_code, :tracking
14
-
13
+ attr_accessor :line_item_id, :description, :quantity, :unit_amount, :discount_rate, :item_code, :tax_type, :tax_amount, :account_code, :tracking
14
+
15
15
  def initialize(params = {})
16
16
  @errors ||= []
17
17
  @tracking ||= []
18
18
  @quantity = 1
19
19
  @unit_amount = BigDecimal.new('0')
20
-
20
+
21
21
  params.each do |k,v|
22
22
  self.send("#{k}=", v)
23
23
  end
24
24
  end
25
-
25
+
26
26
  # Validate the LineItem record according to what will be valid by the gateway.
27
27
  #
28
- # Usage:
28
+ # Usage:
29
29
  # line_item.valid? # Returns true/false
30
- #
30
+ #
31
31
  # Additionally sets line_item.errors array to an array of field/error.
32
32
  def valid?
33
33
  @errors = []
34
-
34
+
35
35
  if !line_item_id.nil? && line_item_id !~ GUID_REGEX
36
36
  @errors << ['line_item_id', 'must be blank or a valid Xero GUID']
37
37
  end
38
-
38
+
39
39
  unless description
40
40
  @errors << ['description', "can't be blank"]
41
41
  end
42
-
42
+
43
43
  if tax_type && !TAX_TYPE[tax_type]
44
44
  @errors << ['tax_type', "must be one of #{TAX_TYPE.keys.join('/')}"]
45
45
  end
46
-
46
+
47
47
  @errors.size == 0
48
48
  end
49
-
49
+
50
50
  def has_tracking?
51
51
  return false if tracking.nil?
52
-
52
+
53
53
  if tracking.is_a?(Array)
54
54
  return tracking.any?
55
55
  else
56
56
  return tracking.is_a?(TrackingCategory)
57
57
  end
58
58
  end
59
-
59
+
60
60
  # Deprecated (but API for setter remains).
61
61
  #
62
62
  # As line_amount must equal quantity * unit_amount for the API call to pass, this is now
63
63
  # automatically calculated in the line_amount method.
64
64
  def line_amount=(value)
65
65
  end
66
-
66
+
67
67
  # Calculate the line_amount as quantity * unit_amount as this value must be correct
68
68
  # for the API call to succeed.
69
69
  def line_amount
70
- quantity * unit_amount
70
+ total = quantity * unit_amount
71
+ total = total * (1 - (discount_rate / BigDecimal.new(100))) if discount_rate
72
+ total
71
73
  end
72
-
74
+
73
75
  def to_xml(b = Builder::XmlMarkup.new)
74
76
  b.LineItem {
75
77
  b.Description description
@@ -79,6 +81,7 @@ module XeroGateway
79
81
  b.TaxType tax_type if tax_type
80
82
  b.TaxAmount tax_amount if tax_amount
81
83
  b.LineAmount line_amount if line_amount
84
+ b.DiscountRate discount_rate if discount_rate
82
85
  b.AccountCode account_code if account_code
83
86
  if has_tracking?
84
87
  b.Tracking {
@@ -92,7 +95,7 @@ module XeroGateway
92
95
  end
93
96
  }
94
97
  end
95
-
98
+
96
99
  def self.from_xml(line_item_element)
97
100
  line_item = LineItem.new
98
101
  line_item_element.children.each do |element|
@@ -105,6 +108,7 @@ module XeroGateway
105
108
  when "TaxType" then line_item.tax_type = element.text
106
109
  when "TaxAmount" then line_item.tax_amount = BigDecimal.new(element.text)
107
110
  when "LineAmount" then line_item.line_amount = BigDecimal.new(element.text)
111
+ when "DiscountRate" then line_item.discount_rate = BigDecimal.new(element.text)
108
112
  when "AccountCode" then line_item.account_code = element.text
109
113
  when "Tracking" then
110
114
  element.children.each do | tracking_element |
@@ -113,13 +117,13 @@ module XeroGateway
113
117
  end
114
118
  end
115
119
  line_item
116
- end
120
+ end
117
121
 
118
122
  def ==(other)
119
- [:description, :quantity, :unit_amount, :tax_type, :tax_amount, :line_amount, :account_code, :item_code].each do |field|
123
+ [:description, :quantity, :unit_amount, :tax_type, :tax_amount, :line_amount, :discount_rate, :account_code, :item_code].each do |field|
120
124
  return false if send(field) != other.send(field)
121
125
  end
122
126
  return true
123
127
  end
124
- end
128
+ end
125
129
  end
@@ -0,0 +1,19 @@
1
+ module XeroGateway
2
+ class PayRun < BaseRecord
3
+ attributes({
4
+ "PayRunID" => :string,
5
+ "PayrollCalendarID" => :string,
6
+ "PayRunPeriodStartDate" => :date,
7
+ "PayRunPeriodEndDate" => :date,
8
+ "PaymentDate" => :date,
9
+ "Wages" => :currency,
10
+ "Deductions" => :currency,
11
+ "Tax" => :currency,
12
+ "Super" => :currency,
13
+ "Reimbursement" => :currency,
14
+ "NetPay" => :currency,
15
+ "PayRunStatus" => :string,
16
+ "UpdatedDateUTC" => :datetime_utc
17
+ })
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module XeroGateway
2
+ class PayrollCalendar < BaseRecord
3
+ attributes({
4
+ "PayrollCalendarID" => :string,
5
+ "Name" => :string,
6
+ "CalendarType" => :string,
7
+ "StartDate" => :date,
8
+ "PaymentDate" => :date,
9
+ "UpdatedDateUTC" => :datetime_utc
10
+ })
11
+ end
12
+ end
@@ -2,14 +2,15 @@ module XeroGateway
2
2
  class PrivateApp < Gateway
3
3
  #
4
4
  # The consumer key and secret here correspond to those provided
5
- # to you by Xero inside the API Previewer.
5
+ # to you by Xero inside the API Previewer.
6
6
  def initialize(consumer_key, consumer_secret, path_to_private_key, options = {})
7
7
  options.merge!(
8
8
  :signature_method => 'RSA-SHA1',
9
9
  :private_key_file => path_to_private_key
10
10
  )
11
-
11
+
12
12
  @xero_url = options[:xero_url] || "https://api.xero.com/api.xro/2.0"
13
+ @payroll_url = options[:payroll_url] || "https://api.xero.com/payroll.xro/1.0"
13
14
  @client = OAuth.new(consumer_key, consumer_secret, options)
14
15
  @client.authorize_from_access(consumer_key, consumer_secret)
15
16
  end
@@ -1,5 +1,9 @@
1
+ require_relative './report/cell'
2
+ require_relative './report/row'
3
+
1
4
  module XeroGateway
2
5
  class Report
6
+
3
7
  include Money
4
8
  include Dates
5
9
 
@@ -7,6 +11,8 @@ module XeroGateway
7
11
  attr_accessor :report_id, :report_name, :report_type, :report_titles, :report_date, :updated_at,
8
12
  :body, :column_names
9
13
 
14
+ alias :rows :body
15
+
10
16
  def initialize(params={})
11
17
  @errors ||= []
12
18
  @report_titles ||= []
@@ -17,78 +23,93 @@ module XeroGateway
17
23
  end
18
24
  end
19
25
 
20
- def self.from_xml(report_element)
21
- report = Report.new
22
- report_element.children.each do | element |
23
- case element.name
24
- when 'ReportID' then report.report_id = element.text
25
- when 'ReportName' then report.report_name = element.text
26
- when 'ReportType' then report.report_type = element.text
27
- when 'ReportTitles'
28
- each_title(element) do |title|
29
- report.report_titles << title
30
- end
31
- when 'ReportDate' then report.report_date = Date.parse(element.text)
32
- when 'UpdatedDateUTC' then report.updated_at = parse_date_time_utc(element.text)
33
- when 'Rows'
34
- report.column_names ||= find_body_column_names(element)
35
- each_row_content(element) do |content_hash|
36
- report.body << OpenStruct.new(content_hash)
37
- end
26
+ class << self
27
+
28
+ def from_xml(report_element)
29
+ report = Report.new
30
+ report_element.children.each do | element |
31
+ case element.name
32
+ when 'ReportID' then report.report_id = element.text
33
+ when 'ReportName' then report.report_name = element.text
34
+ when 'ReportType' then report.report_type = element.text
35
+ when 'ReportTitles'
36
+ each_title(element) do |title|
37
+ report.report_titles << title
38
+ end
39
+ when 'ReportDate' then report.report_date = Date.parse(element.text)
40
+ when 'UpdatedDateUTC' then report.updated_at = parse_date_time_utc(element.text)
41
+ when 'Rows'
42
+ report.column_names ||= find_body_column_names(element)
43
+ each_row_content(element) do |row|
44
+ report.body << row
45
+ end
46
+ end
38
47
  end
48
+ report
39
49
  end
40
- report
41
- end
42
50
 
43
- private
44
-
45
- def self.each_row_content(xml_element, &block)
46
- column_names = find_body_column_names(xml_element).keys
47
- xpath_body = REXML::XPath.first(xml_element, "//RowType[text()='Section']").parent
48
- rows_contents = []
49
- xpath_body.elements.each("Rows/Row") do |xpath_cells|
50
- values = find_body_cell_values(xpath_cells)
51
- content_hash = Hash[column_names.zip values]
52
- rows_contents << content_hash
53
- yield content_hash if block_given?
54
- end
55
- rows_contents
56
- end
51
+ private
57
52
 
58
- def self.each_title(xml_element, &block)
59
- xpath_titles = REXML::XPath.first(xml_element, "//ReportTitles")
60
- xpath_titles.elements.each("//ReportTitle") do |xpath_title|
61
- title = xpath_title.text.strip
62
- yield title if block_given?
63
- end
64
- end
53
+ def each_row_content(xml_element, &block)
54
+ column_names = find_body_column_names(xml_element).values
55
+ report_sections = REXML::XPath.each(xml_element, "//RowType[text()='Section']/parent::Row")
65
56
 
66
- def self.find_body_cell_values(xml_cells)
67
- values = []
68
- xml_cells.elements.each("Cells/Cell") do |xml_cell|
69
- if value = xml_cell.children.first # finds <Value>...</Value>
70
- values << value.text.try(:strip)
71
- next
57
+ report_sections.each do |section_row|
58
+ section_name = section_row.get_elements("Title").first.try(:text)
59
+ section_row.elements.each("Rows/Row") do |xpath_cells|
60
+ values = find_body_cell_values(xpath_cells)
61
+ yield Row.new(column_names, values, section_name)
62
+ end
63
+ end
72
64
  end
73
- values << nil
74
- end
75
- values
76
- end
77
65
 
78
- # returns something like { column_1: "Amount", column_2: "Description", ... }
79
- def self.find_body_column_names(body)
80
- header = REXML::XPath.first(body, "//RowType[text()='Header']")
81
- names_map = {}
82
- column_count = 0
83
- header.parent.elements.each("Cells/Cell") do |header_cell|
84
- column_count += 1
85
- column_key = "column_#{column_count}".to_sym
86
- column_name = nil
87
- name_value = header_cell.children.first
88
- column_name = name_value.text.strip unless name_value.blank? # finds <Value>...</Value>
89
- names_map[column_key] = column_name
90
- end
91
- names_map
66
+ def each_title(xml_element, &block)
67
+ xpath_titles = REXML::XPath.first(xml_element, "//ReportTitles")
68
+ xpath_titles.elements.each("//ReportTitle") do |xpath_title|
69
+ title = xpath_title.text.strip
70
+ yield title if block_given?
71
+ end
72
+ end
73
+
74
+ def find_body_cell_values(xml_cells)
75
+ values = []
76
+ xml_cells.elements.each("Cells/Cell") do |xml_cell|
77
+ if value = xml_cell.children.first # finds <Value>...</Value>
78
+ values << Cell.new(value.text.try(:strip), collect_attributes(xml_cell))
79
+ next
80
+ end
81
+ values << nil
82
+ end
83
+ values
84
+ end
85
+
86
+ # Collects "<Attribute>" elements into a hash
87
+ def collect_attributes(xml_cell)
88
+ Array.wrap(xml_cell.elements["Attributes/Attribute"]).inject({}) do |hash, xml_attribute|
89
+ if (key = xml_attribute.elements["Id"].try(:text)) &&
90
+ (value = xml_attribute.elements["Value"].try(:text))
91
+
92
+ hash[key] = value
93
+ end
94
+ hash
95
+ end.symbolize_keys
96
+ end
97
+
98
+ # returns something like { column_1: "Amount", column_2: "Description", ... }
99
+ def find_body_column_names(body)
100
+ header = REXML::XPath.first(body, "//RowType[text()='Header']")
101
+ names_map = {}
102
+ column_count = 0
103
+ header.parent.elements.each("Cells/Cell") do |header_cell|
104
+ column_count += 1
105
+ column_key = "column_#{column_count}".to_sym
106
+ column_name = nil
107
+ name_value = header_cell.children.first
108
+ column_name = name_value.text.strip unless name_value.blank? # finds <Value>...</Value>
109
+ names_map[column_key] = column_name
110
+ end
111
+ names_map
112
+ end
92
113
  end
93
114
 
94
115
  end
@@ -0,0 +1,28 @@
1
+ module XeroGateway
2
+ class Report
3
+
4
+ # Adds #attributes to the cells we're grabbing, since Xero Report Cells use XML like:
5
+ # <Cell>
6
+ # <Value>Interest Income (270)</Value>
7
+ # <Attributes>
8
+ # <Attribute>
9
+ # <Value>e9482110-7245-4a76-bfe2-14500495a076</Value>
10
+ # <Id>account</Id>
11
+ # </Attribute>
12
+ # </Attributes>
13
+ # </Cell>
14
+ #
15
+ # We delegate to the topmost "<Value>" class and decorate with an "attributes" hash
16
+ # for the "attribute: value" pairs
17
+ class Cell < SimpleDelegator
18
+ attr_reader :attributes, :value
19
+
20
+ def initialize(value, new_attributes = {})
21
+ @value = value
22
+ @attributes = new_attributes
23
+ super(value)
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ module XeroGateway
2
+ class Report
3
+ class Row
4
+ COLUMN_METHOD_NAME_RE = /^column\_([0-9])+$/
5
+
6
+ attr_accessor :section_name
7
+
8
+ def initialize(column_titles, columns, section_name = nil)
9
+ @columns = columns
10
+ @column_titles = column_titles
11
+ @column_titles_underscored = column_titles.map(&:to_s).map(&:underscore)
12
+ @section_name = section_name
13
+ end
14
+
15
+ def [](key)
16
+ return @columns[key] if key.is_a?(Integer)
17
+
18
+ [ @column_titles, @column_titles_underscored ].each do |names|
19
+ if index = names.index(key.to_s)
20
+ return @columns[index]
21
+ end
22
+ end
23
+
24
+ nil
25
+ end
26
+
27
+ def method_missing(method_name, *args, &block)
28
+ if method_name =~ COLUMN_METHOD_NAME_RE
29
+ # support column_#{n} style deprecated API
30
+ ActiveSupport::Deprecation.warn("XeroGateway: The #column_n API for accessing report cells will be deprecated in a future version. Please use the underscored column title, a hash or array index accessor", caller_locations)
31
+ @columns[$1.to_i - 1]
32
+ elsif (column_index = @column_titles_underscored.index(method_name.to_s))
33
+ @columns[column_index]
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def respond_to_missing?(method_name, *args)
40
+ (method_name =~ COLUMN_METHOD_NAME_RE) || @column_titles_underscored.include?(method_name.to_s) || super
41
+ end
42
+
43
+ def inspect
44
+ "<XeroGateway::Report::Row:#{object_id} #{pairs}>"
45
+ end
46
+
47
+ private
48
+
49
+ def pairs
50
+ @column_titles.zip(@columns).map do |title, value|
51
+ "#{title.to_s.underscore}: #{value.inspect}"
52
+ end.join(" ")
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -1,3 +1,3 @@
1
1
  module XeroGateway
2
- VERSION = "2.5.0"
2
+ VERSION = "2.6.0"
3
3
  end
@@ -80,6 +80,14 @@ class ContactTest < Test::Unit::TestCase
80
80
  <PhoneType>MOBILE</PhoneType>
81
81
  </Phone>
82
82
  </Phones>
83
+ <ContactPersons>
84
+ <ContactPerson>
85
+ <FirstName>John</FirstName>
86
+ <LastName>Smith</LastName>
87
+ <EmailAddress>john@acme.com</EmailAddress>
88
+ <IncludeInEmails>true</IncludeInEmails>
89
+ </ContactPerson>
90
+ </ContactPersons>
83
91
  <UpdatedDateUTC>2016-08-31T04:55:39.217</UpdatedDateUTC>
84
92
  <IsSupplier>false</IsSupplier>
85
93
  <IsCustomer>false</IsCustomer>
@@ -172,6 +180,7 @@ class ContactTest < Test::Unit::TestCase
172
180
  contact.phone.number = "12345"
173
181
  contact.is_customer = true
174
182
  contact.is_supplier = true
183
+ contact.add_contact_person(first_name: 'John', last_name: 'Smith', email_address: 'john@acme.com', include_in_emails: true)
175
184
 
176
185
  contact
177
186
  end
@@ -75,6 +75,18 @@ class GatewayTest < Test::Unit::TestCase
75
75
  assert result.response_item.first.is_a? XeroGateway::Payment
76
76
  end
77
77
 
78
+ should :get_payroll_calendars do
79
+ XeroGateway::OAuth.any_instance.stubs(:get).returns(stub(:plain_body => get_file_as_string("payroll_calendars.xml"), :code => "200"))
80
+ result = @gateway.get_payroll_calendars
81
+ assert result.response_item.first.is_a? XeroGateway::PayrollCalendar
82
+ end
83
+
84
+ should :get_pay_runs do
85
+ XeroGateway::OAuth.any_instance.stubs(:get).returns(stub(:plain_body => get_file_as_string("pay_runs.xml"), :code => "200"))
86
+ result = @gateway.get_pay_runs
87
+ assert result.response_item.is_a? XeroGateway::PayRun
88
+ end
89
+
78
90
  should :get_contacts do
79
91
  XeroGateway::OAuth.any_instance.stubs(:get).returns(stub(:plain_body => get_file_as_string("contacts.xml"), :code => "200"))
80
92
  result = @gateway.get_contacts
@@ -68,6 +68,27 @@ class InvoiceTest < Test::Unit::TestCase
68
68
  parsed_invoice = XeroGateway::Invoice.from_xml(invoice_element)
69
69
  assert_equal 'http://example.com?with=params&and=more', parsed_invoice.url
70
70
  end
71
+
72
+ should "work with line_item discount rates" do
73
+ invoice = create_test_invoice
74
+ invoice.line_items.first.discount_rate = 27
75
+ invoice_as_xml = invoice.to_xml
76
+
77
+ invoice_element = REXML::XPath.first(REXML::Document.new(invoice_as_xml), "/Invoice")
78
+ result_invoice = XeroGateway::Invoice.from_xml(invoice_element)
79
+
80
+ assert_equal(invoice, result_invoice)
81
+ assert_equal 27, result_invoice.line_items.first.discount_rate
82
+ end
83
+
84
+ should "handle paid-on date" do
85
+ invoice = create_test_invoice(:fully_paid_on => Date.yesterday)
86
+ invoice_element = REXML::XPath.first(REXML::Document.new(invoice.to_xml), "/Invoice")
87
+ result_invoice = XeroGateway::Invoice.from_xml(invoice_element)
88
+
89
+ assert_equal(invoice, result_invoice)
90
+ assert_equal Date.yesterday, result_invoice.fully_paid_on
91
+ end
71
92
  end
72
93
 
73
94
  # Tests the sub_total calculation and that setting it manually doesn't modify the data.
@@ -154,6 +175,26 @@ class InvoiceTest < Test::Unit::TestCase
154
175
  assert_equal(quantity * line_item.unit_amount, line_item.line_amount)
155
176
  end
156
177
 
178
+ def test_line_amount_discount_calculation
179
+ invoice = create_test_invoice
180
+ line_item = invoice.line_items.first
181
+ line_item.discount_rate = 12.5
182
+
183
+ # Make sure that everything adds up to begin with.
184
+ expected_amount = line_item.quantity * line_item.unit_amount * 0.875
185
+ assert_equal(expected_amount, line_item.line_amount)
186
+
187
+ # Change the line_amount and check that it doesn't modify anything.
188
+ line_item.line_amount = expected_amount * 10
189
+ assert_equal(expected_amount, line_item.line_amount)
190
+
191
+ # Change the quantity and check that the line_amount has been updated.
192
+ quantity = line_item.quantity + 2
193
+ line_item.quantity = quantity
194
+ assert_not_equal(expected_amount, line_item.line_amount)
195
+ assert_equal(quantity * line_item.unit_amount * 0.875, line_item.line_amount)
196
+ end
197
+
157
198
  # Ensure that the totalling methods don't raise exceptions, even when
158
199
  # invoice.line_items is empty.
159
200
  def test_totalling_methods_when_line_items_empty
@@ -286,7 +327,7 @@ class InvoiceTest < Test::Unit::TestCase
286
327
  def test_optional_params
287
328
  eur_code = "EUR"
288
329
  eur_rate = 1.80
289
-
330
+
290
331
  invoice = create_test_invoice(:url => 'http://example.com', :branding_theme_id => 'a94a78db-5cc6-4e26-a52b-045237e56e6e', :currency_code => eur_code, :currency_rate => eur_rate)
291
332
  assert_equal 'http://example.com', invoice.url
292
333
  assert_equal 'a94a78db-5cc6-4e26-a52b-045237e56e6e', invoice.branding_theme_id
@@ -0,0 +1,45 @@
1
+ require_relative '../../test_helper.rb'
2
+
3
+ class ReportRowTest < Test::Unit::TestCase
4
+
5
+ context "with a sample row" do
6
+ setup do
7
+ @row = XeroGateway::Report::Row.new(
8
+ ["Account", "Debit", "Credit"],
9
+ ["Sales (200)", 560_00, 0],
10
+ "Bank Accounts"
11
+ )
12
+ end
13
+
14
+ should "be able to access using the deprecated column_n API" do
15
+ ActiveSupport::Deprecation.silence do
16
+ assert_equal @row.column_1, "Sales (200)"
17
+ assert_equal @row.column_3, 0
18
+
19
+ assert @row.respond_to?(:column_1)
20
+ end
21
+ end
22
+
23
+ should "be able to access using an underscored column name" do
24
+ assert_equal @row.account, "Sales (200)"
25
+ assert @row.respond_to?(:account)
26
+ end
27
+
28
+ should "be able to access using an array index" do
29
+ assert_equal @row[0], "Sales (200)"
30
+ assert_equal @row[1], 560_00
31
+ end
32
+
33
+ should "be able to access using a string index" do
34
+ assert_equal @row["Account"], "Sales (200)"
35
+ assert_equal @row["Debit"], 560_00
36
+ end
37
+
38
+ should "be able to access using a symbol index" do
39
+ assert_equal @row[:account], "Sales (200)"
40
+ assert_equal @row[:debit], 560_00
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -19,60 +19,86 @@ class ReportTest < Test::Unit::TestCase
19
19
  end
20
20
 
21
21
  context :from_xml do
22
- setup do
23
- xml_response = get_file("reports/bank_statement.xml")
24
- xml_response.gsub!(/\n +/,'')
25
- xml_doc = REXML::Document.new(xml_response)
26
- xpath_report = XPath.first(xml_doc, "//Report")
27
- @report = XeroGateway::Report.from_xml(xpath_report)
28
- end
22
+ context "with a bank statement report" do
23
+ setup do
24
+ @report = make_report_from_xml("bank_statement")
25
+ end
26
+
27
+ should "create a bank statement report" do
28
+ assert @report.is_a?(XeroGateway::Report)
29
+ assert_equal [], @report.errors
30
+ assert_equal Date.parse("27 May 2014"), @report.report_date
31
+ assert_equal "BankStatement", @report.report_id
32
+ assert_equal "Bank Statement", @report.report_name
33
+ expected_titles = ["Bank Statement", "Business Bank Account", "Demo Company (NZ)", "From 1 May 2014 to 27 May 2014"]
34
+ assert_equal expected_titles, @report.report_titles
35
+ assert_equal "BankStatement", @report.report_type
36
+ assert_equal Time.parse("2014-05-26 22:36:07 +0000").to_i, @report.updated_at.to_i
37
+ expected_names = { :column_1=>"Date", :column_2=>"Description", :column_3=>"Reference", :column_4=>"Reconciled", :column_5=>"Source", :column_6=>"Amount", :column_7=>"Balance" }
38
+ assert_equal expected_names, @report.column_names
39
+
40
+ ###
41
+ # REPORT BODY
42
+ assert @report.body.is_a?(Array)
29
43
 
30
- should "create a bank statement report" do
31
- assert @report.is_a?(XeroGateway::Report)
32
- assert_equal [], @report.errors
33
- assert_equal Date.parse("27 May 2014"), @report.report_date
34
- assert_equal "BankStatement", @report.report_id
35
- assert_equal "Bank Statement", @report.report_name
36
- expected_titles = ["Bank Statement", "Business Bank Account", "Demo Company (NZ)", "From 1 May 2014 to 27 May 2014"]
37
- assert_equal expected_titles, @report.report_titles
38
- assert_equal "BankStatement", @report.report_type
39
- assert_equal Time.parse("2014-05-26 22:36:07 +0000").to_i, @report.updated_at.to_i
40
- expected_names = { :column_1=>"Date", :column_2=>"Description", :column_3=>"Reference", :column_4=>"Reconciled", :column_5=>"Source", :column_6=>"Amount", :column_7=>"Balance" }
41
- assert_equal expected_names, @report.column_names
44
+ # First = Opening Balance
45
+ first_statement = @report.body.first
46
+ assert_equal "2014-05-01T00:00:00", first_statement.date
47
+ assert_equal "Opening Balance", first_statement.description
48
+ assert_equal nil, first_statement.reference
49
+ assert_equal nil, first_statement.reconciled
50
+ assert_equal nil, first_statement.source
51
+ assert_equal nil, first_statement.amount
52
+ assert_equal "15461.97", first_statement.balance
42
53
 
43
- ###
44
- # REPORT BODY
45
- assert @report.body.is_a?(Array)
54
+ # Second = Bank Transaction/Statement
55
+ second_statement = @report.body.second
56
+ assert_equal "2014-05-01T00:00:00", second_statement.date
57
+ assert_equal "Ridgeway Banking Corporation", second_statement.description
58
+ assert_equal "Fee", second_statement.reference
59
+ assert_equal "No", second_statement.reconciled
60
+ assert_equal "Import", second_statement.source
61
+ assert_equal "-15.00", second_statement.amount
62
+ assert_equal "15446.97", second_statement.balance
63
+
64
+ # Third
65
+ third_statement = @report.body.third
66
+ assert_equal nil, third_statement.description.value # no description, but other attributes
67
+ assert_equal "Eft", third_statement.reference
68
+ assert_equal "No", third_statement.reconciled
69
+ assert_equal "Import", third_statement.source
70
+ assert_equal "-15.75", third_statement.amount
71
+ assert_equal "15431.22", third_statement.balance
72
+ end
73
+ end
46
74
 
47
- # First = Opening Balance
48
- first_statement = @report.body.first
49
- assert_equal "2014-05-01T00:00:00", first_statement.column_1
50
- assert_equal "Opening Balance", first_statement.column_2
51
- assert_equal nil, first_statement.column_3
52
- assert_equal nil, first_statement.column_4
53
- assert_equal nil, first_statement.column_5
54
- assert_equal nil, first_statement.column_6
55
- assert_equal "15461.97", first_statement.column_7
75
+ context "with a trial balance report" do
76
+ setup do
77
+ @report = make_report_from_xml("trial_balance")
78
+ end
56
79
 
57
- # Second = Bank Transaction/Statement
58
- second_statement = @report.body.second
59
- assert_equal "2014-05-01T00:00:00", second_statement.column_1
60
- assert_equal "Ridgeway Banking Corporation", second_statement.column_2
61
- assert_equal "Fee", second_statement.column_3
62
- assert_equal "No", second_statement.column_4
63
- assert_equal "Import", second_statement.column_5
64
- assert_equal "-15.00", second_statement.column_6
65
- assert_equal "15446.97", second_statement.column_7
80
+ should "set attributes on individual cells" do
81
+ first_statement = @report.body.first
82
+ assert_equal "Sales (200)", first_statement.account.value
83
+ assert_equal({ account: "7d05a53d-613d-4eb2-a2fc-dcb6adb80b80" }, first_statement.account.attributes)
84
+ end
66
85
 
67
- # Third
68
- third_statement = @report.body.third
69
- assert_equal nil, third_statement.column_2 # no description, but other attributes
70
- assert_equal "Eft", third_statement.column_3
71
- assert_equal "No", third_statement.column_4
72
- assert_equal "Import", third_statement.column_5
73
- assert_equal "-15.75", third_statement.column_6
74
- assert_equal "15431.22", third_statement.column_7
86
+ should "have all rows and section titles" do
87
+ assert_equal 15, @report.rows.length
88
+ assert_equal %w(Revenue Expenses Assets Liabilities Equity), @report.rows.map(&:section_name).uniq.compact
89
+ end
75
90
  end
91
+
92
+ end
93
+
94
+ private
95
+
96
+ def make_report_from_xml(report_name = "bank_statement")
97
+ xml_response = get_file("reports/#{report_name}.xml")
98
+ xml_response.gsub!(/\n +/,'')
99
+ xml_doc = REXML::Document.new(xml_response)
100
+ xpath_report = XPath.first(xml_doc, "//Report")
101
+ XeroGateway::Report.from_xml(xpath_report)
76
102
  end
77
103
 
78
104
  end
data/xero_gateway.gemspec CHANGED
@@ -10,7 +10,6 @@ Gem::Specification.new do |s|
10
10
  s.email = ["me@nikwakelin.com", "jared@minutedock.com"]
11
11
  s.homepage = "http://github.com/xero-gateway/xero_gateway"
12
12
  s.description = "Enables Ruby based applications to communicate with the Xero API"
13
- s.has_rdoc = false
14
13
  s.authors = ["Tim Connor", "Nik Wakelin", "Jared Armstrong"]
15
14
  s.license = "MIT"
16
15
 
@@ -32,5 +31,6 @@ Gem::Specification.new do |s|
32
31
  s.add_development_dependency "mocha"
33
32
  s.add_development_dependency "shoulda"
34
33
  s.add_development_dependency "libxml-ruby", "2.7.0"
34
+ s.add_development_dependency "byebug"
35
35
 
36
36
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xero_gateway
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Connor
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2017-11-03 00:00:00.000000000 Z
13
+ date: 2019-02-14 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: builder
@@ -152,6 +152,20 @@ dependencies:
152
152
  - - '='
153
153
  - !ruby/object:Gem::Version
154
154
  version: 2.7.0
155
+ - !ruby/object:Gem::Dependency
156
+ name: byebug
157
+ requirement: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ type: :development
163
+ prerelease: false
164
+ version_requirements: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
155
169
  description: Enables Ruby based applications to communicate with the Xero API
156
170
  email:
157
171
  - me@nikwakelin.com
@@ -177,6 +191,7 @@ files:
177
191
  - lib/xero_gateway/ca-certificates.crt
178
192
  - lib/xero_gateway/contact.rb
179
193
  - lib/xero_gateway/contact_group.rb
194
+ - lib/xero_gateway/contact_person.rb
180
195
  - lib/xero_gateway/credit_note.rb
181
196
  - lib/xero_gateway/currency.rb
182
197
  - lib/xero_gateway/dates.rb
@@ -195,10 +210,14 @@ files:
195
210
  - lib/xero_gateway/oauth.rb
196
211
  - lib/xero_gateway/organisation.rb
197
212
  - lib/xero_gateway/partner_app.rb
213
+ - lib/xero_gateway/pay_run.rb
198
214
  - lib/xero_gateway/payment.rb
215
+ - lib/xero_gateway/payroll_calendar.rb
199
216
  - lib/xero_gateway/phone.rb
200
217
  - lib/xero_gateway/private_app.rb
201
218
  - lib/xero_gateway/report.rb
219
+ - lib/xero_gateway/report/cell.rb
220
+ - lib/xero_gateway/report/row.rb
202
221
  - lib/xero_gateway/response.rb
203
222
  - lib/xero_gateway/tax_rate.rb
204
223
  - lib/xero_gateway/tracking_category.rb
@@ -246,6 +265,7 @@ files:
246
265
  - test/unit/oauth_test.rb
247
266
  - test/unit/organisation_test.rb
248
267
  - test/unit/payment_test.rb
268
+ - test/unit/report/row_test.rb
249
269
  - test/unit/report_test.rb
250
270
  - test/unit/tax_rate_test.rb
251
271
  - test/unit/tracking_category_test.rb
@@ -270,7 +290,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
270
290
  version: '0'
271
291
  requirements: []
272
292
  rubyforge_project:
273
- rubygems_version: 2.5.1
293
+ rubygems_version: 2.5.2.3
274
294
  signing_key:
275
295
  specification_version: 4
276
296
  summary: Enables Ruby based applications to communicate with the Xero API