en16931 0.1.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,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module EN16931
6
+ # The invoice as UN/CEFACT CII XML, by default in the XRechnung profile
7
+ # (the German CIUS of EN 16931, which every German business must be able
8
+ # to receive and which a ZUGFeRD "XRECHNUNG" hybrid carries unchanged).
9
+ #
10
+ # The element tree comes from zugpferd; the few things its writer does not
11
+ # cover yet (tax number with scheme FC, BIC and account name, the invoicing
12
+ # period, the preceding invoice of a correction) are added here.
13
+ module XML
14
+ NS = {
15
+ "rsm" => "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100",
16
+ "ram" => "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100",
17
+ "udt" => "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"
18
+ }.freeze
19
+
20
+ module_function
21
+
22
+ # @param profile [:xrechnung, :en16931] the guideline the XML claims (BT-24)
23
+ # @return [String] UTF-8 XML
24
+ def cii(invoice, profile: :xrechnung)
25
+ document = to_zugpferd(invoice, profile:)
26
+ xml = Zugpferd::CII::Writer.new.write(document)
27
+ Extensions.apply(xml, invoice)
28
+ end
29
+
30
+ def guideline(profile)
31
+ case profile
32
+ when :xrechnung then XRECHNUNG_GUIDELINE
33
+ when :en16931 then EN16931_GUIDELINE
34
+ else raise ArgumentError, "unknown profile #{profile.inspect}"
35
+ end
36
+ end
37
+
38
+ # Maps the invoice onto zugpferd's EN 16931 model.
39
+ def to_zugpferd(invoice, profile: :xrechnung)
40
+ totals = invoice.totals
41
+ klass = { "380" => Zugpferd::Model::Invoice, "381" => Zugpferd::Model::CreditNote,
42
+ "384" => Zugpferd::Model::CorrectedInvoice, "389" => Zugpferd::Model::SelfBilledInvoice,
43
+ "326" => Zugpferd::Model::PartialInvoice, "386" => Zugpferd::Model::PrepaymentInvoice }.fetch(invoice.type_code)
44
+ doc = klass.new(number: invoice.number, issue_date: invoice.issue_date, currency_code: invoice.currency)
45
+ doc.customization_id = guideline(profile)
46
+ doc.profile_id = PEPPOL_BILLING_PROCESS
47
+ doc.due_date = invoice.due_date
48
+ doc.delivery_date = invoice.delivery_date
49
+ doc.buyer_reference = invoice.buyer_reference
50
+ doc.note = invoice.notes.join("\n") unless invoice.notes.empty?
51
+ hide_vat_ids = invoice.taxes.any?(&:forbids_vat_identifiers?)
52
+ doc.seller = party(invoice.seller, vat_identifier: !hide_vat_ids, seller: true)
53
+ doc.buyer = party(invoice.buyer, vat_identifier: !hide_vat_ids)
54
+ doc.line_items = invoice.lines.each_with_index.map { |line, i| line_item(invoice, line, i + 1) }
55
+ doc.allowance_charges = (invoice.allowances + invoice.charges).map { |ac| allowance_charge(invoice, ac) }
56
+ doc.tax_breakdown = tax_breakdown(invoice, totals)
57
+ doc.monetary_totals = Zugpferd::Model::MonetaryTotals.new(
58
+ line_extension_amount: totals.line_total, tax_exclusive_amount: totals.net,
59
+ tax_inclusive_amount: totals.gross, payable_amount: totals.payable,
60
+ allowance_total_amount: (totals.allowance_total if totals.allowance_total.positive?),
61
+ charge_total_amount: (totals.charge_total if totals.charge_total.positive?),
62
+ prepaid_amount: (totals.prepaid if totals.prepaid.positive?)
63
+ )
64
+ doc.payment_instructions = payment_instructions(invoice)
65
+ doc
66
+ end
67
+
68
+ # A seller without a VAT id on the invoice (Kleinunternehmer, category O)
69
+ # still has to be identifiable (BR-CO-26): the tax number serves as the
70
+ # seller identifier (BT-29) unless the party carries its own.
71
+ def party(party, vat_identifier: true, seller: false)
72
+ vat_id = (party.vat_id if vat_identifier)
73
+ identifier = party.identifier || (party.tax_number if seller && vat_id.nil?)
74
+ Zugpferd::Model::TradeParty.new(
75
+ name: party.name,
76
+ identifier: identifier,
77
+ legal_registration_id: party.registration_id,
78
+ trading_name: party.trading_name,
79
+ legal_form: party.legal_form,
80
+ vat_identifier: vat_id,
81
+ electronic_address: party.electronic_address_value,
82
+ electronic_address_scheme: party.electronic_address_scheme_value,
83
+ postal_address: Zugpferd::Model::PostalAddress.new(
84
+ country_code: party.country, street_name: party.street, city_name: party.city, postal_zone: party.postal_code
85
+ ),
86
+ contact: contact(party)
87
+ )
88
+ end
89
+
90
+ def contact(party)
91
+ return nil if party.contact_name.nil? && party.phone.nil? && party.email.nil?
92
+
93
+ Zugpferd::Model::Contact.new(name: party.contact_name || party.name, telephone: party.phone, email: party.email)
94
+ end
95
+
96
+ def line_item(invoice, line, position)
97
+ tax = invoice.tax_for(line)
98
+ Zugpferd::Model::LineItem.new(
99
+ id: position.to_s, invoiced_quantity: line.quantity, unit_code: line.unit, line_extension_amount: line.net,
100
+ note: line.note,
101
+ item: Zugpferd::Model::Item.new(name: line.description, sellers_identifier: line.sellers_id,
102
+ tax_category: tax.category, tax_percent: tax.rate_for_xml),
103
+ price: Zugpferd::Model::Price.new(amount: line.unit_price)
104
+ )
105
+ end
106
+
107
+ def allowance_charge(invoice, ac)
108
+ tax = ac.tax || invoice.tax
109
+ Zugpferd::Model::AllowanceCharge.new(
110
+ charge_indicator: ac.charge?, amount: ac.amount, reason: ac.reason, reason_code: ac.reason_code,
111
+ tax_category_code: tax.category, tax_percent: tax.rate_for_xml, currency_code: invoice.currency
112
+ )
113
+ end
114
+
115
+ def tax_breakdown(invoice, totals)
116
+ breakdown = Zugpferd::Model::TaxBreakdown.new(tax_amount: totals.vat, currency_code: invoice.currency)
117
+ breakdown.subtotals = totals.subtotals.map do |sub|
118
+ Zugpferd::Model::TaxSubtotal.new(
119
+ taxable_amount: sub.taxable, tax_amount: sub.amount, category_code: sub.tax.category,
120
+ percent: sub.tax.rate, currency_code: invoice.currency, # BT-119 always (BR-DE-14), also for O
121
+ exemption_reason: sub.tax.exemption_reason, exemption_reason_code: sub.tax.exemption_code
122
+ )
123
+ end
124
+ breakdown
125
+ end
126
+
127
+ def payment_instructions(invoice)
128
+ payment = invoice.payment
129
+ return nil if payment.nil?
130
+
131
+ Zugpferd::Model::PaymentInstructions.new(
132
+ payment_means_code: payment.means_code, account_id: payment.iban, payment_id: payment.reference, note: payment.terms
133
+ )
134
+ end
135
+
136
+ # Elements zugpferd's writer leaves out, inserted at their schema position.
137
+ module Extensions
138
+ module_function
139
+
140
+ def apply(xml, invoice)
141
+ doc = Nokogiri::XML(xml, &:noblanks)
142
+ add_seller_tax_number(doc, invoice)
143
+ add_bank_details(doc, invoice)
144
+ add_billing_period(doc, invoice)
145
+ add_preceding_invoice(doc, invoice)
146
+ doc.to_xml(indent: 2, encoding: "UTF-8")
147
+ end
148
+
149
+ # BT-32 seller tax registration (scheme FC). XRechnung wants a VAT id
150
+ # or a tax number; category O forbids the VAT id, so the number is the
151
+ # only way to identify a Kleinunternehmer or a seller invoicing abroad.
152
+ def add_seller_tax_number(doc, invoice)
153
+ number = invoice.seller.tax_number
154
+ return if number.nil?
155
+
156
+ seller = doc.at_xpath("//ram:SellerTradeParty", NS)
157
+ reg = Nokogiri::XML::Node.new("ram:SpecifiedTaxRegistration", doc)
158
+ id = Nokogiri::XML::Node.new("ram:ID", doc)
159
+ id["schemeID"] = "FC"
160
+ id.content = number
161
+ reg.add_child(id)
162
+ seller.add_child(reg)
163
+ end
164
+
165
+ # BT-85 account name, BT-86 BIC: after the IBAN.
166
+ def add_bank_details(doc, invoice)
167
+ payment = invoice.payment
168
+ return if payment.nil? || payment.iban.nil?
169
+
170
+ account = doc.at_xpath("//ram:PayeePartyCreditorFinancialAccount", NS)
171
+ if payment.holder
172
+ name = Nokogiri::XML::Node.new("ram:AccountName", doc)
173
+ name.content = payment.holder
174
+ account.add_child(name)
175
+ end
176
+ return if payment.bic.nil?
177
+
178
+ inst = Nokogiri::XML::Node.new("ram:PayeeSpecifiedCreditorFinancialInstitution", doc)
179
+ bic = Nokogiri::XML::Node.new("ram:BICID", doc)
180
+ bic.content = payment.bic
181
+ inst.add_child(bic)
182
+ account.add_next_sibling(inst)
183
+ end
184
+
185
+ # BG-14 invoicing period: after the last ApplicableTradeTax.
186
+ def add_billing_period(doc, invoice)
187
+ period = invoice.period
188
+ return if period.nil?
189
+
190
+ node = Nokogiri::XML::Node.new("ram:BillingSpecifiedPeriod", doc)
191
+ node.add_child(date_time(doc, "ram:StartDateTime", period.begin))
192
+ node.add_child(date_time(doc, "ram:EndDateTime", period.end))
193
+ doc.xpath("//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax", NS).last.add_next_sibling(node)
194
+ end
195
+
196
+ # BG-3 preceding invoice reference: after the monetary summation.
197
+ def add_preceding_invoice(doc, invoice)
198
+ preceding = invoice.preceding
199
+ return if preceding.nil?
200
+
201
+ node = Nokogiri::XML::Node.new("ram:InvoiceReferencedDocument", doc)
202
+ id = Nokogiri::XML::Node.new("ram:IssuerAssignedID", doc)
203
+ id.content = preceding.fetch(:number)
204
+ node.add_child(id)
205
+ node.add_child(date_time(doc, "ram:FormattedIssueDateTime", preceding[:issue_date], qualified: true)) if preceding[:issue_date]
206
+ doc.at_xpath("//ram:SpecifiedTradeSettlementHeaderMonetarySummation", NS).add_next_sibling(node)
207
+ end
208
+
209
+ def date_time(doc, name, date, qualified: false)
210
+ wrapper = Nokogiri::XML::Node.new(name, doc)
211
+ inner = Nokogiri::XML::Node.new(qualified ? "qdt:DateTimeString" : "udt:DateTimeString", doc)
212
+ inner["format"] = "102"
213
+ inner.content = date.strftime("%Y%m%d")
214
+ wrapper.add_child(inner)
215
+ wrapper
216
+ end
217
+ end
218
+ end
219
+ end
data/lib/en16931.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "bigdecimal/util"
5
+ require "date"
6
+ require "zugpferd"
7
+
8
+ # EN 16931 e-invoices for Ruby: a small invoice model, CII XML in the
9
+ # XRechnung profile (through zugpferd), a PDF/A-3 hybrid with the XML
10
+ # embedded (through Prawn, pure Ruby), German tax presets, and wrappers for
11
+ # the official validators (KoSIT, veraPDF).
12
+ module EN16931
13
+ class Error < StandardError; end
14
+ class ValidationError < Error; end
15
+
16
+ # Guideline identifiers (BT-24) and business process (BT-23).
17
+ XRECHNUNG_GUIDELINE = "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0"
18
+ EN16931_GUIDELINE = "urn:cen.eu:en16931:2017"
19
+ PEPPOL_BILLING_PROCESS = "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"
20
+
21
+ ROOT = File.expand_path("..", __dir__)
22
+
23
+ def self.font_path(name) = File.join(ROOT, "fonts", name)
24
+ end
25
+
26
+ require_relative "en16931/version"
27
+ require_relative "en16931/tax"
28
+ require_relative "en16931/de"
29
+ require_relative "en16931/party"
30
+ require_relative "en16931/line"
31
+ require_relative "en16931/payment"
32
+ require_relative "en16931/invoice"
33
+ require_relative "en16931/totals"
34
+ require_relative "en16931/branding"
35
+ require_relative "en16931/xml"
36
+ require_relative "en16931/pdf"
37
+ require_relative "en16931/validation"
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: en16931
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Markus Schwed
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: nokogiri
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.16'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.16'
26
+ - !ruby/object:Gem::Dependency
27
+ name: prawn
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.5'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.5'
40
+ - !ruby/object:Gem::Dependency
41
+ name: prawn-table
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.2'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rqrcode_core
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: zugpferd
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.3'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.3'
82
+ description: Build EN 16931 invoices from a small Ruby model, write them as XRechnung
83
+ (CII) and as a ZUGFeRD/Factur-X hybrid PDF/A-3 with Prawn, with German VAT presets
84
+ (Regelbesteuerung, Kleinunternehmer, Reverse Charge, Drittland) and wrappers for
85
+ the official validators.
86
+ email:
87
+ - ma.schwed@googlemail.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - CHANGELOG.md
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - fonts/DejaVuSans-Bold.ttf
97
+ - fonts/DejaVuSans.ttf
98
+ - fonts/LICENSE-DejaVu.txt
99
+ - icc/LICENSE-Compact-ICC-Profiles.txt
100
+ - icc/sRGB-v2-micro.icc
101
+ - lib/en16931.rb
102
+ - lib/en16931/branding.rb
103
+ - lib/en16931/de.rb
104
+ - lib/en16931/invoice.rb
105
+ - lib/en16931/line.rb
106
+ - lib/en16931/party.rb
107
+ - lib/en16931/payment.rb
108
+ - lib/en16931/pdf.rb
109
+ - lib/en16931/pdf/a3.rb
110
+ - lib/en16931/pdf/giro_code.rb
111
+ - lib/en16931/pdf/renderer.rb
112
+ - lib/en16931/pdf/xmp.rb
113
+ - lib/en16931/tax.rb
114
+ - lib/en16931/totals.rb
115
+ - lib/en16931/validation.rb
116
+ - lib/en16931/validation/kosit.rb
117
+ - lib/en16931/validation/report.rb
118
+ - lib/en16931/validation/vera_pdf.rb
119
+ - lib/en16931/version.rb
120
+ - lib/en16931/xml.rb
121
+ homepage: https://github.com/Affliq/en16931
122
+ licenses:
123
+ - MIT
124
+ metadata:
125
+ homepage_uri: https://github.com/Affliq/en16931
126
+ source_code_uri: https://github.com/Affliq/en16931
127
+ changelog_uri: https://github.com/Affliq/en16931/blob/main/CHANGELOG.md
128
+ bug_tracker_uri: https://github.com/Affliq/en16931/issues
129
+ rubygems_mfa_required: 'true'
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: 3.2.0
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubygems_version: 3.6.9
145
+ specification_version: 4
146
+ summary: 'EN 16931 e-invoices for Ruby: XRechnung XML and ZUGFeRD hybrid PDF/A-3,
147
+ in pure Ruby.'
148
+ test_files: []