twinfieldrb 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/codeql-analysis.yml +70 -0
- data/.github/workflows/rspec.yml +33 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +4 -0
- data/README.md +120 -0
- data/Rakefile +1 -0
- data/lib/twinfield/abstract_model.rb +7 -0
- data/lib/twinfield/api/base_api.rb +50 -0
- data/lib/twinfield/api/finder.rb +45 -0
- data/lib/twinfield/api/o_auth_session.rb +58 -0
- data/lib/twinfield/api/process.rb +44 -0
- data/lib/twinfield/api/session.rb +170 -0
- data/lib/twinfield/browse/transaction/cost_center.rb +145 -0
- data/lib/twinfield/browse/transaction/customer.rb +413 -0
- data/lib/twinfield/browse/transaction/general_ledger.rb +144 -0
- data/lib/twinfield/configuration.rb +49 -0
- data/lib/twinfield/create/cost_center.rb +39 -0
- data/lib/twinfield/create/creditor.rb +88 -0
- data/lib/twinfield/create/debtor.rb +88 -0
- data/lib/twinfield/create/error.rb +30 -0
- data/lib/twinfield/create/general_ledger.rb +39 -0
- data/lib/twinfield/create/transaction.rb +97 -0
- data/lib/twinfield/customer.rb +612 -0
- data/lib/twinfield/helpers/parsers.rb +23 -0
- data/lib/twinfield/helpers/transaction_match.rb +40 -0
- data/lib/twinfield/request/find.rb +149 -0
- data/lib/twinfield/request/list.rb +66 -0
- data/lib/twinfield/request/read.rb +111 -0
- data/lib/twinfield/sales_invoice.rb +409 -0
- data/lib/twinfield/transaction.rb +112 -0
- data/lib/twinfield/version.rb +5 -0
- data/lib/twinfield.rb +89 -0
- data/script/boot.rb +58 -0
- data/script/console +2 -0
- data/spec/fixtures/cluster/finder/ivt.xml +1 -0
- data/spec/fixtures/cluster/processxml/columns/sales_transactions.xml +312 -0
- data/spec/fixtures/cluster/processxml/customer/create_success.xml +100 -0
- data/spec/fixtures/cluster/processxml/customer/read_success.xml +93 -0
- data/spec/fixtures/cluster/processxml/customer/update_success.xml +1 -0
- data/spec/fixtures/cluster/processxml/invoice/create_error.xml +8 -0
- data/spec/fixtures/cluster/processxml/invoice/create_final_error.xml +67 -0
- data/spec/fixtures/cluster/processxml/invoice/create_success.xml +64 -0
- data/spec/fixtures/cluster/processxml/invoice/read_not_found.xml +1 -0
- data/spec/fixtures/cluster/processxml/invoice/read_success.xml +106 -0
- data/spec/fixtures/cluster/processxml/read/deb.xml +12 -0
- data/spec/fixtures/cluster/processxml/response.xml +8 -0
- data/spec/fixtures/login/session/wsdl.xml +210 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/stubs/finder_stubs.rb +19 -0
- data/spec/stubs/processxml_stubs.rb +41 -0
- data/spec/stubs/session_stubs.rb +28 -0
- data/spec/twinfield/api/oauth_session_spec.rb +37 -0
- data/spec/twinfield/api/process_spec.rb +7 -0
- data/spec/twinfield/browse/transaction/cost_center_spec.rb +60 -0
- data/spec/twinfield/browse/transaction/general_ledger_spec.rb +60 -0
- data/spec/twinfield/browse/transaction/transaction_spec.rb +72 -0
- data/spec/twinfield/config_spec.rb +60 -0
- data/spec/twinfield/customer_spec.rb +326 -0
- data/spec/twinfield/request/find_spec.rb +24 -0
- data/spec/twinfield/request/list_spec.rb +58 -0
- data/spec/twinfield/request/read_spec.rb +26 -0
- data/spec/twinfield/sales_invoice_spec.rb +253 -0
- data/spec/twinfield/session_spec.rb +77 -0
- data/spec/twinfield/transaction_spec.rb +149 -0
- data/twinfieldrb.gemspec +24 -0
- data/wsdls/accounting/finder.wsdl +157 -0
- data/wsdls/accounting/process.wsdl +199 -0
- data/wsdls/accounting/session.wsdl +452 -0
- data/wsdls/accounting2/finder.wsdl +157 -0
- data/wsdls/accounting2/process.wsdl +199 -0
- data/wsdls/api.accounting/finder.wsdl +157 -0
- data/wsdls/api.accounting/process.wsdl +199 -0
- data/wsdls/session.wsdl +210 -0
- data/wsdls/update +10 -0
- 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
|