facturx 0.1.0 → 0.2.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -1
  3. data/NOTICE.md +2 -0
  4. data/README.md +27 -1
  5. data/lib/facturx/coerce.rb +74 -0
  6. data/lib/facturx/diagnostic.rb +20 -0
  7. data/lib/facturx/document.rb +45 -0
  8. data/lib/facturx/group.rb +5 -0
  9. data/lib/facturx/model/common.rb +8 -0
  10. data/lib/facturx/model/immutable.rb +75 -0
  11. data/lib/facturx/model/line.rb +14 -0
  12. data/lib/facturx/model/party.rb +10 -0
  13. data/lib/facturx/model/payment.rb +9 -0
  14. data/lib/facturx/model/reference.rb +7 -0
  15. data/lib/facturx/model/trade.rb +9 -0
  16. data/lib/facturx/model.rb +9 -0
  17. data/lib/facturx/pdf/extractor.rb +7 -4
  18. data/lib/facturx/reader/semantic_mapper/delivery_mapping.rb +43 -0
  19. data/lib/facturx/reader/semantic_mapper/document_mapping.rb +109 -0
  20. data/lib/facturx/reader/semantic_mapper/helpers.rb +114 -0
  21. data/lib/facturx/reader/semantic_mapper/line_mapping.rb +120 -0
  22. data/lib/facturx/reader/semantic_mapper/party_mapping.rb +90 -0
  23. data/lib/facturx/reader/semantic_mapper/payment_mapping.rb +71 -0
  24. data/lib/facturx/reader/semantic_mapper/trade_mapping.rb +83 -0
  25. data/lib/facturx/reader/semantic_mapper.rb +67 -0
  26. data/lib/facturx/reader/term_reader/coercion.rb +69 -0
  27. data/lib/facturx/reader/term_reader/unmapped.rb +37 -0
  28. data/lib/facturx/reader/term_reader.rb +124 -0
  29. data/lib/facturx/reader.rb +101 -0
  30. data/lib/facturx/reading.rb +23 -0
  31. data/lib/facturx/source_reader.rb +30 -0
  32. data/lib/facturx/term.rb +5 -0
  33. data/lib/facturx/terms/declaration.rb +20 -0
  34. data/lib/facturx/terms/declarations/adjustments.rb +230 -0
  35. data/lib/facturx/terms/declarations/delivery.rb +83 -0
  36. data/lib/facturx/terms/declarations/document.rb +223 -0
  37. data/lib/facturx/terms/declarations/groups.rb +356 -0
  38. data/lib/facturx/terms/declarations/lines.rb +166 -0
  39. data/lib/facturx/terms/declarations/parties.rb +609 -0
  40. data/lib/facturx/terms/declarations/payment.rb +106 -0
  41. data/lib/facturx/terms/declarations/products.rb +103 -0
  42. data/lib/facturx/terms/declarations/references.rb +72 -0
  43. data/lib/facturx/terms/declarations/taxes.rb +92 -0
  44. data/lib/facturx/terms/declarations/totals.rb +107 -0
  45. data/lib/facturx/terms/reference.rb +21 -0
  46. data/lib/facturx/terms.rb +109 -0
  47. data/lib/facturx/version.rb +1 -1
  48. data/lib/facturx.rb +14 -0
  49. metadata +45 -4
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Reader
5
+ class SemanticMapper
6
+ private
7
+
8
+ def reference(id, context: @document, base_xpath: nil, field: :id)
9
+ item = value(id, context:, base_xpath:)
10
+ DocumentReference.new(**{ field => item }) if item
11
+ end
12
+
13
+ def identifier(value_id, scheme_id = nil, context: @document, base_xpath: nil)
14
+ return unless value_id
15
+
16
+ item = value(value_id, context:, base_xpath:)
17
+ unless item
18
+ @terms.mark_value(scheme_id, context:, base_xpath:) if scheme_id
19
+ return
20
+ end
21
+
22
+ scheme = value(scheme_id, context:, base_xpath:) if scheme_id
23
+ Identifier.new(value: item, scheme_id: scheme)
24
+ end
25
+
26
+ def quantity(value_id, unit_id, context:, base_xpath:)
27
+ amount = value(value_id, context:, base_xpath:)
28
+ unless amount
29
+ @terms.mark_value(unit_id, context:, base_xpath:)
30
+ return
31
+ end
32
+
33
+ unit = value(unit_id, context:, base_xpath:)
34
+ Quantity.new(value: amount, unit_code: unit)
35
+ end
36
+
37
+ def tax_registration(attribute, scheme_id, node, base, group_id)
38
+ term = term_for(:party, group_id, attribute)
39
+ return unless term
40
+
41
+ nodes = node.xpath("./ram:SpecifiedTaxRegistration/ram:ID[@schemeID='#{scheme_id}']", NAMESPACES)
42
+ item = value(term.id, context: node, base_xpath: base, nodes:)
43
+ Identifier.new(value: item, scheme_id:) if item
44
+ end
45
+
46
+ def read_price_allowance_indicator(node, base)
47
+ allowance = node.at_xpath('./ram:AppliedTradeAllowanceCharge', NAMESPACES)
48
+ return unless allowance
49
+
50
+ indicator = allowance.at_xpath('./ram:ChargeIndicator', NAMESPACES)
51
+ @terms.mark(indicator)
52
+ allowance_base = "#{base}/ram:AppliedTradeAllowanceCharge"
53
+ value('BT-147-02', context: allowance, base_xpath: allowance_base)
54
+ end
55
+
56
+ def technical_value(xpath, context)
57
+ @terms.raw_value(xpath, context:)
58
+ end
59
+
60
+ def technical_boolean(xpath, context, term_id:, required: false, missing_path: xpath)
61
+ @terms.coerced_value(xpath, context:, type: :boolean, term_id:, required:, missing_path:)
62
+ end
63
+
64
+ def value(id, context: @document, base_group: nil, base_xpath: nil, nodes: nil)
65
+ return unless id
66
+
67
+ @terms.value(id, context:, base_xpath: base_xpath || (base_group && group_xpath(base_group)), nodes:)
68
+ end
69
+
70
+ def scalar_value(id, **)
71
+ Array(value(id, **)).first
72
+ end
73
+
74
+ def scalar_attributes(model, group_id, context: @document, base_xpath: nil, **selection)
75
+ only = selection[:only]
76
+ except = selection.fetch(:except, [])
77
+ model_terms(model, group_id)
78
+ .group_by(&:attribute)
79
+ .select { |attribute, terms| terms.one? && selected_attribute?(attribute, only, except) }
80
+ .to_h { |attribute, terms| [attribute, value(terms.first.id, context:, base_xpath:)] }
81
+ end
82
+
83
+ def selected_attribute?(attribute, only, except)
84
+ (only.nil? || only.include?(attribute)) && !except.include?(attribute)
85
+ end
86
+
87
+ def model_terms(model, group_id)
88
+ @terms_by_model_and_group.fetch([model, group_id], EMPTY_TERMS)
89
+ end
90
+
91
+ def term_for(model, group_id, attribute, xpath_suffix: nil)
92
+ terms = model_terms(model, group_id).select { |term| term.attribute == attribute }
93
+ terms.find { |term| xpath_suffix.nil? || term.xpath.end_with?(xpath_suffix) }
94
+ end
95
+
96
+ def term_for_attribute(model, attribute, xpath_suffix: nil)
97
+ terms = @terms_by_model_and_attribute.fetch([model, attribute], EMPTY_TERMS)
98
+ terms.find { |term| xpath_suffix.nil? || term.xpath.end_with?(xpath_suffix) }
99
+ end
100
+
101
+ def group_nodes(id, **, &)
102
+ @terms.group_nodes(id, **, &)
103
+ end
104
+
105
+ def first_group(id, **)
106
+ group_nodes(id, **).first
107
+ end
108
+
109
+ def group_xpath(id)
110
+ @registry.group(id).xpath
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Reader
5
+ class SemanticMapper
6
+ private
7
+
8
+ def lines
9
+ group_nodes('BG-25').map { |node| line(node) }
10
+ end
11
+
12
+ def line(node)
13
+ base = group_xpath('BG-25')
14
+ Line.new(
15
+ **scalar_attributes(:line, 'BG-25', context: node, base_xpath: base),
16
+ **line_trade_attributes(node, base),
17
+ **line_reference_attributes(node, base)
18
+ )
19
+ end
20
+
21
+ def line_trade_attributes(node, base)
22
+ {
23
+ product: product(node, base),
24
+ quantity: quantity('BT-129', 'BT-130', context: node, base_xpath: base),
25
+ gross_price: price(node, base, gross: true),
26
+ net_price: price(node, base, gross: false),
27
+ tax: line_tax(node, base),
28
+ period: period('BG-26', context: node, base_xpath: base),
29
+ allowances: allowance_charges('BG-27', charge: false, context: node, base_xpath: base),
30
+ charges: allowance_charges('BG-28', charge: true, context: node, base_xpath: base)
31
+ }
32
+ end
33
+
34
+ def line_reference_attributes(node, base)
35
+ {
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)
38
+ }
39
+ end
40
+
41
+ def product(parent, parent_base)
42
+ node = first_group('BG-31', context: parent, base_xpath: parent_base)
43
+ return unless node
44
+
45
+ base = group_xpath('BG-31')
46
+ Product.new(**product_attributes_hash(node, base))
47
+ end
48
+
49
+ def product_attributes_hash(node, base)
50
+ identifiers = %i[seller_identifier buyer_identifier global_identifier]
51
+ scalar_attributes(:product, 'BG-31', context: node, base_xpath: base, except: identifiers).merge(
52
+ seller_identifier: identifier('BT-155', context: node, base_xpath: base),
53
+ buyer_identifier: identifier('BT-156', context: node, base_xpath: base),
54
+ global_identifier: identifier('BT-157', 'BT-157-1', context: node, base_xpath: base),
55
+ attributes: product_attributes(node, base),
56
+ classifications: product_classifications(node, base)
57
+ )
58
+ end
59
+
60
+ def product_attributes(parent, parent_base)
61
+ group_nodes('BG-32', context: parent, base_xpath: parent_base).map do |node|
62
+ base = group_xpath('BG-32')
63
+ ProductAttribute.new(**scalar_attributes(:product_attribute, 'BG-32', context: node, base_xpath: base))
64
+ end
65
+ end
66
+
67
+ def product_classifications(parent, parent_base)
68
+ nodes = parent.xpath('./ram:DesignatedProductClassification', NAMESPACES)
69
+ nodes.map do |node|
70
+ base = "#{parent_base}/ram:DesignatedProductClassification"
71
+ ProductClassification.new(
72
+ code: scalar_value('BT-158', context: node, base_xpath: base),
73
+ list_id: value('BT-158-1', context: node, base_xpath: base),
74
+ list_version_id: value('BT-158-2', context: node, base_xpath: base)
75
+ )
76
+ end
77
+ end
78
+
79
+ def price(parent, parent_base, gross:)
80
+ prefix = gross ? 'Gross' : 'Net'
81
+ node = parent.at_xpath("./ram:SpecifiedLineTradeAgreement/ram:#{prefix}PriceProductTradePrice", NAMESPACES)
82
+ unless node
83
+ value('BT-146', context: parent, base_xpath: parent_base) unless gross
84
+ return
85
+ end
86
+
87
+ base = "#{parent_base}/ram:SpecifiedLineTradeAgreement/ram:#{prefix}PriceProductTradePrice"
88
+ Price.new(**price_attributes(node, base, gross))
89
+ end
90
+
91
+ def price_attributes(node, base, gross)
92
+ ids = gross ? %w[BT-148 BT-149-1 BT-150-1 BT-147] : %w[BT-146 BT-149 BT-150]
93
+ {
94
+ amount: value(ids[0], context: node, base_xpath: base),
95
+ basis_quantity: quantity(ids[1], ids[2], context: node, base_xpath: base),
96
+ discount: gross ? price_discount(node, base, ids[3]) : nil
97
+ }
98
+ end
99
+
100
+ def price_discount(node, base, amount_id)
101
+ amount = value(amount_id, context: node, base_xpath: base)
102
+ indicator = read_price_allowance_indicator(node, base)
103
+ return amount if indicator == false
104
+ return unless indicator == true
105
+
106
+ @terms.add(:invalid_value, 'BT-147-02', base, 'Gross price discount cannot be a charge', value: indicator)
107
+ nil
108
+ end
109
+
110
+ def line_tax(parent, parent_base)
111
+ node = first_group('BG-30', context: parent, base_xpath: parent_base)
112
+ return unless node
113
+
114
+ base = group_xpath('BG-30')
115
+ attributes = scalar_attributes(:tax_breakdown, 'BG-30', context: node, base_xpath: base)
116
+ TaxBreakdown.new(type_code: technical_value('./ram:TypeCode', node), **attributes)
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Reader
5
+ class SemanticMapper
6
+ private
7
+
8
+ def party(group_id, contact_group: nil, address_group: nil)
9
+ node = first_group(group_id)
10
+ return unless node
11
+
12
+ base = group_xpath(group_id)
13
+ Party.new(**party_scalar_attributes(node, base, group_id),
14
+ **party_identifier_attributes(node, base, group_id),
15
+ **party_group_attributes(node, base, contact_group, address_group))
16
+ end
17
+
18
+ def party_scalar_attributes(node, base, group_id)
19
+ scalar_attributes(:party, group_id, context: node, base_xpath: base,
20
+ only: %i[name description trading_name])
21
+ end
22
+
23
+ def party_identifier_attributes(node, base, group_id)
24
+ {
25
+ identifiers: party_identifiers(node, base, group_id),
26
+ legal_registration: compound_identifier(:legal_registration, node, base, group_id),
27
+ vat_identifier: tax_registration(:vat_identifier, 'VA', node, base, group_id),
28
+ tax_identifier: tax_registration(:tax_identifier, 'FC', node, base, group_id),
29
+ electronic_address: compound_identifier(:electronic_address, node, base, group_id)
30
+ }
31
+ end
32
+
33
+ def party_group_attributes(node, base, contact_group, address_group)
34
+ {
35
+ address: address_group && address(node, base, address_group),
36
+ contact: contact_group && contact(node, base, contact_group)
37
+ }
38
+ end
39
+
40
+ def party_identifiers(node, base, group_id)
41
+ value_term = term_for(:party, group_id, :identifiers, xpath_suffix: '/ram:ID')
42
+ return [] unless value_term
43
+
44
+ identifiers = local_party_identifiers(value_term, node, base)
45
+ scheme_term = term_for(:party, group_id, :identifiers, xpath_suffix: '/@schemeID')
46
+ return identifiers unless scheme_term
47
+
48
+ identifiers.concat(global_party_identifiers(node))
49
+ end
50
+
51
+ def local_party_identifiers(term, node, base)
52
+ Array(value(term.id, context: node, base_xpath: base)).compact.map { |item| Identifier.new(value: item) }
53
+ end
54
+
55
+ def global_party_identifiers(node)
56
+ node.xpath('./ram:GlobalID', NAMESPACES).map do |global_id|
57
+ @terms.mark(global_id)
58
+ Identifier.new(value: global_id.text, scheme_id: global_id['schemeID'])
59
+ end
60
+ end
61
+
62
+ def compound_identifier(attribute, node, base, group_id)
63
+ value_term = term_for(:party, group_id, attribute, xpath_suffix: '/ram:ID') ||
64
+ term_for(:party, group_id, attribute, xpath_suffix: '/ram:URIID')
65
+ return unless value_term
66
+
67
+ scheme_term = term_for(:party, group_id, attribute, xpath_suffix: '/@schemeID')
68
+ identifier(value_term.id, scheme_term&.id, context: node, base_xpath: base)
69
+ end
70
+
71
+ def contact(parent, parent_base, group_id)
72
+ node = first_group(group_id, context: parent, base_xpath: parent_base)
73
+ return unless node
74
+
75
+ base = group_xpath(group_id)
76
+ attributes = scalar_attributes(:contact, group_id, context: node, base_xpath: base)
77
+ Contact.new(**attributes)
78
+ end
79
+
80
+ def address(parent, parent_base, group_id)
81
+ node = first_group(group_id, context: parent, base_xpath: parent_base)
82
+ return unless node
83
+
84
+ base = group_xpath(group_id)
85
+ attributes = scalar_attributes(:address, group_id, context: node, base_xpath: base)
86
+ Address.new(**attributes)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Reader
5
+ class SemanticMapper
6
+ private
7
+
8
+ def payment
9
+ nodes = group_nodes('BG-16')
10
+ base = group_xpath('BG-16')
11
+ attributes = settlement_payment_attributes(nodes.first, base)
12
+ attributes.merge!(payment_means_attributes(nodes.first, base)) if nodes.first
13
+ return if attributes.empty?
14
+
15
+ PaymentInstructions.new(**attributes)
16
+ end
17
+
18
+ def settlement_payment_attributes(payment_means, base)
19
+ attributes = scalar_attributes(:payment_instructions, 'BG-19')
20
+ direct_debit_details = direct_debit(payment_means, base)
21
+ attributes[:direct_debit] = direct_debit_details if direct_debit_details
22
+ attributes.compact
23
+ end
24
+
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)
31
+ )
32
+ end
33
+
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)
37
+ end
38
+ end
39
+
40
+ def credit_transfer(node, provider_identifier)
41
+ base = group_xpath('BG-17')
42
+ account = term_for(:credit_transfer, 'BG-17', :account_identifier)
43
+ attributes = scalar_attributes(:credit_transfer, 'BG-17', context: node, base_xpath: base,
44
+ except: [:account_identifier])
45
+ CreditTransfer.new(**attributes, account_identifier: identifier(account.id, context: node, base_xpath: base),
46
+ provider_identifier:)
47
+ end
48
+
49
+ def payment_card(parent, parent_base)
50
+ node = first_group('BG-18', context: parent, base_xpath: parent_base)
51
+ return unless node
52
+
53
+ base = group_xpath('BG-18')
54
+ PaymentCard.new(**scalar_attributes(:payment_card, 'BG-18', context: node, base_xpath: base))
55
+ end
56
+
57
+ def direct_debit(payment_means, base)
58
+ values = %i[mandate_identifier creditor_identifier].to_h do |attribute|
59
+ [attribute, identifier(term_for_attribute(:direct_debit, attribute)&.id)]
60
+ end
61
+ if payment_means
62
+ values[:debtor_account_identifier] = identifier(
63
+ term_for(:direct_debit, 'BG-16', :debtor_account_identifier)&.id,
64
+ context: payment_means, base_xpath: base
65
+ )
66
+ end
67
+ DirectDebit.new(**values) if values.values.any?
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Reader
5
+ class SemanticMapper
6
+ private
7
+
8
+ def read_tax_breakdowns
9
+ nodes = group_nodes('BG-23')
10
+ base = group_xpath('BG-23')
11
+ vat_point_date = nil
12
+ items = nodes.map do |node|
13
+ tax_point_date = value('BT-7', context: node, base_xpath: base)
14
+ vat_point_date ||= tax_point_date
15
+ attributes = scalar_attributes(:tax_breakdown, 'BG-23', context: node, base_xpath: base)
16
+ TaxBreakdown.new(type_code: technical_value('./ram:TypeCode', node), **attributes)
17
+ end
18
+ TaxBreakdownResult.new(items:, vat_point_date:)
19
+ end
20
+
21
+ def allowance_charges(group_id, charge:, context: @document, base_xpath: nil)
22
+ group_nodes(group_id, context:, base_xpath:) { |node| charge_indicator(node, group_id) == charge }.map do |node|
23
+ base = group_xpath(group_id)
24
+ AllowanceCharge.new(**allowance_attributes(node, base, group_id), indicator: charge,
25
+ tax: allowance_tax(node, base, group_id))
26
+ end
27
+ end
28
+
29
+ def allowance_attributes(node, base, group_id)
30
+ scalar_attributes(:allowance_charge, group_id, context: node, base_xpath: base, except: [:tax])
31
+ end
32
+
33
+ def allowance_tax(node, base, group_id)
34
+ technical_value('./ram:CategoryTradeTax/ram:TypeCode', node)
35
+ category = value(term_for(:allowance_charge, group_id, :tax,
36
+ xpath_suffix: '/ram:CategoryTradeTax/ram:CategoryCode')&.id,
37
+ context: node, base_xpath: base)
38
+ rate = value(term_for(:allowance_charge, group_id, :tax,
39
+ xpath_suffix: '/ram:CategoryTradeTax/ram:RateApplicablePercent')&.id,
40
+ context: node, base_xpath: base)
41
+ TaxBreakdown.new(category_code: category, rate:) if category || rate
42
+ end
43
+
44
+ def charge_indicator(node, group_id)
45
+ @charge_indicators ||= {}.compare_by_identity
46
+ @charge_indicators.fetch(node) do
47
+ path = './ram:ChargeIndicator/udt:Indicator'
48
+ @charge_indicators[node] = technical_boolean(
49
+ path, node, term_id: "#{group_id}-1", required: true, missing_path: group_xpath("#{group_id}-1")
50
+ )
51
+ end
52
+ end
53
+
54
+ def period(group_id, context: @document, base_xpath: nil)
55
+ node = first_group(group_id, context:, base_xpath:)
56
+ return unless node
57
+
58
+ base = group_xpath(group_id)
59
+ Period.new(**scalar_attributes(:period, group_id, context: node, base_xpath: base))
60
+ end
61
+
62
+ def totals(currency, tax_currency)
63
+ node = first_group('BG-22')
64
+ return unless node
65
+
66
+ base = group_xpath('BG-22')
67
+ attributes = scalar_attributes(:totals, 'BG-22', context: node, base_xpath: base,
68
+ except: %i[tax_total tax_total_in_tax_currency])
69
+ Totals.new(**attributes, **tax_total_attributes(node, base, currency, tax_currency))
70
+ end
71
+
72
+ def tax_total_attributes(node, base, currency, tax_currency)
73
+ total_nodes = node.xpath('./ram:TaxTotalAmount', NAMESPACES)
74
+ invoice_tax = total_nodes.select { |item| tax_currency.nil? || item['currencyID'] == currency }
75
+ accounting_tax = total_nodes.select { |item| tax_currency && item['currencyID'] == tax_currency }
76
+ {
77
+ tax_total: value('BT-110', context: node, base_xpath: base, nodes: invoice_tax),
78
+ tax_total_in_tax_currency: value('BT-111', context: node, base_xpath: base, nodes: accounting_tax)
79
+ }
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Reader
5
+ class SemanticMapper
6
+ EMPTY_TERMS = [].freeze
7
+ TaxBreakdownResult = Data.define(:items, :vat_point_date)
8
+
9
+ def initialize(document:, profile:, registry:, coercer:, diagnostics:)
10
+ @document = document
11
+ @profile = profile
12
+ @terms = TermReader.new(document:, profile:, registry:, coercer:, diagnostics:)
13
+ @registry = registry
14
+ active_terms = registry.for_profile(profile)
15
+ @terms_by_model_and_group = active_terms.group_by { |term| [term.model, term.group_id] }.freeze
16
+ @terms_by_model_and_attribute = active_terms.group_by { |term| [term.model, term.attribute] }.freeze
17
+ end
18
+
19
+ def call
20
+ tax_breakdowns = read_tax_breakdowns
21
+ attributes = document_attributes(tax_breakdowns).merge(
22
+ party_attributes, transaction_attributes, collection_attributes(tax_breakdowns.items)
23
+ )
24
+ attributes[:totals] = totals(attributes[:currency], attributes[:tax_currency])
25
+ Document.new(**attributes)
26
+ end
27
+
28
+ def add_unmapped(root)
29
+ @terms.add_unmapped(root)
30
+ end
31
+
32
+ private
33
+
34
+ def party_attributes
35
+ {
36
+ seller: party('BG-4', contact_group: 'BG-6', address_group: 'BG-5'),
37
+ buyer: party('BG-7', contact_group: 'BG-9', address_group: 'BG-8'),
38
+ payee: party('BG-10'),
39
+ tax_representative: party('BG-11', address_group: 'BG-12')
40
+ }
41
+ end
42
+
43
+ def transaction_attributes
44
+ {
45
+ delivery: delivery, billing_period: period('BG-14'), payment: payment
46
+ }
47
+ end
48
+
49
+ def collection_attributes(tax_breakdowns)
50
+ {
51
+ notes: notes, lines: lines, tax_breakdowns:,
52
+ allowances: allowance_charges('BG-20', charge: false),
53
+ charges: allowance_charges('BG-21', charge: true),
54
+ preceding_invoices: preceding_invoices, supporting_documents: supporting_documents
55
+ }
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ require_relative 'semantic_mapper/document_mapping'
62
+ require_relative 'semantic_mapper/party_mapping'
63
+ require_relative 'semantic_mapper/delivery_mapping'
64
+ require_relative 'semantic_mapper/payment_mapping'
65
+ require_relative 'semantic_mapper/line_mapping'
66
+ require_relative 'semantic_mapper/trade_mapping'
67
+ require_relative 'semantic_mapper/helpers'
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Reader
5
+ class TermReader
6
+ def coerced_value(xpath, context:, type:, term_id: nil, **options)
7
+ node = context.at_xpath(xpath, NAMESPACES)
8
+ return diagnose_raw_missing(options.fetch(:missing_path, xpath), term_id) if node.nil? && options[:required]
9
+ return unless node
10
+
11
+ coerce_raw_value(node, type:, term_id:)
12
+ rescue CoercionError => e
13
+ add(:invalid_value, term_id, node.path, e.message, e.details)
14
+ nil
15
+ end
16
+
17
+ private
18
+
19
+ def coerce_raw_value(node, type:, term_id:)
20
+ mark(node)
21
+ return diagnose_raw_empty(node, term_id) if node.text.strip.empty?
22
+
23
+ @coercer.call(node.text, type:, term_id:, path: node.path)
24
+ end
25
+
26
+ def coerce_term_value(node, term)
27
+ @coercer.call(node.text, type: term.type, scale: term.scale, term_id: term.id, path: node.path)
28
+ end
29
+
30
+ def coerce(node, term)
31
+ return diagnose_date_format(node, term) if invalid_date_format?(node, term)
32
+ return diagnose_empty(node, term) if node.text.strip.empty?
33
+
34
+ coerce_term_value(node, term)
35
+ rescue CoercionError => e
36
+ add(:invalid_value, term.id, node.path, e.message, e.details)
37
+ nil
38
+ end
39
+
40
+ def diagnose_empty(node, term)
41
+ add(:empty_value, term.id, node.path, 'Value is empty', value: node.text)
42
+ nil
43
+ end
44
+
45
+ def diagnose_raw_empty(node, term_id)
46
+ add(:empty_value, term_id, node.path, 'Value is empty', value: node.text)
47
+ nil
48
+ end
49
+
50
+ def diagnose_raw_missing(path, term_id)
51
+ add(:missing_required_term, term_id, path, 'Required value is missing')
52
+ nil
53
+ end
54
+
55
+ def invalid_date_format?(node, term)
56
+ return false unless term.type == :date_102
57
+
58
+ format = node.attribute('format')
59
+ mark(format)
60
+ format&.value != '102'
61
+ end
62
+
63
+ def diagnose_date_format(node, term)
64
+ add(:invalid_value, term.id, node.path, 'Date format must be 102', format: node['format'])
65
+ nil
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Reader
5
+ class TermReader
6
+ IGNORED_ATTRIBUTES = %w[currencyID schemeID].freeze
7
+
8
+ def add_unmapped(root)
9
+ add_unmapped_elements(root)
10
+ add_unmapped_attributes(root)
11
+ end
12
+
13
+ private
14
+
15
+ def add_unmapped_elements(root)
16
+ root.xpath('.//*[not(*)]').each do |node|
17
+ next if node.text.empty? || matched?(node)
18
+
19
+ add(:unmapped_element, nil, node.path, 'Element is not mapped to the EN16931 model', value: node.text)
20
+ end
21
+ end
22
+
23
+ def add_unmapped_attributes(root)
24
+ root.xpath('.//@*').each do |attribute|
25
+ next if IGNORED_ATTRIBUTES.include?(attribute.name) || matched?(attribute)
26
+
27
+ add(:unmapped_element, nil, attribute.path, 'Attribute is not mapped to the EN16931 model',
28
+ value: attribute.value)
29
+ end
30
+ end
31
+
32
+ def matched?(node)
33
+ @matched_nodes.key?(node)
34
+ end
35
+ end
36
+ end
37
+ end