Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/secretariat/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def valid?
return false
end
line_item_sum = line_items.inject(BigDecimal(0)) do |m, item|
m + BigDecimal(item.charge_amount)
m + BigDecimal(item.quantity.negative? ? -item.charge_amount : item.charge_amount)
end
if line_item_sum != basis
@errors << "Line items do not add up to basis amount #{line_item_sum} / #{basis}"
Expand Down
16 changes: 14 additions & 2 deletions lib/secretariat/line_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def valid?
gross_price = BigDecimal(gross_amount)
charge_price = BigDecimal(charge_amount)
tax = BigDecimal(tax_amount)
unit_price = net_price * BigDecimal(quantity)
unit_price = net_price * BigDecimal(quantity.abs)

if charge_price != unit_price
@errors << "charge price and gross price times quantity deviate: #{charge_price} / #{unit_price}"
Expand All @@ -66,6 +66,7 @@ def valid?

calculated_tax = charge_price * BigDecimal(tax_percent) / BigDecimal(100)
calculated_tax = calculated_tax.round(2)
calculated_tax = -calculated_tax if quantity.negative?
if calculated_tax != tax
@errors << "Tax and calculated tax deviate: #{tax} / #{calculated_tax}"
return false
Expand All @@ -85,6 +86,17 @@ def tax_category_code(version: 2)
end

def to_xml(xml, line_item_index, version: 2, validate: true)
if net_amount&.zero?
self.tax_percent = 0
end
if net_amount&.negative?
# Zugferd doesn't allow negative amounts at the item level.
# Instead, a negative quantity is used.
self.quantity = -quantity
self.gross_amount = gross_amount&.abs
self.net_amount = net_amount&.abs
self.charge_amount = charge_amount&.abs
end
if validate && !valid?
pp errors
raise ValidationError.new("LineItem #{line_item_index} is invalid", errors)
Expand Down Expand Up @@ -160,7 +172,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

Expand Down
143 changes: 111 additions & 32 deletions test/invoice_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ def make_eu_invoice
line_item = LineItem.new(
name: 'Depfu Starter Plan',
quantity: 1,
gross_amount: '29',
net_amount: '29',
gross_amount: BigDecimal('29'),
net_amount: BigDecimal('29'),
unit: :PIECE,
charge_amount: '29',
charge_amount: BigDecimal('29'),
tax_category: :REVERSECHARGE,
tax_percent: 0,
tax_amount: "0",
tax_amount: 0,
origin_country_code: 'DE',
currency_code: 'EUR'
)
Expand All @@ -46,9 +46,9 @@ def make_eu_invoice
payment_type: :CREDITCARD,
payment_text: 'Kreditkarte',
tax_category: :REVERSECHARGE,
tax_amount: '0',
basis_amount: '29',
grand_total_amount: 29,
tax_amount: 0,
basis_amount: BigDecimal('29'),
grand_total_amount: BigDecimal('29'),
due_amount: 0,
paid_amount: 29,
payment_due_date: Date.today + 14
Expand Down Expand Up @@ -76,14 +76,14 @@ def make_de_invoice
name: 'Depfu Starter Plan',
quantity: 1,
unit: :PIECE,
gross_amount: '29',
net_amount: '20',
charge_amount: '20',
discount_amount: '9',
gross_amount: BigDecimal('29'),
net_amount: BigDecimal('20'),
charge_amount: BigDecimal('20'),
discount_amount: BigDecimal('9'),
discount_reason: 'Rabatt',
tax_category: :STANDARDRATE,
tax_percent: '19',
tax_amount: "3.80",
tax_amount: BigDecimal("3.80"),
origin_country_code: 'DE',
currency_code: 'EUR'
)
Expand All @@ -103,11 +103,11 @@ def make_de_invoice
payment_iban: 'DE02120300000000202051',
payment_terms_text: "Zahlbar innerhalb von 14 Tagen ohne Abzug",
tax_category: :STANDARDRATE,
tax_amount: '3.80',
basis_amount: '20',
grand_total_amount: '23.80',
tax_amount: BigDecimal('3.80'),
basis_amount: BigDecimal('20'),
grand_total_amount: BigDecimal('23.80'),
due_amount: 0,
paid_amount: '23.80',
paid_amount: BigDecimal('23.80'),
payment_due_date: Date.today + 14
)
end
Expand All @@ -133,38 +133,38 @@ def make_de_invoice_with_multiple_tax_rates
name: 'Depfu Starter Plan',
quantity: 2,
unit: :PIECE,
gross_amount: '23.80',
net_amount: '20',
charge_amount: '40',
gross_amount: BigDecimal('23.80'),
net_amount: BigDecimal('20'),
charge_amount: BigDecimal('40'),
tax_category: :STANDARDRATE,
tax_percent: '19',
tax_amount: "7.60",
tax_amount: BigDecimal("7.60"),
origin_country_code: 'DE',
currency_code: 'EUR'
)
line_item2 = LineItem.new(
name: 'Cup of Coffee',
quantity: 1,
unit: :PIECE,
gross_amount: '2.68',
net_amount: '2.50',
charge_amount: '2.50',
gross_amount: BigDecimal('2.68'),
net_amount: BigDecimal('2.50'),
charge_amount: BigDecimal('2.50'),
tax_category: :STANDARDRATE,
tax_percent: '7',
tax_amount: "0.18",
tax_amount: BigDecimal("0.18"),
origin_country_code: 'DE',
currency_code: 'EUR'
)
line_item3 = LineItem.new(
name: 'Returnable Deposit',
quantity: 1,
unit: :PIECE,
gross_amount: '5',
net_amount: '5',
charge_amount: '5',
gross_amount: BigDecimal('5'),
net_amount: BigDecimal('5'),
charge_amount: BigDecimal('5'),
tax_category: :ZEROTAXPRODUCTS,
tax_percent: '0',
tax_amount: "0",
tax_amount: BigDecimal("0"),
origin_country_code: 'DE',
currency_code: 'EUR'
)
Expand All @@ -183,11 +183,66 @@ def make_de_invoice_with_multiple_tax_rates
payment_iban: 'DE02120300000000202051',
payment_terms_text: "Zahlbar innerhalb von 14 Tagen ohne Abzug",
tax_category: :STANDARDRATE,
tax_amount: '7.78',
basis_amount: '47.50',
grand_total_amount: '55.28',
tax_amount: BigDecimal('7.78'),
basis_amount: BigDecimal('47.50'),
grand_total_amount: BigDecimal('55.28'),
due_amount: 0,
paid_amount: '55.28',
paid_amount: BigDecimal('55.28'),
payment_due_date: Date.today + 14
)
end

def make_negative_de_invoice
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: 2,
unit: :PIECE,
gross_amount: BigDecimal('-100'),
net_amount: BigDecimal('-100'),
charge_amount: BigDecimal('-200'),
tax_category: :STANDARDRATE,
tax_percent: '19',
tax_amount: BigDecimal('-38'),
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],
currency_code: 'USD',
payment_type: :CREDITCARD,
payment_text: 'Kreditkarte',
payment_reference: 'INV 123123123',
payment_iban: 'DE02120300000000202051',
payment_terms_text: "Wir zahlen die Gutschrift unmittelbar aus",
tax_category: :STANDARDRATE,
tax_amount: BigDecimal('-38'),
basis_amount: BigDecimal('-200'),
grand_total_amount: BigDecimal('-238'),
due_amount: BigDecimal('-238'),
paid_amount: 0,
payment_due_date: Date.today + 14
)
end
Expand Down Expand Up @@ -334,5 +389,29 @@ def test_de_multiple_taxes_invoice_against_schematron_2
end
assert_equal [], errors
end
def test_negative_de_invoice_against_schematron_1
xml = make_negative_de_invoice.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
def test_negative_de_invoice_against_schematron_2
xml = make_negative_de_invoice.to_xml(version: 2)
v = Validator.new(xml, version: 2)
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