factur-x-builder 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.
@@ -0,0 +1,400 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ require_relative "codes"
6
+ require_relative "formatting"
7
+
8
+ module FacturX
9
+ # Serialises an Invoice to Cross Industry Invoice XML.
10
+ #
11
+ # Every complex type in the CII schema is an xs:sequence, so the order in
12
+ # which the emit_* methods write their children is significant and mirrors
13
+ # the XSD. Ordering mistakes surface as schema violations in the test suite.
14
+ class Document
15
+ RSM = "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
16
+ RAM = "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
17
+ QDT = "urn:un:unece:uncefact:data:standard:QualifiedDataType:100"
18
+ UDT = "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"
19
+ XSI = "http://www.w3.org/2001/XMLSchema-instance"
20
+
21
+ NAMESPACES = {
22
+ "xmlns:rsm" => RSM,
23
+ "xmlns:qdt" => QDT,
24
+ "xmlns:ram" => RAM,
25
+ "xmlns:udt" => UDT,
26
+ "xmlns:xsi" => XSI
27
+ }.freeze
28
+
29
+ def initialize(invoice)
30
+ @invoice = invoice
31
+ end
32
+
33
+ def to_xml(indent: 2)
34
+ to_document.to_xml(indent: indent, encoding: "UTF-8")
35
+ end
36
+
37
+ def to_document
38
+ @to_document ||= build
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :invoice
44
+
45
+ def build
46
+ Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
47
+ xml.CrossIndustryInvoice(NAMESPACES) do
48
+ xml.parent.namespace = xml.parent.namespace_definitions.find { |ns| ns.prefix == "rsm" }
49
+ emit_document_context(xml)
50
+ emit_exchanged_document(xml)
51
+ emit_trade_transaction(xml)
52
+ end
53
+ end.doc
54
+ end
55
+
56
+ def emit_document_context(xml)
57
+ xml["rsm"].ExchangedDocumentContext do
58
+ if invoice.business_process
59
+ xml["ram"].BusinessProcessSpecifiedDocumentContextParameter do
60
+ text(xml, :ID, invoice.business_process)
61
+ end
62
+ end
63
+ xml["ram"].GuidelineSpecifiedDocumentContextParameter do
64
+ text(xml, :ID, Codes::EN16931_GUIDELINE)
65
+ end
66
+ end
67
+ end
68
+
69
+ def emit_exchanged_document(xml)
70
+ xml["rsm"].ExchangedDocument do
71
+ text(xml, :ID, invoice.number)
72
+ text(xml, :TypeCode, invoice.type_code)
73
+ date(xml, :IssueDateTime, invoice.issued_on)
74
+ invoice.notes.each { |note| emit_note(xml, note) }
75
+ end
76
+ end
77
+
78
+ def emit_note(xml, note)
79
+ xml["ram"].IncludedNote do
80
+ text(xml, :Content, note.content)
81
+ text(xml, :SubjectCode, note.subject_code)
82
+ end
83
+ end
84
+
85
+ def emit_trade_transaction(xml)
86
+ xml["rsm"].SupplyChainTradeTransaction do
87
+ invoice.lines.each { |line| emit_line(xml, line) }
88
+ emit_header_agreement(xml)
89
+ emit_header_delivery(xml)
90
+ emit_header_settlement(xml)
91
+ end
92
+ end
93
+
94
+ def emit_line(xml, line)
95
+ xml["ram"].IncludedSupplyChainTradeLineItem do
96
+ xml["ram"].AssociatedDocumentLineDocument do
97
+ text(xml, :LineID, line.number)
98
+ xml["ram"].IncludedNote { text(xml, :Content, line.note) } if line.note
99
+ end
100
+ emit_line_product(xml, line)
101
+ emit_line_agreement(xml, line)
102
+ xml["ram"].SpecifiedLineTradeDelivery do
103
+ text(xml, :BilledQuantity, Formatting.quantity(line.quantity), unitCode: line.unit_code)
104
+ end
105
+ emit_line_settlement(xml, line)
106
+ end
107
+ end
108
+
109
+ def emit_line_product(xml, line)
110
+ xml["ram"].SpecifiedTradeProduct do
111
+ text(xml, :GlobalID, line.global_id, scheme_attribute(line.global_id_scheme))
112
+ text(xml, :SellerAssignedID, line.seller_item_id)
113
+ text(xml, :BuyerAssignedID, line.buyer_item_id)
114
+ text(xml, :Name, line.name)
115
+ text(xml, :Description, line.description)
116
+ xml["ram"].OriginTradeCountry { text(xml, :ID, line.origin_country) } if line.origin_country
117
+ end
118
+ end
119
+
120
+ def emit_line_agreement(xml, line)
121
+ xml["ram"].SpecifiedLineTradeAgreement do
122
+ xml["ram"].BuyerOrderReferencedDocument { text(xml, :LineID, line.order_line_id) } if line.order_line_id
123
+ if line.gross_price?
124
+ xml["ram"].GrossPriceProductTradePrice do
125
+ text(xml, :ChargeAmount, Formatting.unit_price(line.gross_unit_price))
126
+ emit_unit_discount(xml, line) if line.unit_discount
127
+ end
128
+ end
129
+ xml["ram"].NetPriceProductTradePrice do
130
+ text(xml, :ChargeAmount, Formatting.unit_price(line.unit_price))
131
+ end
132
+ end
133
+ end
134
+
135
+ def emit_unit_discount(xml, line)
136
+ xml["ram"].AppliedTradeAllowanceCharge do
137
+ xml["ram"].ChargeIndicator { boolean(xml, :Indicator, false) }
138
+ text(xml, :ActualAmount, Formatting.unit_price(line.unit_discount))
139
+ end
140
+ end
141
+
142
+ def emit_line_settlement(xml, line)
143
+ xml["ram"].SpecifiedLineTradeSettlement do
144
+ xml["ram"].ApplicableTradeTax do
145
+ text(xml, :TypeCode, Codes::VAT_TYPE)
146
+ text(xml, :CategoryCode, line.vat_category)
147
+ text(xml, :RateApplicablePercent, Formatting.percentage(line.vat_rate_decimal))
148
+ end
149
+ emit_period(xml, line.period_start, line.period_end)
150
+ xml["ram"].SpecifiedTradeSettlementLineMonetarySummation do
151
+ text(xml, :LineTotalAmount, Formatting.amount(line.net_amount))
152
+ end
153
+ if line.accounting_reference
154
+ xml["ram"].ReceivableSpecifiedTradeAccountingAccount do
155
+ text(xml, :ID, line.accounting_reference)
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ def emit_header_agreement(xml)
162
+ xml["ram"].ApplicableHeaderTradeAgreement do
163
+ text(xml, :BuyerReference, invoice.buyer_reference)
164
+ emit_party(xml, :SellerTradeParty, invoice.seller)
165
+ emit_party(xml, :BuyerTradeParty, invoice.buyer)
166
+ emit_party(xml, :SellerTaxRepresentativeTradeParty, invoice.tax_representative)
167
+ emit_referenced_document(xml, :SellerOrderReferencedDocument, invoice.sales_order_reference)
168
+ emit_referenced_document(xml, :BuyerOrderReferencedDocument, invoice.purchase_order_reference)
169
+ emit_referenced_document(xml, :ContractReferencedDocument, invoice.contract_reference)
170
+ emit_project(xml)
171
+ end
172
+ end
173
+
174
+ def emit_project(xml)
175
+ return unless invoice.project_reference
176
+
177
+ xml["ram"].SpecifiedProcuringProject do
178
+ text(xml, :ID, invoice.project_reference)
179
+ text(xml, :Name, invoice.project_name || invoice.project_reference)
180
+ end
181
+ end
182
+
183
+ def emit_header_delivery(xml)
184
+ xml["ram"].ApplicableHeaderTradeDelivery do
185
+ emit_party(xml, :ShipToTradeParty, invoice.ship_to)
186
+ if invoice.delivered_on
187
+ xml["ram"].ActualDeliverySupplyChainEvent do
188
+ date(xml, :OccurrenceDateTime, invoice.delivered_on)
189
+ end
190
+ end
191
+ emit_referenced_document(xml, :DespatchAdviceReferencedDocument,
192
+ invoice.despatch_advice_reference)
193
+ emit_referenced_document(xml, :ReceivingAdviceReferencedDocument,
194
+ invoice.receiving_advice_reference)
195
+ end
196
+ end
197
+
198
+ def emit_header_settlement(xml)
199
+ xml["ram"].ApplicableHeaderTradeSettlement do
200
+ text(xml, :CreditorReferenceID, invoice.creditor_reference)
201
+ text(xml, :PaymentReference, invoice.payment_reference)
202
+ text(xml, :InvoiceCurrencyCode, invoice.currency)
203
+ emit_party(xml, :PayeeTradeParty, invoice.payee)
204
+ invoice.payment_means.each { |means| emit_payment_means(xml, means) }
205
+ invoice.tax_breakdowns.each { |breakdown| emit_tax_breakdown(xml, breakdown) }
206
+ emit_period(xml, invoice.billing_period_start, invoice.billing_period_end)
207
+ (invoice.allowances + invoice.charges).each { |item| emit_allowance_charge(xml, item) }
208
+ emit_payment_terms(xml)
209
+ emit_totals(xml)
210
+ invoice.preceding_invoices.each { |reference| emit_preceding_invoice(xml, reference) }
211
+ end
212
+ end
213
+
214
+ def emit_payment_means(xml, means)
215
+ xml["ram"].SpecifiedTradeSettlementPaymentMeans do
216
+ text(xml, :TypeCode, means.type_code)
217
+ text(xml, :Information, means.information)
218
+ if means.card?
219
+ xml["ram"].ApplicableTradeSettlementFinancialCard do
220
+ text(xml, :ID, means.card_id)
221
+ text(xml, :CardholderName, means.cardholder_name)
222
+ end
223
+ end
224
+ xml["ram"].PayerPartyDebtorFinancialAccount { text(xml, :IBANID, means.debtor_iban) } if means.debtor_iban
225
+ if means.creditor_account?
226
+ xml["ram"].PayeePartyCreditorFinancialAccount do
227
+ text(xml, :IBANID, means.iban)
228
+ text(xml, :AccountName, means.account_name)
229
+ text(xml, :ProprietaryID, means.account_id)
230
+ end
231
+ end
232
+ xml["ram"].PayeeSpecifiedCreditorFinancialInstitution { text(xml, :BICID, means.bic) } if means.bic
233
+ end
234
+ end
235
+
236
+ def emit_tax_breakdown(xml, breakdown)
237
+ xml["ram"].ApplicableTradeTax do
238
+ text(xml, :CalculatedAmount, Formatting.amount(breakdown.calculated_amount))
239
+ text(xml, :TypeCode, Codes::VAT_TYPE)
240
+ text(xml, :ExemptionReason, breakdown.exemption_reason)
241
+ text(xml, :BasisAmount, Formatting.amount(breakdown.basis_amount))
242
+ text(xml, :CategoryCode, breakdown.category)
243
+ text(xml, :ExemptionReasonCode, breakdown.exemption_reason_code)
244
+ text(xml, :RateApplicablePercent, Formatting.percentage(breakdown.rate_decimal))
245
+ end
246
+ end
247
+
248
+ def emit_allowance_charge(xml, item)
249
+ xml["ram"].SpecifiedTradeAllowanceCharge do
250
+ xml["ram"].ChargeIndicator { boolean(xml, :Indicator, item.charge?) }
251
+ text(xml, :CalculationPercent, Formatting.percentage(item.percentage)) if item.percentage
252
+ text(xml, :BasisAmount, Formatting.amount(item.basis_amount)) if item.basis_amount
253
+ text(xml, :ActualAmount, Formatting.amount(item.amount))
254
+ text(xml, :ReasonCode, item.reason_code)
255
+ text(xml, :Reason, item.reason)
256
+ xml["ram"].CategoryTradeTax do
257
+ text(xml, :TypeCode, Codes::VAT_TYPE)
258
+ text(xml, :CategoryCode, item.vat_category)
259
+ text(xml, :RateApplicablePercent, Formatting.percentage(item.vat_rate || 0))
260
+ end
261
+ end
262
+ end
263
+
264
+ def emit_payment_terms(xml)
265
+ terms = invoice.payment_terms
266
+ return if terms.nil? || terms.empty?
267
+
268
+ xml["ram"].SpecifiedTradePaymentTerms do
269
+ text(xml, :Description, terms.description)
270
+ date(xml, :DueDateDateTime, terms.due_on)
271
+ text(xml, :DirectDebitMandateID, terms.mandate_reference)
272
+ end
273
+ end
274
+
275
+ def emit_totals(xml)
276
+ totals = invoice.totals
277
+ xml["ram"].SpecifiedTradeSettlementHeaderMonetarySummation do
278
+ text(xml, :LineTotalAmount, Formatting.amount(totals.line_total))
279
+ text(xml, :ChargeTotalAmount, optional_amount(totals.charge_total))
280
+ text(xml, :AllowanceTotalAmount, optional_amount(totals.allowance_total))
281
+ text(xml, :TaxBasisTotalAmount, Formatting.amount(totals.tax_basis_total))
282
+ text(xml, :TaxTotalAmount, Formatting.amount(totals.tax_total_decimal),
283
+ currencyID: invoice.currency)
284
+ text(xml, :RoundingAmount, optional_amount(totals.rounding_amount))
285
+ text(xml, :GrandTotalAmount, Formatting.amount(totals.grand_total))
286
+ text(xml, :TotalPrepaidAmount, optional_amount(totals.prepaid))
287
+ text(xml, :DuePayableAmount, Formatting.amount(totals.due_payable))
288
+ end
289
+ end
290
+
291
+ def emit_preceding_invoice(xml, reference)
292
+ xml["ram"].InvoiceReferencedDocument do
293
+ text(xml, :IssuerAssignedID, reference.number)
294
+ next unless reference.issued_on
295
+
296
+ xml["ram"].FormattedIssueDateTime do
297
+ xml["qdt"].DateTimeString(Formatting.date(reference.issued_on),
298
+ format: Formatting::CII_DATE_FORMAT_CODE)
299
+ end
300
+ end
301
+ end
302
+
303
+ def emit_party(xml, element_name, party)
304
+ return if party.nil?
305
+
306
+ xml["ram"].send(element_name) do
307
+ text(xml, :GlobalID, party.global_id, scheme_attribute(party.global_id_scheme))
308
+ text(xml, :Name, party.name)
309
+ emit_legal_organization(xml, party)
310
+ emit_contact(xml, party)
311
+ emit_address(xml, party.address)
312
+ if party.routing_id
313
+ xml["ram"].URIUniversalCommunication do
314
+ text(xml, :URIID, party.routing_id, scheme_attribute(party.routing_id_scheme))
315
+ end
316
+ end
317
+ party.tax_registrations.each do |value, scheme|
318
+ xml["ram"].SpecifiedTaxRegistration { text(xml, :ID, value, schemeID: scheme) }
319
+ end
320
+ end
321
+ end
322
+
323
+ def emit_legal_organization(xml, party)
324
+ return unless party.legal_id || party.trading_name
325
+
326
+ xml["ram"].SpecifiedLegalOrganization do
327
+ text(xml, :ID, party.legal_id, scheme_attribute(party.legal_id_scheme))
328
+ text(xml, :TradingBusinessName, party.trading_name)
329
+ end
330
+ end
331
+
332
+ def emit_contact(xml, party)
333
+ return unless party.contact?
334
+
335
+ contact = party.contact
336
+ xml["ram"].DefinedTradeContact do
337
+ text(xml, :PersonName, contact.person_name)
338
+ text(xml, :DepartmentName, contact.department_name)
339
+ xml["ram"].TelephoneUniversalCommunication { text(xml, :CompleteNumber, contact.phone) } if contact.phone
340
+ xml["ram"].EmailURIUniversalCommunication { text(xml, :URIID, contact.email) } if contact.email
341
+ end
342
+ end
343
+
344
+ def emit_address(xml, address)
345
+ return if address.nil?
346
+
347
+ xml["ram"].PostalTradeAddress do
348
+ text(xml, :PostcodeCode, address.postcode)
349
+ text(xml, :LineOne, address.line_one)
350
+ text(xml, :LineTwo, address.line_two)
351
+ text(xml, :LineThree, address.line_three)
352
+ text(xml, :CityName, address.city)
353
+ text(xml, :CountryID, address.country_code)
354
+ text(xml, :CountrySubDivisionName, address.country_subdivision)
355
+ end
356
+ end
357
+
358
+ def emit_referenced_document(xml, element_name, reference)
359
+ return if reference.nil?
360
+
361
+ xml["ram"].send(element_name) { text(xml, :IssuerAssignedID, reference) }
362
+ end
363
+
364
+ def emit_period(xml, starts_on, ends_on)
365
+ return if starts_on.nil? && ends_on.nil?
366
+
367
+ xml["ram"].BillingSpecifiedPeriod do
368
+ date(xml, :StartDateTime, starts_on)
369
+ date(xml, :EndDateTime, ends_on)
370
+ end
371
+ end
372
+
373
+ def text(xml, name, value, attributes = {})
374
+ return if value.nil?
375
+
376
+ xml["ram"].send(name, value.to_s, attributes)
377
+ end
378
+
379
+ def boolean(xml, name, value)
380
+ xml["udt"].send(name, value.to_s)
381
+ end
382
+
383
+ def date(xml, name, value)
384
+ return if value.nil?
385
+
386
+ xml["ram"].send(name) do
387
+ xml["udt"].DateTimeString(Formatting.date(value),
388
+ format: Formatting::CII_DATE_FORMAT_CODE)
389
+ end
390
+ end
391
+
392
+ def optional_amount(value)
393
+ value.nil? ? nil : Formatting.amount(value)
394
+ end
395
+
396
+ def scheme_attribute(scheme)
397
+ scheme.nil? ? {} : { schemeID: scheme }
398
+ end
399
+ end
400
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FacturX
4
+ class Error < StandardError; end
5
+
6
+ # Raised when the invoice breaks an EN 16931 (or French) business rule.
7
+ class ValidationError < Error
8
+ attr_reader :violations
9
+
10
+ def initialize(violations)
11
+ @violations = Array(violations)
12
+ super(build_message)
13
+ end
14
+
15
+ private
16
+
17
+ def build_message
18
+ return "invoice is not valid" if violations.empty?
19
+
20
+ "invoice is not valid:\n#{bulleted(violations)}"
21
+ end
22
+
23
+ def bulleted(items)
24
+ items.map { |item| " - #{item}" }.join("\n")
25
+ end
26
+ end
27
+
28
+ # Raised when the generated document does not conform to the Factur-X XSD.
29
+ # This signals a bug in the gem rather than bad input.
30
+ class SchemaError < Error
31
+ attr_reader :violations
32
+
33
+ def initialize(violations)
34
+ @violations = Array(violations)
35
+ details = @violations.map { |violation| " - #{violation}" }.join("\n")
36
+ super("generated XML does not conform to the Factur-X EN 16931 schema:\n#{details}")
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "bigdecimal/util"
5
+
6
+ module FacturX
7
+ module Formatting
8
+ AMOUNT_SCALE = 2
9
+ PERCENTAGE_SCALE = 2
10
+ UNIT_PRICE_SCALE = 4
11
+ QUANTITY_SCALE = 4
12
+
13
+ CII_DATE_FORMAT = "%Y%m%d"
14
+ CII_DATE_FORMAT_CODE = "102"
15
+
16
+ module_function
17
+
18
+ def amount(value)
19
+ fixed(value, AMOUNT_SCALE)
20
+ end
21
+
22
+ def percentage(value)
23
+ fixed(value, PERCENTAGE_SCALE)
24
+ end
25
+
26
+ def unit_price(value)
27
+ trimmed(value, UNIT_PRICE_SCALE, AMOUNT_SCALE)
28
+ end
29
+
30
+ def quantity(value)
31
+ trimmed(value, QUANTITY_SCALE, 0)
32
+ end
33
+
34
+ def date(value)
35
+ to_date(value).strftime(CII_DATE_FORMAT)
36
+ end
37
+
38
+ def decimal(value)
39
+ case value
40
+ when BigDecimal then value
41
+ when Integer then BigDecimal(value)
42
+ when Float then BigDecimal(value.to_s)
43
+ when String then parse_string(value)
44
+ when nil then raise Error, "expected a number, got nil"
45
+ else coerce(value)
46
+ end
47
+ end
48
+
49
+ def fixed(value, scale)
50
+ integer, fraction = decimal(value).round(scale).to_s("F").split(".")
51
+ "#{integer}.#{fraction.ljust(scale, "0")}"
52
+ end
53
+
54
+ # Renders with `scale` decimals at most, dropping trailing zeros but never
55
+ # falling below `minimum_scale` (BT-146 allows 4 decimals, BT-129 allows none).
56
+ def trimmed(value, scale, minimum_scale)
57
+ rounded = decimal(value).round(scale)
58
+ integer, fraction = rounded.to_s("F").split(".")
59
+ fraction = fraction.sub(/0+\z/, "").ljust(minimum_scale, "0")
60
+ fraction.empty? ? integer : "#{integer}.#{fraction}"
61
+ end
62
+
63
+ def parse_string(value)
64
+ BigDecimal(value)
65
+ rescue ArgumentError
66
+ raise Error, "#{value.inspect} is not a valid number"
67
+ end
68
+
69
+ def coerce(value)
70
+ return value.to_d if value.respond_to?(:to_d)
71
+
72
+ raise Error, "cannot convert #{value.class} to a number"
73
+ end
74
+
75
+ def to_date(value)
76
+ return value if value.respond_to?(:strftime)
77
+ return Date.parse(value) if value.is_a?(String)
78
+
79
+ raise Error, "cannot convert #{value.class} to a date"
80
+ end
81
+
82
+ private_class_method :parse_string, :coerce, :to_date
83
+ end
84
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "codes"
4
+
5
+ module FacturX
6
+ class Invoice
7
+ attr_reader :number, :type_code, :issued_on, :currency, :business_process,
8
+ :notes, :buyer_reference, :seller, :buyer, :payee,
9
+ :tax_representative, :ship_to, :delivered_on,
10
+ :payment_reference, :creditor_reference, :payment_means, :payment_terms,
11
+ :lines, :allowances, :charges, :tax_breakdowns, :totals,
12
+ :purchase_order_reference, :sales_order_reference, :contract_reference,
13
+ :project_reference, :project_name, :despatch_advice_reference,
14
+ :receiving_advice_reference, :preceding_invoices,
15
+ :billing_period_start, :billing_period_end
16
+
17
+ def initialize(number:, issued_on:, seller:, buyer:, lines:, tax_breakdowns:, totals:,
18
+ type_code: Codes::COMMERCIAL_INVOICE, currency: "EUR",
19
+ business_process: nil, notes: [], buyer_reference: nil,
20
+ payee: nil, tax_representative: nil, ship_to: nil, delivered_on: nil,
21
+ payment_reference: nil, creditor_reference: nil,
22
+ payment_means: [], payment_terms: nil,
23
+ allowances: [], charges: [],
24
+ purchase_order_reference: nil, sales_order_reference: nil,
25
+ contract_reference: nil, project_reference: nil, project_name: nil,
26
+ despatch_advice_reference: nil, receiving_advice_reference: nil,
27
+ preceding_invoices: [],
28
+ billing_period_start: nil, billing_period_end: nil)
29
+ @number = number
30
+ @type_code = type_code
31
+ @issued_on = issued_on
32
+ @currency = currency
33
+ @business_process = business_process
34
+ @notes = Array(notes)
35
+ @buyer_reference = buyer_reference
36
+ @seller = seller
37
+ @buyer = buyer
38
+ @payee = payee
39
+ @tax_representative = tax_representative
40
+ @ship_to = ship_to
41
+ @delivered_on = delivered_on
42
+ @payment_reference = payment_reference
43
+ @creditor_reference = creditor_reference
44
+ @payment_means = Array(payment_means)
45
+ @payment_terms = payment_terms
46
+ @lines = number_lines(Array(lines))
47
+ @allowances = Array(allowances)
48
+ @charges = Array(charges)
49
+ @tax_breakdowns = Array(tax_breakdowns)
50
+ @totals = totals
51
+ @purchase_order_reference = purchase_order_reference
52
+ @sales_order_reference = sales_order_reference
53
+ @contract_reference = contract_reference
54
+ @project_reference = project_reference
55
+ @project_name = project_name
56
+ @despatch_advice_reference = despatch_advice_reference
57
+ @receiving_advice_reference = receiving_advice_reference
58
+ @preceding_invoices = Array(preceding_invoices)
59
+ @billing_period_start = billing_period_start
60
+ @billing_period_end = billing_period_end
61
+ end
62
+
63
+ def credit_note?
64
+ type_code == Codes::CREDIT_NOTE
65
+ end
66
+
67
+ def billing_period?
68
+ !billing_period_start.nil? || !billing_period_end.nil?
69
+ end
70
+
71
+ def to_xml(**options)
72
+ FacturX.build(self, **options)
73
+ end
74
+
75
+ private
76
+
77
+ def number_lines(lines)
78
+ lines.each_with_index.map { |line, index| line.with_number(index + 1) }
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "codes"
4
+ require_relative "formatting"
5
+
6
+ module FacturX
7
+ class Line
8
+ attr_reader :number, :name, :description, :seller_item_id, :buyer_item_id,
9
+ :global_id, :global_id_scheme, :unit_price, :gross_unit_price,
10
+ :unit_discount, :quantity, :unit_code, :vat_category, :vat_rate,
11
+ :net_amount, :note, :origin_country, :period_start, :period_end,
12
+ :order_line_id, :accounting_reference
13
+
14
+ def initialize(name:, unit_price:, quantity:, net_amount:,
15
+ number: nil, description: nil, seller_item_id: nil, buyer_item_id: nil,
16
+ global_id: nil, global_id_scheme: nil, gross_unit_price: nil,
17
+ unit_discount: nil, unit_code: Codes::PIECE_UNIT,
18
+ vat_category: Codes::VatCategory::STANDARD, vat_rate: nil,
19
+ note: nil, origin_country: nil, period_start: nil, period_end: nil,
20
+ order_line_id: nil, accounting_reference: nil)
21
+ @number = number
22
+ @name = name
23
+ @description = description
24
+ @seller_item_id = seller_item_id
25
+ @buyer_item_id = buyer_item_id
26
+ @global_id = global_id
27
+ @global_id_scheme = global_id_scheme
28
+ @unit_price = unit_price
29
+ @gross_unit_price = gross_unit_price
30
+ @unit_discount = unit_discount
31
+ @quantity = quantity
32
+ @unit_code = unit_code
33
+ @vat_category = vat_category
34
+ @vat_rate = vat_rate
35
+ @net_amount = net_amount
36
+ @note = note
37
+ @origin_country = origin_country
38
+ @period_start = period_start
39
+ @period_end = period_end
40
+ @order_line_id = order_line_id
41
+ @accounting_reference = accounting_reference
42
+ end
43
+
44
+ def net_amount_decimal
45
+ Formatting.decimal(net_amount)
46
+ end
47
+
48
+ def vat_rate_decimal
49
+ Formatting.decimal(vat_rate || 0)
50
+ end
51
+
52
+ # Groups lines into VAT breakdowns: BR-S-08 and friends require one
53
+ # breakdown per (category, rate) pair.
54
+ def tax_key
55
+ [vat_category, vat_rate_decimal]
56
+ end
57
+
58
+ def gross_price?
59
+ !gross_unit_price.nil?
60
+ end
61
+
62
+ def with_number(value)
63
+ return self if number
64
+
65
+ self.class.new(**to_h, number: value)
66
+ end
67
+
68
+ def to_h
69
+ {
70
+ number: number, name: name, description: description,
71
+ seller_item_id: seller_item_id, buyer_item_id: buyer_item_id,
72
+ global_id: global_id, global_id_scheme: global_id_scheme,
73
+ unit_price: unit_price, gross_unit_price: gross_unit_price,
74
+ unit_discount: unit_discount, quantity: quantity, unit_code: unit_code,
75
+ vat_category: vat_category, vat_rate: vat_rate, net_amount: net_amount,
76
+ note: note, origin_country: origin_country,
77
+ period_start: period_start, period_end: period_end,
78
+ order_line_id: order_line_id, accounting_reference: accounting_reference
79
+ }
80
+ end
81
+ end
82
+ end