Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UBL invoice and credit note XML support #7

Merged
merged 3 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
currentVersion=0.1.3
currentVersion=0.1.4
mainClassName=io.github.easybill.xrviz.App
52 changes: 46 additions & 6 deletions src/main/java/io/github/easybill/xrviz/XslTransformer.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,53 @@
import java.io.*;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

public class XslTransformer {
static final Logger logger = Logger.getGlobal();
static final String BASE_PATH = Config.getValue(Config.Keys.DATA_PATH);
public static final String CII_VALIDATION_STRING = "<rsm:CrossIndustryInvoice";
public static final String UBL_I_VALIDATION_STRING = "<Invoice";
public static final String UBL_C_VALIDATION_STRING = "<CreditNote";

enum DocumentType {
CII("cii-xr.xsl"),
UBL_I("ubl-invoice-xr.xsl"),
UBL_C("ubl-creditnote-xr.xsl");

private final String xslName;

DocumentType(String xslName) {
this.xslName = xslName;
}

public String getXslName() {
return xslName;
}

public static Optional<DocumentType> detectDocumentType(String xmlContent) {
if (xmlContent == null || xmlContent.isEmpty()) {
return Optional.empty();
}

if (xmlContent.contains(CII_VALIDATION_STRING)) {
return Optional.of(CII);
} else if (xmlContent.contains(UBL_I_VALIDATION_STRING)) {
return Optional.of(UBL_I);
} else if (xmlContent.contains(UBL_C_VALIDATION_STRING)) {
return Optional.of(UBL_C);
}

return Optional.empty();
}
}

public static void validateFiles() {
String[] files = {
"xsl/cii-xr.xsl",
"xsl/ubl-invoice-xr.xsl",
"xsl/xrechnung-html.xsl",
"xsl/xr-pdf.xsl",
"fop/fop.xconf"
Expand All @@ -47,24 +84,27 @@ public static void validateFiles() {
}
}

private static DOMSource transformCiiToXr(String inputXml) throws TransformerException {
private static DOMSource transformXmlToXr(String inputXml, DocumentType type) throws TransformerException {
TransformerFactory factory = TransformerFactory.newInstance();
StreamSource xslCiiXr = new StreamSource("data/xsl/cii-xr.xsl");
Transformer ciiXrTransformer = factory.newTransformer(xslCiiXr);
StreamSource source = new StreamSource("data/xsl/" + type.getXslName());
Transformer transformer = factory.newTransformer(source);
Source xml = new StreamSource(new StringReader(inputXml));
DOMResult domResult = new DOMResult();
ciiXrTransformer.transform(xml, domResult);
transformer.transform(xml, domResult);
return new DOMSource(domResult.getNode());
}

public static String transformToHtml(String inputXml, String language) throws TransformerException {
TransformerFactory factory = TransformerFactory.newInstance();
StreamSource xslXrHtml = new StreamSource("data/xsl/xrechnung-html.xsl");

Transformer xslXrTransformer = factory.newTransformer(xslXrHtml);
xslXrTransformer.setParameter("lang", language);
DOMSource transformedSource = transformCiiToXr(inputXml);

DOMSource transformedSource = transformXmlToXr(inputXml, DocumentType.detectDocumentType(inputXml).orElseThrow());
StringWriter outputString = new StringWriter();
xslXrTransformer.transform(transformedSource, new StreamResult(outputString));

return outputString.toString();
}

Expand All @@ -74,7 +114,7 @@ public static byte[] transformToPdf(String inputXml, String language) throws Tra
Transformer xslXrTransformer = factory.newTransformer(xslXrPdf);
xslXrTransformer.setParameter("lang", language);

DOMSource transformedSource = transformCiiToXr(inputXml);
DOMSource transformedSource = transformXmlToXr(inputXml, DocumentType.detectDocumentType(inputXml).orElseThrow());
StringWriter outputString = new StringWriter();
xslXrTransformer.transform(transformedSource, new StreamResult(outputString));
String foXmlString = outputString.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
import java.net.HttpURLConnection;
import java.util.logging.Logger;

import static io.github.easybill.xrviz.XslTransformer.*;

public abstract class XmlRequestExtractor {
static final Logger logger = Logger.getGlobal();
private static final String CII_VALIDATION_STRING = "<rsm:CrossIndustryInvoice";

Optional<String> validate(HttpExchange exchange) throws IOException {
if (!exchange.getRequestMethod().equalsIgnoreCase("POST")) {
Expand All @@ -23,7 +24,7 @@ Optional<String> validate(HttpExchange exchange) throws IOException {

String xml = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);

if (xml.isBlank() || !xml.contains(CII_VALIDATION_STRING)) {
if (!isXMLValid(xml)) {
logger.severe("Invalid XML content!");

exchange.sendResponseHeaders(HttpURLConnection.HTTP_BAD_REQUEST, -1);
Expand All @@ -38,4 +39,9 @@ String getLanguage(HttpExchange exchange) {
return acceptLanguage != null && acceptLanguage.toLowerCase().contains("en") ? "en" : "de";
}

private boolean isXMLValid(String xml) {
return !xml.isBlank() && (xml.contains(CII_VALIDATION_STRING) ||
xml.contains(UBL_I_VALIDATION_STRING) ||
xml.contains(UBL_C_VALIDATION_STRING));
}
}
30 changes: 29 additions & 1 deletion src/test/http/api-test.http
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,37 @@ Accept-Language: de

< ./EN16931_Einfach.xml

### Generate a HTML file from a UBL Invoice file
POST {{baseUrl}}/convert.html
Content-Type: application/xml
Accept-Language: de

< ./ubl-invoice.xml

### Generate a HTML file from a UBL CreditNote file
POST {{baseUrl}}/convert.html
Content-Type: application/xml
Accept-Language: de

< ./ubl-creditnote.xml

### Generate a PDF file from a XML file
POST {{baseUrl}}/convert.pdf
Content-Type: application/xml
Accept-Language: de

< ./EN16931_Einfach.xml
< ./EN16931_Einfach.xml

### Generate a PDF file from a UBL Invoice file
POST {{baseUrl}}/convert.pdf
Content-Type: application/xml
Accept-Language: de

< ./ubl-invoice.xml

### Generate a PDF file from a UBL CreditNote file
POST {{baseUrl}}/convert.pdf
Content-Type: application/xml
Accept-Language: de

< ./ubl-creditnote.xml
215 changes: 215 additions & 0 deletions src/test/http/ubl-creditnote.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
<?xml version="1.0" encoding="UTF-8"?>
<CreditNote xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>Snippet1</cbc:ID>
<cbc:IssueDate>2017-11-13</cbc:IssueDate>
<cbc:CreditNoteTypeCode>381</cbc:CreditNoteTypeCode>
<cbc:Note>Please note we have a new phone number: 22 22 22 22</cbc:Note>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cbc:AccountingCost>4025:123:4343</cbc:AccountingCost>
<cbc:BuyerReference>0150abc</cbc:BuyerReference>
<cac:BillingReference>
<cac:InvoiceDocumentReference>
<cbc:ID>Snippet1</cbc:ID>
</cac:InvoiceDocumentReference>
</cac:BillingReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="0088">9482348239847239874</cbc:EndpointID>
<cac:PartyIdentification>
<cbc:ID>99887766</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>SupplierTradingName Ltd.</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Main street 1</cbc:StreetName>
<cbc:AdditionalStreetName>Postbox 123</cbc:AdditionalStreetName>
<cbc:CityName>London</cbc:CityName>
<cbc:PostalZone>GB 123 EW</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>GB</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>GB1232434</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>SupplierOfficialName Ltd</cbc:RegistrationName>
<cbc:CompanyID>GB983294</cbc:CompanyID>
</cac:PartyLegalEntity>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="0002">FR23342</cbc:EndpointID>
<cac:PartyIdentification>
<cbc:ID schemeID="0002">FR23342</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
<cbc:Name>BuyerTradingName AS</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Hovedgatan 32</cbc:StreetName>
<cbc:AdditionalStreetName>Po box 878</cbc:AdditionalStreetName>
<cbc:CityName>Stockholm</cbc:CityName>
<cbc:PostalZone>456 34</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>SE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>SE4598375937</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Buyer Official Name</cbc:RegistrationName>
<cbc:CompanyID schemeID="0183">39937423947</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>Lisa Johnson</cbc:Name>
<cbc:Telephone>23434234</cbc:Telephone>
<cbc:ElectronicMail>lj@buyer.se</cbc:ElectronicMail>
</cac:Contact>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:Delivery>
<cbc:ActualDeliveryDate>2017-11-01</cbc:ActualDeliveryDate>
<cac:DeliveryLocation>
<cbc:ID schemeID="0088">9483759475923478</cbc:ID>
<cac:Address>
<cbc:StreetName>Delivery street 2</cbc:StreetName>
<cbc:AdditionalStreetName>Building 56</cbc:AdditionalStreetName>
<cbc:CityName>Stockholm</cbc:CityName>
<cbc:PostalZone>21234</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>SE</cbc:IdentificationCode>
</cac:Country>
</cac:Address>
</cac:DeliveryLocation>
<cac:DeliveryParty>
<cac:PartyName>
<cbc:Name>Delivery party Name</cbc:Name>
</cac:PartyName>
</cac:DeliveryParty>
</cac:Delivery>
<cac:PaymentMeans>
<cbc:PaymentMeansCode name="Credit transfer">30</cbc:PaymentMeansCode>
<cbc:PaymentID>Snippet1</cbc:PaymentID>
<cac:PayeeFinancialAccount>
<cbc:ID>IBAN32423940</cbc:ID>
<cbc:Name>AccountName</cbc:Name>
<cac:FinancialInstitutionBranch>
<cbc:ID>BIC324098</cbc:ID>
</cac:FinancialInstitutionBranch>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>Payment within 10 days, 2% discount</cbc:Note>
</cac:PaymentTerms>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>true</cbc:ChargeIndicator>
<cbc:AllowanceChargeReason>Insurance</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="EUR">25</cbc:Amount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>25.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:AllowanceCharge>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">331.25</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">1325</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">331.25</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>25.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">1300</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">1325</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">1656.25</cbc:TaxInclusiveAmount>
<cbc:ChargeTotalAmount currencyID="EUR">25</cbc:ChargeTotalAmount>
<cbc:PayableAmount currencyID="EUR">1656.25</cbc:PayableAmount>
</cac:LegalMonetaryTotal>

<cac:CreditNoteLine>
<cbc:ID>1</cbc:ID>
<cbc:CreditedQuantity unitCode="DAY">7</cbc:CreditedQuantity>
<cbc:LineExtensionAmount currencyID= "EUR">2800</cbc:LineExtensionAmount>
<cbc:AccountingCost>Konteringsstreng</cbc:AccountingCost>
<cac:OrderLineReference>
<cbc:LineID>123</cbc:LineID>
</cac:OrderLineReference>
<cac:Item>
<cbc:Description>Description of item</cbc:Description>
<cbc:Name>item name</cbc:Name>
<cac:StandardItemIdentification>
<cbc:ID schemeID="0088">21382183120983</cbc:ID>
</cac:StandardItemIdentification>
<cac:OriginCountry>
<cbc:IdentificationCode>NO</cbc:IdentificationCode>
</cac:OriginCountry>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listID="SRV">09348023</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>25.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">400</cbc:PriceAmount>
</cac:Price>
</cac:CreditNoteLine>
<cac:CreditNoteLine>
<cbc:ID>2</cbc:ID>
<cbc:CreditedQuantity unitCode="DAY">-3</cbc:CreditedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">-1500</cbc:LineExtensionAmount>
<cac:OrderLineReference>
<cbc:LineID>123</cbc:LineID>
</cac:OrderLineReference>
<cac:Item>
<cbc:Description>Description 2</cbc:Description>
<cbc:Name>item name 2</cbc:Name>
<cac:StandardItemIdentification>
<cbc:ID schemeID="0088">21382183120983</cbc:ID>
</cac:StandardItemIdentification>
<cac:OriginCountry>
<cbc:IdentificationCode>NO</cbc:IdentificationCode>
</cac:OriginCountry>
<cac:CommodityClassification>
<cbc:ItemClassificationCode listID="SRV">09348023</cbc:ItemClassificationCode>
</cac:CommodityClassification>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>25.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">500</cbc:PriceAmount>
</cac:Price>
</cac:CreditNoteLine>
</CreditNote>
Loading
Loading