diff --git a/History.md b/History.md index 0ebebdfa..099a75c8 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,16 @@ +2.15.1 +======= +- #566 +- have a bean contructor for direct debit +-? lineTotalAmount is null +? be able to access ID in error message +? log error IDs +- closes #579 +- #581 +- #576 +- #578 +- log error ids + 2.15.0 ======= 2024-11-18 diff --git a/library/src/main/java/org/mustangproject/CalculatedInvoice.java b/library/src/main/java/org/mustangproject/CalculatedInvoice.java index 729d0f9b..c6a24fb1 100644 --- a/library/src/main/java/org/mustangproject/CalculatedInvoice.java +++ b/library/src/main/java/org/mustangproject/CalculatedInvoice.java @@ -11,11 +11,13 @@ import java.math.BigDecimal; public class CalculatedInvoice extends Invoice implements Serializable { - protected BigDecimal grandTotal=null; + protected BigDecimal grandTotal=null; + protected BigDecimal lineTotalAmount=null; public void calculate() { TransactionCalculator tc=new TransactionCalculator(this); grandTotal=tc.getGrandTotal(); + lineTotalAmount=tc.getValue(); } public BigDecimal getGrandTotal() { if (grandTotal==null) { @@ -27,4 +29,15 @@ public class CalculatedInvoice extends Invoice implements Serializable { grandTotal=grand; return this; } + public BigDecimal getLineTotalAmount() { + if (lineTotalAmount==null) { + calculate(); + } + return lineTotalAmount; + } + public CalculatedInvoice setLineTotalAmount(BigDecimal total) { + lineTotalAmount=total; + return this; + } + } diff --git a/library/src/main/java/org/mustangproject/DirectDebit.java b/library/src/main/java/org/mustangproject/DirectDebit.java index e4ac9a85..d90ba661 100644 --- a/library/src/main/java/org/mustangproject/DirectDebit.java +++ b/library/src/main/java/org/mustangproject/DirectDebit.java @@ -9,12 +9,12 @@ public class DirectDebit implements IZUGFeRDTradeSettlementDebit { /** * Debited account identifier (BT-91) */ - protected final String IBAN; + protected String IBAN; /** * Mandate reference identifier (BT-89) */ - protected final String mandate; + protected String mandate; /** * bean constructor diff --git a/library/src/main/java/org/mustangproject/Item.java b/library/src/main/java/org/mustangproject/Item.java index 4e644bd6..a5957e27 100644 --- a/library/src/main/java/org/mustangproject/Item.java +++ b/library/src/main/java/org/mustangproject/Item.java @@ -72,6 +72,9 @@ public class Item implements IZUGFeRDExportableItem { icnm.getAsNodeMap("ClassifiedTaxCategory").flatMap(m -> m.getAsBigDecimal("Percent")) .ifPresent(product::setVATPercent); }); + itemMap.getAsNodeMap("AssociatedDocumentLineDocument").ifPresent(icnm -> { + icnm.getAsString("LineID").ifPresent(this::setId); + }); itemMap.getAsNodeMap("Price").ifPresent(icnm -> { // ubl diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableItem.java b/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableItem.java index c2f57f9e..45c210ea 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableItem.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableItem.java @@ -155,6 +155,20 @@ public interface IZUGFeRDExportableItem extends IAbsoluteValueProvider{ return null; } + /*** + * + * @return the line ID + */ + default String getId() { + return null; + } + + /** + * A grouping of business terms to indicate accounting-relevant free texts including a qualification of these. + * + * The information are written to the same xml nodes like {@link #getNotes()} but with explicit subjectCode. + * @return list of the notes + */ default List getNotesWithSubjectCode() { return null; } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index 8729881d..ff5aa321 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -401,6 +401,10 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { int lineID = 0; for (final IZUGFeRDExportableItem currentItem : trans.getZFItems()) { lineID++; + String lineIDStr = Integer.toString(lineID); + if (currentItem.getId()!=null) { + lineIDStr=currentItem.getId(); + } if (currentItem.getProduct().getTaxExemptionReason() != null) { exemptionReason = "" + XMLTools.encodeXML(currentItem.getProduct().getTaxExemptionReason()) + ""; } @@ -408,7 +412,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { if ((getProfile() != Profiles.getByName("Minimum")) && (getProfile() != Profiles.getByName("BasicWL"))) { xml += "" + "" - + "" + lineID + "" + + "" + lineIDStr + "" + buildItemNotes(currentItem) + "" diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java index 46594a26..a137a67f 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java @@ -324,12 +324,21 @@ public class ZUGFeRDInvoiceImporter { } } - xpr = xpath.compile("//*[local-name()=\"PrepaidAmount\"]"); + xpr = xpath.compile("//*[local-name()=\"TotalPrepaidAmount\"]|//*[local-name()=\"PrepaidAmount\"]"); NodeList prepaidNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); if (prepaidNodes.getLength() > 0) { zpp.setTotalPrepaidAmount(new BigDecimal(XMLTools.trimOrNull(prepaidNodes.item(0)))); } + + xpr = xpath.compile("//*[local-name()=\"SpecifiedTradeSettlementHeaderMonetarySummation\"]/*[local-name()=\"LineTotalAmount\"]|//*[local-name()=\"LegalMonetaryTotal\"]/*[local-name()=\"LineExtensionAmount\"]"); + NodeList lineTotalNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); + if (lineTotalNodes.getLength() > 0) { + if (zpp instanceof CalculatedInvoice) { + ((CalculatedInvoice) zpp).setLineTotalAmount(new BigDecimal(XMLTools.trimOrNull(lineTotalNodes.item(0)))); + } + } + Date issueDate = null; Date dueDate = null; Date deliveryDate = null; @@ -628,9 +637,9 @@ public class ZUGFeRDInvoiceImporter { xpr = xpath.compile("//*[local-name()=\"BuyerReference\"]"); String buyerReference = null; - prepaidNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); - if (prepaidNodes.getLength() > 0) { - buyerReference = XMLTools.trimOrNull(prepaidNodes.item(0)); + lineTotalNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); + if (lineTotalNodes.getLength() > 0) { + buyerReference = XMLTools.trimOrNull(lineTotalNodes.item(0)); } if (buyerReference != null) { zpp.setReferenceNumber(buyerReference); diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/DeSerializationTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/DeSerializationTest.java index 7c6d409d..0dbd2dc0 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/DeSerializationTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/DeSerializationTest.java @@ -28,11 +28,17 @@ import org.junit.FixMethodOrder; import org.junit.runners.MethodSorters; import org.mustangproject.*; +import javax.xml.xpath.XPathExpressionException; +import java.io.File; +import java.io.IOException; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.text.ParseException; import java.util.Date; @FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class DeSerializationTest extends TestCase { +public class DeSerializationTest extends ResourceCase { public void testJackson() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); @@ -50,6 +56,33 @@ public class DeSerializationTest extends TestCase { } + public void testInvoiceLine() throws JsonProcessingException { + File inputCII = getResourceAsFile("factur-x.xml"); + boolean hasExceptions = false; + + ZUGFeRDInvoiceImporter zii = new ZUGFeRDInvoiceImporter(); + try { + zii.fromXML(new String(Files.readAllBytes(inputCII.toPath()), StandardCharsets.UTF_8)); + + } catch (IOException e) { + hasExceptions = true; + } + + CalculatedInvoice ci=new CalculatedInvoice(); + try { + zii.extractInto(ci); + } catch (XPathExpressionException e) { + hasExceptions = true; + } catch (ParseException e) { + hasExceptions = true; + } + ObjectMapper mapper = new ObjectMapper(); + String jsonArray = mapper.writeValueAsString(ci); + assertFalse(hasExceptions); + assertTrue(jsonArray.contains("lineTotalAmount")); + + } + public void testAllowanceRead() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java index 9ff1a945..24c73e0f 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java @@ -61,9 +61,9 @@ public class ZF2PushTest extends TestCase { final String TARGET_REVERSECHARGEPDF = "./target/testout-ZF2PushReverseCharge.pdf"; public void testPushExport() { - /*** - * This writes to a filename like an official sample, please consider when changing (probably better not?) - */ + /*** + * This writes to a filename like an official sample, please consider when changing (probably better not?) + */ // the writing part String orgname = "Bei Spiel GmbH"; String number = "RE-20201121/508"; @@ -96,8 +96,8 @@ public class ZF2PushTest extends TestCase { fail("Exception should not be raised"); } -ZUGFeRDInvoiceImporter zii=new ZUGFeRDInvoiceImporter(TARGET_PDF); - Invoice i=new Invoice(); + ZUGFeRDInvoiceImporter zii = new ZUGFeRDInvoiceImporter(TARGET_PDF); + Invoice i = new Invoice(); try { zii.extractInto(i); } catch (XPathExpressionException e) { @@ -164,7 +164,7 @@ ZUGFeRDInvoiceImporter zii=new ZUGFeRDInvoiceImporter(TARGET_PDF); fail("IOException should not be raised"); } ZUGFeRDInvoiceImporter zii = new ZUGFeRDInvoiceImporter(TARGET_ATTACHMENTSPDF); - Invoice i= null; + Invoice i = null; try { i = zii.extractInvoice(); } catch (XPathExpressionException e) { @@ -172,7 +172,7 @@ ZUGFeRDInvoiceImporter zii=new ZUGFeRDInvoiceImporter(TARGET_PDF); } catch (ParseException e) { throw new RuntimeException(e); } - assertEquals(senderDescription,i.getSender().getDescription()); + assertEquals(senderDescription, i.getSender().getDescription()); // now check the contents (like MustangReaderTest) ZUGFeRDImporter zi = new ZUGFeRDImporter(TARGET_ATTACHMENTSPDF); @@ -302,7 +302,7 @@ ZUGFeRDInvoiceImporter zii=new ZUGFeRDInvoiceImporter(TARGET_PDF); // .addItem(new Item(new Product("Testprodukt", "", "C62", new BigDecimal(19)), amount, new BigDecimal(1.0)).addAllowance(new Allowance().setPercent(new BigDecimal(50))))); ze.setTransaction(new Invoice().setDueDate(new Date()).setIssueDate(new Date()).setDeliveryDate(new Date()) - .setSender(new TradeParty(orgname, "teststr", "55232", "teststadt", "DE").addVATID("DE0815").addTaxID ("4711")) + .setSender(new TradeParty(orgname, "teststr", "55232", "teststadt", "DE").addVATID("DE0815").addTaxID("4711")) .setRecipient(new TradeParty("Franz Müller", "teststr.12", "55232", "Entenhausen", "DE").addVATID("DE0816") .setContact(new Contact("contact testname", "123456", "contact.testemail@example.org").setFax("0911623562"))) .setDeliveryAddress(new TradeParty("Franz Müller", "teststr.12", "55232", "Entenhausen", "DE").addVATID("DE0816")) @@ -372,7 +372,7 @@ ZUGFeRDInvoiceImporter zii=new ZUGFeRDInvoiceImporter(TARGET_PDF); fail("IOException should not be raised"); } ZUGFeRDInvoiceImporter zii = new ZUGFeRDInvoiceImporter(TARGET_INTRACOMMUNITYSUPPLYMANUALPDF); - Invoice i= null; + Invoice i = null; try { i = zii.extractInvoice(); } catch (XPathExpressionException e) { @@ -530,7 +530,7 @@ ZUGFeRDInvoiceImporter zii=new ZUGFeRDInvoiceImporter(TARGET_PDF); .setContractReferencedDocument(contractID) .setRecipient(new TradeParty("Franz Müller", "teststr.12", "55232", "Entenhausen", "DE").addGlobalID(gln).setEmail("recipient@test.org").addVATID("DE4711") .setContact(new Contact("Franz Müller", "01779999999", "franz@mueller.de", "teststr. 12", "55232", "Entenhausen", "DE").setFax("++49555123456")).setAdditionalAddress("Hinterhaus 3")) - .addItem(new Item(new Product("Testprodukt", "", "C62", new BigDecimal(16)).addGlobalID(gtin).setSellerAssignedID("4711"), price, new BigDecimal(1.0)).addReferencedLineID("xxx").addNote("item level 1/1").addAllowance(new Allowance(new BigDecimal(0.02)).setReason("item discount").setTaxPercent(new BigDecimal(16))).setDetailedDeliveryPeriod(sdf.parse("2020-01-13"), sdf.parse("2020-01-15"))) + .addItem(new Item(new Product("Testprodukt", "", "H87", new BigDecimal(16)).addGlobalID(gtin).setSellerAssignedID("4711"), price, new BigDecimal(1.0)).setId("a123").addReferencedLineID("xxx").addNote("item level 1/1").addAllowance(new Allowance(new BigDecimal(0.02)).setReason("item discount").setTaxPercent(new BigDecimal(16))).setDetailedDeliveryPeriod(sdf.parse("2020-01-13"), sdf.parse("2020-01-15"))) .addCharge(new Charge(new BigDecimal(0.5)).setReason("quick delivery charge").setTaxPercent(new BigDecimal(16))) .addAllowance(new Allowance(new BigDecimal(0.2)).setReason("discount").setTaxPercent(new BigDecimal(16))) .addCashDiscount(new CashDiscount(new BigDecimal(2), 14)) @@ -562,7 +562,7 @@ ZUGFeRDInvoiceImporter zii=new ZUGFeRDInvoiceImporter(TARGET_PDF); assertTrue(zi.getUTF8().contains(occurrenceFrom)); assertTrue(zi.getUTF8().contains(occurrenceTo)); assertTrue(zi.getUTF8().contains(contractID)); - + assertEquals(zi.importedInvoice.getZFItems()[0].getId(), "a123"); assertTrue(zi.getUTF8().contains("20200113")); // to contain item delivery periods assertTrue(zi.getUTF8().contains("20200115")); // to contain item delivery periods diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java index e32bc28f..f60029c1 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2ZInvoiceImporterTest.java @@ -380,6 +380,31 @@ public class ZF2ZInvoiceImporterTest extends ResourceCase { } + @Test + public void testImportPrepaid() throws XPathExpressionException, ParseException { + InputStream inputStream = this.getClass() + .getResourceAsStream("/EN16931_1_Teilrechnung.pdf"); + ZUGFeRDInvoiceImporter importer = new ZUGFeRDInvoiceImporter(); + importer.doIgnoreCalculationErrors(); + importer.setInputStream(inputStream); + + CalculatedInvoice invoice = new CalculatedInvoice(); + importer.extractInto(invoice); + + boolean isBD=invoice.getTotalPrepaidAmount() instanceof BigDecimal; + assertTrue(isBD); + BigDecimal expectedPrepaid=new BigDecimal(50); + BigDecimal expectedLineTotal=new BigDecimal("180.76"); + if (isBD) { + BigDecimal amread=invoice.getTotalPrepaidAmount(); + BigDecimal amline=invoice.getLineTotalAmount(); + assertTrue(amread.compareTo(expectedPrepaid) == 0); + assertTrue(amline.compareTo(expectedLineTotal) == 0); + } + + } + + @Test public void testImportIncludedNotes() throws XPathExpressionException, ParseException { InputStream inputStream = this.getClass() diff --git a/library/src/test/resources/EN16931_1_Teilrechnung.pdf b/library/src/test/resources/EN16931_1_Teilrechnung.pdf new file mode 100644 index 00000000..0bc3ea77 Binary files /dev/null and b/library/src/test/resources/EN16931_1_Teilrechnung.pdf differ diff --git a/validator/src/main/java/org/mustangproject/validator/PDFValidator.java b/validator/src/main/java/org/mustangproject/validator/PDFValidator.java index c42caa9e..f967eb15 100644 --- a/validator/src/main/java/org/mustangproject/validator/PDFValidator.java +++ b/validator/src/main/java/org/mustangproject/validator/PDFValidator.java @@ -122,7 +122,9 @@ public class PDFValidator extends Validator { } // step 2 validate XMP - final ZUGFeRDImporter zi = new ZUGFeRDImporter(inputStream); + final ZUGFeRDImporter zi = new ZUGFeRDImporter(); + zi.doIgnoreCalculationErrors();//of course the calculation will still be schematron checked + zi.setInputStream(inputStream); final String xmp = zi.getXMP(); final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); diff --git a/validator/src/main/java/org/mustangproject/validator/ValidationContext.java b/validator/src/main/java/org/mustangproject/validator/ValidationContext.java index a74c9f1c..73b95f2e 100644 --- a/validator/src/main/java/org/mustangproject/validator/ValidationContext.java +++ b/validator/src/main/java/org/mustangproject/validator/ValidationContext.java @@ -135,6 +135,22 @@ public class ValidationContext { return String.join(",", errorcodes); } + /*** + * + * @return the unique error IDs as comma separated string + */ + public String getCSVIDResult() { + final ArrayList errorIDs = new ArrayList<>(); + for (final ValidationResultItem validationResultItem : results) { + if (!validationResultItem.getID().isEmpty()) { + final String errorID=validationResultItem.getID(); + errorIDs.add(errorID); + + } + } + return String.join(",", errorIDs); + } + public void setInvalid() { isValid = false; } diff --git a/validator/src/main/java/org/mustangproject/validator/ValidationResultItem.java b/validator/src/main/java/org/mustangproject/validator/ValidationResultItem.java index d5a9b244..b0d6b5a1 100644 --- a/validator/src/main/java/org/mustangproject/validator/ValidationResultItem.java +++ b/validator/src/main/java/org/mustangproject/validator/ValidationResultItem.java @@ -12,6 +12,7 @@ public class ValidationResultItem { protected String message, location=null; protected int section =-1; + protected String id =""; // e.g. "FX-SCH-A-000026" private ESeverity severity=ESeverity.error; @@ -103,6 +104,15 @@ public class ValidationResultItem { return severity; } + public ValidationResultItem setID(String id) { + this.id=id; + return this; + } + + public String getID() { + return id; + } + public int getSection() { return section; } diff --git a/validator/src/main/java/org/mustangproject/validator/XMLValidator.java b/validator/src/main/java/org/mustangproject/validator/XMLValidator.java index 48dfd6f6..632b56f8 100644 --- a/validator/src/main/java/org/mustangproject/validator/XMLValidator.java +++ b/validator/src/main/java/org/mustangproject/validator/XMLValidator.java @@ -453,6 +453,7 @@ public class XMLValidator extends Validator { String thisFailText = ""; String thisFailID = ""; + String thisFailIDStr = ""; String thisFailTest = ""; String thisFailLocation = ""; if (failedAsserts.getLength() > 0) { @@ -461,7 +462,8 @@ public class XMLValidator extends Validator { //nodes.item(i).getTextContent())) { Node currentFailNode = failedAsserts.item(nodeIndex); if (currentFailNode.getAttributes().getNamedItem("id") != null) { - thisFailID = " [ID " + currentFailNode.getAttributes().getNamedItem("id").getNodeValue() + "]"; + thisFailID = currentFailNode.getAttributes().getNamedItem("id").getNodeValue(); + thisFailIDStr = " [ID " + thisFailID + "]"; } if (currentFailNode.getAttributes().getNamedItem("test") != null) { thisFailTest = currentFailNode.getAttributes().getNamedItem("test").getNodeValue(); @@ -494,8 +496,8 @@ public class XMLValidator extends Validator { LOGGER.info("FailedAssert ", thisFailText); - context.addResultItem(new ValidationResultItem(severity, thisFailText + thisFailID + " from " + xsltFilename + ")") - .setLocation(thisFailLocation).setCriterion(thisFailTest).setSection(section) + context.addResultItem(new ValidationResultItem(severity, thisFailText + thisFailIDStr + " from " + xsltFilename + ")") + .setLocation(thisFailLocation).setCriterion(thisFailTest).setSection(section).setID(thisFailID) .setPart(EPart.fx)); failedRules++; diff --git a/validator/src/main/java/org/mustangproject/validator/ZUGFeRDValidator.java b/validator/src/main/java/org/mustangproject/validator/ZUGFeRDValidator.java index edd9a73d..d21317b1 100644 --- a/validator/src/main/java/org/mustangproject/validator/ZUGFeRDValidator.java +++ b/validator/src/main/java/org/mustangproject/validator/ZUGFeRDValidator.java @@ -318,7 +318,7 @@ public class ZUGFeRDValidator { LOGGER.info("Parsed PDF:" + pdfResult + " XML:" + (xmlValidity ? "valid" : "invalid") + " Signature:" + Signature + " Checksum:" + sha1Checksum + " Profile:" + context.getProfile() + " Version:" + context.getGeneration() + " Took:" + duration + "ms Errors:[" + context.getCSVResult() - + "] " + toBeAppended); + + "] ErrorIDs: [" + context.getCSVIDResult() + "]" + toBeAppended); wasCompletelyValid = ((pdfValidity) && (xmlValidity)); return sw.toString(); }