secretariat 3.2.0 → 3.4.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: 8fb7cefb10c5f0b91d7b501d0501be8d52094de356e29f4bf0bbacb08bff3f25
4
- data.tar.gz: 5ac439e6180ad5c51a6dcc6b835f28ca7b9ee4d63d4e42734ef186502a0dfd63
3
+ metadata.gz: f1a42571c7c3ceb77877659b076fbd8974ee661843dbc4f4d294012163a4c99f
4
+ data.tar.gz: 871cf260400cdf9470c867572a77d6ac5d109886b30b9d99120bbbfa94b5f950
5
5
  SHA512:
6
- metadata.gz: 9df593f5068171fb4d6f7aaaf114981eb08edc51e621e1f0f782b8615ce461f16a41e6c80b3557dbac2271805b5e1d22f56807ef35503cb67a07c7ba89d8a75f
7
- data.tar.gz: 2bc606ddd72a5f442a8c2c042859a566a0da3cff6a9e20b34b3b95f31cd9e0516fba1ab8b01b15ce3bf5e29c27bc562bb88a241504ccd34963d085d85fc02a06
6
+ metadata.gz: e69622419e9e48946ddf0dc4921515a02fa3e8c1f53cd1fd23cd14c89d7f2470718e8d220a4c9594142a768db4c143452efd93607917b230c28e9bb647ec537d
7
+ data.tar.gz: f5c1da6eea2d10742eebb2ae95993026f74ee6968ac630caaa2f38face0c26b03e401d6204752b14e816af8c408d8052843e77071bc6b68f37200bd3607099f6
data/README.md CHANGED
@@ -7,7 +7,7 @@ See tests for examples.
7
7
  1. This is an opinionated library optimised for my very specific usecase
8
8
  2. While I did start to add some validations to make sure you can't input absolute garbage into this, I cannot guarantee factual (as in taxation law) correctness of the resulting XML.
9
9
  1. The library, for ZUGFeRD 2.x, currently only supports the EN16931 variant. This is probably what you want as well. PRs welcome.
10
- 3. This does not contain any code to attach the XML to a PDF file, mainly because I have yet to find a ruby library to do that. For software that does this, take a look at [this python library](https://github.com/akretion/factur-x).
10
+ 3. This does not contain any code to attach the XML to a PDF file, mainly because I have yet to find a ruby library to do that. For software that does this, take a look at [this python library](https://github.com/akretion/factur-x) or [this Java library which also does extended validation](https://mustangproject.org)
11
11
 
12
12
  ## Contributors
13
13
 
@@ -0,0 +1,83 @@
1
+ =begin
2
+ Copyright Jan Krutisch
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+
16
+ =end
17
+
18
+
19
+ require 'mime/types'
20
+
21
+ module Secretariat
22
+ Attachment = Struct.new('Attachment',
23
+ :filename,
24
+ :type_code,
25
+ :base64,
26
+
27
+ keyword_init: true
28
+ ) do
29
+ include Versioner
30
+
31
+ def errors
32
+ @errors
33
+ end
34
+
35
+ def valid?
36
+ @errors = []
37
+
38
+ @errors << "the attribute filename needs to be present" if filename.nil? || filename == ''
39
+ @errors << "the attribute type_code needs to be present" if type_code.nil? || type_code == ''
40
+ @errors << "the attribute base64 needs to be present" if base64.nil? || base64 == ''
41
+
42
+ if type_code.to_i != 916
43
+ @errors << "we only support type_code 916"
44
+ return false
45
+ end
46
+
47
+ if mime_code.nil?
48
+ @errors << "cannot determine content type for filename: #{filename}"
49
+ return false
50
+ end
51
+
52
+ if !ALLOWED_MIME_TYPES.include?(mime_code)
53
+ @errors << "the mime_code '#{mime_code}' is not allowed"
54
+ return false
55
+ end
56
+
57
+ return true
58
+ end
59
+
60
+ def mime_code
61
+ type_for = MIME::Types.type_for(filename).first
62
+ return if type_for.nil?
63
+
64
+ type_for.content_type
65
+ end
66
+
67
+ def to_xml(xml, attachment_index, version: 2, validate: true)
68
+ if validate && !valid?
69
+ pp errors
70
+ raise ValidationError.new("Attachment #{attachment_index} is invalid", errors)
71
+ end
72
+
73
+ xml['ram'].AdditionalReferencedDocument do
74
+ xml['ram'].IssuerAssignedID filename
75
+ xml['ram'].TypeCode type_code
76
+ xml['ram'].Name filename
77
+ xml['ram'].AttachmentBinaryObject(mimeCode: mime_code, filename: filename) do
78
+ xml.text(base64)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -15,6 +15,14 @@ limitations under the License.
15
15
  =end
16
16
 
17
17
  module Secretariat
18
+ ALLOWED_MIME_TYPES = [
19
+ "application/pdf",
20
+ "application/vnd.oasis.opendocument.spreadsheet",
21
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
22
+ "image/jpeg",
23
+ "image/png",
24
+ "text/csv"
25
+ ]
18
26
 
19
27
  TAX_CATEGORY_CODES = {
20
28
  :STANDARDRATE => "S",
@@ -45,15 +53,21 @@ module Secretariat
45
53
  :DEBITADVICE => "31",
46
54
  :CREDITCARD => "48",
47
55
  :DEBIT => "49",
48
- :COMPENSATION => "97",
56
+ :COMPENSATION => "97"
49
57
  }
50
58
 
51
59
  TAX_EXEMPTION_REASONS = {
52
60
  :REVERSECHARGE => 'Reverse Charge',
53
61
  :INTRACOMMUNITY => 'Intra-community transaction',
54
- :EXPORT => 'Export outside the EU'
62
+ :EXPORT => 'Export outside the EU',
63
+ :TAXEXEMPT => 'VAT exempt',
64
+ :UNTAXEDSERVICE => 'Not subject to VAT'
55
65
  }
56
66
 
67
+ # For the background of vertical and horizontal tax calculation see https://hilfe.pacemo.de/de-form/articles/3489851-rundungsfehler-bei-rechnungen
68
+ # The idea of introducing an unknown value is that this could be inferred from the given invoice total and line items by probing both variants and selecting the matching one - or reporting a taxation error if neither matches.
69
+ TAX_CALCULATION_METHODS = %i[HORIZONTAL VERTICAL NONE UNKNOWN].freeze
70
+
57
71
  UNIT_CODES = {
58
72
  :PIECE => "C62",
59
73
  :DAY => "DAY",
@@ -28,6 +28,7 @@ module Secretariat
28
28
  :line_items,
29
29
  :currency_code,
30
30
  :payment_type,
31
+ :payment_reference,
31
32
  :payment_text,
32
33
  :payment_terms_text,
33
34
  :payment_due_date,
@@ -40,7 +41,8 @@ module Secretariat
40
41
  :grand_total_amount,
41
42
  :due_amount,
42
43
  :paid_amount,
43
-
44
+ :tax_calculation_method,
45
+ :attachments,
44
46
  keyword_init: true
45
47
  ) do
46
48
 
@@ -54,21 +56,43 @@ module Secretariat
54
56
  tax_reason || TAX_EXEMPTION_REASONS[tax_category]
55
57
  end
56
58
 
57
- def tax_category_code(version: 2)
59
+ def tax_category_code(tax, version: 2)
58
60
  if version == 1
59
- return TAX_CATEGORY_CODES_1[tax_category] || 'S'
61
+ return TAX_CATEGORY_CODES_1[tax.tax_category || tax_category] || 'S'
60
62
  end
61
- TAX_CATEGORY_CODES[tax_category] || 'S'
63
+ TAX_CATEGORY_CODES[tax.tax_category || tax_category] || 'S'
62
64
  end
63
65
 
64
66
  def taxes
65
67
  taxes = {}
68
+ # Shortcut for cases where invoices only have one tax and the calculation is off by a cent because of rounding errors
69
+ # (This can happen if the VAT and the net amount is calculated backwards from a round gross amount)
70
+ if tax_calculation_method == :NONE
71
+ tax = Tax.new(tax_percent: BigDecimal(tax_percent || BigDecimal(0)), tax_category: tax_category)
72
+ tax.base_amount = BigDecimal(basis_amount)
73
+ tax.tax_amount = BigDecimal(tax_amount || 0)
74
+ return [tax]
75
+ end
76
+
66
77
  line_items.each do |line_item|
67
- taxes[line_item.tax_percent] = Tax.new(tax_percent: BigDecimal(line_item.tax_percent)) if taxes[line_item.tax_percent].nil?
68
- taxes[line_item.tax_percent].tax_amount += BigDecimal(line_item.tax_amount)
69
- taxes[line_item.tax_percent].base_amount += BigDecimal(line_item.net_amount) * line_item.quantity
78
+ if line_item.tax_percent.nil?
79
+ taxes['0'] = Tax.new(tax_percent: BigDecimal(0), tax_category: line_item.tax_category, tax_amount: BigDecimal(0)) if taxes['0'].nil?
80
+ taxes['0'].base_amount += BigDecimal(line_item.net_amount) * line_item.quantity
81
+ else
82
+ taxes[line_item.tax_percent] = Tax.new(tax_percent: BigDecimal(line_item.tax_percent), tax_category: line_item.tax_category) if taxes[line_item.tax_percent].nil?
83
+ taxes[line_item.tax_percent].tax_amount += BigDecimal(line_item.tax_amount)
84
+ taxes[line_item.tax_percent].base_amount += BigDecimal(line_item.net_amount) * line_item.quantity
85
+ end
86
+ end
87
+
88
+ if tax_calculation_method == :VERTICAL
89
+ taxes.values.map do |tax|
90
+ tax.tax_amount = (tax.base_amount * tax.tax_percent / 100).round(2)
91
+ tax
92
+ end
93
+ else
94
+ taxes.values
70
95
  end
71
- taxes.values
72
96
  end
73
97
 
74
98
  def payment_code
@@ -89,12 +113,14 @@ module Secretariat
89
113
  @errors << "Base amount and summed tax base amount deviate: #{basis} / #{summed_tax_base_amount}"
90
114
  return false
91
115
  end
92
- taxes.each do |tax|
93
- calc_tax = tax.base_amount * BigDecimal(tax.tax_percent) / BigDecimal(100)
94
- calc_tax = calc_tax.round(2, :down)
95
- if tax.tax_amount != calc_tax
96
- @errors << "Tax amount and calculated tax amount deviate for rate #{tax.tax_percent}: #{tax.tax_amount} / #{calc_tax}"
97
- return false
116
+ if tax_calculation_method != :NONE
117
+ taxes.each do |tax|
118
+ calc_tax = tax.base_amount * BigDecimal(tax.tax_percent) / BigDecimal(100)
119
+ calc_tax = calc_tax.round(2)
120
+ if tax.tax_amount != calc_tax
121
+ @errors << "Tax amount and calculated tax amount deviate for rate #{tax.tax_percent}: #{tax.tax_amount} / #{calc_tax}"
122
+ return false
123
+ end
98
124
  end
99
125
  end
100
126
  grand_total = BigDecimal(grand_total_amount)
@@ -104,7 +130,7 @@ module Secretariat
104
130
  return false
105
131
  end
106
132
  line_item_sum = line_items.inject(BigDecimal(0)) do |m, item|
107
- m + BigDecimal(item.charge_amount)
133
+ m + BigDecimal(item.quantity.negative? ? -item.charge_amount : item.charge_amount)
108
134
  end
109
135
  if line_item_sum != basis
110
136
  @errors << "Line items do not add up to basis amount #{line_item_sum} / #{basis}"
@@ -169,7 +195,7 @@ module Secretariat
169
195
  xml.text(issue_date.strftime("%Y%m%d"))
170
196
  end
171
197
  end
172
-
198
+
173
199
  end
174
200
  transaction = by_version(version, 'SpecifiedSupplyChainTradeTransaction', 'SupplyChainTradeTransaction')
175
201
  xml['rsm'].send(transaction) do
@@ -192,6 +218,13 @@ module Secretariat
192
218
  xml['ram'].BuyerTradeParty do
193
219
  buyer.to_xml(xml, version: version)
194
220
  end
221
+ if version == 2
222
+ if Array(attachments).size > 0
223
+ attachments.each_with_index do |attachment, index|
224
+ attachment.to_xml(xml, index, version: version, validate: validate)
225
+ end
226
+ end
227
+ end
195
228
  end
196
229
 
197
230
  delivery = by_version(version, 'ApplicableSupplyChainTradeDelivery', 'ApplicableHeaderTradeDelivery')
@@ -212,6 +245,9 @@ module Secretariat
212
245
  end
213
246
  trade_settlement = by_version(version, 'ApplicableSupplyChainTradeSettlement', 'ApplicableHeaderTradeSettlement')
214
247
  xml['ram'].send(trade_settlement) do
248
+ if payment_reference && payment_reference != ''
249
+ xml['ram'].PaymentReference payment_reference
250
+ end
215
251
  xml['ram'].InvoiceCurrencyCode currency_code
216
252
  xml['ram'].SpecifiedTradeSettlementPaymentMeans do
217
253
  xml['ram'].TypeCode payment_code
@@ -230,10 +266,11 @@ module Secretariat
230
266
  xml['ram'].ExemptionReason tax_reason_text
231
267
  end
232
268
  Helpers.currency_element(xml, 'ram', 'BasisAmount', tax.base_amount, currency_code, add_currency: version == 1)
233
- xml['ram'].CategoryCode tax_category_code(version: version)
234
-
235
- percent = by_version(version, 'ApplicablePercent', 'RateApplicablePercent')
236
- xml['ram'].send(percent, Helpers.format(tax.tax_percent))
269
+ xml['ram'].CategoryCode tax_category_code(tax, version: version)
270
+ # unless tax.untaxable?
271
+ percent = by_version(version, 'ApplicablePercent', 'RateApplicablePercent')
272
+ xml['ram'].send(percent, Helpers.format(tax.tax_percent))
273
+ # end
237
274
  end
238
275
  end
239
276
  if version == 2 && service_period_start && service_period_end
@@ -271,7 +308,7 @@ module Secretariat
271
308
  end
272
309
  if version == 1
273
310
  line_items.each_with_index do |item, i|
274
- item.to_xml(xml, i + 1, version: version) # one indexed
311
+ item.to_xml(xml, i + 1, version: version, validate: validate) # one indexed
275
312
  end
276
313
  end
277
314
  end
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  =end
16
16
 
17
-
18
-
19
17
  require 'bigdecimal'
20
18
  module Secretariat
21
19
 
@@ -49,7 +47,7 @@ module Secretariat
49
47
  gross_price = BigDecimal(gross_amount)
50
48
  charge_price = BigDecimal(charge_amount)
51
49
  tax = BigDecimal(tax_amount)
52
- unit_price = net_price * BigDecimal(quantity)
50
+ unit_price = net_price * BigDecimal(quantity.abs)
53
51
 
54
52
  if charge_price != unit_price
55
53
  @errors << "charge price and gross price times quantity deviate: #{charge_price} / #{unit_price}"
@@ -63,11 +61,15 @@ module Secretariat
63
61
  return false
64
62
  end
65
63
  end
66
-
67
- calculated_tax = charge_price * BigDecimal(tax_percent) / BigDecimal(100)
68
- if calculated_tax != tax
69
- @errors << "Tax and calculated tax deviate: #{tax} / #{calculated_tax}"
70
- return false
64
+ if tax_category != :UNTAXEDSERVICE
65
+ self.tax_percent ||= BigDecimal(0)
66
+ calculated_tax = charge_price * BigDecimal(tax_percent) / BigDecimal(100)
67
+ calculated_tax = calculated_tax.round(2)
68
+ calculated_tax = -calculated_tax if quantity.negative?
69
+ if calculated_tax != tax
70
+ @errors << "Tax and calculated tax deviate: #{tax} / #{calculated_tax}"
71
+ return false
72
+ end
71
73
  end
72
74
  return true
73
75
  end
@@ -83,9 +85,31 @@ module Secretariat
83
85
  TAX_CATEGORY_CODES[tax_category] || 'S'
84
86
  end
85
87
 
88
+ def untaxable?
89
+ tax_category == :UNTAXEDSERVICE
90
+ end
91
+
86
92
  def to_xml(xml, line_item_index, version: 2, validate: true)
93
+ net_price = net_amount && BigDecimal(net_amount)
94
+ gross_price = gross_amount && BigDecimal(gross_amount)
95
+ charge_price = charge_amount && BigDecimal(charge_amount)
96
+
97
+ self.tax_percent ||= BigDecimal(0)
98
+
99
+ if net_price&.zero?
100
+ self.tax_percent = 0
101
+ end
102
+
103
+ if net_price&.negative?
104
+ # Zugferd doesn't allow negative amounts at the item level.
105
+ # Instead, a negative quantity is used.
106
+ self.quantity = -quantity
107
+ self.gross_amount = gross_price&.abs
108
+ self.net_amount = net_price&.abs
109
+ self.charge_amount = charge_price&.abs
110
+ end
111
+
87
112
  if validate && !valid?
88
- pp errors
89
113
  raise ValidationError.new("LineItem #{line_item_index} is invalid", errors)
90
114
  end
91
115
 
@@ -152,14 +176,14 @@ module Secretariat
152
176
  xml['ram'].ApplicableTradeTax do
153
177
  xml['ram'].TypeCode 'VAT'
154
178
  xml['ram'].CategoryCode tax_category_code(version: version)
155
-
156
- percent = by_version(version, 'ApplicablePercent', 'RateApplicablePercent')
157
- xml['ram'].send(percent,Helpers.format(tax_percent))
158
-
179
+ unless untaxable?
180
+ percent = by_version(version, 'ApplicablePercent', 'RateApplicablePercent')
181
+ xml['ram'].send(percent,Helpers.format(tax_percent))
182
+ end
159
183
  end
160
184
  monetary_summation = by_version(version, 'SpecifiedTradeSettlementMonetarySummation', 'SpecifiedTradeSettlementLineMonetarySummation')
161
185
  xml['ram'].send(monetary_summation) do
162
- Helpers.currency_element(xml, 'ram', 'LineTotalAmount', charge_amount, currency_code, add_currency: version == 1)
186
+ Helpers.currency_element(xml, 'ram', 'LineTotalAmount', (quantity.negative? ? -charge_amount : charge_amount), currency_code, add_currency: version == 1)
163
187
  end
164
188
  end
165
189
 
@@ -20,6 +20,7 @@ module Secretariat
20
20
  Tax = Struct.new('Tax',
21
21
  :tax_percent,
22
22
  :tax_amount,
23
+ :tax_category,
23
24
  :base_amount,
24
25
  keyword_init: true
25
26
  ) do
@@ -29,5 +30,10 @@ module Secretariat
29
30
  self.tax_amount = 0
30
31
  self.base_amount = 0
31
32
  end
33
+
34
+ def untaxable?
35
+ tax_category == :UNTAXEDSERVICE
36
+ end
37
+
32
38
  end
33
39
  end
@@ -16,10 +16,15 @@ limitations under the License.
16
16
 
17
17
  module Secretariat
18
18
  TradeParty = Struct.new('TradeParty',
19
- :name, :street1, :street2, :city, :postal_code, :country_id, :vat_id,
19
+ :name, :street1, :street2, :city, :postal_code, :country_id, :vat_id, :global_id, :global_id_scheme_id, :tax_id,
20
20
  keyword_init: true,
21
21
  ) do
22
22
  def to_xml(xml, exclude_tax: false, version: 2)
23
+ if global_id && global_id != '' && global_id_scheme_id && global_id_scheme_id != ''
24
+ xml['ram'].GlobalID(schemeID: global_id_scheme_id) do
25
+ xml.text(global_id)
26
+ end
27
+ end
23
28
  xml['ram'].Name name
24
29
  xml['ram'].PostalTradeAddress do
25
30
  xml['ram'].PostcodeCode postal_code
@@ -36,9 +41,13 @@ module Secretariat
36
41
  xml.text(vat_id)
37
42
  end
38
43
  end
44
+ elsif tax_id && tax_id != ''
45
+ xml['ram'].SpecifiedTaxRegistration do
46
+ xml['ram'].ID(schemeID: 'FC') do
47
+ xml.text(tax_id)
48
+ end
49
+ end
39
50
  end
40
51
  end
41
52
  end
42
-
43
-
44
53
  end
@@ -15,5 +15,5 @@ limitations under the License.
15
15
  =end
16
16
 
17
17
  module Secretariat
18
- VERSION = '3.2.0'
18
+ VERSION = '3.4.0'
19
19
  end
data/lib/secretariat.rb CHANGED
@@ -24,3 +24,4 @@ require_relative 'secretariat/trade_party'
24
24
  require_relative 'secretariat/line_item'
25
25
  require_relative 'secretariat/validator'
26
26
  require_relative 'secretariat/tax'
27
+ require_relative 'secretariat/attachment'
metadata CHANGED
@@ -1,11 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: secretariat
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Krutisch
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
10
  date: 2024-11-06 00:00:00.000000000 Z
@@ -38,6 +37,20 @@ dependencies:
38
37
  - - "~>"
39
38
  - !ruby/object:Gem::Version
40
39
  version: '3.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: mime-types
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 3.6.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 3.6.0
41
54
  - !ruby/object:Gem::Dependency
42
55
  name: minitest
43
56
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +79,20 @@ dependencies:
66
79
  - - "~>"
67
80
  - !ruby/object:Gem::Version
68
81
  version: '13.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: base64
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 0.2.0
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 0.2.0
69
96
  description: a tool to help generate and validate ZUGFeRD invoice xml files
70
97
  email: jan@krutisch.de
71
98
  executables: []
@@ -75,6 +102,7 @@ files:
75
102
  - README.md
76
103
  - bin/schxslt-cli.jar
77
104
  - lib/secretariat.rb
105
+ - lib/secretariat/attachment.rb
78
106
  - lib/secretariat/constants.rb
79
107
  - lib/secretariat/helpers.rb
80
108
  - lib/secretariat/invoice.rb
@@ -100,7 +128,6 @@ homepage: https://github.com/halfbyte/ruby-secretariat
100
128
  licenses:
101
129
  - Apache-2.0
102
130
  metadata: {}
103
- post_install_message:
104
131
  rdoc_options: []
105
132
  require_paths:
106
133
  - lib
@@ -116,8 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
143
  version: '0'
117
144
  requirements:
118
145
  - To run the validator, Java must be installed
119
- rubygems_version: 3.5.16
120
- signing_key:
146
+ rubygems_version: 3.6.2
121
147
  specification_version: 4
122
148
  summary: A ZUGFeRD xml generator
123
149
  test_files: []