prawn-swiss_qr_bill 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +5 -0
  3. data/README.md +124 -0
  4. data/config/locales/de.yml +15 -0
  5. data/config/locales/en.yml +15 -0
  6. data/config/locales/fr.yml +15 -0
  7. data/config/locales/it.yml +15 -0
  8. data/lib/prawn/swiss_qr_bill/bill.rb +20 -0
  9. data/lib/prawn/swiss_qr_bill/corner_box.rb +79 -0
  10. data/lib/prawn/swiss_qr_bill/cutting_lines.rb +74 -0
  11. data/lib/prawn/swiss_qr_bill/debug_section.rb +140 -0
  12. data/lib/prawn/swiss_qr_bill/extension.rb +18 -0
  13. data/lib/prawn/swiss_qr_bill/helpers/box_helper.rb +14 -0
  14. data/lib/prawn/swiss_qr_bill/helpers/number_helper.rb +16 -0
  15. data/lib/prawn/swiss_qr_bill/iban.rb +84 -0
  16. data/lib/prawn/swiss_qr_bill/images/scissor.png +0 -0
  17. data/lib/prawn/swiss_qr_bill/images/swiss_cross.png +0 -0
  18. data/lib/prawn/swiss_qr_bill/padded_box.rb +52 -0
  19. data/lib/prawn/swiss_qr_bill/qr/data.rb +105 -0
  20. data/lib/prawn/swiss_qr_bill/sections/payment_amount.rb +88 -0
  21. data/lib/prawn/swiss_qr_bill/sections/payment_further_information.rb +14 -0
  22. data/lib/prawn/swiss_qr_bill/sections/payment_information.rb +59 -0
  23. data/lib/prawn/swiss_qr_bill/sections/payment_title.rb +18 -0
  24. data/lib/prawn/swiss_qr_bill/sections/qr_code.rb +102 -0
  25. data/lib/prawn/swiss_qr_bill/sections/receipt_acceptance.rb +18 -0
  26. data/lib/prawn/swiss_qr_bill/sections/receipt_amount.rb +86 -0
  27. data/lib/prawn/swiss_qr_bill/sections/receipt_information.rb +51 -0
  28. data/lib/prawn/swiss_qr_bill/sections/receipt_title.rb +18 -0
  29. data/lib/prawn/swiss_qr_bill/sections/section.rb +88 -0
  30. data/lib/prawn/swiss_qr_bill/sections.rb +29 -0
  31. data/lib/prawn/swiss_qr_bill/specifications.rb +78 -0
  32. data/lib/prawn/swiss_qr_bill/specs.yml +106 -0
  33. data/lib/prawn/swiss_qr_bill/version.rb +7 -0
  34. data/lib/prawn/swiss_qr_bill.rb +43 -0
  35. data/prawn-swiss_qr_bill.gemspec +40 -0
  36. metadata +218 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: faa2f8fb8c38ea2681890f7573bb42ab424cadb413d57699e0e64145088c8911
4
+ data.tar.gz: 2fa0db096bc0d9fb4060858dd0fe88ba187c20a54ae36babcb66848c6a102a33
5
+ SHA512:
6
+ metadata.gz: e1d30550f53e1ed3dc74a0256d826a25fa2290f06a9b4e282e8529dabeaf334907b87613e6babd52667ee3cf248098464dd9381743e4883014a7a85c8783c97d
7
+ data.tar.gz: 536068e41ad5d54e70dcebe9deeeb19a1984eb52b4bee5e020e5644bbdcf4e7c66453b449ae3e811b523f37dc2b3fc1a2dcaacd51c072562a1628b0b3ef509ea
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'http://rubygems.org'
4
+
5
+ gemspec
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # Prawn::SwissQRBill
2
+
3
+ A Ruby library for creating Swiss QR-bill payment slips inside a PDF. It is
4
+ built as a [Prawn](https://github.com/prawnpdf/prawn) extension.
5
+
6
+ ## Installation
7
+
8
+ Add the following line to your `Gemfile`:
9
+
10
+ ```ruby
11
+ gem 'prawn-swiss_qr_bill'
12
+ ```
13
+
14
+ or install it manually:
15
+
16
+ ```bash
17
+ gem install prawn-swiss_qr_bill
18
+ ```
19
+
20
+ ## Basic Usage
21
+
22
+ Define the relevant information for the Swiss QR-bill and render it inside the Prawn block:
23
+
24
+ ```ruby
25
+ require 'prawn'
26
+ require 'prawn/swiss_qr_bill'
27
+
28
+ @qr_data = {
29
+ creditor: {
30
+ iban: 'CH08 3080 8004 1110 4136 9',
31
+ address: {
32
+ name: 'Mischa Schindowski',
33
+ line1: 'Schybenächerweg 553',
34
+ line2: '5324 Full-Reuenthal',
35
+ country: 'CH'
36
+ }
37
+ },
38
+ amount: 9.90,
39
+ currency: 'CHF',
40
+ reference: '00 00000 00000 02202 20202 99991',
41
+ reference_type: 'QRR'
42
+ }
43
+
44
+ Prawn::Document.generate('output.pdf', page_size: 'A4') do
45
+ text 'A Swiss QR bill'
46
+
47
+ swiss_qr_bill(@qr_data)
48
+ end
49
+ ```
50
+
51
+ This will render the Swiss QR-bill at the bottom of the page:
52
+
53
+ ![Swiss QR-bill Example, PDF](./images/sqb_example_01.png)
54
+
55
+ ### Options
56
+
57
+ The following options are available:
58
+
59
+ ```ruby
60
+ # *: mandatory
61
+ @qr_data = {
62
+ creditor: {
63
+ iban: '<iban>', # *
64
+ address: { # *
65
+ type: '<K|S>', # default: K
66
+ name: '<name>', # *
67
+ line1: '<street> <nr>', # *
68
+ line2: '<line 2>',
69
+ postal_code: '<postal code>',
70
+ city: '<city>',
71
+ country: '<country>' # *
72
+ }
73
+ },
74
+ debtor: {
75
+ address: {
76
+ type: '<K|S>', # default: nil
77
+ name: '<name>',
78
+ line1: '<street> <nr>',
79
+ line2: '<line 2>',
80
+ postal_code: '<postal_code>',
81
+ city: '<city>',
82
+ country: '<country>'
83
+ }
84
+ },
85
+ amount: 9.90,
86
+ currency: '<CHF|EUR>', # *
87
+ reference: '<ref nr>',
88
+ reference_type: '<QRR|SCOR|NON>' # default: NON
89
+ }
90
+ ```
91
+
92
+ If `debtor` or `amount` amount is not given, a box will be printed.
93
+
94
+ ## Important
95
+
96
+ This library does not validate (yet) IBAN, reference or the given QR data.
97
+ Please refer to the implementation guidelines and the Swiss QR-bill validaton
98
+ portal by SIX below.
99
+
100
+ ## Contributing
101
+
102
+ If you miss a feature or you've found a bug, please [open a GitHub issue](https://github.com/mitosch/prawn-swiss_qr_bill/issues).
103
+
104
+ Pull requests are highly welcome:
105
+
106
+ * Fork the project
107
+ * Make your changes
108
+ * Send the pull request
109
+
110
+ ## Authors
111
+
112
+ Original author: Mischa Schindowski
113
+
114
+ ## Resources
115
+
116
+ * [Prawn](https://github.com/prawnpdf/prawn): Fast, Nimble PDF Generation For Ruby
117
+ * [Swiss QR-bill Validation Portal](https://validation.iso-payments.ch/qrrechnung)
118
+ * [Swiss Payment Standards](https://www.paymentstandards.ch):
119
+ * [Style Guide QR-bill](https://www.paymentstandards.ch/dam/downloads/style-guide-en.pdf)
120
+ * [Implementation Guidelines QR-bill](https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf)
121
+
122
+ ## Copyright
123
+
124
+ MIT License (http://www.opensource.org/licenses/mit-license.html)
@@ -0,0 +1,15 @@
1
+ de:
2
+ swiss_qr_bill:
3
+ payment: Zahlteil
4
+ creditor: Konto / Zahlbar an
5
+ reference: Referenz
6
+ additional_info: Zusätzliche Informationen
7
+ further_info: Weitere Informationen
8
+ currency: Währung
9
+ amount: Betrag
10
+ receipt: Empfangsschein
11
+ acceptance: Annahmestelle
12
+ separate: Vor der Einzahlung abzutrennen
13
+ debtor: Zahlbar durch
14
+ debtor_blank: Zahlbar durch (Name/Adresse)
15
+ debtor_favour: Zugunsten
@@ -0,0 +1,15 @@
1
+ en:
2
+ swiss_qr_bill:
3
+ payment: Payment part
4
+ creditor: Account / Payable to
5
+ reference: Reference
6
+ additional_info: Additional information
7
+ further_info: Further information
8
+ currency: Currency
9
+ amount: Amount
10
+ receipt: Receipt
11
+ acceptance: Acceptance point
12
+ separate: Separate before paying in
13
+ debtor: Payable by
14
+ debtor_blank: Payable by (name/address)
15
+ debtor_favour: In favour of
@@ -0,0 +1,15 @@
1
+ fr:
2
+ swiss_qr_bill:
3
+ payment: Section paiement
4
+ creditor: Compte / Payable à
5
+ reference: Référence
6
+ additional_info: Informations supplémentaires
7
+ further_info: Informations additionnelles
8
+ currency: Monnaie
9
+ amount: Montant
10
+ receipt: Récépissé
11
+ acceptance: Point de dépôt
12
+ separate: A détacher avant le versement
13
+ debtor: Payable par
14
+ debtor_blank: Payable par (nom/adresse)
15
+ debtor_favour: En faveur de
@@ -0,0 +1,15 @@
1
+ it:
2
+ swiss_qr_bill:
3
+ payment: Sezione pagamento
4
+ creditor: Conto / Pagabile a
5
+ reference: Riferimento
6
+ additional_info: Informazioni supplementari
7
+ further_info: Informazioni aggiuntive
8
+ currency: Valuta
9
+ amount: Importo
10
+ receipt: Ricevuta
11
+ acceptance: Punto di accettazione
12
+ separate: Da staccare prima del versamento
13
+ debtor: Pagabile da
14
+ debtor_blank: Pagabile da (nome/indirizzo)
15
+ debtor_favour: A favore di
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module SwissQRBill
5
+ # Bill renders the Swiss QR-bill at the bottom of a page
6
+ class Bill
7
+ def initialize(document, data)
8
+ @doc = document
9
+ @data = data
10
+ end
11
+
12
+ def draw
13
+ @doc.canvas do
14
+ Sections.draw_all(@doc, @data)
15
+ CuttingLines.new(@doc).draw
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module SwissQRBill
5
+ # Draws a box with corner ticks
6
+ class CornerBox
7
+ TICK_SIZE = 3.mm
8
+
9
+ LINE_WIDTH = 0.75
10
+
11
+ def initialize(document, point, options)
12
+ unless options.key?(:width) && options.key?(:height)
13
+ raise ArgumentError,
14
+ 'corner_box needs the :width and :height option to be set'
15
+ end
16
+
17
+ @document = document
18
+ @point = point
19
+ @width = options[:width]
20
+ @height = options[:height]
21
+
22
+ @brain = {}
23
+ end
24
+
25
+ def draw
26
+ set_styles
27
+ draw_lines
28
+ reset_styles
29
+ end
30
+
31
+ private
32
+
33
+ def set_styles
34
+ @brain[:line_width] = @document.line_width
35
+ @document.line_width = LINE_WIDTH
36
+ end
37
+
38
+ def reset_styles
39
+ @document.line_width = @brain[:line_width]
40
+ end
41
+
42
+ def draw_lines
43
+ point_x, point_y = @point
44
+
45
+ @document.stroke do
46
+ draw_horizontal_lines(point_x, point_y)
47
+ draw_vertical_lines(point_x, point_y)
48
+ end
49
+ end
50
+
51
+ def draw_horizontal_lines(point_x, point_y)
52
+ # upper lines
53
+ @document.horizontal_line point_x, point_x + TICK_SIZE,
54
+ at: point_y
55
+ @document.horizontal_line point_x + @width, point_x + @width - TICK_SIZE,
56
+ at: point_y
57
+ # lower lines
58
+ @document.horizontal_line point_x, point_x + TICK_SIZE,
59
+ at: point_y - @height
60
+ @document.horizontal_line point_x + @width, point_x + @width - TICK_SIZE,
61
+ at: point_y - @height
62
+ end
63
+
64
+ def draw_vertical_lines(point_x, point_y)
65
+ # upper lines
66
+ @document.vertical_line point_y, point_y - TICK_SIZE,
67
+ at: point_x
68
+ @document.vertical_line point_y, point_y - TICK_SIZE,
69
+ at: point_x + @width
70
+
71
+ # lower lines
72
+ @document.vertical_line point_y - @height, point_y - @height + TICK_SIZE,
73
+ at: point_x
74
+ @document.vertical_line point_y - @height, point_y - @height + TICK_SIZE,
75
+ at: point_x + @width
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module SwissQRBill
5
+ # Horizontal and vertical cutting lines with a scissor symbol
6
+ class CuttingLines
7
+ SCISSOR_FILE = File.expand_path("#{__dir__}/images/scissor.png")
8
+
9
+ SCISSOR_WIDTH = 5.mm
10
+ SCISSOR_HEIGHT = 3.mm
11
+
12
+ PAD_LEFT = 5.mm
13
+ PAD_TOP = 1.mm
14
+
15
+ attr_reader :doc, :receipt_width, :receipt_height
16
+
17
+ def initialize(document)
18
+ @doc = document
19
+
20
+ @brain = {}
21
+
22
+ load_specs
23
+ end
24
+
25
+ def draw
26
+ set_styles
27
+
28
+ draw_strokes
29
+ draw_scissors
30
+
31
+ reset_styles
32
+ end
33
+
34
+ private
35
+
36
+ def load_specs
37
+ specs = Specifications.new
38
+ @receipt_height = specs.get('receipt.height').mm
39
+ @receipt_width = specs.get('receipt.width').mm
40
+ end
41
+
42
+ def set_styles
43
+ @brain[:line_width] = doc.line_width
44
+
45
+ doc.line_width 0.5
46
+ doc.dash 2, space: 2
47
+ end
48
+
49
+ def draw_strokes
50
+ doc.stroke { doc.line [0, receipt_height], [doc.bounds.right, receipt_height] }
51
+ doc.stroke { doc.line [receipt_width, receipt_height], [receipt_width, doc.bounds.bottom] }
52
+ end
53
+
54
+ def draw_scissors
55
+ doc.bounding_box([doc.bounds.left + PAD_LEFT, receipt_height + (SCISSOR_HEIGHT / 2)],
56
+ width: SCISSOR_WIDTH, height: SCISSOR_HEIGHT) do
57
+ doc.image SCISSOR_FILE, width: SCISSOR_WIDTH
58
+ end
59
+
60
+ doc.bounding_box([receipt_width - (SCISSOR_HEIGHT / 2), receipt_height - PAD_TOP],
61
+ width: SCISSOR_WIDTH, height: SCISSOR_HEIGHT) do
62
+ doc.rotate(270, origin: [0, 0]) do
63
+ doc.image SCISSOR_FILE, width: SCISSOR_WIDTH
64
+ end
65
+ end
66
+ end
67
+
68
+ def reset_styles
69
+ doc.line_width = @brain[:line_width]
70
+ doc.undash
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module SwissQRBill
5
+ # Draws a blue debug section.
6
+ #
7
+ # Reference:
8
+ # https://www.paymentstandards.ch/dam/downloads/style-guide-en.pdf
9
+ #
10
+ # OPTIMIZE: move several general styling functions to helpers (bb-helper)
11
+ class DebugSection
12
+ SECTIONS = [
13
+ 'receipt',
14
+ 'receipt.title',
15
+ 'receipt.information',
16
+ 'receipt.amount',
17
+ 'receipt.acceptance',
18
+ 'payment',
19
+ 'payment.title',
20
+ 'payment.information',
21
+ 'payment.qr_code',
22
+ 'payment.qr_cross',
23
+ 'payment.amount',
24
+ 'payment.further_information'
25
+ ].freeze
26
+
27
+ attr_reader :doc, :properties
28
+
29
+ def initialize(document)
30
+ @doc = document
31
+
32
+ @brain = { font: {}, border: { color: nil, width: nil } }
33
+
34
+ @spec = Prawn::SwissQRBill::Specifications.new
35
+ end
36
+
37
+ def draw
38
+ SECTIONS.each do |section|
39
+ name = @spec.get(section)['name']
40
+ specs = @spec.get_specs(section)
41
+ point = specs.point
42
+ options = { width: specs.width, height: specs.height }.merge(
43
+ text: name,
44
+ background: false, border: true, font_size: 10
45
+ )
46
+
47
+ draw_debug_box(point, options)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ # Draws a box like in the style guide.
54
+ #
55
+ # NOTE: Background removed for simplicity, color was: D4EDFC
56
+ def draw_debug_box(*args)
57
+ position = args[0]
58
+ text = args[1].delete(:text)
59
+ border_style = { color: '71C4E9', width: 0.75 } if args[1][:border]
60
+ debug_box_opts = args[1].merge(padding: 1.mm,
61
+ border: border_style,
62
+ font: { size: args[1][:font_size] })
63
+
64
+ draw_padded_box(position, debug_box_opts) do
65
+ doc.text text, color: '71C4E9'
66
+ end
67
+ end
68
+
69
+ def draw_padded_box(*args, &block)
70
+ padding = args[1].delete(:padding) || 0
71
+
72
+ doc.bounding_box(*args) do
73
+ height = doc.bounds.height
74
+ width = doc.bounds.width
75
+
76
+ ensure_styles(args[1])
77
+
78
+ doc.bounding_box([padding, height - padding],
79
+ width: width - (2 * padding),
80
+ height: height - (2 * padding)) { yield block }
81
+ end
82
+
83
+ reset_styles
84
+ end
85
+
86
+ # ensures setting the styles by meaningful options:
87
+ #
88
+ # {
89
+ # border: { color: '123456', width: 2 },
90
+ # font: { size: 10 }
91
+ # }
92
+ def ensure_styles(opts)
93
+ stroke_style(opts[:border])
94
+ font_style(opts[:font])
95
+ end
96
+
97
+ def stroke_style(opts)
98
+ return unless opts
99
+
100
+ remember_style(:border)
101
+
102
+ doc.line_width opts[:width]
103
+ doc.stroke_color opts[:color]
104
+ doc.stroke_bounds
105
+ end
106
+
107
+ def font_style(opts)
108
+ return unless opts
109
+
110
+ remember_style(:font)
111
+ doc.font_size opts[:size]
112
+ end
113
+
114
+ def remember_style(style_type)
115
+ case style_type
116
+ when :font
117
+ @brain[:font][:size] = doc.font_size
118
+ when :border
119
+ @brain[:border][:color] = doc.line_width
120
+ @brain[:border][:width] = doc.stroke_color
121
+ end
122
+ end
123
+
124
+ def reset_styles
125
+ reset_style(:font)
126
+ reset_style(:border)
127
+ end
128
+
129
+ def reset_style(style_type)
130
+ case style_type
131
+ when :font
132
+ doc.font_size @brain[:font][:size]
133
+ when :border
134
+ doc.line_width @brain[:border][:color]
135
+ doc.stroke_color @brain[:border][:width]
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module SwissQRBill
5
+ # Extend prawn with *swiss_qr_bill* methods
6
+ module Extension
7
+ def swiss_qr_bill(data)
8
+ Prawn::SwissQRBill::Bill.new(self, data).draw
9
+ end
10
+
11
+ def swiss_qr_bill_sections
12
+ canvas do
13
+ Prawn::SwissQRBill::DebugSection.new(self).draw
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module SwissQRBill
5
+ module Helpers
6
+ # Helpers for drawing boxes
7
+ module BoxHelper
8
+ def corner_box(doc, point, options)
9
+ Prawn::SwissQRBill::CornerBox.new(doc, point, options).draw
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module SwissQRBill
5
+ module Helpers
6
+ # Helpers to format numbers
7
+ module NumberHelper
8
+ def format_with_delimiter(number)
9
+ left, right = format('%.2f', number).split('.')
10
+ left.gsub!(/(\d)(?=(\d\d\d)+(?!\d))/) { |d| "#{d} " }
11
+ [left, right].compact.join('.')
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn
4
+ module SwissQRBill
5
+ # Check validity of IBAN.
6
+ #
7
+ # NOTE: *For Switzerland only*
8
+ #
9
+ # Simple implementation according to
10
+ # https://en.wikipedia.org/wiki/International_Bank_Account_Number#Algorithms
11
+ #
12
+ # TODO: validate QR-iban
13
+ class IBAN
14
+ attr_reader :code
15
+
16
+ def initialize(code)
17
+ @code = standardize(code)
18
+ end
19
+
20
+ def country_code
21
+ @code[0..1]
22
+ end
23
+
24
+ def check_digits
25
+ @code[2..3]
26
+ end
27
+
28
+ def bban
29
+ @code[4..-1]
30
+ end
31
+
32
+ # Convert Alpha-Numeric IBAN to numeric values (incl. rearrangement)
33
+ #
34
+ # Reference:
35
+ # https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
36
+ def to_i
37
+ "#{bban}#{country_code}#{check_digits}".gsub(/[A-Z]/) { |c| c.ord - 55 }.to_i
38
+ end
39
+
40
+ def prettify
41
+ @code.gsub(/(.{4})/, '\1 ').strip
42
+ end
43
+
44
+ # valid IBAN:
45
+ # CH77 8080 8004 1110 4136 9
46
+ #
47
+ # valid QR-IBAN:
48
+ # CH08 3080 8004 1110 4136 9
49
+ def valid?
50
+ valid_check_digits? && valid_swiss_length? && valid_country?
51
+ end
52
+
53
+ def valid_check_digits?
54
+ to_i % 97 == 1
55
+ end
56
+
57
+ # NOTE: Only the length for Switzerland is implemented because it is Swiss QR-bill specific
58
+ def valid_length?
59
+ valid_swiss_length?
60
+ end
61
+
62
+ def valid_swiss_length?
63
+ @code.length == 21
64
+ end
65
+
66
+ def valid_country?
67
+ country_code == 'CH'
68
+ end
69
+
70
+ private
71
+
72
+ # Make the given string standard:
73
+ #
74
+ # CH21 2345 2123 5543 5554
75
+ # ch21 2345 2123 5543 5554
76
+ # " ch21 2345 2123 5543 5554 "
77
+ #
78
+ # => CH212345212355435554
79
+ def standardize(code)
80
+ code.to_s.strip.gsub(/\s+/, '').upcase
81
+ end
82
+ end
83
+ end
84
+ end