factur-x-builder 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,20 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
3
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
4
+ xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100"
5
+ xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
6
+ xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"
7
+ targetNamespace="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
8
+ elementFormDefault="qualified">
9
+ <xs:import namespace="urn:un:unece:uncefact:data:standard:QualifiedDataType:100" schemaLocation="Factur-X_1.09_EN16931_urn_un_unece_uncefact_data_standard_QualifiedDataType_100.xsd"/>
10
+ <xs:import namespace="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" schemaLocation="Factur-X_1.09_EN16931_urn_un_unece_uncefact_data_standard_ReusableAggregateBusinessInformationEntity_100.xsd"/>
11
+ <xs:import namespace="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" schemaLocation="Factur-X_1.09_EN16931_urn_un_unece_uncefact_data_standard_UnqualifiedDataType_100.xsd"/>
12
+ <xs:element name="CrossIndustryInvoice" type="rsm:CrossIndustryInvoiceType"/>
13
+ <xs:complexType name="CrossIndustryInvoiceType">
14
+ <xs:sequence>
15
+ <xs:element name="ExchangedDocumentContext" type="ram:ExchangedDocumentContextType"/>
16
+ <xs:element name="ExchangedDocument" type="ram:ExchangedDocumentType"/>
17
+ <xs:element name="SupplyChainTradeTransaction" type="ram:SupplyChainTradeTransactionType"/>
18
+ </xs:sequence>
19
+ </xs:complexType>
20
+ </xs:schema>
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "codes"
4
+ require_relative "formatting"
5
+
6
+ module FacturX
7
+ # One VAT category / rate pair of the invoice (BG-23).
8
+ class TaxBreakdown
9
+ attr_reader :category, :rate, :basis_amount, :calculated_amount,
10
+ :exemption_reason, :exemption_reason_code
11
+
12
+ def initialize(basis_amount:, calculated_amount:,
13
+ category: Codes::VatCategory::STANDARD, rate: nil,
14
+ exemption_reason: nil, exemption_reason_code: nil)
15
+ @category = category
16
+ @rate = rate
17
+ @basis_amount = basis_amount
18
+ @calculated_amount = calculated_amount
19
+ @exemption_reason = exemption_reason
20
+ @exemption_reason_code = exemption_reason_code
21
+ end
22
+
23
+ def rate_decimal
24
+ Formatting.decimal(rate || 0)
25
+ end
26
+
27
+ def basis_amount_decimal
28
+ Formatting.decimal(basis_amount)
29
+ end
30
+
31
+ def calculated_amount_decimal
32
+ Formatting.decimal(calculated_amount)
33
+ end
34
+
35
+ def tax_key
36
+ [category, rate_decimal]
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "formatting"
4
+
5
+ module FacturX
6
+ # The document level totals (BT-106 to BT-115). The gem never derives
7
+ # these; the invoicing domain supplies them and they are checked for
8
+ # coherence against the lines and the VAT breakdowns.
9
+ class Totals
10
+ attr_reader :line_total, :allowance_total, :charge_total, :tax_basis_total,
11
+ :tax_total, :rounding_amount, :grand_total, :prepaid, :due_payable
12
+
13
+ def initialize(line_total:, tax_basis_total:, grand_total:, due_payable:,
14
+ tax_total: nil, allowance_total: nil, charge_total: nil,
15
+ rounding_amount: nil, prepaid: nil)
16
+ @line_total = line_total
17
+ @allowance_total = allowance_total
18
+ @charge_total = charge_total
19
+ @tax_basis_total = tax_basis_total
20
+ @tax_total = tax_total
21
+ @rounding_amount = rounding_amount
22
+ @grand_total = grand_total
23
+ @prepaid = prepaid
24
+ @due_payable = due_payable
25
+ end
26
+
27
+ %i[line_total allowance_total charge_total tax_basis_total tax_total
28
+ rounding_amount grand_total prepaid due_payable].each do |field|
29
+ define_method("#{field}_decimal") do
30
+ value = public_send(field)
31
+ Formatting.decimal(value.nil? ? 0 : value)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,349 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+
5
+ require_relative "../codes"
6
+ require_relative "../errors"
7
+ require_relative "../formatting"
8
+
9
+ module FacturX
10
+ module Validation
11
+ # Business rules checked in Ruby so the invoicing domain gets fast,
12
+ # readable errors without shelling out to the official schematron.
13
+ # The rule identifiers refer to EN 16931; the schematron remains the
14
+ # authority and the test suite checks these rules agree with it.
15
+ class Rules
16
+ CURRENCY_PATTERN = /\A[A-Z]{3}\z/.freeze
17
+ COUNTRY_PATTERN = /\A[A-Z]{2}\z/.freeze
18
+ IBAN_PATTERN = /\A[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}\z/.freeze
19
+ SIRET_PATTERN = /\A[0-9]{14}\z/.freeze
20
+ SIREN_PATTERN = /\A[0-9]{9}\z/.freeze
21
+ VAT_NUMBER_PATTERN = /\A[A-Z]{2}[A-Z0-9]{2,13}\z/.freeze
22
+
23
+ ROUNDING_TOLERANCE = BigDecimal("0.01")
24
+
25
+ PROFILES = %i[en16931 fr_ctc].freeze
26
+
27
+ # BR-FR-05: every invoice must carry these three statements.
28
+ MANDATORY_FRENCH_NOTES = {
29
+ Codes::NoteSubject::PAYMENT_TERMS => "recovery costs",
30
+ Codes::NoteSubject::PAYMENT_PENALTIES => "late payment penalties",
31
+ Codes::NoteSubject::DISCOUNT_TERMS => "early payment discount"
32
+ }.freeze
33
+
34
+ def initialize(invoice, profile: :fr_ctc)
35
+ raise Error, "unknown profile #{profile.inspect}" unless PROFILES.include?(profile)
36
+
37
+ @invoice = invoice
38
+ @profile = profile
39
+ @violations = []
40
+ end
41
+
42
+ def violations
43
+ @violations = []
44
+ check_document
45
+ check_parties
46
+ check_lines
47
+ check_tax_breakdowns
48
+ check_payment
49
+ check_totals
50
+ check_french_requirements if french?
51
+ @violations
52
+ end
53
+
54
+ def valid?
55
+ violations.empty?
56
+ end
57
+
58
+ def validate!
59
+ found = violations
60
+ raise ValidationError, found unless found.empty?
61
+
62
+ invoice
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :invoice, :profile
68
+
69
+ def french?
70
+ profile == :fr_ctc
71
+ end
72
+
73
+ def add(message)
74
+ @violations << message
75
+ end
76
+
77
+ # Rules the French e-invoicing reform adds on top of EN 16931.
78
+ def check_french_requirements
79
+ if blank?(invoice.business_process)
80
+ add("an invoicing mode is required by the French e-invoicing reform (BT-23, " \
81
+ "BR-FR-08), expected one of #{Codes::BUSINESS_PROCESSES.join(", ")}")
82
+ end
83
+
84
+ declared = invoice.notes.map(&:subject_code)
85
+ MANDATORY_FRENCH_NOTES.each do |code, description|
86
+ next if declared.include?(code)
87
+
88
+ add("the mandatory statement about #{description} is missing from the invoice " \
89
+ "notes (BT-22 with subject code #{code}, BR-FR-05) - " \
90
+ "Note.french_legal_mentions provides the three of them")
91
+ end
92
+ end
93
+
94
+ def check_document
95
+ add("invoice number is required (BT-1)") if blank?(invoice.number)
96
+ add("issue date is required (BT-2)") if invoice.issued_on.nil?
97
+
98
+ unless Codes::DOCUMENT_TYPES.include?(invoice.type_code)
99
+ add("document type #{invoice.type_code.inspect} is not supported (BT-3), " \
100
+ "expected one of #{Codes::DOCUMENT_TYPES.join(", ")}")
101
+ end
102
+
103
+ unless invoice.currency.to_s.match?(CURRENCY_PATTERN)
104
+ add("currency #{invoice.currency.inspect} is not a 3-letter ISO 4217 code (BT-5)")
105
+ end
106
+
107
+ return unless invoice.business_process && !Codes::BUSINESS_PROCESSES.include?(invoice.business_process)
108
+
109
+ add("business process #{invoice.business_process.inspect} is not a Factur-X " \
110
+ "process identifier (BT-23)")
111
+ end
112
+
113
+ def check_parties
114
+ check_party(invoice.seller, "seller")
115
+ check_party(invoice.buyer, "buyer")
116
+
117
+ if invoice.seller && seller_identifier_missing?
118
+ add("seller must carry a VAT number, a tax registration or a legal registration " \
119
+ "identifier (BR-CO-9 / BR-FR-09)")
120
+ end
121
+
122
+ check_french_identifiers(invoice.seller, "seller")
123
+ check_french_identifiers(invoice.buyer, "buyer")
124
+ end
125
+
126
+ def check_party(party, role)
127
+ if party.nil?
128
+ add("#{role} is required (#{role == "seller" ? "BG-4" : "BG-7"})")
129
+ return
130
+ end
131
+
132
+ add("#{role} name is required") if blank?(party.name)
133
+
134
+ if party.address.nil?
135
+ add("#{role} postal address is required")
136
+ return
137
+ end
138
+
139
+ country = party.address.country_code
140
+ return if country.to_s.match?(COUNTRY_PATTERN)
141
+
142
+ add("#{role} country code #{country.inspect} is not a 2-letter ISO 3166 code")
143
+ end
144
+
145
+ def seller_identifier_missing?
146
+ seller = invoice.seller
147
+ [seller.vat_number, seller.tax_id, seller.legal_id, seller.global_id].all? { |v| blank?(v) }
148
+ end
149
+
150
+ def check_french_identifiers(party, role)
151
+ return if party.nil?
152
+
153
+ if party.siret && !party.siret.to_s.match?(SIRET_PATTERN)
154
+ add("#{role} SIRET #{party.siret.inspect} must be 14 digits")
155
+ end
156
+
157
+ if party.siren && !party.siren.to_s.match?(SIREN_PATTERN)
158
+ add("#{role} SIREN #{party.siren.inspect} must be 9 digits")
159
+ end
160
+
161
+ return unless party.vat_number && !party.vat_number.to_s.match?(VAT_NUMBER_PATTERN)
162
+
163
+ add("#{role} VAT number #{party.vat_number.inspect} is not a valid EU VAT identifier")
164
+ end
165
+
166
+ def check_lines
167
+ if invoice.lines.empty?
168
+ add("an invoice must carry at least one line (BR-16)")
169
+ return
170
+ end
171
+
172
+ invoice.lines.each do |line|
173
+ label = "line #{line.number}"
174
+ add("#{label}: name is required (BT-153)") if blank?(line.name)
175
+ add("#{label}: unit code is required (BT-130)") if blank?(line.unit_code)
176
+ check_vat_category(line.vat_category, line.vat_rate, label)
177
+ end
178
+ end
179
+
180
+ def check_vat_category(category, rate, label)
181
+ unless Codes::VatCategory::ALL.include?(category)
182
+ add("#{label}: VAT category #{category.inspect} is not an EN 16931 category code")
183
+ return
184
+ end
185
+
186
+ decimal_rate = rate.nil? ? nil : Formatting.decimal(rate)
187
+
188
+ if category == Codes::VatCategory::STANDARD
189
+ if decimal_rate.nil? || decimal_rate.zero?
190
+ add("#{label}: standard rated items need a VAT rate above zero (BR-S-05)")
191
+ end
192
+ elsif Codes::VatCategory::ZERO_RATE_ONLY.include?(category) &&
193
+ !decimal_rate.nil? && !decimal_rate.zero?
194
+ add("#{label}: VAT category #{category} requires a zero rate, got #{decimal_rate.to_s("F")}")
195
+ end
196
+ end
197
+
198
+ def check_tax_breakdowns
199
+ if invoice.tax_breakdowns.empty?
200
+ add("at least one VAT breakdown is required (BR-CO-18)")
201
+ return
202
+ end
203
+
204
+ invoice.tax_breakdowns.each do |breakdown|
205
+ label = "VAT breakdown #{breakdown.category}/#{breakdown.rate_decimal.to_s("F")}"
206
+ check_vat_category(breakdown.category, breakdown.rate, label)
207
+ check_exemption_reason(breakdown, label)
208
+ check_breakdown_arithmetic(breakdown, label)
209
+ end
210
+
211
+ check_breakdown_coverage
212
+ end
213
+
214
+ def check_exemption_reason(breakdown, label)
215
+ return unless Codes::VatCategory::REQUIRING_EXEMPTION_REASON.include?(breakdown.category)
216
+ return if !blank?(breakdown.exemption_reason) || !blank?(breakdown.exemption_reason_code)
217
+
218
+ add("#{label}: an exemption reason is required for category #{breakdown.category} " \
219
+ "(BR-E-10 / BR-AE-10 / BR-IC-10 / BR-G-10)")
220
+ end
221
+
222
+ def check_breakdown_arithmetic(breakdown, label)
223
+ expected = (breakdown.basis_amount_decimal * breakdown.rate_decimal / 100).round(2)
224
+ actual = breakdown.calculated_amount_decimal.round(2)
225
+ return if (expected - actual).abs <= ROUNDING_TOLERANCE
226
+
227
+ add("#{label}: VAT amount #{actual.to_s("F")} does not match " \
228
+ "#{breakdown.basis_amount_decimal.to_s("F")} x #{breakdown.rate_decimal.to_s("F")}% " \
229
+ "= #{expected.to_s("F")} (BR-CO-17)")
230
+ end
231
+
232
+ def check_breakdown_coverage
233
+ declared = invoice.tax_breakdowns.map(&:tax_key)
234
+ expected_basis = basis_by_tax_key
235
+
236
+ (expected_basis.keys - declared).each do |category, rate|
237
+ add("no VAT breakdown declared for category #{category} at #{rate.to_s("F")}% " \
238
+ "although lines use it (BR-CO-18)")
239
+ end
240
+
241
+ invoice.tax_breakdowns.each do |breakdown|
242
+ expected = expected_basis[breakdown.tax_key]
243
+ if expected.nil?
244
+ add("VAT breakdown #{breakdown.category}/#{breakdown.rate_decimal.to_s("F")} " \
245
+ "matches no invoice line")
246
+ next
247
+ end
248
+
249
+ actual = breakdown.basis_amount_decimal.round(2)
250
+ next if actual == expected.round(2)
251
+
252
+ add("VAT breakdown #{breakdown.category}/#{breakdown.rate_decimal.to_s("F")}: taxable " \
253
+ "base #{actual.to_s("F")} does not match the sum of its lines " \
254
+ "#{expected.round(2).to_s("F")} (BR-S-08)")
255
+ end
256
+ end
257
+
258
+ def basis_by_tax_key
259
+ totals = Hash.new { |hash, key| hash[key] = BigDecimal(0) }
260
+
261
+ invoice.lines.each { |line| totals[line.tax_key] += line.net_amount_decimal }
262
+
263
+ invoice.allowances.each do |allowance|
264
+ totals[tax_key_of(allowance)] -= allowance.amount_decimal
265
+ end
266
+ invoice.charges.each do |charge|
267
+ totals[tax_key_of(charge)] += charge.amount_decimal
268
+ end
269
+
270
+ totals
271
+ end
272
+
273
+ def tax_key_of(item)
274
+ [item.vat_category, Formatting.decimal(item.vat_rate || 0)]
275
+ end
276
+
277
+ def check_payment
278
+ invoice.payment_means.each do |means|
279
+ if blank?(means.type_code)
280
+ add("payment means type code is required (BT-81)")
281
+ next
282
+ end
283
+
284
+ if Codes::PaymentMeansType::REQUIRING_CREDITOR_ACCOUNT.include?(means.type_code) &&
285
+ !means.creditor_account?
286
+ add("payment means #{means.type_code} (credit transfer) requires a creditor " \
287
+ "account identifier (BR-61)")
288
+ end
289
+
290
+ if means.iban && !means.iban.to_s.delete(" ").match?(IBAN_PATTERN)
291
+ add("payment IBAN #{means.iban.inspect} is not a well-formed IBAN")
292
+ end
293
+ end
294
+ end
295
+
296
+ def check_totals
297
+ totals = invoice.totals
298
+ if totals.nil?
299
+ add("invoice totals are required (BG-22)")
300
+ return
301
+ end
302
+
303
+ lines_sum = invoice.lines.reduce(BigDecimal(0)) { |sum, line| sum + line.net_amount_decimal }
304
+ compare(totals.line_total_decimal, lines_sum,
305
+ "sum of invoice line net amounts (BT-106)", "the invoice lines", "BR-CO-10")
306
+
307
+ allowances_sum = sum_of(invoice.allowances)
308
+ charges_sum = sum_of(invoice.charges)
309
+ compare(totals.allowance_total_decimal, allowances_sum,
310
+ "allowances total (BT-107)", "the document allowances", "BR-CO-11")
311
+ compare(totals.charge_total_decimal, charges_sum,
312
+ "charges total (BT-108)", "the document charges", "BR-CO-12")
313
+
314
+ compare(totals.tax_basis_total_decimal,
315
+ totals.line_total_decimal - allowances_sum + charges_sum,
316
+ "total without VAT (BT-109)", "lines minus allowances plus charges", "BR-CO-13")
317
+
318
+ vat_sum = invoice.tax_breakdowns.reduce(BigDecimal(0)) do |sum, breakdown|
319
+ sum + breakdown.calculated_amount_decimal
320
+ end
321
+ compare(totals.tax_total_decimal, vat_sum,
322
+ "total VAT amount (BT-110)", "the VAT breakdowns", "BR-CO-14")
323
+
324
+ compare(totals.grand_total_decimal,
325
+ totals.tax_basis_total_decimal + totals.tax_total_decimal,
326
+ "total with VAT (BT-112)", "total without VAT plus VAT", "BR-CO-15")
327
+
328
+ compare(totals.due_payable_decimal,
329
+ totals.grand_total_decimal - totals.prepaid_decimal + totals.rounding_amount_decimal,
330
+ "amount due for payment (BT-115)", "total with VAT minus prepaid", "BR-CO-16")
331
+ end
332
+
333
+ def sum_of(items)
334
+ items.reduce(BigDecimal(0)) { |sum, item| sum + item.amount_decimal }
335
+ end
336
+
337
+ def compare(actual, expected, subject, source, rule)
338
+ return if actual.round(2) == expected.round(2)
339
+
340
+ add("#{subject} is #{actual.round(2).to_s("F")} but #{source} add up to " \
341
+ "#{expected.round(2).to_s("F")} (#{rule})")
342
+ end
343
+
344
+ def blank?(value)
345
+ value.nil? || value.to_s.strip.empty?
346
+ end
347
+ end
348
+ end
349
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ require_relative "../errors"
6
+
7
+ module FacturX
8
+ module Validation
9
+ # Structural validation against the official Factur-X 1.0.9 EN 16931 XSD.
10
+ module Schema
11
+ ROOT = File.expand_path("../schemas/Factur-X_EN16931.xsd", __dir__)
12
+
13
+ class << self
14
+ def validate(xml)
15
+ document = xml.is_a?(Nokogiri::XML::Document) ? xml : Nokogiri::XML(xml)
16
+ compiled.validate(document).map(&:message)
17
+ end
18
+
19
+ def valid?(xml)
20
+ validate(xml).empty?
21
+ end
22
+
23
+ def validate!(xml)
24
+ violations = validate(xml)
25
+ raise SchemaError, violations unless violations.empty?
26
+
27
+ xml
28
+ end
29
+
30
+ # Compiling the schema costs far more than validating against it, so the
31
+ # imports are resolved once and the result kept for the process lifetime.
32
+ def compiled
33
+ @compiled ||= Nokogiri::XML::Schema.from_document(
34
+ Nokogiri::XML(File.read(ROOT), ROOT)
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FacturX
4
+ VERSION = "0.1.0"
5
+ end
data/lib/factur_x.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "factur_x/version"
4
+ require_relative "factur_x/errors"
5
+ require_relative "factur_x/codes"
6
+ require_relative "factur_x/formatting"
7
+ require_relative "factur_x/address"
8
+ require_relative "factur_x/contact"
9
+ require_relative "factur_x/party"
10
+ require_relative "factur_x/note"
11
+ require_relative "factur_x/line"
12
+ require_relative "factur_x/tax_breakdown"
13
+ require_relative "factur_x/totals"
14
+ require_relative "factur_x/payment"
15
+ require_relative "factur_x/allowance_charge"
16
+ require_relative "factur_x/invoice"
17
+ require_relative "factur_x/document"
18
+ require_relative "factur_x/validation/schema"
19
+ require_relative "factur_x/validation/rules"
20
+
21
+ # Generates the factur-x.xml attachment of a Factur-X PDF/A-3 invoice,
22
+ # in the EN 16931 profile.
23
+ module FacturX
24
+ class << self
25
+ # @param invoice [Invoice]
26
+ # @param profile [Symbol] :fr_ctc adds the rules of the French e-invoicing
27
+ # reform on top of EN 16931; use :en16931 for invoices issued outside France
28
+ # @param validate [Boolean] check the business rules first
29
+ # @param validate_schema [Boolean] check the result against the Factur-X XSD
30
+ # @return [String] the factur-x.xml document
31
+ def build(invoice, profile: :fr_ctc, validate: true, validate_schema: true)
32
+ Validation::Rules.new(invoice, profile: profile).validate! if validate
33
+
34
+ xml = Document.new(invoice).to_xml
35
+ Validation::Schema.validate!(xml) if validate_schema
36
+ xml
37
+ end
38
+
39
+ # Collects every business rule violation instead of raising on the first.
40
+ # @return [Array<String>]
41
+ def violations(invoice, profile: :fr_ctc)
42
+ Validation::Rules.new(invoice, profile: profile).violations
43
+ end
44
+
45
+ def valid?(invoice, profile: :fr_ctc)
46
+ violations(invoice, profile: profile).empty?
47
+ end
48
+ end
49
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: factur-x-builder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Hotentic
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-08-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '1.16'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.15'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.16'
33
+ description: Generates the EN 16931 factur-x.xml attachment of a Factur-X invoice
34
+ from invoicing domain objects, with French e-invoicing identifiers (SIRET, SIREN,
35
+ routing address) supported out of the box.
36
+ email:
37
+ - contact@hotentic.com
38
+ executables: []
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - ".gitignore"
43
+ - ".rubocop.yml"
44
+ - CHANGELOG.md
45
+ - CODE_OF_CONDUCT.md
46
+ - Gemfile
47
+ - Gemfile.lock
48
+ - LICENSE.txt
49
+ - README.md
50
+ - Rakefile
51
+ - bin/console
52
+ - bin/setup
53
+ - factur-x-builder.gemspec
54
+ - lib/factur-x-builder.rb
55
+ - lib/factur_x.rb
56
+ - lib/factur_x/address.rb
57
+ - lib/factur_x/allowance_charge.rb
58
+ - lib/factur_x/codes.rb
59
+ - lib/factur_x/contact.rb
60
+ - lib/factur_x/document.rb
61
+ - lib/factur_x/errors.rb
62
+ - lib/factur_x/formatting.rb
63
+ - lib/factur_x/invoice.rb
64
+ - lib/factur_x/line.rb
65
+ - lib/factur_x/note.rb
66
+ - lib/factur_x/party.rb
67
+ - lib/factur_x/payment.rb
68
+ - lib/factur_x/schemas/Factur-X_1.09_EN16931_QualifiedDataType_100.xsd
69
+ - lib/factur_x/schemas/Factur-X_1.09_EN16931_ReusableAggregateBusinessInformationEntity_100.xsd
70
+ - lib/factur_x/schemas/Factur-X_1.09_EN16931_UnqualifiedDataType_100.xsd
71
+ - lib/factur_x/schemas/Factur-X_EN16931.xsd
72
+ - lib/factur_x/tax_breakdown.rb
73
+ - lib/factur_x/totals.rb
74
+ - lib/factur_x/validation/rules.rb
75
+ - lib/factur_x/validation/schema.rb
76
+ - lib/factur_x/version.rb
77
+ homepage: https://hotentic.com
78
+ licenses:
79
+ - MIT
80
+ metadata:
81
+ homepage_uri: https://hotentic.com
82
+ source_code_uri: https://github.com/hotentic/factur-x-builder
83
+ changelog_uri: https://github.com/hotentic/factur-x-builder/blob/main/CHANGELOG.md
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 2.7.0
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.1.6
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: A simple tool to build a factur-x.xml XML file using required input fields.
103
+ test_files: []