facturx 0.2.0 → 1.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -3
  3. data/DOCUMENTATION.md +271 -0
  4. data/README.md +79 -69
  5. data/lib/facturx/attach.rb +4 -20
  6. data/lib/facturx/builders/association.rb +8 -0
  7. data/lib/facturx/builders/base.rb +129 -0
  8. data/lib/facturx/builders/schema.rb +112 -0
  9. data/lib/facturx/builders.rb +25 -0
  10. data/lib/facturx/error.rb +7 -1
  11. data/lib/facturx/format.rb +85 -0
  12. data/lib/facturx/generate.rb +21 -0
  13. data/lib/facturx/pdf/composer.rb +32 -0
  14. data/lib/facturx/profile.rb +1 -1
  15. data/lib/facturx/profile_resolver.rb +40 -0
  16. data/lib/facturx/profiles.rb +1 -8
  17. data/lib/facturx/reader/semantic_mapper/line_mapping.rb +1 -1
  18. data/lib/facturx/reader/semantic_mapper/line_references.rb +19 -0
  19. data/lib/facturx/reader/semantic_mapper/payment_mapping.rb +48 -11
  20. data/lib/facturx/reader/semantic_mapper.rb +9 -0
  21. data/lib/facturx/reader/term_reader.rb +12 -4
  22. data/lib/facturx/reader.rb +3 -7
  23. data/lib/facturx/source_reader.rb +1 -1
  24. data/lib/facturx/validation.rb +61 -0
  25. data/lib/facturx/version.rb +1 -1
  26. data/lib/facturx/writer/context.rb +53 -0
  27. data/lib/facturx/writer/stage.rb +47 -0
  28. data/lib/facturx/writer/stage_support/contact_details.rb +57 -0
  29. data/lib/facturx/writer/stage_support/emission.rb +78 -0
  30. data/lib/facturx/writer/stage_support/identifiers.rb +129 -0
  31. data/lib/facturx/writer/stage_support/structure.rb +88 -0
  32. data/lib/facturx/writer/stages/document_context.rb +31 -0
  33. data/lib/facturx/writer/stages/exchanged_document.rb +32 -0
  34. data/lib/facturx/writer/stages/trade_agreement/parties.rb +105 -0
  35. data/lib/facturx/writer/stages/trade_agreement/references.rb +92 -0
  36. data/lib/facturx/writer/stages/trade_agreement.rb +45 -0
  37. data/lib/facturx/writer/stages/trade_delivery.rb +77 -0
  38. data/lib/facturx/writer/stages/trade_lines/agreement.rb +92 -0
  39. data/lib/facturx/writer/stages/trade_lines/product.rb +78 -0
  40. data/lib/facturx/writer/stages/trade_lines/quantity.rb +17 -0
  41. data/lib/facturx/writer/stages/trade_lines/settlement.rb +89 -0
  42. data/lib/facturx/writer/stages/trade_lines.rb +67 -0
  43. data/lib/facturx/writer/stages/trade_settlement/parties.rb +33 -0
  44. data/lib/facturx/writer/stages/trade_settlement/payment.rb +97 -0
  45. data/lib/facturx/writer/stages/trade_settlement/summary.rb +88 -0
  46. data/lib/facturx/writer/stages/trade_settlement/taxes.rb +121 -0
  47. data/lib/facturx/writer/stages/trade_settlement.rb +70 -0
  48. data/lib/facturx/writer/tracker.rb +131 -0
  49. data/lib/facturx/writer.rb +76 -0
  50. data/lib/facturx/xml/namespaces.rb +19 -0
  51. data/lib/facturx/xml/parser.rb +1 -1
  52. data/lib/facturx/xml/profile_detector.rb +2 -4
  53. data/lib/facturx/xml/report_builder.rb +44 -0
  54. data/lib/facturx/xml/schema_registry.rb +22 -0
  55. data/lib/facturx/xml/schema_validator.rb +7 -2
  56. data/lib/facturx/xml/validator.rb +32 -0
  57. data/lib/facturx/xml/verifier.rb +16 -11
  58. data/lib/facturx.rb +41 -4
  59. data/rbi/facturx.rbi +1755 -0
  60. metadata +44 -4
@@ -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
@@ -13,7 +13,7 @@ module Facturx
13
13
 
14
14
  def call(source)
15
15
  unless source.is_a?(String)
16
- raise InvalidXmlError.new('Source must be provided as a byte String', input_class: source.class.name)
16
+ raise InvalidSourceError.new('Source must be provided as a byte String', input_class: source.class.name)
17
17
  end
18
18
 
19
19
  return Result.new(xml: source, source_type: :xml) unless pdf?(source)
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error'
4
+ require_relative 'model/immutable'
5
+
6
+ module Facturx
7
+ module Validation
8
+ LAYERS = %i[syntax profile document xsd].freeze
9
+ SEVERITIES = %i[error warning].freeze
10
+ ISSUE_DEFAULTS = {
11
+ severity: :error,
12
+ term_id: nil,
13
+ group_id: nil,
14
+ path: nil,
15
+ line: nil,
16
+ column: nil,
17
+ details: {}
18
+ }.freeze
19
+ private_constant :LAYERS, :SEVERITIES, :ISSUE_DEFAULTS
20
+
21
+ Issue = Data.define(:code, :message, :layer, :severity, :term_id, :group_id, :path, :line, :column, :details) do
22
+ include Model::ValidatedWith
23
+
24
+ def initialize(**attributes)
25
+ Model.validate_attributes!(self.class.members, attributes)
26
+ attributes.fetch_values(:code, :message, :layer)
27
+ values = ISSUE_DEFAULTS.merge(attributes)
28
+ validate_values!(values)
29
+
30
+ super(**values.transform_values { |value| Model.copy_and_freeze(value) })
31
+ end
32
+
33
+ private
34
+
35
+ def validate_values!(values)
36
+ unless LAYERS.include?(values[:layer])
37
+ raise ArgumentError, "Unknown validation layer: #{values[:layer].inspect}"
38
+ end
39
+ unless SEVERITIES.include?(values[:severity])
40
+ raise ArgumentError, "Unknown validation severity: #{values[:severity].inspect}"
41
+ end
42
+ raise TypeError, 'details must be a Hash' unless values[:details].is_a?(Hash)
43
+ end
44
+ end
45
+
46
+ Report = Data.define(:profile, :issues) do
47
+ include Model::ValidatedWith
48
+
49
+ def initialize(profile: nil, issues: [])
50
+ raise TypeError, 'issues must be an Array' unless issues.is_a?(Array)
51
+ raise TypeError, 'issues must contain validation issues' unless issues.all?(Issue)
52
+
53
+ super(profile: Model.copy_and_freeze(profile), issues: Model.copy_and_freeze(issues))
54
+ end
55
+
56
+ def valid? = issues.none? { |issue| issue.severity == :error }
57
+
58
+ def invalid? = !valid?
59
+ end
60
+ end
61
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Facturx
4
- VERSION = '0.2.0'
4
+ VERSION = '1.1.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
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module StageSupport
6
+ module Structure
7
+ private
8
+
9
+ def within_group(id, value, element:, parent:, represented_attributes: [])
10
+ group = Terms.group(id)
11
+ values = present_values(value)
12
+ accepted = tracker.observe_group?(group, count: values.size, path: group.xpath)
13
+ return unless accepted
14
+
15
+ values.each do |item|
16
+ write_group_item(group, item, element, parent, represented_attributes) { |node| yield(node, item) }
17
+ end
18
+ end
19
+
20
+ def each_group(id, values, element:, parent:, represented_attributes: [], &)
21
+ within_group(id, Array(values), element:, parent:, represented_attributes:, &)
22
+ end
23
+
24
+ def write_group_item(group, item, element, parent, represented_attributes)
25
+ if group.attribute
26
+ report_unrepresentable_attributes(group.id, item, model: group.model,
27
+ represented_attributes:)
28
+ end
29
+ node = context.element(parent, element)
30
+ yield node
31
+ remove_if_empty(node)
32
+ end
33
+
34
+ def technical(parent, element, value, default: nil)
35
+ text = value.to_s
36
+ text = default.to_s if text.strip.empty?
37
+ return if text.strip.empty?
38
+
39
+ context.element(parent, element, text:)
40
+ end
41
+
42
+ def container(parent, element)
43
+ node = context.element(parent, element)
44
+ yield node
45
+ remove_if_empty(node)
46
+ node
47
+ end
48
+
49
+ def remove_if_empty(node)
50
+ node.remove if node.element_children.empty? && node.text.empty? && node.attribute_nodes.empty?
51
+ end
52
+
53
+ def report_unrepresentable_attributes(group_id, item, model: model_for(item), represented_attributes: [])
54
+ return unless item.is_a?(Data) && model_for(item) == model
55
+
56
+ group = Terms.group(group_id)
57
+ item.members.each do |attribute|
58
+ value = item.public_send(attribute)
59
+ next unless unrepresentable_attribute?(group, model, attribute, value, represented_attributes)
60
+
61
+ tracker.unrepresentable_attribute(group, model:, attribute:)
62
+ end
63
+ end
64
+
65
+ def report_unrepresentable_attribute(group_id, model:, attribute:)
66
+ tracker.unrepresentable_attribute(Terms.group(group_id), model:, attribute:)
67
+ end
68
+
69
+ def attribute_supported_in_group?(group, model, attribute)
70
+ Terms.all.any? { |term| term.group_id == group.id && term.model == model && term.attribute == attribute }
71
+ end
72
+
73
+ def unrepresentable_attribute?(group, model, attribute, value, represented_attributes)
74
+ populated?(value) && !represented_attributes.include?(attribute) &&
75
+ !attribute_supported_in_group?(group, model, attribute)
76
+ end
77
+
78
+ def populated?(value)
79
+ !value.nil? && (!value.respond_to?(:empty?) || !value.empty?)
80
+ end
81
+
82
+ def model_for(item)
83
+ item.class.name.split('::').last.gsub(/([a-z\\d])([A-Z])/, '\\1_\\2').downcase.to_sym
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ class DocumentContext < Stage
7
+ term_ids 'BT-23', 'BT-24'
8
+
9
+ def call
10
+ within_group('BG-2', document, element: 'rsm:ExchangedDocumentContext', parent: context.root,
11
+ represented_attributes: [:vat_point_date_code]) do |node,|
12
+ emit_context_parameter(node, 'BT-23', document.business_process,
13
+ 'ram:BusinessProcessSpecifiedDocumentContextParameter')
14
+ guideline = document.guideline_urn || profile.guideline_urn
15
+ emit_context_parameter(node, 'BT-24', guideline, 'ram:GuidelineSpecifiedDocumentContextParameter')
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def emit_context_parameter(parent, id, value, element)
22
+ if value
23
+ container(parent, element) { |node| emit(id, value, element: 'ram:ID', parent: node) }
24
+ else
25
+ emit(id, nil, element: 'ram:ID', parent: parent)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ class ExchangedDocument < Stage
7
+ term_ids 'BT-1', 'BT-3', 'BT-2', 'BT-22', 'BT-21'
8
+
9
+ def call
10
+ node = context.element(context.root, 'rsm:ExchangedDocument')
11
+ emit('BT-1', document.invoice_number, element: 'ram:ID', parent: node)
12
+ emit('BT-3', document.type_code, element: 'ram:TypeCode', parent: node)
13
+ issue_date(node)
14
+ notes(node)
15
+ end
16
+
17
+ private
18
+
19
+ def notes(parent)
20
+ each_group('BG-1', document.notes, element: 'ram:IncludedNote', parent:) do |note_node, note|
21
+ emit('BT-22', note.content, element: 'ram:Content', parent: note_node)
22
+ emit('BT-21', note.subject_code, element: 'ram:SubjectCode', parent: note_node)
23
+ end
24
+ end
25
+
26
+ def issue_date(parent)
27
+ emit_date(parent, 'BT-2', document.issue_date, wrapper: 'ram:IssueDateTime')
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end