facturx 0.2.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 +21 -3
- data/README.md +40 -8
- 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/conformance.rb +163 -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/profile_resolver.rb +40 -0
- data/lib/facturx/reader/semantic_mapper/line_mapping.rb +1 -1
- data/lib/facturx/reader/semantic_mapper/line_references.rb +19 -0
- data/lib/facturx/reader/semantic_mapper/payment_mapping.rb +48 -11
- data/lib/facturx/reader/semantic_mapper.rb +9 -0
- data/lib/facturx/reader/term_reader.rb +12 -4
- data/lib/facturx/reader.rb +3 -7
- 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 +28 -0
- data/rbi/facturx.rbi +1090 -0
- metadata +38 -4
|
@@ -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,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
|
|
@@ -34,7 +34,7 @@ module Facturx
|
|
|
34
34
|
def line_reference_attributes(node, base)
|
|
35
35
|
{
|
|
36
36
|
buyer_order_reference: reference('BT-132', context: node, base_xpath: base, field: :line_id),
|
|
37
|
-
invoiced_object_identifier:
|
|
37
|
+
invoiced_object_identifier: line_invoiced_object_identifier(node, base)
|
|
38
38
|
}
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
class Reader
|
|
5
|
+
class SemanticMapper
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def line_invoiced_object_identifier(parent, parent_base)
|
|
9
|
+
relative_path = './ram:SpecifiedLineTradeSettlement/ram:AdditionalReferencedDocument'
|
|
10
|
+
node = parent.at_xpath("#{relative_path}[ram:TypeCode='130']", NAMESPACES)
|
|
11
|
+
return unless node
|
|
12
|
+
|
|
13
|
+
technical_value('./ram:TypeCode', node)
|
|
14
|
+
base = "#{parent_base}/ram:SpecifiedLineTradeSettlement/ram:AdditionalReferencedDocument"
|
|
15
|
+
identifier('BT-128', 'BT-128-1', context: node, base_xpath: base)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -6,10 +6,13 @@ module Facturx
|
|
|
6
6
|
private
|
|
7
7
|
|
|
8
8
|
def payment
|
|
9
|
-
nodes = group_nodes('BG-16')
|
|
9
|
+
nodes = group_nodes('BG-16', collapse_if: method(:projected_credit_transfers?))
|
|
10
10
|
base = group_xpath('BG-16')
|
|
11
11
|
attributes = settlement_payment_attributes(nodes.first, base)
|
|
12
|
-
|
|
12
|
+
if nodes.any?
|
|
13
|
+
attributes.merge!(payment_means_attributes(nodes, base,
|
|
14
|
+
projected: projected_credit_transfers?(nodes)))
|
|
15
|
+
end
|
|
13
16
|
return if attributes.empty?
|
|
14
17
|
|
|
15
18
|
PaymentInstructions.new(**attributes)
|
|
@@ -22,18 +25,52 @@ module Facturx
|
|
|
22
25
|
attributes.compact
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
def payment_means_attributes(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
def payment_means_attributes(nodes, base, projected:)
|
|
29
|
+
attributes = scalar_attributes(:payment_instructions, 'BG-16', context: nodes.first, base_xpath: base)
|
|
30
|
+
payment_nodes = projected ? nodes : [nodes.first]
|
|
31
|
+
mark_repeated_payment_attributes(payment_nodes.drop(1), base) if projected
|
|
32
|
+
attributes.merge(
|
|
33
|
+
credit_transfers: credit_transfers(payment_nodes, base),
|
|
34
|
+
payment_card: payment_nodes.filter_map { |node| payment_card(node, base) }.first
|
|
31
35
|
)
|
|
32
36
|
end
|
|
33
37
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
def mark_repeated_payment_attributes(nodes, base)
|
|
39
|
+
%w[BT-81 BT-82].each do |term_id|
|
|
40
|
+
nodes.each { |node| @terms.mark_value(term_id, context: node, base_xpath: base) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def projected_credit_transfers?(nodes)
|
|
45
|
+
return false unless nodes.size > 1
|
|
46
|
+
return false unless nodes.all? { |node| payment_account_count(node) == 1 }
|
|
47
|
+
|
|
48
|
+
repeated_payment_attributes?(nodes) && nodes.drop(1).all? { |node| supplemental_payment_node?(node) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def repeated_payment_attributes?(nodes)
|
|
52
|
+
%w[ram:TypeCode ram:Information].all? do |xpath|
|
|
53
|
+
nodes.map { |node| node.at_xpath("./#{xpath}", NAMESPACES)&.text }.uniq.one?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def supplemental_payment_node?(node)
|
|
58
|
+
allowed = %w[TypeCode Information PayeePartyCreditorFinancialAccount
|
|
59
|
+
PayeeSpecifiedCreditorFinancialInstitution]
|
|
60
|
+
node.element_children.all? { |child| allowed.include?(child.name) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def payment_account_count(node)
|
|
64
|
+
node.xpath('./ram:PayeePartyCreditorFinancialAccount', NAMESPACES).size
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def credit_transfers(parents, parent_base)
|
|
68
|
+
provider = term_for(:credit_transfer, 'BG-16', :provider_identifier)
|
|
69
|
+
parents.flat_map do |parent|
|
|
70
|
+
provider_identifier = identifier(provider&.id, context: parent, base_xpath: parent_base)
|
|
71
|
+
group_nodes('BG-17', context: parent, base_xpath: parent_base).map do |node|
|
|
72
|
+
credit_transfer(node, provider_identifier)
|
|
73
|
+
end
|
|
37
74
|
end
|
|
38
75
|
end
|
|
39
76
|
|
|
@@ -21,6 +21,7 @@ module Facturx
|
|
|
21
21
|
attributes = document_attributes(tax_breakdowns).merge(
|
|
22
22
|
party_attributes, transaction_attributes, collection_attributes(tax_breakdowns.items)
|
|
23
23
|
)
|
|
24
|
+
attributes[:tax_currency] ||= minimum_tax_currency(attributes[:currency])
|
|
24
25
|
attributes[:totals] = totals(attributes[:currency], attributes[:tax_currency])
|
|
25
26
|
Document.new(**attributes)
|
|
26
27
|
end
|
|
@@ -31,6 +32,13 @@ module Facturx
|
|
|
31
32
|
|
|
32
33
|
private
|
|
33
34
|
|
|
35
|
+
def minimum_tax_currency(currency)
|
|
36
|
+
return unless @profile.id == :minimum
|
|
37
|
+
|
|
38
|
+
nodes = @document.xpath("#{group_xpath('BG-22')}/ram:TaxTotalAmount", NAMESPACES)
|
|
39
|
+
nodes.filter_map { |node| node['currencyID'] }.find { |value| value != currency }
|
|
40
|
+
end
|
|
41
|
+
|
|
34
42
|
def party_attributes
|
|
35
43
|
{
|
|
36
44
|
seller: party('BG-4', contact_group: 'BG-6', address_group: 'BG-5'),
|
|
@@ -63,5 +71,6 @@ require_relative 'semantic_mapper/party_mapping'
|
|
|
63
71
|
require_relative 'semantic_mapper/delivery_mapping'
|
|
64
72
|
require_relative 'semantic_mapper/payment_mapping'
|
|
65
73
|
require_relative 'semantic_mapper/line_mapping'
|
|
74
|
+
require_relative 'semantic_mapper/line_references'
|
|
66
75
|
require_relative 'semantic_mapper/trade_mapping'
|
|
67
76
|
require_relative 'semantic_mapper/helpers'
|
|
@@ -28,14 +28,13 @@ module Facturx
|
|
|
28
28
|
materialize(term, selected, cardinality)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def group_nodes(id, context: @document, base_xpath: nil, &)
|
|
31
|
+
def group_nodes(id, context: @document, base_xpath: nil, collapse_if: nil, &)
|
|
32
32
|
group = active_group(id)
|
|
33
33
|
return [].freeze unless group
|
|
34
34
|
|
|
35
|
-
nodes =
|
|
36
|
-
nodes.select!(&) if block_given?
|
|
35
|
+
nodes = select_group_nodes(group, context, base_xpath, &)
|
|
37
36
|
cardinality = group.cardinalities.fetch(@profile.id)
|
|
38
|
-
diagnose_cardinality(group, nodes, cardinality)
|
|
37
|
+
diagnose_cardinality(group, cardinality_nodes(nodes, collapse_if), cardinality)
|
|
39
38
|
nodes.each { |node| mark(node) }
|
|
40
39
|
nodes.freeze
|
|
41
40
|
end
|
|
@@ -71,6 +70,15 @@ module Facturx
|
|
|
71
70
|
|
|
72
71
|
private
|
|
73
72
|
|
|
73
|
+
def select_group_nodes(group, context, base_xpath, &filter)
|
|
74
|
+
nodes = context.xpath(relative_xpath(group.xpath, base_xpath), NAMESPACES).to_a
|
|
75
|
+
filter ? nodes.select(&filter) : nodes
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def cardinality_nodes(nodes, collapse_if)
|
|
79
|
+
collapse_if&.call(nodes) ? [nodes.first] : nodes
|
|
80
|
+
end
|
|
81
|
+
|
|
74
82
|
def active_term(id)
|
|
75
83
|
term = @registry.fetch(id)
|
|
76
84
|
term if term.cardinalities.key?(@profile.id)
|
data/lib/facturx/reader.rb
CHANGED
|
@@ -9,17 +9,13 @@ require_relative 'reading'
|
|
|
9
9
|
require_relative 'source_reader'
|
|
10
10
|
require_relative 'terms'
|
|
11
11
|
require_relative 'xml/parser'
|
|
12
|
+
require_relative 'xml/namespaces'
|
|
12
13
|
require_relative 'xml/profile_detector'
|
|
13
14
|
|
|
14
15
|
module Facturx
|
|
15
16
|
class Reader
|
|
16
|
-
CII_NAMESPACE =
|
|
17
|
-
NAMESPACES =
|
|
18
|
-
'qdt' => 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100',
|
|
19
|
-
'ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
|
|
20
|
-
'rsm' => CII_NAMESPACE,
|
|
21
|
-
'udt' => 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'
|
|
22
|
-
}.freeze
|
|
17
|
+
CII_NAMESPACE = Xml::Namespaces::CII
|
|
18
|
+
NAMESPACES = Xml::Namespaces::MAP
|
|
23
19
|
UNKNOWN_PROFILE_POLICIES = %i[fallback raise].freeze
|
|
24
20
|
|
|
25
21
|
COLLABORATORS = %i[profile_resolver registry coercer profiles].freeze
|
data/lib/facturx/version.rb
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'nokogiri'
|
|
4
|
+
require_relative '../xml/namespaces'
|
|
5
|
+
|
|
6
|
+
module Facturx
|
|
7
|
+
class Writer
|
|
8
|
+
class Context
|
|
9
|
+
attr_reader :document, :profile, :tracker, :xml, :root
|
|
10
|
+
|
|
11
|
+
def initialize(document:, profile:, tracker:)
|
|
12
|
+
@document = document
|
|
13
|
+
@profile = profile
|
|
14
|
+
@tracker = tracker
|
|
15
|
+
@xml = Nokogiri::XML::Document.new
|
|
16
|
+
@xml.encoding = 'UTF-8'
|
|
17
|
+
@root = namespaced_node('rsm', 'CrossIndustryInvoice')
|
|
18
|
+
Xml::Namespaces::MAP.each { |prefix, uri| @root.add_namespace_definition(prefix, uri) }
|
|
19
|
+
@root.namespace = namespace('rsm')
|
|
20
|
+
@xml.root = @root
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def transaction
|
|
24
|
+
@transaction ||= element(@root, 'rsm:SupplyChainTradeTransaction')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def element(parent, qualified_name, text: nil, attributes: {})
|
|
28
|
+
prefix, name = qualified_name.split(':', 2)
|
|
29
|
+
node = namespaced_node(prefix, name)
|
|
30
|
+
attributes.each { |attribute, value| node[attribute.to_s] = value }
|
|
31
|
+
node.content = text unless text.nil?
|
|
32
|
+
parent.add_child(node)
|
|
33
|
+
node
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_xml
|
|
37
|
+
@xml.to_xml(encoding: 'UTF-8')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def namespaced_node(prefix, name)
|
|
43
|
+
node = Nokogiri::XML::Node.new(name, @xml)
|
|
44
|
+
node.namespace = namespace(prefix) if @root
|
|
45
|
+
node
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def namespace(prefix)
|
|
49
|
+
@root&.namespace_definitions&.find { |item| item.prefix == prefix }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../error'
|
|
4
|
+
require_relative '../format'
|
|
5
|
+
require_relative '../terms'
|
|
6
|
+
require_relative 'stage_support/contact_details'
|
|
7
|
+
require_relative 'stage_support/emission'
|
|
8
|
+
require_relative 'stage_support/identifiers'
|
|
9
|
+
require_relative 'stage_support/structure'
|
|
10
|
+
|
|
11
|
+
module Facturx
|
|
12
|
+
class Writer
|
|
13
|
+
class Stage
|
|
14
|
+
include StageSupport::Emission
|
|
15
|
+
include StageSupport::Structure
|
|
16
|
+
include StageSupport::ContactDetails
|
|
17
|
+
include StageSupport::Identifiers
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def term_ids(*ids)
|
|
21
|
+
@term_ids = ids.map(&:to_s).freeze unless ids.empty?
|
|
22
|
+
@term_ids || [].freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(context)
|
|
27
|
+
@context = context
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :context
|
|
33
|
+
|
|
34
|
+
def document
|
|
35
|
+
context.document
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def profile
|
|
39
|
+
context.profile
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def tracker
|
|
43
|
+
context.tracker
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
class Writer
|
|
5
|
+
module StageSupport
|
|
6
|
+
module ContactDetails
|
|
7
|
+
ADDRESS_ELEMENTS = {
|
|
8
|
+
postcode: 'ram:PostcodeCode',
|
|
9
|
+
line_one: 'ram:LineOne',
|
|
10
|
+
line_two: 'ram:LineTwo',
|
|
11
|
+
line_three: 'ram:LineThree',
|
|
12
|
+
city: 'ram:CityName',
|
|
13
|
+
country_code: 'ram:CountryID',
|
|
14
|
+
country_subdivision: 'ram:CountrySubDivisionName'
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def emit_date(parent, id, value, wrapper:, value_element: 'udt:DateTimeString')
|
|
20
|
+
unless value
|
|
21
|
+
emit(id, nil, element: value_element, parent:)
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
container(parent, wrapper) do |node|
|
|
26
|
+
emit(id, value, element: value_element, parent: node, attributes: { 'format' => '102' })
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def emit_address(parent, address, group_id, ids)
|
|
31
|
+
within_group(group_id, address, element: 'ram:PostalTradeAddress', parent:) do |node, item|
|
|
32
|
+
ADDRESS_ELEMENTS.each do |attribute, element|
|
|
33
|
+
emit(ids.fetch(attribute), item.public_send(attribute), element:, parent: node)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def emit_contact(parent, contact, group_id, ids)
|
|
39
|
+
within_group(group_id, contact, element: 'ram:DefinedTradeContact', parent:) do |node, item|
|
|
40
|
+
emit(ids.fetch(:name), item.name, element: 'ram:PersonName', parent: node)
|
|
41
|
+
communication(node, ids.fetch(:telephone), item.telephone,
|
|
42
|
+
'ram:TelephoneUniversalCommunication', 'ram:CompleteNumber')
|
|
43
|
+
communication(node, ids.fetch(:email), item.email, 'ram:EmailURIUniversalCommunication', 'ram:URIID')
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def communication(parent, id, value, wrapper, element)
|
|
48
|
+
if value
|
|
49
|
+
container(parent, wrapper) { |node| emit(id, value, element:, parent: node) }
|
|
50
|
+
else
|
|
51
|
+
emit(id, nil, element:, parent:)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
class Writer
|
|
5
|
+
module StageSupport
|
|
6
|
+
module Emission
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def emit(id, value, **options)
|
|
10
|
+
term = Terms.fetch(id)
|
|
11
|
+
values = present_values(value)
|
|
12
|
+
return [] unless options.fetch(:group_present, true)
|
|
13
|
+
|
|
14
|
+
accepted = tracker.observe_term?(term, values:, path: term.xpath)
|
|
15
|
+
return [] unless accepted
|
|
16
|
+
|
|
17
|
+
values.filter_map { |item| emit_value(term, item, options) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def emit_attribute(id, value, **options)
|
|
21
|
+
term = Terms.fetch(id)
|
|
22
|
+
values = present_values(value)
|
|
23
|
+
group_present = options.fetch(:group_present, !options.fetch(:node).nil?)
|
|
24
|
+
return unrepresentable(id, 'Qualifier requires its carrier') if !group_present && values.any?
|
|
25
|
+
return unless group_present
|
|
26
|
+
|
|
27
|
+
accepted = tracker.observe_term?(term, values:, path: term.xpath)
|
|
28
|
+
return unless accepted
|
|
29
|
+
|
|
30
|
+
emit_attribute_value(term, values, options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def observe?(id, value, group_present: true)
|
|
34
|
+
return false unless group_present
|
|
35
|
+
|
|
36
|
+
term = Terms.fetch(id)
|
|
37
|
+
tracker.observe_term?(term, values: present_values(value), path: term.xpath)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def unrepresentable(id, message)
|
|
41
|
+
term = Terms.fetch(id)
|
|
42
|
+
tracker.invalid_term(term, error: FormattingError.new(message, path: term.xpath), path: term.xpath)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def present_values(value)
|
|
46
|
+
(value.is_a?(Array) ? value : [value]).compact
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def format(value, term)
|
|
50
|
+
Format.call(value, term:)
|
|
51
|
+
rescue FormattingError => e
|
|
52
|
+
tracker.invalid_term(term, error: e, path: term.xpath)
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def emit_value(term, value, options)
|
|
57
|
+
formatted = format(value, term)
|
|
58
|
+
return unless formatted
|
|
59
|
+
|
|
60
|
+
context.element(
|
|
61
|
+
options.fetch(:parent),
|
|
62
|
+
options.fetch(:element),
|
|
63
|
+
text: formatted,
|
|
64
|
+
attributes: options.fetch(:attributes, {})
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def emit_attribute_value(term, values, options)
|
|
69
|
+
node = options.fetch(:node)
|
|
70
|
+
return unless node && values.one?
|
|
71
|
+
|
|
72
|
+
formatted = format(values.first, term)
|
|
73
|
+
node[options.fetch(:attribute)] = formatted if formatted
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Facturx
|
|
4
|
+
class Writer
|
|
5
|
+
module StageSupport
|
|
6
|
+
module Identifiers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def emit_compound_identifier(parent, identifier, **options)
|
|
10
|
+
value_id = options.fetch(:value_id)
|
|
11
|
+
value = identifier&.value
|
|
12
|
+
node = emit(value_id, value, element: options.fetch(:element), parent:).first
|
|
13
|
+
emit_identifier_scheme(node, identifier, options)
|
|
14
|
+
node
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def emit_party_identifiers(parent, identifiers, value_id, scheme_id)
|
|
18
|
+
items = Array(identifiers)
|
|
19
|
+
local, global = items.partition { |item| item.scheme_id.nil? }
|
|
20
|
+
missing, global = global.partition { |item| item.value.to_s.strip.empty? }
|
|
21
|
+
emit_global_identifiers(parent, missing, value_id, scheme_id)
|
|
22
|
+
|
|
23
|
+
return unless observe?(value_id, items.map(&:value))
|
|
24
|
+
|
|
25
|
+
emit_local_identifiers(parent, local, value_id)
|
|
26
|
+
emit_global_identifiers(parent, global, value_id, scheme_id)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def format_identifier_value(id, value)
|
|
30
|
+
return if value.nil?
|
|
31
|
+
|
|
32
|
+
format(value, Terms.fetch(id))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def emit_tax_registration(parent, id, identifier, scheme)
|
|
36
|
+
report_unrepresentable_identifier_scheme(id, identifier, represented_scheme: scheme)
|
|
37
|
+
value = identifier&.value
|
|
38
|
+
unless value
|
|
39
|
+
observe?(id, nil)
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
container(parent, 'ram:SpecifiedTaxRegistration') do |node|
|
|
44
|
+
emit(id, value, element: 'ram:ID', parent: node, attributes: { 'schemeID' => scheme })
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def emit_document_reference(parent, id, reference, element, represented_attributes: [])
|
|
49
|
+
unless reference
|
|
50
|
+
observe?(id, nil)
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
represented = [:id, *represented_attributes]
|
|
55
|
+
report_unrepresentable_reference_attributes(id, reference, represented_attributes: represented)
|
|
56
|
+
|
|
57
|
+
container(parent, element) do |node|
|
|
58
|
+
emit(id, reference.id, element: 'ram:IssuerAssignedID', parent: node)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def emit_invoiced_object(parent, identifier, value_id, scheme_id)
|
|
63
|
+
unless identifier&.value
|
|
64
|
+
observe?(value_id, nil)
|
|
65
|
+
report_unrepresentable_identifier_scheme(scheme_id, identifier)
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
container(parent, 'ram:AdditionalReferencedDocument') do |node|
|
|
70
|
+
emit(value_id, identifier.value, element: 'ram:IssuerAssignedID', parent: node)
|
|
71
|
+
technical(node, 'ram:TypeCode', '130')
|
|
72
|
+
emit(scheme_id, identifier.scheme_id, element: 'ram:ReferenceTypeCode', parent: node)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def report_unrepresentable_reference_attributes(id, reference, represented_attributes: [])
|
|
77
|
+
%i[id line_id name issue_date].each do |attribute|
|
|
78
|
+
next unless reference.public_send(attribute) && !represented_attributes.include?(attribute)
|
|
79
|
+
|
|
80
|
+
unrepresentable(id, "Document reference #{attribute} cannot be represented")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def emit_identifier_value(id, identifier, element, parent)
|
|
85
|
+
report_unrepresentable_identifier_scheme(id, identifier)
|
|
86
|
+
emit(id, identifier&.value, element:, parent:)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def report_unrepresentable_identifier_scheme(id, identifier, represented_scheme: nil)
|
|
90
|
+
scheme_id = identifier&.scheme_id
|
|
91
|
+
return unless scheme_id && scheme_id != represented_scheme
|
|
92
|
+
|
|
93
|
+
unrepresentable(id, 'Identifier scheme cannot be represented')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def emit_identifier_scheme(node, identifier, options)
|
|
97
|
+
scheme_id = options[:scheme_id]
|
|
98
|
+
return unless scheme_id
|
|
99
|
+
|
|
100
|
+
emit_attribute(
|
|
101
|
+
scheme_id,
|
|
102
|
+
identifier&.scheme_id,
|
|
103
|
+
node:,
|
|
104
|
+
attribute: options.fetch(:scheme_attribute, 'schemeID'),
|
|
105
|
+
group_present: !node.nil?
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def emit_local_identifiers(parent, identifiers, value_id)
|
|
110
|
+
identifiers.each do |item|
|
|
111
|
+
technical(parent, 'ram:ID', format_identifier_value(value_id, item.value))
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def emit_global_identifiers(parent, identifiers, value_id, scheme_id)
|
|
116
|
+
identifiers.each do |item|
|
|
117
|
+
if item.value.to_s.strip.empty?
|
|
118
|
+
unrepresentable(value_id, 'Global identifier requires a value')
|
|
119
|
+
next
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
node = technical(parent, 'ram:GlobalID', format_identifier_value(value_id, item.value))
|
|
123
|
+
emit_attribute(scheme_id, item.scheme_id, node:, attribute: 'schemeID', group_present: true)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|