secretariat 3.2.0 → 3.3.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/lib/secretariat/attachment.rb +83 -0
- data/lib/secretariat/constants.rb +16 -2
- data/lib/secretariat/invoice.rb +30 -11
- data/lib/secretariat/line_item.rb +21 -3
- data/lib/secretariat/tax.rb +1 -0
- data/lib/secretariat/version.rb +1 -1
- data/lib/secretariat.rb +1 -0
- metadata +31 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4aede1e2eafc1189446e824cb5977b9c3844f71fa4c505214aa4d9b9f79e1101
|
4
|
+
data.tar.gz: 533c4f81ae8b50977df60f6c2bc31565cbbfd93066c52d8cf1961215971319fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a3358bd3eec524b4e76fc8f32945dca7cb665d683a256e69350fa31c340db2a75f77f3f64883978da387a52edd9beb4810ffe362d67341c81abe28579a979e1f
|
7
|
+
data.tar.gz: 5f347f7253bc8f814a0e1524c968506679d7bb1108c9a4cf03d9716830829ae393e5cde2df2b9d83210a237b075354863ef06af10df2e3d984a699a9c1d758c3
|
@@ -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 UNKNOWN].freeze
|
70
|
+
|
57
71
|
UNIT_CODES = {
|
58
72
|
:PIECE => "C62",
|
59
73
|
:DAY => "DAY",
|
data/lib/secretariat/invoice.rb
CHANGED
@@ -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,28 @@ 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 = {}
|
66
68
|
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?
|
69
|
+
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?
|
68
70
|
taxes[line_item.tax_percent].tax_amount += BigDecimal(line_item.tax_amount)
|
69
71
|
taxes[line_item.tax_percent].base_amount += BigDecimal(line_item.net_amount) * line_item.quantity
|
70
72
|
end
|
71
|
-
|
73
|
+
if tax_calculation_method == :VERTICAL
|
74
|
+
taxes.values.map do |tax|
|
75
|
+
tax.tax_amount = (tax.base_amount * tax.tax_percent / 100).round(2)
|
76
|
+
tax
|
77
|
+
end
|
78
|
+
else
|
79
|
+
taxes.values
|
80
|
+
end
|
72
81
|
end
|
73
82
|
|
74
83
|
def payment_code
|
@@ -91,7 +100,7 @@ module Secretariat
|
|
91
100
|
end
|
92
101
|
taxes.each do |tax|
|
93
102
|
calc_tax = tax.base_amount * BigDecimal(tax.tax_percent) / BigDecimal(100)
|
94
|
-
calc_tax = calc_tax.round(2
|
103
|
+
calc_tax = calc_tax.round(2)
|
95
104
|
if tax.tax_amount != calc_tax
|
96
105
|
@errors << "Tax amount and calculated tax amount deviate for rate #{tax.tax_percent}: #{tax.tax_amount} / #{calc_tax}"
|
97
106
|
return false
|
@@ -104,7 +113,7 @@ module Secretariat
|
|
104
113
|
return false
|
105
114
|
end
|
106
115
|
line_item_sum = line_items.inject(BigDecimal(0)) do |m, item|
|
107
|
-
m + BigDecimal(item.charge_amount)
|
116
|
+
m + BigDecimal(item.quantity.negative? ? -item.charge_amount : item.charge_amount)
|
108
117
|
end
|
109
118
|
if line_item_sum != basis
|
110
119
|
@errors << "Line items do not add up to basis amount #{line_item_sum} / #{basis}"
|
@@ -169,7 +178,7 @@ module Secretariat
|
|
169
178
|
xml.text(issue_date.strftime("%Y%m%d"))
|
170
179
|
end
|
171
180
|
end
|
172
|
-
|
181
|
+
|
173
182
|
end
|
174
183
|
transaction = by_version(version, 'SpecifiedSupplyChainTradeTransaction', 'SupplyChainTradeTransaction')
|
175
184
|
xml['rsm'].send(transaction) do
|
@@ -192,6 +201,13 @@ module Secretariat
|
|
192
201
|
xml['ram'].BuyerTradeParty do
|
193
202
|
buyer.to_xml(xml, version: version)
|
194
203
|
end
|
204
|
+
if version == 2
|
205
|
+
if Array(attachments).size > 0
|
206
|
+
attachments.each_with_index do |attachment, index|
|
207
|
+
attachment.to_xml(xml, index, version: version, validate: validate)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
195
211
|
end
|
196
212
|
|
197
213
|
delivery = by_version(version, 'ApplicableSupplyChainTradeDelivery', 'ApplicableHeaderTradeDelivery')
|
@@ -212,6 +228,9 @@ module Secretariat
|
|
212
228
|
end
|
213
229
|
trade_settlement = by_version(version, 'ApplicableSupplyChainTradeSettlement', 'ApplicableHeaderTradeSettlement')
|
214
230
|
xml['ram'].send(trade_settlement) do
|
231
|
+
if payment_reference && payment_reference != ''
|
232
|
+
xml['ram'].PaymentReference payment_reference
|
233
|
+
end
|
215
234
|
xml['ram'].InvoiceCurrencyCode currency_code
|
216
235
|
xml['ram'].SpecifiedTradeSettlementPaymentMeans do
|
217
236
|
xml['ram'].TypeCode payment_code
|
@@ -230,7 +249,7 @@ module Secretariat
|
|
230
249
|
xml['ram'].ExemptionReason tax_reason_text
|
231
250
|
end
|
232
251
|
Helpers.currency_element(xml, 'ram', 'BasisAmount', tax.base_amount, currency_code, add_currency: version == 1)
|
233
|
-
xml['ram'].CategoryCode tax_category_code(version: version)
|
252
|
+
xml['ram'].CategoryCode tax_category_code(tax, version: version)
|
234
253
|
|
235
254
|
percent = by_version(version, 'ApplicablePercent', 'RateApplicablePercent')
|
236
255
|
xml['ram'].send(percent, Helpers.format(tax.tax_percent))
|
@@ -271,7 +290,7 @@ module Secretariat
|
|
271
290
|
end
|
272
291
|
if version == 1
|
273
292
|
line_items.each_with_index do |item, i|
|
274
|
-
item.to_xml(xml, i + 1, version: version) # one indexed
|
293
|
+
item.to_xml(xml, i + 1, version: version, validate: validate) # one indexed
|
275
294
|
end
|
276
295
|
end
|
277
296
|
end
|
@@ -49,7 +49,7 @@ module Secretariat
|
|
49
49
|
gross_price = BigDecimal(gross_amount)
|
50
50
|
charge_price = BigDecimal(charge_amount)
|
51
51
|
tax = BigDecimal(tax_amount)
|
52
|
-
unit_price = net_price * BigDecimal(quantity)
|
52
|
+
unit_price = net_price * BigDecimal(quantity.abs)
|
53
53
|
|
54
54
|
if charge_price != unit_price
|
55
55
|
@errors << "charge price and gross price times quantity deviate: #{charge_price} / #{unit_price}"
|
@@ -65,6 +65,8 @@ module Secretariat
|
|
65
65
|
end
|
66
66
|
|
67
67
|
calculated_tax = charge_price * BigDecimal(tax_percent) / BigDecimal(100)
|
68
|
+
calculated_tax = calculated_tax.round(2)
|
69
|
+
calculated_tax = -calculated_tax if quantity.negative?
|
68
70
|
if calculated_tax != tax
|
69
71
|
@errors << "Tax and calculated tax deviate: #{tax} / #{calculated_tax}"
|
70
72
|
return false
|
@@ -84,8 +86,24 @@ module Secretariat
|
|
84
86
|
end
|
85
87
|
|
86
88
|
def to_xml(xml, line_item_index, version: 2, validate: true)
|
89
|
+
net_price = net_amount && BigDecimal(net_amount)
|
90
|
+
gross_price = gross_amount && BigDecimal(gross_amount)
|
91
|
+
charge_price = charge_amount && BigDecimal(charge_amount)
|
92
|
+
|
93
|
+
if net_price&.zero?
|
94
|
+
self.tax_percent = 0
|
95
|
+
end
|
96
|
+
|
97
|
+
if net_price&.negative?
|
98
|
+
# Zugferd doesn't allow negative amounts at the item level.
|
99
|
+
# Instead, a negative quantity is used.
|
100
|
+
self.quantity = -quantity
|
101
|
+
self.gross_amount = gross_price&.abs
|
102
|
+
self.net_amount = net_price&.abs
|
103
|
+
self.charge_amount = charge_price&.abs
|
104
|
+
end
|
105
|
+
|
87
106
|
if validate && !valid?
|
88
|
-
pp errors
|
89
107
|
raise ValidationError.new("LineItem #{line_item_index} is invalid", errors)
|
90
108
|
end
|
91
109
|
|
@@ -159,7 +177,7 @@ module Secretariat
|
|
159
177
|
end
|
160
178
|
monetary_summation = by_version(version, 'SpecifiedTradeSettlementMonetarySummation', 'SpecifiedTradeSettlementLineMonetarySummation')
|
161
179
|
xml['ram'].send(monetary_summation) do
|
162
|
-
Helpers.currency_element(xml, 'ram', 'LineTotalAmount', charge_amount, currency_code, add_currency: version == 1)
|
180
|
+
Helpers.currency_element(xml, 'ram', 'LineTotalAmount', (quantity.negative? ? -charge_amount : charge_amount), currency_code, add_currency: version == 1)
|
163
181
|
end
|
164
182
|
end
|
165
183
|
|
data/lib/secretariat/tax.rb
CHANGED
data/lib/secretariat/version.rb
CHANGED
data/lib/secretariat.rb
CHANGED
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.
|
4
|
+
version: 3.3.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.
|
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: []
|