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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5ccb3bfdda5d49f50b0902f0bb78996c507d66f5f6847322b4dd68be31615d9f
|
|
4
|
+
data.tar.gz: 4202cc126a5556d17239a9989d7ab11a7dd6c8de19de42e885494a0669776c76
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 932d8e285879146e5dcc42b17939ad2f324cbf7fea0eb744681a1572c43ee868cc9a453f1b2be4bfac594ebd5e7d13939af505630f3c4dbd84984232d6005b16
|
|
7
|
+
data.tar.gz: 4df8d76b15d53096521ca7d8835870ad89225b1c15d9d80073bbeab3f6e7db476ba227e225381a8a9495ebd52b46aa88723b9da8e64fdec37fb9b7f9777f91b5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 1.0.0 (2026-11-11)
|
|
4
|
+
|
|
5
|
+
The first release of Tallyman. It carries on from [Payday](https://github.com/webtranslateit/payday), which was published as `webtranslateit-payday`, and picks up where that gem's 2.1.2 left off. Everything before this release is in [Payday's changelog](https://github.com/webtranslateit/payday/blob/main/CHANGELOG.md).
|
|
6
|
+
|
|
7
|
+
Invoice amounts and layout are exactly what they were in `webtranslateit-payday` 2.1.2; only the translated labels listed below differ. To move an application over:
|
|
8
|
+
|
|
9
|
+
* In your Gemfile, replace `gem 'webtranslateit-payday', require: 'payday'` with `gem 'tallyman'`.
|
|
10
|
+
* Rename the `Payday` module to `Tallyman` wherever you use it: `Tallyman::Invoice`, `Tallyman::Config`, `Tallyman::Invoiceable`, `Tallyman::LineItemable`, `Tallyman::Appendix` and so on.
|
|
11
|
+
* If you override the invoice labels, move your translations from the `payday:` I18n key to `tallyman:`.
|
|
12
|
+
|
|
13
|
+
A few things change along the way:
|
|
14
|
+
|
|
15
|
+
* The Rails generator, now `rails generate tallyman:setup`, works on current Rails. It used to fail on Rails 7.1 and later, and the migration it wrote could not run. The migration now creates every column an invoice needs to render, and models generated under custom names (`--invoice-name`, `--line-item-name`) are wired so that the invoice finds its lines.
|
|
16
|
+
* An ActiveRecord invoice with a `bill_to` column is billed to whoever that column names. `Invoiceable#bill_to` used to shadow the column with its placeholder, so such an invoice was addressed to "Goofy McGoofison" unless the model redefined the method.
|
|
17
|
+
* The translations are reviewed, so invoices in French, Spanish, German, Dutch and Simplified Chinese print some labels differently. English is untouched.
|
|
18
|
+
* Every locale now translates every label. Dutch, German and Simplified Chinese were missing the receipt number, and Dutch and Simplified Chinese the REFUNDED stamp, so those came out in English.
|
|
19
|
+
* The amount column no longer reads as a second price in French ("Montant"), Spanish ("Importe") or as "total" in Simplified Chinese ("金额").
|
|
20
|
+
* Spanish: the paid date is "Fecha de pago:", the stamps are "VENCIDO" and "REEMBOLSADO", the unit price is "Precio unitario", and the retention line is "Retención:" without "IRPF", which only applies in Spain.
|
|
21
|
+
* German: the shipping address is "Lieferadresse", the retention line "Einbehalt:" and the refunded stamp "ERSTATTET".
|
|
22
|
+
* Dutch: "Faktuur" is spelled "Factuur", and the addresses, due date, shipping line, overdue stamp and quantity use the usual invoice wording.
|
|
23
|
+
* Simplified Chinese: the invoice number, due date, shipping line and the paid and overdue stamps use the usual invoice wording.
|
|
24
|
+
|
|
25
|
+
To keep a label as it was, override it under your locale's `tallyman` key. To name IRPF on a Spanish retention line, set `retention_description` on the invoice.
|
data/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Tallyman
|
|
2
|
+
|
|
3
|
+
Tallyman is a Ruby library for rendering invoices to PDF.
|
|
4
|
+
|
|
5
|
+
Here's an [example invoice](https://github.com/Rebase-Capital/tallyman/raw/main/spec/assets/testing_predefined_amount.pdf).
|
|
6
|
+
|
|
7
|
+
## What is a tallyman?
|
|
8
|
+
|
|
9
|
+
On the docks, the tallyman is the clerk who stands at the ship's side and counts the cargo as it comes over the rail. Every bale, crate and barrel is checked off against the manifest, and the tally that results is the record both the ship and the shipper are held to: what was delivered, in what quantity, and so what is owed.
|
|
10
|
+
|
|
11
|
+
This gem does the same job on paper. You hand it what was sold and to whom, and it draws up the bill.
|
|
12
|
+
|
|
13
|
+
## Installing
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
gem install tallyman
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or, using Bundler:
|
|
20
|
+
|
|
21
|
+
``` ruby
|
|
22
|
+
gem 'tallyman'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Tallyman needs Ruby 3.2 or later.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Tallyman comes with `Invoice` and `LineItem` classes you can use as they are:
|
|
30
|
+
|
|
31
|
+
``` ruby
|
|
32
|
+
invoice = Tallyman::Invoice.new(invoice_number: 12)
|
|
33
|
+
# Two lines with a price and a quantity. The amount is price * quantity.
|
|
34
|
+
invoice.add_line_item(price: 20, quantity: 5, description: 'Pants')
|
|
35
|
+
invoice.add_line_item(price: 10, quantity: 3, description: 'Shirts')
|
|
36
|
+
# A line with no price or quantity, only a predefined amount (a flat fee).
|
|
37
|
+
invoice.add_line_item(predefined_amount: 10, description: 'Shipping')
|
|
38
|
+
invoice.render_pdf_to_file('/path/to_file.pdf')
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`render_pdf` returns the PDF as a string instead of writing it to disk.
|
|
42
|
+
|
|
43
|
+
API documentation is available at [rubydoc.info](https://www.rubydoc.info/gems/tallyman).
|
|
44
|
+
|
|
45
|
+
## Customizing your invoice
|
|
46
|
+
|
|
47
|
+
`Tallyman::Config` holds the settings shared by every invoice, such as the logo and the company details:
|
|
48
|
+
|
|
49
|
+
``` ruby
|
|
50
|
+
Tallyman::Config.default.invoice_logo = "/path/to/company/logo.png"
|
|
51
|
+
Tallyman::Config.default.company_name = "Awesome Corp"
|
|
52
|
+
Tallyman::Config.default.company_details = "10 This Way\nManhattan, NY 10001\n800-111-2222\nawesome@awesomecorp.com"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The logo can be a PNG, a JPEG or an SVG, and is drawn at its natural size. Pass a hash to constrain it:
|
|
56
|
+
|
|
57
|
+
``` ruby
|
|
58
|
+
Tallyman::Config.default.invoice_logo = {filename: "logo.svg", size: "200x50"}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`page_size` accepts `"LETTER"`, `"A4"` and `"LEGAL"`. `currency` and `date_format` are set the same way.
|
|
62
|
+
|
|
63
|
+
## QR codes
|
|
64
|
+
|
|
65
|
+
An invoice can carry a QR code, which a growing number of countries expect for tax compliance:
|
|
66
|
+
|
|
67
|
+
``` ruby
|
|
68
|
+
invoice = Tallyman::Invoice.new(
|
|
69
|
+
invoice_number: 12345,
|
|
70
|
+
qr_code: 'https://verify.example.com/invoice/12345',
|
|
71
|
+
notes: 'Scan the QR code below to verify this invoice online.'
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The QR code is rendered below the notes, as vector SVG so it stays sharp in print. The code itself comes straight from [rqrcode](https://github.com/whomwah/rqrcode), so the payload and its error correction level are exactly what that gem produces.
|
|
76
|
+
|
|
77
|
+
## Using Tallyman with ActiveRecord (or any other objects)
|
|
78
|
+
|
|
79
|
+
Tallyman works with two objects, an invoice and a line item. To use it with your own classes, include `Tallyman::Invoiceable` in the invoice and `Tallyman::LineItemable` in the line item.
|
|
80
|
+
|
|
81
|
+
In a Rails app, a generator writes both models and the migration for their tables:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
rails generate tallyman:setup
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Run it with `--help` to see how to name the models differently or skip the migration.
|
|
88
|
+
|
|
89
|
+
If you write the classes yourself, an invoice needs to respond to `line_items`, `bill_to`, `invoice_number`, `notes`, `due_at`, `paid_at`, `tax_rate`, `tax_description`, `shipping_rate`, `shipping_description`, `retention_rate` and `retention_description`. The three rates must be numbers, 0 when they don't apply; the rest can be `nil`. A line item needs `description`, `price`, `quantity`, `predefined_amount`, `display_price` and `display_quantity`.
|
|
90
|
+
|
|
91
|
+
## Rendering PDFs to the web
|
|
92
|
+
|
|
93
|
+
In a Rails controller, send the rendered string straight to the browser:
|
|
94
|
+
|
|
95
|
+
``` ruby
|
|
96
|
+
respond_to do |format|
|
|
97
|
+
format.html
|
|
98
|
+
format.pdf do
|
|
99
|
+
send_data invoice.render_pdf, filename: 'Invoice #12.pdf', type: 'application/pdf', disposition: 'inline'
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Appending your own pages
|
|
105
|
+
|
|
106
|
+
Pass a `Tallyman::Appendix` to add pages of your own to the end of an invoice — a per-item breakdown, a delivery note, terms and conditions:
|
|
107
|
+
|
|
108
|
+
``` ruby
|
|
109
|
+
appendix = Tallyman::Appendix.new(
|
|
110
|
+
source: File.read('appendix.typ'),
|
|
111
|
+
inputs: {'shipments' => shipments.to_json},
|
|
112
|
+
dependencies: {'chart.svg' => File.binread('chart.svg')}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
invoice.render_pdf(appendix: appendix)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`source` is [Typst](https://typst.app) markup appended to Tallyman's own template, so it is compiled as part of the invoice rather than as a second PDF you would then have to merge in. Two things follow from that: your pages inherit the invoice's page setup and margins, and the page counter runs across the whole document, so the numbering footer counts your pages too. A one-page invoice with one appended page comes out numbered "1 / 2" and "2 / 2".
|
|
119
|
+
|
|
120
|
+
`inputs` reach the template as [sys_inputs](https://typst.app/docs/reference/foundations/sys/), read with `json(bytes(sys.inputs.shipments))`. `dependencies` are files the source can reach by name, with `image("chart.svg")` and the like. Reusing a name the invoice has already taken — the `invoice` input, or the logo and QR code files — raises an `ArgumentError` rather than quietly replacing it.
|
|
121
|
+
|
|
122
|
+
**Pass your data through `inputs`, never by interpolating it into `source`.** Typst treats a string arriving through `sys.inputs` as literal text, which is what stops a customer-supplied field from affecting the layout. `source` is code: interpolating invoice data into it hands anyone who can set a field on an invoice a way to write Typst.
|
|
123
|
+
|
|
124
|
+
An appendix that reads its data and lists it on a page of its own:
|
|
125
|
+
|
|
126
|
+
``` typst
|
|
127
|
+
#let shipments = json(bytes(sys.inputs.shipments))
|
|
128
|
+
|
|
129
|
+
#pagebreak()
|
|
130
|
+
#text(size: 16pt, weight: "bold")[Shipments]
|
|
131
|
+
|
|
132
|
+
#for shipment in shipments [
|
|
133
|
+
- #shipment.reference
|
|
134
|
+
]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## I18n
|
|
138
|
+
|
|
139
|
+
Tallyman uses the i18n gem for its labels, and ships with English, French, German, Spanish, Dutch and Simplified Chinese. To change a label, add a YAML file to the `config/locales` directory of your Rails app. These are the labels you can override:
|
|
140
|
+
|
|
141
|
+
``` yaml
|
|
142
|
+
en:
|
|
143
|
+
tallyman:
|
|
144
|
+
status:
|
|
145
|
+
paid: PAID
|
|
146
|
+
overdue: OVERDUE
|
|
147
|
+
refunded: REFUNDED
|
|
148
|
+
invoice:
|
|
149
|
+
bill_to: "Bill To"
|
|
150
|
+
ship_to: "Ship To"
|
|
151
|
+
invoice_no: "Invoice number:"
|
|
152
|
+
receipt_no: "Receipt number:"
|
|
153
|
+
due_date: "Due Date:"
|
|
154
|
+
paid_date: "Paid Date:"
|
|
155
|
+
subtotal: "Subtotal:"
|
|
156
|
+
shipping: "Shipping:"
|
|
157
|
+
tax: "Tax:"
|
|
158
|
+
retention: "Retention:"
|
|
159
|
+
total: "Total:"
|
|
160
|
+
notes: "Notes:"
|
|
161
|
+
line_item:
|
|
162
|
+
description: Description
|
|
163
|
+
unit_price: "Unit Price"
|
|
164
|
+
quantity: Quantity
|
|
165
|
+
amount: Amount
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
If you translate the invoice into another language, a pull request adding your locale file is welcome.
|
|
169
|
+
|
|
170
|
+
## Contributing
|
|
171
|
+
|
|
172
|
+
Issues and pull requests are welcome on [GitHub](https://github.com/Rebase-Capital/tallyman). For anything larger than a small fix, open an issue first so that two people don't end up doing the same work.
|
|
173
|
+
|
|
174
|
+
Run the specs and the linter with `bundle exec rake`.
|
|
175
|
+
|
|
176
|
+
## History
|
|
177
|
+
|
|
178
|
+
Tallyman is the continuation of [Payday](https://github.com/webtranslateit/payday), a gem written by Alan Johnson. When the original stopped working on Ruby 2.7 and its source was no longer on GitHub, we recovered the code from [rubygems.org](https://rubygems.org) and maintained it as `webtranslateit-payday`, adding features and fixes along the way. Tallyman 1.0.0 picks up where `webtranslateit-payday` 2.1.2 left off; the earlier history is in [Payday's repository](https://github.com/webtranslateit/payday) and its [changelog](https://github.com/webtranslateit/payday/blob/main/CHANGELOG.md).
|
|
179
|
+
|
|
180
|
+
We use Tallyman at [WebTranslateIt.com](https://webtranslateit.com) and [Trackberry](https://trackberry.com) to generate invoices and receipts.
|
|
181
|
+
|
|
182
|
+
Thanks to everyone who has contributed:
|
|
183
|
+
|
|
184
|
+
* Alan Johnson, the original author
|
|
185
|
+
* Sam Pizzey ([pizzeys](https://github.com/pizzeys))
|
|
186
|
+
* Andrew Nordman ([cadwallion](https://github.com/cadwallion))
|
|
187
|
+
* Pierre Olivier Martel ([pomartel](https://github.com/pomartel))
|
|
188
|
+
* Matt Hoofman ([mhoofman](https://github.com/mhoofman))
|
|
189
|
+
* Édouard Brière ([edouard](https://github.com/edouard))
|
|
190
|
+
* Jim Jones ([aantix](https://github.com/aantix))
|
|
191
|
+
* Hussein Morsy ([husseinmorsy](https://github.com/husseinmorsy))
|
|
192
|
+
|
|
193
|
+
## Acknowledgements
|
|
194
|
+
|
|
195
|
+
Tallyman renders with [Typst](https://typst.app) and its [Ruby binding](https://github.com/actsasflinn/typst-rb).
|
|
196
|
+
|
|
197
|
+
For its first decade this gem was built on [Prawn](https://prawnpdf.org), and it would not exist without that project and the team behind it.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
Copyright (C) 2011 by Alan Johnson
|
|
202
|
+
|
|
203
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
204
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
205
|
+
in the Software without restriction, including without limitation the rights
|
|
206
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
207
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
208
|
+
furnished to do so, subject to the following conditions:
|
|
209
|
+
|
|
210
|
+
The above copyright notice and this permission notice shall be included in
|
|
211
|
+
all copies or substantial portions of the Software.
|
|
212
|
+
|
|
213
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
214
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
215
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
216
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
217
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
218
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
219
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
de:
|
|
2
|
+
tallyman:
|
|
3
|
+
status:
|
|
4
|
+
overdue: ÜBERFÄLLIG
|
|
5
|
+
paid: BEZAHLT
|
|
6
|
+
refunded: ERSTATTET
|
|
7
|
+
invoice:
|
|
8
|
+
bill_to: Rechnung an
|
|
9
|
+
ship_to: Lieferadresse
|
|
10
|
+
invoice_no: "Rechnungsnr.:"
|
|
11
|
+
receipt_no: "Quittungsnr.:"
|
|
12
|
+
due_date: "Fälligkeit:"
|
|
13
|
+
paid_date: "Zahlungsdatum:"
|
|
14
|
+
subtotal: "Zwischensumme:"
|
|
15
|
+
shipping: "Versand:"
|
|
16
|
+
tax: "Steuern:"
|
|
17
|
+
retention: "Einbehalt:"
|
|
18
|
+
total: "Gesamt:"
|
|
19
|
+
notes: "Anmerkungen:"
|
|
20
|
+
line_item:
|
|
21
|
+
description: Beschreibung
|
|
22
|
+
unit_price: Stückpreis
|
|
23
|
+
quantity: Menge
|
|
24
|
+
amount: Betrag
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
en:
|
|
2
|
+
tallyman:
|
|
3
|
+
status:
|
|
4
|
+
paid: PAID
|
|
5
|
+
overdue: OVERDUE
|
|
6
|
+
refunded: REFUNDED
|
|
7
|
+
invoice:
|
|
8
|
+
bill_to: "Bill To"
|
|
9
|
+
ship_to: "Ship To"
|
|
10
|
+
invoice_no: "Invoice number:"
|
|
11
|
+
receipt_no: "Receipt number:"
|
|
12
|
+
due_date: "Due Date:"
|
|
13
|
+
paid_date: "Paid Date:"
|
|
14
|
+
subtotal: "Subtotal:"
|
|
15
|
+
shipping: "Shipping:"
|
|
16
|
+
tax: "Tax:"
|
|
17
|
+
retention: "Retention:"
|
|
18
|
+
total: "Total:"
|
|
19
|
+
notes: "Notes:"
|
|
20
|
+
line_item:
|
|
21
|
+
description: Description
|
|
22
|
+
unit_price: "Unit Price"
|
|
23
|
+
quantity: Quantity
|
|
24
|
+
amount: Amount
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
es:
|
|
2
|
+
tallyman:
|
|
3
|
+
status:
|
|
4
|
+
paid: PAGADO
|
|
5
|
+
overdue: VENCIDO
|
|
6
|
+
refunded: REEMBOLSADO
|
|
7
|
+
invoice:
|
|
8
|
+
bill_to: "Facturar a"
|
|
9
|
+
ship_to: "Enviar a"
|
|
10
|
+
invoice_no: "Número de factura proforma:"
|
|
11
|
+
receipt_no: "Número de factura:"
|
|
12
|
+
due_date: "Vencimiento:"
|
|
13
|
+
paid_date: "Fecha de pago:"
|
|
14
|
+
subtotal: "Subtotal:"
|
|
15
|
+
shipping: "Envío:"
|
|
16
|
+
tax: "Impuesto:"
|
|
17
|
+
retention: "Retención:"
|
|
18
|
+
total: "Total:"
|
|
19
|
+
notes: "Notas:"
|
|
20
|
+
line_item:
|
|
21
|
+
description: Descripción
|
|
22
|
+
unit_price: "Precio unitario"
|
|
23
|
+
quantity: Cantidad
|
|
24
|
+
amount: Importe
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
fr:
|
|
2
|
+
tallyman:
|
|
3
|
+
status:
|
|
4
|
+
paid: PAYÉ
|
|
5
|
+
overdue: "EN RETARD"
|
|
6
|
+
refunded: "REMBOURSÉ"
|
|
7
|
+
invoice:
|
|
8
|
+
bill_to: "Adresse de facturation"
|
|
9
|
+
ship_to: "Adresse de livraison"
|
|
10
|
+
invoice_no: "Numéro de facture proforma :"
|
|
11
|
+
receipt_no: "Numéro de facture :"
|
|
12
|
+
due_date: "Date limite de paiement :"
|
|
13
|
+
paid_date: "Date de paiement :"
|
|
14
|
+
subtotal: "Sous-total :"
|
|
15
|
+
shipping: "Livraison :"
|
|
16
|
+
tax: "Taxes :"
|
|
17
|
+
retention: "Retenue :"
|
|
18
|
+
total: "Total :"
|
|
19
|
+
notes: "Notes :"
|
|
20
|
+
line_item:
|
|
21
|
+
description: Description
|
|
22
|
+
unit_price: "Prix unitaire"
|
|
23
|
+
quantity: Quantité
|
|
24
|
+
amount: Montant
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
nl:
|
|
2
|
+
tallyman:
|
|
3
|
+
status:
|
|
4
|
+
paid: BETAALD
|
|
5
|
+
overdue: VERVALLEN
|
|
6
|
+
refunded: TERUGBETAALD
|
|
7
|
+
invoice:
|
|
8
|
+
bill_to: Factuuradres
|
|
9
|
+
ship_to: Afleveradres
|
|
10
|
+
invoice_no: "Factuurnummer:"
|
|
11
|
+
receipt_no: "Kwitantienummer:"
|
|
12
|
+
due_date: "Vervaldatum:"
|
|
13
|
+
paid_date: "Betaald op:"
|
|
14
|
+
subtotal: "Subtotaal:"
|
|
15
|
+
shipping: "Verzendkosten:"
|
|
16
|
+
tax: "Btw:"
|
|
17
|
+
retention: "Inhouding:"
|
|
18
|
+
total: "Totaal:"
|
|
19
|
+
notes: "Opmerkingen:"
|
|
20
|
+
line_item:
|
|
21
|
+
description: Omschrijving
|
|
22
|
+
unit_price: Eenheidsprijs
|
|
23
|
+
quantity: Aantal
|
|
24
|
+
amount: Bedrag
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
zh-CN:
|
|
2
|
+
tallyman:
|
|
3
|
+
status:
|
|
4
|
+
paid: 已付款
|
|
5
|
+
overdue: 已逾期
|
|
6
|
+
refunded: 已退款
|
|
7
|
+
invoice:
|
|
8
|
+
bill_to: 账单地址
|
|
9
|
+
ship_to: 送货地址
|
|
10
|
+
invoice_no: "发票号码:"
|
|
11
|
+
receipt_no: "收据号码:"
|
|
12
|
+
due_date: "到期日期:"
|
|
13
|
+
paid_date: "支付日期:"
|
|
14
|
+
subtotal: "小计:"
|
|
15
|
+
shipping: "运费:"
|
|
16
|
+
tax: "税费:"
|
|
17
|
+
retention: "扣缴:"
|
|
18
|
+
total: "总计:"
|
|
19
|
+
notes: "备注:"
|
|
20
|
+
line_item:
|
|
21
|
+
description: 描述
|
|
22
|
+
unit_price: 单价
|
|
23
|
+
quantity: 数量
|
|
24
|
+
amount: 金额
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Creates the invoice and line item models Tallyman renders, and a migration
|
|
3
|
+
for their tables with every column Tallyman reads.
|
|
4
|
+
|
|
5
|
+
The models are called Invoice and LineItem unless you name them with
|
|
6
|
+
--invoice-name and --line-item-name. Names can be CamelCase or under_scored.
|
|
7
|
+
|
|
8
|
+
Names are singularized, as `rails generate model` does. Pass --force-plural
|
|
9
|
+
to keep a name such as Media exactly as you typed it.
|
|
10
|
+
|
|
11
|
+
Pass --skip-migration to generate the models only.
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
rails generate tallyman:setup
|
|
15
|
+
|
|
16
|
+
Creates the Invoice and LineItem models and a migration for the
|
|
17
|
+
invoices and line_items tables.
|
|
18
|
+
|
|
19
|
+
rails generate tallyman:setup --invoice-name=Bill --line-item-name=Charge
|
|
20
|
+
|
|
21
|
+
Creates the Bill and Charge models and a migration for the bills and
|
|
22
|
+
charges tables. A bill still reaches its charges through line_items,
|
|
23
|
+
which is the name Tallyman looks for.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
# rails generate tallyman:setup [--invoice-name=Invoice] [--line-item-name=LineItem] [--skip-migration]
|
|
5
|
+
#
|
|
6
|
+
# Generates an invoice model, a line item model and the migration for their tables, with every
|
|
7
|
+
# column Tallyman reads when it renders an invoice.
|
|
8
|
+
module Tallyman
|
|
9
|
+
class SetupGenerator < Rails::Generators::Base
|
|
10
|
+
# Brings in next_migration_number, which follows the app's migration numbering settings.
|
|
11
|
+
include ActiveRecord::Generators::Migration
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
|
|
15
|
+
class_option :invoice_name, type: :string, default: "Invoice",
|
|
16
|
+
desc: "Class name of the invoice model"
|
|
17
|
+
class_option :line_item_name, type: :string, default: "LineItem",
|
|
18
|
+
desc: "Class name of the line item model"
|
|
19
|
+
class_option :force_plural, type: :boolean, default: false,
|
|
20
|
+
desc: "Keeps the model names as given instead of singularizing them"
|
|
21
|
+
class_option :skip_migration, type: :boolean, default: false,
|
|
22
|
+
desc: "Does not create the migration file for the tables"
|
|
23
|
+
|
|
24
|
+
def generate_invoice_model
|
|
25
|
+
template "invoice.rb", "app/models/#{invoice_class.underscore}.rb"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def generate_line_item_model
|
|
29
|
+
template "line_item.rb", "app/models/#{line_item_class.underscore}.rb"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def generate_migration
|
|
33
|
+
return if options.skip_migration?
|
|
34
|
+
|
|
35
|
+
migration_template "migration.rb", "db/migrate/#{migration_name}.rb"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def invoice_class
|
|
41
|
+
model_class(options.invoice_name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def line_item_class
|
|
45
|
+
model_class(options.line_item_name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Singular, the way `rails generate model` would have it, unless --force-plural says the
|
|
49
|
+
# name is right as it stands: "Media" would otherwise come out as "Medium".
|
|
50
|
+
def model_class(name)
|
|
51
|
+
options.force_plural? ? name.camelize : name.camelize.singularize
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Named after the tables rather than after Tallyman, so the generator can be run again
|
|
55
|
+
# for a second pair of models without colliding with its first migration.
|
|
56
|
+
def migration_name
|
|
57
|
+
"create_#{invoice_table}_and_#{line_item_table}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A namespaced model reads the table named after its last segment unless its namespace sets
|
|
61
|
+
# a table_name_prefix, so Billing::Invoice gets "invoices".
|
|
62
|
+
def invoice_table
|
|
63
|
+
invoice_class.demodulize.tableize
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def line_item_table
|
|
67
|
+
line_item_class.demodulize.tableize
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The name of the association from a line item back to its invoice, and so of its foreign key.
|
|
71
|
+
def invoice_reference
|
|
72
|
+
invoice_class.demodulize.underscore
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
class <%= invoice_class %> < ApplicationRecord
|
|
2
|
+
include Tallyman::Invoiceable
|
|
3
|
+
|
|
4
|
+
# Tallyman reads an invoice's lines through +line_items+, whatever the line item class is called.
|
|
5
|
+
has_many :line_items, class_name: "<%= line_item_class %>", foreign_key: :<%= invoice_reference %>_id,
|
|
6
|
+
inverse_of: :<%= invoice_reference %>, dependent: :destroy
|
|
7
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
class <%= migration_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :<%= invoice_table %> do |t|
|
|
4
|
+
t.string :invoice_number
|
|
5
|
+
t.text :bill_to
|
|
6
|
+
t.text :ship_to
|
|
7
|
+
t.text :notes
|
|
8
|
+
|
|
9
|
+
# tax_rate and retention_rate are percentages of the subtotal. shipping_rate is an amount.
|
|
10
|
+
t.decimal :tax_rate, precision: 8, scale: 4, default: 0, null: false
|
|
11
|
+
t.string :tax_description
|
|
12
|
+
t.decimal :shipping_rate, precision: 12, scale: 2, default: 0, null: false
|
|
13
|
+
t.string :shipping_description
|
|
14
|
+
t.decimal :retention_rate, precision: 8, scale: 4, default: 0, null: false
|
|
15
|
+
t.string :retention_description
|
|
16
|
+
|
|
17
|
+
# A due date in the past stamps the invoice OVERDUE, paid_at stamps it PAID and
|
|
18
|
+
# refunded_at stamps it REFUNDED.
|
|
19
|
+
t.date :due_at
|
|
20
|
+
t.date :paid_at
|
|
21
|
+
t.date :refunded_at
|
|
22
|
+
|
|
23
|
+
t.timestamps
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
create_table :<%= line_item_table %> do |t|
|
|
27
|
+
t.references :<%= invoice_reference %>, null: false, foreign_key: {to_table: :<%= invoice_table %>}
|
|
28
|
+
|
|
29
|
+
t.text :description
|
|
30
|
+
t.decimal :price, precision: 12, scale: 2, default: 0, null: false
|
|
31
|
+
t.decimal :quantity, precision: 12, scale: 3, default: 1, null: false
|
|
32
|
+
|
|
33
|
+
# A line with a predefined_amount is a flat fee: it shows no price or quantity.
|
|
34
|
+
t.decimal :predefined_amount, precision: 12, scale: 2
|
|
35
|
+
|
|
36
|
+
# Shown in place of the formatted price and quantity when set, "2 hours" for example.
|
|
37
|
+
t.string :display_price
|
|
38
|
+
t.string :display_quantity
|
|
39
|
+
|
|
40
|
+
t.timestamps
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tallyman
|
|
4
|
+
|
|
5
|
+
# Extra pages compiled onto the end of an invoice.
|
|
6
|
+
#
|
|
7
|
+
# appendix = Tallyman::Appendix.new(
|
|
8
|
+
# source: File.read('appendix.typ'),
|
|
9
|
+
# inputs: {'shipments' => shipments.to_json},
|
|
10
|
+
# dependencies: {'chart.svg' => File.binread('chart.svg')}
|
|
11
|
+
# )
|
|
12
|
+
# invoice.render_pdf(appendix: appendix)
|
|
13
|
+
#
|
|
14
|
+
# +source+ is Typst markup appended to Tallyman's own template, so it is compiled as part of
|
|
15
|
+
# the invoice rather than as a second document that would then have to be merged in. Two
|
|
16
|
+
# things follow from that. The template's +#set page+ rules stay in force, so the appended
|
|
17
|
+
# pages keep the invoice's margins; and the page counter runs across the whole document, so
|
|
18
|
+
# the numbering footer counts the appended pages. A one-page invoice with one appended page
|
|
19
|
+
# numbers "1 / 2", and a three-page invoice continues into "4 / 4".
|
|
20
|
+
#
|
|
21
|
+
# That is also why the source is concatenated rather than +#include+d: an included file is
|
|
22
|
+
# its own module, and the page rules set inside the template would stop at its edge.
|
|
23
|
+
#
|
|
24
|
+
# **Pass your data through +inputs+, never through +source+.** Inputs reach the template as
|
|
25
|
+
# sys_inputs JSON, where Typst treats every string as literal text, which is what stops a
|
|
26
|
+
# customer-supplied field from affecting the layout. Source is code. Interpolating invoice
|
|
27
|
+
# data into it gives that data back the run of the document, and hands anyone who can set a
|
|
28
|
+
# field on an invoice a way to write Typst.
|
|
29
|
+
#
|
|
30
|
+
# +dependencies+ are files the source can reach by name -- +image("chart.svg")+ and the
|
|
31
|
+
# like. Keys are file names, values are the bytes.
|
|
32
|
+
Appendix = Data.define(:source, :inputs, :dependencies) do
|
|
33
|
+
|
|
34
|
+
# Accepts an Appendix, a Hash of the same attributes, or nil.
|
|
35
|
+
def self.wrap(value)
|
|
36
|
+
return value if value.nil? || value.is_a?(Appendix)
|
|
37
|
+
|
|
38
|
+
new(**value)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(source:, inputs: {}, dependencies: {})
|
|
42
|
+
super(source: source, inputs: inputs.transform_keys(&:to_s), dependencies: dependencies.transform_keys(&:to_s))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tallyman
|
|
4
|
+
|
|
5
|
+
# Configuration for Tallyman. This is a singleton, so to set the company_name you would call
|
|
6
|
+
# Tallyman::Config.default.company_name = "Awesome Corp".
|
|
7
|
+
class Config
|
|
8
|
+
|
|
9
|
+
attr_accessor :invoice_logo, :company_name, :company_details, :date_format, :currency
|
|
10
|
+
|
|
11
|
+
# Sets the page size to use. Accepts the names 'LETTER', 'A4'
|
|
12
|
+
# and 'LEGAL', which the template maps onto the equivalent Typst paper. Anything else
|
|
13
|
+
# falls back to A4.
|
|
14
|
+
attr_accessor :page_size
|
|
15
|
+
|
|
16
|
+
# Returns the default configuration instance
|
|
17
|
+
def self.default
|
|
18
|
+
@default ||= new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The invoice's own value for +property+ when it defines one and it is set, otherwise this
|
|
22
|
+
# config's. Every setting an invoice can override is read through here.
|
|
23
|
+
def value_for(invoice, property)
|
|
24
|
+
value = invoice.public_send(property) if invoice.respond_to?(property)
|
|
25
|
+
value || public_send(property)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Internal: Resets a config object back to its default settings.
|
|
29
|
+
#
|
|
30
|
+
# Primarily intended for use in our tests.
|
|
31
|
+
def reset
|
|
32
|
+
# TODO: Move into specs and make minimal configuration required (company name / details)
|
|
33
|
+
self.invoice_logo = File.expand_path('assets/default_logo.png', __dir__)
|
|
34
|
+
self.company_name = 'Awesome Corp'
|
|
35
|
+
self.company_details = 'awesomecorp@commondream.net'
|
|
36
|
+
self.date_format = '%B %e, %Y'
|
|
37
|
+
self.currency = 'USD'
|
|
38
|
+
self.page_size = 'LETTER'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Internal: Contruct a new config object.
|
|
42
|
+
def initialize
|
|
43
|
+
reset
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|