tallyman 1.0.0
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/CHANGELOG.md +25 -0
- data/README.md +219 -0
- data/config/locales/tallyman.de.yml +24 -0
- data/config/locales/tallyman.en.yml +24 -0
- data/config/locales/tallyman.es.yml +24 -0
- data/config/locales/tallyman.fr.yml +24 -0
- data/config/locales/tallyman.nl.yml +24 -0
- data/config/locales/tallyman.zh-CN.yml +24 -0
- data/fonts/NotoSans-Bold.ttf +0 -0
- data/fonts/NotoSans-Regular.ttf +0 -0
- data/lib/generators/tallyman/setup/USAGE +23 -0
- data/lib/generators/tallyman/setup/setup_generator.rb +75 -0
- data/lib/generators/tallyman/setup/templates/invoice.rb +7 -0
- data/lib/generators/tallyman/setup/templates/line_item.rb +5 -0
- data/lib/generators/tallyman/setup/templates/migration.rb +43 -0
- data/lib/tallyman/appendix.rb +47 -0
- data/lib/tallyman/assets/default_logo.png +0 -0
- data/lib/tallyman/config.rb +48 -0
- data/lib/tallyman/invoice.rb +49 -0
- data/lib/tallyman/invoice_presenter.rb +160 -0
- data/lib/tallyman/invoiceable.rb +100 -0
- data/lib/tallyman/line_item.rb +47 -0
- data/lib/tallyman/line_itemable.rb +18 -0
- data/lib/tallyman/markup.rb +61 -0
- data/lib/tallyman/pdf_renderer.rb +115 -0
- data/lib/tallyman/qr_code.rb +99 -0
- data/lib/tallyman/templates/invoice.typ +171 -0
- data/lib/tallyman.rb +28 -0
- data/tallyman.gemspec +30 -0
- metadata +185 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Tallyman's invoice layout.
|
|
2
|
+
//
|
|
3
|
+
// All data arrives as JSON through sys.inputs, where Typst inserts string values as literal
|
|
4
|
+
// text rather than markup. Never interpolate invoice data into this file's source, and never
|
|
5
|
+
// call eval() on a value that came from an invoice.
|
|
6
|
+
|
|
7
|
+
#let d = json(bytes(sys.inputs.invoice))
|
|
8
|
+
|
|
9
|
+
// Gaps between the major blocks, calibrated so the rendered invoice keeps the vertical
|
|
10
|
+
// rhythm the prawn renderer produced. Every within-block gap falls out of the table insets.
|
|
11
|
+
#let gap-after-header = 15pt
|
|
12
|
+
#let gap-after-addresses = 13.2pt
|
|
13
|
+
#let gap-after-details = 8pt
|
|
14
|
+
#let gap-before-totals = -12pt
|
|
15
|
+
#let gap-before-notes = 22.3pt
|
|
16
|
+
#let gap-notes-label-to-rule = -7.1pt
|
|
17
|
+
#let gap-after-notes-rule = -0.4pt
|
|
18
|
+
#let gap-before-qr = -0.6pt
|
|
19
|
+
|
|
20
|
+
// Prawn page-size names mapped onto Typst paper names.
|
|
21
|
+
#let papers = ("LETTER": "us-letter", "A4": "a4", "LEGAL": "us-legal")
|
|
22
|
+
|
|
23
|
+
#set document(title: "Invoice", date: none)
|
|
24
|
+
#set page(
|
|
25
|
+
paper: papers.at(d.page_size, default: "a4"),
|
|
26
|
+
margin: 36pt, // prawn's default half-inch page margin
|
|
27
|
+
numbering: none,
|
|
28
|
+
// Page numbers appear only when the invoice runs to more than one page. This has to be a
|
|
29
|
+
// footer rather than a trailing `set page`, which would only affect the final page.
|
|
30
|
+
footer: context {
|
|
31
|
+
if counter(page).final().first() > 1 {
|
|
32
|
+
align(right, text(size: 9pt, counter(page).display("1 / 1", both: true)))
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
#set text(font: "Noto Sans", size: 10pt)
|
|
37
|
+
|
|
38
|
+
// Renders the styled runs produced by Tallyman::Markup.
|
|
39
|
+
#let render-runs(runs) = {
|
|
40
|
+
if runs == none { return }
|
|
41
|
+
for r in runs {
|
|
42
|
+
let body = text(
|
|
43
|
+
size: if "size" in r { r.size * 1pt } else { 1em },
|
|
44
|
+
fill: if "color" in r { rgb(r.color) } else { black },
|
|
45
|
+
weight: if r.at("bold", default: false) { "bold" } else { "regular" },
|
|
46
|
+
style: if r.at("italic", default: false) { "italic" } else { "normal" },
|
|
47
|
+
r.text,
|
|
48
|
+
)
|
|
49
|
+
if r.at("underline", default: false) { body = underline(body) }
|
|
50
|
+
if r.at("strike", default: false) { body = strike(body) }
|
|
51
|
+
if r.at("sub", default: false) { body = sub(body) }
|
|
52
|
+
if r.at("sup", default: false) { body = super(body) }
|
|
53
|
+
if "link" in r { link(r.link, body) } else { body }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- status stamp ------------------------------------------------------------------
|
|
58
|
+
// Drawn before anything else so it stays at a fixed offset from the top of the page, the way
|
|
59
|
+
// prawn stamped it, rather than drifting with the height of the logo.
|
|
60
|
+
#if d.stamp != none {
|
|
61
|
+
place(top + center, dx: 5.1pt, dy: 33pt, rotate(15deg,
|
|
62
|
+
text(fill: rgb("cc0000"), size: 25pt, weight: "bold")[#d.stamp]))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- logo and company identity ---------------------------------------------------
|
|
66
|
+
#grid(
|
|
67
|
+
columns: (1fr, auto),
|
|
68
|
+
align: (left + top, right + top),
|
|
69
|
+
if d.logo != none {
|
|
70
|
+
image(
|
|
71
|
+
d.logo.name,
|
|
72
|
+
width: if d.logo.width != none { d.logo.width * 1pt } else { auto },
|
|
73
|
+
height: if d.logo.height != none { d.logo.height * 1pt } else { auto },
|
|
74
|
+
fit: "contain",
|
|
75
|
+
)
|
|
76
|
+
},
|
|
77
|
+
align(left, pad(top: 5.7pt)[
|
|
78
|
+
#text(weight: "bold", size: 12pt)[#d.company_name] \
|
|
79
|
+
#d.company_details
|
|
80
|
+
]),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
#v(gap-after-header)
|
|
84
|
+
|
|
85
|
+
// --- bill to / ship to -------------------------------------------------------------
|
|
86
|
+
#grid(
|
|
87
|
+
columns: (1fr, 200pt),
|
|
88
|
+
align: (left + top, left + top),
|
|
89
|
+
[#text(weight: "bold")[#d.labels.bill_to]#v(10.4pt, weak: true)#d.bill_to],
|
|
90
|
+
if d.ship_to != none {
|
|
91
|
+
[#text(weight: "bold")[#d.labels.ship_to]#v(10.4pt, weak: true)#d.ship_to]
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
// prawn drew the ship-to box 2pt taller than its contents, so invoices carrying a ship-to
|
|
96
|
+
// address pushed the details block down by that much.
|
|
97
|
+
#v(gap-after-addresses + if d.ship_to != none { 2pt } else { 0pt })
|
|
98
|
+
|
|
99
|
+
// --- invoice details ---------------------------------------------------------------
|
|
100
|
+
#if d.details.len() > 0 {
|
|
101
|
+
table(
|
|
102
|
+
columns: 2,
|
|
103
|
+
stroke: none,
|
|
104
|
+
inset: (left: 1pt, right: 0pt, y: 4.25pt),
|
|
105
|
+
column-gutter: 10pt,
|
|
106
|
+
align: (left, right),
|
|
107
|
+
..d.details.map(row => (
|
|
108
|
+
text(weight: "bold")[#row.at(0)],
|
|
109
|
+
text(weight: "bold")[#row.at(1)],
|
|
110
|
+
)).flatten()
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#v(gap-after-details)
|
|
115
|
+
|
|
116
|
+
// --- line items ---------------------------------------------------------------------
|
|
117
|
+
#table(
|
|
118
|
+
columns: (1fr, auto, auto, auto),
|
|
119
|
+
align: (left, right, right, right),
|
|
120
|
+
inset: (x: 10pt, y: 8.25pt),
|
|
121
|
+
stroke: (x, y) => (
|
|
122
|
+
left: none,
|
|
123
|
+
right: none,
|
|
124
|
+
top: 0.5pt + rgb("F6F9FC"),
|
|
125
|
+
bottom: 0.5pt + rgb("BCC6D0"),
|
|
126
|
+
),
|
|
127
|
+
fill: (x, y) => if y == 0 { white } else if calc.odd(y) { rgb("F6F9FC") } else { white },
|
|
128
|
+
table.header(
|
|
129
|
+
text(weight: "bold")[#d.labels.description],
|
|
130
|
+
text(weight: "bold")[#d.labels.unit_price],
|
|
131
|
+
text(weight: "bold")[#d.labels.quantity],
|
|
132
|
+
text(weight: "bold")[#d.labels.amount],
|
|
133
|
+
),
|
|
134
|
+
..d.line_items.map(li => (
|
|
135
|
+
render-runs(li.description), [#li.price], [#li.quantity], [#li.amount],
|
|
136
|
+
)).flatten()
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
#v(gap-before-totals)
|
|
140
|
+
|
|
141
|
+
// --- totals ---------------------------------------------------------------------------
|
|
142
|
+
#align(right, table(
|
|
143
|
+
columns: 2,
|
|
144
|
+
stroke: none,
|
|
145
|
+
// The total is set 12pt, whose taller glyph box would otherwise pull the row up.
|
|
146
|
+
inset: (x, y) => (
|
|
147
|
+
left: 5pt, right: 5pt, bottom: 8.25pt,
|
|
148
|
+
top: if y == d.totals.len() - 1 { 8.95pt } else { 8.25pt },
|
|
149
|
+
),
|
|
150
|
+
align: (left, right),
|
|
151
|
+
..d.totals.map(row => {
|
|
152
|
+
let size = if row.at(2) { 12pt } else { 10pt }
|
|
153
|
+
(text(weight: "bold", size: size)[#row.at(0)], text(size: size)[#row.at(1)])
|
|
154
|
+
}).flatten()
|
|
155
|
+
))
|
|
156
|
+
|
|
157
|
+
// --- notes ------------------------------------------------------------------------------
|
|
158
|
+
#if d.notes != none {
|
|
159
|
+
v(gap-before-notes)
|
|
160
|
+
text(weight: "bold")[#d.labels.notes]
|
|
161
|
+
v(gap-notes-label-to-rule)
|
|
162
|
+
line(length: 100%, stroke: 0.5pt + rgb("cccccc"))
|
|
163
|
+
v(gap-after-notes-rule)
|
|
164
|
+
render-runs(d.notes)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- QR code ------------------------------------------------------------------------------
|
|
168
|
+
#if d.qr_code != none {
|
|
169
|
+
v(gap-before-qr)
|
|
170
|
+
image("qr.svg", width: 100pt)
|
|
171
|
+
}
|
data/lib/tallyman.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'zeitwerk'
|
|
4
|
+
loader = Zeitwerk::Loader.for_gem
|
|
5
|
+
loader.ignore("#{__dir__}/generators")
|
|
6
|
+
loader.setup
|
|
7
|
+
|
|
8
|
+
require 'active_support/core_ext/object/blank' # For Object#blank? method
|
|
9
|
+
require 'bigdecimal'
|
|
10
|
+
require 'date'
|
|
11
|
+
require 'i18n'
|
|
12
|
+
require 'json'
|
|
13
|
+
require 'money'
|
|
14
|
+
require 'typst'
|
|
15
|
+
require 'time'
|
|
16
|
+
|
|
17
|
+
# Load our own translations. This has to resolve against the gem rather than the working
|
|
18
|
+
# directory, or the locales only load when the process happens to be running from the gem root.
|
|
19
|
+
I18n.load_path.concat(Dir[File.expand_path('../config/locales/*.yml', __dir__)])
|
|
20
|
+
|
|
21
|
+
module Tallyman
|
|
22
|
+
|
|
23
|
+
# A blank value (nil, "", " ") is 0.
|
|
24
|
+
def self.to_decimal(value)
|
|
25
|
+
BigDecimal(value.to_s.presence || 0)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
data/tallyman.gemspec
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |s|
|
|
4
|
+
s.name = 'tallyman'
|
|
5
|
+
s.version = '1.0.0'
|
|
6
|
+
s.required_ruby_version = '>= 3.2'
|
|
7
|
+
s.platform = Gem::Platform::RUBY
|
|
8
|
+
s.authors = ['Alan Johnson', 'Edouard Briere']
|
|
9
|
+
s.email = ['edouard@webtranslateit.com']
|
|
10
|
+
s.homepage = 'https://github.com/Rebase-Capital/tallyman'
|
|
11
|
+
s.summary = 'A simple library for rendering invoices.'
|
|
12
|
+
s.description = 'Tallyman is a library for rendering invoices to pdf, using Typst.'
|
|
13
|
+
s.license = 'MIT'
|
|
14
|
+
|
|
15
|
+
s.add_dependency 'activesupport', '>= 7', '< 9'
|
|
16
|
+
s.add_dependency 'i18n', '~> 1.12', '< 2'
|
|
17
|
+
s.add_dependency 'money', '>= 6.16', '< 8.0'
|
|
18
|
+
s.add_dependency 'rqrcode', '>= 2', '< 4'
|
|
19
|
+
s.add_dependency 'typst', '~> 0.15'
|
|
20
|
+
s.add_dependency 'zeitwerk', '~> 2.6', '< 3'
|
|
21
|
+
|
|
22
|
+
# Only what the gem needs at run time, plus the docs. The spec suite carries several
|
|
23
|
+
# hundred kilobytes of reference PDFs that nobody installing this wants.
|
|
24
|
+
root_files = %w[README.md CHANGELOG.md tallyman.gemspec].freeze
|
|
25
|
+
s.files = `git ls-files -z`.split("\x0").select do |path|
|
|
26
|
+
path.start_with?('lib/', 'config/locales/', 'fonts/') || root_files.include?(path)
|
|
27
|
+
end
|
|
28
|
+
s.require_paths = ['lib']
|
|
29
|
+
s.metadata['rubygems_mfa_required'] = 'true'
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: tallyman
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alan Johnson
|
|
8
|
+
- Edouard Briere
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '9'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '7'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '9'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: i18n
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.12'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '2'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - "~>"
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '1.12'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '2'
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: money
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '6.16'
|
|
60
|
+
- - "<"
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '8.0'
|
|
63
|
+
type: :runtime
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '6.16'
|
|
70
|
+
- - "<"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '8.0'
|
|
73
|
+
- !ruby/object:Gem::Dependency
|
|
74
|
+
name: rqrcode
|
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '2'
|
|
80
|
+
- - "<"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '4'
|
|
83
|
+
type: :runtime
|
|
84
|
+
prerelease: false
|
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '2'
|
|
90
|
+
- - "<"
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '4'
|
|
93
|
+
- !ruby/object:Gem::Dependency
|
|
94
|
+
name: typst
|
|
95
|
+
requirement: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - "~>"
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '0.15'
|
|
100
|
+
type: :runtime
|
|
101
|
+
prerelease: false
|
|
102
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - "~>"
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '0.15'
|
|
107
|
+
- !ruby/object:Gem::Dependency
|
|
108
|
+
name: zeitwerk
|
|
109
|
+
requirement: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - "~>"
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '2.6'
|
|
114
|
+
- - "<"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '3'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '2.6'
|
|
124
|
+
- - "<"
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '3'
|
|
127
|
+
description: Tallyman is a library for rendering invoices to pdf, using Typst.
|
|
128
|
+
email:
|
|
129
|
+
- edouard@webtranslateit.com
|
|
130
|
+
executables: []
|
|
131
|
+
extensions: []
|
|
132
|
+
extra_rdoc_files: []
|
|
133
|
+
files:
|
|
134
|
+
- CHANGELOG.md
|
|
135
|
+
- README.md
|
|
136
|
+
- config/locales/tallyman.de.yml
|
|
137
|
+
- config/locales/tallyman.en.yml
|
|
138
|
+
- config/locales/tallyman.es.yml
|
|
139
|
+
- config/locales/tallyman.fr.yml
|
|
140
|
+
- config/locales/tallyman.nl.yml
|
|
141
|
+
- config/locales/tallyman.zh-CN.yml
|
|
142
|
+
- fonts/NotoSans-Bold.ttf
|
|
143
|
+
- fonts/NotoSans-Regular.ttf
|
|
144
|
+
- lib/generators/tallyman/setup/USAGE
|
|
145
|
+
- lib/generators/tallyman/setup/setup_generator.rb
|
|
146
|
+
- lib/generators/tallyman/setup/templates/invoice.rb
|
|
147
|
+
- lib/generators/tallyman/setup/templates/line_item.rb
|
|
148
|
+
- lib/generators/tallyman/setup/templates/migration.rb
|
|
149
|
+
- lib/tallyman.rb
|
|
150
|
+
- lib/tallyman/appendix.rb
|
|
151
|
+
- lib/tallyman/assets/default_logo.png
|
|
152
|
+
- lib/tallyman/config.rb
|
|
153
|
+
- lib/tallyman/invoice.rb
|
|
154
|
+
- lib/tallyman/invoice_presenter.rb
|
|
155
|
+
- lib/tallyman/invoiceable.rb
|
|
156
|
+
- lib/tallyman/line_item.rb
|
|
157
|
+
- lib/tallyman/line_itemable.rb
|
|
158
|
+
- lib/tallyman/markup.rb
|
|
159
|
+
- lib/tallyman/pdf_renderer.rb
|
|
160
|
+
- lib/tallyman/qr_code.rb
|
|
161
|
+
- lib/tallyman/templates/invoice.typ
|
|
162
|
+
- tallyman.gemspec
|
|
163
|
+
homepage: https://github.com/Rebase-Capital/tallyman
|
|
164
|
+
licenses:
|
|
165
|
+
- MIT
|
|
166
|
+
metadata:
|
|
167
|
+
rubygems_mfa_required: 'true'
|
|
168
|
+
rdoc_options: []
|
|
169
|
+
require_paths:
|
|
170
|
+
- lib
|
|
171
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
172
|
+
requirements:
|
|
173
|
+
- - ">="
|
|
174
|
+
- !ruby/object:Gem::Version
|
|
175
|
+
version: '3.2'
|
|
176
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
177
|
+
requirements:
|
|
178
|
+
- - ">="
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: '0'
|
|
181
|
+
requirements: []
|
|
182
|
+
rubygems_version: 3.6.9
|
|
183
|
+
specification_version: 4
|
|
184
|
+
summary: A simple library for rendering invoices.
|
|
185
|
+
test_files: []
|