From ac3cc3674891f1bfe88443f5accea995056cf7bc Mon Sep 17 00:00:00 2001 From: Martin Wilhelmi Date: Fri, 31 Jan 2025 14:34:30 +0100 Subject: [PATCH 01/26] Backport changes from Fortytools --- lib/secretariat/constants.rb | 14 +++++++++++--- lib/secretariat/invoice.rb | 27 +++++++++++++++++++++------ lib/secretariat/line_item.rb | 13 +++++++------ lib/secretariat/trade_party.rb | 26 +++++++++++++++++++++++--- lib/secretariat/version.rb | 2 +- lib/secretariat/versioner.rb | 6 +++--- secretariat.gemspec | 2 +- test/invoice_test.rb | 6 ++++-- 8 files changed, 71 insertions(+), 25 deletions(-) diff --git a/lib/secretariat/constants.rb b/lib/secretariat/constants.rb index 3a40a82..0c4be5d 100644 --- a/lib/secretariat/constants.rb +++ b/lib/secretariat/constants.rb @@ -15,6 +15,7 @@ =end module Secretariat + BASIS_QUANTITY = 1.0 TAX_CATEGORY_CODES = { :STANDARDRATE => "S", @@ -45,6 +46,8 @@ module Secretariat :DEBITADVICE => "31", :CREDITCARD => "48", :DEBIT => "49", + :SEPA_CREDIT => "58", + :SEPA_DEBIT => "59", :COMPENSATION => "97", } @@ -55,12 +58,14 @@ module Secretariat } UNIT_CODES = { - :PIECE => "C62", + :ONE => "C62", + :PIECE => "H87", :DAY => "DAY", :HECTARE => "HAR", :HOUR => "HUR", + :MONTH => "MON", :KILOGRAM => "KGM", - :KILOMETER => "KTM", + :KILOMETER => "KMT", :KILOWATTHOUR => "KWH", :FIXEDRATE => "LS", :LITRE => "LTR", @@ -75,6 +80,9 @@ module Secretariat :PERCENT => "P1", :SET => "SET", :TON => "TNE", - :WEEK => "WEE" + :WEEK => "WEE", + :BOTTLE => "BO", + :CARTON => "CT", + :CAN => "CA", } end diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index f609825..b7a81b3 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -134,10 +134,16 @@ def namespaces(version: 1) ) end - def to_xml(version: 1, validate: true) - if version < 1 || version > 2 + def to_xml(version: 1, validate: true, mode: :zugferd) + if version < 1 || version > 3 raise 'Unsupported Document Version' end + if mode != :zugferd && mode != :xrechnung + raise 'Unsupported Document Mode' + end + if mode == :xrechnung && version < 2 + raise 'Mode XRechnung requires Document Version > 1' + end if validate && !valid? raise ValidationError.new("Invoice is invalid", errors) @@ -152,8 +158,17 @@ def to_xml(version: 1, validate: true) context = by_version(version, 'SpecifiedExchangedDocumentContext', 'ExchangedDocumentContext') xml['rsm'].send(context) do + if version == 3 && mode == :xrechnung + xml['ram'].BusinessProcessSpecifiedDocumentContextParameter do + xml['ram'].ID 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0' + end + end xml['ram'].GuidelineSpecifiedDocumentContextParameter do version_id = by_version(version, 'urn:ferd:CrossIndustryDocument:invoice:1p0:comfort', 'urn:cen.eu:en16931:2017') + if mode == :xrechnung + version_id += '#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3' if version == 2 + version_id += '#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0' if version == 3 + end xml['ram'].ID version_id end end @@ -176,7 +191,7 @@ def to_xml(version: 1, validate: true) transaction = by_version(version, 'SpecifiedSupplyChainTradeTransaction', 'SupplyChainTradeTransaction') xml['rsm'].send(transaction) do - if version == 2 + if version >= 2 line_items.each_with_index do |item, i| item.to_xml(xml, i + 1, version: version, validate: validate) # one indexed end @@ -194,7 +209,7 @@ def to_xml(version: 1, validate: true) xml['ram'].BuyerTradeParty do buyer.to_xml(xml, version: version) end - if version == 2 + if version >= 2 if Array(attachments).size > 0 attachments.each_with_index do |attachment, index| attachment.to_xml(xml, index, version: version, validate: validate) @@ -206,7 +221,7 @@ def to_xml(version: 1, validate: true) delivery = by_version(version, 'ApplicableSupplyChainTradeDelivery', 'ApplicableHeaderTradeDelivery') xml['ram'].send(delivery) do - if version == 2 + if version >= 2 xml['ram'].ShipToTradeParty do buyer.to_xml(xml, exclude_tax: true, version: version) end @@ -283,7 +298,7 @@ def to_xml(version: 1, validate: true) end if version == 1 line_items.each_with_index do |item, i| - item.to_xml(xml, i + 1, version: version) # one indexed + item.to_xml(xml, i + 1, version: version, validate: validate) # one indexed end end end diff --git a/lib/secretariat/line_item.rb b/lib/secretariat/line_item.rb index 17a8919..a75573c 100644 --- a/lib/secretariat/line_item.rb +++ b/lib/secretariat/line_item.rb @@ -50,6 +50,7 @@ def valid? charge_price = BigDecimal(charge_amount) tax = BigDecimal(tax_amount) unit_price = net_price * BigDecimal(quantity) + unit_price = unit_price.round(2) if charge_price != unit_price @errors << "charge price and gross price times quantity deviate: #{charge_price} / #{unit_price}" @@ -57,7 +58,7 @@ def valid? end if discount_amount discount = BigDecimal(discount_amount) - calculated_net_price = (gross_price - discount).round(2, :down) + calculated_net_price = (gross_price - discount).round(2) if calculated_net_price != net_price @errors = "Calculated net price and net price deviate: #{calculated_net_price} / #{net_price}" return false @@ -94,7 +95,7 @@ def to_xml(xml, line_item_index, version: 2, validate: true) xml['ram'].AssociatedDocumentLineDocument do xml['ram'].LineID line_item_index end - if (version == 2) + if (version >= 2) xml['ram'].SpecifiedTradeProduct do xml['ram'].Name name xml['ram'].OriginTradeCountry do @@ -107,9 +108,9 @@ def to_xml(xml, line_item_index, version: 2, validate: true) xml['ram'].send(agreement) do xml['ram'].GrossPriceProductTradePrice do Helpers.currency_element(xml, 'ram', 'ChargeAmount', gross_amount, currency_code, add_currency: version == 1, digits: 4) - if version == 2 && discount_amount + if version >= 2 && discount_amount xml['ram'].BasisQuantity(unitCode: unit_code) do - xml.text(Helpers.format(quantity, digits: 4)) + xml.text(Helpers.format(BASIS_QUANTITY, digits: 4)) end xml['ram'].AppliedTradeAllowanceCharge do xml['ram'].ChargeIndicator do @@ -131,9 +132,9 @@ def to_xml(xml, line_item_index, version: 2, validate: true) end xml['ram'].NetPriceProductTradePrice do Helpers.currency_element(xml, 'ram', 'ChargeAmount', net_amount, currency_code, add_currency: version == 1, digits: 4) - if version == 2 + if version >= 2 xml['ram'].BasisQuantity(unitCode: unit_code) do - xml.text(Helpers.format(quantity, digits: 4)) + xml.text(Helpers.format(BASIS_QUANTITY, digits: 4)) end end end diff --git a/lib/secretariat/trade_party.rb b/lib/secretariat/trade_party.rb index d061249..6d9df66 100644 --- a/lib/secretariat/trade_party.rb +++ b/lib/secretariat/trade_party.rb @@ -16,11 +16,26 @@ module Secretariat TradeParty = Struct.new('TradeParty', - :name, :street1, :street2, :city, :postal_code, :country_id, :vat_id, + :name, :street1, :street2, :city, :postal_code, :country_id, :vat_id, :contact_name, :contact_phone, :contact_email, keyword_init: true, ) do def to_xml(xml, exclude_tax: false, version: 2) xml['ram'].Name name + if contact_name && contact_name != '' + xml['ram'].DefinedTradeContact do + xml['ram'].PersonName contact_name + if contact_phone && contact_phone != '' + xml['ram'].TelephoneUniversalCommunication do + xml['ram'].CompleteNumber contact_phone + end + end + if contact_email && contact_email != '' + xml['ram'].EmailURIUniversalCommunication do + xml['ram'].URIID contact_email + end + end + end + end xml['ram'].PostalTradeAddress do xml['ram'].PostcodeCode postal_code xml['ram'].LineOne street1 @@ -30,6 +45,13 @@ def to_xml(xml, exclude_tax: false, version: 2) xml['ram'].CityName city xml['ram'].CountryID country_id end + if version == 3 && contact_email.present? + xml['ram'].URIUniversalCommunication do + xml['ram'].URIID(schemeID: 'EM') do + xml.text(contact_email) + end + end + end if !exclude_tax && vat_id && vat_id != '' xml['ram'].SpecifiedTaxRegistration do xml['ram'].ID(schemeID: 'VA') do @@ -39,6 +61,4 @@ def to_xml(xml, exclude_tax: false, version: 2) end end end - - end diff --git a/lib/secretariat/version.rb b/lib/secretariat/version.rb index 018fb84..b667f45 100644 --- a/lib/secretariat/version.rb +++ b/lib/secretariat/version.rb @@ -15,5 +15,5 @@ =end module Secretariat - VERSION = '3.2.0' + VERSION = '3.3.0' end diff --git a/lib/secretariat/versioner.rb b/lib/secretariat/versioner.rb index 6e60ccf..529afa0 100644 --- a/lib/secretariat/versioner.rb +++ b/lib/secretariat/versioner.rb @@ -15,11 +15,11 @@ =end module Secretariat module Versioner - def by_version(version, v1, v2) + def by_version(version, v1, v2_or_v3) if version == 1 v1 - elsif version == 2 - v2 + elsif version == 2 || version == 3 + v2_or_v3 else raise "Unsupported Version: #{version}" end diff --git a/secretariat.gemspec b/secretariat.gemspec index c974419..d604395 100644 --- a/secretariat.gemspec +++ b/secretariat.gemspec @@ -27,7 +27,7 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.6.0' s.add_runtime_dependency 'nokogiri', '~> 1.10' - s.add_runtime_dependency 'bigdecimal', '~> 3.1' + s.add_runtime_dependency 'bigdecimal', '~> 3.1.9' s.add_runtime_dependency 'mime-types', '~> 3.6.0' s.add_development_dependency 'minitest', '~> 5.13' diff --git a/test/invoice_test.rb b/test/invoice_test.rb index 5108896..d760166 100644 --- a/test/invoice_test.rb +++ b/test/invoice_test.rb @@ -52,7 +52,8 @@ def make_eu_invoice grand_total_amount: 29, due_amount: 0, paid_amount: 29, - payment_due_date: Date.today + 14 + payment_due_date: Date.today + 14, + payment_terms_text: 'paid' ) end @@ -166,7 +167,8 @@ def make_de_invoice grand_total_amount: '23.80', due_amount: 0, paid_amount: '23.80', - payment_due_date: Date.today + 14 + payment_due_date: Date.today + 14, + payment_terms_text: 'paid' ) end From 901276911cf6b529a31e2ababa51192c0f4dcf1f Mon Sep 17 00:00:00 2001 From: Martin Wilhelmi Date: Wed, 5 Feb 2025 11:21:03 +0100 Subject: [PATCH 02/26] Move ALLOWED_MIME_TYPES constant --- lib/secretariat/attachment.rb | 9 --------- lib/secretariat/constants.rb | 9 +++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/secretariat/attachment.rb b/lib/secretariat/attachment.rb index 03a9b55..fd7b81f 100644 --- a/lib/secretariat/attachment.rb +++ b/lib/secretariat/attachment.rb @@ -18,15 +18,6 @@ require 'mime/types' -ALLOWED_MIME_TYPES = [ - "application/pdf", - "application/vnd.oasis.opendocument.spreadsheet", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "image/jpeg", - "image/png", - "text/csv" -] - module Secretariat Attachment = Struct.new('Attachment', :filename, diff --git a/lib/secretariat/constants.rb b/lib/secretariat/constants.rb index 0c4be5d..7c39fce 100644 --- a/lib/secretariat/constants.rb +++ b/lib/secretariat/constants.rb @@ -15,6 +15,15 @@ =end module Secretariat + ALLOWED_MIME_TYPES = [ + "application/pdf", + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "image/jpeg", + "image/png", + "text/csv" + ] + BASIS_QUANTITY = 1.0 TAX_CATEGORY_CODES = { From 13cf3acf73924b142d8765c45061bf5526e59bf5 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Tue, 22 Apr 2025 12:39:41 +0700 Subject: [PATCH 03/26] introduce invoice_type --- lib/secretariat/invoice.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index 3b4bf54..df82fcf 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -22,6 +22,7 @@ module Secretariat :issue_date, :service_period_start, :service_period_end, + :invoice_type, :seller, :buyer, :buyer_reference, @@ -204,7 +205,7 @@ def to_xml(version: 1, validate: true, mode: :zugferd) if version == 1 xml['ram'].Name "RECHNUNG" end - xml['ram'].TypeCode '380' # TODO: make configurable + xml['ram'].TypeCode invoice_type xml['ram'].IssueDateTime do xml['udt'].DateTimeString(format: '102') do xml.text(issue_date.strftime("%Y%m%d")) From cf6aa80ae6d3ad710ffc4518587265e6592583b2 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Tue, 17 Jun 2025 11:32:23 +0700 Subject: [PATCH 04/26] change value of ActualDeliverySupplyChainEvent --- lib/secretariat/invoice.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index df82fcf..8c7fb1f 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -254,7 +254,7 @@ def to_xml(version: 1, validate: true, mode: :zugferd) xml['ram'].ActualDeliverySupplyChainEvent do xml['ram'].OccurrenceDateTime do xml['udt'].DateTimeString(format: '102') do - xml.text(issue_date.strftime("%Y%m%d")) + xml.text(service_period_end.strftime("%Y%m%d")) end end end From 6d0e3dcb5e943f27305ebf5886555f4922682512 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Tue, 17 Jun 2025 11:45:09 +0700 Subject: [PATCH 05/26] remove empty element --- lib/secretariat/invoice.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index 8c7fb1f..e1f290e 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -225,7 +225,7 @@ def to_xml(version: 1, validate: true, mode: :zugferd) trade_agreement = by_version(version, 'ApplicableSupplyChainTradeAgreement', 'ApplicableHeaderTradeAgreement') xml['ram'].send(trade_agreement) do - if buyer_reference + if buyer_reference.present? xml['ram'].BuyerReference buyer_reference end xml['ram'].SellerTradeParty do From 0d0957e01a2df53b58b89e4bcd43c88aaee60e35 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Tue, 17 Jun 2025 11:53:40 +0700 Subject: [PATCH 06/26] remove empty element --- lib/secretariat/invoice.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index e1f290e..a2b66be 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -268,7 +268,7 @@ def to_xml(version: 1, validate: true, mode: :zugferd) xml['ram'].SpecifiedTradeSettlementPaymentMeans do xml['ram'].TypeCode payment_code xml['ram'].Information payment_text - if payment_iban + if payment_iban.present? xml['ram'].PayeePartyCreditorFinancialAccount do xml['ram'].IBANID payment_iban end From 750ae96ad4a281e50479f081f0ba4b8dadfaac58 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Tue, 17 Jun 2025 12:03:02 +0700 Subject: [PATCH 07/26] remove empty element --- lib/secretariat/invoice.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index a2b66be..4d2a73f 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -267,7 +267,7 @@ def to_xml(version: 1, validate: true, mode: :zugferd) xml['ram'].InvoiceCurrencyCode currency_code xml['ram'].SpecifiedTradeSettlementPaymentMeans do xml['ram'].TypeCode payment_code - xml['ram'].Information payment_text + xml['ram'].Information payment_text if payment_text.present? if payment_iban.present? xml['ram'].PayeePartyCreditorFinancialAccount do xml['ram'].IBANID payment_iban From 7da4b6a02a24b1eff212ff6405a6f6189d768746 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Tue, 17 Jun 2025 14:03:15 +0700 Subject: [PATCH 08/26] change present --- lib/secretariat/invoice.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index 4d2a73f..dbe72f2 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -225,7 +225,7 @@ def to_xml(version: 1, validate: true, mode: :zugferd) trade_agreement = by_version(version, 'ApplicableSupplyChainTradeAgreement', 'ApplicableHeaderTradeAgreement') xml['ram'].send(trade_agreement) do - if buyer_reference.present? + if buyer_reference && buyer_reference != '' xml['ram'].BuyerReference buyer_reference end xml['ram'].SellerTradeParty do @@ -267,8 +267,8 @@ def to_xml(version: 1, validate: true, mode: :zugferd) xml['ram'].InvoiceCurrencyCode currency_code xml['ram'].SpecifiedTradeSettlementPaymentMeans do xml['ram'].TypeCode payment_code - xml['ram'].Information payment_text if payment_text.present? - if payment_iban.present? + xml['ram'].Information payment_text if payment_text && payment_text != '' + if payment_iban && payment_iban != '' xml['ram'].PayeePartyCreditorFinancialAccount do xml['ram'].IBANID payment_iban end From 690278e9e81ddfdab5d64af4cba72c8b4b807c81 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Wed, 18 Jun 2025 10:38:57 +0700 Subject: [PATCH 09/26] set default value for invoice type --- lib/secretariat/invoice.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index dbe72f2..33db11b 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -205,7 +205,7 @@ def to_xml(version: 1, validate: true, mode: :zugferd) if version == 1 xml['ram'].Name "RECHNUNG" end - xml['ram'].TypeCode invoice_type + xml['ram'].TypeCode invoice_type || '380' xml['ram'].IssueDateTime do xml['udt'].DateTimeString(format: '102') do xml.text(issue_date.strftime("%Y%m%d")) From 32c00163191ac5c6b2e99fcc1a6c09ab4f8166fb Mon Sep 17 00:00:00 2001 From: Akise17 Date: Thu, 19 Jun 2025 11:39:00 +0700 Subject: [PATCH 10/26] . --- lib/secretariat/line_item.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/line_item.rb b/lib/secretariat/line_item.rb index ec940c8..eb06941 100644 --- a/lib/secretariat/line_item.rb +++ b/lib/secretariat/line_item.rb @@ -184,7 +184,7 @@ def to_xml(xml, line_item_index, version: 2, validate: true) 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) + Helpers.currency_element(xml, 'ram', 'LineTotalAmount', charge_amount, currency_code, add_currency: version == 1) end end From 3a34ca70102bb8f2df5f2721e808567c9f604106 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Thu, 19 Jun 2025 12:24:38 +0700 Subject: [PATCH 11/26] . --- lib/secretariat/invoice.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index 33db11b..45739ec 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -225,7 +225,7 @@ def to_xml(version: 1, validate: true, mode: :zugferd) trade_agreement = by_version(version, 'ApplicableSupplyChainTradeAgreement', 'ApplicableHeaderTradeAgreement') xml['ram'].send(trade_agreement) do - if buyer_reference && buyer_reference != '' + if buyer_reference xml['ram'].BuyerReference buyer_reference end xml['ram'].SellerTradeParty do From c003d7aa17cb7e8b97a3e5d11bd04973fb860bba Mon Sep 17 00:00:00 2001 From: Akise17 Date: Fri, 20 Jun 2025 10:18:46 +0700 Subject: [PATCH 12/26] address BG-3 Warning and gross price error --- lib/secretariat/invoice.rb | 14 ++++++++++ lib/secretariat/line_item.rb | 52 +++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index 45739ec..7f88fa4 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -23,6 +23,8 @@ module Secretariat :service_period_start, :service_period_end, :invoice_type, + :invoice_reference_id, + :invoice_reference_date, :seller, :buyer, :buyer_reference, @@ -321,6 +323,18 @@ def to_xml(version: 1, validate: true, mode: :zugferd) Helpers.currency_element(xml, 'ram', 'TotalPrepaidAmount', paid_amount, currency_code, add_currency: version == 1) Helpers.currency_element(xml, 'ram', 'DuePayableAmount', due_amount, currency_code, add_currency: version == 1) end + + if invoice_reference_id && invoice_reference_date + invoice_reference = by_version(version, 'InvoiceReferencedDocument', 'InvoiceReferencedDocument') + xml['ram'].send(invoice_reference) do + xml['ram'].IssuerAssignedID invoice_reference_id + xml['ram'].FormattedIssueDateTime do + xml['qdt'].DateTimeString(format: '102') do + xml.text(invoice_reference_date.strftime('%Y%m%d')) + end + end + end + end end if version == 1 line_items.each_with_index do |item, i| diff --git a/lib/secretariat/line_item.rb b/lib/secretariat/line_item.rb index eb06941..d306826 100644 --- a/lib/secretariat/line_item.rb +++ b/lib/secretariat/line_item.rb @@ -23,6 +23,7 @@ module Secretariat :quantity, :unit, :gross_amount, + :gross_based, :net_amount, :tax_category, :tax_percent, @@ -129,35 +130,38 @@ def to_xml(xml, line_item_index, version: 2, validate: true) agreement = by_version(version, 'SpecifiedSupplyChainTradeAgreement', 'SpecifiedLineTradeAgreement') xml['ram'].send(agreement) do - xml['ram'].GrossPriceProductTradePrice do - Helpers.currency_element(xml, 'ram', 'ChargeAmount', gross_amount, currency_code, add_currency: version == 1, digits: 4) - if version >= 2 && discount_amount - xml['ram'].BasisQuantity(unitCode: unit_code) do - xml.text(Helpers.format(BASIS_QUANTITY, digits: 4)) - end - xml['ram'].AppliedTradeAllowanceCharge do - xml['ram'].ChargeIndicator do - xml['udt'].Indicator 'false' + if gross_based + xml['ram'].GrossPriceProductTradePrice do + Helpers.currency_element(xml, 'ram', 'ChargeAmount', gross_amount, currency_code, add_currency: version == 1, digits: 4) + if version >= 2 && discount_amount + xml['ram'].BasisQuantity(unitCode: unit_code) do + xml.text(Helpers.format(BASIS_QUANTITY, digits: 4)) + end + xml['ram'].AppliedTradeAllowanceCharge do + xml['ram'].ChargeIndicator do + xml['udt'].Indicator 'false' + end + Helpers.currency_element(xml, 'ram', 'ActualAmount', discount_amount, currency_code, add_currency: version == 1) + xml['ram'].Reason discount_reason end - Helpers.currency_element(xml, 'ram', 'ActualAmount', discount_amount, currency_code, add_currency: version == 1) - xml['ram'].Reason discount_reason end - end - if version == 1 && discount_amount - xml['ram'].AppliedTradeAllowanceCharge do - xml['ram'].ChargeIndicator do - xml['udt'].Indicator 'false' + if version == 1 && discount_amount + xml['ram'].AppliedTradeAllowanceCharge do + xml['ram'].ChargeIndicator do + xml['udt'].Indicator 'false' + end + Helpers.currency_element(xml, 'ram', 'ActualAmount', discount_amount, currency_code, add_currency: version == 1) + xml['ram'].Reason discount_reason end - Helpers.currency_element(xml, 'ram', 'ActualAmount', discount_amount, currency_code, add_currency: version == 1) - xml['ram'].Reason discount_reason end end - end - xml['ram'].NetPriceProductTradePrice do - Helpers.currency_element(xml, 'ram', 'ChargeAmount', net_amount, currency_code, add_currency: version == 1, digits: 4) - if version >= 2 - xml['ram'].BasisQuantity(unitCode: unit_code) do - xml.text(Helpers.format(BASIS_QUANTITY, digits: 4)) + else + xml['ram'].NetPriceProductTradePrice do + Helpers.currency_element(xml, 'ram', 'ChargeAmount', net_amount, currency_code, add_currency: version == 1, digits: 4) + if version >= 2 + xml['ram'].BasisQuantity(unitCode: unit_code) do + xml.text(Helpers.format(BASIS_QUANTITY, digits: 4)) + end end end end From 28e20b2adfa81441cd74ba3649ef7f1a7e1cd778 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Fri, 20 Jun 2025 10:41:50 +0700 Subject: [PATCH 13/26] . --- lib/secretariat/line_item.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/line_item.rb b/lib/secretariat/line_item.rb index d306826..53dbb17 100644 --- a/lib/secretariat/line_item.rb +++ b/lib/secretariat/line_item.rb @@ -188,7 +188,7 @@ def to_xml(xml, line_item_index, version: 2, validate: true) end monetary_summation = by_version(version, 'SpecifiedTradeSettlementMonetarySummation', 'SpecifiedTradeSettlementLineMonetarySummation') xml['ram'].send(monetary_summation) do - Helpers.currency_element(xml, 'ram', 'LineTotalAmount', charge_amount, currency_code, add_currency: version == 1) + Helpers.currency_element(xml, 'ram', 'LineTotalAmount', (quantity.negative? ? -charge_amount : charge_amount), currency_code, add_currency: version == 1) end end From cf7bfe22d86e2a5d24d2d67fd4140c705fa9a307 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Tue, 24 Jun 2025 09:57:14 +0700 Subject: [PATCH 14/26] using amount instead gross_based flag --- lib/secretariat/line_item.rb | 38 +++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/lib/secretariat/line_item.rb b/lib/secretariat/line_item.rb index 53dbb17..f0dc3b1 100644 --- a/lib/secretariat/line_item.rb +++ b/lib/secretariat/line_item.rb @@ -23,7 +23,6 @@ module Secretariat :quantity, :unit, :gross_amount, - :gross_based, :net_amount, :tax_category, :tax_percent, @@ -130,19 +129,21 @@ def to_xml(xml, line_item_index, version: 2, validate: true) agreement = by_version(version, 'SpecifiedSupplyChainTradeAgreement', 'SpecifiedLineTradeAgreement') xml['ram'].send(agreement) do - if gross_based + if gross_amount xml['ram'].GrossPriceProductTradePrice do Helpers.currency_element(xml, 'ram', 'ChargeAmount', gross_amount, currency_code, add_currency: version == 1, digits: 4) - if version >= 2 && discount_amount + if version >= 2 xml['ram'].BasisQuantity(unitCode: unit_code) do xml.text(Helpers.format(BASIS_QUANTITY, digits: 4)) end - xml['ram'].AppliedTradeAllowanceCharge do - xml['ram'].ChargeIndicator do - xml['udt'].Indicator 'false' + if discount + xml['ram'].AppliedTradeAllowanceCharge do + xml['ram'].ChargeIndicator do + xml['udt'].Indicator 'false' + end + Helpers.currency_element(xml, 'ram', 'ActualAmount', discount_amount, currency_code, add_currency: version == 1) + xml['ram'].Reason discount_reason end - Helpers.currency_element(xml, 'ram', 'ActualAmount', discount_amount, currency_code, add_currency: version == 1) - xml['ram'].Reason discount_reason end end if version == 1 && discount_amount @@ -155,13 +156,32 @@ def to_xml(xml, line_item_index, version: 2, validate: true) end end end - else + end + if net_amount xml['ram'].NetPriceProductTradePrice do Helpers.currency_element(xml, 'ram', 'ChargeAmount', net_amount, currency_code, add_currency: version == 1, digits: 4) if version >= 2 xml['ram'].BasisQuantity(unitCode: unit_code) do xml.text(Helpers.format(BASIS_QUANTITY, digits: 4)) end + if discount_amount + xml['ram'].AppliedTradeAllowanceCharge do + xml['ram'].ChargeIndicator do + xml['udt'].Indicator 'false' + end + Helpers.currency_element(xml, 'ram', 'ActualAmount', discount_amount, currency_code, add_currency: version == 1) + xml['ram'].Reason discount_reason + end + end + if version == 1 && discount_amount + xml['ram'].AppliedTradeAllowanceCharge do + xml['ram'].ChargeIndicator do + xml['udt'].Indicator 'false' + end + Helpers.currency_element(xml, 'ram', 'ActualAmount', discount_amount, currency_code, add_currency: version == 1) + xml['ram'].Reason discount_reason + end + end end end end From 3a4b9abb0cb79076ff2bc08ebdf203521fa6e0f3 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Tue, 24 Jun 2025 11:28:31 +0700 Subject: [PATCH 15/26] . --- lib/secretariat/line_item.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/line_item.rb b/lib/secretariat/line_item.rb index f0dc3b1..2ca99bd 100644 --- a/lib/secretariat/line_item.rb +++ b/lib/secretariat/line_item.rb @@ -136,7 +136,7 @@ def to_xml(xml, line_item_index, version: 2, validate: true) xml['ram'].BasisQuantity(unitCode: unit_code) do xml.text(Helpers.format(BASIS_QUANTITY, digits: 4)) end - if discount + if discount_amount xml['ram'].AppliedTradeAllowanceCharge do xml['ram'].ChargeIndicator do xml['udt'].Indicator 'false' From e529ae1fea5beab70f438855a0d13206b60dcc45 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Wed, 9 Jul 2025 11:32:16 +0700 Subject: [PATCH 16/26] add new attribute delivery_date --- lib/secretariat/invoice.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index 7f88fa4..1013f64 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -22,6 +22,7 @@ module Secretariat :issue_date, :service_period_start, :service_period_end, + :delivery_date, :invoice_type, :invoice_reference_id, :invoice_reference_date, @@ -256,7 +257,7 @@ def to_xml(version: 1, validate: true, mode: :zugferd) xml['ram'].ActualDeliverySupplyChainEvent do xml['ram'].OccurrenceDateTime do xml['udt'].DateTimeString(format: '102') do - xml.text(service_period_end.strftime("%Y%m%d")) + xml.text(delivery_date.strftime("%Y%m%d")) end end end From 7af08d9ba1565f307fe5e29df3491122a11f325c Mon Sep 17 00:00:00 2001 From: Akise17 Date: Wed, 9 Jul 2025 17:06:22 +0700 Subject: [PATCH 17/26] fix test --- test/invoice_test.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/invoice_test.rb b/test/invoice_test.rb index ccbeac7..bb7e3fa 100644 --- a/test/invoice_test.rb +++ b/test/invoice_test.rb @@ -40,6 +40,7 @@ def make_eu_invoice(tax_category: :REVERSECHARGE) issue_date: Date.today, service_period_start: Date.today, service_period_end: Date.today + 30, + delivery_date: Date.today + 30, seller: seller, buyer: buyer, line_items: [line_item], @@ -89,6 +90,7 @@ def make_foreign_invoice(tax_category: :TAXEXEMPT) issue_date: Date.today, service_period_start: Date.today, service_period_end: Date.today + 30, + delivery_date: Date.today + 30, seller: seller, buyer: buyer, line_items: [line_item], @@ -146,6 +148,7 @@ def make_eu_invoice_with_attachment issue_date: Date.today, service_period_start: Date.today, service_period_end: Date.today + 30, + delivery_date: Date.today + 30, seller: seller, buyer: buyer, line_items: [line_item], @@ -200,6 +203,7 @@ def make_de_invoice issue_date: Date.today, service_period_start: Date.today, service_period_end: Date.today + 30, + delivery_date: Date.today + 30, seller: seller, buyer: buyer, buyer_reference: "112233", @@ -281,6 +285,7 @@ def make_de_invoice_with_multiple_tax_rates issue_date: Date.today, service_period_start: Date.today, service_period_end: Date.today + 30, + delivery_date: Date.today + 30, seller: seller, buyer: buyer, buyer_reference: "112233", @@ -335,6 +340,7 @@ def make_negative_de_invoice issue_date: Date.today, service_period_start: Date.today, service_period_end: Date.today + 30, + delivery_date: Date.today + 30, seller: seller, buyer: buyer, buyer_reference: "112233", From d8b51ad8db8c2e5adde5f9b2289d76b1ed71559f Mon Sep 17 00:00:00 2001 From: Akise17 Date: Mon, 14 Jul 2025 14:27:18 +0700 Subject: [PATCH 18/26] add condition to delivery date --- lib/secretariat/invoice.rb | 6 +++++- test/invoice_test.rb | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index 1013f64..3c75ef1 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -257,7 +257,11 @@ def to_xml(version: 1, validate: true, mode: :zugferd) xml['ram'].ActualDeliverySupplyChainEvent do xml['ram'].OccurrenceDateTime do xml['udt'].DateTimeString(format: '102') do - xml.text(delivery_date.strftime("%Y%m%d")) + if delivery_date.nil? || issue_date == delivery_date + xml.text(issue_date.strftime("%Y%m%d")) + else + xml.text(delivery_date.strftime("%Y%m%d")) + end end end end diff --git a/test/invoice_test.rb b/test/invoice_test.rb index bb7e3fa..dcd7d68 100644 --- a/test/invoice_test.rb +++ b/test/invoice_test.rb @@ -340,7 +340,7 @@ def make_negative_de_invoice issue_date: Date.today, service_period_start: Date.today, service_period_end: Date.today + 30, - delivery_date: Date.today + 30, + delivery_date: nil, seller: seller, buyer: buyer, buyer_reference: "112233", From 992106353ad0ec933104672da7f444a67dfce7fe Mon Sep 17 00:00:00 2001 From: Akise17 Date: Fri, 25 Jul 2025 10:08:05 +0700 Subject: [PATCH 19/26] handle gross_amount --- lib/secretariat/line_item.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/line_item.rb b/lib/secretariat/line_item.rb index 2ca99bd..4f00a75 100644 --- a/lib/secretariat/line_item.rb +++ b/lib/secretariat/line_item.rb @@ -100,7 +100,7 @@ def to_xml(xml, line_item_index, version: 2, validate: true) if net_price&.zero? self.tax_percent = 0 end - + if net_price&.negative? # Zugferd doesn't allow negative amounts at the item level. # Instead, a negative quantity is used. From 63b3578b7d79b9ae907b22659d7c889d3c4614b2 Mon Sep 17 00:00:00 2001 From: Akise17 Date: Fri, 25 Jul 2025 10:50:49 +0700 Subject: [PATCH 20/26] handle gross_amount --- lib/secretariat/line_item.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/line_item.rb b/lib/secretariat/line_item.rb index 4f00a75..e006db2 100644 --- a/lib/secretariat/line_item.rb +++ b/lib/secretariat/line_item.rb @@ -101,7 +101,7 @@ def to_xml(xml, line_item_index, version: 2, validate: true) self.tax_percent = 0 end - if net_price&.negative? + if net_price&.negative? || gross_price&.negative? # Zugferd doesn't allow negative amounts at the item level. # Instead, a negative quantity is used. self.quantity = -quantity From 571ab8346de4a2541a7053bccba8ca93885d160b Mon Sep 17 00:00:00 2001 From: Eslam Hindawy Date: Wed, 30 Jul 2025 19:14:05 +0200 Subject: [PATCH 21/26] introduce new blank? method to make checking for blank values easier --- lib/secretariat.rb | 2 ++ lib/secretariat/invoice.rb | 12 ++++++++---- lib/secretariat/object_extensions.rb | 10 ++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 lib/secretariat/object_extensions.rb diff --git a/lib/secretariat.rb b/lib/secretariat.rb index d2cf46e..b20ebf0 100644 --- a/lib/secretariat.rb +++ b/lib/secretariat.rb @@ -15,6 +15,8 @@ =end require_relative 'secretariat/version' +require_relative 'secretariat/object_extensions' +# using Secretariat::ObjectExtensions require_relative 'secretariat/constants' require_relative 'secretariat/helpers' require_relative 'secretariat/versioner' diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index 3c75ef1..c3025ac 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -17,6 +17,8 @@ require 'bigdecimal' module Secretariat + using ObjectExtensions + Invoice = Struct.new("Invoice", :id, :issue_date, @@ -268,14 +270,16 @@ def to_xml(version: 1, validate: true, mode: :zugferd) end trade_settlement = by_version(version, 'ApplicableSupplyChainTradeSettlement', 'ApplicableHeaderTradeSettlement') xml['ram'].send(trade_settlement) do - if payment_reference && payment_reference != '' + unless payment_reference.blank? xml['ram'].PaymentReference payment_reference end xml['ram'].InvoiceCurrencyCode currency_code xml['ram'].SpecifiedTradeSettlementPaymentMeans do xml['ram'].TypeCode payment_code - xml['ram'].Information payment_text if payment_text && payment_text != '' - if payment_iban && payment_iban != '' + unless payment_text.blank? + xml['ram'].Information payment_text + end + unless payment_iban.blank? xml['ram'].PayeePartyCreditorFinancialAccount do xml['ram'].IBANID payment_iban end @@ -285,7 +289,7 @@ def to_xml(version: 1, validate: true, mode: :zugferd) xml['ram'].ApplicableTradeTax do Helpers.currency_element(xml, 'ram', 'CalculatedAmount', tax.tax_amount, currency_code, add_currency: version == 1) xml['ram'].TypeCode 'VAT' - if tax_reason_text && tax_reason_text != '' + unless tax_reason_text.blank? xml['ram'].ExemptionReason tax_reason_text end Helpers.currency_element(xml, 'ram', 'BasisAmount', tax.base_amount, currency_code, add_currency: version == 1) diff --git a/lib/secretariat/object_extensions.rb b/lib/secretariat/object_extensions.rb new file mode 100644 index 0000000..5283e90 --- /dev/null +++ b/lib/secretariat/object_extensions.rb @@ -0,0 +1,10 @@ +module Secretariat + module ObjectExtensions + refine Object do + # Copied from activesupport/lib/active_support/core_ext/object/blank.rb, line 18 + def blank? + respond_to?(:empty?) ? !!empty? : !self + end + end + end +end \ No newline at end of file From 946e9f718ffdb5931c5003739bfcddbbf92e454d Mon Sep 17 00:00:00 2001 From: Eslam Hindawy Date: Wed, 30 Jul 2025 19:17:06 +0200 Subject: [PATCH 22/26] remove comment --- lib/secretariat.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/secretariat.rb b/lib/secretariat.rb index b20ebf0..aec4498 100644 --- a/lib/secretariat.rb +++ b/lib/secretariat.rb @@ -16,7 +16,6 @@ require_relative 'secretariat/version' require_relative 'secretariat/object_extensions' -# using Secretariat::ObjectExtensions require_relative 'secretariat/constants' require_relative 'secretariat/helpers' require_relative 'secretariat/versioner' From 3562a80ef9cde035c1fab63f3f8f09fdeb843102 Mon Sep 17 00:00:00 2001 From: Eslam Hindawy Date: Wed, 30 Jul 2025 19:26:20 +0200 Subject: [PATCH 23/26] introduce present? as well --- lib/secretariat/invoice.rb | 8 ++++---- lib/secretariat/object_extensions.rb | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index c3025ac..499abc1 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -18,7 +18,7 @@ module Secretariat using ObjectExtensions - + Invoice = Struct.new("Invoice", :id, :issue_date, @@ -270,16 +270,16 @@ def to_xml(version: 1, validate: true, mode: :zugferd) end trade_settlement = by_version(version, 'ApplicableSupplyChainTradeSettlement', 'ApplicableHeaderTradeSettlement') xml['ram'].send(trade_settlement) do - unless payment_reference.blank? + if payment_reference.present? xml['ram'].PaymentReference payment_reference end xml['ram'].InvoiceCurrencyCode currency_code xml['ram'].SpecifiedTradeSettlementPaymentMeans do xml['ram'].TypeCode payment_code - unless payment_text.blank? + if payment_text.present? xml['ram'].Information payment_text end - unless payment_iban.blank? + if payment_iban.present? xml['ram'].PayeePartyCreditorFinancialAccount do xml['ram'].IBANID payment_iban end diff --git a/lib/secretariat/object_extensions.rb b/lib/secretariat/object_extensions.rb index 5283e90..d81e5d4 100644 --- a/lib/secretariat/object_extensions.rb +++ b/lib/secretariat/object_extensions.rb @@ -5,6 +5,10 @@ module ObjectExtensions def blank? respond_to?(:empty?) ? !!empty? : !self end + + def present? + !blank? + end end end end \ No newline at end of file From 587d5a4a215a66ebc99138e83adabb10173ce833 Mon Sep 17 00:00:00 2001 From: Eslam Hindawy Date: Wed, 30 Jul 2025 19:27:57 +0200 Subject: [PATCH 24/26] use present? --- lib/secretariat/invoice.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index 499abc1..6732e0b 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -289,7 +289,7 @@ def to_xml(version: 1, validate: true, mode: :zugferd) xml['ram'].ApplicableTradeTax do Helpers.currency_element(xml, 'ram', 'CalculatedAmount', tax.tax_amount, currency_code, add_currency: version == 1) xml['ram'].TypeCode 'VAT' - unless tax_reason_text.blank? + if tax_reason_text.present? xml['ram'].ExemptionReason tax_reason_text end Helpers.currency_element(xml, 'ram', 'BasisAmount', tax.base_amount, currency_code, add_currency: version == 1) From 89be24df10c62d822633f57fddcd36178630eabf Mon Sep 17 00:00:00 2001 From: Akise17 Date: Tue, 5 Aug 2025 13:39:20 +0700 Subject: [PATCH 25/26] rename attribute --- lib/secretariat/invoice.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index 6732e0b..9a72999 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -26,7 +26,7 @@ module Secretariat :service_period_end, :delivery_date, :invoice_type, - :invoice_reference_id, + :invoice_reference_number, :invoice_reference_date, :seller, :buyer, From 1c44e6164682e1e179f5b26394ce2347f351888a Mon Sep 17 00:00:00 2001 From: Akise17 Date: Tue, 5 Aug 2025 13:42:30 +0700 Subject: [PATCH 26/26] . --- lib/secretariat/invoice.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/secretariat/invoice.rb b/lib/secretariat/invoice.rb index 9a72999..5701f98 100644 --- a/lib/secretariat/invoice.rb +++ b/lib/secretariat/invoice.rb @@ -333,10 +333,10 @@ def to_xml(version: 1, validate: true, mode: :zugferd) Helpers.currency_element(xml, 'ram', 'DuePayableAmount', due_amount, currency_code, add_currency: version == 1) end - if invoice_reference_id && invoice_reference_date + if invoice_reference_number && invoice_reference_date invoice_reference = by_version(version, 'InvoiceReferencedDocument', 'InvoiceReferencedDocument') xml['ram'].send(invoice_reference) do - xml['ram'].IssuerAssignedID invoice_reference_id + xml['ram'].IssuerAssignedID invoice_reference_number xml['ram'].FormattedIssueDateTime do xml['qdt'].DateTimeString(format: '102') do xml.text(invoice_reference_date.strftime('%Y%m%d'))