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.
- 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,409 @@
|
|
1
|
+
module Twinfield
|
2
|
+
class SalesInvoice < Twinfield::AbstractModel
|
3
|
+
extend Twinfield::Helpers::Parsers
|
4
|
+
|
5
|
+
class Financials
|
6
|
+
attr_accessor :code, :number
|
7
|
+
|
8
|
+
def initialize(code:, number:)
|
9
|
+
@code = code
|
10
|
+
@number = number
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
{
|
15
|
+
code: code,
|
16
|
+
number: number
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class VatLine
|
22
|
+
attr_accessor :vatcode, :vatvalue, :performancetype, :performancedate, :vatname, :invoice
|
23
|
+
|
24
|
+
def initialize(vatcode:, vatvalue:, performancetype:, performancedate:, vatname:)
|
25
|
+
@vatcode = vatcode
|
26
|
+
@vatvalue = vatvalue
|
27
|
+
@performancetype = performancetype
|
28
|
+
@performancedate = performancedate
|
29
|
+
@vatname = vatname
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_h
|
33
|
+
{
|
34
|
+
vatcode: vatcode,
|
35
|
+
vatvalue: vatvalue,
|
36
|
+
performancetype: performancetype,
|
37
|
+
performancedate: performancedate,
|
38
|
+
vatname: vatname
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Line < Twinfield::AbstractModel
|
44
|
+
attr_accessor :id, :article, :subarticle, :quantity, :units, :allowdiscountorpremium, :description, :unitspriceexcl, :unitspriceinc, :freetext1, :freetext2, :freetext3, :dim1, :vatcode, :performancetype, :performancedate, :valueexcl, :vatvalue, :valueinc, :invoice
|
45
|
+
|
46
|
+
def initialize(id: nil, article: "-", subarticle: nil, quantity: 1, units: nil, allowdiscountorpremium: true, description: nil, unitspriceexcl: nil, unitspriceinc: nil, freetext1: nil, freetext2: nil, freetext3: nil, dim1: nil, vatcode: nil, performancetype: nil, performancedate: nil, valueinc: nil, vatvalue: nil, valueexcl: nil)
|
47
|
+
@id = id
|
48
|
+
@article = article # article "-" is an article-less article in Twinfield
|
49
|
+
@subarticle = subarticle if subarticle.to_s != ""
|
50
|
+
@quantity = Float(quantity) unless article == "-"
|
51
|
+
@units = Integer(units) if units && units != ""
|
52
|
+
@allowdiscountorpremium = allowdiscountorpremium unless article == "-"
|
53
|
+
@description = description
|
54
|
+
@unitspriceexcl = Float(unitspriceexcl) if unitspriceexcl
|
55
|
+
@unitspriceinc = Float(unitspriceinc) if unitspriceinc
|
56
|
+
@freetext1 = freetext1 if freetext1.to_s != ""
|
57
|
+
@freetext2 = freetext2 if freetext2.to_s != ""
|
58
|
+
@freetext3 = freetext3 if freetext3.to_s != ""
|
59
|
+
@dim1 = dim1 if dim1.to_s != ""
|
60
|
+
@vatcode = vatcode if vatcode.to_s != ""
|
61
|
+
@performancetype = performancetype if performancetype.to_s != ""
|
62
|
+
@performancedate = performancedate if performancedate.to_s != ""
|
63
|
+
@valueinc = valueinc
|
64
|
+
@vatvalue = vatvalue
|
65
|
+
@valueexcl = valueexcl
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_xml(lineid = id)
|
69
|
+
Nokogiri::XML::Builder.new do |xml|
|
70
|
+
xml.line(id: lineid) {
|
71
|
+
xml.article article
|
72
|
+
xml.subarticle subarticle if subarticle
|
73
|
+
xml.quantity quantity if quantity
|
74
|
+
xml.units units if units
|
75
|
+
xml.allowdiscountorpremium allowdiscountorpremium if allowdiscountorpremium
|
76
|
+
xml.description description if description
|
77
|
+
xml.unitspriceexcl unitspriceexcl if unitspriceexcl
|
78
|
+
xml.unitspriceinc unitspriceinc if unitspriceinc
|
79
|
+
xml.freetext1 freetext1 if freetext1
|
80
|
+
xml.freetext2 freetext2 if freetext2
|
81
|
+
xml.freetext3 freetext3 if freetext3
|
82
|
+
xml.dim1 dim1 if dim1
|
83
|
+
xml.vatcode vatcode if vatcode
|
84
|
+
xml.performancetype performancetype if performancetype
|
85
|
+
xml.performancedate performancedate.strftime("%Y%m%d") if performancedate
|
86
|
+
}
|
87
|
+
end.doc.root.to_xml
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_h
|
91
|
+
{
|
92
|
+
id: id,
|
93
|
+
article: article,
|
94
|
+
subarticle: subarticle,
|
95
|
+
quantity: quantity,
|
96
|
+
units: units,
|
97
|
+
allowdiscountorpremium: allowdiscountorpremium,
|
98
|
+
description: description,
|
99
|
+
unitspriceexcl: unitspriceexcl,
|
100
|
+
unitspriceinc: unitspriceinc,
|
101
|
+
freetext1: freetext1,
|
102
|
+
freetext2: freetext2,
|
103
|
+
freetext3: freetext3,
|
104
|
+
dim1: dim1,
|
105
|
+
vatcode: vatcode,
|
106
|
+
performancetype: performancetype,
|
107
|
+
performancedate: performancedate,
|
108
|
+
valueexcl: valueexcl,
|
109
|
+
valueinc: valueinc,
|
110
|
+
vatvalue: vatvalue
|
111
|
+
}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class << self
|
116
|
+
def find(invoicenumber, invoicetype:)
|
117
|
+
options = {office: Twinfield.configuration.company, code: invoicetype, invoicenumber: invoicenumber}
|
118
|
+
|
119
|
+
invoice_xml = Twinfield::Api::Process.read(:salesinvoice, options)
|
120
|
+
|
121
|
+
invoice = Twinfield::SalesInvoice.new(
|
122
|
+
office: invoice_xml.css("header office").text,
|
123
|
+
invoicetype: invoice_xml.css("header invoicetype").text,
|
124
|
+
invoicedate: parse_date(invoice_xml.css("header invoicedate").text),
|
125
|
+
duedate: parse_date(invoice_xml.css("header duedate").text),
|
126
|
+
bank: invoice_xml.css("header bank").text,
|
127
|
+
deliveraddressnumber: invoice_xml.css("header deliveraddressnumber").text&.to_i,
|
128
|
+
invoiceaddressnumber: invoice_xml.css("header invoiceaddressnumber").text&.to_i,
|
129
|
+
customer: invoice_xml.css("header customer").text,
|
130
|
+
period: invoice_xml.css("header period").text,
|
131
|
+
currency: invoice_xml.css("header currency").text,
|
132
|
+
status: invoice_xml.css("header status").text,
|
133
|
+
paymentmethod: invoice_xml.css("header paymentmethod").text,
|
134
|
+
headertext: invoice_xml.css("header headertext").text,
|
135
|
+
footertext: invoice_xml.css("header footertext").text
|
136
|
+
)
|
137
|
+
|
138
|
+
invoice.invoicenumber = invoice_xml.css("header invoicenumber").text
|
139
|
+
|
140
|
+
return nil if invoice.invoicenumber.strip != invoicenumber.to_s
|
141
|
+
|
142
|
+
invoice.financials = Financials.new(code: invoice_xml.css("financials code").text, number: invoice_xml.css("financials number").text)
|
143
|
+
|
144
|
+
invoice_xml.css("lines line").each do |xml_line|
|
145
|
+
line = Line.new(
|
146
|
+
id: xml_line.attributes["id"].text,
|
147
|
+
article: xml_line.css("article").text,
|
148
|
+
subarticle: xml_line.css("subarticle").text,
|
149
|
+
quantity: parse_float(xml_line.css("quantity").text),
|
150
|
+
units: xml_line.css("units").text,
|
151
|
+
allowdiscountorpremium: xml_line.css("allowdiscountorpremium").text,
|
152
|
+
description: xml_line.css("description").text,
|
153
|
+
unitspriceexcl: parse_float(xml_line.css("unitspriceexcl").text),
|
154
|
+
freetext1: xml_line.css("freetext1").text,
|
155
|
+
freetext2: xml_line.css("freetext2").text,
|
156
|
+
freetext3: xml_line.css("freetext3").text,
|
157
|
+
dim1: xml_line.css("dim1").text,
|
158
|
+
vatcode: xml_line.css("vatcode").text
|
159
|
+
)
|
160
|
+
line.valueexcl = (xml_line.css("valueexcl").text == "") ? nil : xml_line.css("valueexcl").text.to_f
|
161
|
+
line.vatvalue = (xml_line.css("vatvalue").text == "") ? nil : xml_line.css("vatvalue").text.to_f
|
162
|
+
line.valueinc = (xml_line.css("valueinc").text == "") ? nil : xml_line.css("valueinc").text.to_f
|
163
|
+
invoice.lines << line
|
164
|
+
end
|
165
|
+
|
166
|
+
invoice_xml.css("vatlines vatline").each do |xml_line|
|
167
|
+
line = VatLine.new(
|
168
|
+
vatcode: xml_line.css("vatcode").text,
|
169
|
+
vatname: xml_line.css("vatcode")[0].attributes["name"].text,
|
170
|
+
vatvalue: parse_float(xml_line.css("vatvalue").text),
|
171
|
+
performancetype: xml_line.css("performancetype").text,
|
172
|
+
performancedate: xml_line.css("performancedate").text
|
173
|
+
)
|
174
|
+
|
175
|
+
invoice.vat_lines << line
|
176
|
+
end
|
177
|
+
|
178
|
+
invoice
|
179
|
+
end
|
180
|
+
|
181
|
+
def search(options = {})
|
182
|
+
Twinfield::Api::Process.request(:process_xml_string) do
|
183
|
+
%(
|
184
|
+
<columns code="000">
|
185
|
+
<sort>
|
186
|
+
<field>fin.trs.head.code</field>
|
187
|
+
</sort>
|
188
|
+
<column>
|
189
|
+
<field>fin.trs.head.yearperiod</field>
|
190
|
+
<label>Period</label>
|
191
|
+
<visible>true</visible>
|
192
|
+
<ask>true</ask>
|
193
|
+
<operator>between</operator>
|
194
|
+
<from>2021/01</from>
|
195
|
+
<to>2021/12</to>
|
196
|
+
</column>
|
197
|
+
<column>
|
198
|
+
<field>fin.trs.head.code</field>
|
199
|
+
<label>Transaction type</label>
|
200
|
+
<visible>true</visible>
|
201
|
+
</column>
|
202
|
+
<column>
|
203
|
+
<field>fin.trs.head.shortname</field>
|
204
|
+
<label>Name</label>
|
205
|
+
<visible>true</visible>
|
206
|
+
</column>
|
207
|
+
<column>
|
208
|
+
<field>fin.trs.head.number</field>
|
209
|
+
<label>Trans. no.</label>
|
210
|
+
<visible>true</visible>
|
211
|
+
</column>
|
212
|
+
<column>
|
213
|
+
<field>fin.trs.line.dim1</field>
|
214
|
+
<label>General ledger</label>
|
215
|
+
<visible>true</visible>
|
216
|
+
<ask>true</ask>
|
217
|
+
<operator>between</operator>
|
218
|
+
<from>1300</from>
|
219
|
+
<to>1300</to>
|
220
|
+
</column>
|
221
|
+
<column>
|
222
|
+
<field>fin.trs.head.curcode</field>
|
223
|
+
<label>Currency</label>
|
224
|
+
<visible>true</visible>
|
225
|
+
</column>
|
226
|
+
<column>
|
227
|
+
<field>fin.trs.line.valuesigned</field>
|
228
|
+
<label>Value</label>
|
229
|
+
<visible>true</visible>
|
230
|
+
</column>
|
231
|
+
<column>
|
232
|
+
<field>fin.trs.line.description</field>
|
233
|
+
<label>Description</label>
|
234
|
+
<visible>true</visible>
|
235
|
+
</column>
|
236
|
+
|
237
|
+
</columns>
|
238
|
+
)
|
239
|
+
# <column>
|
240
|
+
# <field>fin.trs.line.dim2</field>
|
241
|
+
# <label>Debtor</label><visible>true</visible><from>#{code}</from><to>#{code}</to><operator>between</operator>
|
242
|
+
# </column>
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
attr_accessor :invoicetype, :invoicedate, :duedate, :performancedate, :bank, :invoiceaddressnumber, :deliveraddressnumber, :customer_code, :period, :currency, :status, :paymentmethod, :headertext, :footertext, :lines, :office, :invoicenumber, :vatvalue, :valueinc, :financials, :vat_lines
|
248
|
+
|
249
|
+
def initialize(invoicetype:, duedate: nil, invoicedate: nil, performancedate: nil, bank: nil, invoiceaddressnumber: nil, deliveraddressnumber: nil, customer: nil, customer_code: nil, period: nil, currency: nil, status: "concept", paymentmethod: nil, headertext: nil, footertext: nil, office: nil, invoicenumber: nil, financials: {}, lines: [], vat_lines: [])
|
250
|
+
self.lines = lines.collect { |a| SalesInvoice::Line.new(**a.to_h) }
|
251
|
+
self.vat_lines = vat_lines.collect { |a| SalesInvoice::VatLine.new(**a.to_h) }
|
252
|
+
self.financials = SalesInvoice::Financials.new(**financials.to_h) if financials && financials != {}
|
253
|
+
@invoicetype = invoicetype
|
254
|
+
@invoicedate = invoicedate.is_a?(String) ? Date.parse(invoicedate) : invoicedate
|
255
|
+
@duedate = duedate.is_a?(String) ? Date.parse(duedate) : duedate
|
256
|
+
@performancedate = performancedate.is_a?(String) ? Date.parse(performancedate) : performancedate
|
257
|
+
@bank = bank
|
258
|
+
@invoiceaddressnumber = invoiceaddressnumber
|
259
|
+
@deliveraddressnumber = deliveraddressnumber
|
260
|
+
self.customer = customer || customer_code
|
261
|
+
raise ArgumentError.new("missing keyword: :customer or :customer_code") unless self.customer_code
|
262
|
+
@period = period
|
263
|
+
@currency = currency
|
264
|
+
@status = status
|
265
|
+
@paymentmethod = paymentmethod
|
266
|
+
@headertext = headertext
|
267
|
+
@footertext = footertext
|
268
|
+
@office = office || Twinfield.configuration.company
|
269
|
+
@invoicenumber = invoicenumber
|
270
|
+
end
|
271
|
+
|
272
|
+
def associated_lines
|
273
|
+
lines.map { |a|
|
274
|
+
a.invoice = self
|
275
|
+
a
|
276
|
+
}
|
277
|
+
end
|
278
|
+
|
279
|
+
def raisewarning
|
280
|
+
@raisewarning || false
|
281
|
+
end
|
282
|
+
|
283
|
+
def autobalancevat
|
284
|
+
@autobalancevat || true
|
285
|
+
end
|
286
|
+
|
287
|
+
def generate_lines
|
288
|
+
line_id = 0
|
289
|
+
lines.map do |line|
|
290
|
+
line_id += 1
|
291
|
+
line.to_xml(line_id)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def final?
|
296
|
+
["final", :final].include? status
|
297
|
+
end
|
298
|
+
|
299
|
+
def customer= customer
|
300
|
+
if customer.is_a?(String) || customer.is_a?(Numeric)
|
301
|
+
@customer_code = customer.to_i
|
302
|
+
elsif customer.is_a? Twinfield::Customer
|
303
|
+
@customer_code = customer.code
|
304
|
+
@customer = customer
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def customer
|
309
|
+
@customer ||= Twinfield::Customer.find(customer_code)
|
310
|
+
end
|
311
|
+
|
312
|
+
def deliver_address
|
313
|
+
customer.addresses[deliveraddressnumber - 1] if deliveraddressnumber
|
314
|
+
end
|
315
|
+
|
316
|
+
def invoice_address
|
317
|
+
customer.addresses[invoiceaddressnumber - 1] if invoiceaddressnumber
|
318
|
+
end
|
319
|
+
|
320
|
+
def transaction
|
321
|
+
@transaction ||= financials&.number ? Twinfield::Browse::Transaction::Customer.find(number: financials.number, code: financials.code) : nil
|
322
|
+
end
|
323
|
+
|
324
|
+
# helper method to calculate a total price
|
325
|
+
def totalinc
|
326
|
+
lines.map(&:valueinc).compact.sum
|
327
|
+
end
|
328
|
+
|
329
|
+
alias_method :total, :totalinc
|
330
|
+
|
331
|
+
# helper method to calculate a total excl price
|
332
|
+
def totalexcl
|
333
|
+
lines.map(&:valueexcl).compact.sum
|
334
|
+
end
|
335
|
+
|
336
|
+
def to_xml
|
337
|
+
Nokogiri::XML::Builder.new do |xml|
|
338
|
+
xml.salesinvoice(raisewarning: raisewarning, autobalancevat: autobalancevat) {
|
339
|
+
xml.header do
|
340
|
+
xml.office office
|
341
|
+
xml.invoicenumber invoicenumber if invoicenumber
|
342
|
+
xml.invoicetype invoicetype
|
343
|
+
xml.invoicedate invoicedate&.strftime("%Y%m%d")
|
344
|
+
xml.duedate duedate&.strftime("%Y%m%d")
|
345
|
+
xml.performancedate performancedate.strftime("%Y%m%d") if performancedate
|
346
|
+
xml.bank bank
|
347
|
+
xml.invoiceaddressnumber invoiceaddressnumber if invoiceaddressnumber
|
348
|
+
xml.deliveraddressnumber deliveraddressnumber if deliveraddressnumber
|
349
|
+
xml.customer customer_code
|
350
|
+
xml.period period if period
|
351
|
+
xml.currency currency
|
352
|
+
xml.status status
|
353
|
+
xml.paymentmethod paymentmethod
|
354
|
+
xml.headertext headertext
|
355
|
+
xml.footertext footertext
|
356
|
+
end
|
357
|
+
xml.lines do
|
358
|
+
generate_lines.each do |line|
|
359
|
+
xml << line
|
360
|
+
end
|
361
|
+
end
|
362
|
+
}
|
363
|
+
end.doc.root.to_xml
|
364
|
+
end
|
365
|
+
|
366
|
+
def to_h
|
367
|
+
{
|
368
|
+
lines: lines.collect(&:to_h),
|
369
|
+
vat_lines: vat_lines.collect(&:to_h),
|
370
|
+
financials: financials&.to_h,
|
371
|
+
invoicetype: invoicetype,
|
372
|
+
invoicedate: invoicedate,
|
373
|
+
duedate: duedate,
|
374
|
+
performancedate: performancedate,
|
375
|
+
bank: bank,
|
376
|
+
invoiceaddressnumber: invoiceaddressnumber,
|
377
|
+
deliveraddressnumber: deliveraddressnumber,
|
378
|
+
customer_code: customer_code,
|
379
|
+
period: period,
|
380
|
+
currency: currency,
|
381
|
+
status: status,
|
382
|
+
paymentmethod: paymentmethod,
|
383
|
+
headertext: headertext,
|
384
|
+
footertext: footertext,
|
385
|
+
office: office || Twinfield.configuration.company,
|
386
|
+
invoicenumber: invoicenumber
|
387
|
+
}
|
388
|
+
end
|
389
|
+
|
390
|
+
def save
|
391
|
+
response = Twinfield::Api::Process.request do
|
392
|
+
to_xml
|
393
|
+
end
|
394
|
+
|
395
|
+
xml = Nokogiri::XML(response.body[:process_xml_string_response][:process_xml_string_result])
|
396
|
+
|
397
|
+
if xml.at_css("salesinvoice").attributes["result"].value == "1"
|
398
|
+
self.invoicenumber = xml.at_css("invoicenumber").content
|
399
|
+
self
|
400
|
+
elsif xml.css("header status").text == "final"
|
401
|
+
raise Twinfield::Create::Finalized.new(xml.css("[msg]").map { |x| x.attributes["msg"].value }.join(" "), object: self)
|
402
|
+
elsif lines.count == 0
|
403
|
+
raise Twinfield::Create::EmptyInvoice.new(xml.css("[msg]").map { |x| x.attributes["msg"].value }.join(" "), object: self)
|
404
|
+
else
|
405
|
+
raise Twinfield::Create::Error.new(xml.css("[msg]").map { |x| x.attributes["msg"].value }.join(" "), object: self)
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Twinfield
|
2
|
+
class Transaction < Twinfield::AbstractModel
|
3
|
+
extend Twinfield::Helpers::Parsers
|
4
|
+
include Twinfield::Helpers::TransactionMatch
|
5
|
+
|
6
|
+
class Line
|
7
|
+
attr_accessor :dim1, :dim2, :value, :debitcredit, :description, :invoicenumber, :vatcode, :type
|
8
|
+
|
9
|
+
def initialize(dim1: nil, dim2: nil, value: nil, debitcredit: nil, description: nil, invoicenumber: nil, vatcode: nil, customer_code: nil, balance_code: nil, type: :detail)
|
10
|
+
self.dim1 = dim1 || balance_code
|
11
|
+
self.dim2 = dim2 || customer_code
|
12
|
+
self.value = value.to_f
|
13
|
+
self.debitcredit = debitcredit
|
14
|
+
self.description = description
|
15
|
+
self.invoicenumber = invoicenumber
|
16
|
+
self.vatcode = vatcode
|
17
|
+
self.type = type.to_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
def balance_code
|
21
|
+
dim1
|
22
|
+
end
|
23
|
+
|
24
|
+
def customer_code
|
25
|
+
dim2
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_xml
|
29
|
+
Nokogiri::XML::Builder.new do |xml|
|
30
|
+
xml.line(type: type) {
|
31
|
+
xml.dim1 dim1 if dim1
|
32
|
+
xml.dim2 dim2 if dim2
|
33
|
+
xml.value value if value
|
34
|
+
xml.description description if description && type != :total
|
35
|
+
xml.debitcredit debitcredit if debitcredit
|
36
|
+
xml.invoicenumber invoicenumber if invoicenumber
|
37
|
+
xml.vatcode vatcode if vatcode
|
38
|
+
xml.currencydate Date.today.strftime("%Y%m%d")
|
39
|
+
}
|
40
|
+
end.doc.root.to_xml
|
41
|
+
end
|
42
|
+
|
43
|
+
def detail?
|
44
|
+
type == :detail
|
45
|
+
end
|
46
|
+
|
47
|
+
def total?
|
48
|
+
type == :total
|
49
|
+
end
|
50
|
+
|
51
|
+
def credit?
|
52
|
+
debitcredit == :credit
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_accessor :office, :code, :currency, :date, :period, :lines, :destiny, :number
|
57
|
+
|
58
|
+
def initialize(code:, office: nil, currency: "EUR", date: Date.today, period: nil, destiny: :final, lines: [], number: nil)
|
59
|
+
self.office = office || Twinfield.configuration.company
|
60
|
+
self.code = code
|
61
|
+
self.currency = currency
|
62
|
+
self.date = date
|
63
|
+
self.period = period || "#{date.year}/#{"%02d" % date.month}"
|
64
|
+
self.lines = lines.collect { |a| a.is_a?(Line) ? a : Line.new(**a) }
|
65
|
+
self.destiny = destiny
|
66
|
+
self.number = number
|
67
|
+
end
|
68
|
+
|
69
|
+
def value
|
70
|
+
0.0 - lines.select { |l| l.credit? && l.detail? }.map(&:value).sum
|
71
|
+
end
|
72
|
+
|
73
|
+
def save
|
74
|
+
response = Twinfield::Api::Process.request do
|
75
|
+
to_xml
|
76
|
+
end
|
77
|
+
|
78
|
+
xml = Nokogiri::XML(response.body[:process_xml_string_response][:process_xml_string_result])
|
79
|
+
|
80
|
+
if xml.at_css("transaction").attributes["result"].value == "1"
|
81
|
+
self.number = xml.at_css("header number").content
|
82
|
+
self
|
83
|
+
else
|
84
|
+
raise Twinfield::Create::Error.new(xml.css("[msg]").map { |x| x.attributes["msg"].value }.join(" "), object: self)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_xml
|
89
|
+
Nokogiri::XML::Builder.new do |xml|
|
90
|
+
xml.transaction(destiny: "final") do
|
91
|
+
xml.header do
|
92
|
+
xml.office office
|
93
|
+
xml.code code
|
94
|
+
xml.number number if number
|
95
|
+
xml.currency currency
|
96
|
+
xml.date date.strftime("%Y%m%d")
|
97
|
+
xml.period period
|
98
|
+
end
|
99
|
+
xml.lines do
|
100
|
+
lines.select(&:total?).each do |line|
|
101
|
+
xml << line.to_xml
|
102
|
+
end
|
103
|
+
|
104
|
+
lines.select(&:detail?).each do |line|
|
105
|
+
xml << line.to_xml
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end.doc.root.to_xml
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/twinfield.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require "savon"
|
2
|
+
|
3
|
+
module Twinfield
|
4
|
+
WSDLS = {
|
5
|
+
:session => File.join(__dir__, "..", "wsdls", "session.wsdl"),
|
6
|
+
"accounting" => {
|
7
|
+
session: File.join(__dir__, "..", "wsdls", "session.wsdl"),
|
8
|
+
process: File.join(__dir__, "..", "wsdls", "accounting", "process.wsdl"),
|
9
|
+
finder: File.join(__dir__, "..", "wsdls", "accounting", "finder.wsdl")
|
10
|
+
},
|
11
|
+
"accounting2" => {
|
12
|
+
process: File.join(__dir__, "..", "wsdls", "accounting2", "process.wsdl"),
|
13
|
+
finder: File.join(__dir__, "..", "wsdls", "accounting2", "finder.wsdl")
|
14
|
+
},
|
15
|
+
"api.accounting" => {
|
16
|
+
process: File.join(__dir__, "..", "wsdls", "api.accounting", "process.wsdl"),
|
17
|
+
finder: File.join(__dir__, "..", "wsdls", "api.accounting", "finder.wsdl")
|
18
|
+
}
|
19
|
+
|
20
|
+
}
|
21
|
+
|
22
|
+
ERRORS = {
|
23
|
+
100 => "Unexpected exception.",
|
24
|
+
101 => "Session header is missing.",
|
25
|
+
102 => "Access is denied.",
|
26
|
+
103 => "The log-on credentials are not valid anymore.",
|
27
|
+
104 => "The log-on has been deleted.",
|
28
|
+
105 => "The log-on has been disabled.",
|
29
|
+
106 => "The organisation is no longer active.",
|
30
|
+
107 => "SMS failed to send.",
|
31
|
+
108 => "Access to this server is not allowed because the cluster is invalid.",
|
32
|
+
109 => "You need access to at least one company to log on.",
|
33
|
+
110 => "Login is not allowed on this server"
|
34
|
+
}
|
35
|
+
|
36
|
+
class << self
|
37
|
+
# Holds the configuration for easy access to settings
|
38
|
+
attr_accessor :configuration
|
39
|
+
|
40
|
+
# Configures gem options
|
41
|
+
def configure
|
42
|
+
self.configuration ||= Twinfield::Configuration.new
|
43
|
+
reset_sessions!
|
44
|
+
yield(configuration)
|
45
|
+
end
|
46
|
+
|
47
|
+
def reset_sessions!
|
48
|
+
Twinfield::Api::Process.session = nil
|
49
|
+
Twinfield::Api::Finder.session = nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Helpers
|
55
|
+
require "twinfield/helpers/parsers"
|
56
|
+
require "twinfield/helpers/transaction_match"
|
57
|
+
|
58
|
+
require "twinfield/configuration"
|
59
|
+
require "twinfield/abstract_model"
|
60
|
+
require "twinfield/version"
|
61
|
+
|
62
|
+
# API Helpers
|
63
|
+
require "twinfield/api/base_api"
|
64
|
+
|
65
|
+
require "twinfield/api/o_auth_session"
|
66
|
+
require "twinfield/api/session"
|
67
|
+
require "twinfield/api/process"
|
68
|
+
require "twinfield/api/finder"
|
69
|
+
|
70
|
+
# New style models
|
71
|
+
require "twinfield/browse/transaction/customer"
|
72
|
+
require "twinfield/browse/transaction/cost_center"
|
73
|
+
require "twinfield/browse/transaction/general_ledger"
|
74
|
+
require "twinfield/sales_invoice"
|
75
|
+
require "twinfield/transaction"
|
76
|
+
require "twinfield/customer"
|
77
|
+
|
78
|
+
# Create services (old style)
|
79
|
+
require "twinfield/create/cost_center"
|
80
|
+
require "twinfield/create/general_ledger"
|
81
|
+
require "twinfield/create/debtor"
|
82
|
+
require "twinfield/create/creditor"
|
83
|
+
require "twinfield/create/error"
|
84
|
+
require "twinfield/create/transaction"
|
85
|
+
|
86
|
+
# Requests services (old style)
|
87
|
+
require "twinfield/request/find"
|
88
|
+
require "twinfield/request/list"
|
89
|
+
require "twinfield/request/read"
|
data/script/boot.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "twinfield"
|
5
|
+
require File.expand_path("../config", __FILE__)
|
6
|
+
#
|
7
|
+
# @session = Twinfield::Api::Session.new
|
8
|
+
# @session.logon
|
9
|
+
#
|
10
|
+
# @process = Twinfield::Api::Process.new(@session.session_id, @session.cluster)
|
11
|
+
#
|
12
|
+
# Twinfield::Request::List.browsefields
|
13
|
+
#
|
14
|
+
# @dimensions = Twinfield::Request::List.dimensions( { dimtype: "DEB", office: Twinfield.configuration.company } )
|
15
|
+
#
|
16
|
+
@invoice = Twinfield::SalesInvoice.new
|
17
|
+
|
18
|
+
# Mandatory parameters:
|
19
|
+
@invoice.code = "VRK"
|
20
|
+
@invoice.currency = "EUR"
|
21
|
+
@invoice.date = DateTime.now
|
22
|
+
@invoice.duedate = DateTime.now + 30
|
23
|
+
@invoice.invoicenumber = 110021
|
24
|
+
|
25
|
+
# Obligatory parameters:
|
26
|
+
@invoice.number = 201500016
|
27
|
+
@invoice.destiny = "final"
|
28
|
+
@invoice.raisewarning = false
|
29
|
+
@invoice.autobalancevat = true
|
30
|
+
|
31
|
+
@invoice.lines = [
|
32
|
+
{
|
33
|
+
type: "total",
|
34
|
+
id: 1,
|
35
|
+
dim1: 1300,
|
36
|
+
dim2: "D10000881",
|
37
|
+
value: 1000000,
|
38
|
+
debitcredit: "debit",
|
39
|
+
description: "Totaal regel"
|
40
|
+
},
|
41
|
+
{
|
42
|
+
# Mandatory parameters:
|
43
|
+
type: "detail",
|
44
|
+
id: 2,
|
45
|
+
dim1: 8000,
|
46
|
+
dim2: "A0000",
|
47
|
+
dim3: "M0000",
|
48
|
+
value: 1000000,
|
49
|
+
debitcredit: "credit",
|
50
|
+
description: "Rekening betaald",
|
51
|
+
|
52
|
+
# Obligatory parameters:
|
53
|
+
vatvalue: "0",
|
54
|
+
vatcode: "VL"
|
55
|
+
}
|
56
|
+
]
|
57
|
+
# @result = @invoice.save
|
58
|
+
# puts @result
|
data/script/console
ADDED