diff --git a/library/src/main/java/org/mustangproject/Invoice.java b/library/src/main/java/org/mustangproject/Invoice.java index fc71df6e..8461e977 100644 --- a/library/src/main/java/org/mustangproject/Invoice.java +++ b/library/src/main/java/org/mustangproject/Invoice.java @@ -41,7 +41,7 @@ public class Invoice implements IExportableTransaction { protected String documentName = null, documentCode = null, number = null, ownOrganisationFullPlaintextInfo = null, referenceNumber = null, shipToOrganisationID = null, shipToOrganisationName = null, shipToStreet = null, shipToZIP = null, shipToLocation = null, shipToCountry = null, buyerOrderReferencedDocumentID = null, invoiceReferencedDocumentID = null, buyerOrderReferencedDocumentIssueDateTime = null, ownForeignOrganisationID = null, ownOrganisationName = null, currency = null, paymentTermDescription = null; protected Date issueDate = null, dueDate = null, deliveryDate = null; - protected TradeParty sender = null, recipient = null, deliveryAddress = null; + protected TradeParty sender = null, recipient = null, deliveryAddress = null, payee = null; protected ArrayList cashDiscounts = null; @JsonDeserialize(contentAs = Item.class) protected ArrayList ZFItems = null; @@ -577,6 +577,22 @@ public class Invoice implements IExportableTransaction { this.deliveryAddress = deliveryAddress; return this; } + + @Override + public TradeParty getPayee() { + return this.payee; + } + + /*** + * if the payee is not the seller, it can be specified here + * @param payee the payment receiving organisation + * @return fluent setter + */ + public Invoice setPayee(TradeParty payee) { + this.payee = payee; + return this; + } + /*** * Adds a cash discount (skonto) * @param c the CashDiscount percent/period combination diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java b/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java index 87a29aaa..f43b5f62 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/IExportableTransaction.java @@ -450,11 +450,19 @@ public interface IExportableTransaction { * * @return the IZUGFeRDExportableTradeParty delivery address */ - default IZUGFeRDExportableTradeParty getDeliveryAddress() { return null; } + /*** + * payee / payment receiver, if different from seller, ram:Payee (only supported for zf2) + * + * @return the IZUGFeRDExportableTradeParty payment receiver, if different from sellver + */ + default IZUGFeRDExportableTradeParty getPayee() { + return null; + } + /*** * specifies the document level delivery period, will be included in a * BillingSpecifiedPeriod element diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/TransactionCalculator.java b/library/src/main/java/org/mustangproject/ZUGFeRD/TransactionCalculator.java index 4d9382ea..1e1f757f 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/TransactionCalculator.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/TransactionCalculator.java @@ -39,8 +39,8 @@ public class TransactionCalculator implements IAbsoluteValueProvider { } /*** - * the invoice total with VAT, corrected by prepaid amount, allowances and - * charges + * the invoice total with VAT, allowances and + * charges, WITHOUT considering prepaid amount * * @return the invoice total including taxes */ diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index 1b5b0242..56dd9f46 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -155,11 +155,11 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { if ((party.getContact() != null) && (isSender || profile == Profiles.getByName("EN16931") || profile == Profiles.getByName("Extended") || profile == Profiles.getByName("XRechnung"))) { xml += ""; if (party.getContact().getName() != null) { - xml += "" + XMLTools.encodeXML(party.getContact().getName()) + xml += "" + + XMLTools.encodeXML(party.getContact().getName()) + ""; } if (party.getContact().getPhone() != null) { - xml += "" + XMLTools.encodeXML(party.getContact().getPhone()) + "" + ""; @@ -200,7 +200,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { + ""; } -//country IS mandatory + //country IS mandatory xml += "" + XMLTools.encodeXML(party.getCountry()) + "" + ""; @@ -228,6 +228,30 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { } + protected String getTradePartyPayeeAsXML(IZUGFeRDExportableTradeParty party) { + String xml = ""; + // According EN16931 either GlobalID or seller assigned ID might be present for a Payee + if (party.getID() != null) { + xml += "" + XMLTools.encodeXML(party.getID()) + ""; + } + if ((party.getGlobalIDScheme() != null) && (party.getGlobalID() != null)) { + xml += "" + + XMLTools.encodeXML(party.getGlobalID()) + + ""; + } + xml += "" + XMLTools.encodeXML(party.getName()) + ""; + + if (party.getLegalOrganisation() != null) { + xml += " "; + if (party.getLegalOrganisation().getSchemedID() != null) { + xml += "" + XMLTools.encodeXML(party.getLegalOrganisation().getSchemedID().getID()) + ""; + } + xml += ""; + } + + return xml; + } + /*** * returns the XML for a charge or allowance on item level @@ -247,8 +271,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { } String reason = ""; - if ((allowance.getReason() != null) && (profile == Profiles.getByName("Extended"))) { - // only in extended profile + if ((allowance.getReason() != null) && (profile == Profiles.getByName("Extended") || profile == Profiles.getByName("XRechnung"))) { reason = "" + XMLTools.encodeXML(allowance.getReason()) + ""; } String reasonCode = ""; @@ -283,8 +306,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { } String reason = ""; - if ((allowance.getReason() != null) && (profile == Profiles.getByName("Extended"))) { - // only in extended profile + if ((allowance.getReason() != null) && (profile == Profiles.getByName("Extended") || profile == Profiles.getByName("XRechnung"))) { reason = "" + XMLTools.encodeXML(allowance.getReason()) + ""; } String reasonCode = ""; @@ -588,6 +610,12 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { ""; } + if (this.trans.getPayee() != null) { + xml += "" + + getTradePartyPayeeAsXML(this.trans.getPayee()) + + ""; + } + if (trans.getDeliveryDate() != null) { xml += "" diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java index 117254fb..e330853b 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Date; import java.util.List; +import java.util.Objects; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; @@ -74,6 +75,10 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter { xpr = xpath.compile("//*[local-name()=\"BuyerTradeParty\"]|//*[local-name()=\"AccountingCustomerParty\"]/*"); NodeList BuyerNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); + + xpr = xpath.compile("//*[local-name()=\"PayeeTradeParty\"]|//*[local-name()=\"PayeeParty\"]/*"); + NodeList payeeNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); + xpr = xpath.compile("//*[local-name()=\"ExchangedDocument\"]|//*[local-name()=\"HeaderExchangedDocument\"]"); NodeList ExchangedDocumentNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); @@ -84,6 +89,12 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter { expectedGrandTotal = new BigDecimal(totalNodes.item(0).getTextContent()); } + xpr = xpath.compile("//*[local-name()=\"PrepaidAmount\"]"); + NodeList prepaidNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); + if (prepaidNodes.getLength() > 0) { + zpp.setTotalPrepaidAmount(new BigDecimal(prepaidNodes.item(0).getTextContent())); + } + Date issueDate = null; Date dueDate = null; Date deliveryDate = null; @@ -282,8 +293,13 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter { } zpp.setDueDate(dueDate).setDeliveryDate(deliveryDate).setIssueDate(issueDate).setSender(new TradeParty(SellerNodes)).setRecipient(new TradeParty(BuyerNodes)).setNumber(number).setDocumentCode(typeCode); + bankDetails.forEach(bankDetail -> zpp.getSender().addBankDetails(bankDetail)); + if (payeeNodes.getLength() > 0) { + zpp.setPayee(new TradeParty(payeeNodes)); + } + if (buyerOrderIssuerAssignedID != null) { zpp.setBuyerOrderReferencedDocumentID(buyerOrderIssuerAssignedID); } @@ -298,9 +314,9 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter { xpr = xpath.compile("//*[local-name()=\"BuyerReference\"]"); String buyerReference = null; - totalNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); - if (totalNodes.getLength() > 0) { - buyerReference = totalNodes.item(0).getTextContent(); + prepaidNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); + if (prepaidNodes.getLength() > 0) { + buyerReference = prepaidNodes.item(0).getTextContent(); } if (buyerReference != null) { zpp.setReferenceNumber(buyerReference); @@ -400,7 +416,8 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter { } TransactionCalculator tc = new TransactionCalculator(zpp); - String expectedStringTotalGross = tc.getGrandTotal().toPlainString(); + String expectedStringTotalGross = tc.getGrandTotal() + .subtract(Objects.requireNonNullElse(zpp.getTotalPrepaidAmount(), BigDecimal.ZERO)).toPlainString(); EStandard whichType; try { whichType = getStandard(); diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/UBLTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/UBLTest.java index 7dba9fdf..31d1ec12 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/UBLTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/UBLTest.java @@ -123,6 +123,23 @@ public class UBLTest extends ResourceCase { } + public void testEdgeInvoiceImportUBL2() { + File UBLinputFile = getResourceAsFile("ubl/04.01a-INVOICE_ubl.xml"); + boolean hasExceptions = false; + + ZUGFeRDInvoiceImporter zii = null; + Invoice invoice = null; + try { + zii = new ZUGFeRDInvoiceImporter(new FileInputStream(UBLinputFile)); + invoice = zii.extractInvoice(); + } catch (XPathExpressionException | ParseException | FileNotFoundException e) { + e.printStackTrace(); + hasExceptions = true; + } + assertFalse(hasExceptions); + + assertEquals(new BigDecimal("10000.0"), invoice.getTotalPrepaidAmount()); + } /* public void testInvoiceImportOtherUBL() { diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java index 56dfeb5e..ae2e9edf 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java @@ -95,6 +95,7 @@ public class XRTest extends TestCase { .setReferenceNumber("991-01484-64")//leitweg-id // not using any VAT, this is also a test of zero-rated goods: .setNumber(number).addItem(new Item(new Product("Testprodukt", "", "C62", BigDecimal.ZERO), amount, new BigDecimal(1.0))) + .setPayee( new TradeParty().setName("VR Factoring GmbH").setID("DE813838785").setLegalOrganisation(new LegalOrganisation("0199", "391200LDDFJDMIPPMZ54"))) .embedFileInXML(fe1); @@ -109,6 +110,9 @@ public class XRTest extends TestCase { .asInt() .isEqualTo(1); //2 errors are OK because there is a known bug + assertThat(theXML).valueByXPath("count(//*[local-name()='PayeeTradeParty'])") + .asInt() + .isEqualTo(1); assertThat(theXML).valueByXPath("//*[local-name()='DuePayableAmount']") .asDouble() diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java index cde54bc0..a5f00648 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java @@ -139,6 +139,9 @@ public class ZF2ZInvoiceImporterTest extends ResourceCase { assertEquals("DE", invoice.getSender().getCountry()); assertEquals("Stadthausen", invoice.getSender().getLocation()); + assertTrue(invoice.getPayee() != null); + assertEquals("VR Factoring GmbH", invoice.getPayee().getName()); + TransactionCalculator tc = new TransactionCalculator(invoice); assertEquals(new BigDecimal("571.04"), tc.getGrandTotal()); @@ -285,6 +288,8 @@ public class ZF2ZInvoiceImporterTest extends ResourceCase { TransactionCalculator tc = new TransactionCalculator(invoice); assertEquals(new BigDecimal("1.00"), tc.getGrandTotal()); + assertTrue(invoice.getPayee() != null); + assertEquals("VR Factoring GmbH", invoice.getPayee().getName()); } /** diff --git a/library/src/test/resources/testout-ZF2new.ubl.xml b/library/src/test/resources/testout-ZF2new.ubl.xml index 35eabf13..9be2f765 100644 --- a/library/src/test/resources/testout-ZF2new.ubl.xml +++ b/library/src/test/resources/testout-ZF2new.ubl.xml @@ -66,6 +66,11 @@ + + + VR Factoring GmbH + + 2017-05-07 diff --git a/library/src/test/resources/ubl/04.01a-INVOICE_ubl.xml b/library/src/test/resources/ubl/04.01a-INVOICE_ubl.xml new file mode 100644 index 00000000..e0014a4d --- /dev/null +++ b/library/src/test/resources/ubl/04.01a-INVOICE_ubl.xml @@ -0,0 +1,388 @@ + + urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.2#conformant#urn:xoev-de:kosit:extension:xrechnung_2.2 + 12345 + 2019-05-15 + 380 + EUR + 12345678-12345-83 + + 2019-02-01 + 2019-05-07 + + + 123 + + + + + M. Meier Handwerk GbR + + + Hauptstraße 2 + Musterstadt + 12345 + + DE + + + + DE/12/345/67890 + + VAT + + + + M. Meier Handwerk GbR + 1122334455 + + + M. Meier Handwerk GbR + 010/12345 + info@m-meier.de + + + + + + + 345LA5324 + + + Dorfplatz 3 + Musterort + 54321 + + DE + + + + Beispielkunde + + + + + 31 + + DE091111222233334444 + + + + Zahlbar innerhalb 14 Tagen nach Erhalt der Rechnung. + + + 2382 + + 12536.84 + 2382 + + S + 19 + + VAT + + + + + + 12536.84 + 12536.84 + 14918.84 + 10000.0 + 4918.84 + + + 1 + 1 + 818.04 + + Sanitär und Zubehör + + S + 19 + + VAT + + + + + 818.04 + 1 + + + 1 1 + 1 + 335.79 + + Demontage Sanitär + + S + 19 + + VAT + + + + + 335.79 + 1 + + + 1 1 1 + 149 + 193.70 + + Demontage TW-Rohrleitung DN 12-25 + + S + 19 + + VAT + + + + + 1.30 + 1 + + + + 1 1 2 + 32.50 + 48.75 + + Demontage SML DN 100 + + S + 19 + + VAT + + + + + 1.50 + 1 + + + + 1 1 3 + 71.80 + 93.34 + + Demontage AW-Rohrleitungen Kunststoff PP bis DN100 + + S + 19 + + VAT + + + + + 1.30 + 1 + + + + + 1 2 + 1 + 482.25 + + Abwasserleitungen und Zubehör + + S + 19 + + VAT + + + + + 482.25 + 1 + + + 1 2 1 + 9.5 + 99.75 + + Abwasserleitung HT-PP DN 100 + + S + 19 + + VAT + + + + + 10.50 + 1 + + + + 1 2 2 + 45.0 + 382.50 + + Abwasserleitung HT-PP DN 50 + + S + 19 + + VAT + + + + + 8.50 + 1 + + + + + + 2 + 1 + 11718.8 + + Heizung und Zubehör + + S + 19 + + VAT + + + + + 11718.8 + 1 + + + 2 1 + 1 + 1324 + + Demontage Heizung + + S + 19 + + VAT + + + + + 1324 + 1 + + + 2 1 1 + 20 + 700 + + Entleeren der Heizungsanlage, teilnetz + + S + 19 + + VAT + + + + + 35 + 1 + + + + 2 1 2 + 52 + 624 + + Demontage Flachheizkörpers + + S + 19 + + VAT + + + + + 12 + 1 + + + + + 2 2 + 1 + 10394.8 + + Heizflächen und Zubehör + + S + 19 + + VAT + + + + + 10394.8 + 1 + + + 2 2 1 + 52 + 10202.4 + + Flachheizkörper Stahl profiliert Seitent. H 300mm L -500mm Typ22 + + S + 19 + + VAT + + + + + 196.20 + 1 + + + + 2 2 2 + 104 + 192.4 + + Einzelrosette aus Kunststoff, weiß + + S + 19 + + VAT + + + + + 1.85 + 1 + + + + + \ No newline at end of file