Skip to content

Commit

Permalink
support multiple tax rates per invoice (#9)
Browse files Browse the repository at this point in the history
* support multiple tax rates per invoice

* take into account line_item.quantity
  • Loading branch information
SubandiK authored Dec 4, 2024
1 parent 9b01938 commit 7fd7fb6
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 16 deletions.
1 change: 1 addition & 0 deletions lib/secretariat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@
require_relative 'secretariat/trade_party'
require_relative 'secretariat/line_item'
require_relative 'secretariat/validator'
require_relative 'secretariat/tax'
51 changes: 37 additions & 14 deletions lib/secretariat/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ def tax_category_code(version: 2)
TAX_CATEGORY_CODES[tax_category] || 'S'
end

def taxes
taxes = {}
line_items.each do |line_item|
taxes[line_item.tax_percent] = Tax.new(tax_percent: BigDecimal(line_item.tax_percent)) if taxes[line_item.tax_percent].nil?
taxes[line_item.tax_percent].tax_amount += BigDecimal(line_item.tax_amount)
taxes[line_item.tax_percent].base_amount += BigDecimal(line_item.net_amount) * line_item.quantity
end
taxes.values
end

def payment_code
PAYMENT_CODES[payment_type] || '1'
end
Expand All @@ -69,12 +79,24 @@ def valid?
@errors = []
tax = BigDecimal(tax_amount)
basis = BigDecimal(basis_amount)
calc_tax = basis * BigDecimal(tax_percent) / BigDecimal(100)
calc_tax = calc_tax.round(2, :down)
if tax != calc_tax
@errors << "Tax amount and calculated tax amount deviate: #{tax} / #{calc_tax}"
summed_tax_amount = taxes.sum(&:tax_amount)
if tax != summed_tax_amount
@errors << "Tax amount and summed tax amounts deviate: #{tax_amount} / #{summed_tax_amount}"
return false
end
summed_tax_base_amount = taxes.sum(&:base_amount)
if basis != summed_tax_base_amount
@errors << "Base amount and summed tax base amount deviate: #{basis} / #{summed_tax_base_amount}"
return false
end
taxes.each do |tax|
calc_tax = tax.base_amount * BigDecimal(tax.tax_percent) / BigDecimal(100)
calc_tax = calc_tax.round(2, :down)
if tax.tax_amount != calc_tax
@errors << "Tax amount and calculated tax amount deviate for rate #{tax.tax_percent}: #{tax.tax_amount} / #{calc_tax}"
return false
end
end
grand_total = BigDecimal(grand_total_amount)
calc_grand_total = basis + tax
if grand_total != calc_grand_total
Expand Down Expand Up @@ -200,18 +222,19 @@ def to_xml(version: 1, validate: true)
end
end
end
xml['ram'].ApplicableTradeTax do
taxes.each do |tax|
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 != ''
xml['ram'].ExemptionReason tax_reason_text
end
Helpers.currency_element(xml, 'ram', 'BasisAmount', tax.base_amount, currency_code, add_currency: version == 1)
xml['ram'].CategoryCode tax_category_code(version: version)

Helpers.currency_element(xml, 'ram', 'CalculatedAmount', tax_amount, currency_code, add_currency: version == 1)
xml['ram'].TypeCode 'VAT'
if tax_reason_text && tax_reason_text != ''
xml['ram'].ExemptionReason tax_reason_text
percent = by_version(version, 'ApplicablePercent', 'RateApplicablePercent')
xml['ram'].send(percent, Helpers.format(tax.tax_percent))
end
Helpers.currency_element(xml, 'ram', 'BasisAmount', basis_amount, currency_code, add_currency: version == 1)
xml['ram'].CategoryCode tax_category_code(version: version)

percent = by_version(version, 'ApplicablePercent', 'RateApplicablePercent')
xml['ram'].send(percent, Helpers.format(tax_percent))
end
if version == 2 && service_period_start && service_period_end
xml['ram'].BillingSpecifiedPeriod do
Expand Down
34 changes: 34 additions & 0 deletions lib/secretariat/tax.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
=begin
Copyright Jan Krutisch
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
=end

require 'bigdecimal'

module Secretariat
Tax = Struct.new('Tax',
:tax_percent,
:tax_amount,
:base_amount,
keyword_init: true
) do

def initialize(*)
super
self.tax_amount = 0
self.base_amount = 0
end
end
end
108 changes: 106 additions & 2 deletions test/invoice_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ def make_eu_invoice
payment_type: :CREDITCARD,
payment_text: 'Kreditkarte',
tax_category: :REVERSECHARGE,
tax_percent: 0,
tax_amount: '0',
basis_amount: '29',
grand_total_amount: 29,
Expand Down Expand Up @@ -103,7 +102,6 @@ def make_de_invoice
payment_iban: 'DE02120300000000202051',
payment_terms_text: "Zahlbar innerhalb von 14 Tagen ohne Abzug",
tax_category: :STANDARDRATE,
tax_percent: '19',
tax_amount: '3.80',
basis_amount: '20',
grand_total_amount: '23.80',
Expand All @@ -113,6 +111,73 @@ def make_de_invoice
)
end

def make_de_invoice_with_multiple_tax_rates
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: 'DE',
vat_id: 'DE304755032'
)
line_item = LineItem.new(
name: 'Depfu Starter Plan',
quantity: 1,
unit: :PIECE,
gross_amount: '23.80',
net_amount: '20',
charge_amount: '20',
tax_category: :STANDARDRATE,
tax_percent: '19',
tax_amount: "3.80",
origin_country_code: 'DE',
currency_code: 'EUR'
)
line_item2 = LineItem.new(
name: 'Cup of Coffee',
quantity: 2,
unit: :PIECE,
gross_amount: '2.14',
net_amount: '2',
charge_amount: '4',
tax_category: :STANDARDRATE,
tax_percent: '7',
tax_amount: "0.28",
origin_country_code: 'DE',
currency_code: 'EUR'
)
Invoice.new(
id: '12345',
issue_date: Date.today,
service_period_start: Date.today,
service_period_end: Date.today + 30,
seller: seller,
buyer: buyer,
buyer_reference: "112233",
line_items: [line_item, line_item2],
currency_code: 'USD',
payment_type: :CREDITCARD,
payment_text: 'Kreditkarte',
payment_iban: 'DE02120300000000202051',
payment_terms_text: "Zahlbar innerhalb von 14 Tagen ohne Abzug",
tax_category: :STANDARDRATE,
tax_amount: '4.08',
basis_amount: '24',
grand_total_amount: '28.08',
due_amount: 0,
paid_amount: '28.08',
payment_due_date: Date.today + 14
)
end

def test_simple_eu_invoice_v2
begin
xml = make_eu_invoice.to_xml(version: 2)
Expand Down Expand Up @@ -204,5 +269,44 @@ def test_simple_de_invoice_against_schematron
end
assert_equal [], errors
end

def test_de_multiple_taxes_invoice_v1
xml = make_de_invoice_with_multiple_tax_rates.to_xml(version: 1)
v = Validator.new(xml, version: 1)
errors = v.validate_against_schema
if !errors.empty?
puts xml
errors.each do |error|
puts error
end
end
assert_equal [], errors
end

def test_de_multiple_taxes_invoice_v2
xml = make_de_invoice_with_multiple_tax_rates.to_xml(version: 2)
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
end

def test_de_multiple_taxes_invoice_against_schematron
xml = make_de_invoice_with_multiple_tax_rates.to_xml(version: 1)
v = Validator.new(xml, version: 1)
errors = v.validate_against_schematron
if !errors.empty?
puts xml
errors.each do |error|
puts "#{error[:line]}: #{error[:message]}"
end
end
assert_equal [], errors
end
end
end

0 comments on commit 7fd7fb6

Please sign in to comment.