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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +173 -0
- data/Rakefile +11 -0
- data/examples/czech_invoice.pdf +967 -0
- data/examples/czech_invoice.rb +79 -0
- data/examples/example.jpg +0 -0
- data/invoice_printer.gemspec +25 -0
- data/lib/invoice_printer.rb +67 -0
- data/lib/invoice_printer/document.rb +142 -0
- data/lib/invoice_printer/document/item.rb +49 -0
- data/lib/invoice_printer/pdf_document.rb +519 -0
- data/lib/invoice_printer/version.rb +3 -0
- data/test/dates_box_test.rb +53 -0
- data/test/invoice_printer_test.rb +14 -0
- data/test/items_table_test.rb +55 -0
- data/test/labels_test.rb +28 -0
- data/test/payment_box_test.rb +92 -0
- data/test/test_ext.rb +102 -0
- data/test/test_helper.rb +57 -0
- metadata +129 -0
@@ -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
|