xero_gateway-n8vision 2.0.20

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.
Files changed (78) hide show
  1. data/Gemfile +12 -0
  2. data/LICENSE +14 -0
  3. data/README.textile +357 -0
  4. data/Rakefile +14 -0
  5. data/examples/oauth.rb +25 -0
  6. data/examples/partner_app.rb +36 -0
  7. data/init.rb +1 -0
  8. data/lib/oauth/oauth_consumer.rb +14 -0
  9. data/lib/xero_gateway.rb +41 -0
  10. data/lib/xero_gateway/account.rb +86 -0
  11. data/lib/xero_gateway/accounts_list.rb +73 -0
  12. data/lib/xero_gateway/address.rb +96 -0
  13. data/lib/xero_gateway/bank_transaction.rb +175 -0
  14. data/lib/xero_gateway/ca-certificates.crt +2560 -0
  15. data/lib/xero_gateway/contact.rb +203 -0
  16. data/lib/xero_gateway/credit_note.rb +220 -0
  17. data/lib/xero_gateway/currency.rb +56 -0
  18. data/lib/xero_gateway/dates.rb +25 -0
  19. data/lib/xero_gateway/error.rb +18 -0
  20. data/lib/xero_gateway/exceptions.rb +51 -0
  21. data/lib/xero_gateway/gateway.rb +698 -0
  22. data/lib/xero_gateway/http.rb +135 -0
  23. data/lib/xero_gateway/http_encoding_helper.rb +49 -0
  24. data/lib/xero_gateway/invoice.rb +238 -0
  25. data/lib/xero_gateway/journal_line.rb +102 -0
  26. data/lib/xero_gateway/line_item.rb +125 -0
  27. data/lib/xero_gateway/line_item_calculations.rb +51 -0
  28. data/lib/xero_gateway/manual_journal.rb +163 -0
  29. data/lib/xero_gateway/money.rb +16 -0
  30. data/lib/xero_gateway/oauth.rb +92 -0
  31. data/lib/xero_gateway/organisation.rb +75 -0
  32. data/lib/xero_gateway/partner_app.rb +30 -0
  33. data/lib/xero_gateway/payment.rb +43 -0
  34. data/lib/xero_gateway/phone.rb +77 -0
  35. data/lib/xero_gateway/private_app.rb +17 -0
  36. data/lib/xero_gateway/response.rb +43 -0
  37. data/lib/xero_gateway/tax_rate.rb +63 -0
  38. data/lib/xero_gateway/tracking_category.rb +87 -0
  39. data/test/integration/accounts_list_test.rb +109 -0
  40. data/test/integration/create_bank_transaction_test.rb +38 -0
  41. data/test/integration/create_contact_test.rb +66 -0
  42. data/test/integration/create_credit_note_test.rb +49 -0
  43. data/test/integration/create_invoice_test.rb +49 -0
  44. data/test/integration/create_manual_journal_test.rb +35 -0
  45. data/test/integration/get_accounts_test.rb +23 -0
  46. data/test/integration/get_bank_transaction_test.rb +51 -0
  47. data/test/integration/get_bank_transactions_test.rb +88 -0
  48. data/test/integration/get_contact_test.rb +28 -0
  49. data/test/integration/get_contacts_test.rb +40 -0
  50. data/test/integration/get_credit_note_test.rb +48 -0
  51. data/test/integration/get_credit_notes_test.rb +90 -0
  52. data/test/integration/get_currencies_test.rb +25 -0
  53. data/test/integration/get_invoice_test.rb +48 -0
  54. data/test/integration/get_invoices_test.rb +92 -0
  55. data/test/integration/get_manual_journal_test.rb +50 -0
  56. data/test/integration/get_manual_journals_test.rb +88 -0
  57. data/test/integration/get_organisation_test.rb +24 -0
  58. data/test/integration/get_tax_rates_test.rb +25 -0
  59. data/test/integration/get_tracking_categories_test.rb +27 -0
  60. data/test/integration/update_bank_transaction_test.rb +31 -0
  61. data/test/integration/update_contact_test.rb +31 -0
  62. data/test/integration/update_invoice_test.rb +31 -0
  63. data/test/integration/update_manual_journal_test.rb +31 -0
  64. data/test/test_helper.rb +217 -0
  65. data/test/unit/account_test.rb +47 -0
  66. data/test/unit/bank_transaction_test.rb +126 -0
  67. data/test/unit/contact_test.rb +97 -0
  68. data/test/unit/credit_note_test.rb +284 -0
  69. data/test/unit/currency_test.rb +31 -0
  70. data/test/unit/gateway_test.rb +119 -0
  71. data/test/unit/invoice_test.rb +326 -0
  72. data/test/unit/manual_journal_test.rb +93 -0
  73. data/test/unit/oauth_test.rb +116 -0
  74. data/test/unit/organisation_test.rb +38 -0
  75. data/test/unit/tax_rate_test.rb +38 -0
  76. data/test/unit/tracking_category_test.rb +52 -0
  77. data/xero_gateway-n8vision.gemspec +15 -0
  78. metadata +178 -0
@@ -0,0 +1,203 @@
1
+ module XeroGateway
2
+ class Contact
3
+ include Dates
4
+
5
+ GUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ unless defined?(GUID_REGEX)
6
+
7
+ CONTACT_STATUS = {
8
+ 'ACTIVE' => 'Active',
9
+ 'DELETED' => 'Deleted'
10
+ } unless defined?(CONTACT_STATUS)
11
+
12
+ # Xero::Gateway associated with this contact.
13
+ attr_accessor :gateway
14
+
15
+ # Any errors that occurred when the #valid? method called.
16
+ attr_reader :errors
17
+
18
+ attr_accessor :contact_id, :contact_number, :status, :name, :first_name, :last_name, :email, :addresses, :phones, :updated_at,
19
+ :bank_account_details, :tax_number, :accounts_receivable_tax_type, :accounts_payable_tax_type, :is_customer, :is_supplier,
20
+ :default_currency, :contact_groups
21
+
22
+
23
+ def initialize(params = {})
24
+ @errors ||= []
25
+
26
+ params = {}.merge(params)
27
+ params.each do |k,v|
28
+ self.send("#{k}=", v)
29
+ end
30
+
31
+ @phones ||= []
32
+ @addresses ||= []
33
+ end
34
+
35
+ def address=(address)
36
+ self.addresses = [address]
37
+ end
38
+
39
+ def address
40
+ self.addresses[0] ||= Address.new
41
+ end
42
+
43
+ # Helper method to add a new address object to this contact.
44
+ #
45
+ # Usage:
46
+ # contact.add_address({
47
+ # :address_type => 'STREET',
48
+ # :line_1 => '100 Queen Street',
49
+ # :city => 'Brisbane',
50
+ # :region => 'QLD',
51
+ # :post_code => '4000',
52
+ # :country => 'Australia'
53
+ # })
54
+ def add_address(address_params)
55
+ self.addresses << Address.new(address_params)
56
+ end
57
+
58
+ def phone=(phone)
59
+ self.phones = [phone]
60
+ end
61
+
62
+ def phone
63
+ if @phones.size > 1
64
+ @phones.detect {|p| p.phone_type == 'DEFAULT'} || phones[0]
65
+ else
66
+ @phones[0] ||= Phone.new
67
+ end
68
+ end
69
+
70
+ # Helper method to add a new phone object to this contact.
71
+ #
72
+ # Usage:
73
+ # contact.add_phone({
74
+ # :phone_type => 'MOBILE',
75
+ # :number => '0400123123'
76
+ # })
77
+ def add_phone(phone_params = {})
78
+ self.phones << Phone.new(phone_params)
79
+ end
80
+
81
+ # Validate the Contact record according to what will be valid by the gateway.
82
+ #
83
+ # Usage:
84
+ # contact.valid? # Returns true/false
85
+ #
86
+ # Additionally sets contact.errors array to an array of field/error.
87
+ def valid?
88
+ @errors = []
89
+
90
+ if !contact_id.nil? && contact_id !~ GUID_REGEX
91
+ @errors << ['contact_id', 'must be blank or a valid Xero GUID']
92
+ end
93
+
94
+ if status && !CONTACT_STATUS[status]
95
+ @errors << ['status', "must be one of #{CONTACT_STATUS.keys.join('/')}"]
96
+ end
97
+
98
+ unless name
99
+ @errors << ['name', "can't be blank"]
100
+ end
101
+
102
+ # Make sure all addresses are correct.
103
+ unless addresses.all? { | address | address.valid? }
104
+ @errors << ['addresses', 'at least one address is invalid']
105
+ end
106
+
107
+ # Make sure all phone numbers are correct.
108
+ unless phones.all? { | phone | phone.valid? }
109
+ @errors << ['phones', 'at least one phone is invalid']
110
+ end
111
+
112
+ @errors.size == 0
113
+ end
114
+
115
+ # General purpose create/save method.
116
+ # If contact_id and contact_number are nil then create, otherwise, attempt to save.
117
+ def save
118
+ if contact_id.nil? && contact_number.nil?
119
+ create
120
+ else
121
+ update
122
+ end
123
+ end
124
+
125
+ # Creates this contact record (using gateway.create_contact) with the associated gateway.
126
+ # If no gateway set, raise a NoGatewayError exception.
127
+ def create
128
+ raise NoGatewayError unless gateway
129
+ gateway.create_contact(self)
130
+ end
131
+
132
+ # Creates this contact record (using gateway.update_contact) with the associated gateway.
133
+ # If no gateway set, raise a NoGatewayError exception.
134
+ def update
135
+ raise NoGatewayError unless gateway
136
+ gateway.update_contact(self)
137
+ end
138
+
139
+ def to_xml(b = Builder::XmlMarkup.new)
140
+ b.Contact {
141
+ b.ContactID self.contact_id if self.contact_id
142
+ b.ContactNumber self.contact_number if self.contact_number
143
+ b.Name self.name
144
+ b.EmailAddress self.email if self.email
145
+ b.FirstName self.first_name if self.first_name
146
+ b.LastName self.last_name if self.last_name
147
+ b.BankAccountDetails self.bank_account_details if self.bank_account_details
148
+ b.TaxNumber self.tax_number if self.tax_number
149
+ b.AccountsReceivableTaxType self.accounts_receivable_tax_type if self.accounts_receivable_tax_type
150
+ b.AccountsPayableTaxType self.accounts_payable_tax_type if self.accounts_payable_tax_type
151
+ b.ContactGroups if self.contact_groups
152
+ b.IsCustomer true if self.is_customer
153
+ b.IsSupplier true if self.is_supplier
154
+ b.DefaultCurrency if self.default_currency
155
+ b.Addresses {
156
+ addresses.each { |address| address.to_xml(b) }
157
+ }
158
+ b.Phones {
159
+ phones.each { |phone| phone.to_xml(b) }
160
+ }
161
+ }
162
+ end
163
+
164
+ # Take a Contact element and convert it into an Contact object
165
+ def self.from_xml(contact_element, gateway = nil)
166
+ contact = Contact.new(:gateway => gateway)
167
+ contact_element.children.each do |element|
168
+ case(element.name)
169
+ when "ContactID" then contact.contact_id = element.text
170
+ when "ContactNumber" then contact.contact_number = element.text
171
+ when "ContactStatus" then contact.status = element.text
172
+ when "Name" then contact.name = element.text
173
+ when "FirstName" then contact.first_name = element.text
174
+ when "LastName" then contact.last_name = element.text
175
+ when "EmailAddress" then contact.email = element.text
176
+ when "Addresses" then element.children.each {|address_element| contact.addresses << Address.from_xml(address_element)}
177
+ when "Phones" then element.children.each {|phone_element| contact.phones << Phone.from_xml(phone_element)}
178
+ when "FirstName" then contact.first_name = element.text
179
+ when "LastName" then contact.last_name = element.text
180
+ when "BankAccountDetails" then contact.bank_account_details = element.text
181
+ when "TaxNumber" then contact.tax_number = element.text
182
+ when "AccountsReceivableTaxType" then contact.accounts_receivable_tax_type = element.text
183
+ when "AccountsPayableTaxType" then contact.accounts_payable_tax_type = element.text
184
+ when "ContactGroups" then contact.contact_groups = element.text
185
+ when "IsCustomer" then contact.is_customer = (element.text == "true")
186
+ when "IsSupplier" then contact.is_supplier = (element.text == "true")
187
+ when "DefaultCurrency" then contact.default_currency = element.text
188
+ end
189
+ end
190
+ contact
191
+ end
192
+
193
+ def ==(other)
194
+ [ :contact_id, :contact_number, :status, :name, :first_name, :last_name, :email, :addresses, :phones, :updated_at,
195
+ :bank_account_details, :tax_number, :accounts_receivable_tax_type, :accounts_payable_tax_type, :is_customer, :is_supplier,
196
+ :default_currency, :contact_groups ].each do |field|
197
+ return false if send(field) != other.send(field)
198
+ end
199
+ return true
200
+ end
201
+
202
+ end
203
+ end
@@ -0,0 +1,220 @@
1
+ module XeroGateway
2
+ class CreditNote
3
+ include Dates
4
+ include Money
5
+ include LineItemCalculations
6
+
7
+ CREDIT_NOTE_TYPE = {
8
+ 'ACCRECCREDIT' => 'Accounts Receivable',
9
+ 'ACCPAYCREDIT' => 'Accounts Payable'
10
+ } unless defined?(CREDIT_NOTE_TYPE)
11
+
12
+ LINE_AMOUNT_TYPES = {
13
+ "Inclusive" => 'CreditNote lines are inclusive tax',
14
+ "Exclusive" => 'CreditNote lines are exclusive of tax (default)',
15
+ "NoTax" => 'CreditNotes lines have no tax'
16
+ } unless defined?(LINE_AMOUNT_TYPES)
17
+
18
+ CREDIT_NOTE_STATUS = {
19
+ 'AUTHORISED' => 'Approved credit_notes awaiting payment',
20
+ 'DELETED' => 'Draft credit_notes that are deleted',
21
+ 'DRAFT' => 'CreditNotes saved as draft or entered via API',
22
+ 'PAID' => 'CreditNotes approved and fully paid',
23
+ 'SUBMITTED' => 'CreditNotes entered by an employee awaiting approval',
24
+ 'VOID' => 'Approved credit_notes that are voided'
25
+ } unless defined?(CREDIT_NOTE_STATUS)
26
+
27
+ GUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ unless defined?(GUID_REGEX)
28
+
29
+ # Xero::Gateway associated with this credit_note.
30
+ attr_accessor :gateway
31
+
32
+ # Any errors that occurred when the #valid? method called.
33
+ attr_reader :errors
34
+
35
+ # Represents whether the line_items have been downloaded when getting from GET /API.XRO/2.0/CreditNotes
36
+ attr_accessor :line_items_downloaded
37
+
38
+ # All accessible fields
39
+ attr_accessor :credit_note_id, :credit_note_number, :type, :status, :date, :reference, :line_amount_types, :currency_code, :line_items, :contact, :payments, :fully_paid_on, :amount_credited
40
+
41
+
42
+ def initialize(params = {})
43
+ @errors ||= []
44
+ @payments ||= []
45
+
46
+ # Check if the line items have been downloaded.
47
+ @line_items_downloaded = (params.delete(:line_items_downloaded) == true)
48
+
49
+ params = {
50
+ :line_amount_types => "Inclusive"
51
+ }.merge(params)
52
+
53
+ params.each do |k,v|
54
+ self.send("#{k}=", v)
55
+ end
56
+
57
+ @line_items ||= []
58
+ end
59
+
60
+ # Validate the Address record according to what will be valid by the gateway.
61
+ #
62
+ # Usage:
63
+ # address.valid? # Returns true/false
64
+ #
65
+ # Additionally sets address.errors array to an array of field/error.
66
+ def valid?
67
+ @errors = []
68
+
69
+ if !credit_note_id.nil? && credit_note_id !~ GUID_REGEX
70
+ @errors << ['credit_note_id', 'must be blank or a valid Xero GUID']
71
+ end
72
+
73
+ if status && !CREDIT_NOTE_STATUS[status]
74
+ @errors << ['status', "must be one of #{CREDIT_NOTE_STATUS.keys.join('/')}"]
75
+ end
76
+
77
+ if line_amount_types && !LINE_AMOUNT_TYPES[line_amount_types]
78
+ @errors << ['line_amount_types', "must be one of #{LINE_AMOUNT_TYPES.keys.join('/')}"]
79
+ end
80
+
81
+ unless date
82
+ @errors << ['credit_note_date', "can't be blank"]
83
+ end
84
+
85
+ # Make sure contact is valid.
86
+ unless @contact && @contact.valid?
87
+ @errors << ['contact', 'is invalid']
88
+ end
89
+
90
+ # Make sure all line_items are valid.
91
+ unless line_items.all? { | line_item | line_item.valid? }
92
+ @errors << ['line_items', "at least one line item invalid"]
93
+ end
94
+
95
+ @errors.size == 0
96
+ end
97
+
98
+ # Helper method to create the associated contact object.
99
+ def build_contact(params = {})
100
+ self.contact = gateway ? gateway.build_contact(params) : Contact.new(params)
101
+ end
102
+
103
+ def contact
104
+ @contact ||= build_contact
105
+ end
106
+
107
+ # Helper method to check if the credit_note is accounts payable.
108
+ def accounts_payable?
109
+ type == 'ACCPAYCREDIT'
110
+ end
111
+
112
+ # Helper method to check if the credit_note is accounts receivable.
113
+ def accounts_receivable?
114
+ type == 'ACCRECCREDIT'
115
+ end
116
+
117
+ # Whether or not the line_items have been downloaded (GET/credit_notes does not download line items).
118
+ def line_items_downloaded?
119
+ @line_items_downloaded
120
+ end
121
+
122
+ # If line items are not downloaded, then attempt a download now (if this record was found to begin with).
123
+ def line_items
124
+ if line_items_downloaded?
125
+ @line_items
126
+
127
+ # There is an credit_note_is so we can assume this record was loaded from Xero.
128
+ # attempt to download the line_item records.
129
+ elsif credit_note_id =~ GUID_REGEX
130
+ raise NoGatewayError unless @gateway
131
+
132
+ response = @gateway.get_credit_note(credit_note_id)
133
+ raise CreditNoteNotFoundError, "CreditNote with ID #{credit_note_id} not found in Xero." unless response.success? && response.credit_note.is_a?(XeroGateway::CreditNote)
134
+
135
+ @line_items = response.credit_note.line_items
136
+ @line_items_downloaded = true
137
+
138
+ @line_items
139
+
140
+ # Otherwise, this is a new credit_note, so return the line_items reference.
141
+ else
142
+ @line_items
143
+ end
144
+ end
145
+
146
+ def ==(other)
147
+ ["credit_note_number", "type", "status", "reference", "currency_code", "line_amount_types", "contact", "line_items"].each do |field|
148
+ return false if send(field) != other.send(field)
149
+ end
150
+
151
+ ["date"].each do |field|
152
+ return false if send(field).to_s != other.send(field).to_s
153
+ end
154
+ return true
155
+ end
156
+
157
+ # General purpose createsave method.
158
+ # If contact_id and contact_number are nil then create, otherwise, attempt to save.
159
+ def save
160
+ create
161
+ end
162
+
163
+ # Creates this credit_note record (using gateway.create_credit_note) with the associated gateway.
164
+ # If no gateway set, raise a NoGatewayError exception.
165
+ def create
166
+ raise NoGatewayError unless gateway
167
+ gateway.create_credit_note(self)
168
+ end
169
+
170
+ # Alias create as save as this is currently the only write action.
171
+ alias_method :save, :create
172
+
173
+ def to_xml(b = Builder::XmlMarkup.new)
174
+ b.CreditNote {
175
+ b.Type self.type
176
+ contact.to_xml(b)
177
+ b.Date CreditNote.format_date(self.date || Date.today)
178
+ b.Status self.status if self.status
179
+ b.CreditNoteNumber self.credit_note_number if credit_note_number
180
+ b.Reference self.reference if self.reference
181
+ b.CurrencyCode self.currency_code if self.currency_code
182
+ b.LineAmountTypes self.line_amount_types
183
+ b.LineItems {
184
+ self.line_items.each do |line_item|
185
+ line_item.to_xml(b)
186
+ end
187
+ }
188
+ }
189
+ end
190
+
191
+ #TODO UpdatedDateUTC
192
+ def self.from_xml(credit_note_element, gateway = nil, options = {})
193
+ credit_note = CreditNote.new(options.merge({:gateway => gateway}))
194
+ credit_note_element.children.each do |element|
195
+ case(element.name)
196
+ when "CreditNoteID" then credit_note.credit_note_id = element.text
197
+ when "CreditNoteNumber" then credit_note.credit_note_number = element.text
198
+ when "Type" then credit_note.type = element.text
199
+ when "CurrencyCode" then credit_note.currency_code = element.text
200
+ when "Contact" then credit_note.contact = Contact.from_xml(element)
201
+ when "Date" then credit_note.date = parse_date(element.text)
202
+ when "Status" then credit_note.status = element.text
203
+ when "Reference" then credit_note.reference = element.text
204
+ when "LineAmountTypes" then credit_note.line_amount_types = element.text
205
+ when "LineItems" then element.children.each {|line_item| credit_note.line_items_downloaded = true; credit_note.line_items << LineItem.from_xml(line_item) }
206
+ when "SubTotal" then credit_note.sub_total = BigDecimal.new(element.text)
207
+ when "TotalTax" then credit_note.total_tax = BigDecimal.new(element.text)
208
+ when "Total" then credit_note.total = BigDecimal.new(element.text)
209
+ when "CreditNoteID" then credit_note.credit_note_id = element.text
210
+ when "CreditNoteNumber" then credit_note.credit_note_number = element.text
211
+ when "Payments" then element.children.each { | payment | credit_note.payments << Payment.from_xml(payment) }
212
+ when "AmountDue" then credit_note.amount_due = BigDecimal.new(element.text)
213
+ when "AmountPaid" then credit_note.amount_paid = BigDecimal.new(element.text)
214
+ when "AmountCredited" then credit_note.amount_credited = BigDecimal.new(element.text)
215
+ end
216
+ end
217
+ credit_note
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,56 @@
1
+ module XeroGateway
2
+ class Currency
3
+
4
+ unless defined? ATTRS
5
+ ATTRS = {
6
+ "Code" => :string, # 3 letter alpha code for the currency – see list of currency codes
7
+ "Description" => :string, # Name of Currency
8
+ }
9
+ end
10
+
11
+ attr_accessor *ATTRS.keys.map(&:underscore)
12
+
13
+ def initialize(params = {})
14
+ params.each do |k,v|
15
+ self.send("#{k}=", v)
16
+ end
17
+ end
18
+
19
+ def ==(other)
20
+ ATTRS.keys.map(&:underscore).each do |field|
21
+ return false if send(field) != other.send(field)
22
+ end
23
+ return true
24
+ end
25
+
26
+ def to_xml
27
+ b = Builder::XmlMarkup.new
28
+
29
+ b.Currency do
30
+ ATTRS.keys.each do |attr|
31
+ eval("b.#{attr} '#{self.send(attr.underscore.to_sym)}'")
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.from_xml(currency_element)
37
+ Currency.new.tap do |currency|
38
+ currency_element.children.each do |element|
39
+
40
+ attribute = element.name
41
+ underscored_attribute = element.name.underscore
42
+
43
+ raise "Unknown attribute: #{attribute}" unless ATTRS.keys.include?(attribute)
44
+
45
+ case (ATTRS[attribute])
46
+ when :boolean then currency.send("#{underscored_attribute}=", (element.text == "true"))
47
+ when :float then currency.send("#{underscored_attribute}=", element.text.to_f)
48
+ else currency.send("#{underscored_attribute}=", element.text)
49
+ end
50
+
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+ end