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.
@@ -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
@@ -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
+
@@ -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