Skip to content

Commit f8d05bf

Browse files
authored
Add UBL invoice and credit note XML support (#7)
* Add UBL invoice and credit note XML support
1 parent c010b30 commit f8d05bf

File tree

7 files changed

+712
-13
lines changed

7 files changed

+712
-13
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
currentVersion=0.1.3
1+
currentVersion=0.1.4
22
mainClassName=io.github.easybill.xrviz.App

src/main/java/io/github/easybill/xrviz/XslTransformer.java

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,53 @@
2020
import java.io.*;
2121
import java.nio.file.Paths;
2222
import java.util.Arrays;
23+
import java.util.Optional;
2324
import java.util.logging.Level;
2425
import java.util.logging.Logger;
2526

2627
public class XslTransformer {
2728
static final Logger logger = Logger.getGlobal();
2829
static final String BASE_PATH = Config.getValue(Config.Keys.DATA_PATH);
30+
public static final String CII_VALIDATION_STRING = "<rsm:CrossIndustryInvoice";
31+
public static final String UBL_I_VALIDATION_STRING = "<Invoice";
32+
public static final String UBL_C_VALIDATION_STRING = "<CreditNote";
33+
34+
enum DocumentType {
35+
CII("cii-xr.xsl"),
36+
UBL_I("ubl-invoice-xr.xsl"),
37+
UBL_C("ubl-creditnote-xr.xsl");
38+
39+
private final String xslName;
40+
41+
DocumentType(String xslName) {
42+
this.xslName = xslName;
43+
}
44+
45+
public String getXslName() {
46+
return xslName;
47+
}
48+
49+
public static Optional<DocumentType> detectDocumentType(String xmlContent) {
50+
if (xmlContent == null || xmlContent.isEmpty()) {
51+
return Optional.empty();
52+
}
53+
54+
if (xmlContent.contains(CII_VALIDATION_STRING)) {
55+
return Optional.of(CII);
56+
} else if (xmlContent.contains(UBL_I_VALIDATION_STRING)) {
57+
return Optional.of(UBL_I);
58+
} else if (xmlContent.contains(UBL_C_VALIDATION_STRING)) {
59+
return Optional.of(UBL_C);
60+
}
61+
62+
return Optional.empty();
63+
}
64+
}
2965

3066
public static void validateFiles() {
3167
String[] files = {
3268
"xsl/cii-xr.xsl",
69+
"xsl/ubl-invoice-xr.xsl",
3370
"xsl/xrechnung-html.xsl",
3471
"xsl/xr-pdf.xsl",
3572
"fop/fop.xconf"
@@ -47,24 +84,27 @@ public static void validateFiles() {
4784
}
4885
}
4986

50-
private static DOMSource transformCiiToXr(String inputXml) throws TransformerException {
87+
private static DOMSource transformXmlToXr(String inputXml, DocumentType type) throws TransformerException {
5188
TransformerFactory factory = TransformerFactory.newInstance();
52-
StreamSource xslCiiXr = new StreamSource("data/xsl/cii-xr.xsl");
53-
Transformer ciiXrTransformer = factory.newTransformer(xslCiiXr);
89+
StreamSource source = new StreamSource("data/xsl/" + type.getXslName());
90+
Transformer transformer = factory.newTransformer(source);
5491
Source xml = new StreamSource(new StringReader(inputXml));
5592
DOMResult domResult = new DOMResult();
56-
ciiXrTransformer.transform(xml, domResult);
93+
transformer.transform(xml, domResult);
5794
return new DOMSource(domResult.getNode());
5895
}
5996

6097
public static String transformToHtml(String inputXml, String language) throws TransformerException {
6198
TransformerFactory factory = TransformerFactory.newInstance();
6299
StreamSource xslXrHtml = new StreamSource("data/xsl/xrechnung-html.xsl");
100+
63101
Transformer xslXrTransformer = factory.newTransformer(xslXrHtml);
64102
xslXrTransformer.setParameter("lang", language);
65-
DOMSource transformedSource = transformCiiToXr(inputXml);
103+
104+
DOMSource transformedSource = transformXmlToXr(inputXml, DocumentType.detectDocumentType(inputXml).orElseThrow());
66105
StringWriter outputString = new StringWriter();
67106
xslXrTransformer.transform(transformedSource, new StreamResult(outputString));
107+
68108
return outputString.toString();
69109
}
70110

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

77-
DOMSource transformedSource = transformCiiToXr(inputXml);
117+
DOMSource transformedSource = transformXmlToXr(inputXml, DocumentType.detectDocumentType(inputXml).orElseThrow());
78118
StringWriter outputString = new StringWriter();
79119
xslXrTransformer.transform(transformedSource, new StreamResult(outputString));
80120
String foXmlString = outputString.toString();

src/main/java/io/github/easybill/xrviz/handler/XmlRequestExtractor.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
import java.net.HttpURLConnection;
1010
import java.util.logging.Logger;
1111

12+
import static io.github.easybill.xrviz.XslTransformer.*;
13+
1214
public abstract class XmlRequestExtractor {
1315
static final Logger logger = Logger.getGlobal();
14-
private static final String CII_VALIDATION_STRING = "<rsm:CrossIndustryInvoice";
1516

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

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

26-
if (xml.isBlank() || !xml.contains(CII_VALIDATION_STRING)) {
27+
if (!isXMLValid(xml)) {
2728
logger.severe("Invalid XML content!");
2829

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

42+
private boolean isXMLValid(String xml) {
43+
return !xml.isBlank() && (xml.contains(CII_VALIDATION_STRING) ||
44+
xml.contains(UBL_I_VALIDATION_STRING) ||
45+
xml.contains(UBL_C_VALIDATION_STRING));
46+
}
4147
}

src/test/http/api-test.http

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,37 @@ Accept-Language: de
1010

1111
< ./EN16931_Einfach.xml
1212

13+
### Generate a HTML file from a UBL Invoice file
14+
POST {{baseUrl}}/convert.html
15+
Content-Type: application/xml
16+
Accept-Language: de
17+
18+
< ./ubl-invoice.xml
19+
20+
### Generate a HTML file from a UBL CreditNote file
21+
POST {{baseUrl}}/convert.html
22+
Content-Type: application/xml
23+
Accept-Language: de
24+
25+
< ./ubl-creditnote.xml
26+
1327
### Generate a PDF file from a XML file
1428
POST {{baseUrl}}/convert.pdf
1529
Content-Type: application/xml
1630
Accept-Language: de
1731

18-
< ./EN16931_Einfach.xml
32+
< ./EN16931_Einfach.xml
33+
34+
### Generate a PDF file from a UBL Invoice file
35+
POST {{baseUrl}}/convert.pdf
36+
Content-Type: application/xml
37+
Accept-Language: de
38+
39+
< ./ubl-invoice.xml
40+
41+
### Generate a PDF file from a UBL CreditNote file
42+
POST {{baseUrl}}/convert.pdf
43+
Content-Type: application/xml
44+
Accept-Language: de
45+
46+
< ./ubl-creditnote.xml

src/test/http/ubl-creditnote.xml

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<CreditNote xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
3+
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
4+
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
5+
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
6+
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
7+
<cbc:ID>Snippet1</cbc:ID>
8+
<cbc:IssueDate>2017-11-13</cbc:IssueDate>
9+
<cbc:CreditNoteTypeCode>381</cbc:CreditNoteTypeCode>
10+
<cbc:Note>Please note we have a new phone number: 22 22 22 22</cbc:Note>
11+
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
12+
<cbc:AccountingCost>4025:123:4343</cbc:AccountingCost>
13+
<cbc:BuyerReference>0150abc</cbc:BuyerReference>
14+
<cac:BillingReference>
15+
<cac:InvoiceDocumentReference>
16+
<cbc:ID>Snippet1</cbc:ID>
17+
</cac:InvoiceDocumentReference>
18+
</cac:BillingReference>
19+
<cac:AccountingSupplierParty>
20+
<cac:Party>
21+
<cbc:EndpointID schemeID="0088">9482348239847239874</cbc:EndpointID>
22+
<cac:PartyIdentification>
23+
<cbc:ID>99887766</cbc:ID>
24+
</cac:PartyIdentification>
25+
<cac:PartyName>
26+
<cbc:Name>SupplierTradingName Ltd.</cbc:Name>
27+
</cac:PartyName>
28+
<cac:PostalAddress>
29+
<cbc:StreetName>Main street 1</cbc:StreetName>
30+
<cbc:AdditionalStreetName>Postbox 123</cbc:AdditionalStreetName>
31+
<cbc:CityName>London</cbc:CityName>
32+
<cbc:PostalZone>GB 123 EW</cbc:PostalZone>
33+
<cac:Country>
34+
<cbc:IdentificationCode>GB</cbc:IdentificationCode>
35+
</cac:Country>
36+
</cac:PostalAddress>
37+
<cac:PartyTaxScheme>
38+
<cbc:CompanyID>GB1232434</cbc:CompanyID>
39+
<cac:TaxScheme>
40+
<cbc:ID>VAT</cbc:ID>
41+
</cac:TaxScheme>
42+
</cac:PartyTaxScheme>
43+
<cac:PartyLegalEntity>
44+
<cbc:RegistrationName>SupplierOfficialName Ltd</cbc:RegistrationName>
45+
<cbc:CompanyID>GB983294</cbc:CompanyID>
46+
</cac:PartyLegalEntity>
47+
</cac:Party>
48+
</cac:AccountingSupplierParty>
49+
<cac:AccountingCustomerParty>
50+
<cac:Party>
51+
<cbc:EndpointID schemeID="0002">FR23342</cbc:EndpointID>
52+
<cac:PartyIdentification>
53+
<cbc:ID schemeID="0002">FR23342</cbc:ID>
54+
</cac:PartyIdentification>
55+
<cac:PartyName>
56+
<cbc:Name>BuyerTradingName AS</cbc:Name>
57+
</cac:PartyName>
58+
<cac:PostalAddress>
59+
<cbc:StreetName>Hovedgatan 32</cbc:StreetName>
60+
<cbc:AdditionalStreetName>Po box 878</cbc:AdditionalStreetName>
61+
<cbc:CityName>Stockholm</cbc:CityName>
62+
<cbc:PostalZone>456 34</cbc:PostalZone>
63+
<cac:Country>
64+
<cbc:IdentificationCode>SE</cbc:IdentificationCode>
65+
</cac:Country>
66+
</cac:PostalAddress>
67+
<cac:PartyTaxScheme>
68+
<cbc:CompanyID>SE4598375937</cbc:CompanyID>
69+
<cac:TaxScheme>
70+
<cbc:ID>VAT</cbc:ID>
71+
</cac:TaxScheme>
72+
</cac:PartyTaxScheme>
73+
<cac:PartyLegalEntity>
74+
<cbc:RegistrationName>Buyer Official Name</cbc:RegistrationName>
75+
<cbc:CompanyID schemeID="0183">39937423947</cbc:CompanyID>
76+
</cac:PartyLegalEntity>
77+
<cac:Contact>
78+
<cbc:Name>Lisa Johnson</cbc:Name>
79+
<cbc:Telephone>23434234</cbc:Telephone>
80+
<cbc:ElectronicMail>[email protected]</cbc:ElectronicMail>
81+
</cac:Contact>
82+
</cac:Party>
83+
</cac:AccountingCustomerParty>
84+
<cac:Delivery>
85+
<cbc:ActualDeliveryDate>2017-11-01</cbc:ActualDeliveryDate>
86+
<cac:DeliveryLocation>
87+
<cbc:ID schemeID="0088">9483759475923478</cbc:ID>
88+
<cac:Address>
89+
<cbc:StreetName>Delivery street 2</cbc:StreetName>
90+
<cbc:AdditionalStreetName>Building 56</cbc:AdditionalStreetName>
91+
<cbc:CityName>Stockholm</cbc:CityName>
92+
<cbc:PostalZone>21234</cbc:PostalZone>
93+
<cac:Country>
94+
<cbc:IdentificationCode>SE</cbc:IdentificationCode>
95+
</cac:Country>
96+
</cac:Address>
97+
</cac:DeliveryLocation>
98+
<cac:DeliveryParty>
99+
<cac:PartyName>
100+
<cbc:Name>Delivery party Name</cbc:Name>
101+
</cac:PartyName>
102+
</cac:DeliveryParty>
103+
</cac:Delivery>
104+
<cac:PaymentMeans>
105+
<cbc:PaymentMeansCode name="Credit transfer">30</cbc:PaymentMeansCode>
106+
<cbc:PaymentID>Snippet1</cbc:PaymentID>
107+
<cac:PayeeFinancialAccount>
108+
<cbc:ID>IBAN32423940</cbc:ID>
109+
<cbc:Name>AccountName</cbc:Name>
110+
<cac:FinancialInstitutionBranch>
111+
<cbc:ID>BIC324098</cbc:ID>
112+
</cac:FinancialInstitutionBranch>
113+
</cac:PayeeFinancialAccount>
114+
</cac:PaymentMeans>
115+
<cac:PaymentTerms>
116+
<cbc:Note>Payment within 10 days, 2% discount</cbc:Note>
117+
</cac:PaymentTerms>
118+
<cac:AllowanceCharge>
119+
<cbc:ChargeIndicator>true</cbc:ChargeIndicator>
120+
<cbc:AllowanceChargeReason>Insurance</cbc:AllowanceChargeReason>
121+
<cbc:Amount currencyID="EUR">25</cbc:Amount>
122+
<cac:TaxCategory>
123+
<cbc:ID>S</cbc:ID>
124+
<cbc:Percent>25.0</cbc:Percent>
125+
<cac:TaxScheme>
126+
<cbc:ID>VAT</cbc:ID>
127+
</cac:TaxScheme>
128+
</cac:TaxCategory>
129+
</cac:AllowanceCharge>
130+
<cac:TaxTotal>
131+
<cbc:TaxAmount currencyID="EUR">331.25</cbc:TaxAmount>
132+
<cac:TaxSubtotal>
133+
<cbc:TaxableAmount currencyID="EUR">1325</cbc:TaxableAmount>
134+
<cbc:TaxAmount currencyID="EUR">331.25</cbc:TaxAmount>
135+
<cac:TaxCategory>
136+
<cbc:ID>S</cbc:ID>
137+
<cbc:Percent>25.0</cbc:Percent>
138+
<cac:TaxScheme>
139+
<cbc:ID>VAT</cbc:ID>
140+
</cac:TaxScheme>
141+
</cac:TaxCategory>
142+
</cac:TaxSubtotal>
143+
</cac:TaxTotal>
144+
<cac:LegalMonetaryTotal>
145+
<cbc:LineExtensionAmount currencyID="EUR">1300</cbc:LineExtensionAmount>
146+
<cbc:TaxExclusiveAmount currencyID="EUR">1325</cbc:TaxExclusiveAmount>
147+
<cbc:TaxInclusiveAmount currencyID="EUR">1656.25</cbc:TaxInclusiveAmount>
148+
<cbc:ChargeTotalAmount currencyID="EUR">25</cbc:ChargeTotalAmount>
149+
<cbc:PayableAmount currencyID="EUR">1656.25</cbc:PayableAmount>
150+
</cac:LegalMonetaryTotal>
151+
152+
<cac:CreditNoteLine>
153+
<cbc:ID>1</cbc:ID>
154+
<cbc:CreditedQuantity unitCode="DAY">7</cbc:CreditedQuantity>
155+
<cbc:LineExtensionAmount currencyID= "EUR">2800</cbc:LineExtensionAmount>
156+
<cbc:AccountingCost>Konteringsstreng</cbc:AccountingCost>
157+
<cac:OrderLineReference>
158+
<cbc:LineID>123</cbc:LineID>
159+
</cac:OrderLineReference>
160+
<cac:Item>
161+
<cbc:Description>Description of item</cbc:Description>
162+
<cbc:Name>item name</cbc:Name>
163+
<cac:StandardItemIdentification>
164+
<cbc:ID schemeID="0088">21382183120983</cbc:ID>
165+
</cac:StandardItemIdentification>
166+
<cac:OriginCountry>
167+
<cbc:IdentificationCode>NO</cbc:IdentificationCode>
168+
</cac:OriginCountry>
169+
<cac:CommodityClassification>
170+
<cbc:ItemClassificationCode listID="SRV">09348023</cbc:ItemClassificationCode>
171+
</cac:CommodityClassification>
172+
<cac:ClassifiedTaxCategory>
173+
<cbc:ID>S</cbc:ID>
174+
<cbc:Percent>25.0</cbc:Percent>
175+
<cac:TaxScheme>
176+
<cbc:ID>VAT</cbc:ID>
177+
</cac:TaxScheme>
178+
</cac:ClassifiedTaxCategory>
179+
</cac:Item>
180+
<cac:Price>
181+
<cbc:PriceAmount currencyID="EUR">400</cbc:PriceAmount>
182+
</cac:Price>
183+
</cac:CreditNoteLine>
184+
<cac:CreditNoteLine>
185+
<cbc:ID>2</cbc:ID>
186+
<cbc:CreditedQuantity unitCode="DAY">-3</cbc:CreditedQuantity>
187+
<cbc:LineExtensionAmount currencyID="EUR">-1500</cbc:LineExtensionAmount>
188+
<cac:OrderLineReference>
189+
<cbc:LineID>123</cbc:LineID>
190+
</cac:OrderLineReference>
191+
<cac:Item>
192+
<cbc:Description>Description 2</cbc:Description>
193+
<cbc:Name>item name 2</cbc:Name>
194+
<cac:StandardItemIdentification>
195+
<cbc:ID schemeID="0088">21382183120983</cbc:ID>
196+
</cac:StandardItemIdentification>
197+
<cac:OriginCountry>
198+
<cbc:IdentificationCode>NO</cbc:IdentificationCode>
199+
</cac:OriginCountry>
200+
<cac:CommodityClassification>
201+
<cbc:ItemClassificationCode listID="SRV">09348023</cbc:ItemClassificationCode>
202+
</cac:CommodityClassification>
203+
<cac:ClassifiedTaxCategory>
204+
<cbc:ID>S</cbc:ID>
205+
<cbc:Percent>25.0</cbc:Percent>
206+
<cac:TaxScheme>
207+
<cbc:ID>VAT</cbc:ID>
208+
</cac:TaxScheme>
209+
</cac:ClassifiedTaxCategory>
210+
</cac:Item>
211+
<cac:Price>
212+
<cbc:PriceAmount currencyID="EUR">500</cbc:PriceAmount>
213+
</cac:Price>
214+
</cac:CreditNoteLine>
215+
</CreditNote>

0 commit comments

Comments
 (0)