facturx 0.1.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -2
- data/NOTICE.md +2 -0
- data/README.md +62 -4
- data/lib/facturx/builders/association.rb +8 -0
- data/lib/facturx/builders/base.rb +129 -0
- data/lib/facturx/builders/schema.rb +112 -0
- data/lib/facturx/builders.rb +25 -0
- data/lib/facturx/coerce.rb +74 -0
- data/lib/facturx/conformance.rb +163 -0
- data/lib/facturx/diagnostic.rb +20 -0
- data/lib/facturx/document.rb +45 -0
- data/lib/facturx/error.rb +3 -0
- data/lib/facturx/format.rb +85 -0
- data/lib/facturx/generate.rb +21 -0
- data/lib/facturx/group.rb +5 -0
- data/lib/facturx/model/common.rb +8 -0
- data/lib/facturx/model/immutable.rb +75 -0
- data/lib/facturx/model/line.rb +14 -0
- data/lib/facturx/model/party.rb +10 -0
- data/lib/facturx/model/payment.rb +9 -0
- data/lib/facturx/model/reference.rb +7 -0
- data/lib/facturx/model/trade.rb +9 -0
- data/lib/facturx/model.rb +9 -0
- data/lib/facturx/pdf/extractor.rb +7 -4
- data/lib/facturx/profile_resolver.rb +40 -0
- data/lib/facturx/reader/semantic_mapper/delivery_mapping.rb +43 -0
- data/lib/facturx/reader/semantic_mapper/document_mapping.rb +109 -0
- data/lib/facturx/reader/semantic_mapper/helpers.rb +114 -0
- data/lib/facturx/reader/semantic_mapper/line_mapping.rb +120 -0
- data/lib/facturx/reader/semantic_mapper/line_references.rb +19 -0
- data/lib/facturx/reader/semantic_mapper/party_mapping.rb +90 -0
- data/lib/facturx/reader/semantic_mapper/payment_mapping.rb +108 -0
- data/lib/facturx/reader/semantic_mapper/trade_mapping.rb +83 -0
- data/lib/facturx/reader/semantic_mapper.rb +76 -0
- data/lib/facturx/reader/term_reader/coercion.rb +69 -0
- data/lib/facturx/reader/term_reader/unmapped.rb +37 -0
- data/lib/facturx/reader/term_reader.rb +132 -0
- data/lib/facturx/reader.rb +97 -0
- data/lib/facturx/reading.rb +23 -0
- data/lib/facturx/source_reader.rb +30 -0
- data/lib/facturx/term.rb +5 -0
- data/lib/facturx/terms/declaration.rb +20 -0
- data/lib/facturx/terms/declarations/adjustments.rb +230 -0
- data/lib/facturx/terms/declarations/delivery.rb +83 -0
- data/lib/facturx/terms/declarations/document.rb +223 -0
- data/lib/facturx/terms/declarations/groups.rb +356 -0
- data/lib/facturx/terms/declarations/lines.rb +166 -0
- data/lib/facturx/terms/declarations/parties.rb +609 -0
- data/lib/facturx/terms/declarations/payment.rb +106 -0
- data/lib/facturx/terms/declarations/products.rb +103 -0
- data/lib/facturx/terms/declarations/references.rb +72 -0
- data/lib/facturx/terms/declarations/taxes.rb +92 -0
- data/lib/facturx/terms/declarations/totals.rb +107 -0
- data/lib/facturx/terms/reference.rb +21 -0
- data/lib/facturx/terms.rb +109 -0
- data/lib/facturx/version.rb +1 -1
- data/lib/facturx/writer/context.rb +53 -0
- data/lib/facturx/writer/stage.rb +47 -0
- data/lib/facturx/writer/stage_support/contact_details.rb +57 -0
- data/lib/facturx/writer/stage_support/emission.rb +78 -0
- data/lib/facturx/writer/stage_support/identifiers.rb +129 -0
- data/lib/facturx/writer/stage_support/structure.rb +88 -0
- data/lib/facturx/writer/stages/document_context.rb +31 -0
- data/lib/facturx/writer/stages/exchanged_document.rb +32 -0
- data/lib/facturx/writer/stages/trade_agreement/parties.rb +105 -0
- data/lib/facturx/writer/stages/trade_agreement/references.rb +92 -0
- data/lib/facturx/writer/stages/trade_agreement.rb +45 -0
- data/lib/facturx/writer/stages/trade_delivery.rb +77 -0
- data/lib/facturx/writer/stages/trade_lines/agreement.rb +92 -0
- data/lib/facturx/writer/stages/trade_lines/product.rb +78 -0
- data/lib/facturx/writer/stages/trade_lines/quantity.rb +17 -0
- data/lib/facturx/writer/stages/trade_lines/settlement.rb +89 -0
- data/lib/facturx/writer/stages/trade_lines.rb +67 -0
- data/lib/facturx/writer/stages/trade_settlement/parties.rb +33 -0
- data/lib/facturx/writer/stages/trade_settlement/payment.rb +97 -0
- data/lib/facturx/writer/stages/trade_settlement/summary.rb +88 -0
- data/lib/facturx/writer/stages/trade_settlement/taxes.rb +121 -0
- data/lib/facturx/writer/stages/trade_settlement.rb +70 -0
- data/lib/facturx/writer.rb +68 -0
- data/lib/facturx/xml/namespaces.rb +19 -0
- data/lib/facturx/xml/profile_detector.rb +2 -4
- data/lib/facturx.rb +42 -0
- data/rbi/facturx.rbi +1090 -0
- metadata +79 -4
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model/immutable'
|
|
4
|
+
|
|
5
|
+
module Facturx
|
|
6
|
+
Diagnostic = Data.define(:code, :term_id, :path, :message, :details) do
|
|
7
|
+
include Model::ValidatedWith
|
|
8
|
+
|
|
9
|
+
def initialize(code:, message:, term_id: nil, path: nil, details: {})
|
|
10
|
+
raise TypeError, 'details must be a Hash' unless details.is_a?(Hash)
|
|
11
|
+
|
|
12
|
+
bounded_details = details.to_h do |key, value|
|
|
13
|
+
[key, value.is_a?(String) ? value.byteslice(0, 200).scrub('') : value]
|
|
14
|
+
end
|
|
15
|
+
super(code: Model.copy_and_freeze(code), term_id: Model.copy_and_freeze(term_id),
|
|
16
|
+
path: Model.copy_and_freeze(path), message: Model.copy_and_freeze(message),
|
|
17
|
+
details: Model.copy_and_freeze(bounded_details))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model'
|
|
4
|
+
|
|
5
|
+
module Facturx
|
|
6
|
+
Document = Model.define(
|
|
7
|
+
:guideline_urn,
|
|
8
|
+
:business_process,
|
|
9
|
+
:invoice_number,
|
|
10
|
+
:issue_date,
|
|
11
|
+
:type_code,
|
|
12
|
+
:currency,
|
|
13
|
+
:tax_currency,
|
|
14
|
+
:vat_point_date,
|
|
15
|
+
:vat_point_date_code,
|
|
16
|
+
:payment_due_date,
|
|
17
|
+
:buyer_reference,
|
|
18
|
+
:project_reference,
|
|
19
|
+
:contract_reference,
|
|
20
|
+
:purchase_order_reference,
|
|
21
|
+
:sales_order_reference,
|
|
22
|
+
:receiving_advice_reference,
|
|
23
|
+
:despatch_advice_reference,
|
|
24
|
+
:tender_or_lot_reference,
|
|
25
|
+
:invoiced_object_identifier,
|
|
26
|
+
:buyer_accounting_reference,
|
|
27
|
+
:payment_terms,
|
|
28
|
+
:notes,
|
|
29
|
+
:seller,
|
|
30
|
+
:buyer,
|
|
31
|
+
:payee,
|
|
32
|
+
:tax_representative,
|
|
33
|
+
:delivery,
|
|
34
|
+
:billing_period,
|
|
35
|
+
:payment,
|
|
36
|
+
:lines,
|
|
37
|
+
:tax_breakdowns,
|
|
38
|
+
:allowances,
|
|
39
|
+
:charges,
|
|
40
|
+
:totals,
|
|
41
|
+
:preceding_invoices,
|
|
42
|
+
:supporting_documents,
|
|
43
|
+
collections: %i[notes lines tax_breakdowns allowances charges preceding_invoices supporting_documents]
|
|
44
|
+
)
|
|
45
|
+
end
|
data/lib/facturx/error.rb
CHANGED
|
@@ -13,6 +13,9 @@ module Facturx
|
|
|
13
13
|
class InvalidXmlError < Error; end
|
|
14
14
|
class UnknownProfileError < InvalidXmlError; end
|
|
15
15
|
class XsdValidationError < InvalidXmlError; end
|
|
16
|
+
class UnsupportedProfileError < Error; end
|
|
17
|
+
class FormattingError < Error; end
|
|
18
|
+
class ConformanceError < Error; end
|
|
16
19
|
class InvalidPdfError < Error; end
|
|
17
20
|
class ProtectedPdfError < InvalidPdfError; end
|
|
18
21
|
class ComposerUnavailableError < Error; end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
4
|
+
require 'date'
|
|
5
|
+
require_relative 'error'
|
|
6
|
+
|
|
7
|
+
module Facturx
|
|
8
|
+
module Format
|
|
9
|
+
FORMATTERS = {
|
|
10
|
+
string: :string,
|
|
11
|
+
date_102: :formatted_date,
|
|
12
|
+
decimal: :decimal,
|
|
13
|
+
boolean: :boolean,
|
|
14
|
+
binary: :binary
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def call(value, term:)
|
|
20
|
+
formatter = FORMATTERS[term.type]
|
|
21
|
+
raise ArgumentError, "Unsupported format type: #{term.type.inspect}" unless formatter
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
public_send(formatter, value, term.scale)
|
|
25
|
+
rescue ArgumentError, TypeError => e
|
|
26
|
+
raise FormattingError.new(
|
|
27
|
+
'Value cannot be formatted',
|
|
28
|
+
value:, type: term.type, scale: term.scale, term_id: term.id, group_id: term.group_id, path: term.xpath
|
|
29
|
+
), cause: e
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def string(value, _scale = nil)
|
|
34
|
+
raise TypeError, 'expected a String' unless value.is_a?(String)
|
|
35
|
+
raise ArgumentError, 'String must contain a non-whitespace character' if value.match?(/\A[[:space:]]*\z/)
|
|
36
|
+
|
|
37
|
+
value.dup.freeze
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def formatted_date(value, _scale = nil)
|
|
41
|
+
raise TypeError, 'expected a Date' unless value.instance_of?(Date)
|
|
42
|
+
|
|
43
|
+
value.strftime('%Y%m%d').freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def decimal(value, scale = nil)
|
|
47
|
+
raise TypeError, 'expected a BigDecimal or Integer' unless value.is_a?(BigDecimal) || value.is_a?(Integer)
|
|
48
|
+
raise ArgumentError, 'decimal must be finite' if value.is_a?(BigDecimal) && !value.finite?
|
|
49
|
+
|
|
50
|
+
case scale
|
|
51
|
+
when nil then fixed_decimal(value)
|
|
52
|
+
when 2 then fixed_scale(value, 2)
|
|
53
|
+
else raise ArgumentError, "Unsupported decimal scale: #{scale.inspect}"
|
|
54
|
+
end.freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def boolean(value, _scale = nil)
|
|
58
|
+
return 'true' if value.equal?(true)
|
|
59
|
+
return 'false' if value.equal?(false)
|
|
60
|
+
|
|
61
|
+
raise TypeError, 'expected true or false'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def binary(value, _scale = nil)
|
|
65
|
+
raise TypeError, 'expected a binary String' unless value.is_a?(String) && value.encoding == Encoding::BINARY
|
|
66
|
+
|
|
67
|
+
[value].pack('m0').freeze
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def fixed_decimal(value)
|
|
71
|
+
value.is_a?(Integer) ? value.to_s : value.to_s('F')
|
|
72
|
+
end
|
|
73
|
+
private_class_method :fixed_decimal
|
|
74
|
+
|
|
75
|
+
def fixed_scale(value, scale)
|
|
76
|
+
decimal_value = value.is_a?(Integer) ? BigDecimal(value) : value
|
|
77
|
+
rounded = decimal_value.round(scale)
|
|
78
|
+
raise ArgumentError, "decimal has more than #{scale} non-zero fractional digits" unless rounded == decimal_value
|
|
79
|
+
|
|
80
|
+
integer, fraction = rounded.to_s('F').split('.', 2)
|
|
81
|
+
"#{integer}.#{fraction.to_s.ljust(scale, '0')}"
|
|
82
|
+
end
|
|
83
|
+
private_class_method :fixed_scale
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'attach'
|
|
4
|
+
require_relative 'profile_resolver'
|
|
5
|
+
require_relative 'writer'
|
|
6
|
+
|
|
7
|
+
module Facturx
|
|
8
|
+
class Generate
|
|
9
|
+
def initialize(writer: Writer.new, attacher: Attach.new, profile_resolver: ProfileResolver.new)
|
|
10
|
+
@writer = writer
|
|
11
|
+
@attacher = attacher
|
|
12
|
+
@profile_resolver = profile_resolver
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(pdf:, document:, profile:)
|
|
16
|
+
canonical_profile = @profile_resolver.call(profile)
|
|
17
|
+
xml = @writer.call(document:, profile: canonical_profile)
|
|
18
|
+
@attacher.call(pdf:, xml:)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
module Model
|
|
5
|
+
module Immutability
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def copy(value)
|
|
9
|
+
case value
|
|
10
|
+
when Float then raise TypeError, 'Float values are not supported; use BigDecimal for decimal values'
|
|
11
|
+
when String then value.dup.freeze
|
|
12
|
+
when Array then value.map { |item| copy(item) }.freeze
|
|
13
|
+
when Hash then value.to_h { |key, item| [copy(key), copy(item)] }.freeze
|
|
14
|
+
else copy_object(value)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def copy_object(value)
|
|
19
|
+
immutable = value.nil? || value == true || value == false || value.is_a?(Numeric) || value.is_a?(Symbol)
|
|
20
|
+
return value if immutable || value.frozen?
|
|
21
|
+
|
|
22
|
+
value.dup.freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module ValidatedWith
|
|
27
|
+
def with(**attributes)
|
|
28
|
+
self.class.new(**to_h, **attributes)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
MISSING = Object.new.freeze
|
|
33
|
+
private_constant :MISSING
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def define(*members, collections: [])
|
|
38
|
+
collection_members = collections.map(&:to_sym).freeze
|
|
39
|
+
|
|
40
|
+
Data.define(*members) do
|
|
41
|
+
include ValidatedWith
|
|
42
|
+
|
|
43
|
+
define_method(:initialize) do |**attributes|
|
|
44
|
+
normalized = Model.normalize_attributes(self.class.members, collection_members, attributes)
|
|
45
|
+
super(**normalized)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def normalize_attributes(members, collections, attributes)
|
|
51
|
+
validate_attributes!(members, attributes)
|
|
52
|
+
members.to_h do |member|
|
|
53
|
+
value = attributes.fetch(member, MISSING)
|
|
54
|
+
[member, normalize_value(member, value, collections.include?(member))]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate_attributes!(members, attributes)
|
|
59
|
+
unknown = attributes.keys - members
|
|
60
|
+
raise ArgumentError, "Unknown attributes: #{unknown.map(&:inspect).join(', ')}" unless unknown.empty?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def normalize_value(member, value, collection)
|
|
64
|
+
value = nil if value.equal?(MISSING)
|
|
65
|
+
value = [] if collection && value.nil?
|
|
66
|
+
raise TypeError, "#{member} must be an Array" if collection && !value.is_a?(Array)
|
|
67
|
+
|
|
68
|
+
copy_and_freeze(value)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def copy_and_freeze(value)
|
|
72
|
+
Immutability.copy(value)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
ProductAttribute = Model.define(:name, :value)
|
|
5
|
+
ProductClassification = Model.define(:code, :list_id, :list_version_id)
|
|
6
|
+
Product = Model.define(:name, :description, :seller_identifier, :buyer_identifier, :global_identifier,
|
|
7
|
+
:origin_country_code, :attributes, :classifications,
|
|
8
|
+
collections: %i[attributes classifications])
|
|
9
|
+
Price = Model.define(:amount, :basis_quantity, :discount)
|
|
10
|
+
Line = Model.define(:id, :note, :product, :quantity, :gross_price, :net_price, :tax, :period, :net_amount,
|
|
11
|
+
:allowances, :charges, :buyer_order_reference, :buyer_accounting_reference,
|
|
12
|
+
:invoiced_object_identifier,
|
|
13
|
+
collections: %i[allowances charges])
|
|
14
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
Contact = Model.define(:name, :telephone, :email)
|
|
5
|
+
Address = Model.define(:postcode, :line_one, :line_two, :line_three, :city, :country_code,
|
|
6
|
+
:country_subdivision)
|
|
7
|
+
Party = Model.define(:identifiers, :name, :description, :legal_registration, :trading_name, :address,
|
|
8
|
+
:vat_identifier, :tax_identifier, :electronic_address, :contact,
|
|
9
|
+
collections: [:identifiers])
|
|
10
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
CreditTransfer = Model.define(:account_identifier, :account_name, :provider_identifier)
|
|
5
|
+
PaymentCard = Model.define(:primary_account_number, :holder_name)
|
|
6
|
+
DirectDebit = Model.define(:mandate_identifier, :creditor_identifier, :debtor_account_identifier)
|
|
7
|
+
PaymentInstructions = Model.define(:means_code, :means_text, :remittance_information, :credit_transfers,
|
|
8
|
+
:payment_card, :direct_debit, collections: [:credit_transfers])
|
|
9
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
DocumentReference = Model.define(:id, :line_id, :name, :issue_date)
|
|
5
|
+
Delivery = Model.define(:location_identifier, :party, :date)
|
|
6
|
+
SupportingDocument = Model.define(:reference, :description, :external_location, :content, :mime_code, :filename)
|
|
7
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
TaxBreakdown = Model.define(:type_code, :category_code, :rate, :basis_amount, :tax_amount,
|
|
5
|
+
:exemption_reason, :exemption_reason_code, :due_date_type_code)
|
|
6
|
+
AllowanceCharge = Model.define(:indicator, :amount, :base_amount, :percentage, :reason, :reason_code, :tax)
|
|
7
|
+
Totals = Model.define(:line_total, :charge_total, :allowance_total, :tax_basis_total, :tax_total,
|
|
8
|
+
:tax_total_in_tax_currency, :grand_total, :prepaid, :rounding, :due_payable)
|
|
9
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model/immutable'
|
|
4
|
+
require_relative 'model/common'
|
|
5
|
+
require_relative 'model/party'
|
|
6
|
+
require_relative 'model/reference'
|
|
7
|
+
require_relative 'model/payment'
|
|
8
|
+
require_relative 'model/trade'
|
|
9
|
+
require_relative 'model/line'
|
|
@@ -7,6 +7,7 @@ require_relative '../embedding'
|
|
|
7
7
|
module Facturx
|
|
8
8
|
module Pdf
|
|
9
9
|
class Extractor
|
|
10
|
+
ACCEPTED_RELATIONSHIPS = %i[Alternative Data].freeze
|
|
10
11
|
Result = Data.define(:xml, :filename, :relationship, :metadata, :page_count)
|
|
11
12
|
Candidate = Data.define(:file_specification, :names)
|
|
12
13
|
|
|
@@ -31,7 +32,7 @@ module Facturx
|
|
|
31
32
|
|
|
32
33
|
Result.new(xml: embedded_xml(document, file_specification),
|
|
33
34
|
filename: FACTURX_EMBEDDING.filename,
|
|
34
|
-
relationship:
|
|
35
|
+
relationship: file_specification[:AFRelationship],
|
|
35
36
|
metadata: document.metadata,
|
|
36
37
|
page_count: document.page_count)
|
|
37
38
|
end
|
|
@@ -50,7 +51,9 @@ module Facturx
|
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def matching_candidates(candidates)
|
|
53
|
-
candidates
|
|
54
|
+
candidates
|
|
55
|
+
.select { |candidate| candidate.names.include?(FACTURX_EMBEDDING.filename) }
|
|
56
|
+
.uniq { |candidate| candidate.file_specification.object_id }
|
|
54
57
|
end
|
|
55
58
|
|
|
56
59
|
def associated_file_candidates(document)
|
|
@@ -93,9 +96,9 @@ module Facturx
|
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
def validate_relationship!(file_specification)
|
|
96
|
-
return if file_specification[:AFRelationship]
|
|
99
|
+
return if ACCEPTED_RELATIONSHIPS.include?(file_specification[:AFRelationship])
|
|
97
100
|
|
|
98
|
-
raise ExtractionError.new('Factur-X attachment
|
|
101
|
+
raise ExtractionError.new('Factur-X attachment uses an unsupported relationship',
|
|
99
102
|
reason: :invalid_relationship,
|
|
100
103
|
relationship: file_specification[:AFRelationship])
|
|
101
104
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'error'
|
|
4
|
+
require_relative 'profile'
|
|
5
|
+
require_relative 'profiles'
|
|
6
|
+
|
|
7
|
+
module Facturx
|
|
8
|
+
class ProfileResolver
|
|
9
|
+
def initialize(profiles: Profiles)
|
|
10
|
+
@profiles = profiles
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(profile)
|
|
14
|
+
canonical = resolve(profile)
|
|
15
|
+
return canonical unless profile.is_a?(Profile) && !profile.equal?(canonical)
|
|
16
|
+
|
|
17
|
+
unsupported!(profile)
|
|
18
|
+
rescue KeyError
|
|
19
|
+
unsupported!(profile)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def resolve(profile)
|
|
25
|
+
case profile
|
|
26
|
+
when Symbol then @profiles.fetch(profile)
|
|
27
|
+
when Profile
|
|
28
|
+
return unsupported!(profile) unless profile.id.is_a?(Symbol)
|
|
29
|
+
|
|
30
|
+
@profiles.fetch(profile.id)
|
|
31
|
+
else unsupported!(profile)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def unsupported!(profile)
|
|
36
|
+
identifier = profile.respond_to?(:id) ? profile.id : profile
|
|
37
|
+
raise UnsupportedProfileError.new('Factur-X profile is unsupported', profile: identifier)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
class Reader
|
|
5
|
+
class SemanticMapper
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def delivery
|
|
9
|
+
node = first_group('BG-13-00')
|
|
10
|
+
return unless node
|
|
11
|
+
|
|
12
|
+
base = group_xpath('BG-13-00')
|
|
13
|
+
party_node = first_group('BG-13', context: node, base_xpath: base)
|
|
14
|
+
party = delivery_party(party_node) if party_node
|
|
15
|
+
Delivery.new(
|
|
16
|
+
location_identifier: party_node && delivery_identifier(party_node),
|
|
17
|
+
party:,
|
|
18
|
+
date: value('BT-72', context: node, base_xpath: base)
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def delivery_party(node)
|
|
23
|
+
base = group_xpath('BG-13')
|
|
24
|
+
Party.new(
|
|
25
|
+
name: value('BT-70', context: node, base_xpath: base),
|
|
26
|
+
address: address(node, base, 'BG-15')
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def delivery_identifier(node)
|
|
31
|
+
base = group_xpath('BG-13')
|
|
32
|
+
global_id = node.at_xpath('./ram:GlobalID', NAMESPACES)
|
|
33
|
+
@terms.mark(global_id) if global_id
|
|
34
|
+
local_id = value('BT-71', context: node, base_xpath: base)
|
|
35
|
+
return Identifier.new(value: local_id) if local_id
|
|
36
|
+
|
|
37
|
+
return unless global_id
|
|
38
|
+
|
|
39
|
+
Identifier.new(value: global_id.text, scheme_id: global_id['schemeID'])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
class Reader
|
|
5
|
+
class SemanticMapper
|
|
6
|
+
DOCUMENT_REFERENCE_ATTRIBUTES = %i[
|
|
7
|
+
sales_order_reference purchase_order_reference contract_reference receiving_advice_reference
|
|
8
|
+
despatch_advice_reference
|
|
9
|
+
].freeze
|
|
10
|
+
DOCUMENT_COMPOSITE_ATTRIBUTES = [
|
|
11
|
+
*DOCUMENT_REFERENCE_ATTRIBUTES, :tender_or_lot_reference, :invoiced_object_identifier, :project_reference
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def document_attributes(tax_breakdowns)
|
|
17
|
+
scalar_document_attributes(tax_breakdowns).merge(
|
|
18
|
+
reference_document_attributes,
|
|
19
|
+
tender_or_lot_reference: additional_reference(:tender_or_lot_reference, type_code: '50'),
|
|
20
|
+
invoiced_object_identifier: invoiced_object_identifier,
|
|
21
|
+
project_reference: project_reference
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def scalar_document_attributes(tax_breakdowns)
|
|
26
|
+
attributes = [nil, 'BG-2', 'BG-19'].each_with_object({}) do |group_id, result|
|
|
27
|
+
result.merge!(scalar_attributes(:document, group_id, except: DOCUMENT_COMPOSITE_ATTRIBUTES))
|
|
28
|
+
end
|
|
29
|
+
attributes.merge(vat_point_date: tax_breakdowns.vat_point_date,
|
|
30
|
+
vat_point_date_code: tax_breakdowns.items.find(&:due_date_type_code)&.due_date_type_code)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reference_document_attributes
|
|
34
|
+
DOCUMENT_REFERENCE_ATTRIBUTES.to_h do |attribute|
|
|
35
|
+
[attribute, reference(term_for_attribute(:document, attribute)&.id)]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def notes
|
|
40
|
+
group_nodes('BG-1').map do |node|
|
|
41
|
+
Note.new(**scalar_attributes(:note, 'BG-1', context: node, base_xpath: group_xpath('BG-1')))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def preceding_invoices
|
|
46
|
+
group_nodes('BG-3').map do |node|
|
|
47
|
+
base = group_xpath('BG-3')
|
|
48
|
+
DocumentReference.new(**scalar_attributes(:document_reference, 'BG-3', context: node, base_xpath: base))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def supporting_documents
|
|
53
|
+
group_nodes('BG-24') { |node| supporting_document?(node) }.map do |node|
|
|
54
|
+
base = group_xpath('BG-24')
|
|
55
|
+
SupportingDocument.new(**supporting_document_attributes(node, base))
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def supporting_document_attributes(node, base)
|
|
60
|
+
binary = %i[content mime_code filename]
|
|
61
|
+
attributes = scalar_attributes(:supporting_document, 'BG-24', context: node, base_xpath: base, except: binary)
|
|
62
|
+
attributes.merge!(embedded_document_attributes(node, base)) if node.at_xpath('./ram:AttachmentBinaryObject',
|
|
63
|
+
NAMESPACES)
|
|
64
|
+
attributes
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def embedded_document_attributes(node, base)
|
|
68
|
+
scalar_attributes(:supporting_document, 'BG-24', context: node, base_xpath: base,
|
|
69
|
+
only: %i[content mime_code filename])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def supporting_document?(node)
|
|
73
|
+
technical_value('./ram:TypeCode', node) == '916'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def additional_reference(attribute, type_code:)
|
|
77
|
+
node = @document.at_xpath("#{group_xpath('BG-24')}[ram:TypeCode='#{type_code}']", NAMESPACES)
|
|
78
|
+
return unless node
|
|
79
|
+
|
|
80
|
+
technical_value('./ram:TypeCode', node)
|
|
81
|
+
term = term_for_attribute(:document, attribute)
|
|
82
|
+
reference(term&.id, context: node, base_xpath: group_xpath('BG-24'))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def invoiced_object_identifier
|
|
86
|
+
node = @document.at_xpath("#{group_xpath('BG-24')}[ram:TypeCode='130']", NAMESPACES)
|
|
87
|
+
return unless node
|
|
88
|
+
|
|
89
|
+
technical_value('./ram:TypeCode', node)
|
|
90
|
+
value_term = term_for_attribute(:document, :invoiced_object_identifier, xpath_suffix: '/ram:IssuerAssignedID')
|
|
91
|
+
scheme_term = term_for_attribute(:document, :invoiced_object_identifier, xpath_suffix: '/ram:ReferenceTypeCode')
|
|
92
|
+
identifier(value_term&.id, scheme_term&.id, context: node, base_xpath: group_xpath('BG-24'))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def project_reference
|
|
96
|
+
node = @document.at_xpath(
|
|
97
|
+
'/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/' \
|
|
98
|
+
'ram:ApplicableHeaderTradeAgreement/ram:SpecifiedProcuringProject',
|
|
99
|
+
NAMESPACES
|
|
100
|
+
)
|
|
101
|
+
return unless node
|
|
102
|
+
|
|
103
|
+
id = value(term_for_attribute(:document, :project_reference)&.id)
|
|
104
|
+
name = technical_value('./ram:Name', node)
|
|
105
|
+
DocumentReference.new(id:, name:)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|