secretariat 1.0.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 +7 -0
- data/README.md +22 -0
- data/lib/secretariat.rb +23 -0
- data/lib/secretariat/constants.rb +56 -0
- data/lib/secretariat/invoice.rb +168 -0
- data/lib/secretariat/line_item.rb +115 -0
- data/lib/secretariat/trade_party.rb +44 -0
- data/lib/secretariat/validation_error.rb +25 -0
- data/lib/secretariat/validator.rb +50 -0
- data/lib/secretariat/version.rb +19 -0
- data/schemas/zf_en16931.sch +9581 -0
- data/schemas/zf_en16931.xsd +20 -0
- data/schemas/zf_en16931_codedb.xml +4971 -0
- data/schemas/zf_en16931_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd +1649 -0
- data/schemas/zf_en16931_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd +318 -0
- data/schemas/zf_en16931_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd +85 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ea7afc077807afc9c37b543936f44c92dd47efde7b88afd9cd0189962a725582
|
4
|
+
data.tar.gz: bf531f1c30ded4f468a0b7b4c8657dece3d9abb3f758b9d1fad167f0ddccdc4a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b00aa871cdbe0610476e5689756db15ec8f5d584c658548513d2e22c895e1f393e74b42f33b92c7ebb516bf08ebea2b64d240ab58235ef89b0a47a3fa4a9503d
|
7
|
+
data.tar.gz: d34cccd54819f93c2fdf6850a2ffee4306d42034ded2748961814c7159258aab5c06bda3251497ef85e0e417958ab4d044945c544c3e757e244c2df1d7d1a018
|
data/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# A ZUGFeRD xml generator and validator
|
2
|
+
|
3
|
+
More info coming soon. See tests for examples.
|
4
|
+
|
5
|
+
# Some words of caution
|
6
|
+
|
7
|
+
1. This is an opinionated library optimised for my very specific usecase
|
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
|
+
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.
|
10
|
+
|
11
|
+
# LICENSE
|
12
|
+
|
13
|
+
See [LICENSE](LICENSE).
|
14
|
+
|
15
|
+
Additionally, this project contains material, such as the schema files, which,
|
16
|
+
according to the ZUGFeRD documentation, are also licensed under the Apache
|
17
|
+
License.
|
18
|
+
|
19
|
+
Additionally, this project uses nokogiri and schematron-nokogiri, both
|
20
|
+
licensed unter the MIT license.
|
21
|
+
|
22
|
+
|
data/lib/secretariat.rb
ADDED
@@ -0,0 +1,23 @@
|
|
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
|
+
=end
|
16
|
+
|
17
|
+
require_relative 'secretariat/version'
|
18
|
+
require_relative 'secretariat/constants'
|
19
|
+
require_relative 'secretariat/validation_error'
|
20
|
+
require_relative 'secretariat/invoice'
|
21
|
+
require_relative 'secretariat/trade_party'
|
22
|
+
require_relative 'secretariat/line_item'
|
23
|
+
require_relative 'secretariat/validator'
|
@@ -0,0 +1,56 @@
|
|
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
|
+
=end
|
16
|
+
|
17
|
+
module Secretariat
|
18
|
+
|
19
|
+
TAX_CATEGORY_CODES = {
|
20
|
+
:STANDARDRATE => "S",
|
21
|
+
:REVERSECHARGE => "AE",
|
22
|
+
:TAXEXEMPT => "E",
|
23
|
+
:ZEROTAXPRODUCTS => "Z",
|
24
|
+
:UNTAXEDSERVICE => "O",
|
25
|
+
:INTRACOMMUNITY => "K"
|
26
|
+
}
|
27
|
+
|
28
|
+
TAX_EXEMPTION_REASONS = {
|
29
|
+
:REVERSECHARGE => 'Reverse Charge',
|
30
|
+
:INTRACOMMUNITY => ''
|
31
|
+
}
|
32
|
+
|
33
|
+
UNIT_CODES = {
|
34
|
+
:PIECE => "C62",
|
35
|
+
:DAY => "DAY",
|
36
|
+
:HECTARE => "HAR",
|
37
|
+
:HOUR => "HUR",
|
38
|
+
:KILOGRAM => "KGM",
|
39
|
+
:KILOMETER => "KTM",
|
40
|
+
:KILOWATTHOUR => "KWH",
|
41
|
+
:FIXEDRATE => "LS",
|
42
|
+
:LITRE => "LTR",
|
43
|
+
:MINUTE => "MIN",
|
44
|
+
:SQUAREMILLIMETER => "MMK",
|
45
|
+
:MILLIMETER => "MMT",
|
46
|
+
:SQUAREMETER => "MTK",
|
47
|
+
:CUBICMETER => "MTQ",
|
48
|
+
:METER => "MTR",
|
49
|
+
:PRODUCTCOUNT => "NAR",
|
50
|
+
:PRODUCTPAIR => "NPR",
|
51
|
+
:PERCENT => "P1",
|
52
|
+
:SET => "SET",
|
53
|
+
:TON => "TNE",
|
54
|
+
:WEEK => "WEE"
|
55
|
+
}
|
56
|
+
end
|
@@ -0,0 +1,168 @@
|
|
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
|
+
=end
|
16
|
+
|
17
|
+
require 'bigdecimal'
|
18
|
+
|
19
|
+
module Secretariat
|
20
|
+
Invoice = Struct.new("Invoice",
|
21
|
+
:id,
|
22
|
+
:issue_date,
|
23
|
+
:seller,
|
24
|
+
:buyer,
|
25
|
+
:line_items,
|
26
|
+
:currency_code,
|
27
|
+
:payment_code,
|
28
|
+
:payment_text,
|
29
|
+
:tax_category,
|
30
|
+
:tax_percent,
|
31
|
+
:tax_amount,
|
32
|
+
:tax_reason,
|
33
|
+
:basis_amount,
|
34
|
+
:grand_total_amount,
|
35
|
+
:due_amount,
|
36
|
+
:paid_amount,
|
37
|
+
|
38
|
+
keyword_init: true
|
39
|
+
) do
|
40
|
+
|
41
|
+
def errors
|
42
|
+
@errors
|
43
|
+
end
|
44
|
+
|
45
|
+
def tax_reason_text
|
46
|
+
tax_reason || TAX_EXEMPTION_REASONS[tax_category]
|
47
|
+
end
|
48
|
+
|
49
|
+
def tax_category_code
|
50
|
+
TAX_CATEGORY_CODES[tax_category] || 'S'
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid?
|
54
|
+
tax = BigDecimal(tax_amount)
|
55
|
+
basis = BigDecimal(basis_amount)
|
56
|
+
calc_tax = basis * BigDecimal(tax_percent) / BigDecimal(100)
|
57
|
+
if tax != calc_tax
|
58
|
+
@errors << "Tax amount and calculated tax amount deviate"
|
59
|
+
return false
|
60
|
+
end
|
61
|
+
grand_total = BigDecimal(grand_total_amount)
|
62
|
+
calc_grand_total = basis + tax
|
63
|
+
if grand_total != calc_grand_total
|
64
|
+
@errors << "Grand total amount and calculated grand total amount deviate"
|
65
|
+
return false
|
66
|
+
end
|
67
|
+
line_item_sum = line_items.inject(BigDecimal(0)) do |m, item|
|
68
|
+
m + BigDecimal(item.charge_amount)
|
69
|
+
end
|
70
|
+
if line_item_sum != basis
|
71
|
+
@errors << "Line items do not add up to basis amount"
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
return true
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
def to_xml()
|
79
|
+
|
80
|
+
unless valid?
|
81
|
+
raise ValidationError.new("Invoice is invalid", errors)
|
82
|
+
end
|
83
|
+
|
84
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
85
|
+
xml.CrossIndustryInvoice({
|
86
|
+
'xmlns:qdt' => 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100',
|
87
|
+
'xmlns:ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
|
88
|
+
'xmlns:udt' => 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100',
|
89
|
+
'xmlns' => 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
|
90
|
+
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
|
91
|
+
}) do
|
92
|
+
xml.ExchangedDocumentContext do
|
93
|
+
xml['ram'].GuidelineSpecifiedDocumentContextParameter do
|
94
|
+
xml['ram'].ID 'urn:cen.eu:en16931:2017'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
xml.ExchangedDocument do
|
99
|
+
xml['ram'].ID id
|
100
|
+
xml['ram'].TypeCode '380' # TODO: make configurable
|
101
|
+
xml['ram'].IssueDateTime do
|
102
|
+
xml['udt'].DateTimeString(format: '102') do
|
103
|
+
xml.text(issue_date.strftime("%Y%m%d"))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
xml.SupplyChainTradeTransaction do
|
108
|
+
line_items.each_with_index do |item, i|
|
109
|
+
item.to_xml(xml, i + 1) # one indexed
|
110
|
+
end
|
111
|
+
xml['ram'].ApplicableHeaderTradeAgreement do
|
112
|
+
xml['ram'].SellerTradeParty do
|
113
|
+
seller.to_xml(xml)
|
114
|
+
end
|
115
|
+
xml['ram'].BuyerTradeParty do
|
116
|
+
buyer.to_xml(xml)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
xml['ram'].ApplicableHeaderTradeDelivery do
|
120
|
+
xml['ram'].ShipToTradeParty do
|
121
|
+
buyer.to_xml(xml, exclude_tax: true)
|
122
|
+
end
|
123
|
+
xml['ram'].ActualDeliverySupplyChainEvent do
|
124
|
+
xml['ram'].OccurrenceDateTime do
|
125
|
+
xml['udt'].DateTimeString(format: '102') do
|
126
|
+
xml.text(issue_date.strftime("%Y%m%d"))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
xml['ram'].ApplicableHeaderTradeSettlement do
|
132
|
+
xml['ram'].InvoiceCurrencyCode currency_code
|
133
|
+
xml['ram'].SpecifiedTradeSettlementPaymentMeans do
|
134
|
+
xml['ram'].TypeCode payment_code
|
135
|
+
xml['ram'].Information payment_text
|
136
|
+
end
|
137
|
+
xml['ram'].ApplicableTradeTax do
|
138
|
+
xml['ram'].CalculatedAmount tax_amount
|
139
|
+
xml['ram'].TypeCode 'VAT'
|
140
|
+
if tax_reason_text && tax_reason_text != ''
|
141
|
+
xml['ram'].ExemptionReason tax_reason
|
142
|
+
end
|
143
|
+
xml['ram'].BasisAmount basis_amount
|
144
|
+
xml['ram'].CategoryCode tax_category_code
|
145
|
+
xml['ram'].RateApplicablePercent tax_percent
|
146
|
+
end
|
147
|
+
xml['ram'].SpecifiedTradePaymentTerms do
|
148
|
+
xml['ram'].Description "Paid"
|
149
|
+
end
|
150
|
+
xml['ram'].SpecifiedTradeSettlementHeaderMonetarySummation do
|
151
|
+
xml['ram'].LineTotalAmount basis_amount
|
152
|
+
xml['ram'].TaxBasisTotalAmount basis_amount
|
153
|
+
xml['ram'].TaxTotalAmount(currencyID: currency_code) do
|
154
|
+
xml.text(tax_amount)
|
155
|
+
end
|
156
|
+
xml['ram'].GrandTotalAmount grand_total_amount
|
157
|
+
xml['ram'].TotalPrepaidAmount paid_amount
|
158
|
+
xml['ram'].DuePayableAmount due_amount
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
builder.to_xml
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
@@ -0,0 +1,115 @@
|
|
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
|
+
|
20
|
+
require 'bigdecimal'
|
21
|
+
module Secretariat
|
22
|
+
|
23
|
+
|
24
|
+
LineItem = Struct.new('LineItem',
|
25
|
+
:name,
|
26
|
+
:quantity,
|
27
|
+
:unit,
|
28
|
+
:unit_amount,
|
29
|
+
:charge_amount,
|
30
|
+
:tax_category,
|
31
|
+
:tax_percent,
|
32
|
+
:tax_amount,
|
33
|
+
:origin_country_code,
|
34
|
+
keyword_init: true
|
35
|
+
) do
|
36
|
+
|
37
|
+
def errors
|
38
|
+
@errors
|
39
|
+
end
|
40
|
+
|
41
|
+
def valid?
|
42
|
+
unit_price = BigDecimal(unit_amount)
|
43
|
+
charge_price = BigDecimal(charge_amount)
|
44
|
+
tax = BigDecimal(tax_amount)
|
45
|
+
|
46
|
+
if charge_price != unit_price * BigDecimal(quantity)
|
47
|
+
@errors << "charge price and unit price times quantity deviate"
|
48
|
+
return false
|
49
|
+
end
|
50
|
+
|
51
|
+
calculated_tax = charge_price * BigDecimal(tax_percent) / BigDecimal(100)
|
52
|
+
if calculated_tax != tax
|
53
|
+
@errors << "Tax and calculated tax deviate"
|
54
|
+
return false
|
55
|
+
end
|
56
|
+
return true
|
57
|
+
end
|
58
|
+
|
59
|
+
def unit_code
|
60
|
+
UNIT_CODES[unit] || 'C62'
|
61
|
+
end
|
62
|
+
|
63
|
+
def tax_category_code
|
64
|
+
TAX_CATEGORY_CODES[tax_category] || 'S'
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_xml(xml, line_item_index)
|
68
|
+
if !valid?
|
69
|
+
raise ValidationError.new("LineItem #{line_item_index} is invalid", errors)
|
70
|
+
end
|
71
|
+
|
72
|
+
xml['ram'].IncludedSupplyChainTradeLineItem do
|
73
|
+
xml['ram'].AssociatedDocumentLineDocument do
|
74
|
+
xml['ram'].LineID line_item_index
|
75
|
+
end
|
76
|
+
xml['ram'].SpecifiedTradeProduct do
|
77
|
+
xml['ram'].Name name
|
78
|
+
xml['ram'].OriginTradeCountry do
|
79
|
+
xml['ram'].ID origin_country_code
|
80
|
+
end
|
81
|
+
end
|
82
|
+
xml['ram'].SpecifiedLineTradeAgreement do
|
83
|
+
xml['ram'].GrossPriceProductTradePrice do
|
84
|
+
xml['ram'].ChargeAmount unit_amount
|
85
|
+
xml['ram'].BasisQuantity(unitCode: unit_code) do
|
86
|
+
xml.text(quantity)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
xml['ram'].NetPriceProductTradePrice do
|
90
|
+
xml['ram'].ChargeAmount unit_amount
|
91
|
+
xml['ram'].BasisQuantity(unitCode: unit_code) do
|
92
|
+
xml.text(quantity)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
xml['ram'].SpecifiedLineTradeDelivery do
|
97
|
+
xml['ram'].BilledQuantity(unitCode: unit_code) do
|
98
|
+
xml.text(quantity)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
xml['ram'].SpecifiedLineTradeSettlement do
|
102
|
+
xml['ram'].ApplicableTradeTax do
|
103
|
+
xml['ram'].TypeCode 'VAT'
|
104
|
+
xml['ram'].CategoryCode tax_category_code
|
105
|
+
xml['ram'].RateApplicablePercent tax_percent
|
106
|
+
|
107
|
+
end
|
108
|
+
xml['ram'].SpecifiedTradeSettlementLineMonetarySummation do
|
109
|
+
xml['ram'].LineTotalAmount charge_amount
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,44 @@
|
|
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
|
+
=end
|
16
|
+
|
17
|
+
module Secretariat
|
18
|
+
TradeParty = Struct.new('TradeParty',
|
19
|
+
:name, :street1, :street2, :city, :postal_code, :country_id, :vat_id,
|
20
|
+
keyword_init: true,
|
21
|
+
) do
|
22
|
+
def to_xml(xml, exclude_tax: false)
|
23
|
+
xml['ram'].Name name
|
24
|
+
xml['ram'].PostalTradeAddress do
|
25
|
+
xml['ram'].PostcodeCode postal_code
|
26
|
+
xml['ram'].LineOne street1
|
27
|
+
if street2 && street2 != ''
|
28
|
+
xml['ram'].LineTwo street2
|
29
|
+
end
|
30
|
+
xml['ram'].CityName city
|
31
|
+
xml['ram'].CountryID country_id
|
32
|
+
end
|
33
|
+
if !exclude_tax && vat_id && vat_id != ''
|
34
|
+
xml['ram'].SpecifiedTaxRegistration do
|
35
|
+
xml['ram'].ID(schemeID: 'VA') do
|
36
|
+
xml.text(vat_id)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,25 @@
|
|
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
|
+
=end
|
16
|
+
|
17
|
+
module Secretariat
|
18
|
+
class ValidationError < StandardError
|
19
|
+
attr_reader :errors
|
20
|
+
def initialize(reason, errors = [])
|
21
|
+
super(reason)
|
22
|
+
@errors = errors
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|