This commit is contained in:
jstaerk
2024-09-10 19:12:48 +02:00
parent 958d268cac
commit 3d9bfbfa36
6 changed files with 96 additions and 28 deletions

View File

@@ -13,6 +13,8 @@ import java.util.HashMap;
public class Product implements IZUGFeRDExportableProduct {
protected String unit, name, sellerAssignedID, buyerAssignedID;
protected String description="";
protected String taxExemptionReason=null;
protected String taxCategoryCode=null;
protected BigDecimal VATPercent;
protected boolean isReverseCharge = false;
protected boolean isIntraCommunitySupply = false;
@@ -66,6 +68,44 @@ public class Product implements IZUGFeRDExportableProduct {
return this;
}
/***
*
* @return e.g. intra-commnunity supply or small business
*/
@Override
public String getTaxExemptionReason() {
return taxExemptionReason;
}
/***
*
* @param taxExemptionReasonText String e.g. Kleinunternehmer gemäß §19 UStG https://github.com/ZUGFeRD/mustangproject/issues/463
* @return
*/
public Product setTaxExemptionReason(String taxExemptionReasonText) {
taxExemptionReason = taxExemptionReasonText;
return this;
}
/***
*
* @return e.g. S (normal tax), Z=zero rated, E (e.g. small business) or K (intrra community supply)
*/
@Override
public String getTaxCategoryCode() {
return taxCategoryCode;
}
/***
*
* @param code e.g. S (normal tax), Z=zero rated, E (e.g. small business) or K (intrra community supply) see also https://github.com/ZUGFeRD/mustangproject/issues/463
* @return
*/
public Product setTaxCategoryCode(String code) {
taxCategoryCode = code;
return this;
}
@Override
public String getSellerAssignedID() {

View File

@@ -172,6 +172,10 @@ public class TransactionCalculator implements IAbsoluteValueProvider {
LineCalculator lc = new LineCalculator(currentItem);
VATAmount itemVATAmount = new VATAmount(lc.getItemTotalNetAmount(), lc.getItemTotalVATAmount(),
currentItem.getProduct().getTaxCategoryCode(), vatDueDateTypeCode);
String reasonText=currentItem.getProduct().getTaxExemptionReason();
if (reasonText!=null) {
itemVATAmount.setVatExemptionReasonText(reasonText);
}
VATAmount current = hm.get(percent.stripTrailingZeros());
if (current == null) {
hm.put(percent.stripTrailingZeros(), itemVATAmount);

View File

@@ -32,6 +32,16 @@ import java.math.RoundingMode;
*/
public class VATAmount {
protected BigDecimal basis, calculated, applicablePercent;
protected String categoryCode;
protected String vatExemptionReasonText;
protected String dueDateTypeCode;
public VATAmount(BigDecimal basis, BigDecimal calculated, String categoryCode) {
super();
this.basis = basis;
@@ -48,35 +58,41 @@ public class VATAmount {
this.dueDateTypeCode = dueDateTypeCode;
}
BigDecimal basis, calculated, applicablePercent;
String categoryCode;
String dueDateTypeCode;
public BigDecimal getApplicablePercent() {
return applicablePercent;
}
public void setApplicablePercent(BigDecimal applicablePercent) {
public VATAmount setApplicablePercent(BigDecimal applicablePercent) {
this.applicablePercent = applicablePercent;
return this;
}
public BigDecimal getBasis() {
return basis;
}
public void setBasis(BigDecimal basis) {
public VATAmount setBasis(BigDecimal basis) {
this.basis = basis.setScale(2, RoundingMode.HALF_UP);
return this;
}
public BigDecimal getCalculated() {
return calculated;
}
public void setCalculated(BigDecimal calculated) {
public VATAmount setCalculated(BigDecimal calculated) {
this.calculated = calculated;
return this;
}
public String getVatExemptionReasonText() {
return vatExemptionReasonText;
}
public VATAmount setVatExemptionReasonText(String theText) {
this.vatExemptionReasonText = theText;
return this;
}
/**
@@ -94,24 +110,27 @@ public class VATAmount {
* @deprecated Use {@link #setCategoryCode(String)} instead
*/
@Deprecated
public void setDocumentCode(String documentCode) {
public VATAmount setDocumentCode(String documentCode) {
this.categoryCode = documentCode;
return this;
}
public String getCategoryCode() {
return categoryCode;
}
public void setCategoryCode(String categoryCode) {
public VATAmount setCategoryCode(String categoryCode) {
this.categoryCode = categoryCode;
return this;
}
public String getDueDateTypeCode() {
return dueDateTypeCode;
}
public void setDueDateTypeCode(String dueDateTypeCode) {
public VATAmount setDueDateTypeCode(String dueDateTypeCode) {
this.dueDateTypeCode = dueDateTypeCode;
return this;
}
public VATAmount add(VATAmount v) {

View File

@@ -51,7 +51,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ZUGFeRD2PullProvider implements IXMLProvider {
private static final Logger LOGGER = LoggerFactory.getLogger (ZUGFeRD2PullProvider.class);
private static final Logger LOGGER = LoggerFactory.getLogger(ZUGFeRD2PullProvider.class);
protected byte[] zugferdData;
protected IExportableTransaction trans;
@@ -93,7 +93,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
try {
document = DocumentHelper.parseText(new String(zugferdData));
} catch (final DocumentException e1) {
LOGGER.error ("Failed to parse ZUGFeRD data", e1);
LOGGER.error("Failed to parse ZUGFeRD data", e1);
}
try {
final OutputFormat format = OutputFormat.createPrettyPrint();
@@ -103,7 +103,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
res = sw.toString().getBytes(StandardCharsets.UTF_8);
} catch (final IOException e) {
LOGGER.error ("Failed to write ZUGFeRD data", e);
LOGGER.error("Failed to write ZUGFeRD data", e);
}
return res;
@@ -325,7 +325,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
paymentTermsDescription += discount.getAsXRechnung();
}
} else if ((paymentTermsDescription == null) && (trans.getDocumentCode() != DocumentCodeTypeConstants.CORRECTEDINVOICE) && (trans.getDocumentCode() != DocumentCodeTypeConstants.CREDITNOTE)) {
if ( trans.getDueDate() != null ) {
if (trans.getDueDate() != null) {
paymentTermsDescription = "Please remit until " + germanDateFormat.format(trans.getDueDate());
}
}
@@ -424,7 +424,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
"</ram:Description>";
}
if (currentItem.getProduct().getAttributes() != null) {
for ( Entry<String, String> entry : currentItem.getProduct().getAttributes().entrySet() ) {
for (Entry<String, String> entry : currentItem.getProduct().getAttributes().entrySet()) {
xml += "<ram:ApplicableProductCharacteristic>" +
"<ram:Description>" + XMLTools.encodeXML(entry.getKey()) + "</ram:Description>" +
"<ram:Value>" + XMLTools.encodeXML(entry.getValue()) + "</ram:Value>" +
@@ -433,8 +433,8 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
}
if (currentItem.getProduct().getCountryOfOrigin() != null) {
xml += "<ram:OriginTradeCountry><ram:ID>" +
XMLTools.encodeXML(currentItem.getProduct().getCountryOfOrigin()) +
"</ram:ID></ram:OriginTradeCountry>";
XMLTools.encodeXML(currentItem.getProduct().getCountryOfOrigin()) +
"</ram:ID></ram:OriginTradeCountry>";
}
xml += "</ram:SpecifiedTradeProduct>"
@@ -654,12 +654,17 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
final String amountDueDateTypeCode = amount.getDueDateTypeCode();
final boolean displayExemptionReason = CATEGORY_CODES_WITH_EXEMPTION_REASON.contains(amountCategoryCode);
if (getProfile() != Profiles.getByName("Minimum")) {
String exemptionReasonTextXML = "";
if ((displayExemptionReason) && (amount.getVatExemptionReasonText() != null)) {
exemptionReasonTextXML = "<ram:ExemptionReason>" + XMLTools.encodeXML(amount.getVatExemptionReasonText()) + "</ram:ExemptionReason>";
}
xml += "<ram:ApplicableTradeTax>"
+ "<ram:CalculatedAmount>" + currencyFormat(amount.getCalculated())
+ "</ram:CalculatedAmount>" //currencyID=\"EUR\"
+ "<ram:TypeCode>VAT</ram:TypeCode>"
+ (displayExemptionReason ? exemptionReason : "")
+ exemptionReasonTextXML
+ "<ram:BasisAmount>" + currencyFormat(amount.getBasis()) + "</ram:BasisAmount>" // currencyID=\"EUR\"
+ "<ram:CategoryCode>" + amountCategoryCode + "</ram:CategoryCode>"
+ (amountDueDateTypeCode != null ? "<ram:DueDateTypeCode>" + amountDueDateTypeCode + "</ram:DueDateTypeCode>" : "")
@@ -681,7 +686,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
if ((trans.getZFCharges() != null) && (trans.getZFCharges().length > 0)) {
if (profile == Profiles.getByName("XRechnung")) {
for(IZUGFeRDAllowanceCharge charge : trans.getZFCharges()) {
for (IZUGFeRDAllowanceCharge charge : trans.getZFCharges()) {
xml += "<ram:SpecifiedTradeAllowanceCharge>" +
"<ram:ChargeIndicator>" +
"<udt:Indicator>true</udt:Indicator>" +
@@ -724,7 +729,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
if ((trans.getZFAllowances() != null) && (trans.getZFAllowances().length > 0)) {
if (profile == Profiles.getByName("XRechnung")) {
for(IZUGFeRDAllowanceCharge allowance : trans.getZFAllowances()) {
for (IZUGFeRDAllowanceCharge allowance : trans.getZFAllowances()) {
xml += "<ram:SpecifiedTradeAllowanceCharge>" +
"<ram:ChargeIndicator>" +
"<udt:Indicator>false</udt:Indicator>" +

View File

@@ -30,5 +30,5 @@ public class TaxCategoryCodeTypeConstants {
public static final String UNTAXEDSERVICE = "O";
public static final String INTRACOMMUNITY = "K";
public static Set<String> CATEGORY_CODES_WITH_EXEMPTION_REASON = Stream.of(INTRACOMMUNITY, REVERSECHARGE).collect(Collectors.toSet());
public static Set<String> CATEGORY_CODES_WITH_EXEMPTION_REASON = Stream.of(INTRACOMMUNITY, REVERSECHARGE, TAXEXEMPT).collect(Collectors.toSet());
}

View File

@@ -125,7 +125,7 @@ public class ZF2PushTest extends TestCase {
String number = "123";
String priceStr = "1.00";
String senderDescription = "Kein Kleinunternehmer";
String senderDescription = "Kleinunternehmer";
String taxID = "9990815";
BigDecimal price = new BigDecimal(priceStr);
try {
@@ -144,7 +144,7 @@ public class ZF2PushTest extends TestCase {
.setRecipient(new TradeParty("Franz Müller", "teststr.12", "55232", "Entenhausen", "DE").addVATID("DE4711")
.setContact(new Contact("Franz Müller", "01779999999", "franz@mueller.de", "teststr. 12", "55232", "Entenhausen", "DE")))
.setNumber(number)
.addItem(new Item(new Product("Testprodukt", "", "C62", new BigDecimal(19)), price, new BigDecimal(1.0)))
.addItem(new Item(new Product("Testprodukt", "", "C62", new BigDecimal(0)).setTaxExemptionReason("Kleinunternehmer gemäß §19 UStG").setTaxCategoryCode("E"), price, new BigDecimal(1.0)))
);
String theXML = new String(ze.getProvider().getXML());
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
@@ -152,7 +152,7 @@ public class ZF2PushTest extends TestCase {
} catch (IOException e) {
fail("IOException should not be raised");
}
ZUGFeRDInvoiceImporter zii = new ZUGFeRDInvoiceImporter(TARGET_PDF);
ZUGFeRDInvoiceImporter zii = new ZUGFeRDInvoiceImporter(TARGET_ATTACHMENTSPDF);
Invoice i= null;
try {
i = zii.extractInvoice();
@@ -170,7 +170,7 @@ public class ZF2PushTest extends TestCase {
assertTrue(zi.getUTF8().contains(taxID));
// Reading ZUGFeRD
assertEquals("1.19", zi.getAmount());
assertEquals("1.00", zi.getAmount());
assertEquals(orgname, zi.getHolder());
assertEquals(number, zi.getForeignReference());
try {