From 5555db58aa6e3a0aaac499571d3996fd35aafac4 Mon Sep 17 00:00:00 2001 From: jstaerk Date: Tue, 29 Jul 2025 11:15:28 +0200 Subject: [PATCH] also read/import product discounts --- doc/development_documentation.md | 18 ++- .../main/java/org/mustangproject/Item.java | 49 ++++-- .../main/java/org/mustangproject/Product.java | 18 +++ .../ZUGFeRD/CalculationTest.java | 18 +-- .../ZUGFeRD/DeSerializationTest.java | 13 +- .../mustangproject/ZUGFeRD/ZF2PushTest.java | 144 +++++++++++++----- .../ZUGFeRD/ZF2ZInvoiceImporterTest.java | 2 +- 7 files changed, 199 insertions(+), 63 deletions(-) diff --git a/doc/development_documentation.md b/doc/development_documentation.md index 852a6071..e940ebda 100644 --- a/doc/development_documentation.md +++ b/doc/development_documentation.md @@ -57,12 +57,26 @@ to validate the XML part of the invoices. ![Architecture of the validator](ZUV-Architektur.svg "Graph of the architecture of the validator component") -## New build +## Aspects +Apart from the fact that apart from +* the code +* we need tests and apart from implementing it in -Target platform is java 1.17 +* the interface +* usually we need functionality in or via the invoice class. + +Reading should work for both +* CII and +* UBL + +And when writing, +* it should be readable as well, usually in the invoiceimporter, +* and it should be readable and writeable via Jackson (i.e. JSON) ## Build +Target platform is java 1.17 + The package can be build with ``` mvnw clean package diff --git a/library/src/main/java/org/mustangproject/Item.java b/library/src/main/java/org/mustangproject/Item.java index c062e620..d2add5d7 100644 --- a/library/src/main/java/org/mustangproject/Item.java +++ b/library/src/main/java/org/mustangproject/Item.java @@ -120,10 +120,14 @@ public class Item implements IZUGFeRDExportableItem { itemMap.getAsString("ID") .ifPresent(this::setId); - itemMap.getAsString("Note") .ifPresent(this::addNote); + if (itemMap.getNode("SpecifiedTradeProduct").isPresent()) { + product = new Product(itemMap.getNode("SpecifiedTradeProduct").get()); + } else { + product = new Product(); + } itemMap.getAsNodeMap("SpecifiedLineTradeAgreement", "SpecifiedSupplyChainTradeAgreement").ifPresent(icnm -> { icnm.getAsNodeMap("BuyerOrderReferencedDocument") @@ -138,14 +142,39 @@ public class Item implements IZUGFeRDExportableItem { npptpNodes.getAsBigDecimal("ChargeAmount").ifPresent(this::setPrice); npptpNodes.getAsBigDecimal("BasisQuantity").ifPresent(this::setBasisQuantity); }); + icnm.getAsNodeMap("GrossPriceProductTradePrice").ifPresent(gpptpNodes -> { + gpptpNodes.getAsNodeMap("AppliedTradeAllowanceCharge").ifPresent(gpptpAtacNodes -> { + + /** mustang attributes differences between net and gross price to the product */ + String chargeIndicator = gpptpAtacNodes.getAsStringOrNull("ChargeIndicator"); + if ((chargeIndicator != null)&&(gpptpAtacNodes.getAsBigDecimal("ActualAmount").isPresent())) { + BigDecimal actual = gpptpAtacNodes.getAsBigDecimal("ActualAmount").get(); + if (chargeIndicator.equals("true")) { + product.addCharge(new Charge(actual)); + setPrice(getPrice().subtract(actual)); // the gross price affects the net price, which is read, + // so if we do not ignore charges|allowances we have to re-compensate the net price + } else { + product.addAllowance(new Allowance(actual)); + setPrice(getPrice().add(actual)); + } + + } + }); + }); + + + /* + String chargeIndicator = gpptpNodes.getAsNodeMap("AppliedTradeAllowanceCharge").flatMap(acChargeIndicatorNodes -> acChargeIndicatorNodes.getAsString("ChargeIndicator")).get(); + if (chargeIndicator != null) { + BigDecimal actual = gpptpNodes.getAsNodeMap("AppliedTradeAllowanceCharge").flatMap(acChargeIndicatorNodes -> acChargeIndicatorNodes.getAsBigDecimal("ActualAmount")).get(); + if (actual != null) { + } + }*/ icnm.getAllNodes("AdditionalReferencedDocument").map(ReferencedDocument::fromNode). forEach(this::addReferencedDocument); }); - itemMap.getNode("SpecifiedTradeProduct").map(Product::new).ifPresent(this::setProduct);//CII - itemMap.getNode("SpecifiedTradeProduct").map(Product::new).ifPresent(this::setProduct);//UBL - // RequestedQuantity is for Order-X, BilledQuantity for FX and ZF itemMap.getAsNodeMap("SpecifiedLineTradeDelivery", "SpecifiedSupplyChainTradeDelivery") .flatMap(icnm -> icnm.getNode("BilledQuantity", "RequestedQuantity", "DespatchedQuantity")) @@ -182,7 +211,7 @@ public class Item implements IZUGFeRDExportableItem { } if (amountString != null) { izac.setTotalAmount(new BigDecimal(amountString)); - if (percentString!=null&&(!percentString.equals("0"))) { + if (percentString != null && (!percentString.equals("0"))) { izac.setTotalAmount(new BigDecimal(amountString).divide(getQuantity())); } } @@ -304,14 +333,16 @@ public class Item implements IZUGFeRDExportableItem { } @JsonIgnore - @Override public IZUGFeRDAllowanceCharge[] getAllowances() { // in JSON is already returned as itemAllowances (and only read from there) - IZUGFeRDAllowanceCharge[] izac=new IZUGFeRDAllowanceCharge[Allowances.size()]; + @Override + public IZUGFeRDAllowanceCharge[] getAllowances() { // in JSON is already returned as itemAllowances (and only read from there) + IZUGFeRDAllowanceCharge[] izac = new IZUGFeRDAllowanceCharge[Allowances.size()]; return Allowances.toArray(izac); } @JsonIgnore - @Override public IZUGFeRDAllowanceCharge[] getCharges() { // in JSON is already returned as itemAllowances (and only read from there) - IZUGFeRDAllowanceCharge[] izac=new IZUGFeRDAllowanceCharge[Charges.size()]; + @Override + public IZUGFeRDAllowanceCharge[] getCharges() { // in JSON is already returned as itemAllowances (and only read from there) + IZUGFeRDAllowanceCharge[] izac = new IZUGFeRDAllowanceCharge[Charges.size()]; return Charges.toArray(izac); } diff --git a/library/src/main/java/org/mustangproject/Product.java b/library/src/main/java/org/mustangproject/Product.java index 93e48a79..c394b7a4 100644 --- a/library/src/main/java/org/mustangproject/Product.java +++ b/library/src/main/java/org/mustangproject/Product.java @@ -406,6 +406,16 @@ public class Product implements IZUGFeRDExportableProduct { return this; } + + /*** + * Jackson courtesy function, please use addCharge if you have the choice + * @return array of or null, if none + */ + public Product setCharges(ArrayList charges) { + this.charges=charges; + return this; + } + /*** * returns the AppliedTradeAllowanceCharges of this product which are actually Charges * @return array of or null, if none @@ -432,5 +442,13 @@ public class Product implements IZUGFeRDExportableProduct { return allowances.toArray(allowanceArr); } + /*** + * Jackson courtesy function, please use addAllowance if you have the choice + * @return array of or null, if none + */ + public Product setAllowances(ArrayList allowances) { + this.allowances=allowances; + return this; + } } diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/CalculationTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/CalculationTest.java index ebdcf643..f386d655 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/CalculationTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/CalculationTest.java @@ -106,14 +106,14 @@ public class CalculationTest extends ResourceCase { Product product; Item item; - product = new Product("Pens", "", "H84", new BigDecimal(25)); + product = new Product("Pens", "", "H87", new BigDecimal(25)); product.addAllowance(new Allowance(new BigDecimal(1))); item = new Item(product, new BigDecimal("9.50"), new BigDecimal(25)); item.addCharge(new Charge(new BigDecimal(10)).setReasonCode("ZZZ").setReason("Zuschlag")); LineCalculator lc = new LineCalculator(item); assertEquals(new BigDecimal("222.50"), lc.getItemTotalNetAmount()); invoice.addItem(item); - product = new Product("Paper", "", "H84", new BigDecimal(25)); + product = new Product("Paper", "", "H87", new BigDecimal(25)); item = new Item(product, new BigDecimal("4.50"), new BigDecimal(15)); item.addAllowance(new Allowance().setPercent(new BigDecimal(5)).setReasonCode("ZZZ").setReason("Zuschlag")); lc = new LineCalculator(item); @@ -201,7 +201,7 @@ public class CalculationTest extends ResourceCase { Product product; Item item; - product = new Product("AAA", "", "H84", sales_tax_percent1).setSellerAssignedID("1AAA"); + product = new Product("AAA", "", "H87", sales_tax_percent1).setSellerAssignedID("1AAA"); item = new Item(product, new BigDecimal("4.750"), new BigDecimal(5.00)); // set values for additional charge and discount used for next lines @@ -218,19 +218,19 @@ public class CalculationTest extends ResourceCase { invoice.addItem(item); - product = new Product("BBB", "", "H84", sales_tax_percent1).setSellerAssignedID("2BBB"); + product = new Product("BBB", "", "H87", sales_tax_percent1).setSellerAssignedID("2BBB"); item = new Item(product, new BigDecimal("5.750"), new BigDecimal(4.00)); invoice.addItem(item); - product = new Product("CCC", "", "H84", sales_tax_percent1).setSellerAssignedID("3CCC"); + product = new Product("CCC", "", "H87", sales_tax_percent1).setSellerAssignedID("3CCC"); item = new Item(product, new BigDecimal("6.750"), new BigDecimal(3.00)); invoice.addItem(item); - product = new Product("DDD", "", "H84", sales_tax_percent1).setSellerAssignedID("4DDD"); + product = new Product("DDD", "", "H87", sales_tax_percent1).setSellerAssignedID("4DDD"); item = new Item(product, new BigDecimal("7.750"), new BigDecimal(2.00)); invoice.addItem(item); - product = new Product("EEE", "", "H84", sales_tax_percent1).setSellerAssignedID("5EEE"); + product = new Product("EEE", "", "H87", sales_tax_percent1).setSellerAssignedID("5EEE"); item = new Item(product, new BigDecimal("8.750"), new BigDecimal(1.00)); invoice.addItem(item); @@ -277,7 +277,7 @@ public class CalculationTest extends ResourceCase { Product product; Item item; - product = new Product("AAA", "", "H84", BigDecimal.ZERO); + product = new Product("AAA", "", "H87", BigDecimal.ZERO); item = new Item(product, new BigDecimal("1.10"), new BigDecimal(5.00)); item.addAllowance(new Allowance().setPercent(new BigDecimal(10)).setTaxPercent(BigDecimal.ZERO)); @@ -369,7 +369,7 @@ public class CalculationTest extends ResourceCase { Product product; Item item; - product = new Product("AAA", "", "H84", BigDecimal.ZERO); + product = new Product("AAA", "", "H87", BigDecimal.ZERO); item = new Item(product, new BigDecimal("1.00"), new BigDecimal(5.00)); item.addAllowance(new Allowance(new BigDecimal(1)).setTaxPercent(BigDecimal.ZERO)); diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/DeSerializationTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/DeSerializationTest.java index f57c9520..c0ac9f3a 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/DeSerializationTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/DeSerializationTest.java @@ -446,9 +446,8 @@ public class DeSerializationTest extends ResourceCase { } assertEquals(newInvoiceFromJSON.getBuyerOrderReferencedDocumentID(), "28934"); assertFalse(hasExceptions); - - } + public void testFromJSON() throws JsonProcessingException { String globalID = "4000001123452"; String globalIDScheme = "0088"; @@ -467,6 +466,16 @@ public class DeSerializationTest extends ResourceCase { assertEquals("sender@test.org", fromJSON.getSender().getEmail()); } + public void testGrossFromJSON() throws JsonProcessingException { + + String json="{ \"documentCode\": \"380\", \"number\": \"123\", \"currency\": \"EUR\", \"paymentTermDescription\": \"Please remit until 28.07.2025\", \"issueDate\": 1753653600000, \"dueDate\": 1753653600000, \"sender\": { \"name\": \"Test company\", \"zip\": \"55232\", \"street\": \"teststr\", \"location\": \"teststadt\", \"country\": \"DE\", \"taxID\": \"4711\", \"vatID\": \"DE0815\", \"vatid\": \"DE0815\" }, \"recipient\": { \"name\": \"Franz Müller\", \"zip\": \"55232\", \"street\": \"teststr.12\", \"location\": \"Entenhausen\", \"country\": \"DE\", \"contact\": { \"name\": \"contact testname\", \"phone\": \"123456\", \"email\": \"contact.testemail@example.org\", \"fax\": \"0911623562\" } }, \"totalPrepaidAmount\": 0.00, \"lineTotalAmount\": 29.00, \"duePayable\": 34.51, \"grandTotal\": 34.51, \"taxBasis\": 29.00, \"valid\": true, \"zfitems\": [ { \"price\": 3.0000, \"quantity\": 10.0000, \"basisQuantity\": 1.0000, \"id\": \"1\", \"product\": { \"unit\": \"H87\", \"name\": \"Testprodukt\", \"taxCategoryCode\": \"S\", \"allowances\": [ { \"totalAmount\": 0.1000, \"categoryCode\": \"S\" } ], \"vatpercent\": 19.00, \"intraCommunitySupply\": false, \"reverseCharge\": false }, \"value\": 3.0000 } ], \"ownVATID\": \"DE0815\", \"ownTaxID\": \"4711\", \"ownLocation\": \"teststadt\", \"ownZIP\": \"55232\", \"ownCountry\": \"DE\", \"ownStreet\": \"teststr\"}"; + + ObjectMapper mapper = new ObjectMapper(); + CalculatedInvoice fromJSON = mapper.readValue(json, CalculatedInvoice.class); + fromJSON.calculate(); + assertEquals(new BigDecimal("34.51"),fromJSON.getDuePayable()); + } + public void testDueDateRoundtrip() 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 a72a7953..9ad9cd53 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/ZF2PushTest.java @@ -53,6 +53,7 @@ public class ZF2PushTest extends TestCase { final String TARGET_ALLOWANCESPDF = "./target/testout-ZF2PushAllowances.pdf"; final String TARGET_CREDITNOTEPDF = "./target/testout-ZF2PushCreditNote.pdf"; final String TARGET_CORRECTIONPDF = "./target/testout-ZF2PushCorrection.pdf"; + final String TARGET_ITEMGROSS = "./target/testout-ZF2PushGross.pdf"; final String TARGET_ITEMCHARGESALLOWANCESPDF = "./target/testout-ZF2PushItemChargesAllowances.pdf"; final String TARGET_CHARGESALLOWANCESPDF = "./target/testout-ZF2PushChargesAllowances.pdf"; final String TARGET_RELATIVECHARGESALLOWANCESPDF = "./target/testout-ZF2PushRelativeChargesAllowances.pdf"; @@ -159,7 +160,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(0)).setTaxExemptionReason("Kleinunternehmer gemäß §19 UStG").setTaxCategoryCode("E"), price, new BigDecimal(1.0)).addNote(theNote)) + .addItem(new Item(new Product("Testprodukt", "", "H87", new BigDecimal(0)).setTaxExemptionReason("Kleinunternehmer gemäß §19 UStG").setTaxCategoryCode("E"), price, new BigDecimal(1.0)).addNote(theNote)) ); String theXML = new String(ze.getProvider().getXML()); assertTrue(theXML.contains("