secretariat 1.0.0

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