xero_exporter 1.3.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 +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: []
|