xero_gateway 2.5.0 → 2.6.0
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.
- checksums.yaml +4 -4
- data/README.md +24 -9
- data/lib/xero_gateway.rb +3 -0
- data/lib/xero_gateway/base_record.rb +7 -3
- data/lib/xero_gateway/contact.rb +10 -1
- data/lib/xero_gateway/contact_person.rb +33 -0
- data/lib/xero_gateway/gateway.rb +24 -2
- data/lib/xero_gateway/invoice.rb +2 -0
- data/lib/xero_gateway/line_item.rb +25 -21
- data/lib/xero_gateway/pay_run.rb +19 -0
- data/lib/xero_gateway/payroll_calendar.rb +12 -0
- data/lib/xero_gateway/private_app.rb +3 -2
- data/lib/xero_gateway/report.rb +86 -65
- data/lib/xero_gateway/report/cell.rb +28 -0
- data/lib/xero_gateway/report/row.rb +57 -0
- data/lib/xero_gateway/version.rb +1 -1
- data/test/unit/contact_test.rb +9 -0
- data/test/unit/gateway_test.rb +12 -0
- data/test/unit/invoice_test.rb +42 -1
- data/test/unit/report/row_test.rb +45 -0
- data/test/unit/report_test.rb +74 -48
- data/xero_gateway.gemspec +1 -1
- metadata +23 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15bce269e603d39203dbafb5858843c8e8fe8b3e
|
4
|
+
data.tar.gz: a996fa84e4ca2471b3efc06eb68c080fbe4ed8cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
29
|
+
### Authenticating: Public Applications
|
30
30
|
|
31
|
-
Public
|
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://
|
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
|
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
|
-
|
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
|
-
|
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 -
|
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
|
-
|
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
|
80
|
-
when :float
|
81
|
-
when :integer
|
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?
|
data/lib/xero_gateway/contact.rb
CHANGED
@@ -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
|
data/lib/xero_gateway/gateway.rb
CHANGED
@@ -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)
|
data/lib/xero_gateway/invoice.rb
CHANGED
@@ -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
|
data/lib/xero_gateway/report.rb
CHANGED
@@ -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
|
-
|
21
|
-
|
22
|
-
report_element
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
report.
|
35
|
-
|
36
|
-
report.
|
37
|
-
|
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
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
data/lib/xero_gateway/version.rb
CHANGED
data/test/unit/contact_test.rb
CHANGED
@@ -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
|
data/test/unit/gateway_test.rb
CHANGED
@@ -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
|
data/test/unit/invoice_test.rb
CHANGED
@@ -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
|
data/test/unit/report_test.rb
CHANGED
@@ -19,60 +19,86 @@ class ReportTest < Test::Unit::TestCase
|
|
19
19
|
end
|
20
20
|
|
21
21
|
context :from_xml do
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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.
|
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:
|
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.
|
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
|