crunchaccounting-api 1.0.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/lib/crunchaccounting-api.rb +483 -0
- metadata +58 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 49a5d2789710062012d399193d9bceccbeb37585
|
4
|
+
data.tar.gz: ef876975580ed903538622672712f8ae9235fe43
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: df041d42161e0bb8d2454b6b94f4acefe8e0ecdaa9b3be8d76c4fbbb2ca668c56c47bd312013c2afd79fee898eecfbca923a050a1db843daa9a9d10d1879e917
|
7
|
+
data.tar.gz: d736fada0e4933383349cd0110e63edd8d01d057fc5a807d728d0f80c71736a1917bc4bfb57d86fc646e50278c787f88c065ef499b8c2c28bfc1a9041b31f186
|
@@ -0,0 +1,483 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'oauth'
|
4
|
+
require 'json'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
VAT_RATE_DEFAULT = 20
|
8
|
+
|
9
|
+
class CrunchAPI
|
10
|
+
attr_reader :oauth_token
|
11
|
+
attr_reader :oauth_token_secret
|
12
|
+
|
13
|
+
def initialize(params={})
|
14
|
+
@vat_rate = VAT_RATE_DEFAULT
|
15
|
+
|
16
|
+
params.each do |key, value|
|
17
|
+
instance_variable_set("@" + key.to_s, value)
|
18
|
+
end
|
19
|
+
|
20
|
+
if @oauth_token and @oauth_token_secret
|
21
|
+
initialise_consumer
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialise_consumer
|
26
|
+
@consumer = get_consumer(@api_endpoint)
|
27
|
+
|
28
|
+
@access_token = OAuth::AccessToken.from_hash(
|
29
|
+
@consumer, {
|
30
|
+
oauth_token: @oauth_token,
|
31
|
+
oauth_token_secret: @oauth_token_secret
|
32
|
+
}
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def authenticated?
|
37
|
+
@access_token ? true : false
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_consumer(endpoint)
|
41
|
+
consumer = OAuth::Consumer.new(
|
42
|
+
@consumer_key,
|
43
|
+
@consumer_secret,
|
44
|
+
:scheme => :header,
|
45
|
+
:site => endpoint,
|
46
|
+
:request_token_path => "/crunch-core/oauth/request_token",
|
47
|
+
:authorize_path => "/crunch-core/login/oauth-login.seam",
|
48
|
+
:access_token_path => "/crunch-core/oauth/access_token"
|
49
|
+
)
|
50
|
+
|
51
|
+
@debug and consumer.http.set_debug_output($stdout)
|
52
|
+
|
53
|
+
consumer
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_auth_url
|
57
|
+
@consumer = get_consumer(@auth_endpoint)
|
58
|
+
|
59
|
+
request_token = @consumer.get_request_token(:oauth_callback => "")
|
60
|
+
|
61
|
+
@request_token = request_token.token
|
62
|
+
@request_secret = request_token.secret
|
63
|
+
|
64
|
+
return request_token.authorize_url(:oauth_callback => "")
|
65
|
+
end
|
66
|
+
|
67
|
+
def verify_token(oauth_verifier)
|
68
|
+
hash = { oauth_token: @request_token, oauth_token_secret: @request_secret }
|
69
|
+
|
70
|
+
request_token = OAuth::RequestToken.from_hash(@consumer, hash)
|
71
|
+
|
72
|
+
@access_token = request_token.get_access_token(:oauth_verifier => oauth_verifier)
|
73
|
+
@oauth_token = @access_token.params[:oauth_token]
|
74
|
+
@oauth_token_secret = @access_token.params[:oauth_token_secret]
|
75
|
+
|
76
|
+
initialise_consumer
|
77
|
+
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
def get(uri)
|
82
|
+
!@consumer and @consumer = get_consumer(@api_endpoint)
|
83
|
+
|
84
|
+
resp = @access_token.get(uri, { "Accept" => "application/json" } )
|
85
|
+
|
86
|
+
JSON.parse(resp.body)
|
87
|
+
end
|
88
|
+
|
89
|
+
def post(uri, params={})
|
90
|
+
!@consumer and @consumer = get_consumer(@api_endpoint)
|
91
|
+
|
92
|
+
resp = @access_token.post(uri, params.to_json, { "Content-Type" => "application/json", "Accept" => "application/json" } )
|
93
|
+
|
94
|
+
JSON.parse(resp.body)
|
95
|
+
end
|
96
|
+
|
97
|
+
def put(uri, params={})
|
98
|
+
!@consumer and @consumer = get_consumer(@api_endpoint)
|
99
|
+
|
100
|
+
resp = @access_token.put(uri, params.to_json, { "Content-Type" => "application/json", "Accept" => "application/json" } )
|
101
|
+
end
|
102
|
+
|
103
|
+
def delete(uri)
|
104
|
+
!@consumer and @consumer = get_consumer(@api_endpoint)
|
105
|
+
|
106
|
+
resp = @access_token.delete(uri, { "Accept" => "application/json" } )
|
107
|
+
|
108
|
+
JSON.parse(resp.body)
|
109
|
+
end
|
110
|
+
|
111
|
+
def accounts(type: nil)
|
112
|
+
if type
|
113
|
+
return get("/rest/v2/accounts/#{type}")
|
114
|
+
else
|
115
|
+
@accounts = {}
|
116
|
+
|
117
|
+
resp = get("/rest/v2/accounts")
|
118
|
+
|
119
|
+
resp["bankAccounts"].each do |account|
|
120
|
+
if !@accounts[account["account"]]
|
121
|
+
@accounts[account["account"]] = account
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
return resp
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def account_by_name(name)
|
130
|
+
if @accounts
|
131
|
+
return @accounts[name]
|
132
|
+
end
|
133
|
+
|
134
|
+
accounts
|
135
|
+
account_by_name name
|
136
|
+
end
|
137
|
+
|
138
|
+
def client_payments
|
139
|
+
get "/rest/v2/client_payments"
|
140
|
+
end
|
141
|
+
|
142
|
+
def add_client_payment(params={})
|
143
|
+
post "/rest/v2/client_payments", params
|
144
|
+
end
|
145
|
+
|
146
|
+
def expense_types
|
147
|
+
get "/rest/v2/expense_types"
|
148
|
+
end
|
149
|
+
|
150
|
+
def expenses
|
151
|
+
resp = get "/rest/v2/expenses"
|
152
|
+
|
153
|
+
@expenses = []
|
154
|
+
|
155
|
+
resp["expense"].each do |expense|
|
156
|
+
@expenses.push expense
|
157
|
+
end
|
158
|
+
|
159
|
+
@expenses
|
160
|
+
end
|
161
|
+
|
162
|
+
def suppliers
|
163
|
+
resp = get "/rest/v2/suppliers"
|
164
|
+
|
165
|
+
@suppliers = {}
|
166
|
+
|
167
|
+
resp["supplier"].each do |supplier|
|
168
|
+
if !@suppliers[supplier["name"]]
|
169
|
+
@suppliers[supplier["name"]] = supplier
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
@suppliers
|
174
|
+
end
|
175
|
+
|
176
|
+
def supplier_by_name(name)
|
177
|
+
if @suppliers
|
178
|
+
return @suppliers[name]
|
179
|
+
end
|
180
|
+
|
181
|
+
suppliers
|
182
|
+
|
183
|
+
supplier_by_name(name)
|
184
|
+
end
|
185
|
+
|
186
|
+
def delete_supplier(supplier_id)
|
187
|
+
delete "/rest/v2/suppliers/#{supplier_id}"
|
188
|
+
end
|
189
|
+
|
190
|
+
def subject_to_vat?(expense_type)
|
191
|
+
if [
|
192
|
+
"GENERAL_INSURANCE",
|
193
|
+
"MILEAGE_ALLOWANCE"
|
194
|
+
].include? expense_type
|
195
|
+
return false
|
196
|
+
end
|
197
|
+
|
198
|
+
if [
|
199
|
+
"ACCOUNTANCY"
|
200
|
+
].include? expense_type
|
201
|
+
return true
|
202
|
+
end
|
203
|
+
|
204
|
+
raise "Don't know if #{expense_type} is subject to VAT"
|
205
|
+
end
|
206
|
+
|
207
|
+
def add_expense(supplier_id:, date:, payment_date:, payment_method: nil, bank_account_id:, amount:, expense_type:, description:, director_id:nil, invoice:nil)
|
208
|
+
!payment_method and payment_method = "EFT"
|
209
|
+
|
210
|
+
if subject_to_vat?(expense_type)
|
211
|
+
gross_amount = amount
|
212
|
+
vat_amount = (amount / 100) * @vat_rate
|
213
|
+
net_amount = amount - vat_amount
|
214
|
+
else
|
215
|
+
gross_amount = amount
|
216
|
+
vat_amount = 0
|
217
|
+
net_amount = amount
|
218
|
+
end
|
219
|
+
|
220
|
+
expense = {
|
221
|
+
"amount" => amount,
|
222
|
+
"expenseDetails" => {
|
223
|
+
"supplier" => {
|
224
|
+
"supplierId" => supplier_id
|
225
|
+
},
|
226
|
+
"postingDate" => date
|
227
|
+
},
|
228
|
+
"paymentDetails" => {
|
229
|
+
"payment" => [{
|
230
|
+
"paymentDate" => payment_date,
|
231
|
+
"paymentMethod" => payment_method,
|
232
|
+
"bankAccount" => {
|
233
|
+
"accountId" => bank_account_id
|
234
|
+
},
|
235
|
+
"amount" => amount
|
236
|
+
}]
|
237
|
+
},
|
238
|
+
"expenseLineItems" => {
|
239
|
+
"count": 1,
|
240
|
+
"lineItemGrossTotal": gross_amount,
|
241
|
+
"expenseLineItems" => [
|
242
|
+
{
|
243
|
+
"expenseType" => expense_type,
|
244
|
+
"benefitingDirector": director_id,
|
245
|
+
"lineItemDescription" => description,
|
246
|
+
"lineItemAmount" => {
|
247
|
+
"currencyCode": "GBP",
|
248
|
+
"netAmount" => net_amount,
|
249
|
+
"grossAmount" => gross_amount,
|
250
|
+
"vatAmount" => vat_amount
|
251
|
+
}
|
252
|
+
}
|
253
|
+
]
|
254
|
+
}
|
255
|
+
}
|
256
|
+
|
257
|
+
if invoice
|
258
|
+
mimetype = file_mimetype(invoice)
|
259
|
+
filename = invoice.split("/").last
|
260
|
+
|
261
|
+
expense["receipts"] = {
|
262
|
+
"count" => 1,
|
263
|
+
"receipt" => [
|
264
|
+
{
|
265
|
+
"fileName" => filename,
|
266
|
+
"contentType" => mimetype,
|
267
|
+
"fileData": Base64.encode64(File.read(invoice))
|
268
|
+
}
|
269
|
+
]
|
270
|
+
}
|
271
|
+
end
|
272
|
+
|
273
|
+
post "/rest/v2/expenses", expense
|
274
|
+
end
|
275
|
+
|
276
|
+
def file_mimetype(filename)
|
277
|
+
esc = Shellwords.escape(filename)
|
278
|
+
`/usr/bin/file -bi #{esc}`.chomp.gsub(/;.*\z/, '')
|
279
|
+
end
|
280
|
+
|
281
|
+
def find_expense(supplier_id:, date:, payment_date:, payment_method: nil, bank_account_id:, amount:, expense_type:, ignore_ids:[])
|
282
|
+
!payment_method and payment_method = "EFT"
|
283
|
+
|
284
|
+
if !@expenses
|
285
|
+
expenses
|
286
|
+
end
|
287
|
+
|
288
|
+
@expenses.each do |expense|
|
289
|
+
if ignore_ids.include?(expense["expenseId"])
|
290
|
+
next
|
291
|
+
end
|
292
|
+
|
293
|
+
if expense["expenseDetails"]["supplier"]["supplierId"] == supplier_id and
|
294
|
+
expense["expenseDetails"]["postingDate"] == date and
|
295
|
+
expense["paymentDetails"]["payment"][0]["paymentDate"] == payment_date and
|
296
|
+
expense["paymentDetails"]["payment"][0]["paymentMethod"] == payment_method and
|
297
|
+
expense["paymentDetails"]["payment"][0]["bankAccount"]["accountId"] == bank_account_id and
|
298
|
+
expense["paymentDetails"]["payment"][0]["amount"] == amount and
|
299
|
+
expense["expenseLineItems"]["expenseLineItems"][0]["expenseType"] == expense_type
|
300
|
+
|
301
|
+
return expense
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
false
|
306
|
+
end
|
307
|
+
|
308
|
+
def clients
|
309
|
+
get "/rest/v2/clients"
|
310
|
+
end
|
311
|
+
|
312
|
+
def find_client(name)
|
313
|
+
clients["client"].each do |client|
|
314
|
+
if client["name"] == name
|
315
|
+
return client
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
false
|
320
|
+
end
|
321
|
+
|
322
|
+
def invoices
|
323
|
+
get "/rest/v2/sales_invoices"
|
324
|
+
end
|
325
|
+
|
326
|
+
def find_outstanding_client_invoice(client_id, amount)
|
327
|
+
invoices["salesInvoice"].each do |invoice|
|
328
|
+
total = 0
|
329
|
+
|
330
|
+
invoice["salesInvoiceLineItems"]["salesInvoiceLineItem"].each do |item|
|
331
|
+
total += item["lineItemAmount"]["grossAmount"]
|
332
|
+
end
|
333
|
+
|
334
|
+
if invoice["salesInvoiceDetails"]["client"]["clientId"] == client_id and
|
335
|
+
total == amount and
|
336
|
+
invoice["salesInvoiceDetails"]["state"] != "SETTLED"
|
337
|
+
|
338
|
+
return invoice
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
false
|
343
|
+
end
|
344
|
+
|
345
|
+
def find_draft_invoice(client_id, date)
|
346
|
+
invoices["salesInvoice"].each do |invoice|
|
347
|
+
if invoice["salesInvoiceDetails"]["client"]["clientId"] == client_id and
|
348
|
+
invoice["salesInvoiceDetails"]["issuedDate"] == date and
|
349
|
+
invoice["salesInvoiceDetails"]["state"] == "DRAFT"
|
350
|
+
|
351
|
+
return invoice
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
false
|
356
|
+
end
|
357
|
+
|
358
|
+
def issue_invoice(invoice)
|
359
|
+
put "/rest/v2/sales_invoices/#{invoice["salesInvoiceId"]}/issue"
|
360
|
+
end
|
361
|
+
|
362
|
+
def find_client_payment(client_id:, date:, payment_method: nil, bank_account_id:, amount:, invoice_id:)
|
363
|
+
!payment_method and payment_method = "EFT"
|
364
|
+
|
365
|
+
client_payments.each do |client_payment|
|
366
|
+
if client_payment["paymentDate"] == date and
|
367
|
+
client_payment["paymentMethod"] == payment_method and
|
368
|
+
client_payment["bankAccount"]["accountId"] == bank_account_id and
|
369
|
+
client_payment["amount"] == amount and
|
370
|
+
client_payment["client"]["clientId"] == client_id and
|
371
|
+
client_payment["salesInvoices"]["salesInvoice"][0]["salesInvoiceId"] == invoice_id
|
372
|
+
|
373
|
+
return client_payment
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
false
|
378
|
+
end
|
379
|
+
|
380
|
+
def add_client_payment(client_id:, date:, payment_method: nil, bank_account_id:, amount:, invoice_id:)
|
381
|
+
!payment_method and payment_method = "EFT"
|
382
|
+
|
383
|
+
payment = {
|
384
|
+
"paymentDate" => date,
|
385
|
+
"paymentMethod" => payment_method,
|
386
|
+
"bankAccount" => {
|
387
|
+
"accountId" => bank_account_id,
|
388
|
+
},
|
389
|
+
"amount" => amount,
|
390
|
+
"currency" => "GBP",
|
391
|
+
"client" => {
|
392
|
+
"clientId" => client_id,
|
393
|
+
},
|
394
|
+
"salesInvoices" => {
|
395
|
+
"salesInvoice" => [
|
396
|
+
{
|
397
|
+
"salesInvoiceId" => invoice_id,
|
398
|
+
"allocatedAmount" => amount,
|
399
|
+
}
|
400
|
+
]
|
401
|
+
}
|
402
|
+
}
|
403
|
+
|
404
|
+
post "/rest/v2/client_payments", payment
|
405
|
+
end
|
406
|
+
|
407
|
+
def get_next_client_ref_for_client(client)
|
408
|
+
abbr = ""
|
409
|
+
|
410
|
+
for i in 0...client.length
|
411
|
+
if client["name"][i].match(/[A-Z]/)
|
412
|
+
abbr += client["name"][i]
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
used = []
|
417
|
+
|
418
|
+
invoices["salesInvoice"].each do |invoice|
|
419
|
+
if invoice["salesInvoiceDetails"]["client"]["clientId"] == client["clientId"]
|
420
|
+
used.push invoice["salesInvoiceDetails"]["clientReference"]
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
n = 1
|
425
|
+
ref = "#{abbr}#{n.to_s.rjust(3,'0')}"
|
426
|
+
|
427
|
+
while used.include? ref
|
428
|
+
n += 1
|
429
|
+
ref = "#{abbr}#{n.to_s.rjust(3,'0')}"
|
430
|
+
end
|
431
|
+
|
432
|
+
ref
|
433
|
+
end
|
434
|
+
|
435
|
+
def raise_invoice(client_id:,date:,client_ref:nil,description:,rate:,quantity:,add_vat:true)
|
436
|
+
amount = rate * quantity
|
437
|
+
|
438
|
+
if add_vat
|
439
|
+
vat = (amount / 100) * @vat_rate
|
440
|
+
vat_type = "STANDARD"
|
441
|
+
else
|
442
|
+
vat = 0
|
443
|
+
vat_type = "OUTSIDE_SCOPE"
|
444
|
+
end
|
445
|
+
|
446
|
+
client = get "/rest/v2/clients/#{client_id}"
|
447
|
+
|
448
|
+
if !client_ref
|
449
|
+
client_ref = get_next_client_ref_for_client(client)
|
450
|
+
end
|
451
|
+
|
452
|
+
invoice = {
|
453
|
+
"currency" => "GBP",
|
454
|
+
"salesInvoiceLineItems" => {
|
455
|
+
"salesInvoiceLineItem" => [
|
456
|
+
{
|
457
|
+
"lineItemDescription" => description,
|
458
|
+
"quantity" => quantity,
|
459
|
+
"rate" => rate,
|
460
|
+
"lineItemAmount" => {
|
461
|
+
"netAmount" => amount,
|
462
|
+
"grossAmount" => amount + vat,
|
463
|
+
"vatAmount" => vat,
|
464
|
+
"vatRate" => @vat_rate
|
465
|
+
},
|
466
|
+
"vatType" => vat_type
|
467
|
+
},
|
468
|
+
],
|
469
|
+
"count" => 1
|
470
|
+
},
|
471
|
+
"salesInvoiceDetails" => {
|
472
|
+
"client" => {
|
473
|
+
"clientId" => client_id,
|
474
|
+
},
|
475
|
+
"clientReference" => client_ref,
|
476
|
+
"issuedDate" => date,
|
477
|
+
"paymentTermsDays" => client["paymentTermsDays"],
|
478
|
+
}
|
479
|
+
}
|
480
|
+
|
481
|
+
post "/rest/v2/sales_invoices", invoice
|
482
|
+
end
|
483
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: crunchaccounting-api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- m4rkw
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-07-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: oauth
|
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: Crunch Accounting API gem
|
28
|
+
email: m@rkw.io
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- lib/crunchaccounting-api.rb
|
34
|
+
homepage: https://github.com/m4rkw/crunchaccounting-api
|
35
|
+
licenses:
|
36
|
+
- MIT
|
37
|
+
metadata: {}
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 2.5.2
|
55
|
+
signing_key:
|
56
|
+
specification_version: 4
|
57
|
+
summary: Crunch Accounting API
|
58
|
+
test_files: []
|