secretariat 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|