webtranslateit-payday 1.7.4 → 2.1.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 +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +47 -3
- data/lib/payday/appendix.rb +47 -0
- data/lib/payday/config.rb +4 -4
- data/lib/payday/invoice_presenter.rb +162 -0
- data/lib/payday/invoiceable.rb +6 -5
- data/lib/payday/markup.rb +61 -0
- data/lib/payday/pdf_renderer.rb +78 -314
- data/lib/payday/qr_code.rb +99 -0
- data/lib/payday/templates/invoice.typ +171 -0
- data/lib/payday.rb +5 -5
- data/payday.gemspec +9 -6
- metadata +19 -83
- data/.gitattributes +0 -1
- data/.github/dependabot.yml +0 -28
- data/.github/release-drafter.yml +0 -36
- data/.github/workflows/ci.yml +0 -47
- data/.github/workflows/release-drafter.yml +0 -24
- data/.gitignore +0 -8
- data/.rubocop.yml +0 -16
- data/.rubocop_todo.yml +0 -0
- data/Gemfile +0 -14
- data/Gemfile.lock +0 -210
- data/Guardfile +0 -12
- data/Rakefile +0 -14
- data/spec/assets/example_invoice_with_qr.pdf +0 -0
- data/spec/assets/svg.pdf +0 -2568
- data/spec/assets/testing.pdf +1 -14241
- data/spec/assets/testing_es.pdf +0 -0
- data/spec/assets/testing_predefined_amount.pdf +0 -0
- data/spec/assets/tiger.svg +0 -52
- data/spec/invoice_spec.rb +0 -403
- data/spec/line_item_spec.rb +0 -34
- data/spec/pdf_renderer_spec.rb +0 -15
- data/spec/spec_helper.rb +0 -11
- data/spec/support/asset_matchers.rb +0 -28
- /data/{spec → lib/payday}/assets/default_logo.png +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4cb98acc78809a09e4945f8d8525aee1bbafd3ed17a45234be6fb0a8836f3135
|
|
4
|
+
data.tar.gz: b3379b263fb55f5d7fdb8ec48d66924e0f0b1110325b43e15bbd0a764fbc6154
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c0473911f660484293361d1fbfb7020efed4614c06dd89b000fc7995f90e52b995c763781f175609578fd42090923ed9969baab0d4bffb85140f887356964690
|
|
7
|
+
data.tar.gz: 0a401edbb42eb073bf35602caaf6c2b7b762450dff19a04f2a8ee4d3ae8aca0ecf2ceab4485300a0647a2005b22116edd2cd1eba0be60a07117f5d44e5637b39
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 2.1.0 (2026-08-27)
|
|
4
|
+
|
|
5
|
+
* Add `Payday::Appendix`, for compiling pages of your own onto the end of an invoice. `render_pdf`, `render_pdf_to_file`, `PdfRenderer.render` and `PdfRenderer.render_to_file` all take an `appendix:` keyword; every signature stays backward compatible and an invoice rendered without one is byte-for-byte what it was.
|
|
6
|
+
|
|
7
|
+
The appendix carries Typst `source`, plus `inputs` that reach it as sys_inputs JSON and `dependencies` it can resolve by name. The source is concatenated onto Payday's template rather than compiled separately, so the appended pages keep the invoice's page setup and the numbering footer counts them: a one-page invoice with an appendix numbers "1 / 2" and "2 / 2". Merging a second PDF in could do neither.
|
|
8
|
+
|
|
9
|
+
Data belongs in `inputs`, where Typst treats it as literal text. Interpolating invoice data into `source` would give it the run of the document, which is the same rule the invoice template already follows.
|
|
10
|
+
|
|
11
|
+
An appendix that reuses a name the invoice has taken — the `invoice` input, or the logo and QR code files — raises `ArgumentError` instead of replacing it.
|
|
12
|
+
|
|
13
|
+
## 2.0.0 (2026-08-26)
|
|
14
|
+
|
|
15
|
+
**Breaking:** Payday now renders with [Typst](https://typst.app) instead of Prawn. The `prawn`, `prawn-table` and `prawn-svg` dependencies are gone, replaced by the single Apache-2.0 licensed `typst` gem, which ships precompiled native builds for macOS and Linux on both x86_64 and arm64.
|
|
16
|
+
|
|
17
|
+
The motivation was `prawn-table`, which has had no release since 2015 while the renderer depended on it for every table.
|
|
18
|
+
|
|
19
|
+
* The public API is unchanged. `Invoiceable#render_pdf`, `#render_pdf_to_file`, `PdfRenderer.render`, `PdfRenderer.render_to_file` and every `Payday::Config` accessor keep their signatures, and rendered invoices keep their existing layout.
|
|
20
|
+
* Prawn is no longer loaded for you. If your application relied on Payday requiring Prawn, require it yourself.
|
|
21
|
+
* Layout now lives in `lib/payday/templates/invoice.typ`, and invoice data is turned into a plain Hash by the new `Payday::InvoicePresenter`.
|
|
22
|
+
* The `inline_format` markup Payday accepted in notes and line item descriptions still works. `Payday::Markup` converts `<b>`, `<i>`, `<u>`, `<strikethrough>`, `<sub>`, `<sup>`, `<font size>`, `<color rgb>`, `<link href>` and `<br>` into styled runs.
|
|
23
|
+
* Invoice data reaches the template only as JSON, where Typst treats every string as literal text, so customer-supplied fields can no longer affect the layout.
|
|
24
|
+
* Rendered PDFs are now byte-for-byte reproducible for the same input.
|
|
25
|
+
* QR codes are drawn as vector SVG rather than a scaled 200px PNG, so they stay sharp in print, and the three finder patterns are drawn as rounded frames. The matrix still comes straight from RQRCode: the payload and its error correction level are untouched, and the rendered code is module-for-module identical to what the encoder produces.
|
|
26
|
+
* `Config#company_details` has any per-line indentation stripped. Prawn rendered each line as its own table cell, which trimmed leading whitespace, so a value written as an indented heredoc used to look fine and would otherwise have started rendering with its indentation.
|
|
27
|
+
* Logos still accept PNG, JPEG and SVG, and a logo given as a bare path keeps its natural size. Only the `{filename:, size:}` form constrains the dimensions.
|
|
28
|
+
|
|
29
|
+
Two packaging bugs are fixed along the way, both of which predate this release:
|
|
30
|
+
|
|
31
|
+
* Payday's own translations now load. The load path was resolved against the working directory, so unless a process happened to run from the gem's root the locales never loaded at all, and `I18n.t` raised `InvalidLocale` rather than falling back to the built-in English defaults. The German, Spanish, French, Dutch and Chinese translations that ship with the gem have effectively never been used.
|
|
32
|
+
* The default `invoice_logo` points inside the gem. It used to reference a file under `spec/`, which only resolved because the gemspec shipped the whole test suite.
|
|
33
|
+
|
|
34
|
+
The packaged gem now carries only `lib`, `config/locales`, `fonts` and the docs, which takes it from 563K to 277K.
|
|
35
|
+
|
|
36
|
+
Invoices keep the layout the prawn renderer produced. Page margins, table row heights, block spacing and the position of the status stamp were all measured against the previous output, and four of the five reference invoices in the test suite now match it within a single point at every word.
|
|
37
|
+
|
|
3
38
|
## 1.7.4 (2026-08-13)
|
|
4
39
|
|
|
5
40
|
* Pass `enable_web_requests: false` when rendering SVG logos. Logos are read from local files, so nothing needs fetching over the network. This silences prawn-svg's deprecation warning and keeps behaviour stable when prawn-svg 1.0 flips the default to `false`.
|
data/README.md
CHANGED
|
@@ -2,7 +2,9 @@ Payday!
|
|
|
2
2
|
===
|
|
3
3
|
Payday is a library for rendering invoices to pdfs.
|
|
4
4
|
|
|
5
|
-
We use payday intensively at [WebTranslateIt.com](https://webtranslateit.com) to generate customer’s invoices and receipts. When we upgraded our app to ruby 2.7 this gem failed in many ways and the original source code by Alan Johnson was nowhere to be found on GitHub. So we took the source code out of [rubygems.org](https://rubygems.org) and created this repo. We’ve since added a few new features and fixes, which can be seen [in the changelog](https://github.com/webtranslateit/payday/blob/main/CHANGELOG.md).
|
|
5
|
+
We use payday intensively at [WebTranslateIt.com](https://webtranslateit.com) and [Trackberry](https://trackberry.com) to generate customer’s invoices and receipts. When we upgraded our app to ruby 2.7 this gem failed in many ways and the original source code by Alan Johnson was nowhere to be found on GitHub. So we took the source code out of [rubygems.org](https://rubygems.org) and created this repo. We’ve since added a few new features and fixes, which can be seen [in the changelog](https://github.com/webtranslateit/payday/blob/main/CHANGELOG.md).
|
|
6
|
+
|
|
7
|
+
Invoices can now carry a QR code, which a growing number of countries expect for tax compliance, and as of 2.0 the PDFs are rendered with [Typst](https://typst.app) rather than Prawn.
|
|
6
8
|
|
|
7
9
|
Here’s an [example PDF Invoice](https://github.com/webtranslateit/payday/raw/main/spec/assets/testing_predefined_amount.pdf) generated with our fork of payday.
|
|
8
10
|
|
|
@@ -53,6 +55,15 @@ Payday::Config.default.company_name = "Awesome Corp"
|
|
|
53
55
|
Payday::Config.default.company_details = "10 This Way\nManhattan, NY 10001\n800-111-2222\nawesome@awesomecorp.com"
|
|
54
56
|
```
|
|
55
57
|
|
|
58
|
+
The logo can be a PNG, a JPEG or an SVG, and is drawn at its natural size. Pass a hash to
|
|
59
|
+
constrain it:
|
|
60
|
+
|
|
61
|
+
``` ruby
|
|
62
|
+
Payday::Config.default.invoice_logo = {filename: "logo.svg", size: "200x50"}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`page_size` accepts `"LETTER"`, `"A4"` and `"LEGAL"`.
|
|
66
|
+
|
|
56
67
|
QR Code Support
|
|
57
68
|
===
|
|
58
69
|
Invoices can include QR codes for verification purposes, useful for tax compliance requirements in various countries.
|
|
@@ -67,7 +78,9 @@ invoice = Payday::Invoice.new(
|
|
|
67
78
|
)
|
|
68
79
|
```
|
|
69
80
|
|
|
70
|
-
QR codes are automatically rendered below the notes section
|
|
81
|
+
QR codes are automatically rendered below the notes section, as vector SVG so they stay
|
|
82
|
+
sharp in print. The code itself comes straight from [rqrcode](https://github.com/whomwah/rqrcode),
|
|
83
|
+
so the payload and its error correction level are exactly what that gem produces.
|
|
71
84
|
|
|
72
85
|
Using Payday with ActiveRecord Objects (or any other objects, for that matter)
|
|
73
86
|
===
|
|
@@ -102,6 +115,35 @@ end
|
|
|
102
115
|
|
|
103
116
|
Be sure to restart your server after you edit the mime_types initializer. The updated setting won't take effect until you do.
|
|
104
117
|
|
|
118
|
+
Appending your own pages
|
|
119
|
+
===
|
|
120
|
+
Pass a `Payday::Appendix` to add pages of your own to the end of an invoice — a per-item breakdown, a delivery note, terms and conditions:
|
|
121
|
+
|
|
122
|
+
``` ruby
|
|
123
|
+
appendix = Payday::Appendix.new(
|
|
124
|
+
source: File.read('appendix.typ'),
|
|
125
|
+
inputs: {'shipments' => shipments.to_json},
|
|
126
|
+
dependencies: {'chart.svg' => File.binread('chart.svg')}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
invoice.render_pdf(appendix: appendix)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`source` is [Typst](https://typst.app) markup appended to Payday'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".
|
|
133
|
+
|
|
134
|
+
`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.
|
|
135
|
+
|
|
136
|
+
**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.
|
|
137
|
+
|
|
138
|
+
An appendix that reads its data and prints a heading:
|
|
139
|
+
|
|
140
|
+
``` typst
|
|
141
|
+
#let a = json(bytes(sys.inputs.shipments))
|
|
142
|
+
|
|
143
|
+
#pagebreak()
|
|
144
|
+
#text(size: 16pt, weight: "bold")[#a.title]
|
|
145
|
+
```
|
|
146
|
+
|
|
105
147
|
I18n
|
|
106
148
|
===
|
|
107
149
|
Payday uses the i18n gem to provide support for custom labels and internationalized applications. You can change the default labels by adding a YAML file in the `config/locales` directory of your Rails app. Here are the default labels you can customize:
|
|
@@ -166,7 +208,9 @@ Here's what we're planning on working on with Payday in the near future:
|
|
|
166
208
|
|
|
167
209
|
Acknowledgements
|
|
168
210
|
===
|
|
169
|
-
|
|
211
|
+
Payday renders with [Typst](https://typst.app) and its [Ruby binding](https://github.com/actsasflinn/typst-rb).
|
|
212
|
+
|
|
213
|
+
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.
|
|
170
214
|
|
|
171
215
|
License
|
|
172
216
|
===
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payday
|
|
4
|
+
|
|
5
|
+
# Extra pages compiled onto the end of an invoice.
|
|
6
|
+
#
|
|
7
|
+
# appendix = Payday::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 Payday'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
|
data/lib/payday/config.rb
CHANGED
|
@@ -8,9 +8,9 @@ module Payday
|
|
|
8
8
|
|
|
9
9
|
attr_accessor :invoice_logo, :company_name, :company_details, :date_format, :currency
|
|
10
10
|
|
|
11
|
-
# Sets the page size to use.
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# Sets the page size to use. Accepts the names Payday has always taken, 'LETTER', 'A4'
|
|
12
|
+
# and 'LEGAL', which the template maps onto the equivalent Typst paper. Anything else
|
|
13
|
+
# falls back to A4.
|
|
14
14
|
attr_accessor :page_size
|
|
15
15
|
|
|
16
16
|
# Returns the default configuration instance
|
|
@@ -23,7 +23,7 @@ module Payday
|
|
|
23
23
|
# Primarily intended for use in our tests.
|
|
24
24
|
def reset
|
|
25
25
|
# TODO: Move into specs and make minimal configuration required (company name / details)
|
|
26
|
-
self.invoice_logo = File.
|
|
26
|
+
self.invoice_logo = File.expand_path('assets/default_logo.png', __dir__)
|
|
27
27
|
self.company_name = 'Awesome Corp'
|
|
28
28
|
self.company_details = 'awesomecorp@commondream.net'
|
|
29
29
|
self.date_format = '%B %e, %Y'
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payday
|
|
4
|
+
|
|
5
|
+
# Turns an Invoiceable into the plain Hash the Typst template consumes.
|
|
6
|
+
#
|
|
7
|
+
# Every currency, date and translation decision happens here, so the template stays a pure
|
|
8
|
+
# layout concern and receives nothing but literal strings.
|
|
9
|
+
class InvoicePresenter
|
|
10
|
+
|
|
11
|
+
def initialize(invoice)
|
|
12
|
+
@invoice = invoice
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{
|
|
17
|
+
page_size: setting(:page_size),
|
|
18
|
+
company_name: setting(:company_name).strip,
|
|
19
|
+
company_details: company_details,
|
|
20
|
+
stamp: stamp,
|
|
21
|
+
bill_to: @invoice.bill_to,
|
|
22
|
+
ship_to: ship_to,
|
|
23
|
+
labels: labels,
|
|
24
|
+
details: details,
|
|
25
|
+
line_items: line_items,
|
|
26
|
+
totals: totals,
|
|
27
|
+
notes: Markup.to_runs(@invoice.notes),
|
|
28
|
+
qr_code: qr_code
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def line_items
|
|
33
|
+
@invoice.line_items.map { |line| line_item(line) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def totals
|
|
37
|
+
[[t('invoice.subtotal', 'Subtotal:'), money(@invoice.subtotal), false],
|
|
38
|
+
[tax_label, money(@invoice.tax), false],
|
|
39
|
+
*shipping_row,
|
|
40
|
+
*retention_row,
|
|
41
|
+
[t('invoice.total', 'Total:'), money(@invoice.total), true]]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def stamp
|
|
45
|
+
return t('status.refunded', 'REFUNDED') if @invoice.refunded?
|
|
46
|
+
return t('status.paid', 'PAID') if @invoice.paid?
|
|
47
|
+
|
|
48
|
+
t('status.overdue', 'OVERDUE') if @invoice.overdue?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def details
|
|
52
|
+
rows = [*number_row, *due_row, *paid_row]
|
|
53
|
+
@invoice.each_detail { |key, value| rows << [key.to_s, value.to_s] }
|
|
54
|
+
rows
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def labels
|
|
58
|
+
{
|
|
59
|
+
bill_to: t('invoice.bill_to', 'Bill To'),
|
|
60
|
+
ship_to: t('invoice.ship_to', 'Ship To'),
|
|
61
|
+
notes: t('invoice.notes', 'Notes'),
|
|
62
|
+
description: t('line_item.description', 'Description'),
|
|
63
|
+
unit_price: t('line_item.unit_price', 'Unit Price'),
|
|
64
|
+
quantity: t('line_item.quantity', 'Quantity'),
|
|
65
|
+
amount: t('line_item.amount', 'Amount')
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def shipping_row
|
|
72
|
+
[[shipping_label, money(@invoice.shipping), false]] if @invoice.shipping_rate.positive?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def retention_row
|
|
76
|
+
[[retention_label, money(-@invoice.retention), false]] if @invoice.retention_rate.positive?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def number_row
|
|
80
|
+
[[number_label, @invoice.invoice_number.to_s]] if @invoice.invoice_number
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def due_row
|
|
84
|
+
[[t('invoice.due_date', 'Due Date:'), date(@invoice.due_at)]] if @invoice.due_at
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def paid_row
|
|
88
|
+
[[t('invoice.paid_date', 'Paid Date:'), date(@invoice.paid_at)]] if @invoice.paid_at
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def qr_code
|
|
92
|
+
return nil unless @invoice.respond_to?(:qr_code) && @invoice.qr_code.to_s.strip.present?
|
|
93
|
+
|
|
94
|
+
@invoice.qr_code.to_s
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def line_item(line)
|
|
98
|
+
return predefined_line_item(line) if line.predefined_amount
|
|
99
|
+
|
|
100
|
+
{description: Markup.to_runs(line.description),
|
|
101
|
+
price: line.display_price || money(line.price),
|
|
102
|
+
quantity: line.display_quantity || BigDecimal(line.quantity.to_s).to_s('F'),
|
|
103
|
+
amount: money(line.amount)}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def predefined_line_item(line)
|
|
107
|
+
{description: Markup.to_runs(line.description), price: '', quantity: '',
|
|
108
|
+
amount: money(line.predefined_amount)}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def tax_label
|
|
112
|
+
@invoice.tax_description || t('invoice.tax', 'Tax:')
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def shipping_label
|
|
116
|
+
@invoice.shipping_description || t('invoice.shipping', 'Shipping:')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def retention_label
|
|
120
|
+
@invoice.retention_description || t('invoice.retention', 'Retention:')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def money(number)
|
|
124
|
+
PdfRenderer.number_to_currency(number, @invoice)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Prawn rendered each line of company_details as its own table cell, which collapsed any
|
|
128
|
+
# leading indentation. Config is commonly set from an indented heredoc, so strip it here
|
|
129
|
+
# rather than let it show up in the PDF.
|
|
130
|
+
def company_details
|
|
131
|
+
setting(:company_details).to_s.lines.map(&:strip).join("\n").strip
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def ship_to
|
|
135
|
+
@invoice.ship_to if @invoice.respond_to?(:ship_to)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def number_label
|
|
139
|
+
return t('invoice.receipt_no', 'Receipt #:') if @invoice.paid?
|
|
140
|
+
|
|
141
|
+
t('invoice.invoice_no', 'Invoice #:')
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def date(value)
|
|
145
|
+
return value.to_s unless value.is_a?(Date) || value.is_a?(Time)
|
|
146
|
+
|
|
147
|
+
value.strftime(Payday::Config.default.date_format)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def t(key, default)
|
|
151
|
+
I18n.t("payday.#{key}", default: default)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def setting(property)
|
|
155
|
+
return @invoice.send(property) if @invoice.respond_to?(property) && @invoice.send(property)
|
|
156
|
+
|
|
157
|
+
Payday::Config.default.send(property)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
end
|
data/lib/payday/invoiceable.rb
CHANGED
|
@@ -78,14 +78,15 @@ module Payday
|
|
|
78
78
|
defined?(paid_at) && !!paid_at
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
# Renders this invoice to pdf as a string
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
# Renders this invoice to pdf as a string. Pass a Payday::Appendix to compile pages of
|
|
82
|
+
# your own onto the end of it.
|
|
83
|
+
def render_pdf(appendix: nil)
|
|
84
|
+
Payday::PdfRenderer.render(self, appendix: appendix)
|
|
84
85
|
end
|
|
85
86
|
|
|
86
87
|
# Renders this invoice to pdf
|
|
87
|
-
def render_pdf_to_file(path)
|
|
88
|
-
Payday::PdfRenderer.render_to_file(self, path)
|
|
88
|
+
def render_pdf_to_file(path, appendix: nil)
|
|
89
|
+
Payday::PdfRenderer.render_to_file(self, path, appendix: appendix)
|
|
89
90
|
end
|
|
90
91
|
|
|
91
92
|
# Iterates through the details on this invoiceable. The block given should accept
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payday
|
|
4
|
+
|
|
5
|
+
# Converts the Prawn +inline_format+ markup Payday used to accept in notes and line item
|
|
6
|
+
# descriptions into structured runs that the Typst template can style.
|
|
7
|
+
#
|
|
8
|
+
# We deliberately produce data rather than Typst source. The template renders each run's
|
|
9
|
+
# +text+ as a literal string, so a customer-supplied description can never become Typst
|
|
10
|
+
# code. Never change this to emit markup.
|
|
11
|
+
class Markup
|
|
12
|
+
|
|
13
|
+
# Tags that simply switch a style on, mapped to the key they set on a run.
|
|
14
|
+
FLAGS = {'b' => :bold, 'i' => :italic, 'u' => :underline, 'strikethrough' => :strike,
|
|
15
|
+
'sub' => :sub, 'sup' => :sup}.freeze
|
|
16
|
+
|
|
17
|
+
TAG = %r{<(?<name>b|i|u|strikethrough|sub|sup|font|color|link)(?<attrs>[^>]*)>(?<body>.*?)</\k<name>>}m
|
|
18
|
+
LINE_BREAK = %r{<br\s*/?>}
|
|
19
|
+
|
|
20
|
+
# Returns an Array of Hashes, each with a :text key and any of :bold, :italic, :underline,
|
|
21
|
+
# :strike, :sub, :sup, :size, :color and :link. Returns nil when there is nothing to render.
|
|
22
|
+
def self.to_runs(text)
|
|
23
|
+
return nil if text.nil?
|
|
24
|
+
|
|
25
|
+
runs(text.to_s.gsub(LINE_BREAK, "\n"), {})
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.runs(text, style)
|
|
29
|
+
result = []
|
|
30
|
+
position = 0
|
|
31
|
+
|
|
32
|
+
while (match = TAG.match(text, position))
|
|
33
|
+
append(result, style, text[position...match.begin(0)])
|
|
34
|
+
result.concat(runs(match[:body], style.merge(style_for(match[:name], match[:attrs]))))
|
|
35
|
+
position = match.end(0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
append(result, style, text[position..])
|
|
39
|
+
result
|
|
40
|
+
end
|
|
41
|
+
private_class_method :runs
|
|
42
|
+
|
|
43
|
+
def self.append(result, style, text)
|
|
44
|
+
result << style.merge(text: text) unless text.nil? || text.empty?
|
|
45
|
+
end
|
|
46
|
+
private_class_method :append
|
|
47
|
+
|
|
48
|
+
def self.style_for(name, attrs)
|
|
49
|
+
return {FLAGS[name] => true} if FLAGS.key?(name)
|
|
50
|
+
|
|
51
|
+
case name
|
|
52
|
+
when 'font' then {size: attrs[/size=['"](\d+)['"]/, 1].to_i}
|
|
53
|
+
when 'color' then {color: attrs[/rgb=['"]#?(\h{6})['"]/, 1]}
|
|
54
|
+
when 'link' then {link: attrs[/href=['"](.*?)['"]/, 1]}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
private_class_method :style_for
|
|
58
|
+
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
end
|