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,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ class TradeLines
7
+ class Settlement < Stage
8
+ ADJUSTMENT_IDS = {
9
+ 'BG-27' => %w[BT-138 BT-137 BT-136 BT-140 BT-139],
10
+ 'BG-28' => %w[BT-143 BT-142 BT-141 BT-145 BT-144]
11
+ }.freeze
12
+
13
+ def call(parent:, line:)
14
+ node = context.element(parent, 'ram:SpecifiedLineTradeSettlement')
15
+ line_tax(node, line.tax)
16
+ period(node, line.period)
17
+ adjustment_groups(node, line.allowances, 'BG-27', false)
18
+ adjustment_groups(node, line.charges, 'BG-28', true)
19
+ monetary_sum(node, line.net_amount)
20
+ line_invoiced_object(node, line.invoiced_object_identifier)
21
+ accounting_reference(node, 'BT-133', line.buyer_accounting_reference)
22
+ end
23
+
24
+ private
25
+
26
+ def line_tax(parent, value)
27
+ within_group('BG-30', value, element: 'ram:ApplicableTradeTax', parent:,
28
+ represented_attributes: [:type_code]) do |node, item|
29
+ technical(node, 'ram:TypeCode', item.type_code, default: 'VAT')
30
+ emit('BT-151', item.category_code, element: 'ram:CategoryCode', parent: node)
31
+ emit('BT-152', item.rate, element: 'ram:RateApplicablePercent', parent: node)
32
+ end
33
+ end
34
+
35
+ def period(parent, value)
36
+ within_group('BG-26', value, element: 'ram:BillingSpecifiedPeriod', parent:) do |node, item|
37
+ emit_date(node, 'BT-134', item.start_date, wrapper: 'ram:StartDateTime')
38
+ emit_date(node, 'BT-135', item.end_date, wrapper: 'ram:EndDateTime')
39
+ end
40
+ end
41
+
42
+ def adjustment_groups(parent, values, group_id, indicator)
43
+ ids = ADJUSTMENT_IDS.fetch(group_id)
44
+ each_group(group_id, values, element: 'ram:SpecifiedTradeAllowanceCharge', parent:,
45
+ represented_attributes: [:indicator]) do |node, item|
46
+ report_unrepresentable_attributes(group_id, item.tax) if item.tax
47
+ adjustment_indicator(node, group_id, indicator)
48
+ emit_adjustment_fields(node, item, ids)
49
+ end
50
+ end
51
+
52
+ def emit_adjustment_fields(parent, item, ids)
53
+ emit(ids[0], item.percentage, element: 'ram:CalculationPercent', parent:)
54
+ emit(ids[1], item.base_amount, element: 'ram:BasisAmount', parent:)
55
+ emit(ids[2], item.amount, element: 'ram:ActualAmount', parent:)
56
+ emit(ids[3], item.reason_code, element: 'ram:ReasonCode', parent:)
57
+ emit(ids[4], item.reason, element: 'ram:Reason', parent:)
58
+ end
59
+
60
+ def adjustment_indicator(parent, group_id, value)
61
+ within_group("#{group_id}-0", value, element: 'ram:ChargeIndicator', parent:) do |node, indicator|
62
+ within_group("#{group_id}-1", indicator, element: 'udt:Indicator', parent: node) do |leaf, raw|
63
+ leaf.content = raw ? 'true' : 'false'
64
+ end
65
+ end
66
+ end
67
+
68
+ def monetary_sum(parent, value)
69
+ container(parent, 'ram:SpecifiedTradeSettlementLineMonetarySummation') do |node|
70
+ emit('BT-131', value, element: 'ram:LineTotalAmount', parent: node)
71
+ end
72
+ end
73
+
74
+ def line_invoiced_object(parent, value)
75
+ emit_invoiced_object(parent, value, 'BT-128', 'BT-128-1')
76
+ end
77
+
78
+ def accounting_reference(parent, id, value)
79
+ return observe?(id, nil) unless value
80
+
81
+ container(parent, 'ram:ReceivableSpecifiedTradeAccountingAccount') do |node|
82
+ emit(id, value, element: 'ram:ID', parent: node)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ class TradeLines < Stage
7
+ REPRESENTED_ATTRIBUTES = %i[
8
+ product gross_price net_price tax period allowances charges buyer_order_reference
9
+ ].freeze
10
+
11
+ term_ids(*%w[
12
+ BT-126 BT-127 BT-157 BT-157-1 BT-155 BT-156 BT-153 BT-154 BT-160 BT-161 BT-158 BT-158-1
13
+ BT-158-2 BT-159 BT-132 BT-148 BT-149-1 BT-150-1 BT-147-01 BT-147-02 BT-147 BT-146 BT-149
14
+ BT-150 BT-129 BT-130 BT-151 BT-152 BT-134 BT-135 BT-138 BT-137 BT-136 BT-140 BT-139 BT-143
15
+ BT-142 BT-141 BT-145 BT-144 BT-131 BT-128 BT-128-1 BT-133
16
+ ])
17
+
18
+ def initialize(context)
19
+ super
20
+ quantity = Quantity.new(context)
21
+ @product = Product.new(context)
22
+ @agreement = Agreement.new(context, quantity:)
23
+ @settlement = Settlement.new(context)
24
+ @quantity = quantity
25
+ end
26
+
27
+ def call
28
+ each_group('BG-25', document.lines, element: 'ram:IncludedSupplyChainTradeLineItem',
29
+ parent: context.transaction,
30
+ represented_attributes: REPRESENTED_ATTRIBUTES) do |node, line|
31
+ line_document(node, line)
32
+ @product.call(parent: node, value: line.product)
33
+ @agreement.call(parent: node, line:)
34
+ delivery(node, line)
35
+ @settlement.call(parent: node, line:)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def line_document(parent, line)
42
+ container(parent, 'ram:AssociatedDocumentLineDocument') do |node|
43
+ emit('BT-126', line.id, element: 'ram:LineID', parent: node)
44
+ if line.note
45
+ container(node, 'ram:IncludedNote') do |note|
46
+ emit('BT-127', line.note, element: 'ram:Content', parent: note)
47
+ end
48
+ else
49
+ observe?('BT-127', nil)
50
+ end
51
+ end
52
+ end
53
+
54
+ def delivery(parent, line)
55
+ node = context.element(parent, 'ram:SpecifiedLineTradeDelivery')
56
+ @quantity.call(parent: node, value: line.quantity, amount_id: 'BT-129', unit_id: 'BT-130',
57
+ element: 'ram:BilledQuantity')
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ require_relative 'trade_lines/quantity'
65
+ require_relative 'trade_lines/product'
66
+ require_relative 'trade_lines/agreement'
67
+ require_relative 'trade_lines/settlement'
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ module TradeSettlementParties
7
+ private
8
+
9
+ def payee(parent)
10
+ within_group('BG-10', document.payee, element: 'ram:PayeeTradeParty', parent:) do |node, party|
11
+ emit_party_identifiers(node, party.identifiers, 'BT-60', 'BT-60-1')
12
+ emit('BT-59', party.name, element: 'ram:Name', parent: node)
13
+ payee_legal_organization(node, party)
14
+ end
15
+ end
16
+
17
+ def payee_legal_organization(parent, party)
18
+ legal = party.legal_registration
19
+ return missing_payee_registration? unless legal
20
+
21
+ container(parent, 'ram:SpecifiedLegalOrganization') do |node|
22
+ emit_compound_identifier(node, legal, value_id: 'BT-61', scheme_id: 'BT-61-1', element: 'ram:ID')
23
+ end
24
+ end
25
+
26
+ def missing_payee_registration?
27
+ observe?('BT-61', nil)
28
+ observe?('BT-61-1', nil, group_present: false)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ module TradeSettlementPayment
7
+ private
8
+
9
+ def payment_means(parent, payment)
10
+ value = payment if payment_means_present?(payment)
11
+ within_group('BG-16', value, element: 'ram:SpecifiedTradeSettlementPaymentMeans', parent:,
12
+ represented_attributes: %i[
13
+ remittance_information payment_card direct_debit credit_transfers
14
+ ]) do |node, item|
15
+ emit_payment_means(parent, node, item)
16
+ end
17
+ end
18
+
19
+ def emit_payment_means(parent, node, payment)
20
+ transfers = payment.credit_transfers
21
+ nodes = [node, *additional_payment_means(parent, transfers)]
22
+ means_code, means_text = payment_descriptions(node, payment)
23
+ duplicate_payment_terms(nodes.drop(1), means_code, means_text)
24
+ payment_card(node, payment.payment_card)
25
+ debtor_account(node, payment.direct_debit)
26
+ credit_transfers(nodes, transfers)
27
+ end
28
+
29
+ def payment_descriptions(parent, payment)
30
+ code = emit('BT-81', payment.means_code, element: 'ram:TypeCode', parent:).first&.text
31
+ text = emit('BT-82', payment.means_text, element: 'ram:Information', parent:).first&.text
32
+ [code, text]
33
+ end
34
+
35
+ def additional_payment_means(parent, transfers)
36
+ Array.new([Array(transfers).size - 1, 0].max) do
37
+ context.element(parent, 'ram:SpecifiedTradeSettlementPaymentMeans')
38
+ end
39
+ end
40
+
41
+ def duplicate_payment_terms(nodes, means_code, means_text)
42
+ nodes.each do |node|
43
+ technical(node, 'ram:TypeCode', means_code)
44
+ technical(node, 'ram:Information', means_text)
45
+ end
46
+ end
47
+
48
+ def payment_means_present?(payment)
49
+ return false unless payment
50
+
51
+ [payment.means_code, payment.means_text, payment.payment_card,
52
+ payment.direct_debit&.debtor_account_identifier, *payment.credit_transfers].any?
53
+ end
54
+
55
+ def payment_card(parent, value)
56
+ within_group('BG-18', value, element: 'ram:ApplicableTradeSettlementFinancialCard', parent:) do |node, card|
57
+ emit('BT-87', card.primary_account_number, element: 'ram:ID', parent: node)
58
+ emit('BT-88', card.holder_name, element: 'ram:CardholderName', parent: node)
59
+ end
60
+ end
61
+
62
+ def debtor_account(parent, value)
63
+ identifier = value&.debtor_account_identifier
64
+ return observe?('BT-91', nil) unless identifier
65
+
66
+ container(parent, 'ram:PayerPartyDebtorFinancialAccount') do |node|
67
+ emit_identifier_value('BT-91', identifier, 'ram:IBANID', node)
68
+ end
69
+ end
70
+
71
+ def credit_transfers(parents, values)
72
+ transfers = Array(values)
73
+ group = Terms.group('BG-17')
74
+ return unless tracker.observe_group?(group, count: transfers.size, path: group.xpath)
75
+
76
+ transfers.zip(parents).each { |item, parent| emit_credit_transfer(parent, item) }
77
+ end
78
+
79
+ def emit_credit_transfer(parent, transfer)
80
+ container(parent, 'ram:PayeePartyCreditorFinancialAccount') do |node|
81
+ emit_identifier_value('BT-84', transfer.account_identifier, 'ram:IBANID', node)
82
+ emit('BT-85', transfer.account_name, element: 'ram:AccountName', parent: node)
83
+ end
84
+ emit_provider(parent, transfer.provider_identifier)
85
+ end
86
+
87
+ def emit_provider(parent, identifier)
88
+ return observe?('BT-86', nil) unless identifier
89
+
90
+ container(parent, 'ram:PayeeSpecifiedCreditorFinancialInstitution') do |node|
91
+ emit_identifier_value('BT-86', identifier, 'ram:BICID', node)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ module TradeSettlementSummary
7
+ private
8
+
9
+ def payment_terms(parent, direct_debit)
10
+ values = [document.payment_terms, document.payment_due_date, direct_debit&.mandate_identifier]
11
+ return missing_payment_terms? unless values.any?
12
+
13
+ container(parent, 'ram:SpecifiedTradePaymentTerms') do |node|
14
+ emit('BT-20', document.payment_terms, element: 'ram:Description', parent: node)
15
+ emit_date(node, 'BT-9', document.payment_due_date, wrapper: 'ram:DueDateDateTime')
16
+ emit_identifier_value('BT-89', direct_debit&.mandate_identifier, 'ram:DirectDebitMandateID', node)
17
+ end
18
+ end
19
+
20
+ def missing_payment_terms?
21
+ observe?('BT-20', nil)
22
+ observe?('BT-9', nil)
23
+ observe?('BT-89', nil)
24
+ end
25
+
26
+ def totals(parent)
27
+ within_group('BG-22', document.totals, element: 'ram:SpecifiedTradeSettlementHeaderMonetarySummation',
28
+ parent:) do |node, item|
29
+ emit_subtotals(node, item)
30
+ emit_tax_totals(node, item)
31
+ emit_balance(node, item)
32
+ end
33
+ end
34
+
35
+ def emit_subtotals(parent, totals)
36
+ emit('BT-106', totals.line_total, element: 'ram:LineTotalAmount', parent:)
37
+ emit('BT-108', totals.charge_total, element: 'ram:ChargeTotalAmount', parent:)
38
+ emit('BT-107', totals.allowance_total, element: 'ram:AllowanceTotalAmount', parent:)
39
+ emit('BT-109', totals.tax_basis_total, element: 'ram:TaxBasisTotalAmount', parent:)
40
+ end
41
+
42
+ def emit_tax_totals(parent, totals)
43
+ emit('BT-110', totals.tax_total, element: 'ram:TaxTotalAmount', parent:,
44
+ attributes: { 'currencyID' => document.currency })
45
+ emit_tax_total_in_tax_currency(parent, totals.tax_total_in_tax_currency)
46
+ end
47
+
48
+ def emit_tax_total_in_tax_currency(parent, value)
49
+ return observe?('BT-111', nil) unless value
50
+ return unrepresentable('BT-111', 'Tax total requires a distinct tax currency') unless distinct_tax_currency?
51
+
52
+ emit('BT-111', value, element: 'ram:TaxTotalAmount', parent:,
53
+ attributes: { 'currencyID' => document.tax_currency })
54
+ end
55
+
56
+ def distinct_tax_currency?
57
+ tax_currency = document.tax_currency
58
+ !tax_currency.to_s.strip.empty? && tax_currency != document.currency
59
+ end
60
+
61
+ def emit_balance(parent, totals)
62
+ emit('BT-114', totals.rounding, element: 'ram:RoundingAmount', parent:)
63
+ emit('BT-112', totals.grand_total, element: 'ram:GrandTotalAmount', parent:)
64
+ emit('BT-113', totals.prepaid, element: 'ram:TotalPrepaidAmount', parent:)
65
+ emit('BT-115', totals.due_payable, element: 'ram:DuePayableAmount', parent:)
66
+ end
67
+
68
+ def preceding_invoices(parent)
69
+ each_group('BG-3', document.preceding_invoices,
70
+ element: 'ram:InvoiceReferencedDocument', parent:) do |node, item|
71
+ emit('BT-25', item.id, element: 'ram:IssuerAssignedID', parent: node)
72
+ emit_date(node, 'BT-26', item.issue_date, wrapper: 'ram:FormattedIssueDateTime',
73
+ value_element: 'qdt:DateTimeString')
74
+ end
75
+ end
76
+
77
+ def accounting_reference(parent)
78
+ value = document.buyer_accounting_reference
79
+ return observe?('BT-19', nil) unless value
80
+
81
+ container(parent, 'ram:ReceivableSpecifiedTradeAccountingAccount') do |node|
82
+ emit('BT-19', value, element: 'ram:ID', parent: node)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ module TradeSettlementTaxes
7
+ ADJUSTMENT_IDS = {
8
+ false => %w[BT-94 BT-93 BT-92 BT-98 BT-97 BT-95 BT-96],
9
+ true => %w[BT-101 BT-100 BT-99 BT-105 BT-104 BT-102 BT-103]
10
+ }.freeze
11
+ ADJUSTMENT_FIELDS = [
12
+ [:percentage, 'ram:CalculationPercent'],
13
+ [:base_amount, 'ram:BasisAmount'],
14
+ [:amount, 'ram:ActualAmount'],
15
+ [:reason_code, 'ram:ReasonCode'],
16
+ [:reason, 'ram:Reason']
17
+ ].freeze
18
+ private_constant :ADJUSTMENT_IDS, :ADJUSTMENT_FIELDS
19
+
20
+ private
21
+
22
+ def taxes(parent)
23
+ first = true
24
+ each_group('BG-23', document.tax_breakdowns, element: 'ram:ApplicableTradeTax', parent:,
25
+ represented_attributes: [:type_code]) do |node, item|
26
+ emit_tax(node, item, include_tax_point: first)
27
+ first = false
28
+ end
29
+ return unless first
30
+
31
+ report_vat_point_attributes
32
+ end
33
+
34
+ def emit_tax(parent, tax, include_tax_point:)
35
+ emit('BT-117', tax.tax_amount, element: 'ram:CalculatedAmount', parent:)
36
+ technical(parent, 'ram:TypeCode', tax.type_code, default: 'VAT')
37
+ emit('BT-120', tax.exemption_reason, element: 'ram:ExemptionReason', parent:)
38
+ emit('BT-116', tax.basis_amount, element: 'ram:BasisAmount', parent:)
39
+ emit('BT-118', tax.category_code, element: 'ram:CategoryCode', parent:)
40
+ emit('BT-121', tax.exemption_reason_code, element: 'ram:ExemptionReasonCode', parent:)
41
+ tax_point_date(parent) if include_tax_point
42
+ tax_due_date_type(parent, tax, check_conflict: include_tax_point)
43
+ emit('BT-119', tax.rate, element: 'ram:RateApplicablePercent', parent:)
44
+ end
45
+
46
+ def tax_due_date_type(parent, tax, check_conflict:)
47
+ document_code = document.vat_point_date_code
48
+ if check_conflict && tax.due_date_type_code && document_code && tax.due_date_type_code != document_code
49
+ report_unrepresentable_attribute('BG-23', model: :document, attribute: :vat_point_date_code)
50
+ end
51
+ emit('BT-8', tax.due_date_type_code || document_code, element: 'ram:DueDateTypeCode', parent:)
52
+ end
53
+
54
+ def tax_point_date(parent)
55
+ emit_date(
56
+ parent, 'BT-7', document.vat_point_date, wrapper: 'ram:TaxPointDate', value_element: 'udt:DateString'
57
+ )
58
+ end
59
+
60
+ def report_vat_point_attributes
61
+ %i[vat_point_date vat_point_date_code].each do |attribute|
62
+ value = document.public_send(attribute)
63
+ report_unrepresentable_attribute('BG-23', model: :document, attribute:) if value
64
+ end
65
+ end
66
+
67
+ def billing_period(parent)
68
+ within_group('BG-14', document.billing_period, element: 'ram:BillingSpecifiedPeriod', parent:) do |node, item|
69
+ emit_date(node, 'BT-73', item.start_date, wrapper: 'ram:StartDateTime')
70
+ emit_date(node, 'BT-74', item.end_date, wrapper: 'ram:EndDateTime')
71
+ end
72
+ end
73
+
74
+ def adjustments(parent, values, group_id, indicator)
75
+ ids = ADJUSTMENT_IDS.fetch(indicator)
76
+ each_group(group_id, values, element: 'ram:SpecifiedTradeAllowanceCharge', parent:,
77
+ represented_attributes: [:indicator]) do |node, item|
78
+ emit_adjustment(node, item, group_id, indicator, ids)
79
+ end
80
+ end
81
+
82
+ def emit_adjustment(parent, adjustment, group_id, indicator, ids)
83
+ adjustment_indicator(parent, group_id, indicator)
84
+ emit_adjustment_values(parent, adjustment, ids)
85
+ adjustment_tax(parent, adjustment.tax, group_id, ids[5], ids[6])
86
+ end
87
+
88
+ def emit_adjustment_values(parent, adjustment, ids)
89
+ ADJUSTMENT_FIELDS.each_with_index do |(attribute, element), index|
90
+ emit(ids[index], adjustment.public_send(attribute), element:, parent:)
91
+ end
92
+ end
93
+
94
+ def adjustment_indicator(parent, group_id, value)
95
+ within_group("#{group_id}-0", value, element: 'ram:ChargeIndicator', parent:) do |node, indicator|
96
+ within_group("#{group_id}-1", indicator, element: 'udt:Indicator', parent: node) do |leaf, raw|
97
+ leaf.content = raw ? 'true' : 'false'
98
+ end
99
+ end
100
+ end
101
+
102
+ def adjustment_tax(parent, tax, group_id, category_id, rate_id)
103
+ return missing_adjustment_tax?(category_id, rate_id) unless tax
104
+
105
+ report_unrepresentable_attributes(group_id, tax, represented_attributes: %i[type_code category_code rate])
106
+
107
+ container(parent, 'ram:CategoryTradeTax') do |node|
108
+ technical(node, 'ram:TypeCode', tax.type_code, default: 'VAT')
109
+ emit(category_id, tax.category_code, element: 'ram:CategoryCode', parent: node)
110
+ emit(rate_id, tax.rate, element: 'ram:RateApplicablePercent', parent: node)
111
+ end
112
+ end
113
+
114
+ def missing_adjustment_tax?(category_id, rate_id)
115
+ observe?(category_id, nil)
116
+ observe?(rate_id, nil)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'trade_settlement/parties'
4
+ require_relative 'trade_settlement/payment'
5
+ require_relative 'trade_settlement/taxes'
6
+ require_relative 'trade_settlement/summary'
7
+
8
+ module Facturx
9
+ class Writer
10
+ module Stages
11
+ class TradeSettlement < Stage
12
+ include TradeSettlementParties
13
+ include TradeSettlementPayment
14
+ include TradeSettlementTaxes
15
+ include TradeSettlementSummary
16
+
17
+ term_ids(*%w[
18
+ BT-90 BT-83 BT-6 BT-5 BT-60 BT-60-1 BT-59 BT-61 BT-61-1 BT-81 BT-82 BT-87 BT-88 BT-91 BT-84
19
+ BT-85 BT-86 BT-117 BT-120 BT-116 BT-118 BT-121 BT-7 BT-8 BT-119 BT-73 BT-74 BT-94 BT-93 BT-92
20
+ BT-98 BT-97 BT-95 BT-96 BT-101 BT-100 BT-99 BT-105 BT-104 BT-102 BT-103 BT-20 BT-9 BT-89 BT-106
21
+ BT-108 BT-107 BT-109 BT-110 BT-111 BT-114 BT-112 BT-113 BT-115 BT-25 BT-26 BT-19
22
+ ])
23
+
24
+ def call
25
+ node = context.element(context.transaction, 'ram:ApplicableHeaderTradeSettlement')
26
+ payment = document.payment
27
+ emit_header(node, payment)
28
+ emit_content(node, payment)
29
+ end
30
+
31
+ private
32
+
33
+ def emit_header(parent, payment)
34
+ direct_debit = payment&.direct_debit
35
+ emit_identifier_value('BT-90', direct_debit&.creditor_identifier, 'ram:CreditorReferenceID', parent)
36
+ emit('BT-83', payment&.remittance_information, element: 'ram:PaymentReference', parent:)
37
+ emit('BT-6', tax_currency_term_value, element: 'ram:TaxCurrencyCode', parent:)
38
+ emit('BT-5', document.currency, element: 'ram:InvoiceCurrencyCode', parent:)
39
+ end
40
+
41
+ def tax_currency_term_value
42
+ return if profile.id == :minimum && document.totals&.tax_total_in_tax_currency
43
+
44
+ document.tax_currency
45
+ end
46
+
47
+ def emit_content(parent, payment)
48
+ payee(parent)
49
+ payment_means(parent, payment)
50
+ taxes(parent)
51
+ billing_period(parent)
52
+ document_adjustments(parent)
53
+ settlement_summary(parent, payment&.direct_debit)
54
+ end
55
+
56
+ def document_adjustments(parent)
57
+ adjustments(parent, document.allowances, 'BG-20', false)
58
+ adjustments(parent, document.charges, 'BG-21', true)
59
+ end
60
+
61
+ def settlement_summary(parent, direct_debit)
62
+ payment_terms(parent, direct_debit)
63
+ totals(parent)
64
+ preceding_invoices(parent)
65
+ accounting_reference(parent)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'conformance'
4
+ require_relative 'error'
5
+ require_relative 'terms'
6
+ require_relative 'writer/context'
7
+ require_relative 'writer/stage'
8
+ require_relative 'writer/stages/document_context'
9
+ require_relative 'writer/stages/exchanged_document'
10
+ require_relative 'writer/stages/trade_lines'
11
+ require_relative 'writer/stages/trade_agreement'
12
+ require_relative 'writer/stages/trade_delivery'
13
+ require_relative 'writer/stages/trade_settlement'
14
+ require_relative 'xml/schema_validator'
15
+
16
+ module Facturx
17
+ class Writer
18
+ STAGES = [
19
+ Stages::DocumentContext,
20
+ Stages::ExchangedDocument,
21
+ Stages::TradeLines,
22
+ Stages::TradeAgreement,
23
+ Stages::TradeDelivery,
24
+ Stages::TradeSettlement
25
+ ].freeze
26
+ MODELED_TERM_IDS = STAGES.flat_map(&:term_ids).freeze
27
+
28
+ unless MODELED_TERM_IDS.sort == Terms.all.map(&:id).sort
29
+ raise 'Factur-X writer stages must cover every modeled term exactly once'
30
+ end
31
+
32
+ def initialize(schema_validator: Xml::SchemaValidator.new, tracker: Conformance::Tracker, stages: STAGES)
33
+ @schema_validator = schema_validator
34
+ @tracker_class = tracker
35
+ @stages = stages.freeze
36
+ end
37
+
38
+ def call(document:, profile:)
39
+ context = compile(document:, profile:)
40
+ report = context.tracker.report
41
+ raise_conformance_error(profile, report) if report.invalid?
42
+
43
+ @schema_validator.call(document: context.xml, profile:)
44
+ context.to_xml
45
+ end
46
+
47
+ def validate(document:, profile:)
48
+ compile(document:, profile:).tracker.report
49
+ end
50
+
51
+ private
52
+
53
+ def compile(document:, profile:)
54
+ tracker = @tracker_class.new(profile:)
55
+ tracker.check_document(document, path: Terms.fetch('BT-24').xpath)
56
+ context = Context.new(document:, profile:, tracker:)
57
+ @stages.each { |stage| stage.new(context).call }
58
+ context
59
+ end
60
+
61
+ def raise_conformance_error(profile, report)
62
+ raise ConformanceError.new(
63
+ 'Factur-X document does not conform to the selected profile',
64
+ profile: profile.id, report:, issues: report.issues
65
+ )
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ module Xml
5
+ module Namespaces
6
+ CII = 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'
7
+ QUALIFIED = 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100'
8
+ REUSABLE = 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'
9
+ UNQUALIFIED = 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'
10
+
11
+ MAP = {
12
+ 'qdt' => QUALIFIED,
13
+ 'ram' => REUSABLE,
14
+ 'rsm' => CII,
15
+ 'udt' => UNQUALIFIED
16
+ }.freeze
17
+ end
18
+ end
19
+ end
@@ -2,14 +2,12 @@
2
2
 
3
3
  require_relative '../profiles'
4
4
  require_relative '../error'
5
+ require_relative 'namespaces'
5
6
 
6
7
  module Facturx
7
8
  module Xml
8
9
  class ProfileDetector
9
- NAMESPACES = {
10
- 'ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
11
- 'rsm' => 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'
12
- }.freeze
10
+ NAMESPACES = Namespaces::MAP
13
11
  GUIDELINE_XPATH = '/rsm:CrossIndustryInvoice/rsm:ExchangedDocumentContext/' \
14
12
  'ram:GuidelineSpecifiedDocumentContextParameter/ram:ID'
15
13