invoice_printer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|