webtranslateit-payday 2.0.0 → 2.1.1
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 +16 -0
- data/README.md +29 -0
- data/lib/payday/appendix.rb +47 -0
- data/lib/payday/config.rb +7 -0
- data/lib/payday/invoice.rb +17 -29
- data/lib/payday/invoice_presenter.rb +2 -4
- data/lib/payday/invoiceable.rb +24 -31
- data/lib/payday/line_item.rb +13 -11
- data/lib/payday/pdf_renderer.rb +43 -23
- data/payday.gemspec +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e997f24c5936a24f8deb333262cdfb74ecf989ddd229052797211579748a8f8c
|
|
4
|
+
data.tar.gz: af5120a7dd9edd4a7cf8e980d5090f2c20a2d1e0c32d654aa711c93b777cc429
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73f336ccbb26a4e1ab7b98ca3f95cd407e5730d7508884c361049e1ad1120b6a09f02609f44f5edb4e492d5dff0563c3021c16fc72e67756926c50463308875e
|
|
7
|
+
data.tar.gz: ab853437c9e8d4bcdb647bde8535e0d0a5e18a9e93e5d132be4cebaccec844a5e649915123ac4216d50f832c79604905a3d7eb755b5e5dbae59cfa957d749126
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 2.1.1 (2026-11-08)
|
|
4
|
+
|
|
5
|
+
* An invoice that defines `date_format` now has its dates formatted with it, the way it could already override `currency`, `invoice_logo`, `page_size`, `company_name` and `company_details`. Every one of those is read through the new `Config#value_for(invoice, property)` (#319, #321). `Payday::Invoice` has no `date_format`, so its output is unchanged.
|
|
6
|
+
* Internal refactoring, no behaviour change: `Invoice#initialize` assigns its options from one list (#314); the invoice rate setters and the `LineItem` setters each share one decimal conversion (#315, #316); `Invoiceable#tax`, `#shipping` and `#retention` are one-liners (#315); `overdue?` reads through `Invoiceable.past?` (#317); `each_detail` is tidied (#318); and two doc comments no longer need RuboCop line-length directives (#320).
|
|
7
|
+
* The code satisfies RuboCop 1.90's `Style/DirectiveScope` cop, the linter runs on Ruby 4.0, and guard and pry are gone from the development setup.
|
|
8
|
+
|
|
9
|
+
## 2.1.0 (2026-08-27)
|
|
10
|
+
|
|
11
|
+
* 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.
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
3
19
|
## 2.0.0 (2026-08-26)
|
|
4
20
|
|
|
5
21
|
**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
|
data/lib/payday/config.rb
CHANGED
|
@@ -18,6 +18,13 @@ module Payday
|
|
|
18
18
|
@default ||= new
|
|
19
19
|
end
|
|
20
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
|
+
|
|
21
28
|
# Internal: Resets a config object back to its default settings.
|
|
22
29
|
#
|
|
23
30
|
# Primarily intended for use in our tests.
|
data/lib/payday/invoice.rb
CHANGED
|
@@ -13,49 +13,30 @@ module Payday
|
|
|
13
13
|
|
|
14
14
|
attr_reader :tax_rate, :shipping_rate, :retention_rate
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
self.notes = options[:notes] || nil
|
|
16
|
+
# Options assigned as given; a missing one is nil, which the rate setters turn into 0.
|
|
17
|
+
OPTIONS = %i[invoice_number bill_to ship_to notes shipping_rate shipping_description tax_rate
|
|
18
|
+
tax_description retention_rate retention_description due_at paid_at refunded_at
|
|
19
|
+
currency invoice_date qr_code].freeze
|
|
20
|
+
|
|
21
|
+
def initialize(options = {})
|
|
22
|
+
OPTIONS.each { |name| public_send(:"#{name}=", options[name]) }
|
|
24
23
|
self.line_items = options[:line_items] || []
|
|
25
|
-
self.shipping_rate = options[:shipping_rate] || nil
|
|
26
|
-
self.shipping_description = options[:shipping_description] || nil
|
|
27
|
-
self.tax_rate = options[:tax_rate] || nil
|
|
28
|
-
self.tax_description = options[:tax_description] || nil
|
|
29
|
-
self.retention_rate = options[:retention_rate] || nil
|
|
30
|
-
self.retention_description = options[:retention_description] || nil
|
|
31
|
-
self.due_at = options[:due_at] || nil
|
|
32
|
-
self.paid_at = options[:paid_at] || nil
|
|
33
|
-
self.refunded_at = options[:refunded_at] || nil
|
|
34
|
-
self.currency = options[:currency] || nil
|
|
35
24
|
self.invoice_details = options[:invoice_details] || []
|
|
36
|
-
self.invoice_date = options[:invoice_date] || nil
|
|
37
|
-
self.qr_code = options[:qr_code] || nil
|
|
38
25
|
end
|
|
39
|
-
# rubocop:enable Metrics/AbcSize
|
|
40
|
-
# rubocop:enable Metrics/MethodLength
|
|
41
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
|
42
26
|
|
|
43
27
|
# The tax rate that we're applying, as a BigDecimal
|
|
44
28
|
def tax_rate=(value)
|
|
45
|
-
|
|
46
|
-
@tax_rate = BigDecimal(value.to_s)
|
|
29
|
+
@tax_rate = to_rate(value)
|
|
47
30
|
end
|
|
48
31
|
|
|
49
32
|
# Shipping rate
|
|
50
33
|
def shipping_rate=(value)
|
|
51
|
-
|
|
52
|
-
@shipping_rate = BigDecimal(value.to_s)
|
|
34
|
+
@shipping_rate = to_rate(value)
|
|
53
35
|
end
|
|
54
36
|
|
|
55
37
|
# Retention rate (e.g. IRPF). Applied as a percentage of the subtotal and deducted from the total.
|
|
56
38
|
def retention_rate=(value)
|
|
57
|
-
|
|
58
|
-
@retention_rate = BigDecimal(value.to_s)
|
|
39
|
+
@retention_rate = to_rate(value)
|
|
59
40
|
end
|
|
60
41
|
|
|
61
42
|
# Adds a line item
|
|
@@ -63,6 +44,13 @@ module Payday
|
|
|
63
44
|
line_items << Payday::LineItem.new(options)
|
|
64
45
|
end
|
|
65
46
|
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# A blank rate (nil, "", " ") is 0.
|
|
50
|
+
def to_rate(value)
|
|
51
|
+
BigDecimal(value.to_s.presence || 0)
|
|
52
|
+
end
|
|
53
|
+
|
|
66
54
|
end
|
|
67
55
|
|
|
68
56
|
end
|
|
@@ -144,7 +144,7 @@ module Payday
|
|
|
144
144
|
def date(value)
|
|
145
145
|
return value.to_s unless value.is_a?(Date) || value.is_a?(Time)
|
|
146
146
|
|
|
147
|
-
value.strftime(
|
|
147
|
+
value.strftime(setting(:date_format))
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
def t(key, default)
|
|
@@ -152,9 +152,7 @@ module Payday
|
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
def setting(property)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
Payday::Config.default.send(property)
|
|
155
|
+
Payday::Config.default.value_for(@invoice, property)
|
|
158
156
|
end
|
|
159
157
|
|
|
160
158
|
end
|
data/lib/payday/invoiceable.rb
CHANGED
|
@@ -12,10 +12,8 @@
|
|
|
12
12
|
# when generating an invoice. We include a simple tax method that calculates tax, but it's probably wiser
|
|
13
13
|
# to override this in your class (our calculated tax won't be stored to a database by default, for example).
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# rubocop:enable Layout/LineLength
|
|
18
|
-
# paid dates, and refunded dates, as well as stamps showing if the invoice is paid or due.
|
|
15
|
+
# If the +due_at+, +paid_at+, and +refunded_at+ methods are available, {Payday::Invoiceable} will use them
|
|
16
|
+
# to show due dates, paid dates, and refunded dates, as well as stamps showing if the invoice is paid or due.
|
|
19
17
|
module Payday
|
|
20
18
|
|
|
21
19
|
module Invoiceable
|
|
@@ -32,31 +30,19 @@ module Payday
|
|
|
32
30
|
|
|
33
31
|
# The tax for this invoice, as a BigDecimal
|
|
34
32
|
def tax
|
|
35
|
-
|
|
36
|
-
subtotal * tax_rate / 100
|
|
37
|
-
else
|
|
38
|
-
0
|
|
39
|
-
end
|
|
33
|
+
defined?(tax_rate) ? subtotal * tax_rate / 100 : 0
|
|
40
34
|
end
|
|
41
35
|
|
|
42
36
|
# TODO: Add a per weight unit shipping cost
|
|
43
37
|
# Calculates the shipping
|
|
44
38
|
def shipping
|
|
45
|
-
|
|
46
|
-
shipping_rate
|
|
47
|
-
else
|
|
48
|
-
0
|
|
49
|
-
end
|
|
39
|
+
defined?(shipping_rate) ? shipping_rate : 0
|
|
50
40
|
end
|
|
51
41
|
|
|
52
42
|
# Calculates the retention (e.g. IRPF) as a percentage of the subtotal.
|
|
53
43
|
# This is a post-tax deduction subtracted from the total.
|
|
54
44
|
def retention
|
|
55
|
-
|
|
56
|
-
subtotal * retention_rate / 100
|
|
57
|
-
else
|
|
58
|
-
0
|
|
59
|
-
end
|
|
45
|
+
defined?(retention_rate) ? subtotal * retention_rate / 100 : 0
|
|
60
46
|
end
|
|
61
47
|
|
|
62
48
|
# Calculates the total for this invoice.
|
|
@@ -65,9 +51,17 @@ module Payday
|
|
|
65
51
|
end
|
|
66
52
|
|
|
67
53
|
def overdue?
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
54
|
+
defined?(due_at) && Invoiceable.past?(due_at) && !paid_at
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Whether a Date is before today or a Time before now. A module function rather than a
|
|
58
|
+
# private method, so it does not land in the classes that include Invoiceable.
|
|
59
|
+
def self.past?(moment)
|
|
60
|
+
case moment
|
|
61
|
+
when Date then moment < Date.today
|
|
62
|
+
when Time then moment < Time.now
|
|
63
|
+
else false
|
|
64
|
+
end
|
|
71
65
|
end
|
|
72
66
|
|
|
73
67
|
def refunded?
|
|
@@ -78,24 +72,23 @@ module Payday
|
|
|
78
72
|
defined?(paid_at) && !!paid_at
|
|
79
73
|
end
|
|
80
74
|
|
|
81
|
-
# Renders this invoice to pdf as a string
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
# Renders this invoice to pdf as a string. Pass a Payday::Appendix to compile pages of
|
|
76
|
+
# your own onto the end of it.
|
|
77
|
+
def render_pdf(appendix: nil)
|
|
78
|
+
Payday::PdfRenderer.render(self, appendix: appendix)
|
|
84
79
|
end
|
|
85
80
|
|
|
86
81
|
# Renders this invoice to pdf
|
|
87
|
-
def render_pdf_to_file(path)
|
|
88
|
-
Payday::PdfRenderer.render_to_file(self, path)
|
|
82
|
+
def render_pdf_to_file(path, appendix: nil)
|
|
83
|
+
Payday::PdfRenderer.render_to_file(self, path, appendix: appendix)
|
|
89
84
|
end
|
|
90
85
|
|
|
91
86
|
# Iterates through the details on this invoiceable. The block given should accept
|
|
92
87
|
# two parameters, the detail name and the actual detail value.
|
|
93
88
|
def each_detail
|
|
94
|
-
return
|
|
89
|
+
return unless defined?(invoice_details)
|
|
95
90
|
|
|
96
|
-
invoice_details.each
|
|
97
|
-
yield(detail[0], detail[1])
|
|
98
|
-
end
|
|
91
|
+
invoice_details.each { |detail| yield detail[0], detail[1] }
|
|
99
92
|
end
|
|
100
93
|
|
|
101
94
|
end
|
data/lib/payday/line_item.rb
CHANGED
|
@@ -4,11 +4,9 @@ module Payday
|
|
|
4
4
|
|
|
5
5
|
# Represents a line item in an invoice.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# them your invoice math could get pretty messed up. It's recommended that both values be set to +BigDecimal+ values.
|
|
11
|
-
# Otherwise, we'll do our best to convert the set values to a +BigDecimal+.
|
|
7
|
+
# +quantity+ and +price+ are written to be pretty picky, primarily because if we're not picky about what
|
|
8
|
+
# values are set to them your invoice math could get pretty messed up. It's recommended that both values be
|
|
9
|
+
# set to +BigDecimal+ values. Otherwise, we'll do our best to convert the set values to a +BigDecimal+.
|
|
12
10
|
class LineItem
|
|
13
11
|
|
|
14
12
|
include LineItemable
|
|
@@ -31,20 +29,24 @@ module Payday
|
|
|
31
29
|
|
|
32
30
|
# Sets the quantity of this {LineItem}
|
|
33
31
|
def quantity=(value)
|
|
34
|
-
|
|
35
|
-
@quantity = BigDecimal(value.to_s)
|
|
32
|
+
@quantity = to_decimal(value)
|
|
36
33
|
end
|
|
37
34
|
|
|
38
35
|
# Sets the price for this {LineItem}
|
|
39
36
|
def price=(value)
|
|
40
|
-
|
|
41
|
-
@price = BigDecimal(value.to_s)
|
|
37
|
+
@price = to_decimal(value)
|
|
42
38
|
end
|
|
43
39
|
|
|
44
40
|
# Sets the predefined_amount for this {LineItem}
|
|
45
41
|
def predefined_amount=(value)
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
@predefined_amount = to_decimal(value)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# A blank value (nil, "", " ") is 0.
|
|
48
|
+
def to_decimal(value)
|
|
49
|
+
BigDecimal(value.to_s.presence || 0)
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
end
|
data/lib/payday/pdf_renderer.rb
CHANGED
|
@@ -13,34 +13,29 @@ 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
|
-
|
|
22
|
-
|
|
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
|
|
26
27
|
def self.number_to_currency(number, invoice)
|
|
27
28
|
Money.locale_backend = :currency
|
|
28
29
|
Money.rounding_mode = BigDecimal::ROUND_HALF_UP
|
|
29
|
-
currency = Money::Currency.wrap(
|
|
30
|
+
currency = Money::Currency.wrap(Payday::Config.default.value_for(invoice, :currency))
|
|
30
31
|
number *= currency.subunit_to_unit
|
|
31
32
|
number = number.round unless Money.default_infinite_precision
|
|
32
33
|
Money.new(number, currency).format
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
def
|
|
36
|
-
return invoice.currency if invoice.respond_to?(:currency) && invoice.currency
|
|
37
|
-
|
|
38
|
-
Payday::Config.default.currency
|
|
39
|
-
end
|
|
40
|
-
private_class_method :currency_for
|
|
41
|
-
|
|
42
|
-
def initialize(invoice)
|
|
36
|
+
def initialize(invoice, appendix: nil)
|
|
43
37
|
@invoice = invoice
|
|
38
|
+
@appendix = Appendix.wrap(appendix)
|
|
44
39
|
@dependencies = {}
|
|
45
40
|
end
|
|
46
41
|
|
|
@@ -49,20 +44,51 @@ module Payday
|
|
|
49
44
|
register_qr_code(data[:qr_code])
|
|
50
45
|
data[:logo] = logo
|
|
51
46
|
|
|
52
|
-
Typst(body:
|
|
53
|
-
sys_inputs: {'invoice' => data.to_json})
|
|
47
|
+
Typst(body: body, dependencies: dependencies, fonts: fonts, sys_inputs: sys_inputs(data))
|
|
54
48
|
.compile(:pdf).bytes.flatten.pack('C*')
|
|
55
49
|
end
|
|
56
50
|
|
|
57
51
|
private
|
|
58
52
|
|
|
53
|
+
# An appendix is concatenated rather than +#include+d, which would make it a module of its
|
|
54
|
+
# own and leave the template's page rules -- the margins, and the numbering footer --
|
|
55
|
+
# stopping at its edge.
|
|
56
|
+
def body
|
|
57
|
+
template = File.read(TEMPLATE)
|
|
58
|
+
return template if @appendix.nil?
|
|
59
|
+
|
|
60
|
+
"#{template}\n#{@appendix.source}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def sys_inputs(data)
|
|
64
|
+
merge({'invoice' => data.to_json}, @appendix&.inputs, 'sys_input')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Read after the logo and the QR code have registered theirs, so an appendix reusing one
|
|
68
|
+
# of those names is caught rather than silently replacing the file.
|
|
69
|
+
def dependencies
|
|
70
|
+
merge(@dependencies, @appendix&.dependencies, 'dependency')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def merge(ours, theirs, what)
|
|
74
|
+
return ours if theirs.nil?
|
|
75
|
+
|
|
76
|
+
taken = ours.keys & theirs.keys
|
|
77
|
+
if taken.any?
|
|
78
|
+
raise ArgumentError,
|
|
79
|
+
"appendix #{what} #{taken.map(&:inspect).join(', ')} is reserved by the invoice"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
ours.merge(theirs)
|
|
83
|
+
end
|
|
84
|
+
|
|
59
85
|
def fonts
|
|
60
86
|
FONTS.to_h { |name| [name, File.binread(File.join(FONT_DIR, name))] }
|
|
61
87
|
end
|
|
62
88
|
|
|
63
89
|
# Config#invoice_logo is either a path or a {filename:, size: "WxH"} Hash.
|
|
64
90
|
def logo
|
|
65
|
-
setting = invoice_logo
|
|
91
|
+
setting = Payday::Config.default.value_for(@invoice, :invoice_logo)
|
|
66
92
|
return nil if setting.nil?
|
|
67
93
|
|
|
68
94
|
path, width, height = logo_parts(setting)
|
|
@@ -71,12 +97,6 @@ module Payday
|
|
|
71
97
|
{name: name, width: width, height: height}
|
|
72
98
|
end
|
|
73
99
|
|
|
74
|
-
def invoice_logo
|
|
75
|
-
return @invoice.invoice_logo if @invoice.respond_to?(:invoice_logo) && @invoice.invoice_logo
|
|
76
|
-
|
|
77
|
-
Payday::Config.default.invoice_logo
|
|
78
|
-
end
|
|
79
|
-
|
|
80
100
|
def logo_parts(setting)
|
|
81
101
|
return [setting, nil, nil] unless setting.is_a?(Hash)
|
|
82
102
|
|
data/payday.gemspec
CHANGED
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.
|
|
4
|
+
version: 2.1.1
|
|
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
|