diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index a9534091..f5cd94a9 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -416,8 +416,16 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { if ((getProfile() != Profiles.getByName("Minimum")) && (getProfile() != Profiles.getByName("BasicWL"))) { xml += "" + "" - + "" + lineIDStr + "" - + buildItemNotes(currentItem) + + "" + lineIDStr + ""; + if (getProfile() == Profiles.getByName("Extended")) { + if (currentItem.getParentLineID() != null) { + xml += "" + XMLTools.encodeXML(currentItem.getParentLineID()) + ""; + } + if (currentItem.getLineStatusReasonCode() != null) { + xml += "" + XMLTools.encodeXML(currentItem.getLineStatusReasonCode()) + ""; + } + } + xml += buildItemNotes(currentItem) + "" + ""; diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/SubInvoiceLineExportTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/SubInvoiceLineExportTest.java new file mode 100644 index 00000000..0cfab810 --- /dev/null +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/SubInvoiceLineExportTest.java @@ -0,0 +1,154 @@ +package org.mustangproject.ZUGFeRD; + +import org.junit.jupiter.api.Test; +import org.mustangproject.*; + +import java.io.File; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.*; + +public class SubInvoiceLineExportTest { + + private static final String TARGET_PDF = "./target/testout-SubInvoiceLines.pdf"; + + @Test + public void testSubInvoiceLineExportRoundTrip() throws Exception { + + // -- Build invoice with hierarchical positions -- + TradeParty sender = new TradeParty("Test Seller", "Test Street 1", "12345", "Test City", "DE") + .addTaxID("DE123456789") + .addVATID("DE123456789"); + Contact senderContact = new Contact("Max Mustermann", "012345", "max@test.de"); + sender.setContact(senderContact); + + TradeParty recipient = new TradeParty("Test Buyer", "Buyer Street 1", "54321", "Buyer City", "DE") + .addVATID("DE987654321"); + + // DETAIL line 0101: child of group 01 + Item detail0101 = new Item( + new Product("Laser printer", "", "H87", new BigDecimal("19")), + new BigDecimal("300.00"), + new BigDecimal("2.0000") + ); + detail0101.setId("0101"); + detail0101.setParentLineID("01"); + detail0101.setLineStatusReasonCode("DETAIL"); + + // DETAIL line 0102: child of group 01 + Item detail0102 = new Item( + new Product("Ink printer", "", "H87", new BigDecimal("19")), + new BigDecimal("150.00"), + new BigDecimal("3.0000") + ); + detail0102.setId("0102"); + detail0102.setParentLineID("01"); + detail0102.setLineStatusReasonCode("DETAIL"); + + // INFORMATION line 0103: child of group 01, non-calculation-relevant + Item info0103 = new Item( + new Product("Setup instructions", "", "H87", new BigDecimal("19")), + new BigDecimal("0.00"), + new BigDecimal("1.0000") + ); + info0103.setId("0103"); + info0103.setParentLineID("01"); + info0103.setLineStatusReasonCode("INFORMATION"); + + // GROUP line 01: parent, LineTotalAmount = sum of DETAIL children = 1050 + Item group01 = new Item( + new Product("Hardware bundle", "", "H87", new BigDecimal("19")), + new BigDecimal("1050.00"), + new BigDecimal("1.0000") + ); + group01.setId("01"); + group01.setLineStatusReasonCode("GROUP"); + + Invoice invoice = new Invoice() + .setNumber("SUB-INV-001") + .setIssueDate(new Date()) + .setDeliveryDate(new Date()) + .setDueDate(new Date()) + .setSender(sender) + .setRecipient(recipient) + .setCurrency("EUR") + .setPaymentTermDescription("Net 30") + .addItem(detail0101) + .addItem(detail0102) + .addItem(info0103) + .addItem(group01); + + // -- Export to PDF with EXTENDED profile -- + try (InputStream sourcePDF = this.getClass() + .getResourceAsStream("/MustangGnuaccountingBeispielRE-20170509_505PDFA3.pdf"); + ZUGFeRDExporterFromA3 exporter = new ZUGFeRDExporterFromA3() + .setProducer("Mustang SubInvoiceLine Test") + .setCreator("Test") + .setZUGFeRDVersion(2) + .setProfile("EXTENDED") + .load(sourcePDF)) { + + exporter.setTransaction(invoice); + + // Verify XML contains the hierarchical elements + String xml = new String(exporter.getProvider().getXML(), StandardCharsets.UTF_8); + assertTrue(xml.contains("01"), + "XML must contain ParentLineID element"); + assertTrue(xml.contains("DETAIL"), + "XML must contain DETAIL LineStatusReasonCode"); + assertTrue(xml.contains("GROUP"), + "XML must contain GROUP LineStatusReasonCode"); + assertTrue(xml.contains("INFORMATION"), + "XML must contain INFORMATION LineStatusReasonCode"); + + exporter.export(TARGET_PDF); + } + + // -- Re-import and verify round-trip -- + ZUGFeRDImporter importer = new ZUGFeRDImporter(TARGET_PDF); + Invoice reimported = new Invoice(); + importer.extractInto(reimported); + + // Find items by ID + Item rDetail0101 = findItemById(reimported, "0101"); + assertNotNull(rDetail0101, "DETAIL item 0101 must be reimported"); + assertEquals("01", rDetail0101.getParentLineID()); + assertEquals("DETAIL", rDetail0101.getLineStatusReasonCode()); + assertTrue(rDetail0101.isCalculationRelevant()); + + Item rDetail0102 = findItemById(reimported, "0102"); + assertNotNull(rDetail0102, "DETAIL item 0102 must be reimported"); + assertEquals("01", rDetail0102.getParentLineID()); + assertEquals("DETAIL", rDetail0102.getLineStatusReasonCode()); + assertTrue(rDetail0102.isCalculationRelevant()); + + Item rInfo0103 = findItemById(reimported, "0103"); + assertNotNull(rInfo0103, "INFORMATION item 0103 must be reimported"); + assertEquals("01", rInfo0103.getParentLineID()); + assertEquals("INFORMATION", rInfo0103.getLineStatusReasonCode()); + assertFalse(rInfo0103.isCalculationRelevant()); + + Item rGroup01 = findItemById(reimported, "01"); + assertNotNull(rGroup01, "GROUP item 01 must be reimported"); + assertNull(rGroup01.getParentLineID()); + assertEquals("GROUP", rGroup01.getLineStatusReasonCode()); + assertFalse(rGroup01.isCalculationRelevant()); + + // Verify only DETAIL lines contribute to totals + // DETAIL: 0101=600, 0102=450 = 1050 + TransactionCalculator tc = new TransactionCalculator(reimported); + assertEquals(new BigDecimal("1050.00"), tc.getTotal().setScale(2)); + } + + private Item findItemById(Invoice invoice, String id) { + for (IZUGFeRDExportableItem item : invoice.getZFItems()) { + if (item instanceof Item && id.equals(((Item) item).getId())) { + return (Item) item; + } + } + return null; + } +} diff --git a/validator/src/test/java/org/mustangproject/validator/LibraryTest.java b/validator/src/test/java/org/mustangproject/validator/LibraryTest.java index 8a072a0c..e101b4fc 100644 --- a/validator/src/test/java/org/mustangproject/validator/LibraryTest.java +++ b/validator/src/test/java/org/mustangproject/validator/LibraryTest.java @@ -331,4 +331,29 @@ public class LibraryTest extends ResourceCase { .isEqualTo(0); } + public void testSubInvoiceLineExportValidity() { + File tempFile = new File("../library/target/testout-SubInvoiceLines.pdf"); + assertTrue(tempFile.exists()); + ZUGFeRDValidator zfv = new ZUGFeRDValidator(); + + String res = zfv.validate(tempFile.getAbsolutePath()); + + assertThat(res).valueByXPath("/validation/pdf/summary/@status") + .isEqualTo("valid"); + + assertThat(res).valueByXPath("/validation/xml/summary/@status") + .isEqualTo("valid"); + + assertThat(res).valueByXPath("/validation/summary/@status") + .isEqualTo("valid"); + + assertThat(res).valueByXPath("count(//error)") + .asInt() + .isEqualTo(0); + + assertThat(res).valueByXPath("count(//warning)") + .asInt() + .isEqualTo(0); + } + }