invoice_printer 0.0.1

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.
@@ -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