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,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
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ module TradeAgreementSupport
7
+ module Parties
8
+ private
9
+
10
+ def write_parties(parent)
11
+ seller(parent)
12
+ buyer(parent)
13
+ tax_representative(parent)
14
+ end
15
+
16
+ def seller(parent)
17
+ within_group('BG-4', document.seller, element: 'ram:SellerTradeParty', parent:,
18
+ represented_attributes: %i[address contact]) do |node, party|
19
+ write_seller_identity(node, party)
20
+ write_seller_details(node, party)
21
+ end
22
+ end
23
+
24
+ def write_seller_identity(node, party)
25
+ emit_party_identifiers(node, party.identifiers, 'BT-29', 'BT-29-1')
26
+ emit('BT-27', party.name, element: 'ram:Name', parent: node)
27
+ emit('BT-33', party.description, element: 'ram:Description', parent: node)
28
+ legal_organization(node, party, 'BT-30', 'BT-30-1', trading_name_id: 'BT-28')
29
+ end
30
+
31
+ def write_seller_details(node, party)
32
+ emit_contact(node, party.contact, 'BG-6', self.class::SELLER_CONTACT)
33
+ emit_address(node, party.address, 'BG-5', self.class::SELLER_ADDRESS)
34
+ electronic_address(node, party.electronic_address, 'BT-34', 'BT-34-1')
35
+ emit_tax_registration(node, 'BT-31', party.vat_identifier, 'VA')
36
+ emit_tax_registration(node, 'BT-32', party.tax_identifier, 'FC')
37
+ end
38
+
39
+ def buyer(parent)
40
+ within_group('BG-7', document.buyer, element: 'ram:BuyerTradeParty', parent:,
41
+ represented_attributes: %i[address contact]) do |node, party|
42
+ write_buyer_identity(node, party)
43
+ write_buyer_details(node, party)
44
+ end
45
+ end
46
+
47
+ def write_buyer_identity(node, party)
48
+ emit_party_identifiers(node, party.identifiers, 'BT-46', 'BT-46-1')
49
+ emit('BT-44', party.name, element: 'ram:Name', parent: node)
50
+ legal_organization(node, party, 'BT-47', 'BT-47-1', trading_name_id: 'BT-45')
51
+ end
52
+
53
+ def write_buyer_details(node, party)
54
+ emit_contact(node, party.contact, 'BG-9', self.class::BUYER_CONTACT)
55
+ emit_address(node, party.address, 'BG-8', self.class::BUYER_ADDRESS)
56
+ electronic_address(node, party.electronic_address, 'BT-49', 'BT-49-1')
57
+ emit_tax_registration(node, 'BT-48', party.vat_identifier, 'VA')
58
+ end
59
+
60
+ def tax_representative(parent)
61
+ within_group('BG-11', document.tax_representative, element: 'ram:SellerTaxRepresentativeTradeParty',
62
+ parent:,
63
+ represented_attributes: [:address]) do |node, party|
64
+ emit('BT-62', party.name, element: 'ram:Name', parent: node)
65
+ emit_address(node, party.address, 'BG-12', self.class::REPRESENTATIVE_ADDRESS)
66
+ emit_tax_registration(node, 'BT-63', party.vat_identifier, 'VA')
67
+ end
68
+ end
69
+
70
+ def legal_organization(parent, party, value_id, scheme_id, trading_name_id:)
71
+ legal = party.legal_registration
72
+ trading = party.trading_name
73
+ return observe_missing_legal_organization(value_id, scheme_id, trading_name_id) unless legal || trading
74
+
75
+ container(parent, 'ram:SpecifiedLegalOrganization') do |node|
76
+ emit_compound_identifier(node, legal, value_id:, scheme_id:, element: 'ram:ID')
77
+ emit(trading_name_id, trading, element: 'ram:TradingBusinessName', parent: node)
78
+ end
79
+ end
80
+
81
+ def observe_missing_legal_organization(value_id, scheme_id, trading_name_id)
82
+ observe?(value_id, nil)
83
+ observe?(scheme_id, nil, group_present: false)
84
+ observe?(trading_name_id, nil)
85
+ nil
86
+ end
87
+
88
+ def electronic_address(parent, identifier, value_id, scheme_id)
89
+ return observe_missing_identifier(value_id, scheme_id) unless identifier
90
+
91
+ container(parent, 'ram:URIUniversalCommunication') do |node|
92
+ emit_compound_identifier(node, identifier, value_id:, scheme_id:, element: 'ram:URIID')
93
+ end
94
+ end
95
+
96
+ def observe_missing_identifier(value_id, scheme_id)
97
+ observe?(value_id, nil)
98
+ observe?(scheme_id, nil, group_present: false)
99
+ nil
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ module TradeAgreementSupport
7
+ module References
8
+ private
9
+
10
+ def write_references(parent)
11
+ document_references(parent)
12
+ additional_references(parent)
13
+ project(parent)
14
+ end
15
+
16
+ def document_references(parent)
17
+ emit_document_reference(parent, 'BT-14', document.sales_order_reference,
18
+ 'ram:SellerOrderReferencedDocument')
19
+ emit_document_reference(parent, 'BT-13', document.purchase_order_reference,
20
+ 'ram:BuyerOrderReferencedDocument')
21
+ emit_document_reference(parent, 'BT-12', document.contract_reference, 'ram:ContractReferencedDocument')
22
+ end
23
+
24
+ def additional_references(parent)
25
+ typed_reference(parent, 'BT-17', document.tender_or_lot_reference, '50')
26
+ invoiced_object_reference(parent)
27
+ each_group('BG-24', document.supporting_documents, element: 'ram:AdditionalReferencedDocument',
28
+ parent:) do |node, item|
29
+ write_supporting_document(node, item)
30
+ end
31
+ end
32
+
33
+ def write_supporting_document(node, item)
34
+ emit('BT-122', item.reference, element: 'ram:IssuerAssignedID', parent: node)
35
+ emit('BT-124', item.external_location, element: 'ram:URIID', parent: node)
36
+ technical(node, 'ram:TypeCode', '916')
37
+ emit('BT-123', item.description, element: 'ram:Name', parent: node)
38
+ attachment(node, item)
39
+ end
40
+
41
+ def typed_reference(parent, id, value, type_code)
42
+ report_unrepresentable_reference_attributes(id, value, represented_attributes: [:id]) if value
43
+
44
+ unless value&.id
45
+ observe?(id, nil)
46
+ return
47
+ end
48
+
49
+ container(parent, 'ram:AdditionalReferencedDocument') do |node|
50
+ emit(id, value.id, element: 'ram:IssuerAssignedID', parent: node)
51
+ technical(node, 'ram:TypeCode', type_code)
52
+ end
53
+ end
54
+
55
+ def invoiced_object_reference(parent)
56
+ emit_invoiced_object(parent, document.invoiced_object_identifier, 'BT-18', 'BT-18-1')
57
+ end
58
+
59
+ def attachment(parent, item)
60
+ node = emit('BT-125', item.content, element: 'ram:AttachmentBinaryObject', parent:).first
61
+ present = !item.content.nil?
62
+ emit_attribute('BT-125-1', item.mime_code, node:, attribute: 'mimeCode', group_present: present)
63
+ emit_attribute('BT-125-2', item.filename, node:, attribute: 'filename', group_present: present)
64
+ end
65
+
66
+ def project(parent)
67
+ value = document.project_reference
68
+ return observe?('BT-11', nil) unless project_reference?(value)
69
+ return unless representable_project_reference?(value)
70
+
71
+ container(parent, 'ram:SpecifiedProcuringProject') do |node|
72
+ emit('BT-11', value.id, element: 'ram:ID', parent: node)
73
+ technical(node, 'ram:Name', value.name, default: value.id)
74
+ end
75
+ end
76
+
77
+ def project_reference?(value)
78
+ value && (value.id || value.name)
79
+ end
80
+
81
+ def representable_project_reference?(value)
82
+ report_unrepresentable_reference_attributes('BT-11', value, represented_attributes: %i[id name])
83
+ return true if value.id
84
+
85
+ unrepresentable('BT-11', 'Project reference requires an identifier')
86
+ false
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'trade_agreement/parties'
4
+ require_relative 'trade_agreement/references'
5
+
6
+ module Facturx
7
+ class Writer
8
+ module Stages
9
+ class TradeAgreement < Stage
10
+ include TradeAgreementSupport::Parties
11
+ include TradeAgreementSupport::References
12
+
13
+ SELLER_ADDRESS = {
14
+ postcode: 'BT-38', line_one: 'BT-35', line_two: 'BT-36', line_three: 'BT-162', city: 'BT-37',
15
+ country_code: 'BT-40', country_subdivision: 'BT-39'
16
+ }.freeze
17
+ BUYER_ADDRESS = {
18
+ postcode: 'BT-53', line_one: 'BT-50', line_two: 'BT-51', line_three: 'BT-163', city: 'BT-52',
19
+ country_code: 'BT-55', country_subdivision: 'BT-54'
20
+ }.freeze
21
+ REPRESENTATIVE_ADDRESS = {
22
+ postcode: 'BT-67', line_one: 'BT-64', line_two: 'BT-65', line_three: 'BT-164', city: 'BT-66',
23
+ country_code: 'BT-69', country_subdivision: 'BT-68'
24
+ }.freeze
25
+ SELLER_CONTACT = { name: 'BT-41', telephone: 'BT-42', email: 'BT-43' }.freeze
26
+ BUYER_CONTACT = { name: 'BT-56', telephone: 'BT-57', email: 'BT-58' }.freeze
27
+
28
+ term_ids(*%w[
29
+ BT-10 BT-29 BT-29-1 BT-27 BT-33 BT-30 BT-30-1 BT-28 BT-41 BT-42 BT-43 BT-38 BT-35 BT-36
30
+ BT-162 BT-37 BT-40 BT-39 BT-34 BT-34-1 BT-31 BT-32 BT-46 BT-46-1 BT-44 BT-47 BT-47-1 BT-45
31
+ BT-56 BT-57 BT-58 BT-53 BT-50 BT-51 BT-163 BT-52 BT-55 BT-54 BT-49 BT-49-1 BT-48 BT-62 BT-67
32
+ BT-64 BT-65 BT-164 BT-66 BT-69 BT-68 BT-63 BT-14 BT-13 BT-12 BT-17 BT-18 BT-18-1 BT-122 BT-124
33
+ BT-123 BT-125 BT-125-1 BT-125-2 BT-11
34
+ ])
35
+
36
+ def call
37
+ agreement = context.element(context.transaction, 'ram:ApplicableHeaderTradeAgreement')
38
+ emit('BT-10', document.buyer_reference, element: 'ram:BuyerReference', parent: agreement)
39
+ write_parties(agreement)
40
+ write_references(agreement)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ class TradeDelivery < Stage
7
+ ADDRESS_IDS = {
8
+ postcode: 'BT-78', line_one: 'BT-75', line_two: 'BT-76', line_three: 'BT-165', city: 'BT-77',
9
+ country_code: 'BT-80', country_subdivision: 'BT-79'
10
+ }.freeze
11
+
12
+ term_ids(*%w[BT-71 BT-71-1 BT-70 BT-78 BT-75 BT-76 BT-165 BT-77 BT-80 BT-79 BT-72 BT-16 BT-15])
13
+
14
+ def call
15
+ value = document.delivery
16
+ node = context.element(context.transaction, 'ram:ApplicableHeaderTradeDelivery')
17
+ delivery(node, value) if delivery_accepted?(value)
18
+ references(node)
19
+ end
20
+
21
+ private
22
+
23
+ def delivery_accepted?(value)
24
+ group = Terms.group('BG-13-00')
25
+ tracker.observe_group?(group, count: value ? 1 : 0, path: group.xpath)
26
+ end
27
+
28
+ def references(parent)
29
+ emit_document_reference(parent, 'BT-16', document.despatch_advice_reference,
30
+ 'ram:DespatchAdviceReferencedDocument')
31
+ emit_document_reference(parent, 'BT-15', document.receiving_advice_reference,
32
+ 'ram:ReceivingAdviceReferencedDocument')
33
+ end
34
+
35
+ def delivery(parent, value)
36
+ ship_to(parent, value)
37
+ delivery_date(parent, value.date)
38
+ end
39
+
40
+ def delivery_date(parent, value)
41
+ unless value
42
+ observe?('BT-72', nil)
43
+ return
44
+ end
45
+
46
+ container(parent, 'ram:ActualDeliverySupplyChainEvent') do |event|
47
+ emit_date(event, 'BT-72', value, wrapper: 'ram:OccurrenceDateTime')
48
+ end
49
+ end
50
+
51
+ def ship_to(parent, delivery)
52
+ party = delivery.party
53
+ location = delivery.location_identifier
54
+ present = party || location
55
+ within_group('BG-13', present, element: 'ram:ShipToTradeParty', parent:,
56
+ represented_attributes: %i[name address]) do |node,|
57
+ delivery_identifier(node, location)
58
+ emit('BT-70', party&.name, element: 'ram:Name', parent: node)
59
+ emit_address(node, party&.address, 'BG-15', ADDRESS_IDS)
60
+ end
61
+ end
62
+
63
+ def delivery_identifier(parent, value)
64
+ if value&.scheme_id
65
+ node = if observe?('BT-71', value.value)
66
+ technical(parent, 'ram:GlobalID', format_identifier_value('BT-71', value.value))
67
+ end
68
+ emit_attribute('BT-71-1', value.scheme_id, node:, attribute: 'schemeID', group_present: !node.nil?)
69
+ else
70
+ emit('BT-71', value&.value, element: 'ram:ID', parent:)
71
+ observe?('BT-71-1', nil, group_present: false)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ class TradeLines
7
+ class Agreement < Stage
8
+ PRICES = {
9
+ gross: { ids: %w[BT-148 BT-149-1 BT-150-1], element: 'ram:GrossPriceProductTradePrice' },
10
+ net: { ids: %w[BT-146 BT-149 BT-150], element: 'ram:NetPriceProductTradePrice' }
11
+ }.freeze
12
+
13
+ def initialize(context, quantity:)
14
+ super(context)
15
+ @quantity = quantity
16
+ end
17
+
18
+ def call(parent:, line:)
19
+ within_group('BG-29', line, element: 'ram:SpecifiedLineTradeAgreement', parent:) do |node, item|
20
+ buyer_order_reference(node, item.buyer_order_reference)
21
+ price(node, item.gross_price, kind: :gross)
22
+ price(node, item.net_price, kind: :net)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def buyer_order_reference(parent, value)
29
+ return observe?('BT-132', nil) unless value
30
+
31
+ report_unrepresentable_reference_attributes('BT-132', value, represented_attributes: [:line_id])
32
+
33
+ container(parent, 'ram:BuyerOrderReferencedDocument') do |node|
34
+ emit('BT-132', value.line_id, element: 'ram:LineID', parent: node)
35
+ end
36
+ end
37
+
38
+ def price(parent, value, kind:)
39
+ config = PRICES.fetch(kind)
40
+ return observe_missing_price(config, kind:) unless value
41
+ return unrepresentable(config.fetch(:ids).first, 'Price requires an amount') unless value.amount
42
+
43
+ represented_attributes = %i[amount basis_quantity]
44
+ represented_attributes << :discount if kind == :gross
45
+ report_unrepresentable_attributes('BG-29', value, represented_attributes:)
46
+
47
+ container(parent, config.fetch(:element)) do |node|
48
+ emit_price(node, value, config.fetch(:ids))
49
+ gross_discount(node, value.discount) if kind == :gross
50
+ end
51
+ end
52
+
53
+ def emit_price(parent, value, ids)
54
+ emit(ids[0], value.amount, element: 'ram:ChargeAmount', parent:)
55
+ @quantity.call(parent:, value: value.basis_quantity, amount_id: ids[1], unit_id: ids[2],
56
+ element: 'ram:BasisQuantity')
57
+ end
58
+
59
+ def observe_missing_price(config, kind:)
60
+ config.fetch(:ids).each { |id| observe?(id, nil) }
61
+ return unless kind == :gross
62
+
63
+ observe?('BT-147-01', nil)
64
+ observe?('BT-147', nil)
65
+ end
66
+
67
+ def gross_discount(parent, value)
68
+ return observe_missing_discount? unless value
69
+ return unless observe?('BT-147-01', false) && observe?('BT-147-02', false)
70
+
71
+ emit_discount(parent, value)
72
+ end
73
+
74
+ def observe_missing_discount?
75
+ observe?('BT-147-01', nil)
76
+ observe?('BT-147-02', nil, group_present: false)
77
+ observe?('BT-147', nil)
78
+ end
79
+
80
+ def emit_discount(parent, value)
81
+ container(parent, 'ram:AppliedTradeAllowanceCharge') do |node|
82
+ container(node, 'ram:ChargeIndicator') do |indicator|
83
+ technical(indicator, 'udt:Indicator', format_identifier_value('BT-147-02', false))
84
+ end
85
+ emit('BT-147', value, element: 'ram:ActualAmount', parent: node)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ class TradeLines
7
+ class Product < Stage
8
+ def call(parent:, value:)
9
+ within_group('BG-31', value, element: 'ram:SpecifiedTradeProduct', parent:,
10
+ represented_attributes: %i[attributes classifications]) do |node, item|
11
+ identifiers(node, item)
12
+ emit('BT-153', item.name, element: 'ram:Name', parent: node)
13
+ emit('BT-154', item.description, element: 'ram:Description', parent: node)
14
+ attributes(node, item.attributes)
15
+ classifications(node, item.classifications)
16
+ origin_country(node, item.origin_country_code)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def identifiers(parent, item)
23
+ emit_compound_identifier(parent, item.global_identifier, value_id: 'BT-157', scheme_id: 'BT-157-1',
24
+ element: 'ram:GlobalID')
25
+ emit_identifier_value('BT-155', item.seller_identifier, 'ram:SellerAssignedID', parent)
26
+ emit_identifier_value('BT-156', item.buyer_identifier, 'ram:BuyerAssignedID', parent)
27
+ end
28
+
29
+ def attributes(parent, values)
30
+ each_group('BG-32', values, element: 'ram:ApplicableProductCharacteristic', parent:) do |node, item|
31
+ emit('BT-160', item.name, element: 'ram:Description', parent: node)
32
+ emit('BT-161', item.value, element: 'ram:Value', parent: node)
33
+ end
34
+ end
35
+
36
+ def classifications(parent, values)
37
+ emit_unrepresentable_classifications(parent, values)
38
+ emit_representable_classifications(parent, values)
39
+ end
40
+
41
+ def emit_unrepresentable_classifications(parent, values)
42
+ Array(values).reject { |item| classification_code?(item) }.each do |item|
43
+ classification(parent, item)
44
+ end
45
+ end
46
+
47
+ def emit_representable_classifications(parent, values)
48
+ items = Array(values).select { |item| classification_code?(item) }
49
+ return unless observe?('BT-158', items.map(&:code))
50
+
51
+ items.each { |item| classification(parent, item) }
52
+ end
53
+
54
+ def classification_code?(item)
55
+ !item.code.to_s.strip.empty?
56
+ end
57
+
58
+ def classification(parent, item)
59
+ container(parent, 'ram:DesignatedProductClassification') do |node|
60
+ code = technical(node, 'ram:ClassCode', format_identifier_value('BT-158', item.code))
61
+ emit_attribute('BT-158-1', item.list_id, node: code, attribute: 'listID', group_present: !code.nil?)
62
+ emit_attribute('BT-158-2', item.list_version_id, node: code, attribute: 'listVersionID',
63
+ group_present: !code.nil?)
64
+ end
65
+ end
66
+
67
+ def origin_country(parent, value)
68
+ return observe?('BT-159', nil) unless value
69
+
70
+ container(parent, 'ram:OriginTradeCountry') do |node|
71
+ emit('BT-159', value, element: 'ram:ID', parent: node)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facturx
4
+ class Writer
5
+ module Stages
6
+ class TradeLines
7
+ class Quantity < Stage
8
+ def call(parent:, value:, amount_id:, unit_id:, element:)
9
+ node = emit(amount_id, value&.value, element:, parent:).first
10
+ emit_attribute(unit_id, value&.unit_code, node:, attribute: 'unitCode',
11
+ group_present: !node.nil?)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end