tlconnor-xero_gateway 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.textile +11 -0
- data/README.textile +21 -8
- data/lib/xero_gateway.rb +14 -13
- data/lib/xero_gateway/account.rb +17 -2
- data/lib/xero_gateway/accounts_list.rb +77 -0
- data/lib/xero_gateway/address.rb +62 -3
- data/lib/xero_gateway/ca-certificates.crt +2560 -0
- data/lib/xero_gateway/contact.rb +110 -62
- data/lib/xero_gateway/error.rb +1 -2
- data/lib/xero_gateway/gateway.rb +55 -17
- data/lib/xero_gateway/http.rb +42 -46
- data/lib/xero_gateway/invoice.rb +132 -75
- data/lib/xero_gateway/line_item.rb +93 -7
- data/lib/xero_gateway/phone.rb +56 -2
- data/lib/xero_gateway/response.rb +1 -2
- data/lib/xero_gateway/tracking_category.rb +13 -12
- data/test/integration/accounts_list_test.rb +111 -0
- data/test/integration/create_contact_test.rb +46 -6
- data/test/integration/create_invoice_test.rb +29 -6
- data/test/test_helper.rb +4 -5
- data/test/unit/contact_test.rb +43 -0
- data/test/unit/invoice_test.rb +215 -22
- data/xero_gateway.gemspec +5 -2
- metadata +8 -3
data/lib/xero_gateway/invoice.rb
CHANGED
@@ -3,10 +3,35 @@ module XeroGateway
|
|
3
3
|
include Dates
|
4
4
|
include Money
|
5
5
|
|
6
|
+
class Error < RuntimeError; end
|
7
|
+
class NoGatewayError < Error; end
|
8
|
+
|
9
|
+
INVOICE_TYPE = {
|
10
|
+
'ACCREC' => 'Accounts Receivable',
|
11
|
+
'ACCPAY' => 'Accounts Payable'
|
12
|
+
} unless defined?(INVOICE_TYPE)
|
13
|
+
|
14
|
+
INVOICE_STATUS = {
|
15
|
+
'AUTHORISED' => 'Approved invoices awaiting payment',
|
16
|
+
'DELETED' => 'Draft invoices that are deleted',
|
17
|
+
'DRAFT' => 'Invoices saved as draft or entered via API',
|
18
|
+
'PAID' => 'Invoices approved and fully paid',
|
19
|
+
'SUBMITTED' => 'Invoices entered by an employee awaiting approval',
|
20
|
+
'VOID' => 'Approved invoices that are voided'
|
21
|
+
} unless defined?(INVOICE_STATUS)
|
22
|
+
|
23
|
+
# Xero::Gateway associated with this invoice.
|
24
|
+
attr_accessor :gateway
|
25
|
+
|
26
|
+
# Any errors that occurred when the #valid? method called.
|
27
|
+
attr_reader :errors
|
28
|
+
|
6
29
|
# All accessible fields
|
7
|
-
attr_accessor :invoice_id, :invoice_number, :invoice_type, :invoice_status, :date, :due_date, :reference, :tax_inclusive, :includes_tax, :
|
30
|
+
attr_accessor :invoice_id, :invoice_number, :invoice_type, :invoice_status, :date, :due_date, :reference, :tax_inclusive, :includes_tax, :line_items, :contact
|
8
31
|
|
9
32
|
def initialize(params = {})
|
33
|
+
@errors ||= []
|
34
|
+
|
10
35
|
params = {
|
11
36
|
:contact => Contact.new,
|
12
37
|
:date => Time.now,
|
@@ -15,13 +40,96 @@ module XeroGateway
|
|
15
40
|
}.merge(params)
|
16
41
|
|
17
42
|
params.each do |k,v|
|
18
|
-
self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair
|
19
43
|
self.send("#{k}=", v)
|
20
44
|
end
|
21
45
|
|
22
46
|
@line_items ||= []
|
23
|
-
end
|
24
|
-
|
47
|
+
end
|
48
|
+
|
49
|
+
# Validate the Address record according to what will be valid by the gateway.
|
50
|
+
#
|
51
|
+
# Usage:
|
52
|
+
# address.valid? # Returns true/false
|
53
|
+
#
|
54
|
+
# Additionally sets address.errors array to an array of field/error.
|
55
|
+
def valid?
|
56
|
+
@errors = []
|
57
|
+
|
58
|
+
if !invoice_id.nil? && invoice_id !~ GUID_REGEX
|
59
|
+
@errors << ['invoice_id', 'must be blank or a valid Xero GUID']
|
60
|
+
end
|
61
|
+
|
62
|
+
if invoice_status && !INVOICE_STATUS[invoice_status]
|
63
|
+
@errors << ['invoice_status', "must be one of #{INVOICE_STATUS.keys.join('/')}"]
|
64
|
+
end
|
65
|
+
|
66
|
+
unless invoice_number
|
67
|
+
@errors << ['invoice_number', "can't be blank"]
|
68
|
+
end
|
69
|
+
|
70
|
+
unless date
|
71
|
+
@errors << ['invoice_date', "can't be blank"]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Make sure contact is valid.
|
75
|
+
unless contact.valid?
|
76
|
+
@errors << ['contact', 'is invalid']
|
77
|
+
end
|
78
|
+
|
79
|
+
# Make sure all line_items are valid.
|
80
|
+
unless line_items.all? { | line_item | line_item.valid? }
|
81
|
+
@errors << ['line_items', "at least one line item invalid"]
|
82
|
+
end
|
83
|
+
|
84
|
+
@errors.size == 0
|
85
|
+
end
|
86
|
+
|
87
|
+
# Deprecated (but API for setter remains).
|
88
|
+
#
|
89
|
+
# As sub_total must equal SUM(line_item.line_amount) for the API call to pass, this is now
|
90
|
+
# automatically calculated in the sub_total method.
|
91
|
+
def sub_total=(value)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Calculate the sub_total as the SUM(line_item.line_amount).
|
95
|
+
def sub_total
|
96
|
+
line_items.inject(BigDecimal.new('0')) { | sum, line_item | sum + BigDecimal.new(line_item.line_amount.to_s) }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Deprecated (but API for setter remains).
|
100
|
+
#
|
101
|
+
# As total_tax must equal SUM(line_item.tax_amount) for the API call to pass, this is now
|
102
|
+
# automatically calculated in the total_tax method.
|
103
|
+
def total_tax=(value)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Calculate the total_tax as the SUM(line_item.tax_amount).
|
107
|
+
def total_tax
|
108
|
+
line_items.inject(BigDecimal.new('0')) { | sum, line_item | sum + BigDecimal.new(line_item.tax_amount.to_s) }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Deprecated (but API for setter remains).
|
112
|
+
#
|
113
|
+
# As total must equal sub_total + total_tax for the API call to pass, this is now
|
114
|
+
# automatically calculated in the total method.
|
115
|
+
def total=(value)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Calculate the toal as sub_total + total_tax.
|
119
|
+
def total
|
120
|
+
sub_total + total_tax
|
121
|
+
end
|
122
|
+
|
123
|
+
# Helper method to check if the invoice is accounts payable.
|
124
|
+
def accounts_payable?
|
125
|
+
invoice_type == 'ACCPAY'
|
126
|
+
end
|
127
|
+
|
128
|
+
# Helper method to check if the invoice is accounts receivable.
|
129
|
+
def accounts_receivable?
|
130
|
+
invoice_type == 'ACCREC'
|
131
|
+
end
|
132
|
+
|
25
133
|
def ==(other)
|
26
134
|
["invoice_number", "invoice_type", "invoice_status", "reference", "tax_inclusive", "includes_tax", "sub_total", "total_tax", "total", "contact", "line_items"].each do |field|
|
27
135
|
return false if send(field) != other.send(field)
|
@@ -32,41 +140,28 @@ module XeroGateway
|
|
32
140
|
return true
|
33
141
|
end
|
34
142
|
|
143
|
+
# General purpose createsave method.
|
144
|
+
# If contact_id and contact_number are nil then create, otherwise, attempt to save.
|
145
|
+
def save
|
146
|
+
create
|
147
|
+
end
|
148
|
+
|
149
|
+
# Creates this invoice record (using gateway.create_invoice) with the associated gateway.
|
150
|
+
# If no gateway set, raise a Xero::Invoice::NoGatewayError exception.
|
151
|
+
def create
|
152
|
+
raise NoGatewayError unless gateway
|
153
|
+
gateway.create_invoice(self)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Alias create as save as this is currently the only write action.
|
157
|
+
alias_method :save, :create
|
158
|
+
|
35
159
|
def to_xml
|
36
160
|
b = Builder::XmlMarkup.new
|
37
161
|
|
38
162
|
b.Invoice {
|
39
163
|
b.InvoiceType self.invoice_type
|
40
|
-
b
|
41
|
-
b.ContactID self.contact.contact_id if self.contact.contact_id
|
42
|
-
b.Name self.contact.name
|
43
|
-
b.EmailAddress self.contact.email if self.contact.email
|
44
|
-
b.Addresses {
|
45
|
-
self.contact.addresses.each do |address|
|
46
|
-
b.Address {
|
47
|
-
b.AddressType address.address_type
|
48
|
-
b.AddressLine1 address.line_1 if address.line_1
|
49
|
-
b.AddressLine2 address.line_2 if address.line_2
|
50
|
-
b.AddressLine3 address.line_3 if address.line_3
|
51
|
-
b.AddressLine4 address.line_4 if address.line_4
|
52
|
-
b.City address.city if address.city
|
53
|
-
b.Region address.region if address.region
|
54
|
-
b.PostalCode address.post_code if address.post_code
|
55
|
-
b.Country address.country if address.country
|
56
|
-
}
|
57
|
-
end
|
58
|
-
}
|
59
|
-
b.Phones {
|
60
|
-
self.contact.phones.each do |phone|
|
61
|
-
b.Phone {
|
62
|
-
b.PhoneType phone.phone_type
|
63
|
-
b.PhoneNumber phone.number
|
64
|
-
b.PhoneAreaCode phone.area_code if phone.area_code
|
65
|
-
b.PhoneCountryCode phone.country_code if phone.country_code
|
66
|
-
}
|
67
|
-
end
|
68
|
-
}
|
69
|
-
}
|
164
|
+
contact.to_xml(b)
|
70
165
|
b.InvoiceDate Invoice.format_date_time(self.date)
|
71
166
|
b.DueDate Invoice.format_date_time(self.due_date) if self.due_date
|
72
167
|
b.InvoiceNumber self.invoice_number
|
@@ -78,21 +173,7 @@ module XeroGateway
|
|
78
173
|
b.Total Invoice.format_money(self.total) if self.total
|
79
174
|
b.LineItems {
|
80
175
|
self.line_items.each do |line_item|
|
81
|
-
b
|
82
|
-
b.Description line_item.description
|
83
|
-
b.Quantity line_item.quantity if line_item.quantity
|
84
|
-
b.UnitAmount Invoice.format_money(line_item.unit_amount)
|
85
|
-
b.TaxType line_item.tax_type if line_item.tax_type
|
86
|
-
b.TaxAmount Invoice.format_money(line_item.tax_amount) if line_item.tax_amount
|
87
|
-
b.LineAmount Invoice.format_money(line_item.line_amount)
|
88
|
-
b.AccountCode line_item.account_code || 200
|
89
|
-
b.Tracking {
|
90
|
-
b.TrackingCategory {
|
91
|
-
b.Name line_item.tracking_category
|
92
|
-
b.Option line_item.tracking_option
|
93
|
-
}
|
94
|
-
}
|
95
|
-
}
|
176
|
+
line_item.to_xml(b)
|
96
177
|
end
|
97
178
|
}
|
98
179
|
}
|
@@ -115,34 +196,10 @@ module XeroGateway
|
|
115
196
|
when "TotalTax" then invoice.total_tax = BigDecimal.new(element.text)
|
116
197
|
when "Total" then invoice.total = BigDecimal.new(element.text)
|
117
198
|
when "Contact" then invoice.contact = Contact.from_xml(element)
|
118
|
-
when "LineItems" then element.children.each {|line_item| invoice.line_items <<
|
199
|
+
when "LineItems" then element.children.each {|line_item| invoice.line_items << LineItem.from_xml(line_item)}
|
119
200
|
end
|
120
201
|
end
|
121
202
|
invoice
|
122
|
-
end
|
123
|
-
|
124
|
-
private
|
125
|
-
|
126
|
-
def self.parse_line_item(line_item_element)
|
127
|
-
line_item = LineItem.new
|
128
|
-
line_item_element.children.each do |element|
|
129
|
-
case(element.name)
|
130
|
-
when "LineItemID" then line_item.line_item_id = element.text
|
131
|
-
when "Description" then line_item.description = element.text
|
132
|
-
when "Quantity" then line_item.quantity = element.text.to_i
|
133
|
-
when "UnitAmount" then line_item.unit_amount = BigDecimal.new(element.text)
|
134
|
-
when "TaxType" then line_item.tax_type = element.text
|
135
|
-
when "TaxAmount" then line_item.tax_amount = BigDecimal.new(element.text)
|
136
|
-
when "LineAmount" then line_item.line_amount = BigDecimal.new(element.text)
|
137
|
-
when "AccountCode" then line_item.account_code = element.text
|
138
|
-
when "Tracking" then
|
139
|
-
if element.elements['TrackingCategory']
|
140
|
-
line_item.tracking_category = element.elements['TrackingCategory/Name'].text
|
141
|
-
line_item.tracking_option = element.elements['TrackingCategory/Option'].text
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|
145
|
-
line_item
|
146
|
-
end
|
203
|
+
end
|
147
204
|
end
|
148
205
|
end
|
@@ -1,17 +1,103 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'account')
|
2
|
+
|
1
3
|
module XeroGateway
|
2
4
|
class LineItem
|
3
|
-
|
4
|
-
attr_accessor :line_item_id, :description, :quantity, :unit_amount, :tax_type, :tax_amount, :line_amount, :account_code, :tracking_category, :tracking_option
|
5
|
+
include Money
|
5
6
|
|
7
|
+
TAX_TYPE = Account::TAX_TYPE unless defined?(TAX_TYPE)
|
8
|
+
|
9
|
+
# Any errors that occurred when the #valid? method called.
|
10
|
+
attr_reader :errors
|
11
|
+
|
12
|
+
# All accessible fields
|
13
|
+
attr_accessor :line_item_id, :description, :quantity, :unit_amount, :tax_type, :tax_amount, :account_code, :tracking_category, :tracking_option
|
14
|
+
|
6
15
|
def initialize(params = {})
|
7
|
-
|
8
|
-
|
9
|
-
|
16
|
+
@errors ||= []
|
17
|
+
@quantity = 1
|
18
|
+
@unit_amount = BigDecimal.new('0')
|
19
|
+
@tax_amount = BigDecimal.new('0')
|
10
20
|
|
11
21
|
params.each do |k,v|
|
12
|
-
self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair
|
13
22
|
self.send("#{k}=", v)
|
14
23
|
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Validate the LineItem record according to what will be valid by the gateway.
|
27
|
+
#
|
28
|
+
# Usage:
|
29
|
+
# line_item.valid? # Returns true/false
|
30
|
+
#
|
31
|
+
# Additionally sets line_item.errors array to an array of field/error.
|
32
|
+
def valid?
|
33
|
+
@errors = []
|
34
|
+
|
35
|
+
if !line_item_id.nil? && line_item_id !~ GUID_REGEX
|
36
|
+
@errors << ['line_item_id', 'must be blank or a valid Xero GUID']
|
37
|
+
end
|
38
|
+
|
39
|
+
unless description
|
40
|
+
@errors << ['description', "can't be blank"]
|
41
|
+
end
|
42
|
+
|
43
|
+
if tax_type && !TAX_TYPE[tax_type]
|
44
|
+
@errors << ['tax_type', "must be one of #{TAX_TYPE.keys.join('/')}"]
|
45
|
+
end
|
46
|
+
|
47
|
+
@errors.size == 0
|
48
|
+
end
|
49
|
+
|
50
|
+
# Deprecated (but API for setter remains).
|
51
|
+
#
|
52
|
+
# As line_amount must equal quantity * unit_amount for the API call to pass, this is now
|
53
|
+
# automatically calculated in the line_amount method.
|
54
|
+
def line_amount=(value)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Calculate the line_amount as quantity * unit_amount as this value must be correct
|
58
|
+
# for the API call to succeed.
|
59
|
+
def line_amount
|
60
|
+
quantity * unit_amount
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_xml(b = Builder::XmlMarkup.new)
|
64
|
+
b.LineItem {
|
65
|
+
b.Description description
|
66
|
+
b.Quantity quantity if quantity
|
67
|
+
b.UnitAmount LineItem.format_money(unit_amount)
|
68
|
+
b.TaxType tax_type if tax_type
|
69
|
+
b.TaxAmount LineItem.format_money(tax_amount) if tax_amount
|
70
|
+
b.LineAmount LineItem.format_money(line_amount)
|
71
|
+
b.AccountCode account_code if account_code
|
72
|
+
b.Tracking {
|
73
|
+
b.TrackingCategory {
|
74
|
+
b.Name tracking_category
|
75
|
+
b.Option tracking_option
|
76
|
+
}
|
77
|
+
}
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.from_xml(line_item_element)
|
82
|
+
line_item = LineItem.new
|
83
|
+
line_item_element.children.each do |element|
|
84
|
+
case(element.name)
|
85
|
+
when "LineItemID" then line_item.line_item_id = element.text
|
86
|
+
when "Description" then line_item.description = element.text
|
87
|
+
when "Quantity" then line_item.quantity = element.text.to_i
|
88
|
+
when "UnitAmount" then line_item.unit_amount = BigDecimal.new(element.text)
|
89
|
+
when "TaxType" then line_item.tax_type = element.text
|
90
|
+
when "TaxAmount" then line_item.tax_amount = BigDecimal.new(element.text)
|
91
|
+
when "LineAmount" then line_item.line_amount = BigDecimal.new(element.text)
|
92
|
+
when "AccountCode" then line_item.account_code = element.text
|
93
|
+
when "Tracking" then
|
94
|
+
if element.elements['TrackingCategory']
|
95
|
+
line_item.tracking_category = element.elements['TrackingCategory/Name'].text
|
96
|
+
line_item.tracking_option = element.elements['TrackingCategory/Option'].text
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
line_item
|
15
101
|
end
|
16
102
|
|
17
103
|
def ==(other)
|
@@ -21,5 +107,5 @@ module XeroGateway
|
|
21
107
|
end
|
22
108
|
return true
|
23
109
|
end
|
24
|
-
end
|
110
|
+
end
|
25
111
|
end
|
data/lib/xero_gateway/phone.rb
CHANGED
@@ -1,18 +1,72 @@
|
|
1
1
|
module XeroGateway
|
2
2
|
class Phone
|
3
|
+
|
4
|
+
PHONE_TYPE = {
|
5
|
+
'DEFAULT' => 'Default',
|
6
|
+
'DDI' => 'Direct Dial-In',
|
7
|
+
'MOBILE' => 'Mobile',
|
8
|
+
'FAX' => 'Fax'
|
9
|
+
} unless defined?(PHONE_TYPE)
|
10
|
+
|
11
|
+
# Any errors that occurred when the #valid? method called.
|
12
|
+
attr_reader :errors
|
13
|
+
|
3
14
|
attr_accessor :phone_type, :number, :area_code, :country_code
|
4
15
|
|
5
16
|
def initialize(params = {})
|
17
|
+
@errors ||= []
|
18
|
+
|
6
19
|
params = {
|
7
20
|
:phone_type => "DEFAULT"
|
8
21
|
}.merge(params)
|
9
22
|
|
10
23
|
params.each do |k,v|
|
11
|
-
self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair
|
12
24
|
self.send("#{k}=", v)
|
13
25
|
end
|
14
26
|
end
|
15
27
|
|
28
|
+
# Validate the Phone record according to what will be valid by the gateway.
|
29
|
+
#
|
30
|
+
# Usage:
|
31
|
+
# phone.valid? # Returns true/false
|
32
|
+
#
|
33
|
+
# Additionally sets phone.errors array to an array of field/error.
|
34
|
+
def valid?
|
35
|
+
@errors = []
|
36
|
+
|
37
|
+
unless number
|
38
|
+
@errors << ['number', "can't be blank"]
|
39
|
+
end
|
40
|
+
|
41
|
+
if phone_type && !PHONE_TYPE[phone_type]
|
42
|
+
@errors << ['phone_type', "must be one of #{PHONE_TYPE.keys.join('/')}"]
|
43
|
+
end
|
44
|
+
|
45
|
+
@errors.size == 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_xml(b = Builder::XmlMarkup.new)
|
49
|
+
b.Phone {
|
50
|
+
b.PhoneType phone_type
|
51
|
+
b.PhoneNumber number
|
52
|
+
b.PhoneAreaCode area_code if area_code
|
53
|
+
b.PhoneCountryCode country_code if country_code
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.from_xml(phone_element)
|
58
|
+
phone = Phone.new
|
59
|
+
phone_element.children.each do |element|
|
60
|
+
case(element.name)
|
61
|
+
when "PhoneType" then phone.phone_type = element.text
|
62
|
+
when "PhoneNumber" then phone.number = element.text
|
63
|
+
when "PhoneAreaCode" then phone.area_code = element.text
|
64
|
+
when "PhoneCountryCode" then phone.country_code = element.text
|
65
|
+
end
|
66
|
+
end
|
67
|
+
phone
|
68
|
+
end
|
69
|
+
|
16
70
|
def ==(other)
|
17
71
|
[:phone_type, :number, :area_code, :country_code].each do |field|
|
18
72
|
return false if send(field) != other.send(field)
|
@@ -20,4 +74,4 @@ module XeroGateway
|
|
20
74
|
return true
|
21
75
|
end
|
22
76
|
end
|
23
|
-
end
|
77
|
+
end
|