hexapdf-extras 1.0.1 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +1 -1
- data/lib/hexapdf/extras/layout/swiss_qr_bill.rb +656 -0
- data/lib/hexapdf/extras/layout.rb +1 -0
- data/lib/hexapdf/extras/version.rb +1 -1
- data/lib/hexapdf/extras.rb +30 -0
- data/test/hexapdf/extras/layout/test_qr_code_box.rb +11 -7
- data/test/hexapdf/extras/layout/test_swiss_qr_bill.rb +233 -0
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e7fe626cf7fd851cd203b739c69bdd193b5debffcd129c656d59b51c2d326ad
|
4
|
+
data.tar.gz: 588a1a744477c437035ca66fd187f4c3563b135946efb8a21d5f73e77f15d238
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb195eef642905d2797b96c545f63eaee7edff117982e16e8c3965d05c9abdf537331d35cfa09df6d9aebac3d03e4a28dbb921714bc3a4968984789f2f4548ea
|
7
|
+
data.tar.gz: 8682207937162bef5c86ca5f0f061dbb25469b989e65fa51dc4fbe7f3bc75739cc382911efee4c954e30a14650322bd870aa308e56436a1e18a665e0f4174a9d
|
data/Rakefile
CHANGED
@@ -27,7 +27,7 @@ task publish_files: [:package] do
|
|
27
27
|
end
|
28
28
|
|
29
29
|
task :test_all do
|
30
|
-
versions = `rbenv versions --bare | grep -i 2.[
|
30
|
+
versions = `rbenv versions --bare | grep -i 2.[7]\\\\\\|3.`.split("\n")
|
31
31
|
versions.each do |version|
|
32
32
|
sh "eval \"$(rbenv init -)\"; rbenv shell #{version} && ruby -v && rake test"
|
33
33
|
end
|
@@ -0,0 +1,656 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hexapdf/error'
|
4
|
+
require 'hexapdf/layout/box'
|
5
|
+
require 'hexapdf/extras/layout'
|
6
|
+
|
7
|
+
module HexaPDF::Extras::Layout::NumericMeasurementHelper #:nodoc:
|
8
|
+
refine Numeric do
|
9
|
+
def mm
|
10
|
+
self * 72 / 25.4
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
using HexaPDF::Extras::Layout::NumericMeasurementHelper
|
16
|
+
|
17
|
+
module HexaPDF
|
18
|
+
module Extras
|
19
|
+
module Layout
|
20
|
+
|
21
|
+
# Displays a Swiss QR-bill.
|
22
|
+
#
|
23
|
+
# This class implements version 2.2 of the Swiss QR-bill specification but takes into account
|
24
|
+
# version 2.3 where appropriate.
|
25
|
+
#
|
26
|
+
#
|
27
|
+
# == Requirements
|
28
|
+
#
|
29
|
+
# * Liberation Sans TrueType font installed in standard Linux path (or changed via the
|
30
|
+
# configuration option, see next section)
|
31
|
+
# * Rubygem +rqrcode_core+ for generating the QR code
|
32
|
+
#
|
33
|
+
#
|
34
|
+
# == Configuration option 'layout.swiss_qr_bill'
|
35
|
+
#
|
36
|
+
# The configuration option 'layout.swiss_qr_bill' is a hash containing styling information for
|
37
|
+
# the various text parts: section heading, payment heading/value, receipt heading/value and
|
38
|
+
# the alternative procedures. The default values are taking from the QR-bill style guide.
|
39
|
+
#
|
40
|
+
# The keys of this hash are strings of the form 'part.subpart.property' where
|
41
|
+
#
|
42
|
+
# * 'part' can be 'section', 'payment', 'receipt', or 'alternative_procedures',
|
43
|
+
# * 'subpart' can be 'heading' or 'value' (the latter not for 'section'),
|
44
|
+
# * 'property' can be 'font', 'font_size' or 'line_height' (the latter not for 'section').
|
45
|
+
#
|
46
|
+
# The default font is Liberation Sans which is one of the four allowed fonts (the others being
|
47
|
+
# Arial, Frutiger, and Helvetica). The font files themselves are *not* included and the 'font'
|
48
|
+
# property, by default, references the standard Linux path where the fonts would be found.
|
49
|
+
# Note that all '*.heading.font' values should reference the bold version of the font whereas
|
50
|
+
# the '*.value.font' values should reference the regular version.
|
51
|
+
#
|
52
|
+
#
|
53
|
+
# == Data Structure
|
54
|
+
#
|
55
|
+
# All the necessary information for generating the Swiss QR-bill is provided on
|
56
|
+
# initialization. The following keys can be used:
|
57
|
+
#
|
58
|
+
# :lang::
|
59
|
+
# The language to use for the literal text strings appearing in the QR-bill. One of :en,
|
60
|
+
# :de, :fr or :it.
|
61
|
+
#
|
62
|
+
# Defaults to :en if not specified.
|
63
|
+
#
|
64
|
+
# :creditor::
|
65
|
+
# (required) The creditor of the transaction. This is a hash that can contain the
|
66
|
+
# following elements:
|
67
|
+
#
|
68
|
+
# :iban::
|
69
|
+
# (required) The IBAN of the creditor (21 characters, no spaces, only IBANs for CH or
|
70
|
+
# LI). Note that the IBAN is not checked for validity.
|
71
|
+
#
|
72
|
+
# :name::
|
73
|
+
# (required) The name of the creditor (maximum 70 characters).
|
74
|
+
#
|
75
|
+
# :address_type::
|
76
|
+
# (required) The type of address, either :structured or :combined. Defaults to
|
77
|
+
# :structured which is the only choice in version 2.3 of the specification.
|
78
|
+
#
|
79
|
+
# :address_line1::
|
80
|
+
# The first line of the creditor's address (maximum 70 characters). In case of a
|
81
|
+
# structured address, this is the street. Otherwise this has to be the street and
|
82
|
+
# building number together.
|
83
|
+
#
|
84
|
+
# :address_line2::
|
85
|
+
# The second line of the creditor's address. In case of a structured address, this
|
86
|
+
# has to be the building number (maximum 16 characters). Otherwise it has to be the
|
87
|
+
# postal code and town (maximum 70 characters).
|
88
|
+
#
|
89
|
+
# :postal_code::
|
90
|
+
# The postal code of the creditor's address (maximum 16 characters, only for
|
91
|
+
# structured addresses).
|
92
|
+
#
|
93
|
+
# :town::
|
94
|
+
# The town from the creditor's address (maximum 35 characters, only for structured
|
95
|
+
# addresses).
|
96
|
+
#
|
97
|
+
# :country::
|
98
|
+
# (required) The country from the creditor's address (ISO 3166-1 two-letter country
|
99
|
+
# code).
|
100
|
+
#
|
101
|
+
# :debtor::
|
102
|
+
# The debtor information for the transaction. This information is optional but if used
|
103
|
+
# some elements are required. The value is a hash that can contain the same elements as
|
104
|
+
# the +:creditor+ key with the exception of the +:iban+.
|
105
|
+
#
|
106
|
+
# :amount::
|
107
|
+
# The payment amount (between 0.01 and 999,999,999.99). If not filled in, a blank field is
|
108
|
+
# shown for adding the amount by hand later. If the amount is set to zero, it means that
|
109
|
+
# the QR-bill should be used as notification and the :message is set according to the
|
110
|
+
# specification.
|
111
|
+
#
|
112
|
+
# :currency::
|
113
|
+
# (required) The payment currency (either CHF or EUR).
|
114
|
+
#
|
115
|
+
# :reference_type::
|
116
|
+
# The payment reference type (either QRR, SCOR or NON). Defaults to NON.
|
117
|
+
#
|
118
|
+
# :reference::
|
119
|
+
# The structured reference data. All whitespace is removed before processing.
|
120
|
+
#
|
121
|
+
# In case of a QRR reference, the value has to be a 26 digit reference without check digit
|
122
|
+
# or a 27 digit reference with check digit. The check digit is validated.
|
123
|
+
#
|
124
|
+
# In case of a SCOR reference, the value has to contain between 5 and 25 alpha-numeric
|
125
|
+
# characters. The check digits are validated.
|
126
|
+
#
|
127
|
+
# :message::
|
128
|
+
# Additional, unstructured information (maximum 140 characters).
|
129
|
+
#
|
130
|
+
# :billing_information::
|
131
|
+
# Billing information for automated booking of the payment (maximum 140 characters).
|
132
|
+
#
|
133
|
+
# :alternative_schemes::
|
134
|
+
# Alternative schemes parameters. Either a single string with a maximum of 100 characters
|
135
|
+
# or an array of two such strings.
|
136
|
+
#
|
137
|
+
# == Example
|
138
|
+
#
|
139
|
+
# HexaPDF::Composer.create("sample-qr-bill.pdf", margin: 0) do |composer|
|
140
|
+
# data = {
|
141
|
+
# lang: :de,
|
142
|
+
# creditor: {
|
143
|
+
# iban: "CH44 3199 9123 0008 8901 2",
|
144
|
+
# name: "Max Muster & Söhne",
|
145
|
+
# address_line1: "Musterstrasse",
|
146
|
+
# address_line2: "123",
|
147
|
+
# postal_code: "8000",
|
148
|
+
# town: "Seldwyla",
|
149
|
+
# country: "CH",
|
150
|
+
# },
|
151
|
+
# debtor: {
|
152
|
+
# address_type: :combined,
|
153
|
+
# name: "Simon Muster",
|
154
|
+
# address_line1: "Musterstrasse 1",
|
155
|
+
# address_line2: "8000 Seldwyla",
|
156
|
+
# country: "CH"
|
157
|
+
# },
|
158
|
+
# amount: 2500.25,
|
159
|
+
# currency: 'CHF',
|
160
|
+
# }
|
161
|
+
# composer.swiss_qr_bill(data: data, valign: :bottom)
|
162
|
+
# end
|
163
|
+
#
|
164
|
+
# == References
|
165
|
+
#
|
166
|
+
# * Website https://www.six-group.com/en/products-services/banking-services/billing-and-payments/qr-bill.html
|
167
|
+
# * 2.2 Specification https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/ig-qr-bill-v2.2-en.pdf
|
168
|
+
# * 2.3 Specification https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/ig-qr-bill-v2.3-en.pdf
|
169
|
+
# * Style guide https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/style-guide-qr-bill-en.pdf
|
170
|
+
class SwissQRBill < HexaPDF::Layout::Box
|
171
|
+
|
172
|
+
# Thrown when an error occurs when working with the SwissQRBill class.
|
173
|
+
class Error < HexaPDF::Error
|
174
|
+
end
|
175
|
+
|
176
|
+
# Mapping of the English text literals to their German, French and Italian counterparts,
|
177
|
+
# taken from Annex C of the specification.
|
178
|
+
TEXT_LITERALS = { #:nodoc:
|
179
|
+
de: {
|
180
|
+
'Payment part' => 'Zahlteil',
|
181
|
+
'Receipt' => 'Empfangsschein',
|
182
|
+
'Account / Payable to' => 'Konto / Zahlbar an',
|
183
|
+
'Reference' => 'Referenz',
|
184
|
+
'Additional information' => 'Zusätzliche Informationen',
|
185
|
+
'Payable by' => 'Zahlbar durch',
|
186
|
+
'Payable by (name/address)' => 'Zahlbar durch (Name/Adresse)',
|
187
|
+
'Currency' => 'Währung',
|
188
|
+
'Amount' => 'Betrag',
|
189
|
+
'Acceptance point' => 'Annahmestelle',
|
190
|
+
'In favour of' => 'Zugunsten',
|
191
|
+
'DO NOT USE FOR PAYMENT' => 'NICHT ZUR ZAHLUNG VERWENDEN',
|
192
|
+
},
|
193
|
+
fr: {
|
194
|
+
'Payment part' => 'Section paiement',
|
195
|
+
'Receipt' => 'Récépissé',
|
196
|
+
'Account / Payable to' => 'Compte / Payable à',
|
197
|
+
'Reference' => 'Référence',
|
198
|
+
'Additional information' => 'Information supplémentaires',
|
199
|
+
'Payable by' => 'Payable par',
|
200
|
+
'Payable by (name/address)' => 'Payable par (nom/address)',
|
201
|
+
'Currency' => 'Monnaie',
|
202
|
+
'Amount' => 'Montant',
|
203
|
+
'Acceptance point' => 'Point de dépôt',
|
204
|
+
'In favour of' => 'En faveur de',
|
205
|
+
'DO NOT USE FOR PAYMENT' => 'NE PAS UTILISER POUR LE PAIEMENT',
|
206
|
+
},
|
207
|
+
it: {
|
208
|
+
'Payment part' => 'Sezione pagamento',
|
209
|
+
'Receipt' => 'Ricevuta',
|
210
|
+
'Account / Payable to' => 'Conto / Pagabile a',
|
211
|
+
'Reference' => 'Riferimento',
|
212
|
+
'Additional information' => 'Informazioni supplementari',
|
213
|
+
'Payable by' => 'Pagabile da',
|
214
|
+
'Payable by (name/address)' => 'Pagabile da (nome/indirizzo)',
|
215
|
+
'Currency' => 'Value date ',
|
216
|
+
'Amount' => 'Importo',
|
217
|
+
'Acceptance point' => 'Punto di accettazione',
|
218
|
+
'In favour of' => 'A favore di',
|
219
|
+
'DO NOT USE FOR PAYMENT' => 'NON UTILIZZARE PER IL PAGAMENTO',
|
220
|
+
}
|
221
|
+
}
|
222
|
+
|
223
|
+
# The payment data - see the SwissQRBill class documentation for details.
|
224
|
+
attr_reader :data
|
225
|
+
|
226
|
+
# Creates a new SwissQRBill object for the given payment +data+ (see the class documentation
|
227
|
+
# for details).
|
228
|
+
#
|
229
|
+
# If the arguments +width+ and +height+ are provided, they are ignored since the QR-bill has
|
230
|
+
# a fixed size of 210mm x 105mm.
|
231
|
+
def initialize(data:, **kwargs)
|
232
|
+
super(**kwargs, width: 210.mm, height: 105.mm)
|
233
|
+
validate_data(data)
|
234
|
+
end
|
235
|
+
|
236
|
+
private
|
237
|
+
|
238
|
+
QRR_MODULO10_TABLE = [ #:nodoc:
|
239
|
+
[0, 9, 4, 6, 8, 2, 7, 1, 3, 5],
|
240
|
+
[9, 4, 6, 8, 2, 7, 1, 3, 5, 0],
|
241
|
+
[4, 6, 8, 2, 7, 1, 3, 5, 0, 9],
|
242
|
+
[6, 8, 2, 7, 1, 3, 5, 0, 9, 4],
|
243
|
+
[8, 2, 7, 1, 3, 5, 0, 9, 4, 6],
|
244
|
+
[2, 7, 1, 3, 5, 0, 9, 4, 6, 8],
|
245
|
+
[7, 1, 3, 5, 0, 9, 4, 6, 8, 2],
|
246
|
+
[1, 3, 5, 0, 9, 4, 6, 8, 2, 7],
|
247
|
+
[3, 5, 0, 9, 4, 6, 8, 2, 7, 1],
|
248
|
+
[5, 0, 9, 4, 6, 8, 2, 7, 1, 3]
|
249
|
+
].freeze
|
250
|
+
|
251
|
+
# Validates the given data and raises an error if the data is not valid
|
252
|
+
def validate_data(data)
|
253
|
+
@data = data
|
254
|
+
|
255
|
+
if @data[:currency] != 'EUR' && @data[:currency] != 'CHF'
|
256
|
+
raise Error, "Data field :currency must be EUR or CHR, not #{@data[:currency].inspect}"
|
257
|
+
end
|
258
|
+
|
259
|
+
if @data[:amount] == 0
|
260
|
+
@data[:message] = text('DO NOT USE FOR PAYMENT')
|
261
|
+
elsif @data[:amount] && (@data[:amount] < 0.01 || @data[:amount] > 999_999_999.99)
|
262
|
+
raise Error, "Data field :amount must be between 0.01 and 999_999_999.99"
|
263
|
+
end
|
264
|
+
|
265
|
+
validate_address = lambda do |hash|
|
266
|
+
hash[:address_type] ||= :structured
|
267
|
+
if hash[:address_type] != :structured && hash[:address_type] != :combined
|
268
|
+
raise Error, "Address type must be :structured or :combined, not #{hash[:address_type]}"
|
269
|
+
end
|
270
|
+
structured = (hash[:address_type] == :structured)
|
271
|
+
|
272
|
+
if (value = hash[:name]) && value.size > 70
|
273
|
+
raise Error, "Name in addresss must not contain more than 70 characters"
|
274
|
+
elsif !value
|
275
|
+
raise Error, "Name in address must be provided"
|
276
|
+
end
|
277
|
+
|
278
|
+
if hash[:address_line1] && hash[:address_line1].size > 70
|
279
|
+
raise Error, "Address line 1 must not contain more than 70 characters"
|
280
|
+
end
|
281
|
+
|
282
|
+
if (value = hash[:address_line2])
|
283
|
+
if structured && value.size > 16
|
284
|
+
raise Error, "Address line 2 of a structured address must not contain " \
|
285
|
+
"more than 16 characters"
|
286
|
+
elsif value.size > 70
|
287
|
+
raise Error, "Address line 2 of a combined address must not contain " \
|
288
|
+
"more than 70 characters"
|
289
|
+
end
|
290
|
+
elsif !structured
|
291
|
+
raise Error, "Address line 2 must be provided for a combined address"
|
292
|
+
end
|
293
|
+
|
294
|
+
if (value = hash[:postal_code])
|
295
|
+
if !structured
|
296
|
+
raise Error, "Postal code must not be provided for a combined address"
|
297
|
+
elsif value.size > 16
|
298
|
+
raise Error, "Postal code must not contain more than 16 characters"
|
299
|
+
end
|
300
|
+
elsif structured
|
301
|
+
raise Error, "Postal code must be provided for a structured address"
|
302
|
+
end
|
303
|
+
|
304
|
+
if (value = hash[:town])
|
305
|
+
if !structured
|
306
|
+
raise Error, "Town must not be provided for a combined address"
|
307
|
+
elsif value.size > 35
|
308
|
+
raise Error, "Town must not contain more than 35 characters"
|
309
|
+
end
|
310
|
+
elsif structured
|
311
|
+
raise Error, "Town must be provided for a structured address"
|
312
|
+
end
|
313
|
+
|
314
|
+
if (value = hash[:country]) && value.size != 2
|
315
|
+
raise Error, "Country must be a two-letter ISO-3166-1 code"
|
316
|
+
elsif !value
|
317
|
+
raise Error, "Country must be provided"
|
318
|
+
end
|
319
|
+
end
|
320
|
+
validate_address.call(@data[:creditor])
|
321
|
+
validate_address.call(@data[:debtor]) if @data[:debtor]
|
322
|
+
|
323
|
+
@data[:reference_type] ||= "NON"
|
324
|
+
case @data[:reference_type]
|
325
|
+
when "QRR"
|
326
|
+
value = @data[:reference]
|
327
|
+
unless value
|
328
|
+
raise Error, "Data field :reference must be filled in for QRR reference type"
|
329
|
+
end
|
330
|
+
value.gsub!(/\s*/, '')
|
331
|
+
if value !~ /\A\d{26,27}\z/
|
332
|
+
raise Error, "Data field :reference for QRR must contain 26 or 27 digits"
|
333
|
+
end
|
334
|
+
result = value[0, 26].each_codepoint.inject(0) do |memo, codepoint|
|
335
|
+
QRR_MODULO10_TABLE[memo][codepoint - 48]
|
336
|
+
end
|
337
|
+
check_digit = (10 - result) % 10
|
338
|
+
value << check_digit.to_s if value.size == 26
|
339
|
+
if value[26].to_i != check_digit
|
340
|
+
raise Error, "Data field :reference for QRR contains an invalid check digit, " \
|
341
|
+
"should be #{check_digit}"
|
342
|
+
end
|
343
|
+
when "SCOR"
|
344
|
+
value = @data[:reference]
|
345
|
+
unless value
|
346
|
+
raise Error, "Data field :reference must be filled in for SCOR reference type"
|
347
|
+
end
|
348
|
+
value.gsub!(/\s*/, '')
|
349
|
+
if value !~ /\A\w{5,25}\z/
|
350
|
+
raise Error, "Data field :reference for SCOR must contain between 5 and 25 " \
|
351
|
+
"alpha-numeric characters"
|
352
|
+
elsif value !~ /\ARF\d\d/
|
353
|
+
raise Error, "Data field :reference for SCOR must start with RF and check digits"
|
354
|
+
end
|
355
|
+
# See https://www.mobilefish.com/services/creditor_reference/creditor_reference.php
|
356
|
+
result = "#{value[4..-1]}#{value[0, 4]}".upcase.gsub(/[A-Z]/) {|c| c.ord - 55 }.to_i % 97
|
357
|
+
unless result == 1
|
358
|
+
raise Error, "Data field :reference for SCOR has invalid check digits"
|
359
|
+
end
|
360
|
+
when "NON"
|
361
|
+
if @data[:reference]
|
362
|
+
raise Error, "Data field :reference must not be provided for NON reference type"
|
363
|
+
end
|
364
|
+
else
|
365
|
+
raise Error, "Data field :reference_type must be one of QRR, SCOR or NON"
|
366
|
+
end
|
367
|
+
|
368
|
+
if @data[:message] && @data[:message].size > 140
|
369
|
+
raise Error, "Data field :message must not contain more than 140 characters"
|
370
|
+
end
|
371
|
+
if @data[:billing_information] && @data[:billing_information].size > 140
|
372
|
+
raise Error, "Data field :billing_information must not contain more than 140 characters"
|
373
|
+
end
|
374
|
+
if (@data[:message].to_s + @data[:billing_information].to_s).size > 140
|
375
|
+
raise Error, "Data fields :message and :billing_information together must not " \
|
376
|
+
"contain more than 140 characters"
|
377
|
+
end
|
378
|
+
|
379
|
+
@data[:alternative_schemes] = Array(@data[:alternative_schemes])
|
380
|
+
if @data[:alternative_schemes].any? {|as| as.size > 100 } ||
|
381
|
+
@data[:alternative_schemes].size > 2
|
382
|
+
raise Error, "Data field :alternative_schemes must at most contain 2 strings with " \
|
383
|
+
"not more than 100 characters each"
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
# Draws the SwissQRBill onto the canvas at position [x, y].
|
388
|
+
def draw_content(canvas, x, y)
|
389
|
+
layout = canvas.context.document.layout
|
390
|
+
frame = HexaPDF::Layout::Frame.new(0, 0, width, height, context: canvas.context)
|
391
|
+
box_fitter = HexaPDF::Layout::BoxFitter.new([frame])
|
392
|
+
styles = set_up_styles(canvas.context.document.config['layout.swiss_qr_bill'], layout)
|
393
|
+
|
394
|
+
box_fitter.fit(receipt(layout, styles))
|
395
|
+
box_fitter.fit(payment(layout, styles, qr_code_cross(canvas.context.document)))
|
396
|
+
unless box_fitter.fit_successful?
|
397
|
+
raise HexaPDF::Error, "The Swiss QR-bill could not be fit"
|
398
|
+
end
|
399
|
+
|
400
|
+
canvas.translate(x, y) do
|
401
|
+
box_fitter.fit_results.each {|fit_result| fit_result.draw(canvas)}
|
402
|
+
canvas.stroke_color(0).line_width(0.5).line_dash_pattern(2).
|
403
|
+
line(62.mm, 0, 62.mm, 105.mm).
|
404
|
+
line(0, 105.mm, 210.mm, 105.mm).stroke
|
405
|
+
canvas.font('ZapfDingbats', size: 15).text("✂", at: [5.mm, 103.1.mm])
|
406
|
+
canvas.font('ZapfDingbats', size: 15).text_matrix(0, -1, 1, 0, 60.175.mm, 100.mm).text("✂")
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
# Returns a hash with styles that are used throughout the QR-bill creation for consistency.
|
411
|
+
def set_up_styles(config, layout)
|
412
|
+
{
|
413
|
+
section_heading: {font_size: config['section.heading.font_size'],
|
414
|
+
font: config['section.heading.font']},
|
415
|
+
payment_heading: {font_size: config['payment.heading.font_size'],
|
416
|
+
font: config['payment.heading.font'],
|
417
|
+
line_height: config['payment.heading.line_height'],
|
418
|
+
line_spacing: {type: :fixed, value: config['payment.heading.line_height']}},
|
419
|
+
payment_value: {font_size: config['payment.value.font_size'],
|
420
|
+
font: config['payment.value.font'],
|
421
|
+
line_height: config['payment.value.line_height'],
|
422
|
+
line_spacing: {type: :fixed, value: config['payment.value.line_height']},
|
423
|
+
padding: [0, 0, config['payment.value.line_height']]},
|
424
|
+
receipt_heading: {font_size: config['receipt.heading.font_size'],
|
425
|
+
font: config['receipt.heading.font'],
|
426
|
+
line_height: config['receipt.heading.line_height'],
|
427
|
+
line_spacing: {type: :fixed, value: config['receipt.heading.line_height']}},
|
428
|
+
receipt_value: {font_size: config['receipt.value.font_size'],
|
429
|
+
font: config['receipt.value.font'],
|
430
|
+
line_height: config['receipt.value.line_height'],
|
431
|
+
line_spacing: {type: :fixed, value: config['receipt.value.line_height']},
|
432
|
+
padding: [0, 0, config['receipt.value.line_height']]},
|
433
|
+
alternative_procedures_heading: {
|
434
|
+
font_size: config['alternative_procedures.heading.font_size'],
|
435
|
+
font: config['alternative_procedures.heading.font'],
|
436
|
+
line_height: config['alternative_procedures.heading.line_height'],
|
437
|
+
line_spacing: {type: :fixed, value: config['alternative_procedures.heading.line_height']}
|
438
|
+
},
|
439
|
+
alternative_procedures_value: {
|
440
|
+
font_size: config['alternative_procedures.value.font_size'],
|
441
|
+
font: config['alternative_procedures.value.font'],
|
442
|
+
line_height: config['alternative_procedures.value.line_height'],
|
443
|
+
line_spacing: {type: :fixed, value: config['alternative_procedures.value.line_height']},
|
444
|
+
padding: [0, 0, config['alternative_procedures.value.line_height']]
|
445
|
+
},
|
446
|
+
}.transform_values! {|value| layout.style(:base).dup.update(**value) }
|
447
|
+
end
|
448
|
+
|
449
|
+
# Returns a box containing the receipt part of the QR-bill.
|
450
|
+
def receipt(layout, styles)
|
451
|
+
layout.container(width: 62.mm, style: {padding: 5.mm, mask_mode: :fill_vertical}) do |col|
|
452
|
+
col.text(text('Receipt'), height: 7.mm, style: styles[:section_heading])
|
453
|
+
col.container(height: 56.mm) do |info|
|
454
|
+
info.text(text('Account / Payable to'), style: styles[:receipt_heading])
|
455
|
+
info.text("#{@data[:creditor][:iban]}\n#{address(@data[:creditor])}", style: styles[:receipt_value])
|
456
|
+
|
457
|
+
if @data[:reference_type] != 'NON'
|
458
|
+
info.text(text('Reference'), style: styles[:receipt_heading])
|
459
|
+
info.text(@data[:reference], style: styles[:receipt_value])
|
460
|
+
end
|
461
|
+
|
462
|
+
if @data[:debtor]
|
463
|
+
info.text(text('Payable by'), style: styles[:receipt_heading])
|
464
|
+
info.text(address(@data[:debtor]), style: styles[:receipt_value])
|
465
|
+
else
|
466
|
+
info.text(text('Payable by (name/address)'), style: styles[:receipt_heading])
|
467
|
+
blank_field(info, 52.mm, 20.mm)
|
468
|
+
end
|
469
|
+
end
|
470
|
+
receipt_amount(col, styles)
|
471
|
+
col.text(text('Acceptance point'), style: styles[:receipt_heading], text_align: :right)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
# Adds the appropriate boxes to the given composer for the amount part of the receipt
|
476
|
+
# section.
|
477
|
+
def receipt_amount(composer, styles)
|
478
|
+
composer.container(height: 14.mm) do |amount|
|
479
|
+
if @data[:amount]
|
480
|
+
amount.column(columns: [26.mm, -1], gaps: 0) do |inner|
|
481
|
+
inner.text(text('Currency'), style: styles[:receipt_heading], padding: [0, 0, 1.mm])
|
482
|
+
inner.text(@data[:currency], style: styles[:receipt_value])
|
483
|
+
inner.text(text('Amount'), style: styles[:receipt_heading], padding: [0, 0, 1.mm])
|
484
|
+
inner.text(formatted_amount, style: styles[:receipt_value])
|
485
|
+
end
|
486
|
+
else
|
487
|
+
amount.column(columns: [-1, 31.mm], gaps: 0) do |inner|
|
488
|
+
inner.text(text('Currency') + " " + text("Amount"),
|
489
|
+
style: styles[:receipt_heading], padding: [0, 0, 1.mm])
|
490
|
+
inner.text(@data[:currency], style: styles[:receipt_value], mask_mode: :fill)
|
491
|
+
inner.box(:base, height: 2)
|
492
|
+
blank_field(inner, 30.mm, 10.mm)
|
493
|
+
end
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
# Returns a box containing the payment part of the QR-bill.
|
499
|
+
def payment(layout, styles, cross)
|
500
|
+
layout.container(width: 148.mm, style: {padding: 5.mm}) do |col|
|
501
|
+
col.container(width: 51.mm, height: 85.mm, style: {mask_mode: :box}) do |left_col|
|
502
|
+
left_col.text(text('Payment part'), height: 7.mm, style: styles[:section_heading])
|
503
|
+
left_col.box(:qrcode, data: qr_code_data, level: :m, style: {padding: [5.mm, 5.mm, 5.mm, 0.mm]})
|
504
|
+
left_col.image(cross, width: 7.mm, position: [19.5.mm, 46.5.mm])
|
505
|
+
payment_amount(left_col, styles)
|
506
|
+
end
|
507
|
+
col.container(height: 85.mm) do |info|
|
508
|
+
info.text(text('Account / Payable to'), style: styles[:payment_heading])
|
509
|
+
info.text("#{@data[:creditor][:iban]}\n#{address(@data[:creditor])}", style: styles[:payment_value])
|
510
|
+
|
511
|
+
if @data[:reference_type] != 'NON'
|
512
|
+
info.text(text('Reference'), style: styles[:payment_heading])
|
513
|
+
info.text(@data[:reference], style: styles[:payment_value])
|
514
|
+
end
|
515
|
+
|
516
|
+
if @data[:message] || @data[:billing_information]
|
517
|
+
info.text(text('Additional information'), style: styles[:payment_heading])
|
518
|
+
info.text([@data[:message], @data[:billing_information]].compact.join("\n"), style: styles[:payment_value])
|
519
|
+
end
|
520
|
+
|
521
|
+
if @data[:debtor]
|
522
|
+
info.text(text('Payable by'), style: styles[:payment_heading])
|
523
|
+
info.text(address(@data[:debtor]), style: styles[:payment_value])
|
524
|
+
else
|
525
|
+
info.text(text('Payable by (name/address)'), style: styles[:payment_heading])
|
526
|
+
blank_field(info, 65.mm, 25.mm)
|
527
|
+
end
|
528
|
+
end
|
529
|
+
if @data[:alternative_schemes].size > 0
|
530
|
+
@data[:alternative_schemes].each do |as|
|
531
|
+
provider, data = *as.split('/', 2)
|
532
|
+
col.formatted_text([{text: provider, style: styles[:alternative_procedures_heading]},
|
533
|
+
{text: data, style: styles[:alternative_procedures_value]}])
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
# Adds the appropriate boxes to the given composer for the amount part of the payment
|
540
|
+
# section.
|
541
|
+
def payment_amount(composer, styles)
|
542
|
+
composer.container(height: 22.mm) do |amount|
|
543
|
+
if @data[:amount]
|
544
|
+
amount.column(columns: [23.mm, -1], gaps: 0) do |inner|
|
545
|
+
inner.text(text('Currency'), style: styles[:payment_heading], padding: [0, 0, 1.mm])
|
546
|
+
inner.text(@data[:currency], style: styles[:payment_value])
|
547
|
+
inner.text(text('Amount'), style: styles[:payment_heading], padding: [0, 0, 1.mm])
|
548
|
+
inner.text(formatted_amount, style: styles[:payment_value])
|
549
|
+
end
|
550
|
+
else
|
551
|
+
amount.text(text('Currency') + " " + text("Amount"),
|
552
|
+
style: styles[:payment_heading], padding: [0, 0, 1.mm])
|
553
|
+
amount.column(columns: [-1, 41.mm], gaps: 0) do |inner|
|
554
|
+
inner.text(@data[:currency], style: styles[:payment_value])
|
555
|
+
blank_field(inner, 40.mm, 15.mm)
|
556
|
+
end
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
# Returns the correctly localized text for the given string +str+.
|
562
|
+
def text(str)
|
563
|
+
TEXT_LITERALS.dig(@data[:lang], str) || str
|
564
|
+
end
|
565
|
+
|
566
|
+
# Returns a string containing the formatted address for output using the provided data.
|
567
|
+
def address(data)
|
568
|
+
result = +''
|
569
|
+
result << "#{data[:name]}\n"
|
570
|
+
if data[:address_type] == :structured
|
571
|
+
addr = [data[:address_line1], data[:address_line2]].compact
|
572
|
+
result << "#{addr.join(' ')}\n" unless addr.empty?
|
573
|
+
result << "#{data[:country]}-#{data[:postal_code]} #{data[:town]}"
|
574
|
+
else
|
575
|
+
result << "#{data[:address_line1]}\n" if data.key?(:address_line1)
|
576
|
+
result << "#{data[:country]}-#{data[:address_line2]}\n"
|
577
|
+
end
|
578
|
+
result
|
579
|
+
end
|
580
|
+
|
581
|
+
# Returns the amount formatted according to the specification.
|
582
|
+
def formatted_amount
|
583
|
+
a, b = format('%.2f', @data[:amount]).split('.')
|
584
|
+
a.gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1 ") << '.' << b
|
585
|
+
end
|
586
|
+
|
587
|
+
# Creates the content of the QR code using the information provided in #data.
|
588
|
+
def qr_code_data
|
589
|
+
qr_code_data = []
|
590
|
+
add_address_data = lambda do |hash|
|
591
|
+
qr_code_data << (hash[:address_type] == :structured ? "S" : "K")
|
592
|
+
qr_code_data << hash[:name] <<
|
593
|
+
hash[:address_line1].to_s << hash[:address_line2].to_s <<
|
594
|
+
hash[:postal_code].to_s << hash[:town].to_s << hash[:country]
|
595
|
+
end
|
596
|
+
|
597
|
+
# Header information
|
598
|
+
qr_code_data.concat(["SPC", "0200", "1"])
|
599
|
+
|
600
|
+
# Creditor information
|
601
|
+
qr_code_data << @data[:creditor][:iban]
|
602
|
+
add_address_data.call(@data[:creditor])
|
603
|
+
|
604
|
+
# Ultimate creditor information
|
605
|
+
qr_code_data.concat(["", "", "", "", "", "", ""])
|
606
|
+
|
607
|
+
# Amount information
|
608
|
+
qr_code_data << (@data[:amount] ? format('%.2f', @data[:amount]) : "") << @data[:currency]
|
609
|
+
|
610
|
+
# Debtor information
|
611
|
+
add_address_data.call(@data[:debtor]) if @data[:debtor]
|
612
|
+
|
613
|
+
# Payment reference
|
614
|
+
qr_code_data << @data[:reference_type] << @data[:reference].to_s <<
|
615
|
+
@data[:message].to_s << "EPD"
|
616
|
+
qr_code_data << @data[:billing_information] if @data[:billing_information]
|
617
|
+
|
618
|
+
qr_code_data.join("\r\n")
|
619
|
+
end
|
620
|
+
|
621
|
+
# Creates a Form XObject for the Swiss cross that is to be overlaid over the QR code.
|
622
|
+
#
|
623
|
+
# The measurements are reverse-engineered from the images provided at
|
624
|
+
# https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/swiss-cross-graphic-en.zip
|
625
|
+
def qr_code_cross(document)
|
626
|
+
cross = document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 7.mm, 7.mm]})
|
627
|
+
canvas = cross.canvas
|
628
|
+
canvas.fill_color(1.0).rectangle(0, 0, 7.mm, 7.mm).fill
|
629
|
+
canvas.fill_color(0.0).rectangle(0.5.mm, 0.5.mm, 6.mm, 6.mm).fill
|
630
|
+
canvas.fill_color(1.0).
|
631
|
+
rectangle(2.93.mm, 1.67.mm, 1.17.mm, 3.9.mm).
|
632
|
+
rectangle(1.57.mm, 3.04.mm, 3.9.mm, 1.17.mm).
|
633
|
+
fill
|
634
|
+
cross
|
635
|
+
end
|
636
|
+
|
637
|
+
CORNER_MARK_LENGTH = 3.mm # :nodoc:
|
638
|
+
|
639
|
+
# Creates a blank rectangle of the given +width+ and +height+, with corner marks as
|
640
|
+
# specified by the specification.
|
641
|
+
def blank_field(layout, width, height)
|
642
|
+
layout.box(width: width, height: height) do |canvas, box|
|
643
|
+
canvas.stroke_color(0).line_width(0.75).
|
644
|
+
polyline(0, CORNER_MARK_LENGTH, 0, 0, CORNER_MARK_LENGTH, 0).
|
645
|
+
polyline(width - CORNER_MARK_LENGTH, 0, width, 0, width, CORNER_MARK_LENGTH).
|
646
|
+
polyline(width, height - CORNER_MARK_LENGTH, width, height, width - CORNER_MARK_LENGTH, height).
|
647
|
+
polyline(CORNER_MARK_LENGTH, height, 0, height, 0, height - CORNER_MARK_LENGTH).
|
648
|
+
stroke
|
649
|
+
end
|
650
|
+
end
|
651
|
+
|
652
|
+
end
|
653
|
+
|
654
|
+
end
|
655
|
+
end
|
656
|
+
end
|
data/lib/hexapdf/extras.rb
CHANGED
@@ -14,3 +14,33 @@ HexaPDF::DefaultDocumentConfiguration['graphic_object.map'][:qrcode] =
|
|
14
14
|
|
15
15
|
HexaPDF::DefaultDocumentConfiguration['layout.boxes.map'][:qrcode] =
|
16
16
|
'HexaPDF::Extras::Layout::QRCodeBox'
|
17
|
+
|
18
|
+
HexaPDF::DefaultDocumentConfiguration['layout.boxes.map'][:swiss_qr_bill] =
|
19
|
+
'HexaPDF::Extras::Layout::SwissQRBill'
|
20
|
+
|
21
|
+
# These values comes from the "Style Guide QR-bill", available at
|
22
|
+
# https://www.six-group.com/dam/download/banking-services/standardization/qr-bill/style-guide-qr-bill-en.pdf
|
23
|
+
font = '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf'
|
24
|
+
font_bold = '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf'
|
25
|
+
HexaPDF::DefaultDocumentConfiguration['layout.swiss_qr_bill'] = {
|
26
|
+
'section.heading.font' => font_bold,
|
27
|
+
'section.heading.font_size' => 11,
|
28
|
+
'payment.heading.font' => font_bold,
|
29
|
+
'payment.heading.font_size' => 8,
|
30
|
+
'payment.heading.line_height' => 11,
|
31
|
+
'payment.value.font' => font,
|
32
|
+
'payment.value.font_size' => 10,
|
33
|
+
'payment.value.line_height' => 11,
|
34
|
+
'receipt.heading.font' => font_bold,
|
35
|
+
'receipt.heading.font_size' => 6,
|
36
|
+
'receipt.heading.line_height' => 9,
|
37
|
+
'receipt.value.font' => font,
|
38
|
+
'receipt.value.font_size' => 8,
|
39
|
+
'receipt.value.line_height' => 9,
|
40
|
+
'alternative_procedures.heading.font' => font_bold,
|
41
|
+
'alternative_procedures.heading.font_size' => 7,
|
42
|
+
'alternative_procedures.heading.line_height' => 8,
|
43
|
+
'alternative_procedures.value.font' => font,
|
44
|
+
'alternative_procedures.value.font_size' => 7,
|
45
|
+
'alternative_procedures.value.line_height' => 8,
|
46
|
+
}
|
@@ -7,6 +7,10 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
|
|
7
7
|
HexaPDF::Extras::Layout::QRCodeBox.new(**kwargs)
|
8
8
|
end
|
9
9
|
|
10
|
+
before do
|
11
|
+
@frame = HexaPDF::Layout::Frame.new(0, 0, 100, 100)
|
12
|
+
end
|
13
|
+
|
10
14
|
describe "initialize" do
|
11
15
|
it "can take the common box arguments" do
|
12
16
|
box = create_box(width: 10, height: 15)
|
@@ -25,7 +29,7 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
|
|
25
29
|
it "uses the smaller value of width/height for the dimensions if smaller than available_width/height" do
|
26
30
|
[{width: 10}, {width: 10, height: 50}, {height: 10}, {width: 50, height: 10}].each do |args|
|
27
31
|
box = create_box(**args)
|
28
|
-
assert(box.fit(100, 100,
|
32
|
+
assert(box.fit(100, 100, @frame))
|
29
33
|
assert_equal(10, box.width)
|
30
34
|
assert_equal(10, box.height)
|
31
35
|
assert_equal(10, box.qr_code.size)
|
@@ -34,12 +38,12 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
|
|
34
38
|
|
35
39
|
it "uses the smaller value of available_width/height for the dimensions" do
|
36
40
|
box = create_box
|
37
|
-
assert(box.fit(10, 20,
|
41
|
+
assert(box.fit(10, 20, @frame))
|
38
42
|
assert_equal(10, box.width)
|
39
43
|
assert_equal(10, box.height)
|
40
44
|
assert_equal(10, box.qr_code.size)
|
41
45
|
|
42
|
-
assert(box.fit(20, 15,
|
46
|
+
assert(box.fit(20, 15, @frame))
|
43
47
|
assert_equal(15, box.width)
|
44
48
|
assert_equal(15, box.height)
|
45
49
|
assert_equal(15, box.qr_code.size)
|
@@ -47,19 +51,19 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
|
|
47
51
|
|
48
52
|
it "takes the border and padding into account for the QR code size" do
|
49
53
|
box = create_box(style: {padding: [1, 2], border: {width: [3, 4]}})
|
50
|
-
assert(box.fit(100, 100,
|
54
|
+
assert(box.fit(100, 100, @frame))
|
51
55
|
assert_equal(88, box.qr_code.size)
|
52
56
|
assert_equal(100, box.width)
|
53
57
|
assert_equal(96, box.height)
|
54
58
|
|
55
59
|
box = create_box(style: {padding: [2, 1], border: {width: [4, 3]}})
|
56
|
-
assert(box.fit(100, 100,
|
60
|
+
assert(box.fit(100, 100, @frame))
|
57
61
|
assert_equal(88, box.qr_code.size)
|
58
62
|
assert_equal(96, box.width)
|
59
63
|
assert_equal(100, box.height)
|
60
64
|
|
61
65
|
box = create_box(style: {padding: [5, 5, 5, 0], border: {width: [2, 2, 2, 0]}})
|
62
|
-
assert(box.fit(50, 100,
|
66
|
+
assert(box.fit(50, 100, @frame))
|
63
67
|
assert_equal(43, box.qr_code.size)
|
64
68
|
assert_equal(50, box.width)
|
65
69
|
assert_equal(57, box.height)
|
@@ -69,7 +73,7 @@ describe HexaPDF::Extras::Layout::QRCodeBox do
|
|
69
73
|
describe "draw" do
|
70
74
|
it "draws the qrcode" do
|
71
75
|
box = create_box(width: 10)
|
72
|
-
box.fit(100, 100,
|
76
|
+
box.fit(100, 100, @frame)
|
73
77
|
|
74
78
|
canvas = Minitest::Mock.new
|
75
79
|
canvas.expect(:draw, nil, [box.qr_code])
|
@@ -0,0 +1,233 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'hexapdf'
|
3
|
+
require 'hexapdf/extras'
|
4
|
+
require 'hexapdf/extras/layout/swiss_qr_bill'
|
5
|
+
|
6
|
+
using HexaPDF::Extras::Layout::NumericMeasurementHelper
|
7
|
+
|
8
|
+
describe HexaPDF::Extras::Layout::SwissQRBill do
|
9
|
+
def create_box(data, **kwargs)
|
10
|
+
HexaPDF::Extras::Layout::SwissQRBill.new(data: data, **kwargs)
|
11
|
+
end
|
12
|
+
|
13
|
+
def data
|
14
|
+
@data ||= {
|
15
|
+
creditor: {
|
16
|
+
iban: "CH44 3199 9123 0008 8901 2",
|
17
|
+
name: "Max Muster & Söhne",
|
18
|
+
address_line1: "Musterstrasse",
|
19
|
+
address_line2: "123",
|
20
|
+
postal_code: "8000",
|
21
|
+
town: "Seldwyla",
|
22
|
+
country: "CH",
|
23
|
+
},
|
24
|
+
debtor: {
|
25
|
+
address_type: :combined,
|
26
|
+
name: "Simon Muster",
|
27
|
+
address_line1: "Musterstrasse 1",
|
28
|
+
address_line2: "8000 Seldwyla",
|
29
|
+
country: "CH"
|
30
|
+
},
|
31
|
+
lang: :de,
|
32
|
+
amount: 2500.25,
|
33
|
+
currency: 'CHF',
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
before do
|
38
|
+
@error_class = HexaPDF::Extras::Layout::SwissQRBill::Error
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "initialize" do
|
42
|
+
it "overrides any set width or height" do
|
43
|
+
box = create_box(data, width: 10, height: 15)
|
44
|
+
assert_equal(210.mm, box.width)
|
45
|
+
assert_equal(105.mm, box.height)
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "data validation" do
|
49
|
+
def assert_invalid_data(data = self.data, message)
|
50
|
+
err = assert_raises(@error_class) { create_box(data) }
|
51
|
+
assert_match(message, err.message)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "ensures a correct currency value" do
|
55
|
+
assert_equal('EUR', create_box(data.update(currency: 'EUR')).data[:currency])
|
56
|
+
assert_equal('CHF', create_box(data.update(currency: 'CHF')).data[:currency])
|
57
|
+
assert_invalid_data(data.update(currency: 'USD'), /field :currency/)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "ensure a correct amount value" do
|
61
|
+
assert_invalid_data(data.update(amount: 0.001), /field :amount/)
|
62
|
+
assert_invalid_data(data.update(amount: 1_000_000_000), /field :amount/)
|
63
|
+
data.update(amount: 0.00)
|
64
|
+
assert_equal('NICHT ZUR ZAHLUNG VERWENDEN', create_box(data).data[:message])
|
65
|
+
end
|
66
|
+
|
67
|
+
it "sets the address type to structured by default" do
|
68
|
+
assert_equal(:structured, create_box(data, width: 10, height: 15).data[:creditor][:address_type])
|
69
|
+
end
|
70
|
+
|
71
|
+
it "ensures a correct address type" do
|
72
|
+
data[:creditor].update(address_type: :invalid)
|
73
|
+
assert_invalid_data(/Address type must be/)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "ensures a name for an address exists and has a valid length" do
|
77
|
+
data[:creditor][:name] = 'a' * 71
|
78
|
+
assert_invalid_data(/Name in address.*70/)
|
79
|
+
data[:creditor].delete(:name)
|
80
|
+
assert_invalid_data(/Name in address must be provided/)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "ensures that address line 1 has a valid length" do
|
84
|
+
data[:creditor][:address_line1] = 'a' * 71
|
85
|
+
assert_invalid_data(/Address line 1.*70/)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "ensures a correct country code" do
|
89
|
+
data[:creditor][:country] = 'AMS'
|
90
|
+
assert_invalid_data(/Country must.*ISO-3166-1/)
|
91
|
+
data[:creditor].delete(:country)
|
92
|
+
assert_invalid_data(/Country must be provided/)
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "ensures a correct structured address" do
|
96
|
+
it "ensures that address line 2 has a valid length" do
|
97
|
+
data[:creditor][:address_line2] = 'a' * 17
|
98
|
+
assert_invalid_data(/Address line 2.*structured.*16/)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "ensures that the postal code exists and has a valid length" do
|
102
|
+
data[:creditor][:postal_code] = 'a' * 17
|
103
|
+
assert_invalid_data(/Postal code.*16/)
|
104
|
+
data[:creditor].delete(:postal_code)
|
105
|
+
assert_invalid_data(/Postal code must be provided.*structured/)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "ensures that the town exists and has a valid length" do
|
109
|
+
data[:creditor][:town] = 'a' * 36
|
110
|
+
assert_invalid_data(/Town.*35/)
|
111
|
+
data[:creditor].delete(:town)
|
112
|
+
assert_invalid_data(/Town must be provided.*structured/)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "ensures a correct combined address" do
|
117
|
+
it "ensures that address line 2 exists and has a valid length" do
|
118
|
+
data[:debtor][:address_line2] = 'a' * 71
|
119
|
+
assert_invalid_data(/Address line 2.*combined.*70/)
|
120
|
+
data[:debtor].delete(:address_line2)
|
121
|
+
assert_invalid_data(/Address line 2 must be provided.*combined/)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "ensures that the postal code does not exist" do
|
125
|
+
data[:debtor][:postal_code] = 'a'
|
126
|
+
assert_invalid_data(/Postal code must not be provided.*combined/)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "ensures that the town exists and has a valid length" do
|
130
|
+
data[:debtor][:town] = 'a'
|
131
|
+
assert_invalid_data(/Town must not be provided.*combined/)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "reference" do
|
136
|
+
it "ensures the QRR reference value exists and is valid" do
|
137
|
+
data[:reference_type] = 'QRR'
|
138
|
+
assert_invalid_data(/:reference must be filled.*QRR/)
|
139
|
+
data[:reference] = 'adsfads'
|
140
|
+
assert_invalid_data(/:reference for QRR.*27/)
|
141
|
+
data[:reference] = '210000000003139471430009011'
|
142
|
+
assert_invalid_data(/:reference for QRR.*invalid check digit.*7/)
|
143
|
+
data[:reference] = '21000000000313947143000901'
|
144
|
+
assert_equal('7', create_box(data).data[:reference][26])
|
145
|
+
data[:reference] = '210000000 0031394 71430009017'
|
146
|
+
assert(create_box(data))
|
147
|
+
end
|
148
|
+
|
149
|
+
it "ensures the SCOR reference value exists and is valid" do
|
150
|
+
data[:reference_type] = 'SCOR'
|
151
|
+
assert_invalid_data(/:reference must be filled.*SCOR/)
|
152
|
+
data[:reference] = 'RF11'
|
153
|
+
assert_invalid_data(/:reference for SCOR.*5 and 25/)
|
154
|
+
data[:reference] = 'RFa' * 9
|
155
|
+
assert_invalid_data(/:reference for SCOR.*5 and 25/)
|
156
|
+
data[:reference] = 'RF123323;ö'
|
157
|
+
assert_invalid_data(/:reference for SCOR.*alpha-numeric/)
|
158
|
+
data[:reference] = 'a' * 20
|
159
|
+
assert_invalid_data(/:reference for SCOR must start.*RF/)
|
160
|
+
data[:reference] = 'RFabcdefgh'
|
161
|
+
assert_invalid_data(/:reference for SCOR must start.*RF and check digits/)
|
162
|
+
data[:reference] = 'RF 48 5000056789012345d'
|
163
|
+
assert_invalid_data(/:reference for SCOR has invalid check digits/)
|
164
|
+
data[:reference] = 'RF 48 5000056789012345'
|
165
|
+
assert(create_box(data))
|
166
|
+
end
|
167
|
+
|
168
|
+
it "ensures that no reference value is specified for a NON type" do
|
169
|
+
data[:reference_type] = 'NON'
|
170
|
+
data[:reference] = 'something'
|
171
|
+
assert_invalid_data(/:reference must not be provided.*NON/)
|
172
|
+
end
|
173
|
+
|
174
|
+
it "ensures a valid reference_type value" do
|
175
|
+
data[:reference_type] = 'OTHER'
|
176
|
+
assert_invalid_data(/:reference_type must be one of/)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
it "ensures a correct message value" do
|
181
|
+
data[:message] = 'a' * 141
|
182
|
+
assert_invalid_data(/:message must not contain.*140/)
|
183
|
+
end
|
184
|
+
|
185
|
+
it "ensures a correct billing information value" do
|
186
|
+
data[:billing_information] = 'a' * 141
|
187
|
+
assert_invalid_data(/:billing_information must not contain.*140/)
|
188
|
+
end
|
189
|
+
|
190
|
+
it "ensures a total combined length of message + billing_information of max 140 characters" do
|
191
|
+
data[:message] = 'a' * 80
|
192
|
+
data[:billing_information] = 'a' * 80
|
193
|
+
assert_invalid_data(/:message and :billing_information together.*140/)
|
194
|
+
end
|
195
|
+
|
196
|
+
it "ensures a correct alternative schemes value" do
|
197
|
+
data[:alternative_schemes] = 'a' * 101
|
198
|
+
assert_invalid_data(/alternative_schemes.*not more than 100/)
|
199
|
+
data[:alternative_schemes] = ['a', 'b', 'c']
|
200
|
+
assert_invalid_data(/alternative_schemes.*at most contain 2 strings/)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe "draw" do
|
206
|
+
before do
|
207
|
+
@doc = HexaPDF::Document.new
|
208
|
+
@composer = @doc.pages.add.canvas.composer(margin: 0)
|
209
|
+
end
|
210
|
+
|
211
|
+
it "works with all information filled in" do
|
212
|
+
data[:reference_type] = 'QRR'
|
213
|
+
data[:reference] = '210000000 0031394 71430009017'
|
214
|
+
data[:message] = 'Please just pay the bills, Jim!'
|
215
|
+
data[:billing_information] = '//S/hit/30/50/what/do/I/do/'
|
216
|
+
data[:alternative_schemes] = 'ebill/here/comes/data'
|
217
|
+
assert(@composer.box(:swiss_qr_bill, data: data))
|
218
|
+
end
|
219
|
+
|
220
|
+
it "works with no amount and no debtor" do
|
221
|
+
data.delete(:debtor)
|
222
|
+
data.delete(:amount)
|
223
|
+
assert(@composer.box(:swiss_qr_bill, data: data))
|
224
|
+
end
|
225
|
+
|
226
|
+
it "fails if the content is too big for the box" do
|
227
|
+
box = create_box(data)
|
228
|
+
box.data[:message] = 'x' * 1400
|
229
|
+
err = assert_raises(HexaPDF::Error) { @composer.draw_box(box) }
|
230
|
+
assert_match(/Swiss QR-bill could not be fit/, err.message)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hexapdf-extras
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Leitner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hexapdf
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0.
|
19
|
+
version: '0.36'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0.
|
26
|
+
version: '0.36'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rqrcode_core
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -53,9 +53,11 @@ files:
|
|
53
53
|
- lib/hexapdf/extras/graphic_object/qr_code.rb
|
54
54
|
- lib/hexapdf/extras/layout.rb
|
55
55
|
- lib/hexapdf/extras/layout/qr_code_box.rb
|
56
|
+
- lib/hexapdf/extras/layout/swiss_qr_bill.rb
|
56
57
|
- lib/hexapdf/extras/version.rb
|
57
58
|
- test/hexapdf/extras/graphic_object/test_qr_code.rb
|
58
59
|
- test/hexapdf/extras/layout/test_qr_code_box.rb
|
60
|
+
- test/hexapdf/extras/layout/test_swiss_qr_bill.rb
|
59
61
|
- test/test_helper.rb
|
60
62
|
homepage: https://hexapdf-extras.gettalong.org
|
61
63
|
licenses:
|
@@ -76,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
78
|
- !ruby/object:Gem::Version
|
77
79
|
version: '0'
|
78
80
|
requirements: []
|
79
|
-
rubygems_version: 3.
|
81
|
+
rubygems_version: 3.5.3
|
80
82
|
signing_key:
|
81
83
|
specification_version: 4
|
82
84
|
summary: Additional functionality for HexaPDF
|