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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -3
- data/README.md +40 -8
- 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/conformance.rb +163 -0
- data/lib/facturx/error.rb +3 -0
- data/lib/facturx/format.rb +85 -0
- data/lib/facturx/generate.rb +21 -0
- data/lib/facturx/profile_resolver.rb +40 -0
- 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/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.rb +68 -0
- data/lib/facturx/xml/namespaces.rb +19 -0
- data/lib/facturx/xml/profile_detector.rb +2 -4
- data/lib/facturx.rb +28 -0
- data/rbi/facturx.rbi +1090 -0
- 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
|
|