zugpferd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1bafe4631b8105289e0f6c6efba696b91e0502cfbf99173918dc0d57de096263
4
+ data.tar.gz: 3afe5b0d992a4622f3eadfabfbc1b4495ff5deca092f15eb659b5a8b4b529f23
5
+ SHA512:
6
+ metadata.gz: 8c50bedc1c561b09d28fb69c808dcc4b0e80b655e9a2c69a138229f05752ff4898ac64fd6bf144a4eb3d0cfdcfa84742576a64565097a7ae9f4e666c686fb06e
7
+ data.tar.gz: ad2b0b0bc923f4debd510f40a9e1b296212af4fcbff0df9af0df5c295b2608e01430c80993000257de81d8d247c30939143567d9a8efa80805f02028dd36c99e
@@ -0,0 +1,153 @@
1
+ module Zugpferd
2
+ module CII
3
+ module Mapping
4
+ NS = {
5
+ "rsm" => "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100",
6
+ "ram" => "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100",
7
+ "udt" => "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100",
8
+ "qdt" => "urn:un:unece:uncefact:data:standard:QualifiedDataType:100",
9
+ }.freeze
10
+
11
+ # Document context
12
+ CONTEXT = "rsm:ExchangedDocumentContext"
13
+ DOCUMENT = "rsm:ExchangedDocument"
14
+ TRANSACTION = "rsm:SupplyChainTradeTransaction"
15
+
16
+ # Invoice header (BG-0)
17
+ INVOICE = {
18
+ number: "#{DOCUMENT}/ram:ID",
19
+ issue_date: "#{DOCUMENT}/ram:IssueDateTime/udt:DateTimeString",
20
+ type_code: "#{DOCUMENT}/ram:TypeCode",
21
+ note: "#{DOCUMENT}/ram:IncludedNote/ram:Content",
22
+ customization_id: "#{CONTEXT}/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID",
23
+ profile_id: "#{CONTEXT}/ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID",
24
+ }.freeze
25
+
26
+ # Settlement (contains currency, payment, tax, totals)
27
+ SETTLEMENT = "#{TRANSACTION}/ram:ApplicableHeaderTradeSettlement"
28
+ AGREEMENT = "#{TRANSACTION}/ram:ApplicableHeaderTradeAgreement"
29
+
30
+ INVOICE_SETTLEMENT = {
31
+ currency_code: "#{SETTLEMENT}/ram:InvoiceCurrencyCode",
32
+ buyer_reference: "#{AGREEMENT}/ram:BuyerReference",
33
+ }.freeze
34
+
35
+ # Seller (BG-4)
36
+ SELLER = "#{AGREEMENT}/ram:SellerTradeParty"
37
+ # Buyer (BG-7)
38
+ BUYER = "#{AGREEMENT}/ram:BuyerTradeParty"
39
+
40
+ # TradeParty fields
41
+ PARTY = {
42
+ name: "ram:Name",
43
+ trading_name: "ram:SpecifiedLegalOrganization/ram:TradingBusinessName",
44
+ identifier: "ram:ID",
45
+ legal_registration_id: "ram:SpecifiedLegalOrganization/ram:ID",
46
+ legal_form: "ram:Description",
47
+ vat_identifier: "ram:SpecifiedTaxRegistration/ram:ID[@schemeID='VA']",
48
+ electronic_address: "ram:URIUniversalCommunication/ram:URIID",
49
+ }.freeze
50
+
51
+ # PostalAddress (BG-5 / BG-8)
52
+ POSTAL_ADDRESS = "ram:PostalTradeAddress"
53
+ ADDRESS = {
54
+ street_name: "ram:LineOne",
55
+ city_name: "ram:CityName",
56
+ postal_zone: "ram:PostcodeCode",
57
+ country_code: "ram:CountryID",
58
+ }.freeze
59
+
60
+ # Contact (BG-6 / BG-9)
61
+ CONTACT = "ram:DefinedTradeContact"
62
+ CONTACT_FIELDS = {
63
+ name: "ram:PersonName",
64
+ telephone: "ram:TelephoneUniversalCommunication/ram:CompleteNumber",
65
+ email: "ram:EmailURIUniversalCommunication/ram:URIID",
66
+ }.freeze
67
+
68
+ # PaymentMeans (BG-16)
69
+ PAYMENT_MEANS = "ram:SpecifiedTradeSettlementPaymentMeans"
70
+ PAYMENT = {
71
+ payment_means_code: "ram:TypeCode",
72
+ account_id: "ram:PayeePartyCreditorFinancialAccount/ram:IBANID",
73
+ card_account_id: "ram:ApplicableTradeSettlementFinancialCard/ram:ID",
74
+ card_holder_name: "ram:ApplicableTradeSettlementFinancialCard/ram:CardholderName",
75
+ debited_account_id: "ram:PayerPartyDebtorFinancialAccount/ram:IBANID",
76
+ }.freeze
77
+ CREDITOR_REFERENCE_ID = "ram:CreditorReferenceID"
78
+ PAYMENT_REFERENCE = "ram:PaymentReference"
79
+ PAYMENT_TERMS_NOTE = "ram:SpecifiedTradePaymentTerms/ram:Description"
80
+ PAYMENT_TERMS_DUE_DATE = "ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString"
81
+ PAYMENT_TERMS_MANDATE = "ram:SpecifiedTradePaymentTerms/ram:DirectDebitMandateID"
82
+
83
+ # TaxTotal (BG-23)
84
+ TAX_SUBTOTAL = "ram:ApplicableTradeTax"
85
+ TAX = {
86
+ taxable_amount: "ram:BasisAmount",
87
+ tax_amount: "ram:CalculatedAmount",
88
+ category_code: "ram:CategoryCode",
89
+ percent: "ram:RateApplicablePercent",
90
+ exemption_reason: "ram:ExemptionReason",
91
+ exemption_reason_code: "ram:ExemptionReasonCode",
92
+ }.freeze
93
+
94
+ # LegalMonetaryTotal (BG-22)
95
+ MONETARY_TOTAL = "ram:SpecifiedTradeSettlementHeaderMonetarySummation"
96
+ TOTALS = {
97
+ line_extension_amount: "ram:LineTotalAmount",
98
+ tax_exclusive_amount: "ram:TaxBasisTotalAmount",
99
+ tax_inclusive_amount: "ram:GrandTotalAmount",
100
+ prepaid_amount: "ram:TotalPrepaidAmount",
101
+ payable_rounding_amount: "ram:RoundingAmount",
102
+ payable_amount: "ram:DuePayableAmount",
103
+ tax_total_amount: "ram:TaxTotalAmount",
104
+ allowance_total_amount: "ram:AllowanceTotalAmount",
105
+ charge_total_amount: "ram:ChargeTotalAmount",
106
+ }.freeze
107
+
108
+ # AllowanceCharge (BG-20 / BG-21)
109
+ ALLOWANCE_CHARGE = "ram:SpecifiedTradeAllowanceCharge"
110
+ ALLOWANCE_CHARGE_FIELDS = {
111
+ charge_indicator: "ram:ChargeIndicator/udt:Indicator",
112
+ reason: "ram:Reason",
113
+ reason_code: "ram:ReasonCode",
114
+ amount: "ram:ActualAmount",
115
+ base_amount: "ram:BasisAmount",
116
+ multiplier_factor: "ram:CalculationPercent",
117
+ tax_category_code: "ram:CategoryTradeTax/ram:CategoryCode",
118
+ tax_percent: "ram:CategoryTradeTax/ram:RateApplicablePercent",
119
+ }.freeze
120
+
121
+ # InvoiceLine (BG-25)
122
+ INVOICE_LINE = "#{TRANSACTION}/ram:IncludedSupplyChainTradeLineItem"
123
+ LINE = {
124
+ id: "ram:AssociatedDocumentLineDocument/ram:LineID",
125
+ note: "ram:AssociatedDocumentLineDocument/ram:IncludedNote/ram:Content",
126
+ invoiced_quantity: "ram:SpecifiedLineTradeDelivery/ram:BilledQuantity",
127
+ unit_code: "ram:SpecifiedLineTradeDelivery/ram:BilledQuantity/@unitCode",
128
+ line_extension_amount: "ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount",
129
+ }.freeze
130
+
131
+ # Item (BG-31)
132
+ ITEM = "ram:SpecifiedTradeProduct"
133
+ ITEM_FIELDS = {
134
+ name: "ram:Name",
135
+ description: "ram:Description",
136
+ sellers_identifier: "ram:SellerAssignedID",
137
+ }.freeze
138
+
139
+ # Item tax (from line settlement, not from product)
140
+ ITEM_TAX = "ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax"
141
+ ITEM_TAX_FIELDS = {
142
+ tax_category: "ram:CategoryCode",
143
+ tax_percent: "ram:RateApplicablePercent",
144
+ }.freeze
145
+
146
+ # Price (BG-29)
147
+ PRICE = "ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice"
148
+ PRICE_FIELDS = {
149
+ amount: "ram:ChargeAmount",
150
+ }.freeze
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,219 @@
1
+ require "nokogiri"
2
+ require "date"
3
+ require "bigdecimal"
4
+ require_relative "mapping"
5
+
6
+ module Zugpferd
7
+ module CII
8
+ # Reads UN/CEFACT CII CrossIndustryInvoice XML into {Model::Invoice}.
9
+ #
10
+ # @example
11
+ # invoice = Zugpferd::CII::Reader.new.read(File.read("invoice.xml"))
12
+ class Reader
13
+ include Mapping
14
+
15
+ # Parses a CII CrossIndustryInvoice XML string.
16
+ #
17
+ # @param xml_string [String] valid CII D16B XML
18
+ # @return [Model::Invoice]
19
+ # @raise [Nokogiri::XML::SyntaxError] if the XML is malformed
20
+ def read(xml_string)
21
+ doc = Nokogiri::XML(xml_string) { |config| config.strict }
22
+ root = doc.root
23
+ build_invoice(root)
24
+ end
25
+
26
+ private
27
+
28
+ def build_invoice(root)
29
+ settlement = root.at_xpath(SETTLEMENT, NS)
30
+ Model::Invoice.new(
31
+ number: text(root, INVOICE[:number]),
32
+ issue_date: parse_cii_date(text(root, INVOICE[:issue_date])),
33
+ due_date: parse_cii_date(settlement ? text(settlement, PAYMENT_TERMS_DUE_DATE) : nil),
34
+ type_code: text(root, INVOICE[:type_code]),
35
+ currency_code: text(root, INVOICE_SETTLEMENT[:currency_code]),
36
+ buyer_reference: text(root, INVOICE_SETTLEMENT[:buyer_reference]),
37
+ customization_id: text(root, INVOICE[:customization_id]),
38
+ profile_id: text(root, INVOICE[:profile_id]),
39
+ note: text(root, INVOICE[:note]),
40
+ seller: build_party(root.at_xpath(SELLER, NS)),
41
+ buyer: build_party(root.at_xpath(BUYER, NS)),
42
+ line_items: root.xpath(INVOICE_LINE, NS).map { |n| build_line_item(n) },
43
+ allowance_charges: settlement ? build_allowance_charges(settlement) : [],
44
+ tax_breakdown: build_tax_breakdown(settlement),
45
+ monetary_totals: build_monetary_totals(settlement&.at_xpath(MONETARY_TOTAL, NS)),
46
+ payment_instructions: build_payment_instructions(settlement),
47
+ )
48
+ end
49
+
50
+ def build_party(node)
51
+ return nil unless node
52
+
53
+ party = Model::TradeParty.new(
54
+ name: text(node, PARTY[:name]),
55
+ trading_name: text(node, PARTY[:trading_name]),
56
+ identifier: text(node, PARTY[:identifier]),
57
+ legal_registration_id: text(node, PARTY[:legal_registration_id]),
58
+ legal_form: text(node, PARTY[:legal_form]),
59
+ vat_identifier: text(node, PARTY[:vat_identifier]),
60
+ electronic_address: text(node, PARTY[:electronic_address]),
61
+ )
62
+
63
+ endpoint = node.at_xpath(PARTY[:electronic_address], NS)
64
+ party.electronic_address_scheme = endpoint["schemeID"] if endpoint
65
+
66
+ addr_node = node.at_xpath(POSTAL_ADDRESS, NS)
67
+ party.postal_address = build_postal_address(addr_node) if addr_node
68
+
69
+ contact_node = node.at_xpath(CONTACT, NS)
70
+ party.contact = build_contact(contact_node) if contact_node
71
+
72
+ party
73
+ end
74
+
75
+ def build_postal_address(node)
76
+ Model::PostalAddress.new(
77
+ country_code: text(node, ADDRESS[:country_code]),
78
+ street_name: text(node, ADDRESS[:street_name]),
79
+ city_name: text(node, ADDRESS[:city_name]),
80
+ postal_zone: text(node, ADDRESS[:postal_zone]),
81
+ )
82
+ end
83
+
84
+ def build_contact(node)
85
+ Model::Contact.new(
86
+ name: text(node, CONTACT_FIELDS[:name]),
87
+ telephone: text(node, CONTACT_FIELDS[:telephone]),
88
+ email: text(node, CONTACT_FIELDS[:email]),
89
+ )
90
+ end
91
+
92
+ def build_payment_instructions(settlement_node)
93
+ return nil unless settlement_node
94
+
95
+ means_node = settlement_node.at_xpath(PAYMENT_MEANS, NS)
96
+ return nil unless means_node
97
+
98
+ Model::PaymentInstructions.new(
99
+ payment_means_code: text(means_node, PAYMENT[:payment_means_code]),
100
+ payment_id: text(settlement_node, PAYMENT_REFERENCE),
101
+ account_id: text(means_node, PAYMENT[:account_id]),
102
+ card_account_id: text(means_node, PAYMENT[:card_account_id]),
103
+ card_holder_name: text(means_node, PAYMENT[:card_holder_name]),
104
+ debited_account_id: text(means_node, PAYMENT[:debited_account_id]),
105
+ creditor_reference_id: text(settlement_node, CREDITOR_REFERENCE_ID),
106
+ mandate_reference: text(settlement_node, PAYMENT_TERMS_MANDATE),
107
+ note: text(settlement_node, PAYMENT_TERMS_NOTE),
108
+ )
109
+ end
110
+
111
+ def build_tax_breakdown(settlement_node)
112
+ return nil unless settlement_node
113
+
114
+ totals_node = settlement_node.at_xpath(MONETARY_TOTAL, NS)
115
+ tax_total_node = totals_node&.at_xpath(TOTALS[:tax_total_amount], NS)
116
+ currency = tax_total_node&.[]("currencyID")
117
+
118
+ breakdown = Model::TaxBreakdown.new(
119
+ tax_amount: tax_total_node&.text,
120
+ currency_code: currency,
121
+ )
122
+
123
+ breakdown.subtotals = settlement_node.xpath(TAX_SUBTOTAL, NS).map do |sub|
124
+ Model::TaxSubtotal.new(
125
+ taxable_amount: text(sub, TAX[:taxable_amount]),
126
+ tax_amount: text(sub, TAX[:tax_amount]),
127
+ category_code: text(sub, TAX[:category_code]),
128
+ percent: parse_decimal(text(sub, TAX[:percent])),
129
+ currency_code: currency,
130
+ exemption_reason: text(sub, TAX[:exemption_reason]),
131
+ exemption_reason_code: text(sub, TAX[:exemption_reason_code]),
132
+ )
133
+ end
134
+
135
+ breakdown
136
+ end
137
+
138
+ def build_monetary_totals(node)
139
+ return nil unless node
140
+
141
+ Model::MonetaryTotals.new(
142
+ line_extension_amount: text(node, TOTALS[:line_extension_amount]),
143
+ tax_exclusive_amount: text(node, TOTALS[:tax_exclusive_amount]),
144
+ tax_inclusive_amount: text(node, TOTALS[:tax_inclusive_amount]),
145
+ prepaid_amount: parse_decimal(text(node, TOTALS[:prepaid_amount])),
146
+ payable_rounding_amount: parse_decimal(text(node, TOTALS[:payable_rounding_amount])),
147
+ allowance_total_amount: parse_decimal(text(node, TOTALS[:allowance_total_amount])),
148
+ charge_total_amount: parse_decimal(text(node, TOTALS[:charge_total_amount])),
149
+ payable_amount: text(node, TOTALS[:payable_amount]),
150
+ )
151
+ end
152
+
153
+ def build_line_item(node)
154
+ item_node = node.at_xpath(ITEM, NS)
155
+ price_node = node.at_xpath(PRICE, NS)
156
+ tax_node = node.at_xpath(ITEM_TAX, NS)
157
+
158
+ Model::LineItem.new(
159
+ id: text(node, LINE[:id]),
160
+ invoiced_quantity: text(node, LINE[:invoiced_quantity]),
161
+ unit_code: node.at_xpath(LINE[:unit_code], NS)&.text,
162
+ line_extension_amount: text(node, LINE[:line_extension_amount]),
163
+ note: text(node, LINE[:note]),
164
+ item: build_item(item_node, tax_node),
165
+ price: build_price(price_node),
166
+ )
167
+ end
168
+
169
+ def build_item(node, tax_node)
170
+ return nil unless node
171
+
172
+ Model::Item.new(
173
+ name: text(node, ITEM_FIELDS[:name]),
174
+ description: text(node, ITEM_FIELDS[:description]),
175
+ sellers_identifier: text(node, ITEM_FIELDS[:sellers_identifier]),
176
+ tax_category: tax_node ? text(tax_node, ITEM_TAX_FIELDS[:tax_category]) : nil,
177
+ tax_percent: tax_node ? parse_decimal(text(tax_node, ITEM_TAX_FIELDS[:tax_percent])) : nil,
178
+ )
179
+ end
180
+
181
+ def build_price(node)
182
+ return nil unless node
183
+
184
+ Model::Price.new(
185
+ amount: text(node, PRICE_FIELDS[:amount]),
186
+ )
187
+ end
188
+
189
+ def build_allowance_charges(settlement_node)
190
+ settlement_node.xpath(ALLOWANCE_CHARGE, NS).map do |node|
191
+ Model::AllowanceCharge.new(
192
+ charge_indicator: text(node, ALLOWANCE_CHARGE_FIELDS[:charge_indicator]) == "true",
193
+ reason: text(node, ALLOWANCE_CHARGE_FIELDS[:reason]),
194
+ reason_code: text(node, ALLOWANCE_CHARGE_FIELDS[:reason_code]),
195
+ amount: text(node, ALLOWANCE_CHARGE_FIELDS[:amount]),
196
+ base_amount: parse_decimal(text(node, ALLOWANCE_CHARGE_FIELDS[:base_amount])),
197
+ multiplier_factor: parse_decimal(text(node, ALLOWANCE_CHARGE_FIELDS[:multiplier_factor])),
198
+ tax_category_code: text(node, ALLOWANCE_CHARGE_FIELDS[:tax_category_code]),
199
+ tax_percent: parse_decimal(text(node, ALLOWANCE_CHARGE_FIELDS[:tax_percent])),
200
+ )
201
+ end
202
+ end
203
+
204
+ def text(node, xpath)
205
+ node.at_xpath(xpath, NS)&.text
206
+ end
207
+
208
+ def parse_cii_date(str)
209
+ return nil unless str
210
+ # CII format 102 = YYYYMMDD
211
+ Date.strptime(str, "%Y%m%d")
212
+ end
213
+
214
+ def parse_decimal(str)
215
+ BigDecimal(str) if str
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,314 @@
1
+ require "nokogiri"
2
+ require_relative "mapping"
3
+
4
+ module Zugpferd
5
+ module CII
6
+ # Writes {Model::Invoice} to UN/CEFACT CII CrossIndustryInvoice XML.
7
+ #
8
+ # @example
9
+ # xml = Zugpferd::CII::Writer.new.write(invoice)
10
+ class Writer
11
+ include Mapping
12
+
13
+ # Serializes an invoice to CII D16B XML.
14
+ #
15
+ # @param invoice [Model::Invoice] the invoice to serialize
16
+ # @return [String] UTF-8 encoded XML string
17
+ def write(invoice)
18
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
19
+ xml["rsm"].CrossIndustryInvoice(
20
+ "xmlns:rsm" => NS["rsm"],
21
+ "xmlns:ram" => NS["ram"],
22
+ "xmlns:qdt" => NS["qdt"],
23
+ "xmlns:udt" => NS["udt"]
24
+ ) do
25
+ build_document_context(xml, invoice)
26
+ build_exchanged_document(xml, invoice)
27
+ build_transaction(xml, invoice)
28
+ end
29
+ end
30
+ builder.to_xml
31
+ end
32
+
33
+ private
34
+
35
+ def build_document_context(xml, inv)
36
+ xml["rsm"].ExchangedDocumentContext do
37
+ if inv.profile_id
38
+ xml["ram"].BusinessProcessSpecifiedDocumentContextParameter do
39
+ xml["ram"].ID inv.profile_id
40
+ end
41
+ end
42
+ if inv.customization_id
43
+ xml["ram"].GuidelineSpecifiedDocumentContextParameter do
44
+ xml["ram"].ID inv.customization_id
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def build_exchanged_document(xml, inv)
51
+ xml["rsm"].ExchangedDocument do
52
+ xml["ram"].ID inv.number
53
+ xml["ram"].TypeCode inv.type_code
54
+ xml["ram"].IssueDateTime do
55
+ xml["udt"].DateTimeString(format_cii_date(inv.issue_date), format: "102")
56
+ end
57
+ if inv.note
58
+ xml["ram"].IncludedNote do
59
+ xml["ram"].Content inv.note
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def build_transaction(xml, inv)
66
+ xml["rsm"].SupplyChainTradeTransaction do
67
+ inv.line_items.each { |li| build_line_item(xml, li) }
68
+ build_agreement(xml, inv)
69
+ xml["ram"].ApplicableHeaderTradeDelivery
70
+ build_settlement(xml, inv)
71
+ end
72
+ end
73
+
74
+ def build_agreement(xml, inv)
75
+ xml["ram"].ApplicableHeaderTradeAgreement do
76
+ xml["ram"].BuyerReference inv.buyer_reference if inv.buyer_reference
77
+ build_party(xml, "SellerTradeParty", inv.seller) if inv.seller
78
+ build_party(xml, "BuyerTradeParty", inv.buyer) if inv.buyer
79
+ end
80
+ end
81
+
82
+ def build_party(xml, element_name, party)
83
+ xml["ram"].send(element_name) do
84
+ if party.identifier
85
+ xml["ram"].ID party.identifier
86
+ end
87
+
88
+ xml["ram"].Name party.name
89
+
90
+ xml["ram"].Description party.legal_form if party.legal_form
91
+
92
+ if party.legal_registration_id || party.trading_name
93
+ xml["ram"].SpecifiedLegalOrganization do
94
+ xml["ram"].ID party.legal_registration_id if party.legal_registration_id
95
+ xml["ram"].TradingBusinessName party.trading_name if party.trading_name
96
+ end
97
+ end
98
+
99
+ build_contact(xml, party.contact) if party.contact
100
+
101
+ build_postal_address(xml, party.postal_address) if party.postal_address
102
+
103
+ if party.electronic_address
104
+ xml["ram"].URIUniversalCommunication do
105
+ attrs = {}
106
+ attrs[:schemeID] = party.electronic_address_scheme if party.electronic_address_scheme
107
+ xml["ram"].URIID(party.electronic_address, attrs)
108
+ end
109
+ end
110
+
111
+ if party.vat_identifier
112
+ xml["ram"].SpecifiedTaxRegistration do
113
+ xml["ram"].ID(party.vat_identifier, schemeID: "VA")
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ def build_postal_address(xml, addr)
120
+ xml["ram"].PostalTradeAddress do
121
+ xml["ram"].PostcodeCode addr.postal_zone if addr.postal_zone
122
+ xml["ram"].LineOne addr.street_name if addr.street_name
123
+ xml["ram"].CityName addr.city_name if addr.city_name
124
+ xml["ram"].CountryID addr.country_code
125
+ end
126
+ end
127
+
128
+ def build_contact(xml, contact)
129
+ xml["ram"].DefinedTradeContact do
130
+ xml["ram"].PersonName contact.name if contact.name
131
+ if contact.telephone
132
+ xml["ram"].TelephoneUniversalCommunication do
133
+ xml["ram"].CompleteNumber contact.telephone
134
+ end
135
+ end
136
+ if contact.email
137
+ xml["ram"].EmailURIUniversalCommunication do
138
+ xml["ram"].URIID contact.email
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ def build_settlement(xml, inv)
145
+ xml["ram"].ApplicableHeaderTradeSettlement do
146
+ if inv.payment_instructions&.creditor_reference_id
147
+ xml["ram"].CreditorReferenceID inv.payment_instructions.creditor_reference_id
148
+ end
149
+
150
+ if inv.payment_instructions&.payment_id
151
+ xml["ram"].PaymentReference inv.payment_instructions.payment_id
152
+ end
153
+
154
+ xml["ram"].InvoiceCurrencyCode inv.currency_code
155
+
156
+ build_payment_means(xml, inv.payment_instructions) if inv.payment_instructions
157
+
158
+ if inv.tax_breakdown
159
+ inv.tax_breakdown.subtotals.each do |sub|
160
+ build_tax_subtotal(xml, sub)
161
+ end
162
+ end
163
+
164
+ inv.allowance_charges.each { |ac| build_allowance_charge(xml, ac) }
165
+
166
+ if inv.payment_instructions&.note || inv.due_date || inv.payment_instructions&.mandate_reference
167
+ xml["ram"].SpecifiedTradePaymentTerms do
168
+ xml["ram"].Description inv.payment_instructions.note if inv.payment_instructions&.note
169
+ if inv.due_date
170
+ xml["ram"].DueDateDateTime do
171
+ xml["udt"].DateTimeString(format_cii_date(inv.due_date), format: "102")
172
+ end
173
+ end
174
+ xml["ram"].DirectDebitMandateID inv.payment_instructions.mandate_reference if inv.payment_instructions&.mandate_reference
175
+ end
176
+ end
177
+
178
+ build_monetary_total(xml, inv.monetary_totals, inv.tax_breakdown) if inv.monetary_totals
179
+ end
180
+ end
181
+
182
+ def build_payment_means(xml, payment)
183
+ xml["ram"].SpecifiedTradeSettlementPaymentMeans do
184
+ xml["ram"].TypeCode payment.payment_means_code
185
+ if payment.card_account_id
186
+ xml["ram"].ApplicableTradeSettlementFinancialCard do
187
+ xml["ram"].ID payment.card_account_id
188
+ xml["ram"].CardholderName payment.card_holder_name if payment.card_holder_name
189
+ end
190
+ end
191
+ if payment.debited_account_id
192
+ xml["ram"].PayerPartyDebtorFinancialAccount do
193
+ xml["ram"].IBANID payment.debited_account_id
194
+ end
195
+ end
196
+ if payment.account_id
197
+ xml["ram"].PayeePartyCreditorFinancialAccount do
198
+ xml["ram"].IBANID payment.account_id
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ def build_tax_subtotal(xml, sub)
205
+ xml["ram"].ApplicableTradeTax do
206
+ xml["ram"].CalculatedAmount format_decimal(sub.tax_amount)
207
+ xml["ram"].TypeCode "VAT"
208
+ xml["ram"].ExemptionReason sub.exemption_reason if sub.exemption_reason
209
+ xml["ram"].BasisAmount format_decimal(sub.taxable_amount)
210
+ xml["ram"].CategoryCode sub.category_code
211
+ xml["ram"].ExemptionReasonCode sub.exemption_reason_code if sub.exemption_reason_code
212
+ xml["ram"].RateApplicablePercent format_decimal(sub.percent) if sub.percent
213
+ end
214
+ end
215
+
216
+ def build_monetary_total(xml, totals, tax_breakdown)
217
+ xml["ram"].SpecifiedTradeSettlementHeaderMonetarySummation do
218
+ xml["ram"].LineTotalAmount format_decimal(totals.line_extension_amount)
219
+ xml["ram"].ChargeTotalAmount format_decimal(totals.charge_total_amount) if totals.charge_total_amount
220
+ xml["ram"].AllowanceTotalAmount format_decimal(totals.allowance_total_amount) if totals.allowance_total_amount
221
+ xml["ram"].TaxBasisTotalAmount format_decimal(totals.tax_exclusive_amount)
222
+ if tax_breakdown
223
+ xml["ram"].TaxTotalAmount(format_decimal(tax_breakdown.tax_amount),
224
+ currencyID: tax_breakdown.currency_code)
225
+ end
226
+ xml["ram"].GrandTotalAmount format_decimal(totals.tax_inclusive_amount)
227
+ xml["ram"].RoundingAmount format_decimal(totals.payable_rounding_amount) if totals.payable_rounding_amount
228
+ xml["ram"].TotalPrepaidAmount format_decimal(totals.prepaid_amount) if totals.prepaid_amount
229
+ xml["ram"].DuePayableAmount format_decimal(totals.payable_amount)
230
+ end
231
+ end
232
+
233
+ def build_allowance_charge(xml, ac)
234
+ xml["ram"].SpecifiedTradeAllowanceCharge do
235
+ xml["ram"].ChargeIndicator do
236
+ xml["udt"].Indicator ac.charge_indicator.to_s
237
+ end
238
+ xml["ram"].CalculationPercent format_decimal(ac.multiplier_factor) if ac.multiplier_factor
239
+ xml["ram"].BasisAmount format_decimal(ac.base_amount) if ac.base_amount
240
+ xml["ram"].ActualAmount format_decimal(ac.amount)
241
+ xml["ram"].ReasonCode ac.reason_code if ac.reason_code
242
+ xml["ram"].Reason ac.reason if ac.reason
243
+ if ac.tax_category_code
244
+ xml["ram"].CategoryTradeTax do
245
+ xml["ram"].TypeCode "VAT"
246
+ xml["ram"].CategoryCode ac.tax_category_code
247
+ xml["ram"].RateApplicablePercent format_decimal(ac.tax_percent) if ac.tax_percent
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ def build_line_item(xml, line)
254
+ xml["ram"].IncludedSupplyChainTradeLineItem do
255
+ xml["ram"].AssociatedDocumentLineDocument do
256
+ xml["ram"].LineID line.id
257
+ if line.note
258
+ xml["ram"].IncludedNote do
259
+ xml["ram"].Content line.note
260
+ end
261
+ end
262
+ end
263
+
264
+ build_item(xml, line.item) if line.item
265
+
266
+ xml["ram"].SpecifiedLineTradeAgreement do
267
+ if line.price
268
+ xml["ram"].NetPriceProductTradePrice do
269
+ xml["ram"].ChargeAmount format_decimal(line.price.amount)
270
+ end
271
+ end
272
+ end
273
+
274
+ xml["ram"].SpecifiedLineTradeDelivery do
275
+ xml["ram"].BilledQuantity(format_decimal(line.invoiced_quantity),
276
+ unitCode: line.unit_code)
277
+ end
278
+
279
+ xml["ram"].SpecifiedLineTradeSettlement do
280
+ if line.item&.tax_category
281
+ xml["ram"].ApplicableTradeTax do
282
+ xml["ram"].TypeCode "VAT"
283
+ xml["ram"].CategoryCode line.item.tax_category
284
+ xml["ram"].RateApplicablePercent format_decimal(line.item.tax_percent) if line.item.tax_percent
285
+ end
286
+ end
287
+
288
+ xml["ram"].SpecifiedTradeSettlementLineMonetarySummation do
289
+ xml["ram"].LineTotalAmount format_decimal(line.line_extension_amount)
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ def build_item(xml, item)
296
+ xml["ram"].SpecifiedTradeProduct do
297
+ xml["ram"].SellerAssignedID item.sellers_identifier if item.sellers_identifier
298
+ xml["ram"].Name item.name
299
+ xml["ram"].Description item.description if item.description
300
+ end
301
+ end
302
+
303
+ def format_cii_date(date)
304
+ date.strftime("%Y%m%d")
305
+ end
306
+
307
+ def format_decimal(value)
308
+ return value.to_s unless value.is_a?(BigDecimal)
309
+ str = value.to_s("F")
310
+ str.sub(/\.?0+$/, "")
311
+ end
312
+ end
313
+ end
314
+ end