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
*/