Merge branch 'master' into bugfix/1062

This commit is contained in:
Frank Langelage
2026-04-25 12:58:10 +02:00
committed by GitHub
44 changed files with 7467 additions and 554 deletions

View File

@@ -1,5 +1,30 @@
#1049 2.23.0 "Bonne Journées de la Facture Électronique"
#1044 =======
2026-04-23
improved subitem support, support french validation
- #993 Cash discount not parsed
- #1029 Added a breaking change notice regarding the removal of hardcoded values in #729
- #1037 Compress attachments
- #1038 Header allowances are aggregated incorrectly for non-XRechnung profiles
- #1044 Correct version of org.apache.pdfbox:fontbox from 3.0.3 to 3.0.6.
- #1046 Add configurable BT-23 business process ID for CII export
- #1049 Prevent exceptions in validation
- #1050 Add validator information to PDF report (name and version)
- #1052 Remove parsing OriginatorDocumentReference.ID as date
- #1055 Correct order of ApplicableTradeTax sub-elements.
- #1061 Add ability to mark an Invoice as a test invoice.
- #1072 Correct XPath-expressions for GrandTotal and TaxBasisTotalAmount.
- #1073 add hierarchical invoice positions in CII XML export for EXTENDED profile
- #1075 Vulnerability in dependency PDFBox version 3.0.6
- #1076 XMP error during validation based on mustang version 2.22. (XMP Metadata: Could not parse XMP metadata (XML invalid))
- #1082 Centralize and secure DocumentBuilder creation.
- #1083 PDF Visualizations in English and French
- #1084 Fix xml description
- #1089 Add France schematron ruleset
- reduce possible exceptions in metrics action
- added Extended-CTC-FR profile to selection for command line
2.22.0 2.22.0
======= =======
@@ -98,7 +123,7 @@ corrections
- support parsing of BT-90 CreditorReferenceID - support parsing of BT-90 CreditorReferenceID
- #871 schema validation does not ignore external entities - #871 schema validation does not ignore external entities
- #868 Fix wrong version in History.md - #868 Fix wrong version in History.md
- #729 Updates about SpecifiedTradeSettlementHeaderMonetarySummation and SpecifiedTradeSettlementPaymentMeans - #729 Updates about SpecifiedTradeSettlementHeaderMonetarySummation and SpecifiedTradeSettlementPaymentMeans. **Breaking Change:** The hardcoded values of ram:TypeCode (58) and ram:Information ("SEPA credit transfer") in the getSettlementXML() method of the IZUGFeRDTradeSettlementPayment interface have been removed. Starting with this version, any code utilizing this interface must explicitly set the corresponding attributes.
- #863 LineCalculator throws NPE if product is null (since 2.17.0) - #863 LineCalculator throws NPE if product is null (since 2.17.0)
- #731 Got a broken translation key when visualizing XML into PDF (xr:Business_process_type) - #731 Got a broken translation key when visualizing XML into PDF (xr:Business_process_type)
- #865 Add sevdesk signature to PDF creators - #865 Add sevdesk signature to PDF creators
@@ -199,7 +224,7 @@ corrections
- #679 validation of a XR does not ignore whitespace - #679 validation of a XR does not ignore whitespace
- #681 IBAN assigned to invoice sender not recipient on direct debit - #681 IBAN assigned to invoice sender not recipient on direct debit
- #689 incorrect element order when both charge reason and reasoncode are specified - #689 incorrect element order when both charge reason and reasoncode are specified
- be able to set detailedDeliveryPeriodFrom, detailedDeliveryPeriodTo MS188 - be able to set detailedDeliveryPeriodFrom, detailedDeliverDeliveryPeriodTo MS188
- updated verapdf from 1.26.1 to 1.26.2 - updated verapdf from 1.26.1 to 1.26.2
- cashDiscount JSON now corrently ignores values for cii and xr methods - cashDiscount JSON now corrently ignores values for cii and xr methods

View File

@@ -70,7 +70,7 @@ public class Main {
+ " [--version <1|2>]: set ZUGFeRD version\n" + " [--version <1|2>]: set ZUGFeRD version\n"
+ " [--profile <...>]: set ZUGFeRD profile\n" + " [--profile <...>]: set ZUGFeRD profile\n"
+ " For ZUGFeRD v1 or Order-X: <B>ASIC, <C>OMFORT or EX<T>ENDED\n" + " For ZUGFeRD v1 or Order-X: <B>ASIC, <C>OMFORT or EX<T>ENDED\n"
+ " For ZUGFeRD v2: <M>INIMUM, BASIC <W>L, <B>ASIC, <C>IUS, <E>N16931, <X>Rechnung, EX<T>ENDED\n" + " For ZUGFeRD v2: <M>INIMUM, BASIC <W>L, <B>ASIC, <C>IUS, <E>N16931, <X>Rechnung, EXTENDED-CTC-[F]R, EX<T>ENDED\n"
+ " [--attachments <filenames>]: list of file attachments (passing a single empty file name prevents prompting)\n" + " [--attachments <filenames>]: list of file attachments (passing a single empty file name prevents prompting)\n"
+ " [--no-additional-attachments]: prevent prompting for attachments\n" + " [--no-additional-attachments]: prevent prompting for attachments\n"
+ " --action ubl convert UN/CEFACT 2016b CII XML to UBL XML\n" + " --action ubl convert UN/CEFACT 2016b CII XML to UBL XML\n"
@@ -104,8 +104,7 @@ public class Main {
+ " [--out <filename>]: set output HTML file\n" + " [--out <filename>]: set output HTML file\n"
+ " --action pdf convert XML to PDF \n" + " --action pdf convert XML to PDF \n"
+ " [--source <filename>]: set input XML file\n" + " [--source <filename>]: set input XML file\n"
+ " [--out <filename>]: set output PDF file\n" + " [--out <filename>]: set output PDF file\n";
;
} }
private static void printHelp() { private static void printHelp() {
@@ -162,8 +161,10 @@ public class Main {
* Prompts the user for a input or output filename * Prompts the user for a input or output filename
* *
* @param prompt the text the user is asked * @param prompt the text the user is asked
* @param defaultFilename a default Filename. Passing an empty string indicates that specifying a file is optional * @param defaultFilename a default Filename. Passing an empty string
* @param expectedExtension will warn if filename does not match expected file extension, "or" possible with e.g. pdf|xml * indicates that specifying a file is optional
* @param expectedExtension will warn if filename does not match expected file
* extension, "or" possible with e.g. pdf|xml
* @param ensureFileExists will warn if file does NOT exist (for input files) * @param ensureFileExists will warn if file does NOT exist (for input files)
* @param ensureFileNotExists will warn if file DOES exist (for output files) * @param ensureFileNotExists will warn if file DOES exist (for output files)
* @return String * @return String
@@ -187,7 +188,7 @@ public class Main {
if (selectedName.isEmpty()) { if (selectedName.isEmpty()) {
// pressed return without entering anything // pressed return without entering anything
if (defaultFilename.isEmpty()) { if (defaultFilename.isEmpty()) {
return ""; //no default -> this is an optional filename return ""; // no default -> this is an optional filename
} }
selectedName = defaultFilename; selectedName = defaultFilename;
} }
@@ -270,6 +271,7 @@ public class Main {
/*** /***
* converts from CII to UBL * converts from CII to UBL
*
* @param xmlName the name of the xml file * @param xmlName the name of the xml file
* @param outName the name of the output file * @param outName the name of the output file
* @throws IOException * @throws IOException
@@ -309,7 +311,8 @@ public class Main {
} }
// Plain Java // Plain Java
// based on https://mkyong.com/java/how-to-convert-inputstream-to-string-in-java/ // based on
// https://mkyong.com/java/how-to-convert-inputstream-to-string-in-java/
private static String convertInputStreamToString(InputStream is) { private static String convertInputStreamToString(InputStream is) {
try (InputStream inputStream = is) { try (InputStream inputStream = is) {
int DEFAULT_BUFFER_SIZE = 8192; int DEFAULT_BUFFER_SIZE = 8192;
@@ -333,7 +336,9 @@ public class Main {
/*** /***
* the main function of the commandline tool... * the main function of the commandline tool...
* @param args the commandline args, see also https://www.mustangproject.org/commandline/#verbose *
* @param args the commandline args, see also
* https://www.mustangproject.org/commandline/#verbose
*/ */
public static void main(String[] args) { public static void main(String[] args) {
try { try {
@@ -347,12 +352,14 @@ public class Main {
options.addOption(new Option("f", "format", true, "which format to output")); options.addOption(new Option("f", "format", true, "which format to output"));
options.addOption(new Option("version", "version", true, "which version of the standard to use")); options.addOption(new Option("version", "version", true, "which version of the standard to use"));
options.addOption(new Option("profile", "profile", true, "which profile of the standard to use")); options.addOption(new Option("profile", "profile", true, "which profile of the standard to use"));
options.addOption(new Option("no-additional-attachments", "no-additional-attachments", false, "prevent prompting for attachments")); options.addOption(new Option("no-additional-attachments", "no-additional-attachments", false,
"prevent prompting for attachments"));
Option attachmentOpt = new Option("attachments", "attachments", true, "File attachments"); Option attachmentOpt = new Option("attachments", "attachments", true, "File attachments");
attachmentOpt.setValueSeparator(','); attachmentOpt.setValueSeparator(',');
attachmentOpt.setArgs(Option.UNLIMITED_VALUES); attachmentOpt.setArgs(Option.UNLIMITED_VALUES);
options.addOption(attachmentOpt); options.addOption(attachmentOpt);
Option excludeOpt = new Option("exclude", "exclude", true, "Files to exclude from recursive directory traversal"); Option excludeOpt = new Option("exclude", "exclude", true,
"Files to exclude from recursive directory traversal");
excludeOpt.setValueSeparator(','); excludeOpt.setValueSeparator(',');
excludeOpt.setArgs(Option.UNLIMITED_VALUES); excludeOpt.setArgs(Option.UNLIMITED_VALUES);
options.addOption(excludeOpt); options.addOption(excludeOpt);
@@ -361,8 +368,10 @@ public class Main {
options.addOption(new Option("language", "language", true, "output language (en, de or fr)")); options.addOption(new Option("language", "language", true, "output language (en, de or fr)"));
options.addOption(new Option("out", "out", true, "which output file to write to")); options.addOption(new Option("out", "out", true, "which output file to write to"));
options.addOption(new Option("no-notices", "no-notices", false, "suppress non-fatal errors")); options.addOption(new Option("no-notices", "no-notices", false, "suppress non-fatal errors"));
options.addOption(new Option("logAppend", "logAppend", true, "freeform text to be appended to log messages")); options.addOption(
options.addOption(new Option("disable-file-logging", "disable-file-logging", false, "suppress logging to file")); new Option("logAppend", "logAppend", true, "freeform text to be appended to log messages"));
options.addOption(
new Option("disable-file-logging", "disable-file-logging", false, "suppress logging to file"));
options.addOption(new Option("d", "directory", true, "which directory to operate on")); options.addOption(new Option("d", "directory", true, "which directory to operate on"));
options.addOption(new Option("i", "ignorefileextension", false, "ignore non-matching file extensions")); options.addOption(new Option("i", "ignorefileextension", false, "ignore non-matching file extensions"));
options.addOption(new Option("l", "listfromstdin", false, "take list of files from commandline")); options.addOption(new Option("l", "listfromstdin", false, "take list of files from commandline"));
@@ -378,7 +387,7 @@ public class Main {
// Retrieve all options // Retrieve all options
action = cmd.getOptionValue("action"); action = cmd.getOptionValue("action");
String directoryName = cmd.getOptionValue("directory"); String directoryName = cmd.getOptionValue("directory");
boolean filesFromStdIn = cmd.hasOption("listfromstdin");//((Number)cmdLine.getParsedOptionValue("integer-option")).intValue(); boolean filesFromStdIn = cmd.hasOption("listfromstdin");// ((Number)cmdLine.getParsedOptionValue("integer-option")).intValue();
boolean ignoreFileExt = cmd.hasOption("ignorefileextension"); boolean ignoreFileExt = cmd.hasOption("ignorefileextension");
boolean noAttachments = cmd.hasOption("no-additional-attachments"); boolean noAttachments = cmd.hasOption("no-additional-attachments");
boolean helpRequested = cmd.hasOption("help") || ((action != null) && (action.equals("help"))); boolean helpRequested = cmd.hasOption("help") || ((action != null) && (action.equals("help")));
@@ -398,11 +407,10 @@ public class Main {
String[] attachmentFilenames = cmd.hasOption("attachments") ? cmd.getOptionValues("attachments") : null; String[] attachmentFilenames = cmd.hasOption("attachments") ? cmd.getOptionValues("attachments") : null;
String[] excludedFilenames = cmd.hasOption("exclude") ? cmd.getOptionValues("exclude") : null; String[] excludedFilenames = cmd.hasOption("exclude") ? cmd.getOptionValues("exclude") : null;
ArrayList<FileAttachment> attachments = new ArrayList<>(); ArrayList<FileAttachment> attachments = new ArrayList<>();
/* /*
setting system property to disable FILE appender and * setting system property to disable FILE appender and
suppress creation of files and folders. * suppress creation of files and folders.
*/ */
System.setProperty("FILE_APPENDER_ENABLED", ((Boolean) (disableFileLogging == false)).toString()); System.setProperty("FILE_APPENDER_ENABLED", ((Boolean) (disableFileLogging == false)).toString());
@@ -411,14 +419,17 @@ public class Main {
if (helpRequested) { if (helpRequested) {
printHelp(); printHelp();
optionsRecognized = true; optionsRecognized = true;
} /* else if ((action!=null)&&(action.equals("license"))) { } /*
printLicense(); * else if ((action!=null)&&(action.equals("license"))) {
optionsRecognized=true; * printLicense();
} */ else if ((action != null) && (action.equals("metrics"))) { * optionsRecognized=true;
* }
*/ else if ((action != null) && (action.equals("metrics"))) {
performMetrics(directoryName, filesFromStdIn, ignoreFileExt); performMetrics(directoryName, filesFromStdIn, ignoreFileExt);
optionsRecognized = true; optionsRecognized = true;
} else if ((action != null) && (action.equals("combine"))) { } else if ((action != null) && (action.equals("combine"))) {
performCombine(sourceName, sourceXMLName, outName, format, zugferdVersion, zugferdProfile, ignoreFileExt, attachmentFilenames, attachments, noAttachments); performCombine(sourceName, sourceXMLName, outName, format, zugferdVersion, zugferdProfile,
ignoreFileExt, attachmentFilenames, attachments, noAttachments);
optionsRecognized = true; optionsRecognized = true;
} else if ((action != null) && (action.equals("extract"))) { } else if ((action != null) && (action.equals("extract"))) {
performExtract(sourceName, outName); performExtract(sourceName, outName);
@@ -439,7 +450,8 @@ public class Main {
performUBL(sourceName, outName); performUBL(sourceName, outName);
optionsRecognized = true; optionsRecognized = true;
} else if ((action != null) && (action.equals("validate"))) { } else if ((action != null) && (action.equals("validate"))) {
optionsRecognized = performValidate(sourceName, noNotices, cmd.getOptionValue("logAppend"), LogAsPDF); optionsRecognized = performValidate(sourceName, noNotices, cmd.getOptionValue("logAppend"),
LogAsPDF);
} else if ((action != null) && (action.equals("validateExpectValid"))) { } else if ((action != null) && (action.equals("validateExpectValid"))) {
optionsRecognized = performValidateExpect(true, directoryName, excludedFilenames); optionsRecognized = performValidateExpect(true, directoryName, excludedFilenames);
} else if ((action != null) && (action.equals("validateExpectInvalid"))) { } else if ((action != null) && (action.equals("validateExpectInvalid"))) {
@@ -466,7 +478,8 @@ public class Main {
} }
private static boolean performValidate(String sourceName, boolean noNotices, String logAppend, boolean createLogAsPDF) { private static boolean performValidate(String sourceName, boolean noNotices, String logAppend,
boolean createLogAsPDF) {
boolean optionsRecognized; boolean optionsRecognized;
if (sourceName == null) { if (sourceName == null) {
sourceName = getFilenameFromUser("Source PDF or XML", "invoice.pdf", "pdf|xml", true, false); sourceName = getFilenameFromUser("Source PDF or XML", "invoice.pdf", "pdf|xml", true, false);
@@ -482,7 +495,7 @@ public class Main {
String validationResultXML = zfv.validate(sourceName); String validationResultXML = zfv.validate(sourceName);
System.out.println(validationResultXML); System.out.println(validationResultXML);
if( createLogAsPDF) { if (createLogAsPDF) {
ValidationLogVisualizer vlvi = new ValidationLogVisualizer(); ValidationLogVisualizer vlvi = new ValidationLogVisualizer();
String fileBasename = FilenameUtils.getBaseName(sourceName); String fileBasename = FilenameUtils.getBaseName(sourceName);
@@ -516,7 +529,6 @@ public class Main {
return true; return true;
} }
private static void performConvert(String pdfName, String outName) throws IOException { private static void performConvert(String pdfName, String outName) throws IOException {
/* /*
* ZUGFeRDExporter ze= new ZUGFeRDExporterFromA1Factory() * ZUGFeRDExporter ze= new ZUGFeRDExporterFromA1Factory()
@@ -579,7 +591,8 @@ public class Main {
} }
private static void performCombine(String pdfName, String xmlName, String outName, String format, String zfVersion, private static void performCombine(String pdfName, String xmlName, String outName, String format, String zfVersion,
String zfProfile, Boolean ignoreInputErrors, String[] attachmentFilenames, ArrayList<FileAttachment> attachments, Boolean noAttachments) throws Exception { String zfProfile, Boolean ignoreInputErrors, String[] attachmentFilenames,
ArrayList<FileAttachment> attachments, Boolean noAttachments) throws Exception {
/* /*
* ZUGFeRDExporter ze= new ZUGFeRDExporterFromA1Factory() * ZUGFeRDExporter ze= new ZUGFeRDExporterFromA1Factory()
* .setProducer("toecount") .setCreator(System.getProperty("user.name")) * .setProducer("toecount") .setCreator(System.getProperty("user.name"))
@@ -611,11 +624,13 @@ public class Main {
byte attachmentContents[] = null; byte attachmentContents[] = null;
String attachmentFilename, attachmentMime; String attachmentFilename, attachmentMime;
if (!noAttachments) { if (!noAttachments) {
attachmentFilename = getFilenameFromUser("Additional file attachments filename (empty for none)", "", "pdf", true, false); attachmentFilename = getFilenameFromUser("Additional file attachments filename (empty for none)",
"", "pdf", true, false);
if (attachmentFilename.length() != 0) { if (attachmentFilename.length() != 0) {
attachmentContents = Files.readAllBytes(Paths.get(attachmentFilename)); attachmentContents = Files.readAllBytes(Paths.get(attachmentFilename));
attachmentMime = Files.probeContentType(Paths.get(attachmentFilename)); attachmentMime = Files.probeContentType(Paths.get(attachmentFilename));
attachments.add(new FileAttachment(attachmentFilename, attachmentMime, "Data", attachmentContents)); attachments.add(
new FileAttachment(attachmentFilename, attachmentMime, "Data", attachmentContents));
} }
} }
} else { } else {
@@ -624,15 +639,17 @@ public class Main {
if (!attachmentFilename.isEmpty()) { if (!attachmentFilename.isEmpty()) {
byte[] attachmentContents = Files.readAllBytes(Paths.get(attachmentFilename)); byte[] attachmentContents = Files.readAllBytes(Paths.get(attachmentFilename));
String attachmentMime = Files.probeContentType(Paths.get(attachmentFilename)); String attachmentMime = Files.probeContentType(Paths.get(attachmentFilename));
attachments.add(new FileAttachment(attachmentFilename, attachmentMime, "Data", attachmentContents)); attachments.add(
new FileAttachment(attachmentFilename, attachmentMime, "Data", attachmentContents));
} }
} }
} }
if (format == null) { if (format == null) {
try { try {
format = getStringFromUser("Format (fx=Factur-X, zf=ZUGFeRD, ox=Order-X, da=Cross Industry Despatch Advice)", "fx", "fx|zf|ox|da"); format = getStringFromUser(
"Format (fx=Factur-X, zf=ZUGFeRD, ox=Order-X, da=Cross Industry Despatch Advice)", "fx",
"fx|zf|ox|da");
} catch (Exception e) { } catch (Exception e) {
LOGGER.error(e.getMessage(), e); LOGGER.error(e.getMessage(), e);
} }
@@ -642,7 +659,7 @@ public class Main {
if (zfVersion == null) { if (zfVersion == null) {
try { try {
zfVersion = getStringFromUser("Version (1 or 2)", "1", "1|2");//fx default version is 1 zfVersion = getStringFromUser("Version (1 or 2)", "1", "1|2");// fx default version is 1
} catch (Exception e) { } catch (Exception e) {
LOGGER.error(e.getMessage(), e); LOGGER.error(e.getMessage(), e);
} }
@@ -659,7 +676,9 @@ public class Main {
zfProfile = getStringFromUser("Profile (p)ilot", "p", "P|p"); zfProfile = getStringFromUser("Profile (p)ilot", "p", "P|p");
} else { } else {
zfProfile = getStringFromUser( zfProfile = getStringFromUser(
"Profile [M]INIMUM, BASIC [W]L, [B]ASIC,\n" + "[C]IUS, [E]N16931, EX[T]ENDED or [X]RECHNUNG", "E", "Profile [M]INIMUM, BASIC [W]L, [B]ASIC,\n"
+ "[C]IUS, [E]N16931, EX[T]ENDED or [X]RECHNUNG",
"E",
"M|m|W|w|B|b|C|c|E|e|T|t|X|x|"); "M|m|W|w|B|b|C|c|E|e|T|t|X|x|");
} }
} catch (Exception e) { } catch (Exception e) {
@@ -670,7 +689,6 @@ public class Main {
} }
zfProfile = zfProfile.toLowerCase(); zfProfile = zfProfile.toLowerCase();
// Verify params // Verify params
ensureFileExists(pdfName); ensureFileExists(pdfName);
ensureFileExists(xmlName); ensureFileExists(xmlName);
@@ -712,6 +730,8 @@ public class Main {
zfConformanceLevelProfile = Profiles.getByName(standard, "CIUS", zfIntVersion); zfConformanceLevelProfile = Profiles.getByName(standard, "CIUS", zfIntVersion);
} else if (zfProfile.equals("e")) { } else if (zfProfile.equals("e")) {
zfConformanceLevelProfile = Profiles.getByName(standard, "EN16931", zfIntVersion); zfConformanceLevelProfile = Profiles.getByName(standard, "EN16931", zfIntVersion);
} else if (zfProfile.equals("f")) {
zfConformanceLevelProfile = Profiles.getByName(standard, "EXTENDED-CTC-FR", zfIntVersion);
} else if (zfProfile.equals("t")) { } else if (zfProfile.equals("t")) {
zfConformanceLevelProfile = Profiles.getByName(standard, "EXTENDED", zfIntVersion); zfConformanceLevelProfile = Profiles.getByName(standard, "EXTENDED", zfIntVersion);
} else if (zfProfile.equals("x")) { } else if (zfProfile.equals("x")) {
@@ -738,7 +758,8 @@ public class Main {
} else { } else {
if (format.equals("fx")) { if (format.equals("fx")) {
zfIntVersion = 2;// actually we are talking of generation, not version zfIntVersion = 2;// actually we are talking of generation, not version
// so even if someone correctly requested factur-x 1 internally we call it zugferd 2 :-( // so even if someone correctly requested factur-x 1 internally we call it
// zugferd 2 :-(
} }
ze = new ZUGFeRDExporterFromPDFA(); ze = new ZUGFeRDExporterFromPDFA();
if (ignoreInputErrors) { if (ignoreInputErrors) {
@@ -755,9 +776,9 @@ public class Main {
ze.disableFacturX(); ze.disableFacturX();
} }
for (FileAttachment attachment : attachments) { for (FileAttachment attachment : attachments) {
ze.attachFile(attachment.getFilename(), attachment.getData(), attachment.getMimetype(), attachment.getRelation()); ze.attachFile(attachment.getFilename(), attachment.getData(), attachment.getMimetype(),
attachment.getRelation());
} }
ze.setXML(Files.readAllBytes(Paths.get(xmlName))); ze.setXML(Files.readAllBytes(Paths.get(xmlName)));
@@ -818,7 +839,6 @@ public class Main {
} else { } else {
System.out.println("ZUGFeRD XML source set to " + sourceName); System.out.println("ZUGFeRD XML source set to " + sourceName);
} }
if (!intoPDF) {
if (lang == null) { if (lang == null) {
try { try {
lang = getStringFromUser("Output language", "en", "en|de|fr"); lang = getStringFromUser("Output language", "en", "en|de|fr");
@@ -828,7 +848,6 @@ public class Main {
} else { } else {
System.out.println("Output language set to " + lang); System.out.println("Output language set to " + lang);
} }
}
if (outName == null) { if (outName == null) {
String defaultFilename = "factur-x.html"; String defaultFilename = "factur-x.html";
@@ -855,7 +874,6 @@ public class Main {
ZUGFeRDVisualizer zvi = new ZUGFeRDVisualizer(); ZUGFeRDVisualizer zvi = new ZUGFeRDVisualizer();
String xml = null; String xml = null;
try { try {
if (!intoPDF) {
ZUGFeRDVisualizer.Language langCode = ZUGFeRDVisualizer.Language.EN; ZUGFeRDVisualizer.Language langCode = ZUGFeRDVisualizer.Language.EN;
if (lang.equalsIgnoreCase("de")) { if (lang.equalsIgnoreCase("de")) {
langCode = ZUGFeRDVisualizer.Language.DE; langCode = ZUGFeRDVisualizer.Language.DE;
@@ -863,10 +881,11 @@ public class Main {
if (lang.equalsIgnoreCase("fr")) { if (lang.equalsIgnoreCase("fr")) {
langCode = ZUGFeRDVisualizer.Language.FR; langCode = ZUGFeRDVisualizer.Language.FR;
} }
if (!intoPDF) {
xml = zvi.visualize(sourceName, langCode); xml = zvi.visualize(sourceName, langCode);
Files.write(Paths.get(outName), xml.getBytes()); Files.write(Paths.get(outName), xml.getBytes());
} else { } else {
zvi.toPDF(sourceName, outName); zvi.toPDF(sourceName, outName, langCode);
} }
} catch (Exception e) { } catch (Exception e) {
LOGGER.error(e.getMessage(), e); LOGGER.error(e.getMessage(), e);
@@ -880,13 +899,13 @@ public class Main {
ExportResource("/xrechnung-viewer.css"); ExportResource("/xrechnung-viewer.css");
ExportResource("/xrechnung-viewer.js"); ExportResource("/xrechnung-viewer.js");
System.out.println("xrechnung-viewer.css and xrechnung-viewer.js written as well (to local working dir)"); System.out
.println("xrechnung-viewer.css and xrechnung-viewer.js written as well (to local working dir)");
} catch (Exception e) { } catch (Exception e) {
LOGGER.error(e.getMessage(), e); LOGGER.error(e.getMessage(), e);
} }
} }
} }
/** /**
@@ -894,11 +913,14 @@ public class Main {
* *
* @param resourceName ie.: "/SmartLibrary.dll" * @param resourceName ie.: "/SmartLibrary.dll"
* @return The path to the exported resource * @return The path to the exported resource
* @throws Exception e.g. if the specified resource does not exist at the specified location * @throws Exception e.g. if the specified resource does not exist at the
* specified location
*/ */
static public String ExportResource(String resourceName) throws Exception { static public String ExportResource(String resourceName) throws Exception {
String jarFolder; String jarFolder;
try (InputStream stream = Main.class.getResourceAsStream(resourceName)) {//note that each / is a directory down in the "jar tree" been the jar the root of the tree try (InputStream stream = Main.class.getResourceAsStream(resourceName)) {// note that each / is a directory down
// in the "jar tree" been the jar
// the root of the tree
if (stream == null) { if (stream == null) {
throw new Exception("Cannot get resource \"" + resourceName + "\" from Jar file."); throw new Exception("Cannot get resource \"" + resourceName + "\" from Jar file.");
} }

View File

@@ -39,6 +39,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonInclude(JsonInclude.Include.NON_EMPTY)
public class Invoice implements IExportableTransaction { public class Invoice implements IExportableTransaction {
protected boolean testIndicator;
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, buyerOrderReferencedDocumentIssueDateTime = null, ownForeignOrganisationID = null, ownOrganisationName = null, currency = null, paymentTermDescription = null; 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, buyerOrderReferencedDocumentIssueDateTime = null, ownForeignOrganisationID = null, ownOrganisationName = null, currency = null, paymentTermDescription = null;
protected Date issueDate = null, dueDate = null, deliveryDate = null; protected Date issueDate = null, dueDate = null, deliveryDate = null;
protected TradeParty sender = null, recipient = null, deliveryAddress = null, payee = null, invoicer = null, invoicee = null; protected TradeParty sender = null, recipient = null, deliveryAddress = null, payee = null, invoicer = null, invoicee = null;
@@ -75,13 +76,23 @@ public class Invoice implements IExportableTransaction {
protected String creditorReferenceID; // required when direct debit is used. protected String creditorReferenceID; // required when direct debit is used.
private BigDecimal roundingAmount=null; private BigDecimal roundingAmount=null;
private String paymentReference; // Remittance information / Verwendungszweck, BT-83 private String paymentReference; // Remittance information / Verwendungszweck, BT-83
private String businessProcessId;
public Invoice() { public Invoice() {
ZFItems = new ArrayList<>(); ZFItems = new ArrayList<>();
cashDiscounts = new ArrayList<>(); cashDiscounts = new ArrayList<>();
setCurrency("EUR"); setCurrency("EUR");
} }
@Override
public boolean getTestIndicator() {
return testIndicator;
}
public Invoice setTestIndicator() {
this.testIndicator = true;
return this;
}
@Override @Override
public String getDocumentName() { public String getDocumentName() {
return documentName; return documentName;
@@ -197,7 +208,18 @@ public class Invoice implements IExportableTransaction {
setObjectIdentifierReferencedDocument(dr); setObjectIdentifierReferencedDocument(dr);
return this; return this;
} }
public Invoice setObjectIdentifierReferencedDocument(String id, String referenceTypeCode) {
ReferencedDocument dr = new ReferencedDocument(id);
dr.setReferenceTypeCode(referenceTypeCode);
return setObjectIdentifierReferencedDocument(dr);
}
public Invoice setObjectIdentifierReferencedDocument(String id, String referenceTypeCode, Date issueDate) {
ReferencedDocument dr = new ReferencedDocument(id);
dr.setReferenceTypeCode(referenceTypeCode);
dr.setFormattedIssueDateTime(issueDate);
return setObjectIdentifierReferencedDocument(dr);
}
public Invoice setNumber(String number) { public Invoice setNumber(String number) {
this.number = number; this.number = number;
@@ -1156,4 +1178,14 @@ public class Invoice implements IExportableTransaction {
return this; return this;
} }
public Invoice setBusinessProcessId(String id) {
this.businessProcessId = id;
return this;
}
@Override
public String getBusinessProcessId() {
return businessProcessId;
}
} }

View File

@@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.net.URL;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
@@ -11,6 +12,19 @@ import org.apache.commons.io.IOUtils;
import org.dom4j.io.XMLWriter; import org.dom4j.io.XMLWriter;
import org.mustangproject.ZUGFeRD.ZUGFeRDDateFormat; import org.mustangproject.ZUGFeRD.ZUGFeRDDateFormat;
import org.w3c.dom.Node; import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
public class XMLTools extends XMLWriter { public class XMLTools extends XMLWriter {
@Override @Override
@@ -23,6 +37,86 @@ public class XMLTools extends XMLWriter {
return super.escapeElementEntities(s); return super.escapeElementEntities(s);
} }
public static DocumentBuilder getDocumentBuilder(boolean namespaceAware) throws ParserConfigurationException {
final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
//REDHAT
//https://www.blackhat.com/docs/us-15/materials/us-15-Wang-FileCry-The-New-Age-Of-XXE-java-wp.pdf
try {
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (ParserConfigurationException e) {
// ignore
}
try {
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
} catch (IllegalArgumentException e) {
// ignore
}
try {
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
} catch (IllegalArgumentException e) {
// ignore
}
//OWASP
//https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
try {
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
} catch (ParserConfigurationException e) {
// ignore
}
try {
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
} catch (ParserConfigurationException e) {
// ignore
}
try {
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
} catch (ParserConfigurationException e) {
// ignore
}
try {
// Disable external DTDs as well
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
} catch (ParserConfigurationException e) {
// ignore
}
// and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
dbf.setNamespaceAware(namespaceAware);
return dbf.newDocumentBuilder();
}
public static Validator getValidator(URL schemaFile) throws SAXException
{
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
try {
schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
} catch (SAXNotSupportedException | SAXNotRecognizedException e) {
// ignore
}
Schema schema = schemaFactory.newSchema(schemaFile);
Validator validator = schema.newValidator();
try {
validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (SAXNotSupportedException | SAXNotRecognizedException e) {
// ignore
}
return validator;
}
public static TransformerFactory getTransformerFactory()
{
TransformerFactory factory = new net.sf.saxon.TransformerFactoryImpl();
try {
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (TransformerConfigurationException e) {
// ignore
}
return factory;
}
public static String nDigitFormat(BigDecimal value, int scale) { public static String nDigitFormat(BigDecimal value, int scale) {
/* /*
* I needed 123.45, locale independent.I tried * I needed 123.45, locale independent.I tried

View File

@@ -49,12 +49,6 @@ public class CustomXMLProvider implements IXMLProvider {
} }
@Override
public void setTest() {
// TODO Auto-generated method stub
}
@Override @Override
public Profile getProfile() { public Profile getProfile() {
return profile; return profile;

View File

@@ -114,6 +114,7 @@ public class DXExporterFromA1 extends DXExporterFromA3 {
public DXExporterFromA1 setConformanceLevel(PDFAConformanceLevel newLevel) { public DXExporterFromA1 setConformanceLevel(PDFAConformanceLevel newLevel) {
return (DXExporterFromA1) super.setConformanceLevel(newLevel); return (DXExporterFromA1) super.setConformanceLevel(newLevel);
} }
@Override @Override
public DXExporterFromA1 setProducer(String producer){ public DXExporterFromA1 setProducer(String producer){
return (DXExporterFromA1) super.setProducer(producer); return (DXExporterFromA1) super.setProducer(producer);

View File

@@ -71,6 +71,7 @@ import jakarta.activation.FileDataSource;
public class DXExporterFromA3 extends ZUGFeRDExporterFromA3 { public class DXExporterFromA3 extends ZUGFeRDExporterFromA3 {
protected PDFAConformanceLevel conformanceLevel = PDFAConformanceLevel.UNICODE; protected PDFAConformanceLevel conformanceLevel = PDFAConformanceLevel.UNICODE;
protected boolean compressionEnabled = false;
protected ArrayList<FileAttachment> fileAttachments = new ArrayList<>(); protected ArrayList<FileAttachment> fileAttachments = new ArrayList<>();
/** /**
@@ -319,8 +320,8 @@ public class DXExporterFromA3 extends ZUGFeRDExporterFromA3 {
dict.setString("Desc", description); dict.setString("Desc", description);
ByteArrayInputStream fakeFile = new ByteArrayInputStream(data); ByteArrayInputStream fakeFile = new ByteArrayInputStream(data);
PDEmbeddedFile ef = new PDEmbeddedFile(doc, fakeFile); COSName filter = compressionEnabled ? COSName.FLATE_DECODE : null;
// ef.addCompression(); PDEmbeddedFile ef = new PDEmbeddedFile(doc, fakeFile, filter);
ef.setSubtype(subType); ef.setSubtype(subType);
ef.setSize(data.length); ef.setSize(data.length);
ef.setCreationDate(new GregorianCalendar()); ef.setCreationDate(new GregorianCalendar());
@@ -440,7 +441,6 @@ public class DXExporterFromA3 extends ZUGFeRDExporterFromA3 {
return this; return this;
} }
@Override @Override
public DXExporterFromA3 setCreator(String creator) { public DXExporterFromA3 setCreator(String creator) {
this.creator = creator; this.creator = creator;

View File

@@ -38,7 +38,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import org.mustangproject.FileAttachment; import org.mustangproject.FileAttachment;
import org.mustangproject.IncludedNote; import org.mustangproject.IncludedNote;
import org.mustangproject.ReferencedDocument; import org.mustangproject.ReferencedDocument;
import org.mustangproject.TradeParty;
import org.mustangproject.ZUGFeRD.model.DocumentCodeTypeConstants; import org.mustangproject.ZUGFeRD.model.DocumentCodeTypeConstants;
/*** /***
@@ -49,6 +48,11 @@ import org.mustangproject.ZUGFeRD.model.DocumentCodeTypeConstants;
*/ */
public interface IExportableTransaction { public interface IExportableTransaction {
@JsonIgnore
default boolean getTestIndicator() {
return false;
}
/** /**
* appears in /rsm:CrossIndustryDocument/rsm:HeaderExchangedDocument/ram:Name * appears in /rsm:CrossIndustryDocument/rsm:HeaderExchangedDocument/ram:Name
* *
@@ -626,4 +630,16 @@ public interface IExportableTransaction {
default String getCreditorReferenceID() { default String getCreditorReferenceID() {
return null; return null;
} }
/**
* BT-23 Business process identifier
* /rsm:CrossIndustryInvoice/rsm:ExchangedDocumentContext/
* ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID
*
* @return business process ID (e.g. "B1" or a URN) or null if not provided
*/
default String getBusinessProcessId() {
return null;
}
} }

View File

@@ -22,13 +22,9 @@ public interface IXMLProvider {
public byte[] getXML(); public byte[] getXML();
public void setTest();
public void generateXML(IExportableTransaction trans); public void generateXML(IExportableTransaction trans);
public void setProfile(Profile p); public void setProfile(Profile p);
public Profile getProfile(); public Profile getProfile();
} }

View File

@@ -56,6 +56,7 @@ public interface IZUGFeRDExporter extends Closeable, IExporter {
public IZUGFeRDExporter load(InputStream pdfSource) throws IOException; public IZUGFeRDExporter load(InputStream pdfSource) throws IOException;
public IZUGFeRDExporter setCreator(String creator); public IZUGFeRDExporter setCreator(String creator);
public IZUGFeRDExporter setConformanceLevel(PDFAConformanceLevel newLevel); public IZUGFeRDExporter setConformanceLevel(PDFAConformanceLevel newLevel);
public IZUGFeRDExporter setEnablePDFAttachmentCompression(boolean enablePDFAttachmentCompression);
public IZUGFeRDExporter setProducer(String producer); public IZUGFeRDExporter setProducer(String producer);
public IZUGFeRDExporter setZUGFeRDVersion(int version); public IZUGFeRDExporter setZUGFeRDVersion(int version);
public boolean ensurePDFIsValid(final DataSource dataSource) throws IOException; public boolean ensurePDFIsValid(final DataSource dataSource) throws IOException;

View File

@@ -126,7 +126,6 @@ public class OXExporterFromA3 extends ZUGFeRDExporterFromA3 {
*/ */
protected String orderXDocumentType = "ORDER"; protected String orderXDocumentType = "ORDER";
private boolean attachZUGFeRDHeaders = true; private boolean attachZUGFeRDHeaders = true;
/*** /***
@@ -148,6 +147,9 @@ public class OXExporterFromA3 extends ZUGFeRDExporterFromA3 {
return "fx"; return "fx";
} }
/** Defines whether attachments to the PDF should be using FLATE compression */
private boolean compressionEnabled = false;
/** /**
* Makes A PDF/A3a-compliant document from a PDF-A1 compliant document (on the * Makes A PDF/A3a-compliant document from a PDF-A1 compliant document (on the
* metadata level, this will not e.g. convert graphics to JPG-2000) * metadata level, this will not e.g. convert graphics to JPG-2000)
@@ -315,8 +317,8 @@ public class OXExporterFromA3 extends ZUGFeRDExporterFromA3 {
dict.setString("Desc", description); dict.setString("Desc", description);
ByteArrayInputStream fakeFile = new ByteArrayInputStream(data); ByteArrayInputStream fakeFile = new ByteArrayInputStream(data);
PDEmbeddedFile ef = new PDEmbeddedFile(doc, fakeFile); COSName filter = compressionEnabled ? COSName.FLATE_DECODE : null;
// ef.addCompression(); PDEmbeddedFile ef = new PDEmbeddedFile(doc, fakeFile, filter);
ef.setSubtype(subType); ef.setSubtype(subType);
ef.setSize(data.length); ef.setSize(data.length);
ef.setCreationDate(new GregorianCalendar()); ef.setCreationDate(new GregorianCalendar());

View File

@@ -32,6 +32,7 @@ public class Profiles {
{"BASICWL", new Profile("BASICWL", "urn:factur-x.eu:1p0:basicwl")}, {"BASICWL", new Profile("BASICWL", "urn:factur-x.eu:1p0:basicwl")},
{"BASIC", new Profile("BASIC", "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic")}, {"BASIC", new Profile("BASIC", "urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic")},
{"EN16931", new Profile("EN16931", "urn:cen.eu:en16931:2017")}, {"EN16931", new Profile("EN16931", "urn:cen.eu:en16931:2017")},
{"EXTENDED-CTC-FR", new Profile("EXTENDED-CTC-FR", "urn:cen.eu:en16931:2017#conformant#urn.cpro.gouv.fr:1p0:extended-ctc-fr")},
{"EXTENDED", new Profile("EXTENDED", "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended")}, {"EXTENDED", new Profile("EXTENDED", "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended")},
{"XRECHNUNG", new Profile("XRECHNUNG", "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0")} // up next: urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0 {"XRECHNUNG", new Profile("XRECHNUNG", "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0")} // up next: urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0

View File

@@ -136,11 +136,6 @@ public class UBLDAPullProvider implements IXMLProvider {
} }
@Override
public void setTest() {
}
@Override @Override
public void setProfile(Profile p) { public void setProfile(Profile p) {
profile = p; profile = p;
@@ -150,5 +145,4 @@ public class UBLDAPullProvider implements IXMLProvider {
public Profile getProfile() { public Profile getProfile() {
return profile; return profile;
} }
} }

View File

@@ -15,6 +15,7 @@ import org.apache.fop.configuration.ConfigurationException;
import org.apache.fop.configuration.DefaultConfigurationBuilder; import org.apache.fop.configuration.DefaultConfigurationBuilder;
import org.apache.xmlgraphics.util.MimeConstants; import org.apache.xmlgraphics.util.MimeConstants;
import org.mustangproject.ClasspathResolverURIAdapter; import org.mustangproject.ClasspathResolverURIAdapter;
import org.mustangproject.XMLTools;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -42,8 +43,7 @@ public class ValidationLogVisualizer {
public ValidationLogVisualizer() { public ValidationLogVisualizer() {
mFactory = new net.sf.saxon.TransformerFactoryImpl(); mFactory = XMLTools.getTransformerFactory();
// fact = TransformerFactory.newInstance();
mFactory.setURIResolver(new ValidationLogVisualizer.ClasspathResourceURIResolver()); mFactory.setURIResolver(new ValidationLogVisualizer.ClasspathResourceURIResolver());
} }

View File

@@ -16,6 +16,8 @@ import javax.xml.transform.URIResolver;
import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource; import javax.xml.transform.stream.StreamSource;
import org.mustangproject.XMLTools;
/*** /***
* Uses a XSLT transformation to upgrade * Uses a XSLT transformation to upgrade
@@ -31,8 +33,7 @@ public class XMLUpgrader {
private Templates mXsltTemplate = null; private Templates mXsltTemplate = null;
public XMLUpgrader() { public XMLUpgrader() {
mFactory = new net.sf.saxon.TransformerFactoryImpl(); mFactory = XMLTools.getTransformerFactory();
//fact = TransformerFactory.newInstance();
mFactory.setURIResolver(new ClasspathResourceURIResolver()); mFactory.setURIResolver(new ClasspathResourceURIResolver());
} }

View File

@@ -49,14 +49,6 @@ public class ZUGFeRD1PullProvider extends ZUGFeRD2PullProvider {
private String paymentTermsDescription; private String paymentTermsDescription;
protected Profile profile = Profiles.getByName("COMFORT", 1); protected Profile profile = Profiles.getByName("COMFORT", 1);
/**
* enables the flag to indicate a test invoice in the XML structure
*/
@Override
public void setTest() {
}
@Override @Override
protected String vatFormat(BigDecimal value) { protected String vatFormat(BigDecimal value) {
return XMLTools.nDigitFormat(value, 2); return XMLTools.nDigitFormat(value, 2);

View File

@@ -35,7 +35,6 @@ import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.dom4j.Document; import org.dom4j.Document;
@@ -61,14 +60,6 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
private String paymentTermsDescription; private String paymentTermsDescription;
protected Profile profile = Profiles.getByName("EN16931"); protected Profile profile = Profiles.getByName("EN16931");
/**
* enables the flag to indicate a test invoice in the XML structure
*/
@Override
public void setTest() {
}
protected String vatFormat(BigDecimal value) { protected String vatFormat(BigDecimal value) {
return XMLTools.nDigitFormat(value, 2); return XMLTools.nDigitFormat(value, 2);
} }
@@ -384,11 +375,17 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
+ " xmlns:qdt=\"urn:un:unece:uncefact:data:standard:QualifiedDataType:100\">" + " xmlns:qdt=\"urn:un:unece:uncefact:data:standard:QualifiedDataType:100\">"
+ "<!-- generated by: mustangproject.org v" + ZUGFeRD2PullProvider.class.getPackage().getImplementationVersion() + "-->" + "<!-- generated by: mustangproject.org v" + ZUGFeRD2PullProvider.class.getPackage().getImplementationVersion() + "-->"
+ "<rsm:ExchangedDocumentContext>\n"; + "<rsm:ExchangedDocumentContext>\n";
// + "
// <ram:TestIndicator><udt:Indicator>"+testBooleanStr+"</udt:Indicator></ram:TestIndicator>"
//
if (getProfile() == Profiles.getByName("XRechnung")) { if (trans.getTestIndicator() && profile == Profiles.getByName("Extended")) {
xml += "<ram:TestIndicator><udt:Indicator>" + trans.getTestIndicator() + "</udt:Indicator></ram:TestIndicator>";
}
String businessProcessId = trans.getBusinessProcessId();
if (businessProcessId != null && !businessProcessId.isBlank()) {
xml += "<ram:BusinessProcessSpecifiedDocumentContextParameter>\n"
+ "<ram:ID>" + XMLTools.encodeXML(businessProcessId) + "</ram:ID>\n"
+ "</ram:BusinessProcessSpecifiedDocumentContextParameter>\n";
}else if (getProfile() == Profiles.getByName("XRechnung")) {
xml += "<ram:BusinessProcessSpecifiedDocumentContextParameter>\n" xml += "<ram:BusinessProcessSpecifiedDocumentContextParameter>\n"
+ "<ram:ID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</ram:ID>\n" + "<ram:ID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</ram:ID>\n"
+ "</ram:BusinessProcessSpecifiedDocumentContextParameter>\n"; + "</ram:BusinessProcessSpecifiedDocumentContextParameter>\n";
@@ -419,8 +416,16 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
if ((getProfile() != Profiles.getByName("Minimum")) && (getProfile() != Profiles.getByName("BasicWL"))) { if ((getProfile() != Profiles.getByName("Minimum")) && (getProfile() != Profiles.getByName("BasicWL"))) {
xml += "<ram:IncludedSupplyChainTradeLineItem>" + xml += "<ram:IncludedSupplyChainTradeLineItem>" +
"<ram:AssociatedDocumentLineDocument>" "<ram:AssociatedDocumentLineDocument>"
+ "<ram:LineID>" + lineIDStr + "</ram:LineID>" + "<ram:LineID>" + lineIDStr + "</ram:LineID>";
+ buildItemNotes(currentItem) if (getProfile() == Profiles.getByName("Extended")) {
if (currentItem.getParentLineID() != null) {
xml += "<ram:ParentLineID>" + XMLTools.encodeXML(currentItem.getParentLineID()) + "</ram:ParentLineID>";
}
if (currentItem.getLineStatusReasonCode() != null) {
xml += "<ram:LineStatusReasonCode>" + XMLTools.encodeXML(currentItem.getLineStatusReasonCode()) + "</ram:LineStatusReasonCode>";
}
}
xml += buildItemNotes(currentItem)
+ "</ram:AssociatedDocumentLineDocument>" + "</ram:AssociatedDocumentLineDocument>"
+ "<ram:SpecifiedTradeProduct>"; + "<ram:SpecifiedTradeProduct>";
@@ -550,15 +555,16 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
xml += "</ram:SpecifiedLineTradeDelivery>" xml += "</ram:SpecifiedLineTradeDelivery>"
+ "<ram:SpecifiedLineTradeSettlement>" + "<ram:SpecifiedLineTradeSettlement>"
+ "<ram:ApplicableTradeTax>" + "<ram:ApplicableTradeTax>";
+ "<ram:TypeCode>VAT</ram:TypeCode>"; // <CalculatedAmount/>
xml += "<ram:TypeCode>VAT</ram:TypeCode>";
if (currentItem.getProduct().getTaxExemptionReason() != null) { if (currentItem.getProduct().getTaxExemptionReason() != null) {
xml += "<ram:ExemptionReason>" + XMLTools.encodeXML(currentItem.getProduct().getTaxExemptionReason()) + "</ram:ExemptionReason>"; xml += "<ram:ExemptionReason>" + XMLTools.encodeXML(currentItem.getProduct().getTaxExemptionReason()) + "</ram:ExemptionReason>";
} }
xml += "<ram:CategoryCode>" + currentItem.getProduct().getTaxCategoryCode() + "</ram:CategoryCode>";
if (currentItem.getProduct().getTaxExemptionReasonCode() != null) { if (currentItem.getProduct().getTaxExemptionReasonCode() != null) {
xml += "<ram:ExemptionReasonCode>" + XMLTools.encodeXML(currentItem.getProduct().getTaxExemptionReasonCode()) + "</ram:ExemptionReasonCode>"; xml += "<ram:ExemptionReasonCode>" + XMLTools.encodeXML(currentItem.getProduct().getTaxExemptionReasonCode()) + "</ram:ExemptionReasonCode>";
} }
xml += "<ram:CategoryCode>" + currentItem.getProduct().getTaxCategoryCode() + "</ram:CategoryCode>";
BigDecimal vatValue=BigDecimal.ZERO; BigDecimal vatValue=BigDecimal.ZERO;
if (currentItem.getProduct().getTaxCategoryCode().equals(TaxCategoryCodeTypeConstants.ZEROTAXPRODUCTS)) { if (currentItem.getProduct().getTaxCategoryCode().equals(TaxCategoryCodeTypeConstants.ZEROTAXPRODUCTS)) {
vatValue=BigDecimal.ZERO; vatValue=BigDecimal.ZERO;
@@ -662,6 +668,11 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
xml += "<ram:AdditionalReferencedDocument>" xml += "<ram:AdditionalReferencedDocument>"
+ "<ram:IssuerAssignedID>" + XMLTools.encodeXML(trans.getObjectIdentifierReferencedDocument().getIssuerAssignedID()) + "</ram:IssuerAssignedID>" + "<ram:IssuerAssignedID>" + XMLTools.encodeXML(trans.getObjectIdentifierReferencedDocument().getIssuerAssignedID()) + "</ram:IssuerAssignedID>"
+ "<ram:TypeCode>130</ram:TypeCode>"; + "<ram:TypeCode>130</ram:TypeCode>";
// NEW: BT-18-1 scheme identifier
String rtc = trans.getObjectIdentifierReferencedDocument().getReferenceTypeCode();
if (rtc != null && !rtc.isEmpty()) {
xml += "<ram:ReferenceTypeCode>" + XMLTools.encodeXML(rtc) + "</ram:ReferenceTypeCode>";
}
if (trans.getObjectIdentifierReferencedDocument().getFormattedIssueDateTime()!=null) { if (trans.getObjectIdentifierReferencedDocument().getFormattedIssueDateTime()!=null) {
xml += "<ram:FormattedIssueDateTime>" + DATE.qdtFormat(trans.getObjectIdentifierReferencedDocument().getFormattedIssueDateTime()) + "</ram:FormattedIssueDateTime>"; xml += "<ram:FormattedIssueDateTime>" + DATE.qdtFormat(trans.getObjectIdentifierReferencedDocument().getFormattedIssueDateTime()) + "</ram:FormattedIssueDateTime>";
} }
@@ -850,7 +861,7 @@ public class ZUGFeRD2PullProvider implements IXMLProvider {
} }
if ((trans.getZFAllowances() != null) && (trans.getZFAllowances().length > 0)) { if ((trans.getZFAllowances() != null) && (trans.getZFAllowances().length > 0)) {
if (profile == Profiles.getByName("XRechnung")) { if ((profile == Profiles.getByName("XRechnung")) || (profile == Profiles.getByName("EN16931")) || (profile == Profiles.getByName("EXTENDED"))) {
for (IZUGFeRDAllowanceCharge allowance : trans.getZFAllowances()) { for (IZUGFeRDAllowanceCharge allowance : trans.getZFAllowances()) {
xml += "<ram:SpecifiedTradeAllowanceCharge>" + xml += "<ram:SpecifiedTradeAllowanceCharge>" +
"<ram:ChargeIndicator>" + "<ram:ChargeIndicator>" +

View File

@@ -77,6 +77,7 @@ import jakarta.activation.DataSource;
import jakarta.activation.FileDataSource; import jakarta.activation.FileDataSource;
public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporter { public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporter {
private boolean isFacturX = true; private boolean isFacturX = true;
public static final int DefaultZUGFeRDVersion = 2; public static final int DefaultZUGFeRDVersion = 2;
@@ -101,6 +102,9 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
private Profile profile = null; private Profile profile = null;
protected boolean documentPrepared = false; protected boolean documentPrepared = false;
/** Defines whether attachments to the PDF should be using FLATE compression */
private boolean compressionEnabled = false;
/** /**
* Data (XML invoice) to be added to the ZUGFeRD PDF. It may be externally set, * Data (XML invoice) to be added to the ZUGFeRD PDF. It may be externally set,
* in which case passing a IZUGFeRDExportableTransaction is not necessary. By * in which case passing a IZUGFeRDExportableTransaction is not necessary. By
@@ -392,7 +396,8 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
dict.setString("Desc", description); dict.setString("Desc", description);
ByteArrayInputStream fakeFile = new ByteArrayInputStream(data); ByteArrayInputStream fakeFile = new ByteArrayInputStream(data);
PDEmbeddedFile ef = new PDEmbeddedFile(doc, fakeFile); COSName filter = compressionEnabled ? COSName.FLATE_DECODE : null;
PDEmbeddedFile ef = new PDEmbeddedFile(doc, fakeFile, filter);
// ef.addCompression(); // ef.addCompression();
ef.setSubtype(subType); ef.setSubtype(subType);
ef.setSize(data.length); ef.setSize(data.length);
@@ -503,6 +508,12 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte
return this; return this;
} }
@Override
public IZUGFeRDExporter setEnablePDFAttachmentCompression(boolean compressionEnabled) {
this.compressionEnabled = compressionEnabled;
return this;
}
public ZUGFeRDExporterFromA3 setCreator(String creator) { public ZUGFeRDExporterFromA3 setCreator(String creator) {
this.creator = creator; this.creator = creator;

View File

@@ -188,6 +188,10 @@ public class ZUGFeRDExporterFromPDFA implements IZUGFeRDExporter {
return getExporter().setConformanceLevel(newLevel); return getExporter().setConformanceLevel(newLevel);
} }
public IZUGFeRDExporter setEnablePDFAttachmentCompression(boolean compressionEnabled) {
return getExporter().setEnablePDFAttachmentCompression(compressionEnabled);
}
public IZUGFeRDExporter setProducer(String producer) { public IZUGFeRDExporter setProducer(String producer) {
return getExporter().setProducer(producer); return getExporter().setProducer(producer);

View File

@@ -19,6 +19,8 @@ import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath; import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpression;
@@ -29,6 +31,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.w3c.dom.Node; import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class ZUGFeRDImporter extends ZUGFeRDInvoiceImporter { public class ZUGFeRDImporter extends ZUGFeRDInvoiceImporter {
private static final Logger LOGGER = LoggerFactory.getLogger(ZUGFeRDImporter.class); private static final Logger LOGGER = LoggerFactory.getLogger(ZUGFeRDImporter.class);
@@ -472,17 +475,6 @@ public class ZUGFeRDImporter extends ZUGFeRDInvoiceImporter {
} }
/**
* @return raw XML of the invoice
*/
public String getMeta() {
if (rawXML == null) {
return null;
}
return new String(rawXML, StandardCharsets.UTF_8);
}
public int getVersion() throws Exception { public int getVersion() throws Exception {
if (!containsMeta) { if (!containsMeta) {
@@ -517,20 +509,6 @@ public class ZUGFeRDImporter extends ZUGFeRDInvoiceImporter {
} }
/**
* will return true if the metadata (just extract-ed or set with setMeta) contains ZUGFeRD XML
*
* @return true if the invoice contains ZUGFeRD XML
*/
public boolean canParse() {
// SpecifiedExchangedDocumentContext is in the schema, so a relatively good
// indication if zugferd is present - better than just invoice
final String meta = getMeta();
return (meta != null) && (meta.length() > 0) && ((meta.contains("SpecifiedExchangedDocumentContext")
/* ZF1 */ || meta.contains("ExchangedDocumentContext") /* ZF2 */));
}
/** /**
* returns an instance of PostalTradeAddress for SellerTradeParty section * returns an instance of PostalTradeAddress for SellerTradeParty section

View File

@@ -18,9 +18,7 @@ import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath; import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathConstants;
@@ -48,6 +46,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -311,28 +310,52 @@ public class ZUGFeRDInvoiceImporter {
setRawXML(rawXML, true); setRawXML(rawXML, true);
} }
private void setDocument() throws ParserConfigurationException, IOException, SAXException, ParseException {
final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
//REDHAT
//https://www.blackhat.com/docs/us-15/materials/us-15-Wang-FileCry-The-New-Age-Of-XXE-java-wp.pdf
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
//OWASP /**
//https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html * @return raw XML of the invoice
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); */
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); public String getMeta() {
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); if (rawXML == null) {
// Disable external DTDs as well return null;
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); }
// and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
dbf.setXIncludeAware(false); return new String(rawXML, StandardCharsets.UTF_8);
dbf.setExpandEntityReferences(false); }
dbf.setNamespaceAware(true);
final DocumentBuilder builder = dbf.newDocumentBuilder(); /**
* will return true if the metadata (just extract-ed or set with setMeta) contains ZUGFeRD XML
*
* @return true if the invoice contains ZUGFeRD XML
*/
public boolean canParse() {
// SpecifiedExchangedDocumentContext is in the schema, so a relatively good
// indication if zugferd is present - better than just invoice
final String meta = getMeta();
if ((meta == null) || (meta.length() == 0)) {
return false;
}
final ByteArrayInputStream is = new ByteArrayInputStream(rawXML); final ByteArrayInputStream is = new ByteArrayInputStream(rawXML);
/// is.skip(guessBOMSize(is)); /// is.skip(guessBOMSize(is));
try {
DocumentBuilder builder = XMLTools.getDocumentBuilder(true);
document = builder.parse(is);
} catch (Exception e) {
return false;
}
return ((meta.contains("SpecifiedExchangedDocumentContext")
/* ZF1 */ || meta.contains("ExchangedDocumentContext") /* ZF2 */));
}
private void setDocument() throws ParserConfigurationException, IOException, SAXException, ParseException {
final ByteArrayInputStream is = new ByteArrayInputStream(rawXML);
/// is.skip(guessBOMSize(is));
DocumentBuilder builder = XMLTools.getDocumentBuilder(true);
if (canParse()) {
document = builder.parse(is); document = builder.parse(is);
if (parseAutomatically) { if (parseAutomatically) {
try { try {
@@ -342,6 +365,8 @@ public class ZUGFeRDInvoiceImporter {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
}
} }
public void setID(String id) { public void setID(String id) {
@@ -472,7 +497,7 @@ public class ZUGFeRDInvoiceImporter {
xpr = xpath.compile("//*[local-name()=\"ExchangedDocument\"]|//*[local-name()=\"HeaderExchangedDocument\"]"); xpr = xpath.compile("//*[local-name()=\"ExchangedDocument\"]|//*[local-name()=\"HeaderExchangedDocument\"]");
NodeList ExchangedDocumentNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); NodeList ExchangedDocumentNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
xpr = xpath.compile("//*[local-name()=\"GrandTotalAmount\"]|//*[local-name()=\"TaxInclusiveAmount\"]"); xpr = xpath.compile("//*[local-name()=\"SpecifiedTradeSettlementMonetarySummation\"]/*[local-name()=\"GrandTotalAmount\"]|//*[local-name()=\"SpecifiedTradeSettlementHeaderMonetarySummation\"]/*[local-name()=\"GrandTotalAmount\"]|//*[local-name()=\"LegalMonetaryTotal\"]/*[local-name()=\"TaxInclusiveAmount\"]");
BigDecimal expectedGrandTotal = null; BigDecimal expectedGrandTotal = null;
NodeList totalNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); NodeList totalNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
if (totalNodes.getLength() > 0) { if (totalNodes.getLength() > 0) {
@@ -485,7 +510,7 @@ public class ZUGFeRDInvoiceImporter {
} }
} }
xpr = xpath.compile("//*[local-name()=\"TaxBasisTotalAmount\"]|//*[local-name()=\"TaxExclusiveAmount\"]"); xpr = xpath.compile("//*[local-name()=\"SpecifiedTradeSettlementMonetarySummation\"]/*[local-name()=\"TaxBasisTotalAmount\"]|//*[local-name()=\"SpecifiedTradeSettlementHeaderMonetarySummation\"]/*[local-name()=\"TaxBasisTotalAmount\"]|//*[local-name()=\"LegalMonetaryTotal\"]/*[local-name()=\"TaxExclusiveAmount\"]");
BigDecimal expectedTaxBasis = null; BigDecimal expectedTaxBasis = null;
NodeList basisNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); NodeList basisNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
if (basisNodes.getLength() > 0) { if (basisNodes.getLength() > 0) {
@@ -526,9 +551,8 @@ public class ZUGFeRDInvoiceImporter {
NodeList lineDueNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); NodeList lineDueNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
BigDecimal duePayableAmount = null; BigDecimal duePayableAmount = null;
if (lineDueNodes.getLength() > 0) { if (lineDueNodes.getLength() > 0) {
String duePayableAmountStr=XMLTools.trimOrNull(lineDueNodes.item(0)); duePayableAmount = new BigDecimal(XMLTools.trimOrNull(lineDueNodes.item(0)));
if ((zpp instanceof CalculatedInvoice) && (duePayableAmountStr!=null)) { if (zpp instanceof CalculatedInvoice) {
duePayableAmount = new BigDecimal(duePayableAmountStr);
((CalculatedInvoice) zpp).setDuePayable(duePayableAmount); ((CalculatedInvoice) zpp).setDuePayable(duePayableAmount);
} }
} }
@@ -625,16 +649,10 @@ public class ZUGFeRDInvoiceImporter {
} }
String tenderReference = extractString("/*[local-name()=\"Invoice\" or local-name()=\"CreditNote\"]/*[local-name()=\"OriginatorDocumentReference\"]/*[local-name()=\"ID\"]").trim(); String tenderReference = extractString("/*[local-name()=\"Invoice\" or local-name()=\"CreditNote\"]/*[local-name()=\"OriginatorDocumentReference\"]/*[local-name()=\"ID\"]").trim();
String tenderReferenceDate = extractString("/*[local-name()=\"Invoice\" or local-name()=\"CreditNote\"]/*[local-name()=\"OriginatorDocumentReference\"]/*[local-name()=\"ID\"]").trim();
if((tenderReference != null)&&(!tenderReference.isEmpty())){ if((tenderReference != null)&&(!tenderReference.isEmpty())){
if((tenderReferenceDate != null)&&(!tenderReferenceDate.isEmpty())){
zpp.setTenderReferencedDocument(new ReferencedDocument(tenderReference, parseDate(tenderReferenceDate, "yyyy-MM-dd")));
} else {
zpp.setTenderReferencedDocument(tenderReference); zpp.setTenderReferencedDocument(tenderReference);
} }
}
String dueDt = extractString("/*[local-name()=\"Invoice\" or local-name()=\"CreditNote\"]/*[local-name()=\"DueDate\"] | /*[local-name()=\"CreditNote\"]/*[local-name()=\"PaymentMeans\"]/*[local-name()=\"PaymentDueDate\"]").trim(); String dueDt = extractString("/*[local-name()=\"Invoice\" or local-name()=\"CreditNote\"]/*[local-name()=\"DueDate\"] | /*[local-name()=\"CreditNote\"]/*[local-name()=\"PaymentMeans\"]/*[local-name()=\"PaymentDueDate\"]").trim();
if (!dueDt.isEmpty()) { if (!dueDt.isEmpty()) {
dueDate = parseDate(dueDt, "yyyy-MM-dd"); dueDate = parseDate(dueDt, "yyyy-MM-dd");
@@ -1272,6 +1290,25 @@ public class ZUGFeRDInvoiceImporter {
//appliedAmount //appliedAmount
//AppliedTradeTax //AppliedTradeTax
} }
if (cd.getDays() == null) {
NodeList siblingNodes = cashdiscountNodes.item(i).getParentNode().getChildNodes();
for (int siblingIndex = 0; siblingIndex < siblingNodes.getLength(); siblingIndex++) {
if (siblingNodes.item(siblingIndex).getLocalName() != null && siblingNodes.item(siblingIndex).getLocalName().equals("DueDateDateTime")) {
NodeList dueDateChilds = siblingNodes.item(siblingIndex).getChildNodes();
for (int dueDateChildIndex = 0; dueDateChildIndex < dueDateChilds.getLength(); dueDateChildIndex++) {
if ((dueDateChilds.item(dueDateChildIndex).getLocalName() != null) && (dueDateChilds.item(dueDateChildIndex).getLocalName().equals("DateTimeString"))) {
String dueDateString = XMLTools.trimOrNull(dueDateChilds.item(dueDateChildIndex));
dueDate = parseDate(dueDateString, "yyyyMMdd");
long diffInMillies = Math.abs(dueDate.getTime() - zpp.getIssueDate().getTime());
long diffInDays = TimeUnit.DAYS.convert(diffInMillies, TimeUnit.MILLISECONDS);
cd.setDays((int) diffInDays);
}
}
}
}
}
if ((cd.getPercent() != null)&&(cd.getDays() != null)) { if ((cd.getPercent() != null)&&(cd.getDays() != null)) {
zpp.addCashDiscount(cd); zpp.addCashDiscount(cd);
} }
@@ -1328,6 +1365,9 @@ public class ZUGFeRDInvoiceImporter {
+ Stream.of(tc.trans.getZFItems()) + Stream.of(tc.trans.getZFItems())
.map(item -> item.getCalculation().getItemTotalNetAmount().toPlainString()) .map(item -> item.getCalculation().getItemTotalNetAmount().toPlainString())
.collect(Collectors.joining(" + ")); .collect(Collectors.joining(" + "));
if (tc.trans.getRoundingAmount()!=null) {
moreDetails += " and rounding amount "+tc.trans.getRoundingAmount().toPlainString();
}
} catch (Exception ignored) { } catch (Exception ignored) {
} }
throw new ArithmeticException("Payable total in XML is " + payableTotalFromXml + ", but calculated total is " + calculatedPayableTotal + moreDetails); throw new ArithmeticException("Payable total in XML is " + payableTotalFromXml + ", but calculated total is " + calculatedPayableTotal + moreDetails);

View File

@@ -32,6 +32,7 @@ import org.apache.fop.configuration.DefaultConfigurationBuilder;
import org.apache.xmlgraphics.util.MimeConstants; import org.apache.xmlgraphics.util.MimeConstants;
import org.mustangproject.ClasspathResolverURIAdapter; import org.mustangproject.ClasspathResolverURIAdapter;
import org.mustangproject.EStandard; import org.mustangproject.EStandard;
import org.mustangproject.XMLTools;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.w3c.dom.Document; import org.w3c.dom.Document;
@@ -39,7 +40,6 @@ import org.w3c.dom.Element;
import org.xml.sax.InputSource; import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.*; import javax.xml.transform.*;
import javax.xml.transform.sax.SAXResult; import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamResult;
@@ -47,8 +47,6 @@ import javax.xml.transform.stream.StreamSource;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -85,8 +83,7 @@ public class ZUGFeRDVisualizer {
private Templates mXsltZF1HTMLTemplate = null; private Templates mXsltZF1HTMLTemplate = null;
public ZUGFeRDVisualizer() { public ZUGFeRDVisualizer() {
mFactory = new net.sf.saxon.TransformerFactoryImpl(); mFactory = XMLTools.getTransformerFactory();
// fact = TransformerFactory.newInstance();
mFactory.setURIResolver(new ClasspathResourceURIResolver()); mFactory.setURIResolver(new ClasspathResourceURIResolver());
} }
@@ -104,40 +101,8 @@ public class ZUGFeRDVisualizer {
String ublCreditNoteSignature = "CreditNote"; String ublCreditNoteSignature = "CreditNote";
String cioSignature = "SCRDMCCBDACIOMessageStructure"; String cioSignature = "SCRDMCCBDACIOMessageStructure";
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
//REDHAT
//https://www.blackhat.com/docs/us-15/materials/us-15-Wang-FileCry-The-New-Age-Of-XXE-java-wp.pdf
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
try
{
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
}
catch (IllegalArgumentException e)
{
LOGGER.warn("Property: \"Access external DTD\" not supported.");
}
try
{
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
}
catch (IllegalArgumentException e)
{
LOGGER.warn("Property: \"Access external schema\" not supported.");
}
//OWASP
//https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Disable external DTDs as well
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
// and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
dbf.setNamespaceAware(true);
try { try {
DocumentBuilder db = dbf.newDocumentBuilder(); DocumentBuilder db = XMLTools.getDocumentBuilder(true);
Document doc = db.parse(new InputSource(fis)); Document doc = db.parse(new InputSource(fis));
Element root = doc.getDocumentElement(); Element root = doc.getDocumentElement();
if (root.getLocalName().equals(zf1Signature)) { if (root.getLocalName().equals(zf1Signature)) {
@@ -252,7 +217,7 @@ public class ZUGFeRDVisualizer {
} }
} }
protected String toFOP(String xmlFilename) protected String toFOP(String xmlFilename, Language lang)
throws IOException, TransformerException, ParserConfigurationException { throws IOException, TransformerException, ParserConfigurationException {
EStandard theStandard; EStandard theStandard;
try (FileInputStream fis = new FileInputStream(xmlFilename)) { try (FileInputStream fis = new FileInputStream(xmlFilename)) {
@@ -260,11 +225,11 @@ public class ZUGFeRDVisualizer {
} }
try (FileInputStream fis = new FileInputStream(xmlFilename)) { try (FileInputStream fis = new FileInputStream(xmlFilename)) {
return toFOP(fis, theStandard); return toFOP(fis, theStandard, lang);
} }
} }
protected String toFOP(InputStream is, EStandard theStandard) protected String toFOP(InputStream is, EStandard theStandard, Language lang)
throws TransformerException, IOException { throws TransformerException, IOException {
try { try {
@@ -291,12 +256,16 @@ public class ZUGFeRDVisualizer {
Optional<InputStream> in = copyStream(iaos); Optional<InputStream> in = copyStream(iaos);
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
if (in.isPresent()) { if (in.isPresent()) {
applyXSLTToPDF(in.get(), baos); applyXSLTToPDF(in.get(), baos, lang);
} }
return baos.toString(StandardCharsets.UTF_8); return baos.toString(StandardCharsets.UTF_8);
} }
public void toPDF(String xmlFilename, String pdfFilename) { public void toPDF(String xmlFilename, String pdfFilename) {
toPDF(xmlFilename, pdfFilename, Language.DE);
}
public void toPDF(String xmlFilename, String pdfFilename, Language lang) {
// the writing part // the writing part
File XMLinputFile = new File(xmlFilename); File XMLinputFile = new File(xmlFilename);
@@ -307,7 +276,7 @@ public class ZUGFeRDVisualizer {
out from git with arbitrary options (which may include CSRF changes) out from git with arbitrary options (which may include CSRF changes)
*/ */
try { try {
fopInput = this.toFOP(XMLinputFile.getAbsolutePath()); fopInput = this.toFOP(XMLinputFile.getAbsolutePath(), lang);
} catch (TransformerException | IOException | ParserConfigurationException e) { } catch (TransformerException | IOException | ParserConfigurationException e) {
LOGGER.error("Failed to apply FOP", e); LOGGER.error("Failed to apply FOP", e);
} }
@@ -323,6 +292,10 @@ public class ZUGFeRDVisualizer {
} }
public byte[] toPDF(String xmlContent) { public byte[] toPDF(String xmlContent) {
return toPDF(xmlContent, Language.DE);
}
public byte[] toPDF(String xmlContent, Language lang) {
String fopInput = null; String fopInput = null;
@@ -334,7 +307,7 @@ public class ZUGFeRDVisualizer {
EStandard theStandard = findOutStandardFromRootNode(fis); EStandard theStandard = findOutStandardFromRootNode(fis);
fis = new ByteArrayInputStream(xmlContent.getBytes(StandardCharsets.UTF_8));//rewind :-( fis = new ByteArrayInputStream(xmlContent.getBytes(StandardCharsets.UTF_8));//rewind :-(
fopInput = toFOP(fis, theStandard); fopInput = toFOP(fis, theStandard, lang);
} catch (TransformerException | IOException | ParserConfigurationException e) { } catch (TransformerException | IOException | ParserConfigurationException e) {
LOGGER.error("Failed to apply FOP", e); LOGGER.error("Failed to apply FOP", e);
} }
@@ -470,9 +443,10 @@ public class ZUGFeRDVisualizer {
xmlFile.close(); xmlFile.close();
} }
protected void applyXSLTToPDF(final InputStream xmlFile, final OutputStream PDFOutstream) protected void applyXSLTToPDF(final InputStream xmlFile, final OutputStream PDFOutstream, Language lang)
throws TransformerException, IOException { throws TransformerException, IOException {
Transformer transformer = mXsltPDFTemplate.newTransformer(); Transformer transformer = mXsltPDFTemplate.newTransformer();
transformer.setParameter("lang", lang.name().toLowerCase());
transformer.transform(new StreamSource(xmlFile), new StreamResult(PDFOutstream)); transformer.transform(new StreamSource(xmlFile), new StreamResult(PDFOutstream));
xmlFile.close(); xmlFile.close();

View File

@@ -0,0 +1,254 @@
<?xml version="1.0" encoding="UTF-8"?>
<properties>
<entry key="xr:Buyer_reference" id="BT-10">Référence acheteur</entry>
<entry key="xr:Buyer_name" id="BT-44">Nom</entry>
<entry key="xr:Buyer_address_line_1" id="BT-50">Adresse ligne 1</entry>
<entry key="xr:Buyer_address_line_2" id="BT-51">Adresse ligne 2</entry>
<entry key="xr:Buyer_address_line_3" id="BT-163">Adresse ligne 3</entry>
<entry key="xr:Buyer_post_code" id="BT-53">Code postal</entry>
<entry key="xr:Buyer_city" id="BT-52">Ville</entry>
<entry key="xr:Buyer_country_code" id="BT-55">Code pays</entry>
<entry key="xr:Buyer_identifier" id="BT-46">Identifiant</entry>
<entry key="xr:Buyer_identifier/@scheme_identifier" id="BT-46_scheme">Schéma de l'identifiant</entry>
<entry key="xr:Buyer_contact_point" id="BT-56">Nom</entry>
<entry key="xr:Buyer_contact_telephone_number" id="BT-57">Téléphone</entry>
<entry key="xr:Buyer_contact_email_address" id="BT-58">Adresse e-mail</entry>
<entry key="xr:Seller_name" id="BT-27">Raison sociale</entry>
<entry key="xr:Seller_address_line_1" id="BT-35">Adresse ligne 1</entry>
<entry key="xr:Seller_address_line_2" id="BT-36">Adresse ligne 2</entry>
<entry key="xr:Seller_address_line_3" id="BT-162">Adresse ligne 3</entry>
<entry key="xr:Seller_post_code" id="BT-38">Code postal</entry>
<entry key="xr:Seller_city" id="BT-37">Ville</entry>
<entry key="xr:Seller_country_subdivision" id="BT-39">Région</entry>
<entry key="xr:Seller_country_code" id="BT-40">Code pays</entry>
<entry key="xr:Seller_identifier" id="BT-29">Identifiant</entry>
<entry key="xr:Seller_identifier/@scheme_identifier" id="BT-29_scheme">Schéma de l'identifiant</entry>
<entry key="xr:Seller_contact_point" id="BT-41">Nom</entry>
<entry key="xr:Seller_contact_telephone_number" id="BT-42">Téléphone</entry>
<entry key="xr:Seller_contact_email_address" id="BT-43">Adresse e-mail</entry>
<entry key="xr:Invoice_number" id="BT-1">Numéro de facture</entry>
<entry key="xr:Invoice_issue_date" id="BT-2">Date de facture</entry>
<entry key="xr:Invoice_type_code" id="BT-3">Type de facture</entry>
<entry key="xr:Invoice_currency_code" id="BT-5">Devise</entry>
<entry key="xr:Project_reference" id="BT-11">Numéro de projet</entry>
<entry key="xr:Contract_reference" id="BT-12">Numéro de contrat</entry>
<entry key="xr:Purchase_order_reference" id="BT-13">Numéro de commande</entry>
<entry key="xr:Sales_order_reference" id="BT-14">Référence de commande client</entry>
<!-- Be aware that these are for the whole invoice -->
<entry key="xr:Invoicing_period_start_date" id="BT-73">Du</entry>
<entry key="xr:Invoicing_period_end_date" id="BT-74">Au</entry>
<entry key="xr:Preceding_Invoice_reference" id="BT-25">Numéro de facture</entry>
<entry key="xr:Preceding_Invoice_issue_date" id="BT-26">Date de facture</entry>
<entry key="xr:Sum_of_Invoice_line_net_amount" id="BT-106">Total des lignes</entry>
<entry key="xr:Sum_of_allowances_on_document_level" id="BT-107">Total des remises</entry>
<entry key="xr:Sum_of_charges_on_document_level" id="BT-108">Total des frais</entry>
<entry key="xr:Invoice_total_amount_without_VAT" id="BT-109">Total HT</entry>
<entry key="xr:Invoice_total_VAT_amount" id="BT-110">Total TVA</entry>
<entry key="xr:Invoice_total_VAT_amount_in_accounting_currency" id="BT-111">Total TVA en devise comptable</entry>
<entry key="xr:Invoice_total_amount_with_VAT" id="BT-112">Total TTC</entry>
<entry key="xr:Paid_amount" id="BT-113">Montant payé</entry>
<entry key="xr:Rounding_amount" id="BT-114">Montant d'arrondi</entry>
<entry key="xr:Amount_due_for_payment" id="BT-115">Montant dû</entry>
<entry key="xr:VAT_category_code" id="BT-118">Catégorie de TVA</entry>
<entry key="xr:VAT_category_taxable_amount" id="BT-116">Total</entry>
<entry key="xr:VAT_category_rate" id="BT-119">Taux de TVA</entry>
<entry key="xr:VAT_category_tax_amount" id="BT-117">Montant de TVA</entry>
<entry key="xr:VAT_exemption_reason_text" id="BT-120">Motif d'exonération</entry>
<entry key="xr:VAT_exemption_reason_code" id="BT-121">Code du motif d'exonération</entry>
<!-- Remises -->
<entry key="xr:Document_level_allowance_VAT_category_code" id="BT-95">Catégorie de TVA de la remise</entry>
<entry key="xr:Document_level_allowance_base_amount" id="BT-93">Montant de base</entry>
<entry key="xr:Document_level_allowance_percentage" id="BT-94">Pourcentage</entry>
<entry key="xr:Document_level_allowance_amount" id="BT-92">Remise</entry>
<entry key="xr:Document_level_allowance_VAT_rate" id="BT-96">Taux de TVA de la remise</entry>
<entry key="xr:Document_level_allowance_reason" id="BT-97">Motif de la remise</entry>
<entry key="xr:Document_level_allowance_reason_code" id="BT-98">Code du motif de la remise</entry>
<!-- Frais -->
<entry key="xr:Document_level_charge_VAT_category_code" id="BT-102">Catégorie de TVA des frais</entry>
<entry key="xr:Document_level_charge_base_amount" id="BT-100">Montant de base</entry>
<entry key="xr:Document_level_charge_percentage" id="BT-101">Pourcentage</entry>
<entry key="xr:Document_level_charge_amount" id="BT-99">Frais</entry>
<entry key="xr:Document_level_charge_VAT_rate" id="BT-103">Taux de TVA des frais</entry>
<entry key="xr:Document_level_charge_reason" id="BT-104">Motif des frais</entry>
<entry key="xr:Document_level_charge_reason_code" id="BT-105">Code du motif des frais</entry>
<entry key="xr:Payment_terms" id="BT-20">Escompte ; autres conditions de paiement</entry>
<entry key="xr:Payment_due_date" id="BT-9">Date d'échéance</entry>
<entry key="xr:Payment_means_type_code" id="BT-81">Code du moyen de paiement</entry>
<entry key="xr:Payment_means_text" id="BT-82">Moyen de paiement</entry>
<entry key="xr:Remittance_information" id="BT-83">Référence de paiement</entry>
<entry key="xr:Payment_card_primary_account_number" id="BT-87">Numéro de carte</entry>
<entry key="xr:Payment_card_holder_name" id="BT-88">Titulaire de la carte</entry>
<!-- BG-19 Direct Debit -->
<entry key="xr:Mandate_reference_identifier" id="BT-89">Référence du mandat</entry>
<entry key="xr:Debited_account_identifier" id="BT-91">IBAN</entry>
<entry key="xr:Bank_assigned_creditor_identifier" id="BT-90">Identifiant créancier</entry>
<!-- BG-17 Credit Transfer-->
<entry key="xr:Payment_account_name" id="BT-85">Titulaire du compte</entry>
<entry key="xr:Payment_account_identifier" id="BT-84">IBAN</entry>
<entry key="xr:Payment_service_provider_identifier" id="BT-86">BIC</entry>
<entry key="xr:Invoice_note_subject_code" id="BT-21">Objet</entry>
<entry key="xr:Invoice_note" id="BT-22">Remarque</entry>
<!-- BG-4 Seller and others -->
<entry key="xr:Seller_trading_name" id="BT-28">Nom commercial</entry>
<entry key="xr:Seller_electronic_address" id="BT-34">Adresse électronique</entry>
<entry key="xr:Seller_electronic_address/@scheme_identifier" id="BT-34_scheme">Schéma de l'adresse électronique</entry>
<entry key="xr:Seller_legal_registration_identifier" id="BT-30">Numéro SIRET/SIREN</entry>
<entry key="xr:Seller_legal_registration_identifier/@scheme_identifier" id="BT-30_scheme">Schéma du numéro d'enregistrement</entry>
<entry key="xr:Seller_VAT_identifier" id="BT-31">Numéro de TVA intracommunautaire</entry>
<entry key="xr:Seller_tax_registration_identifier" id="BT-32">Numéro fiscal</entry>
<entry key="xr:Seller_additional_legal_information" id="BT-33">Informations juridiques complémentaires</entry>
<entry key="xr:VAT_accounting_currency_code" id="BT-6">Code devise de la TVA</entry>
<!-- BG 12 and 11 Tax Representative party -->
<entry key="xr:Seller_tax_representative_name" id="BT-62">Nom</entry>
<entry key="xr:Tax_representative_address_line_1" id="BT-64">Adresse ligne 1</entry>
<entry key="xr:Tax_representative_address_line_2" id="BT-65">Adresse ligne 2</entry>
<entry key="xr:Tax_representative_address_line_3" id="BT-164">Adresse ligne 3</entry>
<entry key="xr:Tax_representative_post_code" id="BT-67">Code postal</entry>
<entry key="xr:Tax_representative_city" id="BT-66">Ville</entry>
<entry key="xr:Tax_representative_country_subdivision" id="BT-68">Région</entry>
<entry key="xr:Tax_representative_country_code" id="BT-69">Code pays</entry>
<entry key="xr:Seller_tax_representative_VAT_identifier" id="BT-63">Numéro de TVA intracommunautaire</entry>
<!-- Additional buyer information from different bgs -->
<entry key="xr:Buyer_trading_name" id="BT-45">Nom commercial</entry>
<entry key="xr:Buyer_country_subdivision" id="BT-54">Région</entry>
<entry key="xr:Buyer_electronic_address" id="BT-49">Adresse électronique</entry>
<entry key="xr:Buyer_electronic_address/@scheme_identifier" id="BT-49_scheme">Schéma de l'adresse électronique</entry>
<entry key="xr:Buyer_legal_registration_identifier" id="BT-47">Numéro SIRET/SIREN</entry>
<entry key="xr:Buyer_legal_registration_identifier/@scheme_identifier" id="BT-47_scheme">Schéma du numéro d'enregistrement</entry>
<entry key="xr:Buyer_VAT_identifier" id="BT-48">Numéro de TVA intracommunautaire</entry>
<entry key="xr:Value_added_tax_point_date" id="BT-7">Date d'exigibilité de la TVA</entry>
<entry key="xr:Value_added_tax_point_date_code" id="BT-8">Code de la date d'exigibilité de la TVA</entry>
<entry key="xr:Buyer_accounting_reference" id="BT-19">Référence comptable</entry>
<entry key="xr:Deliver_to_location_identifier" id="BT-71">Identifiant du lieu de livraison</entry>
<entry key="xr:Deliver_to_location_identifier/@scheme_identifier" id="BT-71_scheme">Schéma de l'identifiant</entry>
<entry key="xr:Actual_delivery_date" id="BT-72">Date de livraison</entry>
<entry key="xr:Deliver_to_party_name" id="BT-70">Nom du destinataire</entry>
<entry key="xr:Deliver_to_address_line_1" id="BT-75">Adresse ligne 1</entry>
<entry key="xr:Deliver_to_address_line_2" id="BT-76">Adresse ligne 2</entry>
<entry key="xr:Deliver_to_address_line_3" id="BT-165">Adresse ligne 3</entry>
<entry key="xr:Deliver_to_post_code" id="BT-78">Code postal</entry>
<entry key="xr:Deliver_to_city" id="BT-77">Ville</entry>
<entry key="xr:Deliver_to_country_subdivision" id="BT-79">Région</entry>
<entry key="xr:Deliver_to_country_code" id="BT-80">Code pays</entry>
<entry key="xr:Tender_or_lot_reference" id="BT-17">Numéro d'appel d'offres</entry>
<entry key="xr:Receiving_advice_reference" id="BT-15">Référence de l'avis de réception</entry>
<entry key="xr:Despatch_advice_reference" id="BT-16">Référence de l'avis d'expédition</entry>
<entry key="xr:Business_process_type" id="BT-23">Identifiant du processus</entry>
<entry key="xr:Specification_identifier" id="BT-24">Identifiant de spécification</entry>
<entry key="xr:Invoiced_object_identifier" id="BT-18">Identifiant de l'objet</entry>
<entry key="xr:Invoiced_object_identifier/@scheme_identifier" id="BT-18_scheme">Schéma de l'identifiant de l'objet</entry>
<!-- BG 10 -->
<entry key="xr:Payee_name" id="BT-59">Nom</entry>
<entry key="xr:Payee_identifier" id="BT-60">Identifiant</entry>
<entry key="xr:Payee_identifier/@scheme_identifier" id="BT-60_scheme">Schéma de l'identifiant</entry>
<entry key="xr:Payee_legal_registration_identifier" id="BT-61">Numéro SIRET/SIREN</entry>
<entry key="xr:Payee_legal_registration_identifier/@scheme_identifier" id="BT-61_scheme">Schéma du numéro d'enregistrement</entry>
<!-- BG 24 Additonal supporting Documents -->
<entry key="xr:Supporting_document_reference" id="BT-122">Identifiant</entry>
<entry key="xr:Supporting_document_description" id="BT-123">Description</entry>
<entry key="xr:External_document_location" id="BT-124">Référence (ex. adresse Internet)</entry>
<entry key="xr:Attached_document" id="BT-125">Document joint</entry>
<entry key="xr:Attached_document/@mime_code" id="BT-125_mime_code">Format du document joint</entry>
<entry key="xr:Attached_document/@filename" id="BT-125_filename">Nom du document joint</entry>
<entry key="xrv:zeitstempel">Date/Heure</entry>
<entry key="xrv:betreff">Objet</entry>
<entry key="xrv:text">Texte</entry>
<entry key="xrv:details">Détails</entry>
<entry key="xr:Invoice_line_identifier" id="BT-126">Position</entry>
<entry key="xr:Invoice_line_note" id="BT-127">Texte libre</entry>
<entry key="xr:Invoice_line_object_identifier" id="BT-128_identifier">Identifiant de l'objet</entry>
<entry key="xr:Invoice_line_object_identifier/@scheme_identifier" id="BT-128_scheme">Schéma de l'identifiant de l'objet</entry>
<entry key="xr:Referenced_purchase_order_line_reference" id="BT-132">Numéro de ligne de commande</entry>
<entry key="xr:Invoice_line_Buyer_accounting_reference" id="BT-133">Référence comptable</entry>
<entry key="xr:Invoice_line_period_start_date" id="BT-134">Du</entry>
<entry key="xr:Invoice_line_period_end_date" id="BT-135">Au</entry>
<entry key="xr:Invoiced_quantity" id="BT-129">Quantité</entry>
<entry key="xr:Invoiced_quantity_unit_of_measure_code" id="BT-130">Unité</entry>
<entry key="xr:Item_net_price" id="BT-146">Prix unitaire (HT)</entry>
<entry key="xr:Invoice_line_net_amount" id="BT-131">Prix total (HT)</entry>
<entry key="xr:Item_price_discount" id="BT-147">Remise (HT)</entry>
<entry key="xr:Item_gross_price" id="BT-148">Prix catalogue (HT)</entry>
<entry key="xr:Item_price_base_quantity" id="BT-149">Quantité de base</entry>
<entry key="xr:Item_price_base_quantity_unit_of_measure" id="BT-150">Code de l'unité de mesure</entry>
<entry key="xr:Invoiced_item_VAT_category_code" id="BT-151">TVA</entry>
<entry key="xr:Invoiced_item_VAT_rate" id="BT-152">Taux de TVA</entry>
<entry key="xr:Item_name" id="BT-153">Désignation</entry>
<entry key="xr:Item_description" id="BT-154">Description</entry>
<entry key="xr:Item_Sellers_identifier" id="BT-155">Référence article</entry>
<entry key="xr:Item_Buyers_identifier" id="BT-156">Référence article acheteur</entry>
<entry key="xr:Item_standard_identifier" id="BT-157">Identifiant article</entry>
<entry key="xr:Item_standard_identifier/@scheme_identifier" class="BT-157_scheme">Schéma de l'identifiant article</entry>
<entry key="xr:Item_classification_identifier" id="BT-158">Code de classification article</entry>
<entry key="xr:Item_classification_identifier/@scheme_identifier" id="BT-158_scheme">Identifiant du schéma</entry>
<entry key="xr:Item_classification_identifier/@scheme_version_identifier" id="BT-158_scheme_version">Version du schéma</entry>
<entry key="xr:Item_country_of_origin" id="BT-159">Code du pays d'origine</entry>
<entry key="xr:Invoice_line_allowance_base_amount" id="BT-137">Montant de base (HT)</entry>
<entry key="xr:Invoice_line_allowance_percentage" id="BT-138">Pourcentage</entry>
<entry key="xr:Invoice_line_allowance_amount" id="BT-136">Remise (HT)</entry>
<entry key="xr:Invoice_line_allowance_reason" id="BT-139">Motif de la remise</entry>
<entry key="xr:Invoice_line_allowance_reason_code" id="BT-140">Code du motif de la remise</entry>
<entry key="xr:Invoice_line_charge_base_amount" id="BT-142">Montant de base (HT)</entry>
<entry key="xr:Invoice_line_charge_percentage" id="BT-143">Pourcentage</entry>
<entry key="xr:Invoice_line_charge_amount" id="BT-141">Frais (HT)</entry>
<entry key="xr:Invoice_line_charge_reason" id="BT-144">Motif des frais</entry>
<entry key="xr:Invoice_line_charge_reason_code" id="BT-145">Code du motif des frais</entry>
<entry key="xr:Third_party_payment_type" id="BT-DEX-001">Type de créance tierce</entry>
<entry key="xr:Third_party_payment_amount" id="BT-DEX-002">Montant de la créance tierce</entry>
<entry key="xr:Third_party_payment_description" id="BT-DEX-003">Description de la créance tierce</entry>
<entry key="uebersicht">Données de la facture électronique</entry>
<entry key="uebersichtKaeufer" id="BG-7">Informations sur l'acheteur</entry>
<entry key="uebersichtVerkaeufer" id="BG-4">Informations sur le vendeur</entry>
<entry key="uebersichtRechnungsInfo" id="invoice-data">Données de facturation</entry>
<entry key="uebersichtRechnungsuebersicht" id="BG-22">Totaux de la facture</entry>
<entry key="uebersichtFremdleistungen" id="BG-DEX-09">Créance tierce</entry>
<entry key="uebersichtUmsatzsteuer" id="BG-23">Ventilation de la TVA au niveau de la facture</entry>
<entry key="uebersichtNachlass" id="BG-20">Remise au niveau de la facture</entry>
<entry key="uebersichtZuschlaege" id="BG-21">Frais au niveau de la facture</entry>
<entry key="uebersichtRechnungAbrechnungszeitraum">Période de facturation</entry>
<entry key="uebersichtRechnungVorausgegangeneRechnungen" id="BG-3">Factures précédentes</entry>
<entry key="uebersichtZahlungInfo" id="BG-4">Informations de paiement</entry>
<entry key="uebersichtZahlungKarte" id="BG-18">Informations carte</entry>
<entry key="uebersichtZahlungLastschrift" id="BG-19">Prélèvement</entry>
<entry key="uebersichtZahlungUeberweisung" id="BG-17">Virement</entry>
<entry key="uebersichtBemerkungen" id="BG-1">Remarques sur la facture</entry>
<entry key="details">Lignes de facture</entry>
<entry key="detailsPositionAbrechnungszeitraum" id="BG-26">Période de facturation</entry>
<entry key="detailsPositionPreiseinzelheiten" id="BG-29">Détails du prix</entry>
<entry key="detailsPositionNachlaesse" id="BG-27">Remises au niveau de la ligne de facture</entry>
<entry key="detailsPositionZuschlaege" id="BG-28">Frais au niveau de la ligne de facture</entry>
<entry key="detailsPositionArtikelinformationen" id="BG-31">Informations sur l'article</entry>
<entry key="detailsPositionArtikeleigenschaften" id="BG-32">Caractéristiques de l'article</entry>
<entry key="zusaetze">Compléments</entry>
<entry key="zusaetzeVerkaeufer" id="BG-4">Informations sur le vendeur</entry>
<entry key="zusaetzeSteuervertreter" id="BG-11">Représentant fiscal du vendeur</entry>
<entry key="zusaetzeKaeufer" id="BG-7">Informations sur l'acheteur</entry>
<entry key="zusaetzeLieferung" id="BG-13">Informations de livraison</entry>
<entry key="zusaetzeVertrag">Informations contractuelles</entry>
<entry key="zusaetzeZahlungsempfaenger" id="BG-10">Bénéficiaire différent du vendeur</entry>
<entry key="laufzettel">Fiche de suivi</entry>
<entry key="laufzettelHistorie">Historique de traitement</entry>
<entry key="anlagen">Pièces jointes</entry>
<entry key="anlagenListe">Documents justificatifs</entry>
<entry key="date-format">[D]/[M]/[Y]</entry>
<entry key="datetime-format">[D]/[M]/[Y] [H]:[m]:[s]</entry>
<entry key="amount-format">###.##0,00</entry>
<entry key="percentage-format">##0,00</entry>
<entry key="at-least-two-format">###.##0,#################</entry>
<entry key="sum-of-third-party-payment-amounts" id="BT-DEX-002_sum">Total des créances tierces</entry>
<entry key="_disclaimer">Nous déclinons toute responsabilité quant à l'exactitude des données.</entry>
<entry key="_invoice-note-group">Remarques sur la facture</entry>
<entry key="_open">Ouvrir</entry>
<entry key="no-data">Aucune donnée disponible</entry>
<entry key="_net">HT</entry>
<entry key="_gross">TTC</entry>
<entry key="_no-content">Les sections sans contenu ne sont pas affichées !</entry>
<entry key="_description">Description</entry>
<entry key="_price">Prix</entry>
<entry key="_price-unit">Unité de prix</entry>
<entry key="_vat">Taux TVA</entry>
<entry key="_tax-code">Code taxe</entry>
<entry key="_total">Total</entry>
<entry key="_page">Page</entry>
</properties>

View File

@@ -85,6 +85,37 @@
font-weight="bold"> font-weight="bold">
<xsl:text>Prüfbericht</xsl:text> <xsl:text>Prüfbericht</xsl:text>
</fo:block> </fo:block>
<xsl:call-template name="SubHeader">
<xsl:with-param name="text"
select="'Angaben zur Prüfanwendung'"/>
<xsl:with-param name="color"
select="'black'"/>
</xsl:call-template>
<fo:table>
<fo:table-column border-style="none"/>
<fo:table-column column-width="70%"
border-style="none"/>
<fo:table-body>
<fo:table-row>
<fo:table-cell>
<fo:block text-align="left">Name:</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block text-align="right">mustangproject.org</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row>
<fo:table-cell>
<fo:block text-align="left">Version:</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block text-align="right">
<xsl:value-of select="./xml/info/validator/@version"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
<xsl:call-template name="SubHeader"> <xsl:call-template name="SubHeader">
<xsl:with-param name="text" <xsl:with-param name="text"
select="'Angaben zum geprüften Dokument'"/> select="'Angaben zum geprüften Dokument'"/>

View File

@@ -8,6 +8,7 @@
xmlns:xrf="https://projekte.kosit.org/xrechnung/xrechnung-visualization/functions"> xmlns:xrf="https://projekte.kosit.org/xrechnung/xrechnung-visualization/functions">
<xsl:decimal-format name="de" decimal-separator="," grouping-separator="." NaN="" /> <xsl:decimal-format name="de" decimal-separator="," grouping-separator="." NaN="" />
<xsl:decimal-format name="fr" decimal-separator="," grouping-separator="." NaN="" />
<xsl:decimal-format name="en" decimal-separator="." grouping-separator="," NaN="" /> <xsl:decimal-format name="en" decimal-separator="." grouping-separator="," NaN="" />
<xsl:template name="uebersicht"> <xsl:template name="uebersicht">

View File

@@ -0,0 +1,64 @@
package org.mustangproject.ZUGFeRD;
import junit.framework.TestCase;
import org.mustangproject.Invoice;
import org.mustangproject.Item;
import org.mustangproject.Product;
import org.mustangproject.ReferencedDocument;
import org.mustangproject.TradeParty;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import static org.xmlunit.assertj.XmlAssert.assertThat;
public class BT18ReferenceTypeCodeTest extends TestCase {
/**
* Ensures BT-18-1 (scheme identifier) is serialized as ram:ReferenceTypeCode
* inside ram:AdditionalReferencedDocument (TypeCode=130).
*/
public void testBT18ReferenceTypeCodeIsWrittenWhenSet() {
// Minimal invoice (XRechnung profile => CII output)
Invoice i = new Invoice()
.setIssueDate(new Date())
.setDueDate(new Date())
.setDeliveryDate(new Date())
.setSender(new TradeParty("Test company", "teststr", "55232", "teststadt", "DE"))
.setRecipient(new TradeParty("Franz Müller", "teststr.12", "55232", "Entenhausen", "DE"))
.setNumber("INV-123")
.addItem(new Item(
new Product("Testprodukt", "", "C62", BigDecimal.ZERO),
new BigDecimal("1.00"),
BigDecimal.ONE
));
// BT-18: Invoiced Object Identifier (AdditionalReferencedDocument / TypeCode 130)
ReferencedDocument rd = new ReferencedDocument("ID unique");
rd.setReferenceTypeCode("AJW"); // BT-18-1
i.setObjectIdentifierReferencedDocument(rd);
ZUGFeRD2PullProvider zf2p = new ZUGFeRD2PullProvider();
zf2p.setProfile(Profiles.getByName("XRechnung"));
zf2p.generateXML(i);
String xml = new String(zf2p.getXML(), StandardCharsets.UTF_8);
// Assert: we still have a CII invoice
assertTrue(xml.contains("<rsm:CrossIndustryInvoice"));
// Assert: BT-18 block contains ID, TypeCode=130 and ReferenceTypeCode=AJW
assertThat(xml)
.valueByXPath("string(//*[local-name()='AdditionalReferencedDocument']/*[local-name()='TypeCode'])")
.isEqualTo("130");
assertThat(xml)
.valueByXPath("string(//*[local-name()='AdditionalReferencedDocument']/*[local-name()='IssuerAssignedID'])")
.isEqualTo("ID unique");
assertThat(xml)
.valueByXPath("string(//*[local-name()='AdditionalReferencedDocument']/*[local-name()='ReferenceTypeCode'])")
.isEqualTo("AJW");
}
}

View File

@@ -0,0 +1,121 @@
package org.mustangproject.ZUGFeRD;
import org.junit.jupiter.api.Test;
import org.mustangproject.ZUGFeRD.Profiles;
import org.mustangproject.ZUGFeRD.ZUGFeRD2PullProvider;
import org.mustangproject.Invoice;
import org.mustangproject.Item;
import org.mustangproject.Product;
import org.mustangproject.TradeParty;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for BT-23 (BusinessProcessSpecifiedDocumentContextParameter/ID) emission in CII output.
*
* Assumptions / prerequisites for the proposed fix:
* - IExportableTransaction#getBusinessProcessId() exists (as a default method returning null)
* - Invoice#setBusinessProcessId(String) exists and Invoice overrides getBusinessProcessId()
* - ZUGFeRD2PullProvider writes BT-23 if a custom businessProcessId is provided,
* otherwise keeps the current XRechnung default behavior.
*/
public class BusinessProcessIdCiiTest {
@Test
void xrechnung_nonRegression_defaultBusinessProcessId_isStillEmitted() throws Exception {
// ===== GIVEN: A minimal invoice (as provided) WITHOUT setting businessProcessId =====
TradeParty buyer = new TradeParty("Client X", "3 rue C", "33000", "Bordeaux", "FR");
TradeParty seller = new TradeParty("Mairie A", "1 rue A", "75001", "Paris", "FR");
Invoice invoice = new Invoice()
.setNumber("INV-CALC-001") // BT-1: Invoice number -> /.../ExchangedDocument/ram:ID
.setIssueDate(new Date()) // BT-2: Issue date
.setDeliveryDate(new Date()) // Delivery date (depending on profile/exporter mapping)
.setDueDate(new Date()) // BT-9: Payment due date
.setCurrency("EUR") // BT-5: Invoice currency code
.setSender(buyer)
.setRecipient(seller);
Product service = new Product("Prestation", "Service intercommunal", "C62", new BigDecimal("20.00"));
invoice.addItem(new Item(service, BigDecimal.ONE, BigDecimal.TEN).setTax(BigDecimal.valueOf(20)));
// ===== WHEN: Generating CII XML using the XRechnung profile =====
ZUGFeRD2PullProvider provider = new ZUGFeRD2PullProvider();
provider.setProfile(Profiles.getByName("XRechnung"));
provider.generateXML(invoice);
String xml = new String(provider.getXML(), StandardCharsets.UTF_8);
// ===== THEN: Non-regression check =====
// XRechnung currently emits BT-23 with a default fixed URN.
// This behavior must remain unchanged when businessProcessId is NOT explicitly provided by the user.
assertTrue(
xml.contains("<ram:BusinessProcessSpecifiedDocumentContextParameter>"),
"XRechnung should still emit BusinessProcessSpecifiedDocumentContextParameter (BT-23)."
);
assertTrue(
xml.contains("<ram:ID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</ram:ID>"),
"XRechnung default BT-23 URN must remain unchanged when businessProcessId is not set."
);
}
@Test
void whenBusinessProcessIdIsSet_itIsEmitted_inExtendedProfile() throws Exception {
// ===== GIVEN: The same minimal invoice, but WITH an explicit businessProcessId =====
TradeParty buyer = new TradeParty("Client X", "3 rue C", "33000", "Bordeaux", "FR");
TradeParty seller = new TradeParty("Mairie A", "1 rue A", "75001", "Paris", "FR");
Invoice invoice = new Invoice()
.setNumber("INV-CALC-001")
.setIssueDate(new Date())
.setDeliveryDate(new Date())
.setDueDate(new Date())
.setCurrency("EUR")
.setSender(buyer)
.setRecipient(seller);
Product service = new Product("Prestation", "Service intercommunal", "C62", new BigDecimal("20.00"));
invoice.addItem(new Item(service, BigDecimal.ONE, BigDecimal.TEN).setTax(BigDecimal.valueOf(20)));
// Proposed new API: user-provided BT-23 value
invoice.setBusinessProcessId("B1");
// BT-23 path:
// /rsm:CrossIndustryInvoice/rsm:ExchangedDocumentContext/
// ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID
// ===== WHEN: Generating CII XML using the EXTENDED profile =====
ZUGFeRD2PullProvider provider = new ZUGFeRD2PullProvider();
provider.setProfile(Profiles.getByName("EXTENDED"));
provider.generateXML(invoice);
String xml = new String(provider.getXML(), StandardCharsets.UTF_8);
// ===== THEN: BT-23 must be emitted with the user-provided value =====
assertTrue(
xml.contains("<ram:BusinessProcessSpecifiedDocumentContextParameter>"),
"When businessProcessId is set, BT-23 must be emitted even for EXTENDED."
);
assertTrue(
xml.contains("<ram:ID>B1</ram:ID>"),
"BT-23 must contain the user-provided businessProcessId."
);
// Extra sanity check: the guideline/specification identifier (BT-24) should still be present
assertTrue(
xml.contains("<ram:GuidelineSpecifiedDocumentContextParameter>"),
"BT-24 (GuidelineSpecifiedDocumentContextParameter) must be present."
);
}
}

View File

@@ -0,0 +1,178 @@
package org.mustangproject.ZUGFeRD;
import junit.framework.TestCase;
import org.mustangproject.Invoice;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import javax.xml.xpath.XPathExpressionException;
/* https://github.com/ZUGFeRD/mustangproject/issues/993 */
public class CashDiscountFacturXTest extends TestCase {
String XMLString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
" <rsm:CrossIndustryInvoice xmlns:rsm=\"urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:ram=\"urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100\" xmlns:udt=\"urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100\" xmlns:qdt=\"urn:un:unece:uncefact:data:standard:QualifiedDataType:100\">\n" +
" <!-- generated by: mustangproject.org v2.15.1-SNAPSHOT-->\n" +
" <rsm:ExchangedDocumentContext>\n" +
" <ram:GuidelineSpecifiedDocumentContextParameter>\n" +
" <ram:ID>urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended</ram:ID>\n" +
" </ram:GuidelineSpecifiedDocumentContextParameter>\n" +
" </rsm:ExchangedDocumentContext>\n" +
" <rsm:ExchangedDocument>\n" +
" <ram:ID>471102</ram:ID>\n" +
" <ram:TypeCode>380</ram:TypeCode>\n" +
" <ram:IssueDateTime>\n" +
" <udt:DateTimeString format=\"102\">20260201</udt:DateTimeString>\n" +
" </ram:IssueDateTime>\n" +
" </rsm:ExchangedDocument>\n" +
" <rsm:SupplyChainTradeTransaction>\n" +
" <ram:IncludedSupplyChainTradeLineItem>\n" +
" <ram:AssociatedDocumentLineDocument>\n" +
" <ram:LineID>1</ram:LineID>\n" +
" </ram:AssociatedDocumentLineDocument>\n" +
" <ram:SpecifiedTradeProduct>\n" +
" <ram:Name>Trennblätter A4</ram:Name>\n" +
" </ram:SpecifiedTradeProduct>\n" +
" <ram:SpecifiedLineTradeAgreement>\n" +
" <ram:NetPriceProductTradePrice>\n" +
" <ram:ChargeAmount>9.9000</ram:ChargeAmount>\n" +
" <ram:BasisQuantity unitCode=\"H87\">1.0000</ram:BasisQuantity>\n" +
" </ram:NetPriceProductTradePrice>\n" +
" </ram:SpecifiedLineTradeAgreement>\n" +
" <ram:SpecifiedLineTradeDelivery>\n" +
" <ram:BilledQuantity unitCode=\"H87\">20.0000</ram:BilledQuantity>\n" +
" </ram:SpecifiedLineTradeDelivery>\n" +
" <ram:SpecifiedLineTradeSettlement>\n" +
" <ram:ApplicableTradeTax>\n" +
" <ram:TypeCode>VAT</ram:TypeCode>\n" +
" <ram:CategoryCode>S</ram:CategoryCode>\n" +
" <ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>\n" +
" </ram:ApplicableTradeTax>\n" +
" <ram:SpecifiedTradeSettlementLineMonetarySummation>\n" +
" <ram:LineTotalAmount>198.00</ram:LineTotalAmount>\n" +
" </ram:SpecifiedTradeSettlementLineMonetarySummation>\n" +
" </ram:SpecifiedLineTradeSettlement>\n" +
" </ram:IncludedSupplyChainTradeLineItem>\n" +
" <ram:IncludedSupplyChainTradeLineItem>\n" +
" <ram:AssociatedDocumentLineDocument>\n" +
" <ram:LineID>2</ram:LineID>\n" +
" </ram:AssociatedDocumentLineDocument>\n" +
" <ram:SpecifiedTradeProduct>\n" +
" <ram:Name>Joghurt Banane</ram:Name>\n" +
" </ram:SpecifiedTradeProduct>\n" +
" <ram:SpecifiedLineTradeAgreement>\n" +
" <ram:NetPriceProductTradePrice>\n" +
" <ram:ChargeAmount>5.5000</ram:ChargeAmount>\n" +
" <ram:BasisQuantity unitCode=\"H87\">1.0000</ram:BasisQuantity>\n" +
" </ram:NetPriceProductTradePrice>\n" +
" </ram:SpecifiedLineTradeAgreement>\n" +
" <ram:SpecifiedLineTradeDelivery>\n" +
" <ram:BilledQuantity unitCode=\"H87\">50.0000</ram:BilledQuantity>\n" +
" </ram:SpecifiedLineTradeDelivery>\n" +
" <ram:SpecifiedLineTradeSettlement>\n" +
" <ram:ApplicableTradeTax>\n" +
" <ram:TypeCode>VAT</ram:TypeCode>\n" +
" <ram:CategoryCode>S</ram:CategoryCode>\n" +
" <ram:RateApplicablePercent>7.00</ram:RateApplicablePercent>\n" +
" </ram:ApplicableTradeTax>\n" +
" <ram:SpecifiedTradeSettlementLineMonetarySummation>\n" +
" <ram:LineTotalAmount>275.00</ram:LineTotalAmount>\n" +
" </ram:SpecifiedTradeSettlementLineMonetarySummation>\n" +
" </ram:SpecifiedLineTradeSettlement>\n" +
" </ram:IncludedSupplyChainTradeLineItem>\n" +
" <ram:ApplicableHeaderTradeAgreement>\n" +
" <ram:SellerTradeParty>\n" +
" <ram:Name>Lieferant GmbH</ram:Name>\n" +
" <ram:PostalTradeAddress>\n" +
" <ram:PostcodeCode>80333</ram:PostcodeCode>\n" +
" <ram:LineOne>Lieferantenstraße 20</ram:LineOne>\n" +
" <ram:CityName>München</ram:CityName>\n" +
" <ram:CountryID>DE</ram:CountryID>\n" +
" </ram:PostalTradeAddress>\n" +
" <ram:SpecifiedTaxRegistration>\n" +
" <ram:ID schemeID=\"VA\">DE123456789</ram:ID>\n" +
" </ram:SpecifiedTaxRegistration>\n" +
" <ram:SpecifiedTaxRegistration>\n" +
" <ram:ID schemeID=\"FC\">201/113/40209</ram:ID>\n" +
" </ram:SpecifiedTaxRegistration>\n" +
" </ram:SellerTradeParty>\n" +
" <ram:BuyerTradeParty>\n" +
" <ram:Name>Kunden AG Mitte</ram:Name>\n" +
" <ram:PostalTradeAddress>\n" +
" <ram:PostcodeCode>69876</ram:PostcodeCode>\n" +
" <ram:LineOne>Kundenstraße 15</ram:LineOne>\n" +
" <ram:CityName>Frankfurt</ram:CityName>\n" +
" <ram:CountryID>DE</ram:CountryID>\n" +
" </ram:PostalTradeAddress>\n" +
" </ram:BuyerTradeParty>\n" +
" </ram:ApplicableHeaderTradeAgreement>\n" +
" <ram:ApplicableHeaderTradeDelivery>\n" +
" <ram:ActualDeliverySupplyChainEvent>\n" +
" <ram:OccurrenceDateTime>\n" +
" <udt:DateTimeString format=\"102\">20180304</udt:DateTimeString>\n" +
" </ram:OccurrenceDateTime>\n" +
" </ram:ActualDeliverySupplyChainEvent>\n" +
" </ram:ApplicableHeaderTradeDelivery>\n" +
" <ram:ApplicableHeaderTradeSettlement>\n" +
" <ram:PaymentReference>471102</ram:PaymentReference>\n" +
" <ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>\n" +
" <ram:ApplicableTradeTax>\n" +
" <ram:CalculatedAmount>19.25</ram:CalculatedAmount>\n" +
" <ram:TypeCode>VAT</ram:TypeCode>\n" +
" <ram:BasisAmount>275.00</ram:BasisAmount>\n" +
" <ram:CategoryCode>S</ram:CategoryCode>\n" +
" <ram:RateApplicablePercent>7.00</ram:RateApplicablePercent>\n" +
" </ram:ApplicableTradeTax>\n" +
" <ram:ApplicableTradeTax>\n" +
" <ram:CalculatedAmount>37.62</ram:CalculatedAmount>\n" +
" <ram:TypeCode>VAT</ram:TypeCode>\n" +
" <ram:BasisAmount>198.00</ram:BasisAmount>\n" +
" <ram:CategoryCode>S</ram:CategoryCode>\n" +
" <ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>\n" +
" </ram:ApplicableTradeTax>\n" +
" <ram:SpecifiedTradePaymentTerms>\n" +
" <ram:Description>Please remit until 04.03.2018</ram:Description>\n" +
" <ram:DueDateDateTime>\n" +
" <udt:DateTimeString format=\"102\">20180304</udt:DateTimeString>\n" +
" </ram:DueDateDateTime>\n" +
" </ram:SpecifiedTradePaymentTerms>\n" +
" <ram:SpecifiedTradePaymentTerms>\n" +
" <ram:Description>Bis zum 15.02.2026 erhalten Sie 4,000 % Skonto, 21.19 EUR</ram:Description>\n" +
" <ram:DueDateDateTime>\n" +
" <udt:DateTimeString format=\"102\">20260215</udt:DateTimeString>\n" +
" </ram:DueDateDateTime>\n" +
" <ram:ApplicableTradePaymentDiscountTerms>\n" +
" <ram:BasisAmount>529.8700</ram:BasisAmount>\n" +
" <ram:CalculationPercent>4.00</ram:CalculationPercent>\n" +
" <ram:ActualDiscountAmount>21.19</ram:ActualDiscountAmount>\n" +
" </ram:ApplicableTradePaymentDiscountTerms>\n" +
" </ram:SpecifiedTradePaymentTerms>\n" +
" <ram:SpecifiedTradePaymentTerms>\n" +
" <ram:Description>Bis zum 16.02.2026 ohne Abzug</ram:Description>\n" +
" <ram:DueDateDateTime>\n" +
" <udt:DateTimeString format=\"102\">20260216</udt:DateTimeString>\n" +
" </ram:DueDateDateTime>\n" +
" </ram:SpecifiedTradePaymentTerms>\n" +
" <ram:SpecifiedTradeSettlementHeaderMonetarySummation>\n" +
" <ram:LineTotalAmount>473.00</ram:LineTotalAmount>\n" +
" <ram:ChargeTotalAmount>0.00</ram:ChargeTotalAmount>\n" +
" <ram:AllowanceTotalAmount>0.00</ram:AllowanceTotalAmount>\n" +
" <ram:TaxBasisTotalAmount>473.00</ram:TaxBasisTotalAmount>\n" +
" <ram:TaxTotalAmount currencyID=\"EUR\">56.87</ram:TaxTotalAmount>\n" +
" <ram:GrandTotalAmount>529.87</ram:GrandTotalAmount>\n" +
" <ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount>\n" +
" <ram:DuePayableAmount>529.87</ram:DuePayableAmount>\n" +
" </ram:SpecifiedTradeSettlementHeaderMonetarySummation>\n" +
" </ram:ApplicableHeaderTradeSettlement>\n" +
" </rsm:SupplyChainTradeTransaction>\n" +
" </rsm:CrossIndustryInvoice>";
public void testCashDiscount() throws XPathExpressionException, ParseException {
InputStream inputStream = new ByteArrayInputStream(XMLString.getBytes(StandardCharsets.UTF_8));
ZUGFeRDInvoiceImporter zii = new ZUGFeRDInvoiceImporter(inputStream);
Invoice invoice = zii.extractInvoice();
assertEquals(new BigDecimal("4.00"), invoice.getCashDiscounts()[0].getPercent());
}
}

View File

@@ -23,7 +23,11 @@ import junit.framework.TestSuite;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.apache.pdfbox.pdmodel.common.PDMetadata; import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException; import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException;
import org.apache.xmpbox.XMPMetadata; import org.apache.xmpbox.XMPMetadata;
import org.apache.xmpbox.schema.PDFAIdentificationSchema; import org.apache.xmpbox.schema.PDFAIdentificationSchema;
@@ -218,8 +222,6 @@ public class MustangReaderWriterTest extends MustangReaderTestCase {
zi.doRecalculateItemPricesFromLineTotals(); zi.doRecalculateItemPricesFromLineTotals();
zi.doIgnoreCalculationErrors(); zi.doIgnoreCalculationErrors();
zi.setInputStream(inputStream); zi.setInputStream(inputStream);
// Reading ZUGFeRD
String amount = zi.getAmount();
assertEquals("<?xpacket begin=\"\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n" + assertEquals("<?xpacket begin=\"\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n" +
" <x:xmpmeta xmlns:x=\"adobe:ns:meta/\">\n" + " <x:xmpmeta xmlns:x=\"adobe:ns:meta/\">\n" +
@@ -297,8 +299,8 @@ public class MustangReaderWriterTest extends MustangReaderTestCase {
" </x:xmpmeta>\n" + " </x:xmpmeta>\n" +
"<?xpacket end=\"r\"?>", zi.getXMP()); "<?xpacket end=\"r\"?>", zi.getXMP());
// this resembles the data written in MustangReaderWriterCustomXMLTest // this resembles the data written in MustangReaderWriterCustomXMLTest
assertEquals(amount, "1005.55"); assertEquals("845.00", zi.getTaxBasisTotalAmount());
assertEquals("1005.55", zi.getAmount());
} }
@@ -415,10 +417,11 @@ public class MustangReaderWriterTest extends MustangReaderTestCase {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
ze.export(baos); ze.export(baos);
ze.close(); ze.close();
String pdfContent = baos.toString(StandardCharsets.UTF_8); byte[] targetBytes = baos.toByteArray();
assertFalse(pdfContent.indexOf(DocumentContextParameterTypeConstants.BASIC) >= 0); String xmlContent = readEmbeddedXmlFromPdf(targetBytes, "ZUGFeRD-invoice.xml");
assertFalse(pdfContent.indexOf(DocumentContextParameterTypeConstants.EXTENDED) >= 0); assertFalse(xmlContent.contains(DocumentContextParameterTypeConstants.BASIC));
assertTrue(pdfContent.indexOf(DocumentContextParameterTypeConstants.COMFORT) >= 0); assertFalse(xmlContent.contains(DocumentContextParameterTypeConstants.EXTENDED));
assertTrue(xmlContent.contains(DocumentContextParameterTypeConstants.COMFORT));
} catch (IOException e) { } catch (IOException e) {
fail("IOException should not happen in testZExport"); fail("IOException should not happen in testZExport");
@@ -442,7 +445,8 @@ public class MustangReaderWriterTest extends MustangReaderTestCase {
result.write(buffer, 0, length); result.write(buffer, 0, length);
} }
ze.addAdditionalFile("test.pdf", result.toByteArray()); var bytes = result.toByteArray();
ze.addAdditionalFile("test.pdf", bytes);
ze.setTransaction(this); ze.setTransaction(this);
ze.disableAutoClose(true); ze.disableAutoClose(true);
@@ -451,12 +455,14 @@ public class MustangReaderWriterTest extends MustangReaderTestCase {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
ze.export(baos); ze.export(baos);
ze.close(); ze.close();
String pdfContent = baos.toString(StandardCharsets.UTF_8); byte[] targetBytes = baos.toByteArray();
assertFalse(pdfContent.indexOf("(via mustangproject.org") == -1); String pdfContent = new String(targetBytes, StandardCharsets.UTF_8);
String xmlContent = readEmbeddedXmlFromPdf(targetBytes, "factur-x.xml");
assertTrue(pdfContent.contains("(via mustangproject.org"));
// check for pdf-a schema extension // check for pdf-a schema extension
assertFalse(pdfContent.indexOf("<fx:ConformanceLevel>EN 16931</fx:ConformanceLevel>") == -1); assertTrue(pdfContent.contains("<fx:ConformanceLevel>EN 16931</fx:ConformanceLevel>"));
assertFalse(pdfContent.indexOf("<pdfaSchema:prefix>fx</pdfaSchema:prefix>") == -1); assertTrue(pdfContent.contains("<pdfaSchema:prefix>fx</pdfaSchema:prefix>"));
assertFalse(pdfContent.indexOf("urn:cen.eu:en16931:2017") == -1); assertTrue(xmlContent.contains("urn:cen.eu:en16931:2017"));
} }
// now check the contents (like MustangReaderTest) // now check the contents (like MustangReaderTest)
@@ -470,6 +476,26 @@ public class MustangReaderWriterTest extends MustangReaderTestCase {
assertEquals(getNumber(), zi.getForeignReference()); assertEquals(getNumber(), zi.getForeignReference());
} }
private static String readEmbeddedXmlFromPdf(byte[] targetBytes, String xmlFilename) throws IOException {
// Read attached (compressed) XML file
try (PDDocument pdDocument = Loader.loadPDF(targetBytes)) {
PDDocumentNameDictionary names = new PDDocumentNameDictionary(pdDocument.getDocumentCatalog());
PDEmbeddedFilesNameTreeNode embeddedFiles = names.getEmbeddedFiles();
Map<String, PDComplexFileSpecification> fileMap = embeddedFiles.getNames();
PDComplexFileSpecification fileSpec = fileMap.get(xmlFilename);
if (fileSpec == null) {
fail("Files does not contain a file named " + xmlFilename);
return null;
}
PDEmbeddedFile embeddedFile = fileSpec.getEmbeddedFile();
String xmlContent = new String(embeddedFile.toByteArray(), StandardCharsets.UTF_8);
return xmlContent;
}
}
public void testExceptionOnPDF14() { public void testExceptionOnPDF14() {
final String TARGET_PDF = "./target/testout-MustangGnuaccountingBeispielRE-20170509_505new.pdf"; final String TARGET_PDF = "./target/testout-MustangGnuaccountingBeispielRE-20170509_505new.pdf";

View File

@@ -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("<ram:ParentLineID>01</ram:ParentLineID>"),
"XML must contain ParentLineID element");
assertTrue(xml.contains("<ram:LineStatusReasonCode>DETAIL</ram:LineStatusReasonCode>"),
"XML must contain DETAIL LineStatusReasonCode");
assertTrue(xml.contains("<ram:LineStatusReasonCode>GROUP</ram:LineStatusReasonCode>"),
"XML must contain GROUP LineStatusReasonCode");
assertTrue(xml.contains("<ram:LineStatusReasonCode>INFORMATION</ram:LineStatusReasonCode>"),
"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;
}
}

View File

@@ -32,8 +32,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;
@FixMethodOrder(MethodSorters.NAME_ASCENDING) @FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class VisualizationTest extends ResourceCase { public class VisualizationTest extends ResourceCase {
@@ -98,13 +96,8 @@ public class VisualizationTest extends ResourceCase {
public void testPDFVisualizationCII() { public void testPDFVisualizationCII() {
File CIIinputFile = getResourceAsFile("cii/01.01a-INVOICE.cii.xml"); File CIIinputFile = getResourceAsFile("cii/01.01a-INVOICE.cii.xml");
// the writing part
String expected = null;
String result = null;
try { try {
ZUGFeRDVisualizer zvi = new ZUGFeRDVisualizer(); ZUGFeRDVisualizer zvi = new ZUGFeRDVisualizer();
zvi.toPDF(CIIinputFile.getAbsolutePath(), TARGET_PDF_CII); zvi.toPDF(CIIinputFile.getAbsolutePath(), TARGET_PDF_CII);
@@ -116,6 +109,47 @@ public class VisualizationTest extends ResourceCase {
try { try {
assertTrue(ByteArraySearcher.startsWith(Files.readAllBytes(Paths.get(TARGET_PDF_CII)), new byte[]{'%', 'P', 'D', 'F'})); assertTrue(ByteArraySearcher.startsWith(Files.readAllBytes(Paths.get(TARGET_PDF_CII)), new byte[]{'%', 'P', 'D', 'F'}));
assertTrue(ByteArraySearcher.contains(Files.readAllBytes(Paths.get(TARGET_PDF_CII)), "<rdf:li>de</rdf:li>".getBytes()));
} catch (IOException e) {
fail("IOException should not occur");
}
}
public void testPDFVisualizationCIIEnglish() {
File CIIinputFile = getResourceAsFile("cii/01.01a-INVOICE.cii.xml");
try {
ZUGFeRDVisualizer zvi = new ZUGFeRDVisualizer();
zvi.toPDF(CIIinputFile.getAbsolutePath(), TARGET_PDF_CII, Language.EN);
} catch (UnsupportedOperationException e) {
fail("UnsupportedOperationException should not happen: " + e.getMessage());
} catch (IllegalArgumentException e) {
fail("IllegalArgumentException should not happen: " + e.getMessage());
}
try {
assertTrue(ByteArraySearcher.startsWith(Files.readAllBytes(Paths.get(TARGET_PDF_CII)), new byte[]{'%', 'P', 'D', 'F'}));
assertTrue(ByteArraySearcher.contains(Files.readAllBytes(Paths.get(TARGET_PDF_CII)), "<rdf:li>en</rdf:li>".getBytes()));
} catch (IOException e) {
fail("IOException should not occur");
}
}
public void testPDFVisualizationCIIFrench() {
File CIIinputFile = getResourceAsFile("cii/01.01a-INVOICE.cii.xml");
try {
ZUGFeRDVisualizer zvi = new ZUGFeRDVisualizer();
zvi.toPDF(CIIinputFile.getAbsolutePath(), TARGET_PDF_CII, Language.FR);
} catch (UnsupportedOperationException e) {
fail("UnsupportedOperationException should not happen: " + e.getMessage());
} catch (IllegalArgumentException e) {
fail("IllegalArgumentException should not happen: " + e.getMessage());
}
try {
assertTrue(ByteArraySearcher.startsWith(Files.readAllBytes(Paths.get(TARGET_PDF_CII)), new byte[]{'%', 'P', 'D', 'F'}));
assertTrue(ByteArraySearcher.contains(Files.readAllBytes(Paths.get(TARGET_PDF_CII)), "<rdf:li>fr</rdf:li>".getBytes()));
} catch (IOException e) { } catch (IOException e) {
fail("IOException should not occur"); fail("IOException should not occur");
} }

View File

@@ -546,7 +546,7 @@ public class ZF2PushTest extends TestCase {
.addItem(new Item(new Product("Testprodukt", "", "H87", new BigDecimal(19)), price, new BigDecimal(1.0))) .addItem(new Item(new Product("Testprodukt", "", "H87", new BigDecimal(19)), price, new BigDecimal(1.0)))
.addItem(new Item(new Product("Testprodukt", "", "H87", new BigDecimal(19)), price, new BigDecimal(1.0))) .addItem(new Item(new Product("Testprodukt", "", "H87", new BigDecimal(19)), price, new BigDecimal(1.0)))
.addCharge(new Charge(new BigDecimal(0.5)).setTaxPercent(new BigDecimal(19)).setReasonCode("ABK")) .addCharge(new Charge(new BigDecimal(0.5)).setTaxPercent(new BigDecimal(19)).setReasonCode("ABK"))
.addAllowance(new Allowance(new BigDecimal(0.2)).setTaxPercent(new BigDecimal(19)).setReasonCode("ABK")) .addAllowance(new Allowance(new BigDecimal(0.2)).setTaxPercent(new BigDecimal(19)).setReasonCode("95"))
); );
String theXML = new String(ze.getProvider().getXML(), StandardCharsets.UTF_8); String theXML = new String(ze.getProvider().getXML(), StandardCharsets.UTF_8);
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice")); assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
@@ -600,7 +600,7 @@ public class ZF2PushTest extends TestCase {
ReferencedDocument dr1=new ReferencedDocument("90-kl-98798-C", sdf.parse("2025-10-12")); ReferencedDocument dr1=new ReferencedDocument("90-kl-98798-C", sdf.parse("2025-10-12"));
ReferencedDocument dr2=new ReferencedDocument("90-kl-98798-C1", sdf.parse("2025-10-13")); ReferencedDocument dr2=new ReferencedDocument("90-kl-98798-C1", sdf.parse("2025-10-13"));
dr2.setReferenceTypeCode("AAG"); dr2.setReferenceTypeCode("AAG");
ze.setTransaction(new Invoice().setCurrency("CHF").addNote("document level 1/2").addNote("document level 2/2").setDueDate(new Date()).setIssueDate(new Date()).setDeliveryDate(new Date()).setPaymentReference("Verwendungszweck").setDocumentName("Rechnung") ze.setTransaction(new Invoice().setTestIndicator().setCurrency("CHF").addNote("document level 1/2").addNote("document level 2/2").setDueDate(new Date()).setIssueDate(new Date()).setDeliveryDate(new Date()).setPaymentReference("Verwendungszweck").setDocumentName("Rechnung")
.setSellerOrderReferencedDocumentID("9384").setBuyerOrderReferencedDocumentID("28934") .setSellerOrderReferencedDocumentID("9384").setBuyerOrderReferencedDocumentID("28934")
.setDetailedDeliveryPeriod(new SimpleDateFormat("yyyyMMdd").parse(occurrenceFrom), new SimpleDateFormat("yyyyMMdd").parse(occurrenceTo)) .setDetailedDeliveryPeriod(new SimpleDateFormat("yyyyMMdd").parse(occurrenceFrom), new SimpleDateFormat("yyyyMMdd").parse(occurrenceTo))
.setSender(new TradeParty(orgname, "teststr", "55232", "teststadt", "DE").addTaxID(taxID).setEmail("sender@test.org").setID(orgID).addVATID("DE0815")) .setSender(new TradeParty(orgname, "teststr", "55232", "teststadt", "DE").addTaxID(taxID).setEmail("sender@test.org").setID(orgID).addVATID("DE0815"))
@@ -630,6 +630,7 @@ public class ZF2PushTest extends TestCase {
String theXML = new String(ze.getProvider().getXML(), StandardCharsets.UTF_8); String theXML = new String(ze.getProvider().getXML(), StandardCharsets.UTF_8);
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice")); assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));
assertTrue(theXML.contains("<ram:TestIndicator"));
ze.export(TARGET_PUSHEDGE); ze.export(TARGET_PUSHEDGE);
} catch (IOException e) { } catch (IOException e) {
fail("IOException should not be raised"); fail("IOException should not be raised");

View File

@@ -71,7 +71,7 @@
<version.commons-io>2.20.0</version.commons-io><!-- from 2.16.1 --> <version.commons-io>2.20.0</version.commons-io><!-- from 2.16.1 -->
<version.jakarta.xml.bind>4.0.2</version.jakarta.xml.bind> <version.jakarta.xml.bind>4.0.2</version.jakarta.xml.bind>
<version.net.sf.saxon>12.8</version.net.sf.saxon><!-- from 12.4 --> <version.net.sf.saxon>12.8</version.net.sf.saxon><!-- from 12.4 -->
<version.org.apache.pdfbox>3.0.6</version.org.apache.pdfbox> <version.org.apache.pdfbox>3.0.7</version.org.apache.pdfbox>
<version.org.apache.xmlgraphics>2.11</version.org.apache.xmlgraphics><!-- from 2.10 --> <version.org.apache.xmlgraphics>2.11</version.org.apache.xmlgraphics><!-- from 2.10 -->
<version.org.codehaus.janino>3.1.12</version.org.codehaus.janino><!-- from 3.1.7 --> <version.org.codehaus.janino>3.1.12</version.org.codehaus.janino><!-- from 3.1.7 -->
<version.org.dom4j>2.1.5</version.org.dom4j><!-- from 2.1.4 --> <version.org.dom4j>2.1.5</version.org.dom4j><!-- from 2.1.4 -->

View File

@@ -14,7 +14,6 @@ import java.util.HashMap;
import java.util.Set; import java.util.Set;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath; import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathConstants;
@@ -23,6 +22,7 @@ import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory; import javax.xml.xpath.XPathFactory;
import org.mustangproject.util.ByteArraySearcher; import org.mustangproject.util.ByteArraySearcher;
import org.mustangproject.XMLTools;
import org.mustangproject.ZUGFeRD.ZUGFeRDImporter; import org.mustangproject.ZUGFeRD.ZUGFeRDImporter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -127,7 +127,7 @@ public class PDFValidator extends Validator {
final ZUGFeRDImporter zi = new ZUGFeRDImporter(); final ZUGFeRDImporter zi = new ZUGFeRDImporter();
zi.doIgnoreCalculationErrors();//of course the calculation will still be schematron checked zi.doIgnoreCalculationErrors();//of course the calculation will still be schematron checked
zi.setInputStream(inputStream); zi.setInputStream(inputStream);
final String xmp = zi.getXMP(); final String xmp = zi.getXMP().substring(zi.getXMP().indexOf('<'));
final Document docXMP; final Document docXMP;
@@ -142,12 +142,7 @@ public class PDFValidator extends Validator {
* <zf:Version>1.0</zf:Version> * <zf:Version>1.0</zf:Version>
*/ */
try { try {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); final DocumentBuilder builder = XMLTools.getDocumentBuilder(false);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
factory.setXIncludeAware(false);
final DocumentBuilder builder = factory.newDocumentBuilder();
final InputSource is = new InputSource(new StringReader(xmp)); final InputSource is = new InputSource(new StringReader(xmp));
docXMP = builder.parse(is); docXMP = builder.parse(is);

View File

@@ -7,9 +7,8 @@ import java.net.URL;
import javax.xml.XMLConstants; import javax.xml.XMLConstants;
import javax.xml.transform.Source; import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource; import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.mustangproject.XMLTools;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
@@ -59,11 +58,8 @@ public abstract class Validator {
protected void validateSchema(byte[] xmlRawData, String schemaPath, int section, EPart part) throws IrrecoverableValidationError { protected void validateSchema(byte[] xmlRawData, String schemaPath, int section, EPart part) throws IrrecoverableValidationError {
URL schemaFile = Thread.currentThread().getContextClassLoader().getResource("schema/" + schemaPath); URL schemaFile = Thread.currentThread().getContextClassLoader().getResource("schema/" + schemaPath);
Source xmlData = new StreamSource(new ByteArrayInputStream(xmlRawData)); Source xmlData = new StreamSource(new ByteArrayInputStream(xmlRawData));
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
try { try {
schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); javax.xml.validation.Validator validator = XMLTools.getValidator(schemaFile);
Schema schema = schemaFactory.newSchema(schemaFile);
javax.xml.validation.Validator validator = schema.newValidator();
validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
validator.validate(xmlData); validator.validate(xmlData);
} catch (SAXException e) { } catch (SAXException e) {

View File

@@ -13,9 +13,7 @@ import java.text.ParseException;
import java.util.Calendar; import java.util.Calendar;
import java.util.Set; import java.util.Set;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.stream.StreamSource; import javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPath; import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathConstants;
@@ -156,26 +154,7 @@ public class XMLValidator extends Validator {
* *
*/ */
final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); final DocumentBuilder db = XMLTools.getDocumentBuilder(true);
//REDHAT
//https://www.blackhat.com/docs/us-15/materials/us-15-Wang-FileCry-The-New-Age-Of-XXE-java-wp.pdf
dbf.setAttribute(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
//OWASP
//https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Disable external DTDs as well
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
// and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
dbf.setNamespaceAware(true);
final DocumentBuilder db = dbf.newDocumentBuilder();
final InputSource is = new InputSource(new StringReader(zfXML)); final InputSource is = new InputSource(new StringReader(zfXML));
final Document doc = db.parse(is); final Document doc = db.parse(is);
@@ -213,8 +192,10 @@ public class XMLValidator extends Validator {
boolean isExtended = false; boolean isExtended = false;
boolean isXRechnung = false; boolean isXRechnung = false;
String currentZFVersionDir = "ZF_240"; String currentZFVersionDir = "ZF_240";
String currentXPZ12VersionDir ="XP_Z12_012";
int mainSchematronSectionErrorTypeCode = 4; int mainSchematronSectionErrorTypeCode = 4;
String xsltFilename = null; String xsltFilename = null;
boolean runFrenchCiiSchematron = false;
// urn:ferd:CrossIndustryDocument:invoice:1p0:extended, // urn:ferd:CrossIndustryDocument:invoice:1p0:extended,
// urn:ferd:CrossIndustryDocument:invoice:1p0:comfort, // urn:ferd:CrossIndustryDocument:invoice:1p0:comfort,
// urn:ferd:CrossIndustryDocument:invoice:1p0:basic, // urn:ferd:CrossIndustryDocument:invoice:1p0:basic,
@@ -234,6 +215,11 @@ public class XMLValidator extends Validator {
} else if (root.getLocalName().equalsIgnoreCase("CrossIndustryInvoice")) { // ZUGFeRD 2.0 or Factur-X } else if (root.getLocalName().equalsIgnoreCase("CrossIndustryInvoice")) { // ZUGFeRD 2.0 or Factur-X
context.setGeneration("2"); context.setGeneration("2");
final String sellerCountry = getXPathString(doc,
"//*[local-name() = 'CrossIndustryInvoice']//*[local-name() = 'SupplyChainTradeTransaction']//*[local-name() = 'ApplicableHeaderTradeAgreement']//*[local-name() = 'SellerTradeParty']//*[local-name() = 'PostalTradeAddress']/*[local-name() = 'CountryID']/text()");
final String buyerCountry = getXPathString(doc,
"//*[local-name() = 'CrossIndustryInvoice']//*[local-name() = 'SupplyChainTradeTransaction']//*[local-name() = 'ApplicableHeaderTradeAgreement']//*[local-name() = 'BuyerTradeParty']//*[local-name() = 'PostalTradeAddress']/*[local-name() = 'CountryID']/text()");
runFrenchCiiSchematron = "FR".equalsIgnoreCase(sellerCountry) && "FR".equalsIgnoreCase(buyerCountry);
isMiniumum = contextProfile.contains("minimum"); isMiniumum = contextProfile.contains("minimum");
isBasic = contextProfile.contains("basic"); isBasic = contextProfile.contains("basic");
@@ -391,6 +377,11 @@ public class XMLValidator extends Validator {
// main schematron validation // main schematron validation
validateSchematron(zfXML, xsltFilename, mainSchematronSectionErrorTypeCode, ESeverity.error); validateSchematron(zfXML, xsltFilename, mainSchematronSectionErrorTypeCode, ESeverity.error);
if (runFrenchCiiSchematron) {
String xsltFRFilename = "/xslt/" + currentXPZ12VersionDir + "/20260216_BR-FR-Flux2-Schematron-CII_V1.3.0.xsl";
validateSchematron(zfXML, xsltFRFilename, mainSchematronSectionErrorTypeCode, ESeverity.error);
}
} }
if ("CII".equals(context.getFormat()) && ("2".equals(context.getGeneration()))) { if ("CII".equals(context.getFormat()) && ("2".equals(context.getGeneration()))) {
@@ -546,6 +537,16 @@ public class XMLValidator extends Validator {
} }
} }
private String getXPathString(Document doc, String expression) throws IrrecoverableValidationError {
try {
XPath xpath = XPathFactory.newInstance().newXPath();
String value = (String) xpath.compile(expression).evaluate(doc, XPathConstants.STRING);
return value == null ? "" : value.trim();
} catch (XPathExpressionException e) {
throw new IrrecoverableValidationError(e.getMessage());
}
}
public void validateXR(String xml, ESeverity errorImpact) throws IrrecoverableValidationError { public void validateXR(String xml, ESeverity errorImpact) throws IrrecoverableValidationError {
//Guideline ID=urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_1.2 or //Guideline ID=urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_1.2 or

View File

@@ -17,9 +17,7 @@ import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.dom4j.DocumentException; import org.dom4j.DocumentException;
@@ -143,24 +141,7 @@ public class ZUGFeRDValidator {
boolean isXML = false; boolean isXML = false;
String xmlAsString = null; String xmlAsString = null;
try { try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = XMLTools.getDocumentBuilder(true);
//REDHAT
//https://www.blackhat.com/docs/us-15/materials/us-15-Wang-FileCry-The-New-Age-Of-XXE-java-wp.pdf
dbf.setAttribute(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
//OWASP
//https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Disable external DTDs as well
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
// and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
content = XMLTools.removeBOM(content); content = XMLTools.removeBOM(content);
xmlAsString = new String(content, StandardCharsets.UTF_8); xmlAsString = new String(content, StandardCharsets.UTF_8);

View File

@@ -331,4 +331,29 @@ public class LibraryTest extends ResourceCase {
.isEqualTo(0); .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);
}
} }

View File

@@ -179,6 +179,15 @@ public class PDFValidatorTest extends ResourceCase {
assertEquals(true, actual assertEquals(true, actual
.contains("<error type=\"28\">XMP Metadata: Could not parse XMP metadata (XML invalid)</error>")); .contains("<error type=\"28\">XMP Metadata: Could not parse XMP metadata (XML invalid)</error>"));
contents = getResourceAsByteArray("103767-Invoice-2026-03-24-1.pdf");
pv.setFilenameAndContents("103767-Invoice-2026-03-24-1.pdf", contents);
vc.clear();
pv.validate();
actual = pv.getXMLResult();
assertEquals(false, actual.contains("<error"));
} catch (final IrrecoverableValidationError e) { } catch (final IrrecoverableValidationError e) {
// ignore, will be in XML output anyway // ignore, will be in XML output anyway
} }

View File

@@ -337,6 +337,33 @@ public class XMLValidatorTest extends ResourceCase {
} }
public void testFrenchSchematronValidation() {
final ValidationContext ctx = new ValidationContext(null);
final XMLValidator xv = new XMLValidator(ctx);
File tempFile = getResourceAsFile("validV2FR.xml");
try {
xv.setFilename(tempFile.getAbsolutePath());
xv.validate();
String s = "<validation>" + xv.getXMLResult() + "</validation>";
assertThat(s).valueByXPath("count(//error)")
.asInt()
.isEqualTo(0);
assertThat(s).valueByXPath("count(//warning)")
.asInt()
.isEqualTo(12);
assertThat(s).valueByXPath("count(//warning[contains(text(),'XP_Z12_012')])")
.asInt()
.isEqualTo(9);
assertThat(s).valueByXPath("/validation/summary/@status")
.asString()
.isEqualTo("valid");
} catch (final IrrecoverableValidationError e) {
fail(e.getMessage());
}
}
public void testUBLValidation() { public void testUBLValidation() {
ValidationContext ctx = new ValidationContext(null); ValidationContext ctx = new ValidationContext(null);

View File

@@ -261,4 +261,27 @@ public class ZUGFeRDValidatorTest extends ResourceCase {
.isEqualTo("valid"); .isEqualTo("valid");
} }
public void testXMLFRValidation() {
File tempFile = getResourceAsFile("validV2FR.xml");
ZUGFeRDValidator zfv = new ZUGFeRDValidator();
String res = zfv.validate(tempFile.getAbsolutePath());
assertThat(res).valueByXPath("count(//error)")
.asInt()
.isEqualTo(0);
assertThat(res).valueByXPath("count(//warning)")
.asInt()
.isEqualTo(12); // 3 empty-element warnings plus 9 French schematron warnings
assertThat(res).valueByXPath("count(//notice)")
.asInt()
.isEqualTo(9); // 9 notices RE XRechnung 3.0
assertThat(res).valueByXPath("/validation/summary/@status")
.asString()
.isEqualTo("valid");// expect to be valid because XR notices are, well, only notices
assertThat(res).valueByXPath("/validation/xml/summary/@status")
.asString()
.isEqualTo("valid");
}
} }

View File

@@ -0,0 +1,186 @@
<?xml version='1.0' encoding="UTF-8" standalone="yes"?>
<rsm:CrossIndustryInvoice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>FAC-20171118-506</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime><udt:DateTimeString format="102">20171118</udt:DateTimeString></ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>1</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name>Prestation graphique</ram:Name>
<ram:Description></ram:Description>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:GrossPriceProductTradePrice>
<ram:ChargeAmount>160.0000</ram:ChargeAmount>
<ram:BasisQuantity unitCode="HUR">1.0000</ram:BasisQuantity>
</ram:GrossPriceProductTradePrice>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>160.0000</ram:ChargeAmount>
<ram:BasisQuantity unitCode="HUR">1.0000</ram:BasisQuantity>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="HUR">1.0000</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>7.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>160.00</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>2</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name>Ballon decoratif 500 ml</ram:Name>
<ram:Description></ram:Description>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:GrossPriceProductTradePrice>
<ram:ChargeAmount>0.7900</ram:ChargeAmount>
<ram:BasisQuantity unitCode="C62">1.0000</ram:BasisQuantity>
</ram:GrossPriceProductTradePrice>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>0.7900</ram:ChargeAmount>
<ram:BasisQuantity unitCode="C62">1.0000</ram:BasisQuantity>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="C62">400.0000</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>316.00</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>3</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name>Helium par litre</ram:Name>
<ram:Description></ram:Description>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:GrossPriceProductTradePrice>
<ram:ChargeAmount>0.1000</ram:ChargeAmount>
<ram:BasisQuantity unitCode="LTR">1.0000</ram:BasisQuantity>
</ram:GrossPriceProductTradePrice>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>0.1000</ram:ChargeAmount>
<ram:BasisQuantity unitCode="LTR">1.0000</ram:BasisQuantity>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="LTR">200.0000</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>20.00</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Atelier du Ballon SARL</ram:Name>
<ram:PostalTradeAddress>
<ram:PostcodeCode>75001</ram:PostcodeCode>
<ram:LineOne>1 rue du Louvre</ram:LineOne>
<ram:CityName>Paris</ram:CityName>
<ram:CountryID>FR</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="FC">552081317</ram:ID>
</ram:SpecifiedTaxRegistration>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">FR44552081317</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Maison de Evenements SAS</ram:Name>
<ram:PostalTradeAddress>
<ram:PostcodeCode>69002</ram:PostcodeCode>
<ram:LineOne>18 rue de Brest</ram:LineOne>
<ram:CityName>Lyon</ram:CityName>
<ram:CountryID>FR</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">FR19787878784</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery>
<ram:ActualDeliverySupplyChainEvent>
<ram:OccurrenceDateTime><udt:DateTimeString format="102">20171117</udt:DateTimeString></ram:OccurrenceDateTime>
</ram:ActualDeliverySupplyChainEvent>
</ram:ApplicableHeaderTradeDelivery>
<ram:ApplicableHeaderTradeSettlement>
<ram:PaymentReference>FAC-20171118-506</ram:PaymentReference>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>42</ram:TypeCode>
<ram:Information>Virement bancaire</ram:Information>
<ram:PayeePartyCreditorFinancialAccount>
<ram:IBANID>FR76 3000 6000 0112 3456 7890 189</ram:IBANID>
</ram:PayeePartyCreditorFinancialAccount>
<ram:PayeeSpecifiedCreditorFinancialInstitution>
<ram:BICID>AGRIFRPPXXX</ram:BICID>
</ram:PayeeSpecifiedCreditorFinancialInstitution>
</ram:SpecifiedTradeSettlementPaymentMeans>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount>11.20</ram:CalculatedAmount>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:BasisAmount>160.00</ram:BasisAmount>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>7.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount>63.84</ram:CalculatedAmount>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:BasisAmount>336.00</ram:BasisAmount>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradePaymentTerms>
<ram:Description>Paiement sans escompte avant le 09.12.2017</ram:Description>
<ram:DueDateDateTime><udt:DateTimeString format="102">20171209</udt:DateTimeString></ram:DueDateDateTime>
</ram:SpecifiedTradePaymentTerms>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>496.00</ram:LineTotalAmount>
<ram:ChargeTotalAmount>0.00</ram:ChargeTotalAmount>
<ram:AllowanceTotalAmount>0.00</ram:AllowanceTotalAmount>
<ram:TaxBasisTotalAmount>496.00</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">75.04</ram:TaxTotalAmount>
<ram:GrandTotalAmount>571.04</ram:GrandTotalAmount>
<ram:DuePayableAmount>571.04</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>