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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/crunchaccounting-api.rb +483 -0
  3. 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: []