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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -3
- data/DOCUMENTATION.md +271 -0
- data/README.md +79 -69
- data/lib/facturx/attach.rb +4 -20
- 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/error.rb +7 -1
- data/lib/facturx/format.rb +85 -0
- data/lib/facturx/generate.rb +21 -0
- data/lib/facturx/pdf/composer.rb +32 -0
- data/lib/facturx/profile.rb +1 -1
- data/lib/facturx/profile_resolver.rb +40 -0
- data/lib/facturx/profiles.rb +1 -8
- 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/source_reader.rb +1 -1
- data/lib/facturx/validation.rb +61 -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/tracker.rb +131 -0
- data/lib/facturx/writer.rb +76 -0
- data/lib/facturx/xml/namespaces.rb +19 -0
- data/lib/facturx/xml/parser.rb +1 -1
- data/lib/facturx/xml/profile_detector.rb +2 -4
- data/lib/facturx/xml/report_builder.rb +44 -0
- data/lib/facturx/xml/schema_registry.rb +22 -0
- data/lib/facturx/xml/schema_validator.rb +7 -2
- data/lib/facturx/xml/validator.rb +32 -0
- data/lib/facturx/xml/verifier.rb +16 -11
- data/lib/facturx.rb +41 -4
- data/rbi/facturx.rbi +1755 -0
- 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 =
|
|
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
|
|
@@ -13,7 +13,7 @@ module Facturx
|
|
|
13
13
|
|
|
14
14
|
def call(source)
|
|
15
15
|
unless source.is_a?(String)
|
|
16
|
-
raise
|
|
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
|
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
|
|
@@ -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
|