invoice_printer 1.1.0 → 1.2.0.alpha1
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 +4 -4
- data/Gemfile +1 -0
- data/README.md +22 -290
- data/assets/logo.png +0 -0
- data/bin/invoice_printer +0 -3
- data/bin/invoice_printer_server +69 -0
- data/docs/COMMAND_LINE.md +21 -0
- data/docs/INSTALLATION.md +22 -0
- data/docs/LIBRARY.md +242 -0
- data/docs/SERVER.md +96 -0
- data/invoice_printer.gemspec +2 -1
- data/lib/invoice_printer.rb +5 -0
- data/lib/invoice_printer/document.rb +0 -2
- data/lib/invoice_printer/document/item.rb +0 -2
- data/lib/invoice_printer/server.rb +82 -0
- data/lib/invoice_printer/version.rb +1 -1
- data/test/api_test.rb +78 -0
- data/test/test_helper.rb +2 -0
- metadata +43 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8abb69910f861a930c17f4a59b2f00c5c7530556
|
4
|
+
data.tar.gz: b4b108dc6b68db259a24a9fca49db4eb78ab0a9b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 126cddc2f3e5cf893cf94f18abdd2b7966399f993f0ff29826adff0d1b77f26ea08c8de3feb6b036c4c5b12027ebbd90b41ef54f3b1b9fecc6e80bc88c9a530c
|
7
|
+
data.tar.gz: db9b42fb49fa1c303c0f6a6abeb1408cd2940960efc529107372f9581d21248eb982ce1f3eaf4e4bee23ce6d3bb759972aeabfd3f4611aec42cd295d961968e2
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,12 +1,22 @@
|
|
1
|
-
<
|
2
|
-
<img src="./docs/web/logo.png" width="300" />
|
3
|
-
</a>
|
1
|
+
<img src="./assets/logo.png" width="300" />
|
4
2
|
|
5
3
|
|
6
4
|
|
7
|
-
Super simple PDF invoicing
|
5
|
+
**Super simple PDF invoicing.** InvoicePrinter is a server, command line program and pure Ruby library to generate PDF invoices in no time. You can use Ruby or JSON as the invoice representation to build the final PDF.
|
8
6
|
|
9
|
-
|
7
|
+
## Philosophy
|
8
|
+
|
9
|
+
- **Simple**, no styling required, no calculation, no money formatting (bring your own)
|
10
|
+
- **Pure Ruby**, no dependency on system libraries or browsers
|
11
|
+
- **Fast**, so you can render invoice on the fly during request
|
12
|
+
|
13
|
+
## Examples
|
14
|
+
|
15
|
+
| Simple invoice |
|
16
|
+
| -------------- |
|
17
|
+
| <a href="https://github.com/strzibny/invoice_printer/raw/master/examples/promo.pdf"><img src="./examples/picture.jpg" width="180" /></a>|
|
18
|
+
|
19
|
+
See more usecases in the `examples/` directory.
|
10
20
|
|
11
21
|
## Features
|
12
22
|
|
@@ -26,296 +36,18 @@ InvoicePrinter is a Ruby library and a command line program. You can use Ruby or
|
|
26
36
|
- Note
|
27
37
|
- JSON format
|
28
38
|
- CLI
|
39
|
+
- Server
|
29
40
|
- Well tested
|
30
41
|
|
31
|
-
##
|
32
|
-
|
33
|
-
| Simple invoice |
|
34
|
-
| -------------- |
|
35
|
-
| <a href="https://github.com/strzibny/invoice_printer/raw/master/examples/promo.pdf"><img src="./examples/picture.jpg" width="180" /></a>|
|
36
|
-
|
37
|
-
See more usecases in the `examples/` directory.
|
38
|
-
|
39
|
-
## Installation
|
40
|
-
|
41
|
-
Add this line to your application's Gemfile:
|
42
|
-
|
43
|
-
```ruby
|
44
|
-
gem 'invoice_printer'
|
45
|
-
```
|
46
|
-
|
47
|
-
And then execute:
|
48
|
-
|
49
|
-
$ bundle
|
50
|
-
|
51
|
-
Or install it yourself as:
|
52
|
-
|
53
|
-
$ gem install invoice_printer
|
54
|
-
|
55
|
-
## Usage
|
56
|
-
|
57
|
-
The simplest way how to create your invoice PDF is to create an invoice object
|
58
|
-
and pass it to printer:
|
59
|
-
|
60
|
-
```ruby
|
61
|
-
item = InvoicePrinter::Document::Item.new(
|
62
|
-
...
|
63
|
-
)
|
64
|
-
|
65
|
-
invoice = InvoicePrinter::Document.new(
|
66
|
-
...
|
67
|
-
items: [item, ...]
|
68
|
-
)
|
69
|
-
|
70
|
-
InvoicePrinter.print(
|
71
|
-
document: invoice,
|
72
|
-
file_name: 'invoice.pdf'
|
73
|
-
)
|
74
|
-
|
75
|
-
# Or render PDF directly
|
76
|
-
InvoicePrinter.render(
|
77
|
-
document: invoice
|
78
|
-
)
|
79
|
-
```
|
80
|
-
|
81
|
-
Here is an full example for creating the document object:
|
82
|
-
|
83
|
-
```ruby
|
84
|
-
item = InvoicePrinter::Document::Item.new(
|
85
|
-
name: 'Web consultation',
|
86
|
-
quantity: nil,
|
87
|
-
unit: 'hours',
|
88
|
-
price: '$ 25',
|
89
|
-
tax: '$ 1',
|
90
|
-
amount: '$ 100'
|
91
|
-
)
|
92
|
-
|
93
|
-
invoice = InvoicePrinter::Document.new(
|
94
|
-
number: '201604030001',
|
95
|
-
provider_name: 'Business s.r.o.',
|
96
|
-
provider_tax_id: '56565656',
|
97
|
-
provider_tax_id2: '465454',
|
98
|
-
provider_street: 'Rolnicka',
|
99
|
-
provider_street_number: '1',
|
100
|
-
provider_postcode: '747 05',
|
101
|
-
provider_city: 'Opava',
|
102
|
-
provider_city_part: 'Katerinky',
|
103
|
-
provider_extra_address_line: 'Czech Republic',
|
104
|
-
purchaser_name: 'Adam',
|
105
|
-
purchaser_tax_id: '',
|
106
|
-
purchaser_tax_id2: '',
|
107
|
-
purchaser_street: 'Ostravska',
|
108
|
-
purchaser_street_number: '1',
|
109
|
-
purchaser_postcode: '747 70',
|
110
|
-
purchaser_city: 'Opava',
|
111
|
-
purchaser_city_part: '',
|
112
|
-
purchaser_extra_address_line: '',
|
113
|
-
issue_date: '19/03/3939',
|
114
|
-
due_date: '19/03/3939',
|
115
|
-
subtotal: '175',
|
116
|
-
tax: '5',
|
117
|
-
tax2: '10',
|
118
|
-
tax3: '20',
|
119
|
-
total: '$ 200',
|
120
|
-
bank_account_number: '156546546465',
|
121
|
-
account_iban: 'IBAN464545645',
|
122
|
-
account_swift: 'SWIFT5456',
|
123
|
-
items: [item],
|
124
|
-
note: 'A note...'
|
125
|
-
)
|
126
|
-
```
|
127
|
-
|
128
|
-
### Ruby on Rails
|
129
|
-
|
130
|
-
If you want to use InvoicePrinter for printing PDF documents directly from Rails
|
131
|
-
actions, you can:
|
132
|
-
|
133
|
-
```ruby
|
134
|
-
# GET /invoices/1
|
135
|
-
def show
|
136
|
-
invoice = InvoicePrinter::Document.new(...)
|
137
|
-
|
138
|
-
respond_to do |format|
|
139
|
-
format.pdf {
|
140
|
-
@pdf = InvoicePrinter.render(
|
141
|
-
document: invoice
|
142
|
-
)
|
143
|
-
send_data @pdf, type: 'application/pdf', disposition: 'inline'
|
144
|
-
}
|
145
|
-
end
|
146
|
-
end
|
147
|
-
```
|
148
|
-
|
149
|
-
### JSON format
|
150
|
-
|
151
|
-
JSON format is supported via `from_json` method. JSON itself mimicks the original Ruby objects:
|
152
|
-
|
153
|
-
```ruby
|
154
|
-
json = InvoicePrinter::Document.new(...).to_json
|
155
|
-
document = InvoicePrinter::Document.from_json(json)
|
156
|
-
|
157
|
-
|
158
|
-
InvoicePrinter.print(
|
159
|
-
document: document,
|
160
|
-
...
|
161
|
-
)
|
162
|
-
|
163
|
-
```
|
164
|
-
|
165
|
-
## CLI
|
166
|
-
|
167
|
-
InvoicePrinter ships with a command line executable called `invoice_printer`.
|
168
|
-
|
169
|
-
It supports all features except it only accepts JSON as an input.
|
170
|
-
|
171
|
-
```
|
172
|
-
$ invoice_printer --help
|
173
|
-
Usage: invoice_printer [options]
|
174
|
-
|
175
|
-
Options:
|
176
|
-
|
177
|
-
-l, --labels labels as JSON
|
178
|
-
-d, --document document as JSON
|
179
|
-
-s, --stamp path to stamp
|
180
|
-
--logo path to logotype
|
181
|
-
--font path to font
|
182
|
-
--page_size letter or a4 (letter is the default)
|
183
|
-
-f, --filename output path
|
184
|
-
-r, --render directly render PDF stream (filename option will be ignored)
|
185
|
-
```
|
186
|
-
|
187
|
-
|
188
|
-
## Customization
|
189
|
-
|
190
|
-
### Page size
|
191
|
-
|
192
|
-
Both A4 and US letter is supported. Just pass `page_size` as an argument to `print` or `render` methods:
|
193
|
-
|
194
|
-
```ruby
|
195
|
-
InvoicePrinter.print(
|
196
|
-
document: invoice,
|
197
|
-
labels: labels,
|
198
|
-
page_size: :a4,
|
199
|
-
file_name: 'invoice.pdf'
|
200
|
-
)
|
201
|
-
```
|
202
|
-
|
203
|
-
`:letter` is the default.
|
204
|
-
|
205
|
-
|
206
|
-
### Localization
|
207
|
-
|
208
|
-
To localize your documents you can set both global defaults or instance
|
209
|
-
overrides:
|
210
|
-
|
211
|
-
```ruby
|
212
|
-
InvoicePrinter.labels = {
|
213
|
-
provider: 'Dodavatel'
|
214
|
-
}
|
215
|
-
|
216
|
-
labels = {
|
217
|
-
purchaser: 'Customer'
|
218
|
-
}
|
219
|
-
|
220
|
-
InvoicePrinter.print(
|
221
|
-
document: invoice,
|
222
|
-
labels: labels,
|
223
|
-
file_name: 'invoice.pdf'
|
224
|
-
)
|
225
|
-
```
|
226
|
-
|
227
|
-
Here is the full list of labels to configure. You can paste and edit this block
|
228
|
-
to `initializers/invoice_printer.rb` if you are using Rails.
|
229
|
-
|
230
|
-
```ruby
|
231
|
-
InvoicePrinter.labels = {
|
232
|
-
name: 'Invoice',
|
233
|
-
provider: 'Provider',
|
234
|
-
purchaser: 'Purchaser',
|
235
|
-
tax_id: 'Identification number',
|
236
|
-
tax_id2: 'Identification number',
|
237
|
-
payment: 'Payment',
|
238
|
-
payment_by_transfer: 'Payment by bank transfer on the account below:',
|
239
|
-
payment_in_cash: 'Payment in cash',
|
240
|
-
account_number: 'Account NO',
|
241
|
-
swift: 'SWIFT',
|
242
|
-
iban: 'IBAN',
|
243
|
-
issue_date: 'Issue date',
|
244
|
-
due_date: 'Due date',
|
245
|
-
item: 'Item',
|
246
|
-
quantity: 'Quantity',
|
247
|
-
unit: 'Unit',
|
248
|
-
price_per_item: 'Price per item',
|
249
|
-
amount: 'Amount',
|
250
|
-
tax: 'Tax',
|
251
|
-
tax2: 'Tax 2',
|
252
|
-
tax3: 'Tax 3',
|
253
|
-
subtotal: 'Subtotal',
|
254
|
-
total: 'Total'
|
255
|
-
}
|
256
|
-
```
|
257
|
-
|
258
|
-
You can also use sublabels feature to provide the document in two languages:
|
259
|
-
|
260
|
-
```ruby
|
261
|
-
labels = {
|
262
|
-
...
|
263
|
-
}
|
264
|
-
|
265
|
-
sublabels = {
|
266
|
-
name: 'Faktura',
|
267
|
-
provider: 'Prodejce',
|
268
|
-
purchaser: 'Kupující',
|
269
|
-
tax_id: 'IČ',
|
270
|
-
tax_id2: 'DIČ',
|
271
|
-
payment: 'Forma úhrady',
|
272
|
-
payment_by_transfer: 'Platba na následující účet:',
|
273
|
-
account_number: 'Číslo účtu',
|
274
|
-
issue_date: 'Datum vydání',
|
275
|
-
due_date: 'Datum splatnosti',
|
276
|
-
item: 'Položka',
|
277
|
-
quantity: 'Počet',
|
278
|
-
unit: 'MJ',
|
279
|
-
price_per_item: 'Cena za položku',
|
280
|
-
amount: 'Celkem bez daně',
|
281
|
-
subtotal: 'Cena bez daně',
|
282
|
-
tax: 'DPH 21 %',
|
283
|
-
total: 'Celkem'
|
284
|
-
}
|
285
|
-
|
286
|
-
labels.merge!({ sublabels: sublabels })
|
287
|
-
|
288
|
-
...
|
289
|
-
```
|
290
|
-
|
291
|
-
Now the document will have a little sublabels next to the original labels in Czech.
|
292
|
-
|
293
|
-
### Font
|
294
|
-
|
295
|
-
To support specific characters you might need to specify a TTF font to be used:
|
296
|
-
|
297
|
-
``` ruby
|
298
|
-
InvoicePrinter.print(
|
299
|
-
...
|
300
|
-
font: File.expand_path('../Overpass-Regular.ttf', __FILE__)
|
301
|
-
)
|
302
|
-
```
|
303
|
-
|
304
|
-
We recommend you DejaVuSans and Overpass fonts.
|
305
|
-
|
306
|
-
### Background
|
307
|
-
|
308
|
-
To include a background image you might need to create the file according to the size and resolution to be used (see: [examples/background.png](https://github.com/strzibny/invoice_printer/blob/master/examples/background.png)):
|
42
|
+
## Documentation
|
309
43
|
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
)
|
315
|
-
```
|
44
|
+
- [Installation](./docs/INSTALLATION.md)
|
45
|
+
- [Ruby library](./docs/LIBRARY.md)
|
46
|
+
- [Server](./docs/SERVER.md)
|
47
|
+
- [Command line](./docs/COMMAND_LINE.md)
|
316
48
|
|
317
49
|
## Copyright
|
318
50
|
|
319
|
-
Copyright 2015-
|
51
|
+
Copyright 2015-2018 © [Josef Strzibny](http://strzibny.name/). MIT licensed.
|
320
52
|
|
321
53
|
Originally extracted from and created for an open source single-entry invoicing app [InvoiceBar](https://github.com/strzibny/invoicebar).
|
data/assets/logo.png
ADDED
Binary file
|
data/bin/invoice_printer
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
$LOAD_PATH << File.expand_path('lib')
|
3
3
|
|
4
4
|
require 'optparse'
|
5
|
-
require 'json'
|
6
5
|
require 'invoice_printer'
|
7
6
|
|
8
7
|
def show_version
|
@@ -34,8 +33,6 @@ end
|
|
34
33
|
options = {}
|
35
34
|
|
36
35
|
parser = OptionParser.new do|opts|
|
37
|
-
opts.banner = "Usage: invoice_printer [options]"
|
38
|
-
|
39
36
|
opts.on('-l', '--labels JSON') do |json|
|
40
37
|
options[:labels] = json
|
41
38
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH << File.expand_path('lib')
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'rack/handler/puma'
|
6
|
+
require 'invoice_printer/server'
|
7
|
+
|
8
|
+
def show_version
|
9
|
+
puts "InvoicePrinter v#{InvoicePrinter::VERSION}"
|
10
|
+
|
11
|
+
exit 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def show_help
|
15
|
+
puts <<~HELP
|
16
|
+
Usage: invoice_printer_server [options]
|
17
|
+
|
18
|
+
Options:
|
19
|
+
|
20
|
+
-h, --host hostname to listen on (default is 0.0.0.0)
|
21
|
+
-p, --port port to listen on (default is 9393)
|
22
|
+
-w, --workers number of Puma workers (default is 2)
|
23
|
+
|
24
|
+
HELP
|
25
|
+
|
26
|
+
exit 0
|
27
|
+
end
|
28
|
+
|
29
|
+
options = {}
|
30
|
+
|
31
|
+
parser = OptionParser.new do|opts|
|
32
|
+
opts.on('-h', '--hostname ADDRESS') do |address|
|
33
|
+
options[:Host] = address
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on('-p', '--port NUMBER') do |number|
|
37
|
+
options[:Port] = number
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on('-w', '--workers NUMBER') do |number|
|
41
|
+
options[:workers] = number.to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.on('--debug') do
|
45
|
+
options[:debug] = true
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on('-h', '--help') do
|
49
|
+
show_help
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
parser.parse!
|
54
|
+
|
55
|
+
puma_options = { :Host => '0.0.0.0', :Port => 9393, :workers => 2 }.merge(options)
|
56
|
+
|
57
|
+
begin
|
58
|
+
puts 'Starting InvoicePrinter Server...'
|
59
|
+
Rack::Handler::Puma.run(InvoicePrinter::Server.freeze.app, puma_options)
|
60
|
+
rescue => e
|
61
|
+
STDERR.puts "ERROR: #{e.message}"
|
62
|
+
|
63
|
+
if options[:debug]
|
64
|
+
STDERR.puts
|
65
|
+
STDERR.puts e.backtrace
|
66
|
+
end
|
67
|
+
|
68
|
+
exit 1
|
69
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# InvoicePrinter CLI
|
2
|
+
|
3
|
+
InvoicePrinter ships with a command line executable called `invoice_printer`.
|
4
|
+
|
5
|
+
It supports all features except it only accepts JSON as an input.
|
6
|
+
|
7
|
+
```
|
8
|
+
$ invoice_printer --help
|
9
|
+
Usage: invoice_printer [options]
|
10
|
+
|
11
|
+
Options:
|
12
|
+
|
13
|
+
-l, --labels labels as JSON
|
14
|
+
-d, --document document as JSON
|
15
|
+
-s, --stamp path to stamp
|
16
|
+
--logo path to logotype
|
17
|
+
--font path to font
|
18
|
+
--page_size letter or a4 (letter is the default)
|
19
|
+
-f, --filename output path
|
20
|
+
-r, --render directly render PDF stream (filename option will be ignored)
|
21
|
+
```
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Installation
|
2
|
+
|
3
|
+
## Via RubyGems
|
4
|
+
|
5
|
+
This requires Ruby to be installed.
|
6
|
+
|
7
|
+
Then install the gem as:
|
8
|
+
|
9
|
+
$ gem install invoice_printer
|
10
|
+
|
11
|
+
|
12
|
+
### With Bundler
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem 'invoice_printer'
|
18
|
+
```
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle
|
data/docs/LIBRARY.md
ADDED
@@ -0,0 +1,242 @@
|
|
1
|
+
# InvoicePrinter Library
|
2
|
+
|
3
|
+
## Usage
|
4
|
+
|
5
|
+
The simplest way how to create your invoice PDF is to create an invoice object
|
6
|
+
and pass it to printer:
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
item = InvoicePrinter::Document::Item.new(
|
10
|
+
...
|
11
|
+
)
|
12
|
+
|
13
|
+
invoice = InvoicePrinter::Document.new(
|
14
|
+
...
|
15
|
+
items: [item, ...]
|
16
|
+
)
|
17
|
+
|
18
|
+
InvoicePrinter.print(
|
19
|
+
document: invoice,
|
20
|
+
file_name: 'invoice.pdf'
|
21
|
+
)
|
22
|
+
|
23
|
+
# Or render PDF directly
|
24
|
+
InvoicePrinter.render(
|
25
|
+
document: invoice
|
26
|
+
)
|
27
|
+
```
|
28
|
+
|
29
|
+
Here is an full example for creating the document object:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
item = InvoicePrinter::Document::Item.new(
|
33
|
+
name: 'Web consultation',
|
34
|
+
quantity: nil,
|
35
|
+
unit: 'hours',
|
36
|
+
price: '$ 25',
|
37
|
+
tax: '$ 1',
|
38
|
+
amount: '$ 100'
|
39
|
+
)
|
40
|
+
|
41
|
+
invoice = InvoicePrinter::Document.new(
|
42
|
+
number: '201604030001',
|
43
|
+
provider_name: 'Business s.r.o.',
|
44
|
+
provider_tax_id: '56565656',
|
45
|
+
provider_tax_id2: '465454',
|
46
|
+
provider_street: 'Rolnicka',
|
47
|
+
provider_street_number: '1',
|
48
|
+
provider_postcode: '747 05',
|
49
|
+
provider_city: 'Opava',
|
50
|
+
provider_city_part: 'Katerinky',
|
51
|
+
provider_extra_address_line: 'Czech Republic',
|
52
|
+
purchaser_name: 'Adam',
|
53
|
+
purchaser_tax_id: '',
|
54
|
+
purchaser_tax_id2: '',
|
55
|
+
purchaser_street: 'Ostravska',
|
56
|
+
purchaser_street_number: '1',
|
57
|
+
purchaser_postcode: '747 70',
|
58
|
+
purchaser_city: 'Opava',
|
59
|
+
purchaser_city_part: '',
|
60
|
+
purchaser_extra_address_line: '',
|
61
|
+
issue_date: '19/03/3939',
|
62
|
+
due_date: '19/03/3939',
|
63
|
+
subtotal: '175',
|
64
|
+
tax: '5',
|
65
|
+
tax2: '10',
|
66
|
+
tax3: '20',
|
67
|
+
total: '$ 200',
|
68
|
+
bank_account_number: '156546546465',
|
69
|
+
account_iban: 'IBAN464545645',
|
70
|
+
account_swift: 'SWIFT5456',
|
71
|
+
items: [item],
|
72
|
+
note: 'A note...'
|
73
|
+
)
|
74
|
+
```
|
75
|
+
|
76
|
+
### Ruby on Rails
|
77
|
+
|
78
|
+
If you want to use InvoicePrinter for printing PDF documents directly from Rails
|
79
|
+
actions, you can:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
# GET /invoices/1
|
83
|
+
def show
|
84
|
+
invoice = InvoicePrinter::Document.new(...)
|
85
|
+
|
86
|
+
respond_to do |format|
|
87
|
+
format.pdf {
|
88
|
+
@pdf = InvoicePrinter.render(
|
89
|
+
document: invoice
|
90
|
+
)
|
91
|
+
send_data @pdf, type: 'application/pdf', disposition: 'inline'
|
92
|
+
}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
### JSON format
|
98
|
+
|
99
|
+
JSON format is supported via `from_json` method. JSON itself mimicks the original Ruby objects:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
json = InvoicePrinter::Document.new(...).to_json
|
103
|
+
document = InvoicePrinter::Document.from_json(json)
|
104
|
+
|
105
|
+
|
106
|
+
InvoicePrinter.print(
|
107
|
+
document: document,
|
108
|
+
...
|
109
|
+
)
|
110
|
+
|
111
|
+
```
|
112
|
+
|
113
|
+
|
114
|
+
|
115
|
+
## Customization
|
116
|
+
|
117
|
+
### Page size
|
118
|
+
|
119
|
+
Both A4 and US letter is supported. Just pass `page_size` as an argument to `print` or `render` methods:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
InvoicePrinter.print(
|
123
|
+
document: invoice,
|
124
|
+
labels: labels,
|
125
|
+
page_size: :a4,
|
126
|
+
file_name: 'invoice.pdf'
|
127
|
+
)
|
128
|
+
```
|
129
|
+
|
130
|
+
`:letter` is the default.
|
131
|
+
|
132
|
+
|
133
|
+
### Localization
|
134
|
+
|
135
|
+
To localize your documents you can set both global defaults or instance
|
136
|
+
overrides:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
InvoicePrinter.labels = {
|
140
|
+
provider: 'Dodavatel'
|
141
|
+
}
|
142
|
+
|
143
|
+
labels = {
|
144
|
+
purchaser: 'Customer'
|
145
|
+
}
|
146
|
+
|
147
|
+
InvoicePrinter.print(
|
148
|
+
document: invoice,
|
149
|
+
labels: labels,
|
150
|
+
file_name: 'invoice.pdf'
|
151
|
+
)
|
152
|
+
```
|
153
|
+
|
154
|
+
Here is the full list of labels to configure. You can paste and edit this block
|
155
|
+
to `initializers/invoice_printer.rb` if you are using Rails.
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
InvoicePrinter.labels = {
|
159
|
+
name: 'Invoice',
|
160
|
+
provider: 'Provider',
|
161
|
+
purchaser: 'Purchaser',
|
162
|
+
tax_id: 'Identification number',
|
163
|
+
tax_id2: 'Identification number',
|
164
|
+
payment: 'Payment',
|
165
|
+
payment_by_transfer: 'Payment by bank transfer on the account below:',
|
166
|
+
payment_in_cash: 'Payment in cash',
|
167
|
+
account_number: 'Account NO',
|
168
|
+
swift: 'SWIFT',
|
169
|
+
iban: 'IBAN',
|
170
|
+
issue_date: 'Issue date',
|
171
|
+
due_date: 'Due date',
|
172
|
+
item: 'Item',
|
173
|
+
quantity: 'Quantity',
|
174
|
+
unit: 'Unit',
|
175
|
+
price_per_item: 'Price per item',
|
176
|
+
amount: 'Amount',
|
177
|
+
tax: 'Tax',
|
178
|
+
tax2: 'Tax 2',
|
179
|
+
tax3: 'Tax 3',
|
180
|
+
subtotal: 'Subtotal',
|
181
|
+
total: 'Total'
|
182
|
+
}
|
183
|
+
```
|
184
|
+
|
185
|
+
You can also use sublabels feature to provide the document in two languages:
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
labels = {
|
189
|
+
...
|
190
|
+
}
|
191
|
+
|
192
|
+
sublabels = {
|
193
|
+
name: 'Faktura',
|
194
|
+
provider: 'Prodejce',
|
195
|
+
purchaser: 'Kupující',
|
196
|
+
tax_id: 'IČ',
|
197
|
+
tax_id2: 'DIČ',
|
198
|
+
payment: 'Forma úhrady',
|
199
|
+
payment_by_transfer: 'Platba na následující účet:',
|
200
|
+
account_number: 'Číslo účtu',
|
201
|
+
issue_date: 'Datum vydání',
|
202
|
+
due_date: 'Datum splatnosti',
|
203
|
+
item: 'Položka',
|
204
|
+
quantity: 'Počet',
|
205
|
+
unit: 'MJ',
|
206
|
+
price_per_item: 'Cena za položku',
|
207
|
+
amount: 'Celkem bez daně',
|
208
|
+
subtotal: 'Cena bez daně',
|
209
|
+
tax: 'DPH 21 %',
|
210
|
+
total: 'Celkem'
|
211
|
+
}
|
212
|
+
|
213
|
+
labels.merge!({ sublabels: sublabels })
|
214
|
+
|
215
|
+
...
|
216
|
+
```
|
217
|
+
|
218
|
+
Now the document will have a little sublabels next to the original labels in Czech.
|
219
|
+
|
220
|
+
### Font
|
221
|
+
|
222
|
+
To support specific characters you might need to specify a TTF font to be used:
|
223
|
+
|
224
|
+
``` ruby
|
225
|
+
InvoicePrinter.print(
|
226
|
+
...
|
227
|
+
font: File.expand_path('../Overpass-Regular.ttf', __FILE__)
|
228
|
+
)
|
229
|
+
```
|
230
|
+
|
231
|
+
We recommend you DejaVuSans and Overpass fonts.
|
232
|
+
|
233
|
+
### Background
|
234
|
+
|
235
|
+
To include a background image you might need to create the file according to the size and resolution to be used (see: [examples/background.png](https://github.com/strzibny/invoice_printer/blob/master/examples/background.png)):
|
236
|
+
|
237
|
+
``` ruby
|
238
|
+
InvoicePrinter.print(
|
239
|
+
...
|
240
|
+
background: File.expand_path('../background.png', __FILE__)
|
241
|
+
)
|
242
|
+
```
|
data/docs/SERVER.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# InvoicePrinter Server
|
2
|
+
|
3
|
+
InvoicePrinter contains a built in server that can be run from a command line with `invoice_printer_server`.
|
4
|
+
|
5
|
+
Apart from this you can also manually mount the server inside of your Rack application.
|
6
|
+
|
7
|
+
## Running the server
|
8
|
+
|
9
|
+
### From command line
|
10
|
+
|
11
|
+
Once installed, InvoicePrinter provides `invoice_printer_server` executable that starts the Puma server:
|
12
|
+
|
13
|
+
```bash
|
14
|
+
invoice_printer_server -h 0.0.0.0 -p 5000
|
15
|
+
```
|
16
|
+
|
17
|
+
`-h` defines a host and `-p` defines a port. For help you can run `--help`.
|
18
|
+
|
19
|
+
By default server binds to `0.0.0.0:9393`.
|
20
|
+
|
21
|
+
### As mountable Rack app
|
22
|
+
|
23
|
+
If you want you can always run the server from your custom program or mount it directly from a Rack app.
|
24
|
+
|
25
|
+
`InvoicePrinter::Server` is a Rack app as any other. Example:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require 'rack/handler/puma'
|
29
|
+
require 'invoice_printer/server'
|
30
|
+
|
31
|
+
Rack::Handler::Puma.run InvoicePrinter::Server.freeze.app
|
32
|
+
```
|
33
|
+
|
34
|
+
## Available API
|
35
|
+
|
36
|
+
Endpoints accept similar arguments as the corresponding methods to `InvoicePrinter`. `render` is used for directly getting the PDF output whereas `print` would accept `filename` option and save the document to that
|
37
|
+
file.
|
38
|
+
|
39
|
+
A content type is always `application/json` both for requests and responses.
|
40
|
+
|
41
|
+
### `POST /render`
|
42
|
+
|
43
|
+
Directly render PDF data.
|
44
|
+
|
45
|
+
Options:
|
46
|
+
|
47
|
+
- `document` - JSON representation of the document
|
48
|
+
- `labels` - JSON for labels
|
49
|
+
- `stamp` - path to stamp file
|
50
|
+
- `logo` - path to logotype file
|
51
|
+
- `font` - path to font file
|
52
|
+
|
53
|
+
On success a `200` response is returned:
|
54
|
+
|
55
|
+
```json
|
56
|
+
{ "result": "ok", "data": "base64 encoded PDF document" }
|
57
|
+
```
|
58
|
+
|
59
|
+
On error a `400` response is returned:
|
60
|
+
|
61
|
+
```json
|
62
|
+
{ "result": "error", "error": "error description" }
|
63
|
+
```
|
64
|
+
|
65
|
+
#### Example
|
66
|
+
|
67
|
+
Example of calling the API to render a document using `curl`:
|
68
|
+
|
69
|
+
```
|
70
|
+
$ curl -X POST http://0.0.0.0:9393/render -H "Content-Type: application/json" --data '{"document":{"number":"c. 198900000001","provider_name":"Petr Novy","provider_tax_id":"56565656","provider_tax_id2":"","provider_street":"Rolnicka","provider_street_number":"1","provider_postcode":"747 05","provider_city":"Opava","provider_city_part":"Katerinky","provider_extra_address_line":"","purchaser_name":"Adam Cerny","purchaser_tax_id":"","purchaser_tax_id2":"","purchaser_street":"Ostravska","purchaser_street_number":"1","purchaser_postcode":"747 70","purchaser_city":"Opava","purchaser_city_part":"","purchaser_extra_address_line":"","issue_date":"05/03/2016","due_date":"19/03/2016","subtotal":"Kc 10.000","tax":"Kc 2.100","tax2":"","tax3":"","total":"Kc 12.100,-","bank_account_number":"156546546465","account_iban":"IBAN464545645","account_swift":"SWIFT5456","items":[{"name":"Konzultace","quantity":"2","unit":"hod","price":"Kc 500","tax":"","tax2":"","tax3":"","amount":"Kc 1.000"},{"name":"Programovani","quantity":"10","unit":"hod","price":"Kc 900","tax":"","tax2":"","tax3":"","amount":"Kc 9.000"}],"note":"Osoba je zapsána v zivnostenském rejstríku."}}'
|
71
|
+
```
|
72
|
+
|
73
|
+
### `POST /print`
|
74
|
+
|
75
|
+
Print resulting document to a file.
|
76
|
+
|
77
|
+
Options:
|
78
|
+
|
79
|
+
- `document` - JSON representation of the document
|
80
|
+
- `labels` - JSON for labels
|
81
|
+
- `stamp` - path to stamp file
|
82
|
+
- `logo` - path to logotype file
|
83
|
+
- `font` - path to font file
|
84
|
+
- `filename` - path for saving the generated output PDF
|
85
|
+
|
86
|
+
On success a `200` response is returned:
|
87
|
+
|
88
|
+
```json
|
89
|
+
{ "result": "ok", "path": "/path/basically/what/was/sent/as/filepath" }
|
90
|
+
```
|
91
|
+
|
92
|
+
On error a `400` response is returned:
|
93
|
+
|
94
|
+
```json
|
95
|
+
{ "result": "error", "error": "error description" }
|
96
|
+
```
|
data/invoice_printer.gemspec
CHANGED
@@ -15,7 +15,6 @@ Gem::Specification.new do |spec|
|
|
15
15
|
# Remove .pdf files as they take a lot of space
|
16
16
|
package_files = `git ls-files -z`.split("\x0")
|
17
17
|
.reject{ |file| file.match /.*\.pdf/ }
|
18
|
-
.reject{ |file| file.match /docs\/.*/ }
|
19
18
|
|
20
19
|
spec.files = package_files
|
21
20
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
@@ -24,6 +23,8 @@ Gem::Specification.new do |spec|
|
|
24
23
|
spec.bindir = 'bin'
|
25
24
|
|
26
25
|
spec.add_dependency 'json', '~> 2.1'
|
26
|
+
spec.add_dependency 'roda', '3.5.0'
|
27
|
+
spec.add_dependency 'puma', '~> 3.9'
|
27
28
|
spec.add_dependency 'prawn', '2.1.0'
|
28
29
|
spec.add_dependency 'prawn-layout', '0.8.4'
|
29
30
|
|
data/lib/invoice_printer.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
+
require 'json'
|
1
2
|
require 'invoice_printer/version'
|
3
|
+
require 'invoice_printer/document'
|
2
4
|
require 'invoice_printer/document/item'
|
3
5
|
require 'invoice_printer/pdf_document'
|
4
6
|
|
7
|
+
# Skip warning for not specifying TTF font
|
8
|
+
Prawn::Font::AFM.hide_m17n_warning = true
|
9
|
+
|
5
10
|
# Create PDF versions of invoices or receipts using Prawn
|
6
11
|
#
|
7
12
|
# Example:
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'roda'
|
3
|
+
require 'invoice_printer'
|
4
|
+
|
5
|
+
class InvoicePrinter::Server < Roda
|
6
|
+
route do |r|
|
7
|
+
response['Content-Type'] = 'application/json'
|
8
|
+
|
9
|
+
# Assign params
|
10
|
+
if r.content_type == 'application/json'
|
11
|
+
begin
|
12
|
+
params = JSON.parse(r.body.read, symbolize_names: true)
|
13
|
+
rescue => e
|
14
|
+
response.status = 400
|
15
|
+
response.write({ result: 'error', error: 'Invalid JSON.' }.to_json)
|
16
|
+
r.halt
|
17
|
+
end
|
18
|
+
|
19
|
+
labels = params[:labels]
|
20
|
+
document = params[:document]
|
21
|
+
stamp = params[:stamp]
|
22
|
+
logo = params[:logo]
|
23
|
+
font = params[:font]
|
24
|
+
else
|
25
|
+
response.status = 400
|
26
|
+
response.write({ result: 'error', error: 'No JSON. Did you set Content-Type to application/json?' }.to_json)
|
27
|
+
r.halt
|
28
|
+
end
|
29
|
+
|
30
|
+
# Initialize InvoicePrinter::Document from given JSON
|
31
|
+
begin
|
32
|
+
document[:items] ||= []
|
33
|
+
items = document[:items].map { |item| InvoicePrinter::Document::Item.new(**item) }
|
34
|
+
document[:items] = items
|
35
|
+
|
36
|
+
document = InvoicePrinter::Document.new(**document)
|
37
|
+
rescue => e
|
38
|
+
response.status = 400
|
39
|
+
response.write({ result: 'error', error: 'Invalid JSON document.' }.to_json)
|
40
|
+
r.halt
|
41
|
+
end
|
42
|
+
|
43
|
+
# POST /print
|
44
|
+
r.post 'print' do
|
45
|
+
filename = params[:filename] || 'document.pdf'
|
46
|
+
|
47
|
+
begin
|
48
|
+
InvoicePrinter.print(
|
49
|
+
document: document,
|
50
|
+
font: font,
|
51
|
+
stamp: stamp,
|
52
|
+
logo: logo,
|
53
|
+
file_name: filename
|
54
|
+
)
|
55
|
+
|
56
|
+
{ result: 'ok', path: filename }.to_json
|
57
|
+
rescue => e
|
58
|
+
response.status = 400
|
59
|
+
response.write({ result: 'error', error: e.message }.to_json)
|
60
|
+
r.halt
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# POST /render
|
65
|
+
r.post 'render' do
|
66
|
+
begin
|
67
|
+
stream = InvoicePrinter.render(
|
68
|
+
document: document,
|
69
|
+
font: font,
|
70
|
+
stamp: stamp,
|
71
|
+
logo: logo
|
72
|
+
)
|
73
|
+
|
74
|
+
{ result: 'ok', data: Base64.encode64(stream) }.to_json
|
75
|
+
rescue => e
|
76
|
+
response.status = 400
|
77
|
+
response.write({ result: 'error', error: e.message }.to_json)
|
78
|
+
r.halt
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/test/api_test.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'invoice_printer/server'
|
3
|
+
|
4
|
+
class ApiTest < Minitest::Test
|
5
|
+
include Rack::Test::Methods
|
6
|
+
include InvoicePrinterHelpers
|
7
|
+
|
8
|
+
def app
|
9
|
+
InvoicePrinter::Server.freeze.app
|
10
|
+
end
|
11
|
+
|
12
|
+
def setup
|
13
|
+
@test_dir = File.absolute_path('./tmp/invoice_printer_api')
|
14
|
+
FileUtils.mkdir_p @test_dir
|
15
|
+
end
|
16
|
+
|
17
|
+
def teardown
|
18
|
+
FileUtils.rm_rf @test_dir if File.exists?(@test_dir)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Test POST /print
|
22
|
+
|
23
|
+
def test_print_with_invalid_json
|
24
|
+
header 'Content-Type', 'application/json'
|
25
|
+
post '/print', nil
|
26
|
+
|
27
|
+
body = JSON.parse last_response.body
|
28
|
+
|
29
|
+
assert !last_response.ok?
|
30
|
+
assert_equal body, { 'result' => 'error', 'error' => 'Invalid JSON.' }
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_print_with_valid_document
|
34
|
+
invoice = InvoicePrinter::Document.new(default_document_params)
|
35
|
+
|
36
|
+
json = {
|
37
|
+
'document' => invoice.to_h,
|
38
|
+
'filename' => "#{@test_dir}/test"
|
39
|
+
}.to_json
|
40
|
+
|
41
|
+
header 'Content-Type', 'application/json'
|
42
|
+
post '/print', json
|
43
|
+
|
44
|
+
body = JSON.parse last_response.body
|
45
|
+
|
46
|
+
assert last_response.ok?
|
47
|
+
assert_equal body, { 'result' => 'ok', 'path' => "#{@test_dir}/test" }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Test POST /render
|
51
|
+
|
52
|
+
def test_render_with_invalid_json
|
53
|
+
header 'Content-Type', 'application/json'
|
54
|
+
post '/render', nil
|
55
|
+
|
56
|
+
body = JSON.parse last_response.body
|
57
|
+
|
58
|
+
assert !last_response.ok?
|
59
|
+
assert_equal body, { 'result' => 'error', 'error' => 'Invalid JSON.' }
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_render_with_valid_document
|
63
|
+
invoice = InvoicePrinter::Document.new(default_document_params)
|
64
|
+
output = InvoicePrinter.render(document: invoice)
|
65
|
+
|
66
|
+
json = {
|
67
|
+
'document' => invoice.to_h
|
68
|
+
}.to_json
|
69
|
+
|
70
|
+
header 'Content-Type', 'application/json'
|
71
|
+
post '/render', json
|
72
|
+
|
73
|
+
body = JSON.parse last_response.body
|
74
|
+
|
75
|
+
assert last_response.ok?
|
76
|
+
assert_equal body, { 'result' => 'ok', 'data' => Base64.encode64(output) }
|
77
|
+
end
|
78
|
+
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: invoice_printer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0.alpha1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josef Strzibny
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-03-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: roda
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.5.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.5.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: puma
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.9'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.9'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: prawn
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -85,6 +113,7 @@ email:
|
|
85
113
|
- strzibny@strzibny.name
|
86
114
|
executables:
|
87
115
|
- invoice_printer
|
116
|
+
- invoice_printer_server
|
88
117
|
extensions: []
|
89
118
|
extra_rdoc_files: []
|
90
119
|
files:
|
@@ -93,7 +122,13 @@ files:
|
|
93
122
|
- LICENSE.txt
|
94
123
|
- README.md
|
95
124
|
- Rakefile
|
125
|
+
- assets/logo.png
|
96
126
|
- bin/invoice_printer
|
127
|
+
- bin/invoice_printer_server
|
128
|
+
- docs/COMMAND_LINE.md
|
129
|
+
- docs/INSTALLATION.md
|
130
|
+
- docs/LIBRARY.md
|
131
|
+
- docs/SERVER.md
|
97
132
|
- examples/background.png
|
98
133
|
- examples/complex_invoice.rb
|
99
134
|
- examples/czech_invoice.rb
|
@@ -110,7 +145,9 @@ files:
|
|
110
145
|
- lib/invoice_printer/document.rb
|
111
146
|
- lib/invoice_printer/document/item.rb
|
112
147
|
- lib/invoice_printer/pdf_document.rb
|
148
|
+
- lib/invoice_printer/server.rb
|
113
149
|
- lib/invoice_printer/version.rb
|
150
|
+
- test/api_test.rb
|
114
151
|
- test/background_test.rb
|
115
152
|
- test/cli_test.rb
|
116
153
|
- test/dates_box_test.rb
|
@@ -139,16 +176,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
139
176
|
version: '0'
|
140
177
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
141
178
|
requirements:
|
142
|
-
- - "
|
179
|
+
- - ">"
|
143
180
|
- !ruby/object:Gem::Version
|
144
|
-
version:
|
181
|
+
version: 1.3.1
|
145
182
|
requirements: []
|
146
183
|
rubyforge_project:
|
147
|
-
rubygems_version: 2.6.
|
184
|
+
rubygems_version: 2.6.14
|
148
185
|
signing_key:
|
149
186
|
specification_version: 4
|
150
187
|
summary: Super simple PDF invoicing in pure Ruby
|
151
188
|
test_files:
|
189
|
+
- test/api_test.rb
|
152
190
|
- test/background_test.rb
|
153
191
|
- test/cli_test.rb
|
154
192
|
- test/dates_box_test.rb
|