xero_exporter 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/VERSION +1 -0
- data/lib/xero_exporter/api.rb +132 -0
- data/lib/xero_exporter/country.rb +33 -0
- data/lib/xero_exporter/error.rb +6 -0
- data/lib/xero_exporter/executor.rb +426 -0
- data/lib/xero_exporter/export.rb +98 -0
- data/lib/xero_exporter/fee.rb +11 -0
- data/lib/xero_exporter/invoice.rb +43 -0
- data/lib/xero_exporter/invoice_line.rb +11 -0
- data/lib/xero_exporter/logger.rb +17 -0
- data/lib/xero_exporter/payment.rb +11 -0
- data/lib/xero_exporter/proposal.rb +83 -0
- data/lib/xero_exporter/refund.rb +11 -0
- data/lib/xero_exporter/tax_rate.rb +47 -0
- data/lib/xero_exporter/version.rb +12 -0
- data/lib/xero_exporter.rb +9 -0
- data/resource/country-codes.json +252 -0
- metadata +73 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bde627d6af9da6d7c27966b98a072973cfdd6f379e24112dd99a6412fbbf07e5
|
4
|
+
data.tar.gz: 35363a38bd97be4bef05ce1baab513c4b88998ebe8c467a0b5fd6f0d76648abb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: afbea5fce74d9c032f2835cf37cc19e007b37d0192197abf432904512f77bd7bd6233fd1db44deb91fd695495d3ed126f9fb950a43238f36cdde278ad3f4986b
|
7
|
+
data.tar.gz: 33feef318188bb760c235a55684d3684db8d6b919a0e5875563632cd54688b67a0a459f39984186a8e4cf3ae99574a20f534709c65c6408fbf17d688a770e8ed
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
v1.3.0
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module XeroExporter
|
7
|
+
class API
|
8
|
+
|
9
|
+
class APIConnectionError < Error
|
10
|
+
end
|
11
|
+
|
12
|
+
class APIError < Error
|
13
|
+
|
14
|
+
attr_reader :status, :body
|
15
|
+
|
16
|
+
def initialize(status, message, body = nil)
|
17
|
+
@status = status
|
18
|
+
@message = message
|
19
|
+
@body = body
|
20
|
+
end
|
21
|
+
|
22
|
+
def message
|
23
|
+
to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
"[#{@status}] #{@message}"
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
METHOD_MAP = {
|
33
|
+
get: Net::HTTP::Get,
|
34
|
+
post: Net::HTTP::Post,
|
35
|
+
put: Net::HTTP::Put
|
36
|
+
}.freeze
|
37
|
+
|
38
|
+
def initialize(access_token, tenant_id, **options)
|
39
|
+
@access_token = access_token
|
40
|
+
@tenant_id = tenant_id
|
41
|
+
@logger = options[:logger]
|
42
|
+
end
|
43
|
+
|
44
|
+
def get(path, params = {})
|
45
|
+
request(:get, path, params)
|
46
|
+
end
|
47
|
+
|
48
|
+
def post(path, params = {})
|
49
|
+
request(:post, path, params)
|
50
|
+
end
|
51
|
+
|
52
|
+
def put(path, params = {})
|
53
|
+
request(:put, path, params)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def request(method, path, params = {})
|
59
|
+
http = Net::HTTP.new('api.xero.com', 443)
|
60
|
+
http.use_ssl = true
|
61
|
+
|
62
|
+
path = "/api.xro/2.0/#{path}"
|
63
|
+
|
64
|
+
if method == :get && !params.empty?
|
65
|
+
query_string = URI.encode_www_form(params)
|
66
|
+
path = "#{path}?#{query_string}"
|
67
|
+
end
|
68
|
+
|
69
|
+
request = METHOD_MAP[method].new(path)
|
70
|
+
request['Authorization'] = "Bearer #{@access_token}"
|
71
|
+
request['Xero-Tenant-ID'] = @tenant_id
|
72
|
+
request['Accept'] = 'application/json'
|
73
|
+
|
74
|
+
if method != :get && !params.empty?
|
75
|
+
request['Content-Type'] = 'application/json'
|
76
|
+
request.body = params.to_json
|
77
|
+
end
|
78
|
+
|
79
|
+
logger.debug "[#{method.to_s.upcase}] to #{path}"
|
80
|
+
response = make_request_with_error_handling(http, request)
|
81
|
+
|
82
|
+
if response.is_a?(Net::HTTPOK)
|
83
|
+
logger.debug 'Status: 200 OK'
|
84
|
+
JSON.parse(response.body)
|
85
|
+
elsif response.code.to_i == 429
|
86
|
+
logger.debug 'Status: 429 Rate Limit Exceeded'
|
87
|
+
handle_retry(response, method, path, params)
|
88
|
+
else
|
89
|
+
handle_error(response)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def make_request_with_error_handling(http, request)
|
94
|
+
http.request(request)
|
95
|
+
rescue StandardError => e
|
96
|
+
logger.error "#{e.class}: #{e.message}"
|
97
|
+
raise APIConnectionError, "#{e.class}: #{e.message}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def handle_retry(response, method, path, params)
|
101
|
+
problem = response['X-Rate-Limit-Problem']
|
102
|
+
if problem != 'minute'
|
103
|
+
raise APIConnectionError, "Rate limit exceeded (retry not possible) (problem: #{problem})"
|
104
|
+
end
|
105
|
+
|
106
|
+
retry_after = response['Retry-After'].to_i + 2
|
107
|
+
logger.debug "Waiting #{retry_after} seconds"
|
108
|
+
sleep retry_after
|
109
|
+
|
110
|
+
logger.debug 'Retrying after rate limit pause'
|
111
|
+
request(method, path, params)
|
112
|
+
end
|
113
|
+
|
114
|
+
def handle_error(response)
|
115
|
+
logger.error "Status: #{response.code}"
|
116
|
+
logger.debug response.body
|
117
|
+
|
118
|
+
if response['Content-Type'] =~ /application\/json/
|
119
|
+
json = JSON.parse(response.body)
|
120
|
+
logger.debug 'Error was JSON encoded'
|
121
|
+
raise APIError.new(response.code.to_i, "#{json['Type']}: #{json['Message']}", response.body)
|
122
|
+
end
|
123
|
+
|
124
|
+
raise APIError.new(response.code.to_i, response.body)
|
125
|
+
end
|
126
|
+
|
127
|
+
def logger
|
128
|
+
@logger || XeroExporter.logger
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module XeroExporter
|
6
|
+
class Country
|
7
|
+
|
8
|
+
CODES_TO_NAMES = JSON.parse(File.read(File.expand_path('../../resource/country-codes.json', __dir__)))
|
9
|
+
|
10
|
+
attr_reader :code
|
11
|
+
|
12
|
+
def initialize(code)
|
13
|
+
@code = code
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
@code
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
CODES_TO_NAMES[@code.upcase] || @code.upcase
|
22
|
+
end
|
23
|
+
|
24
|
+
def eql?(other)
|
25
|
+
@code == other.code
|
26
|
+
end
|
27
|
+
|
28
|
+
def hash
|
29
|
+
@code.hash
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,426 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'xero_exporter/proposal'
|
5
|
+
require 'bigdecimal'
|
6
|
+
require 'bigdecimal/util'
|
7
|
+
|
8
|
+
module XeroExporter
|
9
|
+
class Executor
|
10
|
+
|
11
|
+
attr_accessor :state_reader
|
12
|
+
attr_accessor :state_writer
|
13
|
+
|
14
|
+
def initialize(export, api, **options)
|
15
|
+
@export = export
|
16
|
+
@api = api
|
17
|
+
@logger = options[:logger]
|
18
|
+
@proposal = Proposal.new(export)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Execute all actions required to perform this export
|
22
|
+
#
|
23
|
+
# @return [void]
|
24
|
+
def execute_all
|
25
|
+
create_invoice
|
26
|
+
create_invoice_payment
|
27
|
+
create_credit_note
|
28
|
+
create_credit_note_payment
|
29
|
+
add_payments
|
30
|
+
add_refunds
|
31
|
+
add_fees
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
# Create an invoice for all invoice lines in the export
|
36
|
+
#
|
37
|
+
# @return [void]
|
38
|
+
def create_invoice
|
39
|
+
run_task :create_invoice do
|
40
|
+
line_items = create_xero_line_items(@proposal.invoice_lines)
|
41
|
+
if line_items.empty?
|
42
|
+
logger.debug 'Not creating an invoice because there are no invoice lines'
|
43
|
+
current_state[:status] = 'not required because no invoice lines'
|
44
|
+
return false
|
45
|
+
end
|
46
|
+
|
47
|
+
logger.debug 'Creating new invoice'
|
48
|
+
|
49
|
+
spec = {
|
50
|
+
'Type' => 'ACCREC',
|
51
|
+
'Contact' => {
|
52
|
+
'ContactID' => find_or_create_xero_contact(@export.invoice_contact_name)
|
53
|
+
},
|
54
|
+
'Date' => @export.date.strftime('%Y-%m-%d'),
|
55
|
+
'DueDate' => @export.date.strftime('%Y-%m-%d'),
|
56
|
+
'InvoiceNumber' => @export.invoice_number,
|
57
|
+
'Reference' => @export.invoice_reference || @export.reference,
|
58
|
+
'CurrencyCode' => @export.currency,
|
59
|
+
'Status' => 'AUTHORISED',
|
60
|
+
'LineItems' => line_items
|
61
|
+
}
|
62
|
+
|
63
|
+
invoice = @api.post('Invoices', spec)['Invoices'].first
|
64
|
+
|
65
|
+
logger.debug "Invoice created with ID #{invoice['InvoiceID']} for #{invoice['AmountDue']}"
|
66
|
+
current_state[:status] = 'invoice created'
|
67
|
+
current_state[:invoice_id] = invoice['InvoiceID']
|
68
|
+
current_state[:amount] = invoice['AmountDue']
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Create a credit note for all credit note lines in the export
|
73
|
+
#
|
74
|
+
# @return [void]
|
75
|
+
def create_credit_note
|
76
|
+
run_task :create_credit_note do
|
77
|
+
line_items = create_xero_line_items(@proposal.credit_note_lines)
|
78
|
+
if line_items.empty?
|
79
|
+
logger.debug 'Not creating a credit note because there are no lines'
|
80
|
+
current_state[:status] = 'not required because no credit note lines'
|
81
|
+
return false
|
82
|
+
end
|
83
|
+
|
84
|
+
logger.debug 'Creating new credit note'
|
85
|
+
spec = {
|
86
|
+
'Type' => 'ACCRECCREDIT',
|
87
|
+
'Contact' => {
|
88
|
+
'ContactID' => find_or_create_xero_contact(@export.invoice_contact_name)
|
89
|
+
},
|
90
|
+
'Date' => @export.date.strftime('%Y-%m-%d'),
|
91
|
+
'Reference' => @export.reference,
|
92
|
+
'CurrencyCode' => @export.currency,
|
93
|
+
'Status' => 'AUTHORISED',
|
94
|
+
'LineItems' => line_items
|
95
|
+
}
|
96
|
+
|
97
|
+
credit_note = @api.post('CreditNotes', spec)['CreditNotes'].first
|
98
|
+
|
99
|
+
logger.debug "Credit note created with ID #{credit_note['CreditNoteID']} for #{credit_note['RemainingCredit']}"
|
100
|
+
current_state[:credit_note_id] = credit_note['CreditNoteID']
|
101
|
+
current_state[:amount] = credit_note['RemainingCredit']
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Create payments for all payments in the export
|
106
|
+
#
|
107
|
+
# @return [void]
|
108
|
+
def add_payments
|
109
|
+
@proposal.payments.each do |bank_account, amount|
|
110
|
+
run_task "add_payments_#{bank_account}" do
|
111
|
+
transfer = add_bank_transfer(@export.receivables_account, bank_account, amount)
|
112
|
+
current_state[:transfer_id] = transfer['BankTransferID']
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Create refunds for all payments in the export
|
118
|
+
#
|
119
|
+
# @return [void]
|
120
|
+
def add_refunds
|
121
|
+
@proposal.refunds.each do |bank_account, amount|
|
122
|
+
run_task "add_refunds_#{bank_account}" do
|
123
|
+
if transfer = add_bank_transfer(bank_account, @export.receivables_account, amount)
|
124
|
+
current_state[:transfer_id] = transfer['BankTransferID']
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Create payments for all payments in the export
|
131
|
+
#
|
132
|
+
# @return [void]
|
133
|
+
def add_fees
|
134
|
+
@proposal.fees.each do |bank_account, amounts_by_category|
|
135
|
+
amounts_by_category.each do |category, amount|
|
136
|
+
category_for_key = category&.downcase&.gsub(' ', '_') || 'none'
|
137
|
+
run_task "add_fees_#{bank_account}_#{category_for_key}" do
|
138
|
+
if transaction = add_fee_transaction(bank_account, amount, category)
|
139
|
+
current_state[:transaction_id] = transaction['BankTransactionID']
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Create a new payment that fully pays the given invoice
|
147
|
+
#
|
148
|
+
# @param invoice [Hash]
|
149
|
+
# @return [void]
|
150
|
+
def create_invoice_payment
|
151
|
+
run_task :create_invoice_payment do
|
152
|
+
if state[:create_invoice].nil?
|
153
|
+
raise Error, 'create_invoice task must be executed before this action'
|
154
|
+
end
|
155
|
+
|
156
|
+
if state[:create_invoice][:amount].nil? || !state[:create_invoice][:amount].positive?
|
157
|
+
logger.debug 'Not adding a payment because the amount is not present or not positive'
|
158
|
+
return
|
159
|
+
end
|
160
|
+
|
161
|
+
logger.debug "Creating payment for invoice #{state[:create_invoice][:invoice_id]} " \
|
162
|
+
"for #{state[:create_invoice][:amount]}"
|
163
|
+
logger.debug "Using receivables account: #{@export.receivables_account}"
|
164
|
+
|
165
|
+
payment = @api.put('Payments', {
|
166
|
+
'Invoice' => { 'InvoiceID' => state[:create_invoice][:invoice_id] },
|
167
|
+
'Account' => { 'Code' => @export.receivables_account },
|
168
|
+
'Date' => @export.date.strftime('%Y-%m-%d'),
|
169
|
+
'Amount' => state[:create_invoice][:amount],
|
170
|
+
'Reference' => @export.reference
|
171
|
+
})['Payments'].first
|
172
|
+
|
173
|
+
current_state[:payment_id] = payment['PaymentID']
|
174
|
+
current_state[:amount] = payment['Amount']
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Create a new payment that fully pays a given credit note
|
179
|
+
#
|
180
|
+
# @param credit_note [Hash]
|
181
|
+
# @return [void]
|
182
|
+
def create_credit_note_payment
|
183
|
+
run_task :create_credit_note_payment do
|
184
|
+
if state[:create_credit_note].nil?
|
185
|
+
raise Error, 'create_credit_note task must be executed before this action'
|
186
|
+
end
|
187
|
+
|
188
|
+
if state[:create_credit_note][:amount].nil? || !state[:create_credit_note][:amount].positive?
|
189
|
+
logger.debug 'Not adding a payment because the amount is not present or not positive'
|
190
|
+
return
|
191
|
+
end
|
192
|
+
|
193
|
+
logger.debug "Creating payment for credit note #{state[:create_credit_note][:credit_note_id]} " \
|
194
|
+
"for #{state[:create_credit_note][:amount]}"
|
195
|
+
logger.debug "Using receivables account: #{@export.receivables_account}"
|
196
|
+
|
197
|
+
payment = @api.put('Payments', {
|
198
|
+
'CreditNote' => { 'CreditNoteID' => state[:create_credit_note][:credit_note_id] },
|
199
|
+
'Account' => { 'Code' => @export.receivables_account },
|
200
|
+
'Date' => @export.date.strftime('%Y-%m-%d'),
|
201
|
+
'Amount' => state[:create_credit_note][:amount],
|
202
|
+
'Reference' => @export.reference
|
203
|
+
})['Payments'].first
|
204
|
+
current_state[:payment_id] = payment['PaymentID']
|
205
|
+
current_state[:amount] = payment['Amount']
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
# Transfer an amount of money from one bank account to another
|
212
|
+
#
|
213
|
+
# @param from [String]
|
214
|
+
# @param to [String]
|
215
|
+
# @param amount [Float]
|
216
|
+
# @return [void]
|
217
|
+
def add_bank_transfer(from, to, amount)
|
218
|
+
amount = amount.round(2)
|
219
|
+
logger.debug "Transferring #{amount} from #{from} to #{to}"
|
220
|
+
@api.put('BankTransfers', {
|
221
|
+
'FromBankAccount' => { 'Code' => from },
|
222
|
+
'ToBankAccount' => { 'Code' => to },
|
223
|
+
'Amount' => amount,
|
224
|
+
'Date' => @export.date.strftime('%Y-%m-%d')
|
225
|
+
})['BankTransfers'].first
|
226
|
+
end
|
227
|
+
|
228
|
+
# Create an array of line item hashes which can be submitted to the Xero
|
229
|
+
# API for the given array of lines
|
230
|
+
#
|
231
|
+
# @param lines [Hash]
|
232
|
+
# @return [Array<Hash>]
|
233
|
+
def create_xero_line_items(lines)
|
234
|
+
lines.map do |(account, country, tax_rate), amounts|
|
235
|
+
xero_tax_rate = find_or_create_tax_rate(country, tax_rate)
|
236
|
+
|
237
|
+
if xero_tax_rate.nil?
|
238
|
+
raise Error, "Could not determine tax rate for #{country} (#{tax_rate})"
|
239
|
+
end
|
240
|
+
|
241
|
+
{
|
242
|
+
'Description' => @proposal.invoice_line_description(account, country, tax_rate),
|
243
|
+
'Quantity' => 1,
|
244
|
+
'AccountCode' => account,
|
245
|
+
'TaxAmount' => amounts[:tax],
|
246
|
+
'LineAmount' => amounts[:amount],
|
247
|
+
'TaxType' => xero_tax_rate,
|
248
|
+
'Tracking' => tracking_options
|
249
|
+
}
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Return an array of tracking options
|
254
|
+
#
|
255
|
+
# @return [Array<Hash>]
|
256
|
+
def tracking_options
|
257
|
+
return [] if @export.tracking.empty?
|
258
|
+
|
259
|
+
@export.tracking.map do |key, value|
|
260
|
+
{
|
261
|
+
'Name' => key,
|
262
|
+
'Option' => value
|
263
|
+
}
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Add a fee transaction for a given amount to a given bank account
|
268
|
+
#
|
269
|
+
# @param bank_account [String]
|
270
|
+
# @param amount [Float]
|
271
|
+
# @return [void]
|
272
|
+
def add_fee_transaction(bank_account, amount, description = nil)
|
273
|
+
return if amount.zero?
|
274
|
+
|
275
|
+
logger.debug "Creating fee transaction for #{amount} from #{bank_account} (#{description})"
|
276
|
+
|
277
|
+
@api.post('BankTransactions', {
|
278
|
+
'Type' => amount.negative? ? 'RECEIVE' : 'SPEND',
|
279
|
+
'Contact' => {
|
280
|
+
'ContactID' => find_or_create_xero_contact(@export.payment_providers[bank_account] ||
|
281
|
+
'Generic Payment Processor')
|
282
|
+
},
|
283
|
+
'Date' => @export.date.strftime('%Y-%m-%d'),
|
284
|
+
'BankAccount' => { 'Code' => bank_account },
|
285
|
+
'Reference' => @export.reference,
|
286
|
+
'LineItems' => [
|
287
|
+
{
|
288
|
+
'Description' => description || 'Fees',
|
289
|
+
'UnitAmount' => amount.abs,
|
290
|
+
'AccountCode' => @export.fee_accounts[bank_account] || '404',
|
291
|
+
'Tracking' => tracking_options
|
292
|
+
}
|
293
|
+
]
|
294
|
+
})['BankTransactions'].first
|
295
|
+
end
|
296
|
+
|
297
|
+
# Find or create a contact with a given name and return the ID of that
|
298
|
+
# contact
|
299
|
+
#
|
300
|
+
# @param name [String]
|
301
|
+
# @return [String]
|
302
|
+
def find_or_create_xero_contact(name)
|
303
|
+
existing = @api.get('Contacts', where: "Name=\"#{name}\"")['Contacts']&.first
|
304
|
+
if existing
|
305
|
+
logger.debug "Found existing contact with name: #{name}"
|
306
|
+
logger.debug "ID: #{existing['ContactID']}"
|
307
|
+
return existing['ContactID']
|
308
|
+
end
|
309
|
+
|
310
|
+
logger.debug "Creating new contact with name: #{name}"
|
311
|
+
response = @api.post('Contacts', 'Name' => name)
|
312
|
+
id = response['Contacts'].first['ContactID']
|
313
|
+
logger.debug "Contact created with ID: #{id}"
|
314
|
+
id
|
315
|
+
end
|
316
|
+
|
317
|
+
# Find or create a tax rate for the given country and tax rate.
|
318
|
+
#
|
319
|
+
# @param country [XeroExporter::Country]
|
320
|
+
# @param tax_rate [XeroExporter::TaxRate]
|
321
|
+
# @return [String]
|
322
|
+
def find_or_create_tax_rate(country, tax_rate)
|
323
|
+
@tax_rate_cache ||= {}
|
324
|
+
if cached_rate = @tax_rate_cache[[country, tax_rate]]
|
325
|
+
return cached_rate
|
326
|
+
end
|
327
|
+
|
328
|
+
existing = tax_rates.find do |rate|
|
329
|
+
rate['Status'] == 'ACTIVE' &&
|
330
|
+
rate['ReportTaxType'] == tax_rate.xero_report_type &&
|
331
|
+
rate['EffectiveRate'].to_d == tax_rate.rate.to_d &&
|
332
|
+
(rate['Name'].include?(country.name) || rate['Name'].include?(country.code.upcase))
|
333
|
+
end
|
334
|
+
|
335
|
+
if existing
|
336
|
+
@tax_rate_cache[[country, tax_rate]] = existing['TaxType']
|
337
|
+
return existing['TaxType']
|
338
|
+
end
|
339
|
+
|
340
|
+
rates = @api.post('TaxRates', {
|
341
|
+
'Name' => tax_rate.xero_name(country),
|
342
|
+
'ReportTaxType' => tax_rate.xero_report_type,
|
343
|
+
'TaxComponents' => [
|
344
|
+
{
|
345
|
+
'Name' => 'Tax',
|
346
|
+
'Rate' => tax_rate.rate
|
347
|
+
}
|
348
|
+
]
|
349
|
+
})
|
350
|
+
|
351
|
+
type = rates['TaxRates'].first['TaxType']
|
352
|
+
@tax_rate_cache[[country, tax_rate]] = type
|
353
|
+
type
|
354
|
+
end
|
355
|
+
|
356
|
+
# Return a full list of all tax rates currently stored within the API
|
357
|
+
#
|
358
|
+
# @return [Array<Hash>]
|
359
|
+
def tax_rates
|
360
|
+
@tax_rates ||= @api.get('TaxRates')['TaxRates'] || []
|
361
|
+
end
|
362
|
+
|
363
|
+
# Return the logger instance
|
364
|
+
#
|
365
|
+
# @return [Logger]
|
366
|
+
def logger
|
367
|
+
@logger || XeroExporter.logger
|
368
|
+
end
|
369
|
+
|
370
|
+
# Executes a named task if it is suitable to be executed
|
371
|
+
#
|
372
|
+
# @return [void]
|
373
|
+
def run_task(name)
|
374
|
+
if @state.nil?
|
375
|
+
@state = load_state
|
376
|
+
end
|
377
|
+
|
378
|
+
if @state[name.to_sym] && @state[name.to_sym][:state] == 'complete'
|
379
|
+
logger.debug "Not executing #{name} because it has already been run"
|
380
|
+
return
|
381
|
+
end
|
382
|
+
|
383
|
+
logger.debug "Running #{name} task"
|
384
|
+
|
385
|
+
@current_state = @state[name.to_sym] ||= {}
|
386
|
+
@current_state[:state] = 'running'
|
387
|
+
@current_state.delete(:error)
|
388
|
+
yield if block_given?
|
389
|
+
rescue StandardError => e
|
390
|
+
if @current_state
|
391
|
+
@current_state[:state] = 'error'
|
392
|
+
@current_state[:error] = { class: e.class.name, message: e.message }
|
393
|
+
end
|
394
|
+
raise
|
395
|
+
ensure
|
396
|
+
if @current_state && @current_state[:state] == 'running'
|
397
|
+
@current_state[:state] = 'complete'
|
398
|
+
end
|
399
|
+
|
400
|
+
@current_state = nil
|
401
|
+
save_state
|
402
|
+
end
|
403
|
+
|
404
|
+
# Loads state as apprppriate
|
405
|
+
#
|
406
|
+
# @return [Hash]
|
407
|
+
def load_state
|
408
|
+
return {} unless @state_reader
|
409
|
+
|
410
|
+
@state_reader.call
|
411
|
+
end
|
412
|
+
|
413
|
+
# Saves the current state as appropriate
|
414
|
+
#
|
415
|
+
# @return [void]
|
416
|
+
def save_state
|
417
|
+
return unless @state_writer
|
418
|
+
|
419
|
+
@state_writer.call(@state)
|
420
|
+
end
|
421
|
+
|
422
|
+
attr_reader :state
|
423
|
+
attr_reader :current_state
|
424
|
+
|
425
|
+
end
|
426
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'xero_exporter/invoice'
|
4
|
+
require 'xero_exporter/payment'
|
5
|
+
require 'xero_exporter/refund'
|
6
|
+
require 'xero_exporter/fee'
|
7
|
+
require 'xero_exporter/executor'
|
8
|
+
|
9
|
+
module XeroExporter
|
10
|
+
class Export
|
11
|
+
|
12
|
+
attr_accessor :id
|
13
|
+
attr_accessor :date
|
14
|
+
attr_accessor :currency
|
15
|
+
attr_accessor :invoice_contact_name
|
16
|
+
attr_accessor :invoice_number
|
17
|
+
attr_accessor :invoice_reference
|
18
|
+
|
19
|
+
attr_accessor :receivables_account
|
20
|
+
attr_accessor :fee_accounts
|
21
|
+
|
22
|
+
attr_reader :invoices
|
23
|
+
attr_reader :payments
|
24
|
+
attr_reader :refunds
|
25
|
+
attr_reader :fees
|
26
|
+
|
27
|
+
attr_reader :payment_providers
|
28
|
+
attr_reader :account_names
|
29
|
+
attr_reader :tracking
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@date = Date.today
|
33
|
+
@currency = 'GBP'
|
34
|
+
@invoice_contact_name = 'Generic Customer'
|
35
|
+
@invoices = []
|
36
|
+
@payments = []
|
37
|
+
@refunds = []
|
38
|
+
@fees = []
|
39
|
+
@payment_providers = {}
|
40
|
+
@fee_accounts = {}
|
41
|
+
@account_names = {}
|
42
|
+
@tracking = {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def reference
|
46
|
+
"#{@date&.strftime('%Y%m%d')}-#{@currency&.upcase}-#{@id}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_invoice
|
50
|
+
invoice = Invoice.new
|
51
|
+
yield invoice if block_given?
|
52
|
+
@invoices << invoice
|
53
|
+
invoice
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_credit_note
|
57
|
+
invoice = Invoice.new
|
58
|
+
invoice.type = :credit_note
|
59
|
+
yield invoice if block_given?
|
60
|
+
@invoices << invoice
|
61
|
+
invoice
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_payment
|
65
|
+
payment = Payment.new
|
66
|
+
yield payment if block_given?
|
67
|
+
@payments << payment
|
68
|
+
payment
|
69
|
+
end
|
70
|
+
|
71
|
+
def add_refund
|
72
|
+
refund = Refund.new
|
73
|
+
yield refund if block_given?
|
74
|
+
@refunds << refund
|
75
|
+
refund
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_fee
|
79
|
+
fee = Fee.new
|
80
|
+
yield fee if block_given?
|
81
|
+
@fees << fee
|
82
|
+
fee
|
83
|
+
end
|
84
|
+
|
85
|
+
def execute(api, **options)
|
86
|
+
executor = Executor.new(self, api, **options)
|
87
|
+
yield executor if block_given?
|
88
|
+
executor.execute_all
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def logger
|
94
|
+
XeroExporter.logger
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'xero_exporter/invoice_line'
|
4
|
+
require 'xero_exporter/tax_rate'
|
5
|
+
require 'xero_exporter/country'
|
6
|
+
|
7
|
+
module XeroExporter
|
8
|
+
class Invoice
|
9
|
+
|
10
|
+
attr_accessor :id
|
11
|
+
attr_accessor :number
|
12
|
+
attr_accessor :type
|
13
|
+
attr_writer :country
|
14
|
+
attr_writer :tax_rate
|
15
|
+
attr_writer :tax_type
|
16
|
+
|
17
|
+
attr_reader :lines
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@type = :invoice
|
21
|
+
@lines = []
|
22
|
+
@tax_type = :normal
|
23
|
+
end
|
24
|
+
|
25
|
+
def tax_rate
|
26
|
+
TaxRate.new(@tax_rate, @tax_type)
|
27
|
+
end
|
28
|
+
|
29
|
+
def country
|
30
|
+
Country.new(@country)
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_line(account_code:, amount:, tax: 0.0)
|
34
|
+
line = InvoiceLine.new
|
35
|
+
line.account_code = account_code
|
36
|
+
line.amount = amount
|
37
|
+
line.tax = tax
|
38
|
+
@lines << line
|
39
|
+
line
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module XeroExporter
|
4
|
+
class Proposal
|
5
|
+
|
6
|
+
def initialize(export)
|
7
|
+
@export = export
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns an array of lines which need to be included on the invoice that will
|
11
|
+
# be generated in Xero.
|
12
|
+
#
|
13
|
+
# @return [Hash]
|
14
|
+
def invoice_lines
|
15
|
+
get_invoice_lines_from_invoices(@export.invoices.select { |i| i.type == :invoice })
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns an array of lines which need to be included on the credit note that will
|
19
|
+
# be generated in Xero.
|
20
|
+
#
|
21
|
+
# @return [Hash]
|
22
|
+
def credit_note_lines
|
23
|
+
get_invoice_lines_from_invoices(@export.invoices.select { |i| i.type == :credit_note })
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the total number of payments per bank account which
|
27
|
+
#
|
28
|
+
# @return [Hash]
|
29
|
+
def payments
|
30
|
+
export_payments = Hash.new(0.0)
|
31
|
+
@export.payments.each do |payment|
|
32
|
+
key = payment.bank_account
|
33
|
+
export_payments[key] += payment.amount || 0.0
|
34
|
+
end
|
35
|
+
export_payments
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return the total number of refund per bank account which
|
39
|
+
#
|
40
|
+
# @return [Hash]
|
41
|
+
def refunds
|
42
|
+
export_refunds = Hash.new(0.0)
|
43
|
+
@export.refunds.each do |refund|
|
44
|
+
key = refund.bank_account
|
45
|
+
export_refunds[key] += refund.amount || 0.0
|
46
|
+
end
|
47
|
+
export_refunds
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return the fees grouped by bank account & category
|
51
|
+
#
|
52
|
+
# @return [Hash]
|
53
|
+
def fees
|
54
|
+
initial_hash = Hash.new { |h, k| h[k] = Hash.new(0.0) }
|
55
|
+
@export.fees.each_with_object(initial_hash) do |fee, hash|
|
56
|
+
hash[fee.bank_account][fee.category] += fee.amount || 0.0
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return the text to go with an invoice line
|
61
|
+
#
|
62
|
+
# @return [String]
|
63
|
+
def invoice_line_description(account, country, tax_rate)
|
64
|
+
name = @export.account_names[account.to_s] || "#{account} Sales"
|
65
|
+
"#{name} (#{country.code}, #{tax_rate.rate}%)"
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def get_invoice_lines_from_invoices(invoices)
|
71
|
+
export_lines = Hash.new { |h, k| h[k] = { amount: 0.0, tax: 0.0 } }
|
72
|
+
invoices.each do |invoice|
|
73
|
+
invoice.lines.each do |line|
|
74
|
+
key = [line.account_code, invoice.country, invoice.tax_rate]
|
75
|
+
export_lines[key][:amount] += line.amount || 0.0
|
76
|
+
export_lines[key][:tax] += line.tax || 0.0
|
77
|
+
end
|
78
|
+
end
|
79
|
+
export_lines
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module XeroExporter
|
4
|
+
class TaxRate
|
5
|
+
|
6
|
+
attr_reader :rate
|
7
|
+
attr_reader :type
|
8
|
+
|
9
|
+
def initialize(rate, type)
|
10
|
+
@rate = rate&.to_f || 0.0
|
11
|
+
@type = type
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
"#{@rate}[#{@type}]"
|
16
|
+
end
|
17
|
+
|
18
|
+
def eql?(other)
|
19
|
+
@rate == other.rate && @type == other.type
|
20
|
+
end
|
21
|
+
|
22
|
+
def hash
|
23
|
+
[@rate, @type].hash
|
24
|
+
end
|
25
|
+
|
26
|
+
def xero_name(country)
|
27
|
+
case @type
|
28
|
+
when :moss
|
29
|
+
"MOSS #{country.name} #{@rate}%"
|
30
|
+
when :reverse_charge
|
31
|
+
"Reverse Charge (#{country.code})"
|
32
|
+
when :ec_services
|
33
|
+
"EC Services for #{country.code} (#{@rate}%)"
|
34
|
+
else
|
35
|
+
"Tax for #{country.code} (#{@rate}%)"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def xero_report_type
|
40
|
+
return 'MOSSSALES' if @type == :moss
|
41
|
+
return 'ECOUTPUTSERVICES' if @type == :ec_services
|
42
|
+
|
43
|
+
'OUTPUT'
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module XeroExporter
|
4
|
+
|
5
|
+
VERSION_FILE_ROOT = File.expand_path('../../VERSION', __dir__)
|
6
|
+
if File.file?(VERSION_FILE_ROOT)
|
7
|
+
VERSION = File.read(VERSION_FILE_ROOT).strip.sub(/\Av/, '')
|
8
|
+
else
|
9
|
+
VERSION = '0.0.0.dev'
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
{
|
2
|
+
"AD": "Andorra",
|
3
|
+
"AE": "United Arab Emirates",
|
4
|
+
"AF": "Afghanistan",
|
5
|
+
"AG": "Antigua and Barbuda",
|
6
|
+
"AI": "Anguilla",
|
7
|
+
"AL": "Albania",
|
8
|
+
"AM": "Armenia",
|
9
|
+
"AO": "Angola",
|
10
|
+
"AQ": "Antarctica",
|
11
|
+
"AR": "Argentina",
|
12
|
+
"AS": "American Samoa",
|
13
|
+
"AT": "Austria",
|
14
|
+
"AU": "Australia",
|
15
|
+
"AW": "Aruba",
|
16
|
+
"AX": "Aland Islands",
|
17
|
+
"AZ": "Azerbaijan",
|
18
|
+
"BA": "Bosnia and Herzegovina",
|
19
|
+
"BB": "Barbados",
|
20
|
+
"BD": "Bangladesh",
|
21
|
+
"BE": "Belgium",
|
22
|
+
"BF": "Burkina Faso",
|
23
|
+
"BG": "Bulgaria",
|
24
|
+
"BH": "Bahrain",
|
25
|
+
"BI": "Burundi",
|
26
|
+
"BJ": "Benin",
|
27
|
+
"BL": "Saint Barthelemy",
|
28
|
+
"BM": "Bermuda",
|
29
|
+
"BN": "Brunei",
|
30
|
+
"BO": "Bolivia",
|
31
|
+
"BQ": "Bonaire, Saint Eustatius and Saba ",
|
32
|
+
"BR": "Brazil",
|
33
|
+
"BS": "Bahamas",
|
34
|
+
"BT": "Bhutan",
|
35
|
+
"BV": "Bouvet Island",
|
36
|
+
"BW": "Botswana",
|
37
|
+
"BY": "Belarus",
|
38
|
+
"BZ": "Belize",
|
39
|
+
"CA": "Canada",
|
40
|
+
"CC": "Cocos Islands",
|
41
|
+
"CD": "Democratic Republic of the Congo",
|
42
|
+
"CF": "Central African Republic",
|
43
|
+
"CG": "Republic of the Congo",
|
44
|
+
"CH": "Switzerland",
|
45
|
+
"CI": "Ivory Coast",
|
46
|
+
"CK": "Cook Islands",
|
47
|
+
"CL": "Chile",
|
48
|
+
"CM": "Cameroon",
|
49
|
+
"CN": "China",
|
50
|
+
"CO": "Colombia",
|
51
|
+
"CR": "Costa Rica",
|
52
|
+
"CU": "Cuba",
|
53
|
+
"CV": "Cape Verde",
|
54
|
+
"CW": "Curacao",
|
55
|
+
"CX": "Christmas Island",
|
56
|
+
"CY": "Cyprus",
|
57
|
+
"CZ": "Czech Republic",
|
58
|
+
"DE": "Germany",
|
59
|
+
"DJ": "Djibouti",
|
60
|
+
"DK": "Denmark",
|
61
|
+
"DM": "Dominica",
|
62
|
+
"DO": "Dominican Republic",
|
63
|
+
"DZ": "Algeria",
|
64
|
+
"EC": "Ecuador",
|
65
|
+
"EE": "Estonia",
|
66
|
+
"EG": "Egypt",
|
67
|
+
"EH": "Western Sahara",
|
68
|
+
"ER": "Eritrea",
|
69
|
+
"ES": "Spain",
|
70
|
+
"ET": "Ethiopia",
|
71
|
+
"FI": "Finland",
|
72
|
+
"FJ": "Fiji",
|
73
|
+
"FK": "Falkland Islands",
|
74
|
+
"FM": "Micronesia",
|
75
|
+
"FO": "Faroe Islands",
|
76
|
+
"FR": "France",
|
77
|
+
"GA": "Gabon",
|
78
|
+
"GB": "United Kingdom",
|
79
|
+
"GD": "Grenada",
|
80
|
+
"GE": "Georgia",
|
81
|
+
"GF": "French Guiana",
|
82
|
+
"GG": "Guernsey",
|
83
|
+
"GH": "Ghana",
|
84
|
+
"GI": "Gibraltar",
|
85
|
+
"GL": "Greenland",
|
86
|
+
"GM": "Gambia",
|
87
|
+
"GN": "Guinea",
|
88
|
+
"GP": "Guadeloupe",
|
89
|
+
"GQ": "Equatorial Guinea",
|
90
|
+
"GR": "Greece",
|
91
|
+
"GS": "South Georgia and the South Sandwich Islands",
|
92
|
+
"GT": "Guatemala",
|
93
|
+
"GU": "Guam",
|
94
|
+
"GW": "Guinea-Bissau",
|
95
|
+
"GY": "Guyana",
|
96
|
+
"HK": "Hong Kong",
|
97
|
+
"HM": "Heard Island and McDonald Islands",
|
98
|
+
"HN": "Honduras",
|
99
|
+
"HR": "Croatia",
|
100
|
+
"HT": "Haiti",
|
101
|
+
"HU": "Hungary",
|
102
|
+
"ID": "Indonesia",
|
103
|
+
"IE": "Ireland",
|
104
|
+
"IL": "Israel",
|
105
|
+
"IM": "Isle of Man",
|
106
|
+
"IN": "India",
|
107
|
+
"IO": "British Indian Ocean Territory",
|
108
|
+
"IQ": "Iraq",
|
109
|
+
"IR": "Iran",
|
110
|
+
"IS": "Iceland",
|
111
|
+
"IT": "Italy",
|
112
|
+
"JE": "Jersey",
|
113
|
+
"JM": "Jamaica",
|
114
|
+
"JO": "Jordan",
|
115
|
+
"JP": "Japan",
|
116
|
+
"KE": "Kenya",
|
117
|
+
"KG": "Kyrgyzstan",
|
118
|
+
"KH": "Cambodia",
|
119
|
+
"KI": "Kiribati",
|
120
|
+
"KM": "Comoros",
|
121
|
+
"KN": "Saint Kitts and Nevis",
|
122
|
+
"KP": "North Korea",
|
123
|
+
"KR": "South Korea",
|
124
|
+
"KW": "Kuwait",
|
125
|
+
"KY": "Cayman Islands",
|
126
|
+
"KZ": "Kazakhstan",
|
127
|
+
"LA": "Laos",
|
128
|
+
"LB": "Lebanon",
|
129
|
+
"LC": "Saint Lucia",
|
130
|
+
"LI": "Liechtenstein",
|
131
|
+
"LK": "Sri Lanka",
|
132
|
+
"LR": "Liberia",
|
133
|
+
"LS": "Lesotho",
|
134
|
+
"LT": "Lithuania",
|
135
|
+
"LU": "Luxembourg",
|
136
|
+
"LV": "Latvia",
|
137
|
+
"LY": "Libya",
|
138
|
+
"MA": "Morocco",
|
139
|
+
"MC": "Monaco",
|
140
|
+
"MD": "Moldova",
|
141
|
+
"ME": "Montenegro",
|
142
|
+
"MF": "Saint Martin",
|
143
|
+
"MG": "Madagascar",
|
144
|
+
"MH": "Marshall Islands",
|
145
|
+
"MK": "Macedonia",
|
146
|
+
"ML": "Mali",
|
147
|
+
"MM": "Myanmar",
|
148
|
+
"MN": "Mongolia",
|
149
|
+
"MO": "Macao",
|
150
|
+
"MP": "Northern Mariana Islands",
|
151
|
+
"MQ": "Martinique",
|
152
|
+
"MR": "Mauritania",
|
153
|
+
"MS": "Montserrat",
|
154
|
+
"MT": "Malta",
|
155
|
+
"MU": "Mauritius",
|
156
|
+
"MV": "Maldives",
|
157
|
+
"MW": "Malawi",
|
158
|
+
"MX": "Mexico",
|
159
|
+
"MY": "Malaysia",
|
160
|
+
"MZ": "Mozambique",
|
161
|
+
"NA": "Namibia",
|
162
|
+
"NC": "New Caledonia",
|
163
|
+
"NE": "Niger",
|
164
|
+
"NF": "Norfolk Island",
|
165
|
+
"NG": "Nigeria",
|
166
|
+
"NI": "Nicaragua",
|
167
|
+
"NL": "Netherlands",
|
168
|
+
"NO": "Norway",
|
169
|
+
"NP": "Nepal",
|
170
|
+
"NR": "Nauru",
|
171
|
+
"NU": "Niue",
|
172
|
+
"NZ": "New Zealand",
|
173
|
+
"OM": "Oman",
|
174
|
+
"PA": "Panama",
|
175
|
+
"PE": "Peru",
|
176
|
+
"PF": "French Polynesia",
|
177
|
+
"PG": "Papua New Guinea",
|
178
|
+
"PH": "Philippines",
|
179
|
+
"PK": "Pakistan",
|
180
|
+
"PL": "Poland",
|
181
|
+
"PM": "Saint Pierre and Miquelon",
|
182
|
+
"PN": "Pitcairn",
|
183
|
+
"PR": "Puerto Rico",
|
184
|
+
"PS": "Palestinian Territory",
|
185
|
+
"PT": "Portugal",
|
186
|
+
"PW": "Palau",
|
187
|
+
"PY": "Paraguay",
|
188
|
+
"QA": "Qatar",
|
189
|
+
"RE": "Reunion",
|
190
|
+
"RO": "Romania",
|
191
|
+
"RS": "Serbia",
|
192
|
+
"RU": "Russia",
|
193
|
+
"RW": "Rwanda",
|
194
|
+
"SA": "Saudi Arabia",
|
195
|
+
"SB": "Solomon Islands",
|
196
|
+
"SC": "Seychelles",
|
197
|
+
"SD": "Sudan",
|
198
|
+
"SE": "Sweden",
|
199
|
+
"SG": "Singapore",
|
200
|
+
"SH": "Saint Helena",
|
201
|
+
"SI": "Slovenia",
|
202
|
+
"SJ": "Svalbard and Jan Mayen",
|
203
|
+
"SK": "Slovakia",
|
204
|
+
"SL": "Sierra Leone",
|
205
|
+
"SM": "San Marino",
|
206
|
+
"SN": "Senegal",
|
207
|
+
"SO": "Somalia",
|
208
|
+
"SR": "Suriname",
|
209
|
+
"SS": "South Sudan",
|
210
|
+
"ST": "Sao Tome and Principe",
|
211
|
+
"SV": "El Salvador",
|
212
|
+
"SX": "Sint Maarten",
|
213
|
+
"SY": "Syria",
|
214
|
+
"SZ": "Swaziland",
|
215
|
+
"TC": "Turks and Caicos Islands",
|
216
|
+
"TD": "Chad",
|
217
|
+
"TF": "French Southern Territories",
|
218
|
+
"TG": "Togo",
|
219
|
+
"TH": "Thailand",
|
220
|
+
"TJ": "Tajikistan",
|
221
|
+
"TK": "Tokelau",
|
222
|
+
"TL": "East Timor",
|
223
|
+
"TM": "Turkmenistan",
|
224
|
+
"TN": "Tunisia",
|
225
|
+
"TO": "Tonga",
|
226
|
+
"TR": "Turkey",
|
227
|
+
"TT": "Trinidad and Tobago",
|
228
|
+
"TV": "Tuvalu",
|
229
|
+
"TW": "Taiwan",
|
230
|
+
"TZ": "Tanzania",
|
231
|
+
"UA": "Ukraine",
|
232
|
+
"UG": "Uganda",
|
233
|
+
"UM": "United States Minor Outlying Islands",
|
234
|
+
"US": "United States",
|
235
|
+
"UY": "Uruguay",
|
236
|
+
"UZ": "Uzbekistan",
|
237
|
+
"VA": "Vatican",
|
238
|
+
"VC": "Saint Vincent and the Grenadines",
|
239
|
+
"VE": "Venezuela",
|
240
|
+
"VG": "British Virgin Islands",
|
241
|
+
"VI": "U.S. Virgin Islands",
|
242
|
+
"VN": "Vietnam",
|
243
|
+
"VU": "Vanuatu",
|
244
|
+
"WF": "Wallis and Futuna",
|
245
|
+
"WS": "Samoa",
|
246
|
+
"XK": "Kosovo",
|
247
|
+
"YE": "Yemen",
|
248
|
+
"YT": "Mayotte",
|
249
|
+
"ZA": "South Africa",
|
250
|
+
"ZM": "Zambia",
|
251
|
+
"ZW": "Zimbabwe"
|
252
|
+
}
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xero_exporter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Cooke
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-01-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: A library for exporting financial data to the Xero API.
|
28
|
+
email:
|
29
|
+
- adam@krystal.uk
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- VERSION
|
35
|
+
- lib/xero_exporter.rb
|
36
|
+
- lib/xero_exporter/api.rb
|
37
|
+
- lib/xero_exporter/country.rb
|
38
|
+
- lib/xero_exporter/error.rb
|
39
|
+
- lib/xero_exporter/executor.rb
|
40
|
+
- lib/xero_exporter/export.rb
|
41
|
+
- lib/xero_exporter/fee.rb
|
42
|
+
- lib/xero_exporter/invoice.rb
|
43
|
+
- lib/xero_exporter/invoice_line.rb
|
44
|
+
- lib/xero_exporter/logger.rb
|
45
|
+
- lib/xero_exporter/payment.rb
|
46
|
+
- lib/xero_exporter/proposal.rb
|
47
|
+
- lib/xero_exporter/refund.rb
|
48
|
+
- lib/xero_exporter/tax_rate.rb
|
49
|
+
- lib/xero_exporter/version.rb
|
50
|
+
- resource/country-codes.json
|
51
|
+
homepage: https://github.com/krystal/xero_exporter
|
52
|
+
licenses: []
|
53
|
+
metadata: {}
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '2.5'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubygems_version: 3.2.21
|
70
|
+
signing_key:
|
71
|
+
specification_version: 4
|
72
|
+
summary: A library for exporting financial data to the Xero API.
|
73
|
+
test_files: []
|