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.
@@ -0,0 +1,171 @@
1
+ // Payday's invoice layout.
2
+ //
3
+ // All data arrives as JSON through sys.inputs, where Typst inserts string values as literal
4
+ // text rather than markup. Never interpolate invoice data into this file's source, and never
5
+ // call eval() on a value that came from an invoice.
6
+
7
+ #let d = json(bytes(sys.inputs.invoice))
8
+
9
+ // Gaps between the major blocks, calibrated so the rendered invoice keeps the vertical
10
+ // rhythm the prawn renderer produced. Every within-block gap falls out of the table insets.
11
+ #let gap-after-header = 15pt
12
+ #let gap-after-addresses = 13.2pt
13
+ #let gap-after-details = 8pt
14
+ #let gap-before-totals = -12pt
15
+ #let gap-before-notes = 22.3pt
16
+ #let gap-notes-label-to-rule = -7.1pt
17
+ #let gap-after-notes-rule = -0.4pt
18
+ #let gap-before-qr = -0.6pt
19
+
20
+ // Prawn page-size names mapped onto Typst paper names.
21
+ #let papers = ("LETTER": "us-letter", "A4": "a4", "LEGAL": "us-legal")
22
+
23
+ #set document(title: "Invoice", date: none)
24
+ #set page(
25
+ paper: papers.at(d.page_size, default: "a4"),
26
+ margin: 36pt, // prawn's default half-inch page margin
27
+ numbering: none,
28
+ // Page numbers appear only when the invoice runs to more than one page. This has to be a
29
+ // footer rather than a trailing `set page`, which would only affect the final page.
30
+ footer: context {
31
+ if counter(page).final().first() > 1 {
32
+ align(right, text(size: 9pt, counter(page).display("1 / 1", both: true)))
33
+ }
34
+ },
35
+ )
36
+ #set text(font: "Noto Sans", size: 10pt)
37
+
38
+ // Renders the styled runs produced by Payday::Markup.
39
+ #let render-runs(runs) = {
40
+ if runs == none { return }
41
+ for r in runs {
42
+ let body = text(
43
+ size: if "size" in r { r.size * 1pt } else { 1em },
44
+ fill: if "color" in r { rgb(r.color) } else { black },
45
+ weight: if r.at("bold", default: false) { "bold" } else { "regular" },
46
+ style: if r.at("italic", default: false) { "italic" } else { "normal" },
47
+ r.text,
48
+ )
49
+ if r.at("underline", default: false) { body = underline(body) }
50
+ if r.at("strike", default: false) { body = strike(body) }
51
+ if r.at("sub", default: false) { body = sub(body) }
52
+ if r.at("sup", default: false) { body = super(body) }
53
+ if "link" in r { link(r.link, body) } else { body }
54
+ }
55
+ }
56
+
57
+ // --- status stamp ------------------------------------------------------------------
58
+ // Drawn before anything else so it stays at a fixed offset from the top of the page, the way
59
+ // prawn stamped it, rather than drifting with the height of the logo.
60
+ #if d.stamp != none {
61
+ place(top + center, dx: 5.1pt, dy: 33pt, rotate(15deg,
62
+ text(fill: rgb("cc0000"), size: 25pt, weight: "bold")[#d.stamp]))
63
+ }
64
+
65
+ // --- logo and company identity ---------------------------------------------------
66
+ #grid(
67
+ columns: (1fr, auto),
68
+ align: (left + top, right + top),
69
+ if d.logo != none {
70
+ image(
71
+ d.logo.name,
72
+ width: if d.logo.width != none { d.logo.width * 1pt } else { auto },
73
+ height: if d.logo.height != none { d.logo.height * 1pt } else { auto },
74
+ fit: "contain",
75
+ )
76
+ },
77
+ align(left, pad(top: 5.7pt)[
78
+ #text(weight: "bold", size: 12pt)[#d.company_name] \
79
+ #d.company_details
80
+ ]),
81
+ )
82
+
83
+ #v(gap-after-header)
84
+
85
+ // --- bill to / ship to -------------------------------------------------------------
86
+ #grid(
87
+ columns: (1fr, 200pt),
88
+ align: (left + top, left + top),
89
+ [#text(weight: "bold")[#d.labels.bill_to]#v(10.4pt, weak: true)#d.bill_to],
90
+ if d.ship_to != none {
91
+ [#text(weight: "bold")[#d.labels.ship_to]#v(10.4pt, weak: true)#d.ship_to]
92
+ },
93
+ )
94
+
95
+ // prawn drew the ship-to box 2pt taller than its contents, so invoices carrying a ship-to
96
+ // address pushed the details block down by that much.
97
+ #v(gap-after-addresses + if d.ship_to != none { 2pt } else { 0pt })
98
+
99
+ // --- invoice details ---------------------------------------------------------------
100
+ #if d.details.len() > 0 {
101
+ table(
102
+ columns: 2,
103
+ stroke: none,
104
+ inset: (left: 1pt, right: 0pt, y: 4.25pt),
105
+ column-gutter: 10pt,
106
+ align: (left, right),
107
+ ..d.details.map(row => (
108
+ text(weight: "bold")[#row.at(0)],
109
+ text(weight: "bold")[#row.at(1)],
110
+ )).flatten()
111
+ )
112
+ }
113
+
114
+ #v(gap-after-details)
115
+
116
+ // --- line items ---------------------------------------------------------------------
117
+ #table(
118
+ columns: (1fr, auto, auto, auto),
119
+ align: (left, right, right, right),
120
+ inset: (x: 10pt, y: 8.25pt),
121
+ stroke: (x, y) => (
122
+ left: none,
123
+ right: none,
124
+ top: 0.5pt + rgb("F6F9FC"),
125
+ bottom: 0.5pt + rgb("BCC6D0"),
126
+ ),
127
+ fill: (x, y) => if y == 0 { white } else if calc.odd(y) { rgb("F6F9FC") } else { white },
128
+ table.header(
129
+ text(weight: "bold")[#d.labels.description],
130
+ text(weight: "bold")[#d.labels.unit_price],
131
+ text(weight: "bold")[#d.labels.quantity],
132
+ text(weight: "bold")[#d.labels.amount],
133
+ ),
134
+ ..d.line_items.map(li => (
135
+ render-runs(li.description), [#li.price], [#li.quantity], [#li.amount],
136
+ )).flatten()
137
+ )
138
+
139
+ #v(gap-before-totals)
140
+
141
+ // --- totals ---------------------------------------------------------------------------
142
+ #align(right, table(
143
+ columns: 2,
144
+ stroke: none,
145
+ // The total is set 12pt, whose taller glyph box would otherwise pull the row up.
146
+ inset: (x, y) => (
147
+ left: 5pt, right: 5pt, bottom: 8.25pt,
148
+ top: if y == d.totals.len() - 1 { 8.95pt } else { 8.25pt },
149
+ ),
150
+ align: (left, right),
151
+ ..d.totals.map(row => {
152
+ let size = if row.at(2) { 12pt } else { 10pt }
153
+ (text(weight: "bold", size: size)[#row.at(0)], text(size: size)[#row.at(1)])
154
+ }).flatten()
155
+ ))
156
+
157
+ // --- notes ------------------------------------------------------------------------------
158
+ #if d.notes != none {
159
+ v(gap-before-notes)
160
+ text(weight: "bold")[#d.labels.notes]
161
+ v(gap-notes-label-to-rule)
162
+ line(length: 100%, stroke: 0.5pt + rgb("cccccc"))
163
+ v(gap-after-notes-rule)
164
+ render-runs(d.notes)
165
+ }
166
+
167
+ // --- QR code ------------------------------------------------------------------------------
168
+ #if d.qr_code != none {
169
+ v(gap-before-qr)
170
+ image("qr.svg", width: 100pt)
171
+ }
data/lib/payday.rb CHANGED
@@ -8,14 +8,14 @@ loader.setup
8
8
  require 'active_support/core_ext/object/blank' # For Object#blank? method
9
9
  require 'bigdecimal'
10
10
  require 'date'
11
+ require 'json'
11
12
  require 'money'
12
- require 'prawn'
13
- require 'prawn/table'
14
- require 'prawn-svg'
13
+ require 'typst'
15
14
  require 'time'
16
15
 
17
- # Load translations from the locale folder
18
- I18n.load_path.concat(Dir[File.join('config', 'locales', '*.yml')])
16
+ # Load our own translations. This has to resolve against the gem rather than the working
17
+ # directory, or the locales only load when the process happens to be running from the gem root.
18
+ I18n.load_path.concat(Dir[File.expand_path('../config/locales/*.yml', __dir__)])
19
19
 
20
20
  module Payday
21
21
  end
data/payday.gemspec CHANGED
@@ -2,26 +2,29 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'webtranslateit-payday'
5
- s.version = '1.7.4'
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']
9
9
  s.email = ['edouard@webtranslateit.com']
10
10
  s.homepage = 'https://github.com/webtranslateit/payday'
11
11
  s.summary = 'A simple library for rendering invoices.'
12
- s.description = 'Payday is a library for rendering invoices to pdf.'
12
+ s.description = 'Payday is a library for rendering invoices to pdf, using Typst.'
13
13
  s.license = 'MIT'
14
14
 
15
15
  s.add_dependency 'activesupport', '>= 7', '< 9'
16
16
  s.add_dependency 'i18n', '~> 1.12', '< 2'
17
17
  s.add_dependency 'money', '>= 6.16', '< 8.0'
18
- s.add_dependency 'prawn', '~> 2.4', '< 3'
19
- s.add_dependency 'prawn-svg', '~> 0.32', '< 1'
20
- s.add_dependency 'prawn-table', '~> 0.2', '< 1'
21
18
  s.add_dependency 'rqrcode', '>= 2', '< 4'
19
+ s.add_dependency 'typst', '~> 0.15'
22
20
  s.add_dependency 'zeitwerk', '~> 2.6', '< 3'
23
21
 
24
- s.files = `git ls-files`.split("\n")
22
+ # Only what the gem needs at run time, plus the docs. The spec suite carries several
23
+ # hundred kilobytes of reference PDFs that nobody installing this wants.
24
+ root_files = %w[README.md CHANGELOG.md payday.gemspec].freeze
25
+ s.files = `git ls-files -z`.split("\x0").select do |path|
26
+ path.start_with?('lib/', 'config/locales/', 'fonts/') || root_files.include?(path)
27
+ end
25
28
  s.require_paths = ['lib']
26
29
  s.metadata['rubygems_mfa_required'] = 'true'
27
30
  end
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: 1.7.4
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alan Johnson
@@ -71,85 +71,39 @@ dependencies:
71
71
  - !ruby/object:Gem::Version
72
72
  version: '8.0'
73
73
  - !ruby/object:Gem::Dependency
74
- name: prawn
75
- requirement: !ruby/object:Gem::Requirement
76
- requirements:
77
- - - "~>"
78
- - !ruby/object:Gem::Version
79
- version: '2.4'
80
- - - "<"
81
- - !ruby/object:Gem::Version
82
- version: '3'
83
- type: :runtime
84
- prerelease: false
85
- version_requirements: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '2.4'
90
- - - "<"
91
- - !ruby/object:Gem::Version
92
- version: '3'
93
- - !ruby/object:Gem::Dependency
94
- name: prawn-svg
74
+ name: rqrcode
95
75
  requirement: !ruby/object:Gem::Requirement
96
76
  requirements:
97
- - - "~>"
77
+ - - ">="
98
78
  - !ruby/object:Gem::Version
99
- version: '0.32'
79
+ version: '2'
100
80
  - - "<"
101
81
  - !ruby/object:Gem::Version
102
- version: '1'
82
+ version: '4'
103
83
  type: :runtime
104
84
  prerelease: false
105
85
  version_requirements: !ruby/object:Gem::Requirement
106
86
  requirements:
107
- - - "~>"
87
+ - - ">="
108
88
  - !ruby/object:Gem::Version
109
- version: '0.32'
89
+ version: '2'
110
90
  - - "<"
111
91
  - !ruby/object:Gem::Version
112
- version: '1'
92
+ version: '4'
113
93
  - !ruby/object:Gem::Dependency
114
- name: prawn-table
94
+ name: typst
115
95
  requirement: !ruby/object:Gem::Requirement
116
96
  requirements:
117
97
  - - "~>"
118
98
  - !ruby/object:Gem::Version
119
- version: '0.2'
120
- - - "<"
121
- - !ruby/object:Gem::Version
122
- version: '1'
99
+ version: '0.15'
123
100
  type: :runtime
124
101
  prerelease: false
125
102
  version_requirements: !ruby/object:Gem::Requirement
126
103
  requirements:
127
104
  - - "~>"
128
105
  - !ruby/object:Gem::Version
129
- version: '0.2'
130
- - - "<"
131
- - !ruby/object:Gem::Version
132
- version: '1'
133
- - !ruby/object:Gem::Dependency
134
- name: rqrcode
135
- requirement: !ruby/object:Gem::Requirement
136
- requirements:
137
- - - ">="
138
- - !ruby/object:Gem::Version
139
- version: '2'
140
- - - "<"
141
- - !ruby/object:Gem::Version
142
- version: '4'
143
- type: :runtime
144
- prerelease: false
145
- version_requirements: !ruby/object:Gem::Requirement
146
- requirements:
147
- - - ">="
148
- - !ruby/object:Gem::Version
149
- version: '2'
150
- - - "<"
151
- - !ruby/object:Gem::Version
152
- version: '4'
106
+ version: '0.15'
153
107
  - !ruby/object:Gem::Dependency
154
108
  name: zeitwerk
155
109
  requirement: !ruby/object:Gem::Requirement
@@ -170,27 +124,15 @@ dependencies:
170
124
  - - "<"
171
125
  - !ruby/object:Gem::Version
172
126
  version: '3'
173
- description: Payday is a library for rendering invoices to pdf.
127
+ description: Payday is a library for rendering invoices to pdf, using Typst.
174
128
  email:
175
129
  - edouard@webtranslateit.com
176
130
  executables: []
177
131
  extensions: []
178
132
  extra_rdoc_files: []
179
133
  files:
180
- - ".gitattributes"
181
- - ".github/dependabot.yml"
182
- - ".github/release-drafter.yml"
183
- - ".github/workflows/ci.yml"
184
- - ".github/workflows/release-drafter.yml"
185
- - ".gitignore"
186
- - ".rubocop.yml"
187
- - ".rubocop_todo.yml"
188
134
  - CHANGELOG.md
189
- - Gemfile
190
- - Gemfile.lock
191
- - Guardfile
192
135
  - README.md
193
- - Rakefile
194
136
  - config/locales/payday.de.yml
195
137
  - config/locales/payday.en.yml
196
138
  - config/locales/payday.es.yml
@@ -205,25 +147,19 @@ files:
205
147
  - lib/generators/payday/setup/templates/line_item.rb
206
148
  - lib/generators/payday/setup/templates/migration.rb
207
149
  - lib/payday.rb
150
+ - lib/payday/appendix.rb
151
+ - lib/payday/assets/default_logo.png
208
152
  - lib/payday/config.rb
209
153
  - lib/payday/invoice.rb
154
+ - lib/payday/invoice_presenter.rb
210
155
  - lib/payday/invoiceable.rb
211
156
  - lib/payday/line_item.rb
212
157
  - lib/payday/line_itemable.rb
158
+ - lib/payday/markup.rb
213
159
  - lib/payday/pdf_renderer.rb
160
+ - lib/payday/qr_code.rb
161
+ - lib/payday/templates/invoice.typ
214
162
  - payday.gemspec
215
- - spec/assets/default_logo.png
216
- - spec/assets/example_invoice_with_qr.pdf
217
- - spec/assets/svg.pdf
218
- - spec/assets/testing.pdf
219
- - spec/assets/testing_es.pdf
220
- - spec/assets/testing_predefined_amount.pdf
221
- - spec/assets/tiger.svg
222
- - spec/invoice_spec.rb
223
- - spec/line_item_spec.rb
224
- - spec/pdf_renderer_spec.rb
225
- - spec/spec_helper.rb
226
- - spec/support/asset_matchers.rb
227
163
  homepage: https://github.com/webtranslateit/payday
228
164
  licenses:
229
165
  - MIT
@@ -243,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
243
179
  - !ruby/object:Gem::Version
244
180
  version: '0'
245
181
  requirements: []
246
- rubygems_version: 4.0.16
182
+ rubygems_version: 3.6.9
247
183
  specification_version: 4
248
184
  summary: A simple library for rendering invoices.
249
185
  test_files: []
data/.gitattributes DELETED
@@ -1 +0,0 @@
1
- *.pdf binary
@@ -1,28 +0,0 @@
1
- # To get started with Dependabot version updates, you'll need to specify which
2
- # package ecosystems to update and where the package manifests are located.
3
- # Please see the documentation for all configuration options:
4
- # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5
-
6
- version: 2
7
- updates:
8
- - package-ecosystem: "bundler" # See documentation for possible values
9
- directory: "/" # Location of package manifests
10
- schedule:
11
- interval: "weekly"
12
- day: "monday"
13
- time: "04:00"
14
- reviewers:
15
- - edouard
16
- assignees:
17
- - edouard
18
-
19
- - package-ecosystem: github-actions
20
- directory: "/"
21
- schedule:
22
- interval: weekly
23
- day: monday
24
- time: "04:00"
25
- reviewers:
26
- - edouard
27
- assignees:
28
- - edouard
@@ -1,36 +0,0 @@
1
- name-template: '$RESOLVED_VERSION 🌈'
2
- tag-template: '$RESOLVED_VERSION'
3
- categories:
4
- - title: '🚀 Features'
5
- labels:
6
- - 'feature'
7
- - 'enhancement'
8
- - title: '🐛 Bug Fixes'
9
- labels:
10
- - 'bug'
11
- - 'design'
12
- - 'performance'
13
- - title: '🧰 Maintenance'
14
- labels:
15
- - 'admin'
16
- - 'refactoring'
17
- - 'dependencies'
18
- - 'github_actions'
19
- - 'tests'
20
- change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
21
- change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
22
- version-resolver:
23
- major:
24
- labels:
25
- - 'major'
26
- minor:
27
- labels:
28
- - 'minor'
29
- patch:
30
- labels:
31
- - 'patch'
32
- default: patch
33
- template: |
34
- ## Changes
35
-
36
- $CHANGES
@@ -1,47 +0,0 @@
1
- # This workflow uses actions that are not certified by GitHub.
2
- # They are provided by a third-party and are governed by
3
- # separate terms of service, privacy policy, and support
4
- # documentation.
5
- # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
- # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
-
8
- name: Ruby
9
-
10
- on: push
11
-
12
- jobs:
13
- test:
14
- runs-on: ubuntu-latest
15
- strategy:
16
- matrix:
17
- ruby-version: ['3.2', '3.3', '3.4', '4.0']
18
-
19
- steps:
20
- - uses: actions/checkout@v7
21
- - name: Set up Ruby
22
- # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
23
- # change this to (see https://github.com/ruby/setup-ruby#versioning):
24
- uses: ruby/setup-ruby@v1
25
- with:
26
- ruby-version: ${{ matrix.ruby-version }}
27
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
28
- - name: Run tests
29
- run: bundle exec rspec
30
-
31
- linters:
32
- name: Linters
33
- runs-on: ubuntu-latest
34
- timeout-minutes: 10
35
-
36
- steps:
37
- - name: Checkout repository
38
- uses: actions/checkout@v7
39
-
40
- - name: Install ruby and gems
41
- uses: ruby/setup-ruby@v1
42
- with:
43
- ruby-version: 3.3.6 # Use .ruby-version file instead of duplicating here and in Gemfile?
44
- bundler-cache: true
45
-
46
- - name: Run ruby linter
47
- run: bundle exec rubocop --format github
@@ -1,24 +0,0 @@
1
- name: Release Drafter
2
-
3
- on:
4
- push:
5
- # branches to consider in the event; optional, defaults to all
6
- branches:
7
- - main
8
- jobs:
9
- update_release_draft:
10
- runs-on: ubuntu-latest
11
- steps:
12
- # (Optional) GitHub Enterprise requires GHE_HOST variable set
13
- #- name: Set GHE_HOST
14
- # run: |
15
- # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV
16
-
17
- # Drafts your next Release notes as Pull Requests are merged into "master"
18
- - uses: release-drafter/release-drafter@v7
19
- # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
20
- # with:
21
- # config-name: my-config.yml
22
- # disable-autolabeler: true
23
- env:
24
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
data/.gitignore DELETED
@@ -1,8 +0,0 @@
1
- tmp/
2
- doc/
3
- .yardoc/
4
- *.gem
5
- .bundle
6
- pkg/*
7
- bin/
8
- .rubocop-http*
data/.rubocop.yml DELETED
@@ -1,16 +0,0 @@
1
- plugins:
2
- - rubocop-performance
3
- - rubocop-rspec
4
-
5
- inherit_from:
6
- - https://raw.githubusercontent.com/webtranslateit/webtranslateit/main/.rubocop_common.yml
7
- - .rubocop_todo.yml
8
-
9
- AllCops:
10
- TargetRubyVersion: 3.2
11
- Exclude:
12
- - lib/generators/**/*
13
-
14
- inherit_mode:
15
- merge:
16
- - Exclude
data/.rubocop_todo.yml DELETED
File without changes
data/Gemfile DELETED
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- gemspec
6
-
7
- gem 'guard'
8
- gem 'guard-rspec'
9
- gem 'guard-rubocop'
10
- gem 'matrix'
11
- gem 'rspec', '~> 3.13.2'
12
- gem 'rubocop'
13
- gem 'rubocop-performance'
14
- gem 'rubocop-rspec'