diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index 02f59b5c..ad87e52f 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -306,7 +306,8 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { String chargeIndicator = "false"; if ((allowance.getPercent() != null) && (profile == Profiles.getByName("Extended"))) { percentage = "" + vatFormat(allowance.getPercent()) + ""; - percentage += "" + currencyFormat(item.getValue()) + ""; + // BT-137/BT-142: BasisAmount = the value the percentage is applied to = line subtotal (price/basisQty)*qty + percentage += "" + currencyFormat(item.getValue().multiply(item.getQuantity())) + ""; } if (allowance.isCharge()) { chargeIndicator = "true"; diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/CalculationTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/CalculationTest.java index d902e6c3..535aa862 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/CalculationTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/CalculationTest.java @@ -559,6 +559,92 @@ public class CalculationTest extends ResourceCase { assertEquals(new BigDecimal("55.00"), lc.getItemTotalNetAmount()); } + @Test + public void testLineLevelAllowanceBasisAmountIsLineSubtotal() { + // BT-137: line-allowance BasisAmount = (price / basisQty) * qty (line subtotal), NOT the per-unit value. + // price=128.49 per 100 LTR, qty=50 LTR, basisQty=100 + // line subtotal before allowance = 128.49 / 100 * 50 = 64.245 -> 64.25 (HALF_UP, BR-DEC-25) + // allowance ActualAmount = 10% of 64.245 = 6.4245 -> 6.42 + SimpleDateFormat sqlDate = new SimpleDateFormat("yyyy-MM-dd"); + + Invoice invoice = new Invoice(); + invoice.setDocumentName("Rechnung"); + invoice.setNumber("BT137-ALLOWANCE"); + try { + invoice.setIssueDate(sqlDate.parse("2024-01-01")); + invoice.setDueDate(sqlDate.parse("2024-01-31")); + } catch (Exception e) { + LOGGER.error("Failed to set dates", e); + } + TradeParty sender = new TradeParty("Sender GmbH", "Hauptstr. 1", "10115", "Berlin", "DE"); + sender.addVATID("DE123456789"); + invoice.setSender(sender); + TradeParty recipient = new TradeParty("Recipient GmbH", "Nebenstr. 2", "10116", "Berlin", "DE"); + recipient.addVATID("DE987654321"); + invoice.setRecipient(recipient); + + Product product = new Product("Testartikel", "", "LTR", BigDecimal.ZERO); + Item item = new Item(product, new BigDecimal("128.49"), new BigDecimal("50")); + item.setBasisQuantity(new BigDecimal("100")); + item.addAllowance(new Allowance().setPercent(new BigDecimal(10)).setTaxPercent(BigDecimal.ZERO)); + invoice.addItem(item); + + ZUGFeRD2PullProvider zf2p = new ZUGFeRD2PullProvider(); + zf2p.setProfile(Profiles.getByName("Extended")); + zf2p.generateXML(invoice); + + String theXML = new String(zf2p.getXML(), StandardCharsets.UTF_8); + assertThat(theXML).valueByXPath("//*[local-name()='SpecifiedTradeAllowanceCharge'][*[local-name()='ChargeIndicator']/*[local-name()='Indicator']='false']/*[local-name()='BasisAmount']") + .asString() + .isEqualTo("64.25"); // (128.49/100)*50 = 64.245 rounded HALF_UP + assertThat(theXML).valueByXPath("//*[local-name()='SpecifiedTradeAllowanceCharge'][*[local-name()='ChargeIndicator']/*[local-name()='Indicator']='false']/*[local-name()='ActualAmount']") + .asString() + .isEqualTo("6.42"); // 64.245 * 0.10 = 6.4245 rounded HALF_UP + } + + @Test + public void testLineLevelChargeBasisAmountIsLineSubtotal() { + // BT-142: line-charge BasisAmount = (price / basisQty) * qty (line subtotal), NOT the per-unit value. + // price=200.00 per 4 units, qty=10, basisQty=4 + // line subtotal before charge = 200.00 / 4 * 10 = 500.00 + // charge ActualAmount = 5% of 500.00 = 25.00 + SimpleDateFormat sqlDate = new SimpleDateFormat("yyyy-MM-dd"); + + Invoice invoice = new Invoice(); + invoice.setDocumentName("Rechnung"); + invoice.setNumber("BT142-CHARGE"); + try { + invoice.setIssueDate(sqlDate.parse("2024-01-01")); + invoice.setDueDate(sqlDate.parse("2024-01-31")); + } catch (Exception e) { + LOGGER.error("Failed to set dates", e); + } + TradeParty sender = new TradeParty("Sender GmbH", "Hauptstr. 1", "10115", "Berlin", "DE"); + sender.addVATID("DE123456789"); + invoice.setSender(sender); + TradeParty recipient = new TradeParty("Recipient GmbH", "Nebenstr. 2", "10116", "Berlin", "DE"); + recipient.addVATID("DE987654321"); + invoice.setRecipient(recipient); + + Product product = new Product("Testartikel", "", "H87", BigDecimal.ZERO); + Item item = new Item(product, new BigDecimal("200.00"), new BigDecimal("10")); + item.setBasisQuantity(new BigDecimal("4")); + item.addCharge(new Charge().setPercent(new BigDecimal(5)).setTaxPercent(BigDecimal.ZERO)); + invoice.addItem(item); + + ZUGFeRD2PullProvider zf2p = new ZUGFeRD2PullProvider(); + zf2p.setProfile(Profiles.getByName("Extended")); + zf2p.generateXML(invoice); + + String theXML = new String(zf2p.getXML(), StandardCharsets.UTF_8); + assertThat(theXML).valueByXPath("//*[local-name()='SpecifiedTradeAllowanceCharge'][*[local-name()='ChargeIndicator']/*[local-name()='Indicator']='true']/*[local-name()='BasisAmount']") + .asString() + .isEqualTo("500.00"); // (200.00/4)*10 = 500.00 + assertThat(theXML).valueByXPath("//*[local-name()='SpecifiedTradeAllowanceCharge'][*[local-name()='ChargeIndicator']/*[local-name()='Indicator']='true']/*[local-name()='ActualAmount']") + .asString() + .isEqualTo("25.00"); // 500.00 * 0.05 = 25.00 + } + /** * LineCalculator should not throw an exception when calculating a non-terminating decimal expansion */