invoice_printer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env ruby
2
+ # This is an example of a Czech invoice.
3
+ #
4
+ # Due to the special characters it requires Overpass-Regular.ttf font to be
5
+ # present in this directory.
6
+
7
+ lib = File.expand_path('../../lib', __FILE__)
8
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
9
+ require 'invoice_printer'
10
+
11
+ labels = {
12
+ name: 'Faktura',
13
+ provider: 'Prodejce',
14
+ purchaser: 'Kupující',
15
+ ic: 'IČ',
16
+ dic: 'DIČ',
17
+ payment: 'Forma úhrady',
18
+ payment_by_transfer: 'Platba na následující účet:',
19
+ account_number: 'Číslo účtu',
20
+ issue_date: 'Datum vydání',
21
+ due_date: 'Datum splatnosti',
22
+ item: 'Položka',
23
+ quantity: 'Počet',
24
+ unit: 'MJ',
25
+ price_per_item: 'Cena za položku',
26
+ amount: 'Celkem bez daně',
27
+ subtotal: 'Cena bez daně',
28
+ tax: 'DPH 21 %',
29
+ total: 'Celkem'
30
+ }
31
+
32
+ first_item = InvoicePrinter::Document::Item.new(
33
+ name: 'Konzultace',
34
+ quantity: '2',
35
+ unit: 'hod',
36
+ price: 'Kč 500',
37
+ amount: 'Kč 1.000'
38
+ )
39
+
40
+ second_item = InvoicePrinter::Document::Item.new(
41
+ name: 'Programování',
42
+ quantity: '10',
43
+ unit: 'hod',
44
+ price: 'Kč 900',
45
+ amount: 'Kč 9.000'
46
+ )
47
+
48
+ invoice = InvoicePrinter::Document.new(
49
+ number: 'č. 198900000001',
50
+ provider_name: 'Petr Nový',
51
+ provider_ic: '56565656',
52
+ provider_street: 'Rolnická',
53
+ provider_street_number: '1',
54
+ provider_postcode: '747 05',
55
+ provider_city: 'Opava',
56
+ provider_city_part: 'Kateřinky',
57
+ purchaser_name: 'Adam Černý',
58
+ purchaser_street: 'Ostravská',
59
+ purchaser_street_number: '1',
60
+ purchaser_postcode: '747 70',
61
+ purchaser_city: 'Opava',
62
+ issue_date: '05/03/2016',
63
+ due_date: '19/03/2016',
64
+ subtotal: 'Kč 10.000',
65
+ tax: 'Kč 2.100',
66
+ total: 'Kč 12.100,-',
67
+ bank_account_number: '156546546465',
68
+ account_iban: 'IBAN464545645',
69
+ account_swift: 'SWIFT5456',
70
+ items: [first_item, second_item]
71
+ )
72
+
73
+ InvoicePrinter.print(
74
+ document: invoice,
75
+ labels: labels,
76
+ font: File.expand_path('../Overpass-Regular.ttf', __FILE__),
77
+ logo: 'example.jpg',
78
+ file_name: 'czech_invoice.pdf'
79
+ )
Binary file
@@ -0,0 +1,25 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'invoice_printer/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'invoice_printer'
7
+ spec.version = InvoicePrinter::VERSION
8
+ spec.authors = ['Josef Strzibny']
9
+ spec.email = ['strzibny@strzibny.name']
10
+ spec.summary = 'Super simple PDF invoicing in pure Ruby'
11
+ spec.description = 'Super simple PDF invoicing in pure Ruby (based on Prawn library).'
12
+ spec.homepage = 'https://github.com/strzibny/invoice_printer'
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_dependency 'prawn'
21
+ spec.add_dependency 'prawn-table'
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.7'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ end
@@ -0,0 +1,67 @@
1
+ require 'invoice_printer/version'
2
+ require 'invoice_printer/document/item'
3
+ require 'invoice_printer/pdf_document'
4
+
5
+ # Create PDF versions of invoices or receipts using Prawn
6
+ #
7
+ # Example:
8
+ #
9
+ # invoice = InvoicePrinter::Document.new(...)
10
+ # InvoicePrinter.print(
11
+ # document: invoice,
12
+ # font: 'path-to-font-file.ttf',
13
+ # logo: 'logo.jpg'
14
+ # file_name: 'invoice.pdf'
15
+ # )
16
+ module InvoicePrinter
17
+ # Override default English labels with a given hash
18
+ #
19
+ # Example:
20
+ #
21
+ # InvoicePrinter.labels = {
22
+ # name: 'Invoice',
23
+ # number: '201604030001'
24
+ # provider: 'Provider',
25
+ # purchaser: 'Purchaser',
26
+ # payment: 'Payment',
27
+ # payment_by_transfer: 'Payment by bank transfer on the account below:',
28
+ # payment_in_cash: 'Payment in cash',
29
+ # account_number: 'Account NO:',
30
+ # swift: 'SWIFT:',
31
+ # iban: 'IBAN:',
32
+ # issue_date: 'Issue date:',
33
+ # due_date: 'Due date:',
34
+ # item: 'Item',
35
+ # quantity: 'Quantity',
36
+ # unit: 'Unit',
37
+ # price_per_item: 'Price per item',
38
+ # amount: 'Amount'
39
+ # }
40
+ def self.labels=(labels)
41
+ PDFDocument.labels = labels
42
+ end
43
+
44
+ def self.labels
45
+ PDFDocument.labels
46
+ end
47
+
48
+ # Print the given InvoicePrinter::Document to PDF file named +file_name+
49
+ def self.print(document:, file_name:, labels: {}, font: nil, logo: nil)
50
+ PDFDocument.new(
51
+ document: document,
52
+ labels: labels,
53
+ font: font,
54
+ logo: logo,
55
+ ).print(file_name)
56
+ end
57
+
58
+ # Render the PDF document InvoicePrinter::Document to PDF directly
59
+ def self.render(document:, labels: {}, font: nil, logo: nil)
60
+ PDFDocument.new(
61
+ document: document,
62
+ labels: labels,
63
+ font: font,
64
+ logo: logo
65
+ ).render
66
+ end
67
+ end
@@ -0,0 +1,142 @@
1
+ module InvoicePrinter
2
+ # Invoice and receipt representation
3
+ #
4
+ # Example:
5
+ #
6
+ # invoice = InvoicePrinter::Document.new(
7
+ # number: '198900000001',
8
+ # provider_name: 'Business s.r.o.',
9
+ # provider_ic: '56565656',
10
+ # provider_dic: '465454',
11
+ # provider_street: 'Rolnicka',
12
+ # provider_street_number: '1',
13
+ # provider_postcode: '747 05',
14
+ # provider_city: 'Opava',
15
+ # provider_city_part: 'Katerinky',
16
+ # provider_extra_address_line: 'Czech Republic',
17
+ # purchaser_name: 'Adam',
18
+ # purchaser_ic: '',
19
+ # purchaser_dic: '',
20
+ # purchaser_street: 'Ostravska',
21
+ # purchaser_street_number: '1',
22
+ # purchaser_postcode: '747 70',
23
+ # purchaser_city: 'Opava',
24
+ # purchaser_city_part: '',
25
+ # purchaser_extra_address_line: '',
26
+ # issue_date: '19/03/3939',
27
+ # due_date: '19/03/3939',
28
+ # subtotal: '$ 150',
29
+ # tax: '$ 50',
30
+ # total: '$ 200',
31
+ # bank_account_number: '156546546465',
32
+ # account_iban: 'IBAN464545645',
33
+ # account_swift: 'SWIFT5456',
34
+ # items: [
35
+ # InvoicePrinter::Document::Item.new,
36
+ # InvoicePrinter::Document::Item.new
37
+ # ]
38
+ # )
39
+ #
40
+ # +amount should equal the sum of all item's +amount+,
41
+ # but this is not enforced.
42
+ class Document
43
+ attr_reader :number,
44
+ # Provider fields
45
+ :provider_name,
46
+ :provider_ic,
47
+ :provider_dic,
48
+ # Provider address fields
49
+ :provider_street,
50
+ :provider_street_number,
51
+ :provider_postcode,
52
+ :provider_city,
53
+ :provider_city_part,
54
+ :provider_extra_address_line,
55
+ # Purchaser fields
56
+ :purchaser_name,
57
+ :purchaser_ic,
58
+ :purchaser_dic,
59
+ # Purchaser address fields
60
+ :purchaser_street,
61
+ :purchaser_street_number,
62
+ :purchaser_postcode,
63
+ :purchaser_city,
64
+ :purchaser_city_part,
65
+ :purchaser_extra_address_line,
66
+ :issue_date,
67
+ :due_date,
68
+ # Account details
69
+ :subtotal,
70
+ :tax,
71
+ :tax2,
72
+ :tax3,
73
+ :total,
74
+ :bank_account_number,
75
+ :account_iban,
76
+ :account_swift,
77
+ # Collection of InvoicePrinter::Invoice::Items
78
+ :items
79
+
80
+ def initialize(number: nil,
81
+ provider_name: nil,
82
+ provider_ic: nil,
83
+ provider_dic: nil,
84
+ provider_street: nil,
85
+ provider_street_number: nil,
86
+ provider_postcode: nil,
87
+ provider_city: nil,
88
+ provider_city_part: nil,
89
+ provider_extra_address_line: nil,
90
+ purchaser_name: nil,
91
+ purchaser_ic: nil,
92
+ purchaser_dic: nil,
93
+ purchaser_street: nil,
94
+ purchaser_street_number: nil,
95
+ purchaser_postcode: nil,
96
+ purchaser_city: nil,
97
+ purchaser_city_part: nil,
98
+ purchaser_extra_address_line: nil,
99
+ issue_date: nil,
100
+ due_date: nil,
101
+ subtotal: nil,
102
+ tax: nil,
103
+ tax2: nil,
104
+ tax3: nil,
105
+ total: nil,
106
+ bank_account_number: nil,
107
+ account_iban: nil,
108
+ account_swift: nil,
109
+ items: nil)
110
+ @number = number
111
+ @provider_name = provider_name
112
+ @provider_ic = provider_ic
113
+ @provider_dic = provider_dic
114
+ @provider_street = provider_street
115
+ @provider_street_number = provider_street_number
116
+ @provider_postcode = provider_postcode
117
+ @provider_city = provider_city
118
+ @provider_city_part = provider_city_part
119
+ @provider_extra_address_line = provider_extra_address_line
120
+ @purchaser_name = purchaser_name
121
+ @purchaser_ic = purchaser_ic
122
+ @purchaser_dic = purchaser_dic
123
+ @purchaser_street = purchaser_street
124
+ @purchaser_street_number = purchaser_street_number
125
+ @purchaser_postcode = purchaser_postcode
126
+ @purchaser_city = purchaser_city
127
+ @purchaser_city_part = purchaser_city_part
128
+ @purchaser_extra_address_line = purchaser_extra_address_line
129
+ @issue_date = issue_date
130
+ @due_date = due_date
131
+ @subtotal = subtotal
132
+ @tax = tax
133
+ @tax2 = tax2
134
+ @tax3 = tax3
135
+ @total = total
136
+ @bank_account_number = bank_account_number
137
+ @account_iban = account_iban
138
+ @account_swift = account_swift
139
+ @items = items
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,49 @@
1
+ require 'invoice_printer/document'
2
+
3
+ module InvoicePrinter
4
+ class Document
5
+ # Line items for InvoicePrinter::Document
6
+ #
7
+ # Example:
8
+ #
9
+ # item = InvoicePrinter::Document::Item.new(
10
+ # name: 'UX consultation',
11
+ # quantity: '4',
12
+ # unit: 'hours',
13
+ # price: '$ 25',
14
+ # tax: '$ 5'
15
+ # amount: '$ 120'
16
+ # )
17
+ #
18
+ # +amount+ should equal the +quantity+ times +price+,
19
+ # but this is not enforced.
20
+ class Item
21
+ attr_reader :name,
22
+ :quantity,
23
+ :unit,
24
+ :price,
25
+ :tax,
26
+ :tax2,
27
+ :tax3,
28
+ :amount
29
+
30
+ def initialize(name: nil,
31
+ quantity: nil,
32
+ unit: nil,
33
+ price: nil,
34
+ tax: nil,
35
+ tax2: nil,
36
+ tax3: nil,
37
+ amount: nil)
38
+ @name = name
39
+ @quantity = quantity
40
+ @unit = unit
41
+ @price = price
42
+ @tax = tax
43
+ @tax2 = tax2
44
+ @tax3 = tax3
45
+ @amount = amount
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,519 @@
1
+ require 'prawn'
2
+ require 'prawn/table'
3
+
4
+ module InvoicePrinter
5
+ # Prawn PDF representation of InvoicePrinter::Document
6
+ #
7
+ # Example:
8
+ #
9
+ # invoice = InvoicePrinter::Document.new(...)
10
+ # invoice_pdf = InvoicePrinter::PDFDocument.new(
11
+ # document: invoice,
12
+ # labels: {},
13
+ # font: 'font.ttf',
14
+ # logo: 'example.jpg'
15
+ # )
16
+ class PDFDocument
17
+ attr_reader :invoice, :labels, :file_name, :font, :logo
18
+
19
+ class FontFileNotFound < StandardError; end
20
+ class LogoFileNotFound < StandardError; end
21
+
22
+ DEFAULT_LABELS = {
23
+ name: 'Invoice',
24
+ provider: 'Provider',
25
+ purchaser: 'Purchaser',
26
+ ic: 'Identification number',
27
+ dic: 'Identification number',
28
+ payment: 'Payment',
29
+ payment_by_transfer: 'Payment by bank transfer on the account below:',
30
+ payment_in_cash: 'Payment in cash',
31
+ account_number: 'Account NO',
32
+ swift: 'SWIFT',
33
+ iban: 'IBAN',
34
+ issue_date: 'Issue date',
35
+ due_date: 'Due date',
36
+ item: 'Item',
37
+ quantity: 'Quantity',
38
+ unit: 'Unit',
39
+ price_per_item: 'Price per item',
40
+ tax: 'Tax',
41
+ tax2: 'Tax 2',
42
+ tax3: 'Tax 3',
43
+ amount: 'Amount',
44
+ subtotal: 'Subtotal',
45
+ total: 'Total'
46
+ }
47
+
48
+ def self.labels
49
+ @@labels ||= DEFAULT_LABELS
50
+ end
51
+
52
+ def self.labels=(labels)
53
+ @@labels = DEFAULT_LABELS.merge(labels)
54
+ end
55
+
56
+
57
+ def initialize(document: Document.new, labels: {}, font: nil, logo: nil)
58
+ @document = document
59
+ @labels = PDFDocument.labels.merge(labels)
60
+ @pdf = Prawn::Document.new
61
+
62
+ if logo && !logo.empty?
63
+ if File.exist?(logo)
64
+ @logo = logo
65
+ else
66
+ fail LogoFileNotFound, "Logotype file not found at #{logo}"
67
+ end
68
+ end
69
+
70
+ if font && !font.empty?
71
+ if File.exist?(font)
72
+ set_fonts(font) if font
73
+ else
74
+ fail FontFileNotFound, "Font file not found at #{font}"
75
+ end
76
+ end
77
+ build_pdf
78
+ end
79
+
80
+ # Create PDF file named +file_name+
81
+ def print(file_name = 'invoice.pdf')
82
+ @pdf.render_file file_name
83
+ end
84
+
85
+ # Directly render the PDF
86
+ def render
87
+ @pdf.render
88
+ end
89
+
90
+ private
91
+
92
+ def set_fonts(font)
93
+ font_name = Pathname.new(font).basename
94
+ @pdf.font_families.update("#{font_name}" => {
95
+ normal: font,
96
+ italic: font,
97
+ bold: font,
98
+ bold_italic: font
99
+ })
100
+ @pdf.font(font_name)
101
+ @font = font_name
102
+ end
103
+
104
+ def build_pdf
105
+ @push_down = 40
106
+ @pdf.fill_color '000000'
107
+ build_header
108
+ build_provider_box
109
+ build_purchaser_box
110
+ build_payment_method_box
111
+ build_info_box
112
+ build_items
113
+ build_total
114
+ build_logo
115
+ build_footer
116
+ end
117
+
118
+ def build_header
119
+ @pdf.text @labels[:name], size: 20
120
+ @pdf.text_box(
121
+ @document.number,
122
+ size: 20,
123
+ at: [240, 720],
124
+ width: 300,
125
+ align: :right
126
+ )
127
+ @pdf.move_down(250)
128
+ end
129
+
130
+ def build_provider_box
131
+ @pdf.text_box(
132
+ @labels[:provider],
133
+ size: 10,
134
+ at: [10, 660],
135
+ width: 240
136
+ )
137
+ @pdf.text_box(
138
+ @document.provider_name,
139
+ size: 14,
140
+ at: [10, 640],
141
+ width: 240
142
+ )
143
+ @pdf.text_box(
144
+ "#{@document.provider_street} #{@document.provider_street_number}",
145
+ size: 10,
146
+ at: [10, 620],
147
+ width: 240
148
+ )
149
+ @pdf.text_box(
150
+ @document.provider_postcode,
151
+ size: 10,
152
+ at: [10, 605],
153
+ width: 240
154
+ )
155
+ @pdf.text_box(
156
+ @document.provider_city,
157
+ size: 10,
158
+ at: [60, 605],
159
+ width: 240
160
+ )
161
+ if @document.provider_city_part && !@document.provider_city_part.empty?
162
+ @pdf.text_box(
163
+ @document.provider_city_part,
164
+ size: 10,
165
+ at: [60, 590],
166
+ width: 240
167
+ )
168
+ end
169
+ if @document.provider_extra_address_line && !@document.provider_extra_address_line.empty?
170
+ @pdf.text_box(
171
+ @document.provider_extra_address_line,
172
+ size: 10,
173
+ at: [10, 575],
174
+ width: 240
175
+ )
176
+ end
177
+ if @document.provider_ic && !@document.provider_ic.empty?
178
+ @pdf.text_box(
179
+ "#{@labels[:ic]}: #{@document.provider_ic}",
180
+ size: 10,
181
+ at: [10, 550],
182
+ width: 240
183
+ )
184
+ end
185
+ if @document.provider_dic && !@document.provider_dic.empty?
186
+ @pdf.text_box(
187
+ "#{@labels[:dic]}: #{@document.provider_dic}",
188
+ size: 10,
189
+ at: [10, 535],
190
+ width: 240
191
+ )
192
+ end
193
+ end
194
+
195
+ def build_purchaser_box
196
+ @pdf.text_box(
197
+ @labels[:purchaser],
198
+ size: 10,
199
+ at: [290, 660],
200
+ width: 240
201
+ )
202
+ @pdf.text_box(
203
+ @document.purchaser_name,
204
+ size: 14,
205
+ at: [290, 640],
206
+ width: 240
207
+ )
208
+ @pdf.text_box(
209
+ "#{@document.purchaser_street} #{@document.purchaser_street_number}",
210
+ size: 10,
211
+ at: [290, 620],
212
+ width: 240
213
+ )
214
+ @pdf.text_box(
215
+ @document.purchaser_postcode,
216
+ size: 10,
217
+ at: [290, 605],
218
+ width: 240
219
+ )
220
+ @pdf.text_box(
221
+ @document.purchaser_city,
222
+ size: 10,
223
+ at: [340, 605],
224
+ width: 240
225
+ )
226
+ if @document.purchaser_city_part && !@document.purchaser_city_part.empty?
227
+ @pdf.text_box(
228
+ @document.purchaser_city_part,
229
+ size: 10,
230
+ at: [340, 590],
231
+ width: 240
232
+ )
233
+ end
234
+ if @document.purchaser_extra_address_line && !@document.purchaser_extra_address_line.empty?
235
+ @pdf.text_box(
236
+ @document.purchaser_extra_address_line,
237
+ size: 10,
238
+ at: [290, 575],
239
+ width: 240
240
+ )
241
+ end
242
+ @pdf.stroke_rounded_rectangle([0, 670], 270, 150, 6)
243
+ @pdf.stroke_rounded_rectangle([280, 670], 270, 150, 6)
244
+ if @document.purchaser_dic && !@document.purchaser_dic.empty?
245
+ @pdf.text_box(
246
+ "#{@labels[:dic]}: #{@document.purchaser_dic}",
247
+ size: 10,
248
+ at: [290, 550],
249
+ width: 240
250
+ )
251
+ end
252
+ if @document.purchaser_ic && !@document.purchaser_ic.empty?
253
+ @pdf.text_box(
254
+ "#{@labels[:ic]}: #{@document.purchaser_ic}",
255
+ size: 10,
256
+ at: [290, 535],
257
+ width: 240
258
+ )
259
+ end
260
+ end
261
+
262
+ def build_payment_method_box
263
+ if @document.bank_account_number.nil?
264
+ @pdf.stroke_rounded_rectangle([0, 540 - @push_down], 270, 45, 6)
265
+ @pdf.text_box(
266
+ @labels[:payment],
267
+ size: 10,
268
+ at: [10, 530 - @push_down],
269
+ width: 240
270
+ )
271
+ @pdf.text_box(
272
+ @labels[:payment_in_cash],
273
+ size: 10,
274
+ at: [10, 515 - @push_down],
275
+ width: 240
276
+ )
277
+ return
278
+ end
279
+ box_height = 45
280
+ push_iban = 0
281
+ @pdf.text_box(
282
+ @labels[:payment_by_transfer],
283
+ size: 10,
284
+ at: [10, 530 - @push_down],
285
+ width: 240
286
+ )
287
+ @pdf.text_box(
288
+ "#{@labels[:account_number]}:",
289
+ size: 10,
290
+ at: [10, 515 - @push_down],
291
+ width: 240
292
+ )
293
+ @pdf.text_box(
294
+ @document.bank_account_number,
295
+ size: 10,
296
+ at: [75, 515 - @push_down],
297
+ width: 240
298
+ )
299
+ if @document.account_swift && !@document.account_swift.empty?
300
+ @pdf.text_box(
301
+ "#{@labels[:swift]}:",
302
+ size: 10,
303
+ at: [10, 500 - @push_down],
304
+ width: 240
305
+ )
306
+ @pdf.text_box(
307
+ @document.account_swift,
308
+ size: 10,
309
+ at: [75, 500 - @push_down],
310
+ width: 240
311
+ )
312
+ box_height += 15
313
+ push_iban = 15
314
+ end
315
+ if @document.account_iban && !@document.account_iban.empty?
316
+ @pdf.text_box(
317
+ "#{@labels[:iban]}:",
318
+ size: 10,
319
+ at: [10, 500 - push_iban - @push_down],
320
+ width: 240
321
+ )
322
+ @pdf.text_box(
323
+ @document.account_iban,
324
+ size: 10,
325
+ at: [75, 500 - push_iban - @push_down],
326
+ width: 240
327
+ )
328
+ box_height += 15
329
+ end
330
+ @pdf.stroke_rounded_rectangle([0, 540 - @push_down], 270, box_height, 6)
331
+ end
332
+
333
+ def build_info_box
334
+ issue_date_present = @document.issue_date && !@document.issue_date.empty?
335
+ due_date_present = @document.due_date && !@document.due_date.empty?
336
+ if issue_date_present
337
+ @pdf.text_box(
338
+ "#{@labels[:issue_date]}:",
339
+ size: 10,
340
+ at: [290, 530 - @push_down],
341
+ width: 240
342
+ )
343
+ @pdf.text_box(
344
+ @document.issue_date,
345
+ size: 10,
346
+ at: [390, 530 - @push_down],
347
+ width: 240
348
+ )
349
+ end
350
+ if due_date_present
351
+ position = issue_date_present ? 515 : 530
352
+ @pdf.text_box(
353
+ "#{@labels[:due_date]}:",
354
+ size: 10,
355
+ at: [290, position - @push_down],
356
+ width: 240
357
+ )
358
+ @pdf.text_box(
359
+ @document.due_date,
360
+ size: 10,
361
+ at: [390, position - @push_down],
362
+ width: 240
363
+ )
364
+ end
365
+ if issue_date_present || due_date_present
366
+ height = (issue_date_present && due_date_present) ? 45 : 30
367
+ @pdf.stroke_rounded_rectangle([280, 540 - @push_down], 270, height, 6)
368
+ end
369
+ end
370
+
371
+ # Build the following table for document items:
372
+ #
373
+ # |===============================================================|
374
+ # |Item | Quantity | Unit | Price per item | Tax | Total per item |
375
+ # |-----|----------|------|----------------|-----|----------------|
376
+ # | x | 2 | hr | $ 2 | $1 | $ 4 |
377
+ # |===============================================================|
378
+ #
379
+ # If a specific column miss data, it's omittted.
380
+ # Tax2 and tax3 fields can be added as well if necessary.
381
+ def build_items
382
+ @pdf.move_down(25 + @push_down)
383
+
384
+ items_params = determine_items_structure
385
+ items = build_items_data(items_params)
386
+ headers = build_items_header(items_params)
387
+
388
+ styles = {
389
+ headers: headers,
390
+ row_colors: ['F5F5F5', nil],
391
+ width: 550,
392
+ align: {
393
+ 0 => :left,
394
+ 1 => :right,
395
+ 2 => :right,
396
+ 3 => :right,
397
+ 4 => :right,
398
+ 5 => :right,
399
+ 6 => :right,
400
+ 7 => :right
401
+ }
402
+ }
403
+
404
+ @pdf.table(items, styles)
405
+ end
406
+
407
+ # Determine sections of the items table to show based on provided data
408
+ def determine_items_structure
409
+ items_params = {}
410
+
411
+ @document.items.each do |item|
412
+ items_params[:names] = true if item.name
413
+ items_params[:quantities] = true if item.quantity
414
+ items_params[:units] = true if item.unit
415
+ items_params[:prices] = true if item.price
416
+ items_params[:taxes] = true if item.tax
417
+ items_params[:taxes2] = true if item.tax2
418
+ items_params[:taxes3] = true if item.tax3
419
+ items_params[:amounts] = true if item.amount
420
+ end
421
+
422
+ items_params
423
+ end
424
+
425
+ # Include only items params with provided data
426
+ def build_items_data(items_params)
427
+ @document.items.map do |item|
428
+ line = []
429
+ line << item.name if items_params[:names]
430
+ line << item.quantity if items_params[:quantities]
431
+ line << item.unit if items_params[:units]
432
+ line << item.price if items_params[:prices]
433
+ line << item.tax if items_params[:taxes]
434
+ line << item.tax2 if items_params[:taxes2]
435
+ line << item.tax3 if items_params[:taxes3]
436
+ line << item.amount if items_params[:amounts]
437
+ line
438
+ end
439
+ end
440
+
441
+ # Include only relevant headers
442
+ def build_items_header(items_params)
443
+ headers = []
444
+ headers << { text: @labels[:item] } if items_params[:names]
445
+ headers << { text: @labels[:quantity] } if items_params[:quantities]
446
+ headers << { text: @labels[:unit] } if items_params[:units]
447
+ headers << { text: @labels[:price_per_item] } if items_params[:prices]
448
+ headers << { text: @labels[:tax] } if items_params[:taxes]
449
+ headers << { text: @labels[:tax2] } if items_params[:taxes2]
450
+ headers << { text: @labels[:tax3] } if items_params[:taxes3]
451
+ headers << { text: @labels[:amount] } if items_params[:amounts]
452
+ headers
453
+ end
454
+
455
+ # Build the following summary:
456
+ #
457
+ # Subtotal: 175
458
+ # Tax: 5
459
+ # Tax 2: 10
460
+ # Tax 3: 20
461
+ #
462
+ # Total: $ 200
463
+ #
464
+ # The first part is implemented as a table without borders.
465
+ def build_total
466
+ @pdf.move_down(25)
467
+
468
+ items = []
469
+ items << ["#{@labels[:subtotal]}:", @document.subtotal] if @document.subtotal
470
+ items << ["#{@labels[:tax]}:", @document.tax] if @document.tax
471
+ items << ["#{@labels[:tax2]}:", @document.tax2] if @document.tax2
472
+ items << ["#{@labels[:tax3]}:", @document.tax3] if @document.tax3
473
+
474
+ width = [
475
+ "#{@labels[:subtotal]}#{@document.subtotal}".size,
476
+ "#{@labels[:tax]}#{@document.tax}".size,
477
+ "#{@labels[:tax2]}#{@document.tax2}".size,
478
+ "#{@labels[:tax3]}#{@document.tax3}".size
479
+ ].max * 7
480
+
481
+ styles = {
482
+ border_width: 0,
483
+ align: {
484
+ 0 => :right,
485
+ 1 => :left
486
+ }
487
+ }
488
+
489
+ @pdf.span(width, position: :right) do
490
+ @pdf.table(items, styles)
491
+ end
492
+
493
+ @pdf.move_down(15)
494
+
495
+ @pdf.text(
496
+ "#{@labels[:total]}: #{@document.total}",
497
+ size: 16,
498
+ align: :right,
499
+ style: :bold
500
+ )
501
+ end
502
+
503
+ def build_logo
504
+ @pdf.image(@logo, at: [0, 50]) if @logo && !@logo.empty?
505
+ end
506
+
507
+ # Include page numbers if we got more than one page
508
+ def build_footer
509
+ @pdf.number_pages(
510
+ '<page> / <total>',
511
+ start_count_at: 1,
512
+ page_filter: ->(page) { page != 0 },
513
+ at: [@pdf.bounds.right - 50, 0],
514
+ align: :right,
515
+ size: 12
516
+ )
517
+ end
518
+ end
519
+ end