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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -3
  3. data/README.md +40 -8
  4. data/lib/facturx/builders/association.rb +8 -0
  5. data/lib/facturx/builders/base.rb +129 -0
  6. data/lib/facturx/builders/schema.rb +112 -0
  7. data/lib/facturx/builders.rb +25 -0
  8. data/lib/facturx/conformance.rb +163 -0
  9. data/lib/facturx/error.rb +3 -0
  10. data/lib/facturx/format.rb +85 -0
  11. data/lib/facturx/generate.rb +21 -0
  12. data/lib/facturx/profile_resolver.rb +40 -0
  13. data/lib/facturx/reader/semantic_mapper/line_mapping.rb +1 -1
  14. data/lib/facturx/reader/semantic_mapper/line_references.rb +19 -0
  15. data/lib/facturx/reader/semantic_mapper/payment_mapping.rb +48 -11
  16. data/lib/facturx/reader/semantic_mapper.rb +9 -0
  17. data/lib/facturx/reader/term_reader.rb +12 -4
  18. data/lib/facturx/reader.rb +3 -7
  19. data/lib/facturx/version.rb +1 -1
  20. data/lib/facturx/writer/context.rb +53 -0
  21. data/lib/facturx/writer/stage.rb +47 -0
  22. data/lib/facturx/writer/stage_support/contact_details.rb +57 -0
  23. data/lib/facturx/writer/stage_support/emission.rb +78 -0
  24. data/lib/facturx/writer/stage_support/identifiers.rb +129 -0
  25. data/lib/facturx/writer/stage_support/structure.rb +88 -0
  26. data/lib/facturx/writer/stages/document_context.rb +31 -0
  27. data/lib/facturx/writer/stages/exchanged_document.rb +32 -0
  28. data/lib/facturx/writer/stages/trade_agreement/parties.rb +105 -0
  29. data/lib/facturx/writer/stages/trade_agreement/references.rb +92 -0
  30. data/lib/facturx/writer/stages/trade_agreement.rb +45 -0
  31. data/lib/facturx/writer/stages/trade_delivery.rb +77 -0
  32. data/lib/facturx/writer/stages/trade_lines/agreement.rb +92 -0
  33. data/lib/facturx/writer/stages/trade_lines/product.rb +78 -0
  34. data/lib/facturx/writer/stages/trade_lines/quantity.rb +17 -0
  35. data/lib/facturx/writer/stages/trade_lines/settlement.rb +89 -0
  36. data/lib/facturx/writer/stages/trade_lines.rb +67 -0
  37. data/lib/facturx/writer/stages/trade_settlement/parties.rb +33 -0
  38. data/lib/facturx/writer/stages/trade_settlement/payment.rb +97 -0
  39. data/lib/facturx/writer/stages/trade_settlement/summary.rb +88 -0
  40. data/lib/facturx/writer/stages/trade_settlement/taxes.rb +121 -0
  41. data/lib/facturx/writer/stages/trade_settlement.rb +70 -0
  42. data/lib/facturx/writer.rb +68 -0
  43. data/lib/facturx/xml/namespaces.rb +19 -0
  44. data/lib/facturx/xml/profile_detector.rb +2 -4
  45. data/lib/facturx.rb +28 -0
  46. data/rbi/facturx.rbi +1090 -0
  47. 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: identifier('BT-128', 'BT-128-1', context: node, base_xpath: base)
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
- attributes.merge!(payment_means_attributes(nodes.first, base)) if nodes.first
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(node, base)
26
- provider = term_for(:credit_transfer, 'BG-16', :provider_identifier)
27
- provider_identifier = identifier(provider&.id, context: node, base_xpath: base)
28
- scalar_attributes(:payment_instructions, 'BG-16', context: node, base_xpath: base).merge(
29
- credit_transfers: credit_transfers(node, base, provider_identifier),
30
- payment_card: payment_card(node, base)
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 credit_transfers(parent, parent_base, provider_identifier)
35
- group_nodes('BG-17', context: parent, base_xpath: parent_base).map do |node|
36
- credit_transfer(node, provider_identifier)
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 = context.xpath(relative_xpath(group.xpath, base_xpath), NAMESPACES).to_a
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)
@@ -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 = 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Facturx
4
- VERSION = '0.2.0'
4
+ VERSION = '1.0.0'
5
5
  end
@@ -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