twinfieldrb 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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,144 @@
1
+ module Twinfield
2
+ module Browse
3
+ module Transaction
4
+ class GeneralLedger < Twinfield::AbstractModel
5
+ extend Twinfield::Helpers::Parsers
6
+ include Twinfield::Helpers::TransactionMatch
7
+
8
+ attr_accessor :number, :yearperiod, :currency, :value, :status, :dim1, :dim2, :key, :code
9
+
10
+ class << self
11
+ def initialize_from_columns_response_row(transaction_xml)
12
+ new(
13
+ number: transaction_xml.css("td[field='fin.trs.head.number']").text,
14
+ yearperiod: transaction_xml.css("td[field='fin.trs.head.yearperiod']").text,
15
+ currency: transaction_xml.css("td[field='fin.trs.head.curcode']").text,
16
+ value: transaction_xml.css("td[field='fin.trs.line.valuesigned']").text&.to_f,
17
+ status: transaction_xml.css("td[field='fin.trs.head.status']").text,
18
+ dim1: transaction_xml.css("td[field='fin.trs.line.dim1']").text,
19
+ dim2: transaction_xml.css("td[field='fin.trs.line.dim2']").text,
20
+ key: transaction_xml.css("key").text.gsub(/\s/, ""),
21
+ code: transaction_xml.css("td[field='fin.trs.head.code']").text
22
+ )
23
+ end
24
+
25
+ def find(customer_code: nil, invoice_number: nil, code: nil, number: nil)
26
+ where(customer_code: customer_code, invoice_number: invoice_number, code: code, number: number).first
27
+ end
28
+
29
+ # @param years: range
30
+ #
31
+ def where(years: ((Date.today.year - 30)..Date.today.year), dim1: nil, dim2: nil)
32
+ build_request = %(
33
+ <column>
34
+ <field>fin.trs.head.yearperiod</field>
35
+ <label>Periode</label>
36
+ <visible>true</visible>
37
+ <ask>false</ask>
38
+ <operator>between</operator>
39
+ <from>#{years.first}/01</from>
40
+ <to>#{years.last}/12</to>
41
+ <finderparam/>
42
+ </column>
43
+ <column>
44
+ <field>fin.trs.line.dim1</field>
45
+ <label>Grootboek</label>
46
+ <visible>true</visible>
47
+ <ask>false</ask>
48
+ <operator>between</operator>
49
+ <from>#{dim1}</from>
50
+ <to>#{dim1}</to>
51
+ <finderparam/>
52
+ </column>
53
+ <column>
54
+ <field>fin.trs.line.dim2</field>
55
+ <label>Kostenplaats</label>
56
+ <visible>true</visible>
57
+ <ask>false</ask>
58
+ <operator>between</operator>
59
+ <from>#{dim2}</from>
60
+ <to>#{dim2}</to>
61
+ </column>
62
+ <column>
63
+ <field>fin.trs.head.code</field>
64
+ <label>Dagboek</label>
65
+ <visible>true</visible>
66
+ <ask>false</ask>
67
+ <operator>equal</operator>
68
+ <from/>
69
+ <to/>
70
+ <finderparam>hidden=1</finderparam>
71
+ </column>
72
+ <column>
73
+ <field>fin.trs.head.number</field>
74
+ <label>Boekst.nr.</label>
75
+ <visible>true</visible>
76
+ <ask>false</ask>
77
+ <operator>between</operator>
78
+ <from/>
79
+ <to/>
80
+ <finderparam/>
81
+ </column>
82
+ <column>
83
+ <field>fin.trs.head.curcode</field>
84
+ <label>Valuta</label>
85
+ <visible>true</visible>
86
+ <ask>false</ask>
87
+ <operator>none</operator>
88
+ <from/>
89
+ <to/>
90
+ <finderparam/>
91
+ </column>
92
+ <column>
93
+ <field>fin.trs.line.valuesigned</field>
94
+ <label>Bedrag</label>
95
+ <visible>true</visible>
96
+ <ask>false</ask>
97
+ <operator>between</operator>
98
+ <from/>
99
+ <to/>
100
+ <finderparam/>
101
+ </column>
102
+ <column>
103
+ <field>fin.trs.head.status</field>
104
+ <label>Status</label>
105
+ <visible>true</visible>
106
+ <ask>false</ask>
107
+ <operator>equal</operator>
108
+ <from>normal</from>
109
+ <to/>
110
+ <finderparam/>
111
+ </column>
112
+ )
113
+
114
+ response = Twinfield::Api::Process.request(:process_xml_string) do
115
+ %(
116
+ <columns code="000">
117
+ #{build_request}
118
+ </columns>
119
+ )
120
+ end
121
+
122
+ xml = Nokogiri::XML(response.body[:process_xml_string_response][:process_xml_string_result])
123
+
124
+ xml.css("tr").map do |transaction_xml|
125
+ Twinfield::Browse::Transaction::GeneralLedger.initialize_from_columns_response_row(transaction_xml)
126
+ end
127
+ end
128
+ end
129
+
130
+ def initialize(number: nil, yearperiod: nil, currency: "EUR", value: nil, status: nil, dim1: nil, dim2: nil, key: nil, code: nil)
131
+ self.number = number
132
+ self.yearperiod = yearperiod
133
+ self.currency = currency
134
+ self.value = value
135
+ self.status = status
136
+ self.dim1 = dim1
137
+ self.dim2 = dim2
138
+ self.key = key
139
+ self.code = code
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,49 @@
1
+ require "base64"
2
+ require "json"
3
+
4
+ module Twinfield
5
+ # Used for configuration of the Twinfield gem.
6
+
7
+ class Configuration
8
+ class Error < StandardError
9
+ end
10
+ attr_accessor :session_type # Twinfield::Api::OAuthSession or Twinfield::Api::Session
11
+
12
+ # in case Twinfield::Api::Session is used
13
+ attr_accessor :username
14
+ attr_accessor :password
15
+ attr_accessor :organisation
16
+ attr_accessor :company
17
+
18
+ # in case Twinfield::Api::OAuthSession is used
19
+ attr_accessor :cluster
20
+ attr_accessor :access_token
21
+
22
+ # Log level, e.g. :info, :debug; currently only forwarded to Savon
23
+ attr_accessor :log_level
24
+ attr_accessor :logger
25
+
26
+ def to_logon_hash
27
+ {
28
+ "user" => @username,
29
+ "password" => @password,
30
+ "organisation" => @organisation
31
+ }
32
+ end
33
+
34
+ def access_token_expired?
35
+ Time.at(JSON.parse(Base64.decode64(access_token.split(".")[1]))["exp"]) < Time.now
36
+ rescue => e
37
+ raise Twinfield::Configuration::Error, "No valid access token provided (#{e.message})"
38
+ end
39
+
40
+ def session_class
41
+ case session_type
42
+ when "Twinfield::Api::OAuthSession"
43
+ Twinfield::Api::OAuthSession
44
+ else
45
+ Twinfield::Api::Session
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ module Twinfield
2
+ module Create
3
+ class CostCenter
4
+ attr_accessor :name, :code
5
+
6
+ def initialize(hash = {})
7
+ hash.each { |k, v| send(:"#{k}=", CGI.escapeHTML(v)) }
8
+ end
9
+
10
+ def save
11
+ response = Twinfield::Api::Process.request do
12
+ %(
13
+ <dimension>
14
+ <office>#{Twinfield.configuration.company}</office>
15
+ <type>KPL</type>
16
+ <name>#{name}</name>
17
+ <code>#{code}</code>
18
+ </dimension>
19
+ )
20
+ end
21
+
22
+ xml = Nokogiri::XML(response.body[:process_xml_string_response][:process_xml_string_result])
23
+
24
+ if xml.at_css("dimension").attributes["result"].value == "1"
25
+ {
26
+ code: code,
27
+ status: 1
28
+ }
29
+ else
30
+ {
31
+ code: code,
32
+ status: 0,
33
+ messages: xml.css("[msg]").map { |x| x.attributes["msg"].value }
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,88 @@
1
+ module Twinfield
2
+ module Create
3
+ class Creditor
4
+ attr_accessor :bank_description, :bank_iban, :bank_country, :country, :financials_duedays,
5
+ :iban, :invoice_address, :invoice_city, :invoice_country, :invoice_name,
6
+ :invoice_zipcode, :name, :shortname, :code, :bank_biccode, :vatcode,
7
+ :invoice_contact_name
8
+
9
+ def initialize(hash = {})
10
+ # Escape all the things.
11
+ hash.each do |k, v|
12
+ val = if v.is_a?(String)
13
+ CGI.escapeHTML(v)
14
+ elsif v.is_a?(Hash)
15
+ v.each_with_object({}) { |(k1, v1), h|
16
+ h[k1] = CGI.escapeHTML(v1)
17
+ }
18
+ else
19
+ v
20
+ end
21
+
22
+ send(:"#{k}=", val)
23
+ end
24
+ end
25
+
26
+ def save
27
+ response = Twinfield::Api::Process.request do
28
+ %(
29
+ <dimension>
30
+ <office>#{Twinfield.configuration.company}</office>
31
+ <type>CRD</type>
32
+ <code>#{code}</code>
33
+ <name>#{name}</name>
34
+ <shortname>#{shortname}</shortname>
35
+ <financials>
36
+ <duedays>#{financials_duedays}</duedays>
37
+ </financials>
38
+ <addresses>
39
+ <address type="invoice">
40
+ <name>#{invoice_name}</name>
41
+ <country>#{invoice_country}</country>
42
+ <city>#{invoice_city}</city>
43
+ <postcode>#{invoice_zipcode}</postcode>
44
+ <field1>#{invoice_contact_name}</field1>
45
+ <field2>#{invoice_address}</field2>
46
+ <field4>#{vatcode}</field4>
47
+ </address>
48
+ </addresses>
49
+ #{bank_xml}
50
+ </dimension>
51
+ )
52
+ end
53
+
54
+ xml = Nokogiri::XML(response.body[:process_xml_string_response][:process_xml_string_result])
55
+
56
+ if xml.at_css("dimension").attributes["result"].value == "1"
57
+ {
58
+ code: code,
59
+ status: 1
60
+ }
61
+ else
62
+ {
63
+ code: code,
64
+ status: 0,
65
+ messages: xml.css("[msg]").map { |x| x.attributes["msg"].value }
66
+ }
67
+ end
68
+ end
69
+
70
+ protected
71
+
72
+ def bank_xml
73
+ if bank_iban.present?
74
+ %(
75
+ <banks>
76
+ <bank>
77
+ <ascription>#{bank_description}</ascription>
78
+ <iban>#{bank_iban}</iban>
79
+ <biccode>#{bank_biccode}</biccode>
80
+ <country>#{bank_country}</country>
81
+ </bank>
82
+ </banks>
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,88 @@
1
+ module Twinfield
2
+ module Create
3
+ class Debtor
4
+ attr_accessor :bank_description, :bank_iban, :bank_country, :country, :financials_duedays,
5
+ :iban, :invoice_address, :invoice_city, :invoice_country, :invoice_name,
6
+ :invoice_zipcode, :name, :shortname, :code, :bank_biccode, :vatcode,
7
+ :invoice_contact_name
8
+
9
+ def initialize(hash = {})
10
+ # Escape all the things.
11
+ hash.each do |k, v|
12
+ val = if v.is_a?(String)
13
+ CGI.escapeHTML(v)
14
+ elsif v.is_a?(Hash)
15
+ v.each_with_object({}) { |(k1, v1), h|
16
+ h[k1] = CGI.escapeHTML(v1)
17
+ }
18
+ else
19
+ v
20
+ end
21
+
22
+ send(:"#{k}=", val)
23
+ end
24
+ end
25
+
26
+ def save
27
+ response = Twinfield::Api::Process.request do
28
+ %(
29
+ <dimension>
30
+ <office>#{Twinfield.configuration.company}</office>
31
+ <type>DEB</type>
32
+ <code>#{code}</code>
33
+ <name>#{name}</name>
34
+ <shortname>#{shortname}</shortname>
35
+ <financials>
36
+ <duedays>#{financials_duedays}</duedays>
37
+ </financials>
38
+ <addresses>
39
+ <address type="invoice">
40
+ <name>#{invoice_name}</name>
41
+ <country>#{invoice_country}</country>
42
+ <city>#{invoice_city}</city>
43
+ <postcode>#{invoice_zipcode}</postcode>
44
+ <field1>#{invoice_contact_name}</field1>
45
+ <field2>#{invoice_address}</field2>
46
+ <field4>#{vatcode}</field4>
47
+ </address>
48
+ </addresses>
49
+ #{bank_xml}
50
+ </dimension>
51
+ )
52
+ end
53
+
54
+ xml = Nokogiri::XML(response.body[:process_xml_string_response][:process_xml_string_result])
55
+
56
+ if xml.at_css("dimension").attributes["result"].value == "1"
57
+ {
58
+ code: code,
59
+ status: 1
60
+ }
61
+ else
62
+ {
63
+ code: code,
64
+ status: 0,
65
+ messages: xml.css("[msg]").map { |x| x.attributes["msg"].value }
66
+ }
67
+ end
68
+ end
69
+
70
+ protected
71
+
72
+ def bank_xml
73
+ if bank_iban.present?
74
+ %(
75
+ <banks>
76
+ <bank>
77
+ <ascription>#{bank_description}</ascription>
78
+ <iban>#{bank_iban}</iban>
79
+ <biccode>#{bank_biccode}</biccode>
80
+ <country>#{bank_country}</country>
81
+ </bank>
82
+ </banks>
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,30 @@
1
+ module Twinfield
2
+ module Create
3
+ class Error < StandardError
4
+ attr_accessor :object
5
+
6
+ def initialize message, object:
7
+ super(message)
8
+ self.object = object
9
+ end
10
+ end
11
+
12
+ class Finalized < Error
13
+ attr_accessor :object
14
+
15
+ def initialize message, object:
16
+ super
17
+ self.object = object
18
+ end
19
+ end
20
+
21
+ class EmptyInvoice < Error
22
+ attr_accessor :object
23
+
24
+ def initialize message, object:
25
+ super
26
+ self.object = object
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,39 @@
1
+ module Twinfield
2
+ module Create
3
+ class GeneralLedger
4
+ attr_accessor :name, :code
5
+
6
+ def initialize(hash = {})
7
+ hash.each { |k, v| send(:"#{k}=", CGI.escapeHTML(v)) }
8
+ end
9
+
10
+ def save
11
+ response = Twinfield::Api::Process.request do
12
+ %(
13
+ <dimension>
14
+ <office>#{Twinfield.configuration.company}</office>
15
+ <type>PNL</type>
16
+ <name>#{name}</name>
17
+ <code>#{code}</code>
18
+ </dimension>
19
+ )
20
+ end
21
+
22
+ xml = Nokogiri::XML(response.body[:process_xml_string_response][:process_xml_string_result])
23
+
24
+ if xml.at_css("dimension").attributes["result"].value == "1"
25
+ {
26
+ code: code,
27
+ status: 1
28
+ }
29
+ else
30
+ {
31
+ code: code,
32
+ status: 0,
33
+ messages: xml.css("[msg]").map { |x| x.attributes["msg"].value }
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,97 @@
1
+ module Twinfield
2
+ module Create
3
+ # Create a transaction
4
+ #
5
+ # In Twinfield there is a clear distinction between sales invoices and sales transactions. Sales invoices are the invoices, which will be sent to customers. Sales transactions are the related financial transactions that will be posted in the accounting part of Twinfield.
6
+ #
7
+ class Transaction
8
+ attr_writer :destiny, :autobalancevat, :raisewarning
9
+ attr_accessor :code, :number, :currency, :date, :duedate, :invoicenumber, :lines
10
+
11
+ def destiny
12
+ @destiny || "temporary"
13
+ end
14
+
15
+ def raisewarning
16
+ @raisewarning || false
17
+ end
18
+
19
+ def autobalancevat
20
+ @autobalancevat || true
21
+ end
22
+
23
+ def initialize(hash = {})
24
+ # Escape all the things.
25
+ hash.each do |k, v|
26
+ val = if v.is_a?(String)
27
+ CGI.escapeHTML(v)
28
+ elsif v.is_a?(Hash)
29
+ v.each_with_object({}) { |(k1, v1), h|
30
+ h[k1] = CGI.escapeHTML(v1)
31
+ }
32
+ else
33
+ v
34
+ end
35
+
36
+ send(:"#{k}=", val)
37
+ end
38
+ end
39
+
40
+ def generate_lines
41
+ xml_lines = lines.map do |line|
42
+ %(
43
+ <line type="#{line[:type]}" id="#{line[:id]}" >
44
+ <dim1>#{line[:dim1]}</dim1>
45
+ <dim2>#{line[:dim2]}</dim2>
46
+ <dim3>#{line[:dim3]}</dim3>
47
+ <value>#{line[:value]}</value>
48
+ #{"<vatvalue>#{line[:vatvalue]}</vatvalue>" if line[:vatvalue]}
49
+ <debitcredit>#{line[:debitcredit]}</debitcredit>
50
+ <description>#{CGI.escapeHTML(line[:description]) if line[:description]}</description>
51
+ #{"<vatcode>#{line[:vatcode]}</vatcode>" if line[:vatcode]}
52
+ </line>
53
+ )
54
+ end
55
+
56
+ xml_lines.join("")
57
+ end
58
+
59
+ def save
60
+ response = Twinfield::Api::Process.request do
61
+ %(
62
+ <transaction destiny="#{destiny}" raisewarning="#{raisewarning}" autobalancevat="#{autobalancevat}">
63
+ <header>
64
+ <code>#{code}</code>
65
+ #{"<number>#{number}</number>" if number}
66
+ <currency>#{currency}</currency>
67
+ <date>#{date.strftime("%Y%m%d")}</date>
68
+ <duedate>#{duedate.strftime("%Y%m%d")}</duedate>
69
+ <invoicenumber>#{invoicenumber}</invoicenumber>
70
+ <office>#{Twinfield.configuration.company}</office>
71
+ </header>
72
+ <lines>
73
+ #{generate_lines}
74
+ </lines>
75
+ </transaction>
76
+ )
77
+ end
78
+
79
+ xml = Nokogiri::XML(response.body[:process_xml_string_response][:process_xml_string_result])
80
+
81
+ if xml.at_css("transaction").attributes["result"].value == "1"
82
+ {
83
+ code: invoicenumber,
84
+ status: 1,
85
+ twinfield_number: xml.at_css("number").content
86
+ }
87
+ else
88
+ {
89
+ code: invoicenumber,
90
+ status: 0,
91
+ messages: xml.css("[msg]").map { |x| x.attributes["msg"].value }
92
+ }
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end