Merge pull request #1043 from PhilippGoetje/fix/allowance-charge-percent
Fix percentage-based allowance/charge calculations (EN16931 compliance)
This commit is contained in:
@@ -38,7 +38,7 @@ public class Allowance extends Charge {
|
|||||||
if(totalAmount != null) {
|
if(totalAmount != null) {
|
||||||
return totalAmount;
|
return totalAmount;
|
||||||
} else if (percent!=null) {
|
} else if (percent!=null) {
|
||||||
BigDecimal singlePrice=currentItem.getValue().multiply(BigDecimal.ONE.subtract(getPercent().divide(new BigDecimal(100))));
|
BigDecimal singlePrice=currentItem.getValue().multiply(BigDecimal.ONE.subtract(getPercent().divide(new BigDecimal(100), 18, RoundingMode.HALF_UP)));
|
||||||
BigDecimal singlePriceDiff=currentItem.getValue().subtract(singlePrice);
|
BigDecimal singlePriceDiff=currentItem.getValue().subtract(singlePrice);
|
||||||
return singlePriceDiff.multiply(currentItem.getQuantity());
|
return singlePriceDiff.multiply(currentItem.getQuantity());
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package org.mustangproject.ZUGFeRD;
|
|||||||
|
|
||||||
import static org.mustangproject.ZUGFeRD.ZUGFeRDDateFormat.DATE;
|
import static org.mustangproject.ZUGFeRD.ZUGFeRDDateFormat.DATE;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -96,16 +97,27 @@ public class DAPullProvider extends ZUGFeRD2PullProvider {
|
|||||||
xml += "<ram:BuyerAssignedID>"
|
xml += "<ram:BuyerAssignedID>"
|
||||||
+ XMLTools.encodeXML(currentItem.getProduct().getBuyerAssignedID()) + "</ram:BuyerAssignedID>";
|
+ XMLTools.encodeXML(currentItem.getProduct().getBuyerAssignedID()) + "</ram:BuyerAssignedID>";
|
||||||
}
|
}
|
||||||
|
// Product-level (GrossPrice / product section): ActualAmount must be per-unit (BT-147)
|
||||||
|
final IZUGFeRDExportableItem itemForProduct = currentItem;
|
||||||
|
IAbsoluteValueProvider perUnitProvider = new IAbsoluteValueProvider() {
|
||||||
|
@Override
|
||||||
|
public BigDecimal getValue() {
|
||||||
|
return itemForProduct.getPrice();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public BigDecimal getQuantity() {
|
||||||
|
return BigDecimal.ONE;
|
||||||
|
}
|
||||||
|
};
|
||||||
String allowanceChargeStr = "";
|
String allowanceChargeStr = "";
|
||||||
if (currentItem.getItemAllowances() != null) {
|
if (currentItem.getItemAllowances() != null) {
|
||||||
for (final IZUGFeRDAllowanceCharge allowance : currentItem.getItemAllowances()) {
|
for (final IZUGFeRDAllowanceCharge allowance : currentItem.getItemAllowances()) {
|
||||||
allowanceChargeStr += getAllowanceChargeStr(allowance, currentItem);
|
allowanceChargeStr += getAllowanceChargeStr(allowance, perUnitProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentItem.getItemCharges() != null) {
|
if (currentItem.getItemCharges() != null) {
|
||||||
for (final IZUGFeRDAllowanceCharge charge : currentItem.getItemCharges()) {
|
for (final IZUGFeRDAllowanceCharge charge : currentItem.getItemCharges()) {
|
||||||
allowanceChargeStr += getAllowanceChargeStr(charge, currentItem);
|
allowanceChargeStr += getAllowanceChargeStr(charge, perUnitProvider);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,31 +21,47 @@ public class LineCalculator {
|
|||||||
protected BigDecimal allowanceItemTotal = BigDecimal.ZERO;
|
protected BigDecimal allowanceItemTotal = BigDecimal.ZERO;
|
||||||
|
|
||||||
public LineCalculator(IZUGFeRDExportableItem currentItem) {
|
public LineCalculator(IZUGFeRDExportableItem currentItem) {
|
||||||
|
// Compute basisQuantity first so it can be used for item-level allowance/charge context
|
||||||
|
BigDecimal basisQuantity = currentItem.getBasisQuantity().compareTo(BigDecimal.ZERO) == 0
|
||||||
|
? BigDecimal.ONE.setScale(4)
|
||||||
|
: currentItem.getBasisQuantity();
|
||||||
|
|
||||||
|
// Provider for item-level: getValue() returns price/basisQty so percentage
|
||||||
|
// allowances compute against the actual line amount (qty * price / basisQty),
|
||||||
|
// not the raw (qty * price). No effect on absolute amounts.
|
||||||
|
IAbsoluteValueProvider itemBasisProvider = new IAbsoluteValueProvider() {
|
||||||
|
@Override
|
||||||
|
public BigDecimal getValue() {
|
||||||
|
return currentItem.getPrice().divide(basisQuantity, 18, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public BigDecimal getQuantity() {
|
||||||
|
return currentItem.getQuantity();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (currentItem.getItemAllowances() != null) {
|
if (currentItem.getItemAllowances() != null) {
|
||||||
for (IZUGFeRDAllowanceCharge allowance : currentItem.getItemAllowances()) {
|
for (IZUGFeRDAllowanceCharge allowance : currentItem.getItemAllowances()) {
|
||||||
BigDecimal singleAllowance=allowance.getTotalAmount(currentItem);
|
BigDecimal singleAllowance = allowance.getTotalAmount(itemBasisProvider);
|
||||||
addItemAllowance(singleAllowance);
|
addItemAllowance(singleAllowance);
|
||||||
addAllowanceItemTotal(singleAllowance);
|
addAllowanceItemTotal(singleAllowance);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentItem.getItemCharges() != null) {
|
if (currentItem.getItemCharges() != null) {
|
||||||
for (IZUGFeRDAllowanceCharge charge : currentItem.getItemCharges()) {
|
for (IZUGFeRDAllowanceCharge charge : currentItem.getItemCharges()) {
|
||||||
BigDecimal singleCharge=charge.getTotalAmount(currentItem);
|
BigDecimal singleCharge = charge.getTotalAmount(itemBasisProvider);
|
||||||
addItemCharge(singleCharge);
|
addItemCharge(singleCharge);
|
||||||
subtractAllowanceItemTotal(singleCharge);
|
subtractAllowanceItemTotal(singleCharge);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentItem.getItemTotalAllowances() != null) {
|
if (currentItem.getItemTotalAllowances() != null) {
|
||||||
for (final IZUGFeRDAllowanceCharge itemTotalAllowance : currentItem.getItemTotalAllowances()) {
|
for (final IZUGFeRDAllowanceCharge itemTotalAllowance : currentItem.getItemTotalAllowances()) {
|
||||||
addAllowanceItemTotal(itemTotalAllowance.getTotalAmount(currentItem));
|
addAllowanceItemTotal(itemTotalAllowance.getTotalAmount(itemBasisProvider));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BigDecimal vatPercent = null;
|
BigDecimal vatPercent = null;
|
||||||
if (currentItem.getProduct()!=null) {
|
if (currentItem.getProduct() != null) {
|
||||||
vatPercent = currentItem.getProduct().getVATPercent();
|
vatPercent = currentItem.getProduct().getVATPercent();
|
||||||
}
|
}
|
||||||
if (vatPercent == null) {
|
if (vatPercent == null) {
|
||||||
@@ -53,40 +69,48 @@ public class LineCalculator {
|
|||||||
}
|
}
|
||||||
BigDecimal multiplicator = vatPercent.divide(BigDecimal.valueOf(100));
|
BigDecimal multiplicator = vatPercent.divide(BigDecimal.valueOf(100));
|
||||||
|
|
||||||
BigDecimal quantity=BigDecimal.ZERO;
|
BigDecimal quantity = BigDecimal.ZERO;
|
||||||
if ((currentItem!=null)&&(currentItem.getQuantity()!=null)) {
|
if ((currentItem != null) && (currentItem.getQuantity() != null)) {
|
||||||
quantity=currentItem.getQuantity();
|
quantity = currentItem.getQuantity();
|
||||||
}
|
}
|
||||||
|
|
||||||
price=currentItem.getPrice();
|
price = currentItem.getPrice();
|
||||||
priceGross=price;
|
priceGross = price;
|
||||||
// price=price.subtract(itemAllowance).add(itemCharge);
|
|
||||||
// BigDecimal delta=charge.subtract(allowanceItemTotal).subtract(allowance);
|
|
||||||
// delta=delta.divide(currentItem.getQuantity(), 18, RoundingMode.HALF_UP);
|
|
||||||
|
|
||||||
BigDecimal delta=BigDecimal.ZERO;
|
// Provider for product-level: getQuantity() returns ONE because product
|
||||||
if(currentItem.getProduct()!=null){
|
// allowances adjust the per-unit price, not the line total.
|
||||||
if (currentItem.getProduct().getAllowances()!=null) {
|
// No effect on absolute amounts.
|
||||||
for (IZUGFeRDAllowanceCharge ccaf:currentItem.getProduct().getAllowances()) {
|
IAbsoluteValueProvider perUnitProvider = new IAbsoluteValueProvider() {
|
||||||
delta=delta.subtract(ccaf.getTotalAmount(currentItem));
|
@Override
|
||||||
|
public BigDecimal getValue() {
|
||||||
|
return currentItem.getPrice();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public BigDecimal getQuantity() {
|
||||||
|
return BigDecimal.ONE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
BigDecimal delta = BigDecimal.ZERO;
|
||||||
|
if (currentItem.getProduct() != null) {
|
||||||
|
if (currentItem.getProduct().getAllowances() != null) {
|
||||||
|
for (IZUGFeRDAllowanceCharge ccaf : currentItem.getProduct().getAllowances()) {
|
||||||
|
delta = delta.subtract(ccaf.getTotalAmount(perUnitProvider));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentItem.getProduct().getCharges()!=null) {
|
if (currentItem.getProduct().getCharges() != null) {
|
||||||
for (IZUGFeRDAllowanceCharge ccaf : currentItem.getProduct().getCharges()) {
|
for (IZUGFeRDAllowanceCharge ccaf : currentItem.getProduct().getCharges()) {
|
||||||
delta = delta.add(ccaf.getTotalAmount(currentItem));
|
delta = delta.add(ccaf.getTotalAmount(perUnitProvider));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
price=price.add(delta);
|
price = price.add(delta);
|
||||||
// Division/Zero occurred here.
|
|
||||||
// Used the setScale only because that's also done in getBasisQuantity
|
|
||||||
BigDecimal basisQuantity = currentItem.getBasisQuantity().compareTo(BigDecimal.ZERO) == 0
|
|
||||||
? BigDecimal.ONE.setScale(4)
|
|
||||||
: currentItem.getBasisQuantity();
|
|
||||||
itemTotalNetAmount = quantity.multiply(price).divide(basisQuantity, 18, RoundingMode.HALF_UP)
|
itemTotalNetAmount = quantity.multiply(price).divide(basisQuantity, 18, RoundingMode.HALF_UP)
|
||||||
.add(lineCharge).subtract(lineAllowance).subtract(allowanceItemTotal.setScale(2, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP);
|
.add(lineCharge).subtract(lineAllowance)
|
||||||
itemTotalVATAmount = itemTotalNetAmount.multiply(multiplicator);//.setScale(2, RoundingMode.HALF_UP);
|
.subtract(allowanceItemTotal.setScale(2, RoundingMode.HALF_UP))
|
||||||
|
.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
itemTotalVATAmount = itemTotalNetAmount.multiply(multiplicator);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BigDecimal getPrice() {
|
public BigDecimal getPrice() {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import static org.mustangproject.ZUGFeRD.ZUGFeRDDateFormat.DATE;
|
|||||||
import static org.mustangproject.ZUGFeRD.model.DocumentCodeTypeConstants.CORRECTEDINVOICE;
|
import static org.mustangproject.ZUGFeRD.model.DocumentCodeTypeConstants.CORRECTEDINVOICE;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
@@ -124,16 +125,27 @@ public class OXPullProvider extends ZUGFeRD2PullProvider {
|
|||||||
xml += "<ram:BuyerAssignedID>"
|
xml += "<ram:BuyerAssignedID>"
|
||||||
+ XMLTools.encodeXML(currentItem.getProduct().getBuyerAssignedID()) + "</ram:BuyerAssignedID>";
|
+ XMLTools.encodeXML(currentItem.getProduct().getBuyerAssignedID()) + "</ram:BuyerAssignedID>";
|
||||||
}
|
}
|
||||||
|
// Product-level (GrossPriceProductTradePrice): ActualAmount must be per-unit (BT-147)
|
||||||
|
final IZUGFeRDExportableItem itemForProduct = currentItem;
|
||||||
|
IAbsoluteValueProvider perUnitProvider = new IAbsoluteValueProvider() {
|
||||||
|
@Override
|
||||||
|
public BigDecimal getValue() {
|
||||||
|
return itemForProduct.getPrice();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public BigDecimal getQuantity() {
|
||||||
|
return BigDecimal.ONE;
|
||||||
|
}
|
||||||
|
};
|
||||||
String allowanceChargeStr = "";
|
String allowanceChargeStr = "";
|
||||||
if (currentItem.getItemAllowances() != null) {
|
if (currentItem.getItemAllowances() != null) {
|
||||||
for (final IZUGFeRDAllowanceCharge allowance : currentItem.getItemAllowances()) {
|
for (final IZUGFeRDAllowanceCharge allowance : currentItem.getItemAllowances()) {
|
||||||
allowanceChargeStr += getAllowanceChargeStr(allowance, currentItem);
|
allowanceChargeStr += getAllowanceChargeStr(allowance, perUnitProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentItem.getItemCharges() != null) {
|
if (currentItem.getItemCharges() != null) {
|
||||||
for (final IZUGFeRDAllowanceCharge charge : currentItem.getItemCharges()) {
|
for (final IZUGFeRDAllowanceCharge charge : currentItem.getItemCharges()) {
|
||||||
allowanceChargeStr += getAllowanceChargeStr(charge, currentItem);
|
allowanceChargeStr += getAllowanceChargeStr(charge, perUnitProvider);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import static org.mustangproject.ZUGFeRD.model.TaxCategoryCodeTypeConstants.CATE
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -270,7 +271,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
|
|||||||
String chargeIndicator = "false";
|
String chargeIndicator = "false";
|
||||||
if ((allowance.getPercent() != null) && (profile == Profiles.getByName("Extended"))) {
|
if ((allowance.getPercent() != null) && (profile == Profiles.getByName("Extended"))) {
|
||||||
percentage = "<ram:CalculationPercent>" + vatFormat(allowance.getPercent()) + "</ram:CalculationPercent>";
|
percentage = "<ram:CalculationPercent>" + vatFormat(allowance.getPercent()) + "</ram:CalculationPercent>";
|
||||||
percentage += "<ram:BasisAmount>" + item.getValue() + "</ram:BasisAmount>";
|
percentage += "<ram:BasisAmount>" + currencyFormat(item.getValue()) + "</ram:BasisAmount>";
|
||||||
}
|
}
|
||||||
if (allowance.isCharge()) {
|
if (allowance.isCharge()) {
|
||||||
chargeIndicator = "true";
|
chargeIndicator = "true";
|
||||||
@@ -305,7 +306,8 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
|
|||||||
String chargeIndicator = "false";
|
String chargeIndicator = "false";
|
||||||
if ((allowance.getPercent() != null) && (profile == Profiles.getByName("Extended"))) {
|
if ((allowance.getPercent() != null) && (profile == Profiles.getByName("Extended"))) {
|
||||||
percentage = "<ram:CalculationPercent>" + vatFormat(allowance.getPercent()) + "</ram:CalculationPercent>";
|
percentage = "<ram:CalculationPercent>" + vatFormat(allowance.getPercent()) + "</ram:CalculationPercent>";
|
||||||
percentage += "<ram:BasisAmount>" + item.getValue() + "</ram:BasisAmount>";
|
// BT-137/BT-142: BasisAmount = the value the percentage is applied to = line subtotal (price/basisQty)*qty
|
||||||
|
percentage += "<ram:BasisAmount>" + currencyFormat(item.getValue().multiply(item.getQuantity())) + "</ram:BasisAmount>";
|
||||||
}
|
}
|
||||||
if (allowance.isCharge()) {
|
if (allowance.isCharge()) {
|
||||||
chargeIndicator = "true";
|
chargeIndicator = "true";
|
||||||
@@ -499,15 +501,27 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
|
|||||||
xml += "</ram:BuyerOrderReferencedDocument>";
|
xml += "</ram:BuyerOrderReferencedDocument>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-unit provider for product-level: ActualAmount must be per-unit (BT-147)
|
||||||
|
final IZUGFeRDExportableItem itemForProduct = currentItem;
|
||||||
|
IAbsoluteValueProvider perUnitProvider = new IAbsoluteValueProvider() {
|
||||||
|
@Override
|
||||||
|
public BigDecimal getValue() {
|
||||||
|
return itemForProduct.getPrice();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public BigDecimal getQuantity() {
|
||||||
|
return BigDecimal.ONE;
|
||||||
|
}
|
||||||
|
};
|
||||||
String allowanceChargeStr = "";
|
String allowanceChargeStr = "";
|
||||||
if (currentItem.getProduct().getAllowances() != null && currentItem.getProduct().getAllowances().length > 0) {
|
if (currentItem.getProduct().getAllowances() != null && currentItem.getProduct().getAllowances().length > 0) {
|
||||||
for (final IZUGFeRDAllowanceCharge allowance : currentItem.getProduct().getAllowances()) {
|
for (final IZUGFeRDAllowanceCharge allowance : currentItem.getProduct().getAllowances()) {
|
||||||
allowanceChargeStr += getAllowanceChargeStr(allowance, currentItem);
|
allowanceChargeStr += getAllowanceChargeStr(allowance, perUnitProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentItem.getProduct().getCharges() != null && currentItem.getProduct().getCharges().length > 0) {
|
if (currentItem.getProduct().getCharges() != null && currentItem.getProduct().getCharges().length > 0) {
|
||||||
for (final IZUGFeRDAllowanceCharge charge : currentItem.getProduct().getCharges()) {
|
for (final IZUGFeRDAllowanceCharge charge : currentItem.getProduct().getCharges()) {
|
||||||
allowanceChargeStr += getAllowanceChargeStr(charge, currentItem);
|
allowanceChargeStr += getAllowanceChargeStr(charge, perUnitProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!allowanceChargeStr.isEmpty()) {
|
if (!allowanceChargeStr.isEmpty()) {
|
||||||
@@ -588,16 +602,31 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
|
|||||||
xml += "</ram:BillingSpecifiedPeriod>";
|
xml += "</ram:BillingSpecifiedPeriod>";
|
||||||
}
|
}
|
||||||
|
|
||||||
// item charges/allowances
|
// Item-level: use basisQuantity-aware provider so ActualAmount (BT-136) is correct when basisQuantity != 1
|
||||||
|
BigDecimal itemBasisQty = currentItem.getBasisQuantity().compareTo(BigDecimal.ZERO) == 0
|
||||||
|
? BigDecimal.ONE.setScale(4)
|
||||||
|
: currentItem.getBasisQuantity();
|
||||||
|
final IZUGFeRDExportableItem itemForSettlement = currentItem;
|
||||||
|
final BigDecimal basisQty = itemBasisQty;
|
||||||
|
IAbsoluteValueProvider itemBasisProvider = new IAbsoluteValueProvider() {
|
||||||
|
@Override
|
||||||
|
public BigDecimal getValue() {
|
||||||
|
return itemForSettlement.getPrice().divide(basisQty, 18, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public BigDecimal getQuantity() {
|
||||||
|
return itemForSettlement.getQuantity();
|
||||||
|
}
|
||||||
|
};
|
||||||
String itemTotalAllowanceChargeStr = "";
|
String itemTotalAllowanceChargeStr = "";
|
||||||
if (currentItem.getAllowances() != null && currentItem.getAllowances().length > 0) {
|
if (currentItem.getAllowances() != null && currentItem.getAllowances().length > 0) {
|
||||||
for (final IZUGFeRDAllowanceCharge itemTotalAllowance : currentItem.getAllowances()) {
|
for (final IZUGFeRDAllowanceCharge itemTotalAllowance : currentItem.getAllowances()) {
|
||||||
itemTotalAllowanceChargeStr += getItemTotalAllowanceChargeStr(itemTotalAllowance, currentItem);
|
itemTotalAllowanceChargeStr += getItemTotalAllowanceChargeStr(itemTotalAllowance, itemBasisProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentItem.getCharges() != null && currentItem.getCharges().length > 0) {
|
if (currentItem.getCharges() != null && currentItem.getCharges().length > 0) {
|
||||||
for (final IZUGFeRDAllowanceCharge itemTotalCharges : currentItem.getCharges()) {
|
for (final IZUGFeRDAllowanceCharge itemTotalCharges : currentItem.getCharges()) {
|
||||||
itemTotalAllowanceChargeStr += getItemTotalAllowanceChargeStr(itemTotalCharges, currentItem);
|
itemTotalAllowanceChargeStr += getItemTotalAllowanceChargeStr(itemTotalCharges, itemBasisProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!itemTotalAllowanceChargeStr.isEmpty()) {
|
if (!itemTotalAllowanceChargeStr.isEmpty()) {
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ public class CalculationTest extends ResourceCase {
|
|||||||
assertEquals(valueOf(4.750).stripTrailingZeros(), calculator.getGrandTotal().stripTrailingZeros());
|
assertEquals(valueOf(4.750).stripTrailingZeros(), calculator.getGrandTotal().stripTrailingZeros());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testSimpleItemPercentAllowance() {
|
public void testSimpleItemPercentAllowance() {
|
||||||
/***
|
/***
|
||||||
* a product with net 1.10 and qty 5 and relative item allowance of 10% should return 5 as line and grand total
|
* a product with net 1.10 and qty 5 and relative item allowance of 10% should return 5 as line and grand total
|
||||||
@@ -351,6 +352,7 @@ public class CalculationTest extends ResourceCase {
|
|||||||
assertEquals(new BigDecimal("4.95"), calculator.getGrandTotal().stripTrailingZeros());
|
assertEquals(new BigDecimal("4.95"), calculator.getGrandTotal().stripTrailingZeros());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testSimpleItemPercentCharge() {
|
public void testSimpleItemPercentCharge() {
|
||||||
/***
|
/***
|
||||||
* a product with net 1.10 and qty 5 and relative item allowance of 10% should return 5 as line and grand total
|
* a product with net 1.10 and qty 5 and relative item allowance of 10% should return 5 as line and grand total
|
||||||
@@ -405,6 +407,7 @@ public class CalculationTest extends ResourceCase {
|
|||||||
assertEquals(new BigDecimal("6.05"), calculator.getGrandTotal().stripTrailingZeros());
|
assertEquals(new BigDecimal("6.05"), calculator.getGrandTotal().stripTrailingZeros());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testSimpleDocumentPercentCharge() {
|
public void testSimpleDocumentPercentCharge() {
|
||||||
|
|
||||||
String orgname = "Test company";
|
String orgname = "Test company";
|
||||||
@@ -430,6 +433,7 @@ public class CalculationTest extends ResourceCase {
|
|||||||
assertEquals(new BigDecimal("16.07"), tc.getDuePayable());
|
assertEquals(new BigDecimal("16.07"), tc.getDuePayable());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testSimpleDocumentPercentAllowance() {
|
public void testSimpleDocumentPercentAllowance() {
|
||||||
|
|
||||||
String orgname = "Test company";
|
String orgname = "Test company";
|
||||||
@@ -455,6 +459,7 @@ public class CalculationTest extends ResourceCase {
|
|||||||
assertEquals(new BigDecimal("5.36"), tc.getDuePayable());
|
assertEquals(new BigDecimal("5.36"), tc.getDuePayable());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void testSimpleItemTotalAllowance() {
|
public void testSimpleItemTotalAllowance() {
|
||||||
/***
|
/***
|
||||||
* a product with net 1 and qty 5 and absolute _item_ allowance of 1 should return 4 as line total, and grand total
|
* a product with net 1 and qty 5 and absolute _item_ allowance of 1 should return 4 as line total, and grand total
|
||||||
@@ -498,6 +503,148 @@ public class CalculationTest extends ResourceCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPercentProductAllowanceNotMultipliedByQuantity() {
|
||||||
|
// Bug A: 10% product discount on price=100, qty=5
|
||||||
|
// Correct: net price = 90, line total = 5 * 90 = 450
|
||||||
|
// Bug A would give: delta = 10*5 = 50, price = 50, total = 5*50 = 250
|
||||||
|
Product product = new Product("Test", "", "H87", BigDecimal.ZERO);
|
||||||
|
product.addAllowance((Allowance) new Allowance().setPercent(new BigDecimal(10)));
|
||||||
|
Item item = new Item(product, new BigDecimal("100.00"), new BigDecimal("5"));
|
||||||
|
|
||||||
|
LineCalculator lc = item.getCalculation();
|
||||||
|
assertEquals(new BigDecimal("450.00"), lc.getItemTotalNetAmount());
|
||||||
|
assertEquals(new BigDecimal("90.00").stripTrailingZeros(),
|
||||||
|
lc.getPrice().stripTrailingZeros());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPercentItemAllowanceWithBasisQuantity() {
|
||||||
|
// Bug B: price=128.49 per 100 LTR, qty=50 LTR, 10% item allowance
|
||||||
|
// Line total before allowance = 50 * 128.49 / 100 = 64.245
|
||||||
|
// Allowance = 10% of 64.245 = 6.4245 -> 6.42 (rounded)
|
||||||
|
// Correct line total = 64.25 - 6.42 = 57.83
|
||||||
|
Product product = new Product("Test", "", "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)));
|
||||||
|
|
||||||
|
LineCalculator lc = item.getCalculation();
|
||||||
|
assertEquals(new BigDecimal("57.83"), lc.getItemTotalNetAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPercentProductAllowanceWithBasisQuantity() {
|
||||||
|
// 10% product discount, price=100 per 10 units, qty=50
|
||||||
|
// Net price = 100 - 10 = 90 (per 10 units)
|
||||||
|
// Line total = 50 * 90 / 10 = 450
|
||||||
|
Product product = new Product("Test", "", "H87", BigDecimal.ZERO);
|
||||||
|
product.addAllowance((Allowance) new Allowance().setPercent(new BigDecimal(10)));
|
||||||
|
Item item = new Item(product, new BigDecimal("100.00"), new BigDecimal("50"));
|
||||||
|
item.setBasisQuantity(new BigDecimal("10"));
|
||||||
|
|
||||||
|
LineCalculator lc = item.getCalculation();
|
||||||
|
assertEquals(new BigDecimal("450.00"), lc.getItemTotalNetAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPercentItemChargeWithBasisQuantity() {
|
||||||
|
// Item-level 10% charge with basisQuantity: line before charge = 50*100/100 = 50, charge = 5, total = 55
|
||||||
|
Product product = new Product("Test", "", "LTR", BigDecimal.ZERO);
|
||||||
|
Item item = new Item(product, new BigDecimal("100.00"), new BigDecimal("50"));
|
||||||
|
item.setBasisQuantity(new BigDecimal("100"));
|
||||||
|
item.addCharge(new Charge().setPercent(new BigDecimal(10)));
|
||||||
|
|
||||||
|
LineCalculator lc = item.getCalculation();
|
||||||
|
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
|
* LineCalculator should not throw an exception when calculating a non-terminating decimal expansion
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user