twinfieldrb 0.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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/codeql-analysis.yml +70 -0
  3. data/.github/workflows/rspec.yml +33 -0
  4. data/.gitignore +6 -0
  5. data/.rspec +1 -0
  6. data/CHANGELOG.md +15 -0
  7. data/Gemfile +4 -0
  8. data/README.md +120 -0
  9. data/Rakefile +1 -0
  10. data/lib/twinfield/abstract_model.rb +7 -0
  11. data/lib/twinfield/api/base_api.rb +50 -0
  12. data/lib/twinfield/api/finder.rb +45 -0
  13. data/lib/twinfield/api/o_auth_session.rb +58 -0
  14. data/lib/twinfield/api/process.rb +44 -0
  15. data/lib/twinfield/api/session.rb +170 -0
  16. data/lib/twinfield/browse/transaction/cost_center.rb +145 -0
  17. data/lib/twinfield/browse/transaction/customer.rb +413 -0
  18. data/lib/twinfield/browse/transaction/general_ledger.rb +144 -0
  19. data/lib/twinfield/configuration.rb +49 -0
  20. data/lib/twinfield/create/cost_center.rb +39 -0
  21. data/lib/twinfield/create/creditor.rb +88 -0
  22. data/lib/twinfield/create/debtor.rb +88 -0
  23. data/lib/twinfield/create/error.rb +30 -0
  24. data/lib/twinfield/create/general_ledger.rb +39 -0
  25. data/lib/twinfield/create/transaction.rb +97 -0
  26. data/lib/twinfield/customer.rb +612 -0
  27. data/lib/twinfield/helpers/parsers.rb +23 -0
  28. data/lib/twinfield/helpers/transaction_match.rb +40 -0
  29. data/lib/twinfield/request/find.rb +149 -0
  30. data/lib/twinfield/request/list.rb +66 -0
  31. data/lib/twinfield/request/read.rb +111 -0
  32. data/lib/twinfield/sales_invoice.rb +409 -0
  33. data/lib/twinfield/transaction.rb +112 -0
  34. data/lib/twinfield/version.rb +5 -0
  35. data/lib/twinfield.rb +89 -0
  36. data/script/boot.rb +58 -0
  37. data/script/console +2 -0
  38. data/spec/fixtures/cluster/finder/ivt.xml +1 -0
  39. data/spec/fixtures/cluster/processxml/columns/sales_transactions.xml +312 -0
  40. data/spec/fixtures/cluster/processxml/customer/create_success.xml +100 -0
  41. data/spec/fixtures/cluster/processxml/customer/read_success.xml +93 -0
  42. data/spec/fixtures/cluster/processxml/customer/update_success.xml +1 -0
  43. data/spec/fixtures/cluster/processxml/invoice/create_error.xml +8 -0
  44. data/spec/fixtures/cluster/processxml/invoice/create_final_error.xml +67 -0
  45. data/spec/fixtures/cluster/processxml/invoice/create_success.xml +64 -0
  46. data/spec/fixtures/cluster/processxml/invoice/read_not_found.xml +1 -0
  47. data/spec/fixtures/cluster/processxml/invoice/read_success.xml +106 -0
  48. data/spec/fixtures/cluster/processxml/read/deb.xml +12 -0
  49. data/spec/fixtures/cluster/processxml/response.xml +8 -0
  50. data/spec/fixtures/login/session/wsdl.xml +210 -0
  51. data/spec/spec_helper.rb +17 -0
  52. data/spec/stubs/finder_stubs.rb +19 -0
  53. data/spec/stubs/processxml_stubs.rb +41 -0
  54. data/spec/stubs/session_stubs.rb +28 -0
  55. data/spec/twinfield/api/oauth_session_spec.rb +37 -0
  56. data/spec/twinfield/api/process_spec.rb +7 -0
  57. data/spec/twinfield/browse/transaction/cost_center_spec.rb +60 -0
  58. data/spec/twinfield/browse/transaction/general_ledger_spec.rb +60 -0
  59. data/spec/twinfield/browse/transaction/transaction_spec.rb +72 -0
  60. data/spec/twinfield/config_spec.rb +60 -0
  61. data/spec/twinfield/customer_spec.rb +326 -0
  62. data/spec/twinfield/request/find_spec.rb +24 -0
  63. data/spec/twinfield/request/list_spec.rb +58 -0
  64. data/spec/twinfield/request/read_spec.rb +26 -0
  65. data/spec/twinfield/sales_invoice_spec.rb +253 -0
  66. data/spec/twinfield/session_spec.rb +77 -0
  67. data/spec/twinfield/transaction_spec.rb +149 -0
  68. data/twinfieldrb.gemspec +24 -0
  69. data/wsdls/accounting/finder.wsdl +157 -0
  70. data/wsdls/accounting/process.wsdl +199 -0
  71. data/wsdls/accounting/session.wsdl +452 -0
  72. data/wsdls/accounting2/finder.wsdl +157 -0
  73. data/wsdls/accounting2/process.wsdl +199 -0
  74. data/wsdls/api.accounting/finder.wsdl +157 -0
  75. data/wsdls/api.accounting/process.wsdl +199 -0
  76. data/wsdls/session.wsdl +210 -0
  77. data/wsdls/update +10 -0
  78. metadata +196 -0
@@ -0,0 +1,612 @@
1
+ module Twinfield
2
+ class Customer < Twinfield::AbstractModel
3
+ extend Twinfield::Helpers::Parsers
4
+
5
+ class CollectMandate
6
+ extend Twinfield::Helpers::Parsers
7
+
8
+ attr_accessor :signaturedate, :id
9
+
10
+ def initialize(signaturedate: nil, id: nil)
11
+ @signaturedate = signaturedate
12
+ @id = id
13
+ end
14
+
15
+ def to_h
16
+ {signaturedate: signaturedate, id: id}
17
+ end
18
+ alias_method :to_hash, :to_h
19
+
20
+ def to_xml
21
+ Nokogiri::XML::Builder.new do |xml|
22
+ xml.collectmandate do
23
+ xml.id id
24
+ xml.signaturedate signaturedate&.strftime("%Y%m%d")
25
+ end
26
+ end.doc.root.to_xml
27
+ end
28
+
29
+ def self.from_xml(nokogiri)
30
+ obj = new
31
+ obj.signaturedate = parse_date(nokogiri.css("signaturedate").text)
32
+ obj.id = nokogiri.css("id").text
33
+ obj
34
+ end
35
+ end
36
+
37
+ class RemittanceAdvice
38
+ attr_accessor :sendtype, :sendmail
39
+
40
+ def initialize(sendtype: nil, sendmail: nil)
41
+ @sendtype = sendtype
42
+ @sendmail = sendmail
43
+ end
44
+
45
+ def to_h
46
+ {sendtype: sendtype, sendmail: sendmail}
47
+ end
48
+ alias_method :to_hash, :to_h
49
+
50
+ def to_xml
51
+ Nokogiri::XML::Builder.new do |xml|
52
+ xml.remittanceadvice do
53
+ xml.sendtype sendtype
54
+ xml.sendmail sendmail
55
+ end
56
+ end.doc.root.to_xml
57
+ end
58
+
59
+ def self.from_xml(nokogiri)
60
+ obj = new
61
+ obj.sendtype = nokogiri.css("sendtype").text
62
+ obj.sendmail = nokogiri.css("sendmail").text
63
+ obj
64
+ end
65
+ end
66
+
67
+ class Bank
68
+ attr_accessor :ascription, :accountnumber, :address, :bankname, :biccode, :city, :country, :iban, :natbiccode, :postcode, :state, :id, :default
69
+
70
+ def initialize(ascription: nil, accountnumber: nil, address: nil, bankname: nil, biccode: nil, city: nil, country: nil, iban: nil, natbiccode: nil, postcode: nil, state: nil, id: nil, default: nil)
71
+ @ascription = ascription
72
+ @accountnumber = accountnumber
73
+ @address = (address.is_a?(Hash) && address != {}) ? Address.new(**address) : address
74
+ @bankname = bankname
75
+ @biccode = biccode
76
+ @city = city
77
+ @country = country
78
+ @iban = iban
79
+ @natbiccode = natbiccode
80
+ @postcode = postcode
81
+ @state = state
82
+ @default = ["true", true, 1, "1"].include?(default)
83
+ @id = id
84
+ end
85
+
86
+ def to_h
87
+ {
88
+ address: address.to_h,
89
+ ascription: ascription,
90
+ accountnumber: accountnumber,
91
+ bankname: bankname,
92
+ biccode: biccode,
93
+ city: city,
94
+ country: country,
95
+ iban: iban,
96
+ natbiccode: natbiccode,
97
+ postcode: postcode,
98
+ state: state,
99
+ id: id,
100
+ default: default
101
+ }
102
+ end
103
+ alias_method :to_hash, :to_h
104
+
105
+ def to_xml
106
+ Nokogiri::XML::Builder.new do |xml|
107
+ attributes = {default: default}
108
+ attributes[:id] = id if id
109
+
110
+ xml.bank(attributes) do
111
+ xml.ascription ascription
112
+ xml.accountnumber accountnumber
113
+ xml.address address
114
+ xml.bankname bankname
115
+ xml.biccode biccode
116
+ xml.city city
117
+ xml.country country
118
+ xml.iban iban
119
+ xml.natbiccode natbiccode
120
+ xml.postcode postcode
121
+ xml.state state
122
+ end
123
+ end.doc.root.to_xml
124
+ end
125
+
126
+ def self.from_xml(nokogiri_xml)
127
+ obj = new(id: nokogiri_xml.attributes["id"].text, default: nokogiri_xml.attributes["default"].text)
128
+ obj.ascription = nokogiri_xml.css("ascription").text
129
+ obj.accountnumber = nokogiri_xml.css("accountnumber").text
130
+ obj.address = Address.from_xml(nokogiri_xml.css("address")[0]) if nokogiri_xml.css("address")[0] && nokogiri_xml.css("address")[0].children.count > 0
131
+ obj.bankname = nokogiri_xml.css("bankname").text
132
+ obj.biccode = nokogiri_xml.css("biccode").text
133
+ obj.city = nokogiri_xml.css("city").text
134
+ obj.country = nokogiri_xml.css("country").text
135
+ obj.iban = nokogiri_xml.css("iban").text
136
+ obj.natbiccode = nokogiri_xml.css("natbiccode").text
137
+ obj.postcode = nokogiri_xml.css("postcode").text
138
+ obj.state = nokogiri_xml.css("state").text
139
+ obj
140
+ end
141
+ end
142
+
143
+ class Financials
144
+ extend Twinfield::Helpers::Parsers
145
+
146
+ attr_writer :meansofpayment
147
+ attr_reader :payavailable
148
+ attr_accessor :matchtype, :accounttype, :subanalyse, :duedays, :level, :paycode, :ebilling, :ebillmail, :substitutewith, :substitutionlevel, :relationsreference, :vattype, :vatcode, :vatobligatory, :performancetype, :collectmandate, :collectionschema, :childvalidations
149
+
150
+ def initialize(matchtype: nil, accounttype: nil, subanalyse: nil, duedays: nil, level: nil, payavailable: nil, meansofpayment: nil, paycode: nil, ebilling: false, ebillmail: nil, substitutewith: nil, substitutionlevel: nil, relationsreference: nil, vattype: nil, vatcode: nil, vatobligatory: nil, performancetype: nil, collectmandate: nil, collectionschema: nil, childvalidations: nil)
151
+ @matchtype = matchtype
152
+ @accounttype = accounttype
153
+ @subanalyse = subanalyse
154
+ @duedays = duedays
155
+ @level = level
156
+ @payavailable = payavailable # Direct Debit / Automatische incasso
157
+ @meansofpayment = meansofpayment
158
+ @paycode = paycode
159
+ @ebilling = ebilling
160
+ @ebillmail = ebillmail
161
+ @substitutewith = substitutewith
162
+ @substitutionlevel = substitutionlevel
163
+ @relationsreference = relationsreference # not in use
164
+ @vattype = vattype # not in use
165
+ @vatcode = vatcode
166
+ @vatobligatory = vatobligatory
167
+ @performancetype = performancetype
168
+ @collectmandate = collectmandate.is_a?(Hash) ? Twinfield::Customer::CollectMandate.new(**collectmandate) : collectmandate
169
+ @collectionschema = collectionschema
170
+ @childvalidations = childvalidations
171
+ end
172
+
173
+ def meansofpayment
174
+ @meansofpayment || (payavailable ? "paymentfile" : "none")
175
+ end
176
+
177
+ def payavailable= value
178
+ if [true, "true"].include? value
179
+ @meansofpayment = nil
180
+ @payavailable = true
181
+ end
182
+ end
183
+
184
+ def to_h
185
+ {
186
+ matchtype: matchtype,
187
+ accounttype: accounttype,
188
+ subanalyse: subanalyse,
189
+ duedays: duedays,
190
+ level: level,
191
+ payavailable: payavailable,
192
+ meansofpayment: meansofpayment,
193
+ paycode: paycode,
194
+ ebilling: ebilling,
195
+ ebillmail: ebillmail,
196
+ substitutewith: substitutewith,
197
+ substitutionlevel: substitutionlevel,
198
+ relationsreference: relationsreference,
199
+ vattype: vattype,
200
+ vatcode: vatcode,
201
+ vatobligatory: vatobligatory,
202
+ performancetype: performancetype,
203
+ collectmandate: collectmandate&.to_h,
204
+ collectionschema: collectionschema,
205
+ childvalidations: childvalidations
206
+ }
207
+ end
208
+ alias_method :to_hash, :to_h
209
+
210
+ def to_xml
211
+ Nokogiri::XML::Builder.new do |xml|
212
+ xml.financials do
213
+ xml.matchtype matchtype if matchtype
214
+ xml.accounttype accounttype if accounttype
215
+ xml.subanalyse subanalyse if subanalyse
216
+ xml.duedays duedays if duedays
217
+ xml.level level if level
218
+ xml.payavailable payavailable if payavailable
219
+ xml.meansofpayment meansofpayment if meansofpayment
220
+ xml.paycode paycode if paycode
221
+ xml.ebilling ebilling if ebilling
222
+ xml.ebillmail ebillmail if ebillmail
223
+ xml.substitutewith substitutewith if substitutewith
224
+ xml.substitutionlevel substitutionlevel if substitutionlevel
225
+ xml.relationsreference relationsreference if relationsreference
226
+ xml.vattype vattype if vattype
227
+ xml.vatcode vatcode if vatcode
228
+ xml.vatobligatory vatobligatory if vatobligatory
229
+ xml.performancetype performancetype if performancetype
230
+ xml << collectmandate.to_xml if collectmandate
231
+ xml.collectionschema collectionschema if collectionschema
232
+ end
233
+ end.doc.root.to_xml
234
+ end
235
+
236
+ def self.from_xml(nokogiri)
237
+ obj = new
238
+ obj.matchtype = nokogiri.css("matchtype").text
239
+ obj.accounttype = nokogiri.css("accounttype").text
240
+ obj.subanalyse = nokogiri.css("subanalyse").text
241
+ obj.duedays = nokogiri.css("duedays").text
242
+ obj.level = nokogiri.css("level").text
243
+ obj.payavailable = nokogiri.css("payavailable").text
244
+ obj.meansofpayment = nokogiri.css("meansofpayment").text
245
+ obj.paycode = nokogiri.css("paycode").text
246
+ obj.ebilling = nokogiri.css("ebilling").text == "true"
247
+ obj.ebillmail = nokogiri.css("ebillmail").text
248
+ obj.substitutewith = nokogiri.css("substitutewith").text
249
+ obj.substitutionlevel = nokogiri.css("substitutionlevel").text
250
+ obj.relationsreference = nokogiri.css("relationsreference").text
251
+ obj.vattype = nokogiri.css("vattype").text
252
+ obj.vatcode = nokogiri.css("vatcode").text
253
+ obj.vatobligatory = nokogiri.css("vatobligatory").text
254
+ obj.performancetype = nokogiri.css("performancetype").text
255
+ obj.collectmandate = CollectMandate.from_xml(nokogiri.css("collectmandate"))
256
+ obj.collectionschema = nokogiri.css("collectionschema").text
257
+ obj.childvalidations = nokogiri.css("childvalidations").text&.strip
258
+ obj
259
+ end
260
+ end
261
+
262
+ class CreditManagement
263
+ attr_accessor :responsibleuser, :basecreditlimit, :sendreminder, :reminderemail, :blocked, :freetext1, :freetext2, :freetext3, :comment
264
+
265
+ def to_h
266
+ {
267
+ responsibleuser: responsibleuser,
268
+ basecreditlimit: basecreditlimit,
269
+ sendreminder: sendreminder,
270
+ reminderemail: reminderemail,
271
+ blocked: blocked,
272
+ freetext1: freetext1,
273
+ freetext2: freetext2,
274
+ freetext3: freetext3,
275
+ comment: comment
276
+ }
277
+ end
278
+ alias_method :to_hash, :to_h
279
+
280
+ def present?
281
+ "#{responsibleuser}#{basecreditlimit}#{sendreminder}#{reminderemail}#{blocked}#{freetext3}#{freetext1}#{freetext2}#{comment}".strip != ""
282
+ end
283
+
284
+ def initialize(responsibleuser: nil, basecreditlimit: nil, sendreminder: nil, reminderemail: nil, blocked: nil, freetext1: nil, freetext2: nil, freetext3: nil, comment: nil)
285
+ @responsibleuser = responsibleuser
286
+ @basecreditlimit = basecreditlimit
287
+ @sendreminder = sendreminder
288
+ @reminderemail = reminderemail
289
+ @blocked = blocked
290
+ @freetext1 = freetext1
291
+ @freetext2 = freetext2
292
+ @freetext3 = freetext3
293
+ @comment = comment
294
+ end
295
+
296
+ def to_xml
297
+ Nokogiri::XML::Builder.new do |xml|
298
+ xml.creditmanagement do
299
+ xml.responsibleuser responsibleuser if responsibleuser
300
+ xml.basecreditlimit basecreditlimit if basecreditlimit
301
+ xml.sendreminder sendreminder if sendreminder
302
+ xml.reminderemail reminderemail if reminderemail
303
+ xml.blocked blocked
304
+ xml.freetext1 freetext1
305
+ xml.freetext2 freetext2
306
+ xml.freetext3 freetext3
307
+ xml.comment comment
308
+ end
309
+ end.doc.root.to_xml
310
+ end
311
+
312
+ def self.from_xml(nokogiri)
313
+ obj = new
314
+ obj.responsibleuser = nokogiri.css("responsibleuser").text
315
+ obj.basecreditlimit = nokogiri.css("basecreditlimit").text
316
+ obj.sendreminder = nokogiri.css("sendreminder").text
317
+ obj.reminderemail = nokogiri.css("reminderemail").text
318
+ obj.blocked = nokogiri.css("blocked").text
319
+ obj.freetext1 = nokogiri.css("freetext1").text
320
+ obj.freetext2 = nokogiri.css("freetext2").text
321
+ obj.freetext3 = nokogiri.css("freetext3").text
322
+ obj.comment = nokogiri.css("comment").text
323
+ obj
324
+ end
325
+ end
326
+
327
+ class Address
328
+ TYPES = [:invoice, :postal, :contact]
329
+
330
+ attr_accessor :name, :country, :ictcountrycode, :city, :postcode, :telephone, :telefax, :email, :contact, :field1, :field2, :field3, :field4, :field5, :field6, :type, :default, :id
331
+
332
+ def initialize(name:, country: nil, ictcountrycode: nil, city: nil, postcode: nil, telephone: nil, telefax: nil, email: nil, contact: nil, field1: nil, field2: nil, field3: nil, field4: nil, field5: nil, field6: nil, id: nil, type: nil, default: nil)
333
+ @name = name
334
+ @country = country
335
+ @ictcountrycode = ictcountrycode
336
+ @city = city
337
+ @postcode = postcode
338
+ @telephone = telephone
339
+ @telefax = telefax
340
+ @email = email
341
+ @contact = contact
342
+ @field1 = field1
343
+ @field2 = field2
344
+ @field3 = field3
345
+ @field4 = field4
346
+ @field5 = field5
347
+ @field6 = field6
348
+ @id = id
349
+ @type = type
350
+ @default = ["true", true, 1, "1"].include?(default)
351
+ end
352
+
353
+ def to_h
354
+ {
355
+ name: name,
356
+ country: country,
357
+ ictcountrycode: ictcountrycode,
358
+ city: city,
359
+ postcode: postcode,
360
+ telephone: telephone,
361
+ telefax: telefax,
362
+ email: email,
363
+ contact: contact,
364
+ field1: field1,
365
+ field2: field2,
366
+ field3: field3,
367
+ field4: field4,
368
+ field5: field5,
369
+ field6: field6,
370
+ type: type,
371
+ id: id,
372
+ default: default
373
+ }
374
+ end
375
+ alias_method :to_hash, :to_h
376
+
377
+ def to_s
378
+ [name, field1, field2, [country, postcode, city].join(" ")].select { |a| !(a.nil? || a.empty?) }.join("\n")
379
+ end
380
+
381
+ def to_xml
382
+ Nokogiri::XML::Builder.new do |xml|
383
+ xml.address(id: @id, type: @type, default: @default) do
384
+ xml.name name
385
+ xml.country country
386
+ xml.ictcountrycode ictcountrycode
387
+ xml.city city
388
+ xml.postcode postcode
389
+ xml.telephone telephone
390
+ xml.telefax telefax
391
+ xml.email email
392
+ xml.contact contact
393
+ xml.field1 field1
394
+ xml.field2 field2
395
+ xml.field3 field3
396
+ xml.field4 field4
397
+ xml.field5 field5
398
+ xml.field6 field6
399
+ end
400
+ end.doc.root.to_xml
401
+ end
402
+
403
+ def self.from_xml nokogiri
404
+ obj = new(id: nokogiri.attributes["id"]&.text, name: nokogiri.css("name")&.text, default: nokogiri.attributes["default"]&.text)
405
+ obj.country = nokogiri.css("country").text
406
+ obj.type = nokogiri.attributes["type"]&.text
407
+ obj.ictcountrycode = nokogiri.css("ictcountrycode")&.text
408
+ obj.city = nokogiri.css("city")&.text
409
+ obj.postcode = nokogiri.css("postcode")&.text
410
+ obj.telephone = nokogiri.css("telephone")&.text
411
+ obj.telefax = nokogiri.css("telefax")&.text
412
+ obj.email = nokogiri.css("email")&.text
413
+ obj.contact = nokogiri.css("contact")&.text
414
+ obj.field1 = nokogiri.css("field1")&.text
415
+ obj.field2 = nokogiri.css("field2")&.text
416
+ obj.field3 = nokogiri.css("field3")&.text
417
+ obj.field4 = nokogiri.css("field4")&.text
418
+ obj.field5 = nokogiri.css("field5")&.text
419
+ obj.field6 = nokogiri.css("field6")&.text
420
+ obj
421
+ end
422
+ end
423
+
424
+ attr_accessor :office, :code, :uid, :name, :shortname, :inuse, :behaviour, :modified, :touched, :beginperiod, :beginyear, :endperiod, :endyear, :website, :cocnumber, :vatnumber, :financials, :creditmanagement, :remittanceadvice, :addresses, :banks, :status
425
+
426
+ def initialize(office: nil, code: nil, uid: nil, name: nil, shortname: nil, inuse: nil, behaviour: nil, modified: nil, touched: nil, beginperiod: nil, beginyear: nil, endperiod: nil, endyear: nil, website: nil, cocnumber: nil, vatnumber: nil, financials: nil, creditmanagement: nil, remittanceadvice: nil, addresses: nil, banks: nil, status: :active)
427
+ @office = office || Twinfield.configuration.company
428
+ @status = status
429
+ @code = code
430
+ @uid = uid
431
+ @name = name
432
+ @shortname = shortname
433
+ @inuse = inuse
434
+ @behaviour = behaviour
435
+ @modified = modified
436
+ @touched = touched
437
+ @beginperiod = beginperiod
438
+ @beginyear = beginyear
439
+ @endperiod = endperiod
440
+ @endyear = endyear
441
+ @website = website
442
+ @cocnumber = cocnumber
443
+ @vatnumber = vatnumber
444
+ @financials = financials.is_a?(Hash) ? Financials.new(**financials) : financials
445
+ @creditmanagement = creditmanagement.is_a?(Hash) ? CreditManagement.new(**creditmanagement) : creditmanagement
446
+ @remittanceadvice = remittanceadvice.is_a?(Hash) ? RemittanceAdvice.new(**remittanceadvice) : remittanceadvice
447
+ @addresses = (addresses || []).map { |a| a.is_a?(Hash) ? Address.new(**a) : a }
448
+ @banks = (banks || []).map { |a| a.is_a?(Hash) ? Bank.new(**a) : a }
449
+ end
450
+
451
+ def to_h
452
+ {
453
+ office: office,
454
+ code: code,
455
+ uid: uid,
456
+ name: name,
457
+ shortname: shortname,
458
+ inuse: inuse,
459
+ behaviour: behaviour,
460
+ modified: modified,
461
+ touched: touched,
462
+ beginperiod: beginperiod,
463
+ beginyear: beginyear,
464
+ endperiod: endperiod,
465
+ endyear: endyear,
466
+ website: website,
467
+ cocnumber: cocnumber,
468
+ vatnumber: vatnumber,
469
+ financials: financials.to_h,
470
+ creditmanagement: creditmanagement.to_h,
471
+ remittanceadvice: remittanceadvice.to_h,
472
+ addresses: addresses.map(&:to_h),
473
+ banks: banks.map(&:to_h),
474
+ status: status
475
+ }
476
+ end
477
+ alias_method :to_hash, :to_h
478
+
479
+ def to_xml
480
+ Nokogiri::XML::Builder.new do |xml|
481
+ xml.dimension(status: status) do
482
+ xml.office office
483
+ xml.type "DEB"
484
+ xml.code code
485
+ xml.uid uid if uid
486
+ xml.name name
487
+ xml.shortname shortname
488
+ # xml.behaviour behaviour #temporarily disable
489
+ xml.beginperiod beginperiod if beginperiod
490
+ xml.beginyear beginyear if beginyear
491
+ xml.endperiod endperiod if endperiod
492
+ xml.endyear endyear if endyear
493
+ xml.website website
494
+ xml << financials&.to_xml
495
+ xml << creditmanagement&.to_xml if creditmanagement&.present?
496
+ xml << remittanceadvice&.to_xml
497
+ xml.addresses do
498
+ addresses.each do |line|
499
+ xml << line.to_xml
500
+ end
501
+ end
502
+ xml.banks do
503
+ banks.each do |line|
504
+ xml << line.to_xml
505
+ end
506
+ end
507
+ end
508
+ end.doc.root.to_xml
509
+ end
510
+
511
+ def save
512
+ response = Twinfield::Api::Process.request { to_xml }
513
+
514
+ xml = Nokogiri::XML(response.body[:process_xml_string_response][:process_xml_string_result])
515
+
516
+ if xml.at_css("dimension").attributes["result"].value == "1"
517
+ self.class.from_xml(xml)
518
+ else
519
+ raise Twinfield::Create::Error.new(xml.css("[msg]").map { |x| x.attributes["msg"].value }.join(" "), object: self)
520
+ end
521
+ end
522
+
523
+ def destroy
524
+ self.status = :deleted
525
+ save
526
+ end
527
+ alias_method :delete, :destroy
528
+
529
+ def transactions(filter_hash = {})
530
+ Twinfield::Browse::Transaction::Customer.where(customer_code: code, **filter_hash)
531
+ end
532
+
533
+ def load
534
+ Customer.find(code)
535
+ end
536
+
537
+ class << self
538
+ # @return Array<Twinfield::Customer>
539
+ def all
540
+ search
541
+ end
542
+
543
+ # helper method that calculates the next unused code
544
+ # @return String
545
+ def next_unused_twinfield_customer_code range = nil
546
+ current_codes = Twinfield::Customer.all.map(&:code).map(&:to_i).sort
547
+ current_codes_in_range = current_codes & range.to_a if range
548
+ latest = (current_codes_in_range || current_codes).last
549
+ latest = latest ? latest + 1 : range.to_a.first
550
+ raise "invalid new customer code" if latest == 1
551
+ latest.to_s
552
+ end
553
+
554
+ def search text = "*"
555
+ text = "*#{text}*" unless text.match?(/[?*]/)
556
+ options = {
557
+ dimtype: "DEB",
558
+ office: Twinfield.configuration.company,
559
+ pattern: text,
560
+ max_rows: 10000
561
+ }
562
+ response = Twinfield::Api::Finder.request("DIM", options)
563
+ if response.body[:search_response][:data][:total_rows].to_i == 1
564
+ resp = response.body[:search_response][:data][:items][:array_of_string][:string]
565
+ [Customer.new(name: resp[1], code: resp[0])]
566
+ elsif response.body[:search_response][:data][:total_rows].to_i > 1
567
+ response.body[:search_response][:data][:items][:array_of_string]
568
+ .map { |item| Customer.new(name: item[:string][1], code: item[:string][0]) }
569
+ else
570
+ []
571
+ end
572
+ end
573
+
574
+ def find(customercode)
575
+ options = {office: Twinfield.configuration.company, code: customercode, dimtype: "DEB"}
576
+ customer_xml = Twinfield::Api::Process.read(:dimensions, options)
577
+ from_xml(customer_xml)
578
+ end
579
+
580
+ def from_xml(nokogiri_or_string)
581
+ nokogiri = nokogiri_or_string.is_a?(Nokogiri::XML::Document) ? nokogiri_or_string : Nokogiri::XML(nokogiri_or_string)
582
+ dimension = nokogiri.css("dimension")[0]
583
+
584
+ if dimension.attributes["result"]&.text == "0"
585
+ return nil
586
+ end
587
+
588
+ obj = new(shortname: nokogiri.css("dimension > shortname").text, name: nokogiri.css("dimension > name").text, code: nokogiri.css("dimension > code").text)
589
+ obj.status = dimension.attributes["status"].text
590
+ obj.office = nokogiri.css("dimension > office").text
591
+ obj.uid = nokogiri.css("dimension > uid").text
592
+ obj.inuse = nokogiri.css("dimension > inuse").text
593
+ obj.behaviour = nokogiri.css("dimension > behaviour").text
594
+ obj.modified = parse_datetime(nokogiri.css("dimension > modified").text)
595
+ obj.touched = nokogiri.css("dimension > touched").text
596
+ obj.beginperiod = nokogiri.css("dimension > beginperiod").text
597
+ obj.beginyear = nokogiri.css("dimension > beginyear").text
598
+ obj.endperiod = nokogiri.css("dimension > endperiod").text
599
+ obj.endyear = nokogiri.css("dimension > endyear").text
600
+ obj.website = nokogiri.css("dimension > website").text
601
+ obj.cocnumber = nokogiri.css("dimension > cocnumber").text
602
+ obj.vatnumber = nokogiri.css("dimension > vatnumber").text
603
+ obj.financials = Financials.from_xml(nokogiri.css("dimension > financials")[0])
604
+ obj.creditmanagement = CreditManagement.from_xml(nokogiri.css("dimension > creditmanagement")[0]) if nokogiri.css("dimension > creditmanagement")[0]
605
+ obj.remittanceadvice = RemittanceAdvice.from_xml(nokogiri.css("dimension > remittanceadvice")[0]) if nokogiri.css("dimension > remittanceadvice")[0]
606
+ obj.addresses = nokogiri.css("dimension > addresses > address").map { |xml_fragment| Address.from_xml(xml_fragment) }
607
+ obj.banks = nokogiri.css("dimension > banks > bank").map { |xml_fragment| Bank.from_xml(xml_fragment) }
608
+ obj
609
+ end
610
+ end
611
+ end
612
+ end
@@ -0,0 +1,23 @@
1
+ module Twinfield
2
+ module Helpers
3
+ module Parsers
4
+ def parse_date string
5
+ if string && string != ""
6
+ Date.strptime(string, "%Y%m%d")
7
+ end
8
+ end
9
+
10
+ def parse_datetime string
11
+ if string && string != ""
12
+ DateTime.strptime(string, "%Y%m%d%H%M%S")
13
+ end
14
+ end
15
+
16
+ def parse_float string
17
+ if string && string != ""
18
+ Float(string)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ module Twinfield
2
+ class TransactionMatchError < StandardError; end
3
+
4
+ module Helpers
5
+ module TransactionMatch
6
+ def to_transaction_match_line_xml(transline)
7
+ "<line><transcode>#{code}</transcode><transnumber>#{number}</transnumber><transline>#{transline}</transline></line>"
8
+ end
9
+
10
+ def to_match_xml other_transaction
11
+ raise Twinfield::TransactionMatchError.new("This transaction and the other transaction don't add up to 0") if (value + other_transaction.value) != 0
12
+ "<match>
13
+ <set>
14
+ <matchcode>170</matchcode>
15
+ <office>#{Twinfield.configuration.company}</office>
16
+ <matchdate>#{Date.today.strftime("%Y%m%d")}</matchdate>
17
+ <lines>
18
+ #{to_transaction_match_line_xml(1)}
19
+ #{other_transaction.to_transaction_match_line_xml(2)}
20
+ </lines>
21
+ </set>
22
+ </match>"
23
+ end
24
+
25
+ def match! other_transaction
26
+ response = Twinfield::Api::Process.request do
27
+ to_match_xml(other_transaction)
28
+ end
29
+
30
+ xml = Nokogiri::XML(response.body[:process_xml_string_response][:process_xml_string_result])
31
+
32
+ if xml.at_css("match").attributes["result"].value == "1"
33
+ self
34
+ else
35
+ raise Twinfield::TransactionMatchError.new(xml.css("[msg]").map { |x| x.attributes["msg"].value }.join(" ") + "(possibly a non-matching invoice number)")
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end