Merge branch 'issues/480'

This commit is contained in:
jstaerk
2024-11-18 09:37:34 +01:00
6 changed files with 82 additions and 28 deletions

View File

@@ -66,6 +66,7 @@ public class Invoice implements IExportableTransaction {
protected String despatchAdviceReferencedDocumentID = null; protected String despatchAdviceReferencedDocumentID = null;
protected String vatDueDateTypeCode = null; protected String vatDueDateTypeCode = null;
protected String creditorReferenceID; // required when direct debit is used. protected String creditorReferenceID; // required when direct debit is used.
private BigDecimal roundingAmount=null;
public Invoice() { public Invoice() {
ZFItems = new ArrayList<>(); ZFItems = new ArrayList<>();
@@ -467,6 +468,24 @@ public class Invoice implements IExportableTransaction {
return sender; return sender;
} }
/***
* for currency rounding differences to 5ct e.g. in Netherlands ("Rappenrundung")
* @return null if not set, otherwise BigDecimal of Euros
*/
@Override
public BigDecimal getRoundingAmount() {
return roundingAmount;
}
/***
* for currency rounding differences to 5ct e.g. in Netherlands ("Rappenrundung")
* @return fluent setter
*/
public Invoice setRoundingAmount(BigDecimal amount) {
roundingAmount=amount;
return this;
}
/*** /***
* sets a named sender contact * sets a named sender contact
* @deprecated use setSender * @deprecated use setSender

View File

@@ -321,6 +321,15 @@ public interface IExportableTransaction {
return false; return false;
} }
/**
* supplier identification assigned by the costumer
*
* @return the sender's identification
*/
default BigDecimal getRoundingAmount() {
return null;
}
/** /**
* get reference document number typically used for Invoice Corrections Will be * get reference document number typically used for Invoice Corrections Will be
* added as IncludedNote in comfort profile * added as IncludedNote in comfort profile

View File

@@ -10,7 +10,7 @@ import java.util.stream.Stream;
/*** /***
* The Transactioncalculator e.g. adds the line totals and applies VAT on whole * The Transactioncalculator e.g. adds the line totals and applies VAT on whole
* invoices * invoices
* *
* @see LineCalculator * @see LineCalculator
*/ */
public class TransactionCalculator implements IAbsoluteValueProvider { public class TransactionCalculator implements IAbsoluteValueProvider {
@@ -27,7 +27,7 @@ public class TransactionCalculator implements IAbsoluteValueProvider {
/*** /***
* if something had already been paid in advance, this will get it from the * if something had already been paid in advance, this will get it from the
* transaction * transaction
* *
* @return prepaid amount * @return prepaid amount
*/ */
protected BigDecimal getTotalPrepaid() { protected BigDecimal getTotalPrepaid() {
@@ -41,19 +41,19 @@ public class TransactionCalculator implements IAbsoluteValueProvider {
/*** /***
* the invoice total with VAT, allowances and * the invoice total with VAT, allowances and
* charges, WITHOUT considering prepaid amount * charges, WITHOUT considering prepaid amount
* *
* @return the invoice total including taxes * @return the invoice total including taxes
*/ */
public BigDecimal getGrandTotal() { public BigDecimal getGrandTotal() {
final BigDecimal res = getTaxBasis(); BigDecimal basis = getTaxBasis();
return getVATPercentAmountMap().values().stream().map(VATAmount::getCalculated) return getVATPercentAmountMap().values().stream().map(VATAmount::getCalculated)
.map(p -> p.setScale(2, RoundingMode.HALF_UP)).reduce(BigDecimal.ZERO, BigDecimal::add).add(res); .map(p -> p.setScale(2, RoundingMode.HALF_UP)).reduce(BigDecimal.ZERO, BigDecimal::add).add(basis);
} }
/*** /***
* returns total of charges for this tax rate * returns total of charges for this tax rate
* *
* @param percent a specific rate, or null for any rate * @param percent a specific rate, or null for any rate
* @return the total amount * @return the total amount
*/ */
@@ -77,7 +77,7 @@ public class TransactionCalculator implements IAbsoluteValueProvider {
/*** /***
* returns a (potentially concatenated) string of charge reasons, or "Charges" * returns a (potentially concatenated) string of charge reasons, or "Charges"
* if none are defined * if none are defined
* *
* @param percent a specific rate, or null for any rate * @param percent a specific rate, or null for any rate
* @return the space separated String * @return the space separated String
*/ */
@@ -95,7 +95,7 @@ public class TransactionCalculator implements IAbsoluteValueProvider {
if ((charges != null) && (charges.length > 0)) { if ((charges != null) && (charges.length > 0)) {
for (IZUGFeRDAllowanceCharge currentCharge : charges) { for (IZUGFeRDAllowanceCharge currentCharge : charges) {
if ((percent == null) || (currentCharge.getTaxPercent().compareTo(percent) == 0) if ((percent == null) || (currentCharge.getTaxPercent().compareTo(percent) == 0)
&& currentCharge.getReason() != null) { && currentCharge.getReason() != null) {
res += currentCharge.getReason() + " "; res += currentCharge.getReason() + " ";
} }
} }
@@ -107,7 +107,7 @@ public class TransactionCalculator implements IAbsoluteValueProvider {
/*** /***
* returns a (potentially concatenated) string of allowance reasons, or * returns a (potentially concatenated) string of allowance reasons, or
* "Allowances", if none are defined * "Allowances", if none are defined
* *
* @param percent a specific rate, or null for any rate * @param percent a specific rate, or null for any rate
* @return the space separated String * @return the space separated String
*/ */
@@ -122,7 +122,7 @@ public class TransactionCalculator implements IAbsoluteValueProvider {
/*** /***
* returns total of allowances for this tax rate * returns total of allowances for this tax rate
* *
* @param percent a specific rate, or null for any rate * @param percent a specific rate, or null for any rate
* @return the total amount * @return the total amount
*/ */
@@ -134,25 +134,25 @@ public class TransactionCalculator implements IAbsoluteValueProvider {
/*** /***
* returns the total net value of all items, without document level * returns the total net value of all items, without document level
* charges/allowances * charges/allowances
* *
* @return item sum * @return item sum
*/ */
protected BigDecimal getTotal() { protected BigDecimal getTotal() {
BigDecimal dec = Stream.of(trans.getZFItems()).map(LineCalculator::new) BigDecimal dec = Stream.of(trans.getZFItems()).map(LineCalculator::new)
.map(LineCalculator::getItemTotalNetAmount).reduce(ZERO, BigDecimal::add); .map(LineCalculator::getItemTotalNetAmount).reduce(ZERO, BigDecimal::add);
return dec; return dec;
} }
/*** /***
* returns the total net value of the invoice, including charges/allowances on * returns the total net value of the invoice, including charges/allowances on
* document level * document level
* *
* @return item sum +- charges/allowances * @return item sum +- charges/allowances
*/ */
protected BigDecimal getTaxBasis() { protected BigDecimal getTaxBasis() {
return getTotal().add(getChargesForPercent(null).setScale(2, RoundingMode.HALF_UP)) return getTotal().add(getChargesForPercent(null).setScale(2, RoundingMode.HALF_UP))
.subtract(getAllowancesForPercent(null).setScale(2, RoundingMode.HALF_UP)) .subtract(getAllowancesForPercent(null).setScale(2, RoundingMode.HALF_UP))
.setScale(2, RoundingMode.HALF_UP); .setScale(2, RoundingMode.HALF_UP);
} }
/** /**
@@ -171,9 +171,9 @@ public class TransactionCalculator implements IAbsoluteValueProvider {
if (percent != null) { if (percent != null) {
LineCalculator lc = new LineCalculator(currentItem); LineCalculator lc = new LineCalculator(currentItem);
VATAmount itemVATAmount = new VATAmount(lc.getItemTotalNetAmount(), lc.getItemTotalVATAmount(), VATAmount itemVATAmount = new VATAmount(lc.getItemTotalNetAmount(), lc.getItemTotalVATAmount(),
currentItem.getProduct().getTaxCategoryCode(), vatDueDateTypeCode); currentItem.getProduct().getTaxCategoryCode(), vatDueDateTypeCode);
String reasonText=currentItem.getProduct().getTaxExemptionReason(); String reasonText = currentItem.getProduct().getTaxExemptionReason();
if (reasonText!=null) { if (reasonText != null) {
itemVATAmount.setVatExemptionReasonText(reasonText); itemVATAmount.setVatExemptionReasonText(reasonText);
} }
VATAmount current = hm.get(percent.stripTrailingZeros()); VATAmount current = hm.get(percent.stripTrailingZeros());
@@ -193,8 +193,8 @@ public class TransactionCalculator implements IAbsoluteValueProvider {
VATAmount theAmount = hm.get(taxPercent.stripTrailingZeros()); VATAmount theAmount = hm.get(taxPercent.stripTrailingZeros());
if (theAmount == null) { if (theAmount == null) {
theAmount = new VATAmount(BigDecimal.ZERO, BigDecimal.ZERO, theAmount = new VATAmount(BigDecimal.ZERO, BigDecimal.ZERO,
currentCharge.getCategoryCode() != null ? currentCharge.getCategoryCode() : "S", currentCharge.getCategoryCode() != null ? currentCharge.getCategoryCode() : "S",
vatDueDateTypeCode); vatDueDateTypeCode);
} }
theAmount.setBasis(theAmount.getBasis().add(currentCharge.getTotalAmount(this))); theAmount.setBasis(theAmount.getBasis().add(currentCharge.getTotalAmount(this)));
BigDecimal factor = taxPercent.divide(new BigDecimal(100)); BigDecimal factor = taxPercent.divide(new BigDecimal(100));
@@ -211,8 +211,8 @@ public class TransactionCalculator implements IAbsoluteValueProvider {
VATAmount theAmount = hm.get(taxPercent.stripTrailingZeros()); VATAmount theAmount = hm.get(taxPercent.stripTrailingZeros());
if (theAmount == null) { if (theAmount == null) {
theAmount = new VATAmount(BigDecimal.ZERO, BigDecimal.ZERO, theAmount = new VATAmount(BigDecimal.ZERO, BigDecimal.ZERO,
currentAllowance.getCategoryCode() != null ? currentAllowance.getCategoryCode() : "S", currentAllowance.getCategoryCode() != null ? currentAllowance.getCategoryCode() : "S",
vatDueDateTypeCode); vatDueDateTypeCode);
} }
theAmount.setBasis(theAmount.getBasis().subtract(currentAllowance.getTotalAmount(this))); theAmount.setBasis(theAmount.getBasis().subtract(currentAllowance.getTotalAmount(this)));
BigDecimal factor = taxPercent.divide(new BigDecimal(100)); BigDecimal factor = taxPercent.divide(new BigDecimal(100));
@@ -239,4 +239,11 @@ public class TransactionCalculator implements IAbsoluteValueProvider {
return getAllowancesForPercent(null).setScale(2, RoundingMode.HALF_UP); return getAllowancesForPercent(null).setScale(2, RoundingMode.HALF_UP);
} }
public BigDecimal getDuePayable() {
BigDecimal res = getGrandTotal().subtract(getTotalPrepaid());
if (trans.getRoundingAmount() != null) {
res = res.add(trans.getRoundingAmount());
}
return res;
}
} }

View File

@@ -334,7 +334,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
this.trans = trans; this.trans = trans;
this.calc = new TransactionCalculator(trans); this.calc = new TransactionCalculator(trans);
boolean hasDueDate = trans.getDueDate()!=null; boolean hasDueDate = trans.getDueDate() != null;
final SimpleDateFormat germanDateFormat = new SimpleDateFormat("dd.MM.yyyy"); final SimpleDateFormat germanDateFormat = new SimpleDateFormat("dd.MM.yyyy");
String exemptionReason = ""; String exemptionReason = "";
@@ -454,7 +454,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
if (currentItem.getProduct().getClassifications() != null && currentItem.getProduct().getClassifications().length > 0) { if (currentItem.getProduct().getClassifications() != null && currentItem.getProduct().getClassifications().length > 0) {
for (IDesignatedProductClassification classification : currentItem.getProduct().getClassifications()) { for (IDesignatedProductClassification classification : currentItem.getProduct().getClassifications()) {
xml += "<ram:DesignatedProductClassification>" xml += "<ram:DesignatedProductClassification>"
+ "<ram:ClassCode listId=\"" + XMLTools.encodeXML(classification.getClassCode().getListID()) + "\""; + "<ram:ClassCode listId=\"" + XMLTools.encodeXML(classification.getClassCode().getListID()) + "\"";
if (classification.getClassCode().getListVersionID() != null) { if (classification.getClassCode().getListVersionID() != null) {
xml += " listVersionID=\"" + XMLTools.encodeXML(classification.getClassCode().getListVersionID()) + "\""; xml += " listVersionID=\"" + XMLTools.encodeXML(classification.getClassCode().getListVersionID()) + "\"";
} }
@@ -856,7 +856,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
final String chargesTotalLine = "<ram:ChargeTotalAmount>" + currencyFormat(calc.getChargesForPercent(null)) + "</ram:ChargeTotalAmount>"; final String chargesTotalLine = "<ram:ChargeTotalAmount>" + currencyFormat(calc.getChargesForPercent(null)) + "</ram:ChargeTotalAmount>";
xml += "<ram:SpecifiedTradeSettlementHeaderMonetarySummation>"; xml += "<ram:SpecifiedTradeSettlementHeaderMonetarySummation>";
if (getProfile() != Profiles.getByName("Minimum")) { if ((getProfile() != Profiles.getByName("Minimum")) && (getProfile() != Profiles.getByName("BASICWL"))) {
xml += "<ram:LineTotalAmount>" + currencyFormat(calc.getTotal()) + "</ram:LineTotalAmount>"; xml += "<ram:LineTotalAmount>" + currencyFormat(calc.getTotal()) + "</ram:LineTotalAmount>";
xml += chargesTotalLine xml += chargesTotalLine
+ allowanceTotalLine; + allowanceTotalLine;
@@ -865,14 +865,18 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
// // // //
// currencyID=\"EUR\" // currencyID=\"EUR\"
+ "<ram:TaxTotalAmount currencyID=\"" + trans.getCurrency() + "\">" + "<ram:TaxTotalAmount currencyID=\"" + trans.getCurrency() + "\">"
+ currencyFormat(calc.getGrandTotal().subtract(calc.getTaxBasis())) + "</ram:TaxTotalAmount>" + currencyFormat(calc.getGrandTotal().subtract(calc.getTaxBasis())) + "</ram:TaxTotalAmount>";
+ "<ram:GrandTotalAmount>" + currencyFormat(calc.getGrandTotal()) + "</ram:GrandTotalAmount>"; if (trans.getRoundingAmount() != null) {
xml += "<ram:RoundingAmount>" + currencyFormat(trans.getRoundingAmount()) + "</ram:RoundingAmount>";
}
xml += "<ram:GrandTotalAmount>" + currencyFormat(calc.getGrandTotal()) + "</ram:GrandTotalAmount>";
// // // //
// currencyID=\"EUR\" // currencyID=\"EUR\"
if (getProfile() != Profiles.getByName("Minimum")) { if (getProfile() != Profiles.getByName("Minimum")) {
xml += "<ram:TotalPrepaidAmount>" + currencyFormat(calc.getTotalPrepaid()) + "</ram:TotalPrepaidAmount>"; xml += "<ram:TotalPrepaidAmount>" + currencyFormat(calc.getTotalPrepaid()) + "</ram:TotalPrepaidAmount>";
} }
xml += "<ram:DuePayableAmount>" + currencyFormat(calc.getGrandTotal().subtract(calc.getTotalPrepaid())) + "</ram:DuePayableAmount>" xml += "<ram:DuePayableAmount>" + currencyFormat(calc.getDuePayable()) + "</ram:DuePayableAmount>"
+ "</ram:SpecifiedTradeSettlementHeaderMonetarySummation>"; + "</ram:SpecifiedTradeSettlementHeaderMonetarySummation>";
if (trans.getInvoiceReferencedDocumentID() != null) { if (trans.getInvoiceReferencedDocumentID() != null) {
xml += "<ram:InvoiceReferencedDocument>" xml += "<ram:InvoiceReferencedDocument>"

View File

@@ -621,6 +621,11 @@ public class ZUGFeRDInvoiceImporter {
zpp.setOwnOrganisationName(extractString("//*[local-name()=\"SellerTradeParty\"]/*[local-name()=\"Name\"]|//*[local-name()=\"AccountingSupplierParty\"]/*[local-name()=\"Party\"]/*[local-name()=\"PartyName\"]").trim()); zpp.setOwnOrganisationName(extractString("//*[local-name()=\"SellerTradeParty\"]/*[local-name()=\"Name\"]|//*[local-name()=\"AccountingSupplierParty\"]/*[local-name()=\"Party\"]/*[local-name()=\"PartyName\"]").trim());
String rounding=extractString("//*[local-name()=\"SpecifiedTradeSettlementHeaderMonetarySummation\"]/*[local-name()=\"RoundingAmount\"]|//*[local-name()=\"LegalMonetaryTotal\"]/*[local-name()=\"Party\"]/*[local-name()=\"PayableRoundingAmount\"]");
if ((rounding!=null)&&(!rounding.isEmpty())) {
zpp.setRoundingAmount(new BigDecimal(rounding.trim()));
}
xpr = xpath.compile("//*[local-name()=\"BuyerReference\"]"); xpr = xpath.compile("//*[local-name()=\"BuyerReference\"]");
String buyerReference = null; String buyerReference = null;
prepaidNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); prepaidNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);

View File

@@ -88,6 +88,7 @@ public class ZF2PushTest extends TestCase {
.addItem(new Item(new Product("Design (hours)", "Of a sample invoice", "HUR", new BigDecimal(7)), price, new BigDecimal(1.0))) .addItem(new Item(new Product("Design (hours)", "Of a sample invoice", "HUR", new BigDecimal(7)), price, new BigDecimal(1.0)))
.addItem(new Item(new Product("Ballons", "various colors, ~2000ml", "H87", new BigDecimal(19)), new BigDecimal("0.79"), new BigDecimal(400.0))) .addItem(new Item(new Product("Ballons", "various colors, ~2000ml", "H87", new BigDecimal(19)), new BigDecimal("0.79"), new BigDecimal(400.0)))
.addItem(new Item(new Product("Hot air „heiße Luft“ (litres)", "", "LTR", new BigDecimal(19)), new BigDecimal("0.025"), new BigDecimal(800.0))) .addItem(new Item(new Product("Hot air „heiße Luft“ (litres)", "", "LTR", new BigDecimal(19)), new BigDecimal("0.025"), new BigDecimal(800.0)))
.setRoundingAmount(new BigDecimal("1"))
); );
ze.export(TARGET_PDF); ze.export(TARGET_PDF);
@@ -95,6 +96,15 @@ public class ZF2PushTest extends TestCase {
fail("Exception should not be raised"); fail("Exception should not be raised");
} }
ZUGFeRDInvoiceImporter zii=new ZUGFeRDInvoiceImporter(TARGET_PDF);
Invoice i=new Invoice();
try {
zii.extractInto(i);
} catch (XPathExpressionException e) {
throw new RuntimeException(e);
} catch (ParseException e) {
throw new RuntimeException(e);
}
// now check the contents (like MustangReaderTest) // now check the contents (like MustangReaderTest)