From 1c4370578dfdab46d0d5cf6eecf3d59d9ba123c2 Mon Sep 17 00:00:00 2001 From: Ronald Sacher Date: Tue, 16 Sep 2025 14:06:30 +0200 Subject: [PATCH] add support for LineItem#service_period_start / service_period_end + unit_code :YEAR --- lib/secretariat/constants.rb | 1 + lib/secretariat/line_item.rb | 18 ++++++++ test/invoice_test.rb | 80 ++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/lib/secretariat/constants.rb b/lib/secretariat/constants.rb index 30f2ae7..67e4d1f 100644 --- a/lib/secretariat/constants.rb +++ b/lib/secretariat/constants.rb @@ -69,6 +69,7 @@ module Secretariat TAX_CALCULATION_METHODS = %i[HORIZONTAL VERTICAL NONE UNKNOWN].freeze UNIT_CODES = { + :YEAR => "ANN", :PIECE => "C62", :DAY => "DAY", :HECTARE => "HAR", diff --git a/lib/secretariat/line_item.rb b/lib/secretariat/line_item.rb index 3418e87..1b86ecb 100644 --- a/lib/secretariat/line_item.rb +++ b/lib/secretariat/line_item.rb @@ -32,6 +32,8 @@ module Secretariat :charge_amount, :origin_country_code, :currency_code, + :service_period_start, # if start present start & end are required + :service_period_end, # end has to be on or after start (secretariat does not validate this) keyword_init: true ) do @@ -181,6 +183,22 @@ def to_xml(xml, line_item_index, version: 2, validate: true) xml['ram'].send(percent,Helpers.format(tax_percent)) end end + + if version == 2 && self.service_period_start && self.service_period_end + xml['ram'].BillingSpecifiedPeriod do + xml['ram'].StartDateTime do + xml['udt'].DateTimeString(format: '102') do + xml.text(service_period_start.strftime("%Y%m%d")) + end + end + xml['ram'].EndDateTime do + xml['udt'].DateTimeString(format: '102') do + xml.text(service_period_end.strftime("%Y%m%d")) + end + end + end + end + monetary_summation = by_version(version, 'SpecifiedTradeSettlementMonetarySummation', 'SpecifiedTradeSettlementLineMonetarySummation') xml['ram'].send(monetary_summation) do Helpers.currency_element(xml, 'ram', 'LineTotalAmount', (quantity.negative? ? -charge_amount : charge_amount), currency_code, add_currency: version == 1) diff --git a/test/invoice_test.rb b/test/invoice_test.rb index cf7fbb7..1982fc5 100644 --- a/test/invoice_test.rb +++ b/test/invoice_test.rb @@ -57,6 +57,61 @@ def make_eu_invoice(tax_category: :REVERSECHARGE) ) end + def make_eu_invoice_with_line_item_billing_period(tax_category: :REVERSECHARGE) + seller = TradeParty.new( + name: 'Depfu inc', + street1: 'Quickbornstr. 46', + city: 'Hamburg', + postal_code: '20253', + country_id: 'DE', + vat_id: 'DE304755032' + ) + buyer = TradeParty.new( + name: 'Depfu inc', + street1: 'Quickbornstr. 46', + city: 'Hamburg', + postal_code: '20253', + country_id: 'SE', + vat_id: 'SE304755032' + ) + line_item = LineItem.new( + name: 'Depfu Premium Plan', + quantity: 1, + gross_amount: BigDecimal('29'), + net_amount: BigDecimal('29'), + unit: :YEAR, + charge_amount: BigDecimal('29'), + tax_category: tax_category, + tax_percent: 0, + tax_amount: 0, + origin_country_code: 'DE', + currency_code: 'EUR', + service_period_start: Date.today, + service_period_end: Date.today + 364, + ) + Invoice.new( + id: '12345', + issue_date: Date.today, + # service_period on line_item. removed here to simplify testing of BillingSpecifiedPeriod presence + # service_period_start: Date.today, + # service_period_end: Date.today + 30, + seller: seller, + buyer: buyer, + line_items: [line_item], + currency_code: 'USD', + payment_type: :CREDITCARD, + payment_text: 'Kreditkarte', + tax_category: tax_category, + tax_amount: 0, + basis_amount: BigDecimal('29'), + grand_total_amount: BigDecimal('29'), + due_amount: 0, + paid_amount: 29, + payment_due_date: Date.today + 14, + notes: "This is a test invoice", + ) + end + def make_foreign_invoice(tax_category: :TAXEXEMPT) seller = TradeParty.new( name: 'Depfu inc', @@ -381,6 +436,31 @@ def test_simple_eu_invoice_v2 puts e.errors end + def test_simple_eu_invoice_v2_with_line_item_billing_period + begin + xml = make_eu_invoice_with_line_item_billing_period.to_xml(version: 2) + rescue ValidationError => e + pp e.errors + end + + assert_match(/AE<\/ram:CategoryCode>/, xml) + assert_match(/Reverse Charge<\/ram:ExemptionReason>/, xml) + assert_match(//, xml) + assert_match(//, xml) + + v = Validator.new(xml, version: 2) + errors = v.validate_against_schema + if !errors.empty? + puts xml + errors.each do |error| + puts error + end + end + assert_equal [], errors + rescue ValidationError => e + puts e.errors + end + def test_simple_foreign_invoice_v2_taxexpempt begin xml = make_foreign_invoice(tax_category: :TAXEXEMPT).to_xml(version: 2)