webtranslateit-payday 2.0.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c69e90db47eb429202583e59002641017e200efa98a05989a8112db2504ca8d6
4
- data.tar.gz: 0e1a0a649e12c88225f5eafd95bc7b2eb2f1a1b3825570ca54611ea072d16a6a
3
+ metadata.gz: 4cb98acc78809a09e4945f8d8525aee1bbafd3ed17a45234be6fb0a8836f3135
4
+ data.tar.gz: b3379b263fb55f5d7fdb8ec48d66924e0f0b1110325b43e15bbd0a764fbc6154
5
5
  SHA512:
6
- metadata.gz: 3bfaddfd9cbd922672a519e1309e723a8abfa5e64d035e1d3e355c9437ce93d0734fdfbfb7b624b1389f2ff66dc65685327a9cd802826055468b6353351df606
7
- data.tar.gz: beac41f39e7cc6013e6eb7f94057e55042daad14c73a17b33a2e99bbcbc8b23d0b63e4c67618c5338298131e02c3f1614349d89983a42805ac693d76a0df1625
6
+ metadata.gz: c0473911f660484293361d1fbfb7020efed4614c06dd89b000fc7995f90e52b995c763781f175609578fd42090923ed9969baab0d4bffb85140f887356964690
7
+ data.tar.gz: 0a401edbb42eb073bf35602caaf6c2b7b762450dff19a04f2a8ee4d3ae8aca0ecf2ceab4485300a0647a2005b22116edd2cd1eba0be60a07117f5d44e5637b39
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
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
+
3
13
  ## 2.0.0 (2026-08-26)
4
14
 
5
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.
data/README.md CHANGED
@@ -115,6 +115,35 @@ end
115
115
 
116
116
  Be sure to restart your server after you edit the mime_types initializer. The updated setting won't take effect until you do.
117
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
+
118
147
  I18n
119
148
  ===
120
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:
@@ -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
@@ -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
- def render_pdf
83
- Payday::PdfRenderer.render(self)
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
@@ -13,13 +13,14 @@ module Payday
13
13
  FONTS = %w[NotoSans-Regular.ttf NotoSans-Bold.ttf].freeze
14
14
 
15
15
  # Renders the given invoice as a pdf on disk
16
- def self.render_to_file(invoice, path)
17
- File.binwrite(path, render(invoice))
16
+ def self.render_to_file(invoice, path, appendix: nil)
17
+ File.binwrite(path, render(invoice, appendix: appendix))
18
18
  end
19
19
 
20
- # Renders the given invoice as a pdf, returning a string
21
- def self.render(invoice)
22
- new(invoice).render
20
+ # Renders the given invoice as a pdf, returning a string. See Payday::Appendix for
21
+ # appending pages of your own to it.
22
+ def self.render(invoice, appendix: nil)
23
+ new(invoice, appendix: appendix).render
23
24
  end
24
25
 
25
26
  # Converts this number to a formatted currency string
@@ -39,8 +40,9 @@ module Payday
39
40
  end
40
41
  private_class_method :currency_for
41
42
 
42
- def initialize(invoice)
43
+ def initialize(invoice, appendix: nil)
43
44
  @invoice = invoice
45
+ @appendix = Appendix.wrap(appendix)
44
46
  @dependencies = {}
45
47
  end
46
48
 
@@ -49,13 +51,44 @@ module Payday
49
51
  register_qr_code(data[:qr_code])
50
52
  data[:logo] = logo
51
53
 
52
- Typst(body: File.read(TEMPLATE), dependencies: @dependencies, fonts: fonts,
53
- sys_inputs: {'invoice' => data.to_json})
54
+ Typst(body: body, dependencies: dependencies, fonts: fonts, sys_inputs: sys_inputs(data))
54
55
  .compile(:pdf).bytes.flatten.pack('C*')
55
56
  end
56
57
 
57
58
  private
58
59
 
60
+ # An appendix is concatenated rather than +#include+d, which would make it a module of its
61
+ # own and leave the template's page rules -- the margins, and the numbering footer --
62
+ # stopping at its edge.
63
+ def body
64
+ template = File.read(TEMPLATE)
65
+ return template if @appendix.nil?
66
+
67
+ "#{template}\n#{@appendix.source}"
68
+ end
69
+
70
+ def sys_inputs(data)
71
+ merge({'invoice' => data.to_json}, @appendix&.inputs, 'sys_input')
72
+ end
73
+
74
+ # Read after the logo and the QR code have registered theirs, so an appendix reusing one
75
+ # of those names is caught rather than silently replacing the file.
76
+ def dependencies
77
+ merge(@dependencies, @appendix&.dependencies, 'dependency')
78
+ end
79
+
80
+ def merge(ours, theirs, what)
81
+ return ours if theirs.nil?
82
+
83
+ taken = ours.keys & theirs.keys
84
+ if taken.any?
85
+ raise ArgumentError,
86
+ "appendix #{what} #{taken.map(&:inspect).join(', ')} is reserved by the invoice"
87
+ end
88
+
89
+ ours.merge(theirs)
90
+ end
91
+
59
92
  def fonts
60
93
  FONTS.to_h { |name| [name, File.binread(File.join(FONT_DIR, name))] }
61
94
  end
data/payday.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'webtranslateit-payday'
5
- s.version = '2.0.0'
5
+ s.version = '2.1.0'
6
6
  s.required_ruby_version = '>= 3.2'
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ['Alan Johnson', 'Edouard Briere']
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webtranslateit-payday
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alan Johnson
@@ -147,6 +147,7 @@ files:
147
147
  - lib/generators/payday/setup/templates/line_item.rb
148
148
  - lib/generators/payday/setup/templates/migration.rb
149
149
  - lib/payday.rb
150
+ - lib/payday/appendix.rb
150
151
  - lib/payday/assets/default_logo.png
151
152
  - lib/payday/config.rb
152
153
  - lib/payday/invoice.rb