A Java library for generating EHF 3.0 / PEPPOL BIS Billing 3.0 compliant invoices, with full support for Norwegian tax rules and B2G (Business-to-Government) requirements.
Built on top of ph-ubl — no XML template files, no string manipulation. Pure UBL 2.1 object model construction.
Generating a PEPPOL-compliant invoice is not just about producing valid XML. The real challenge is the Norwegian localization layer:
- Norwegian MVA-koder (e.g.,
3,5,6,31) must be translated to PEPPOL UNCL5305 tax category codes (S,Z,E,AE, etc.) - The supplier's VAT ID must follow the exact format:
NO+ OrgNr +MVA BuyerReferenceis effectively mandatory for B2G (government) invoicing — missing it causes silent rejection by municipal ERP systems- KID (Kundeidentifikasjonsnummer) for OCR-based bank reconciliation must be mapped to
PaymentID, notPaymentNote - Tax subtotals must be grouped strictly by category and rate (BR-CO-17) — merging two zero-rate categories will fail Schematron validation
This library encodes all of the above so you don't have to rediscover it.
| Dependency | Version |
|---|---|
| Java | 11+ |
| ph-ubl | 9.x |
| Lombok | 1.18+ |
<!-- Maven dependency for ph-ubl --> <dependency> <groupId>com.helger</groupId> <artifactId>ph-ubl21</artifactId> <version>9.x.x</version> </dependency>
// 1. Build the DTO EhfInvoiceDTO dto = new EhfInvoiceDTO(); dto.setInvoiceNo("INV-2024-001"); dto.setInvoiceDate(LocalDate.of(2024, 5, 1)); dto.setInvoiceDueDate(LocalDate.of(2024, 5, 31)); dto.setCurrency("NOK"); dto.setBuyerReference("Morten Tveten"); // Critical for B2G dto.setCompanyOrganizationNo("123456789"); dto.setCompanyName("Supplier AS"); dto.setCompanyLegalName("Supplier AS"); dto.setCompanyCountryCode("NO"); dto.setIsVatRegistered(true); // Triggers NO...MVA format dto.setCustomerOrgCode("987654321"); dto.setCustomerName("Buyer Kommune"); dto.setCustomerCountryCode("NO"); // ... set amounts, line items, VAT details // 2. Generate the UBL object EhfInvoiceFactory factory = new EhfInvoiceFactory(); InvoiceType invoice = factory.createInvoice(dto, null); // 3. Serialize to XML String xml = UBL21Marshaller.invoice() .setFormattedOutput(true) .getAsString(invoice);
See EhfInvoiceGeneratorTest.java for a complete working example with all fields populated.
The NorwegianVatMapper translates internal Norwegian accounting system codes to PEPPOL-compliant UNCL5305 category codes.
| Norwegian MVA-kode | PEPPOL Category | Description |
|---|---|---|
3, 3U, 31, 31U, 32, 33, 33U |
S |
Standard rate (25%, 15%, 12%) |
5, 5U |
Z |
Zero rated goods (books, used cars, etc.) |
51 |
AE |
Reverse charge (domestic construction B2B) |
52 |
G |
Free export item (outside Norway) |
6 |
E |
Exempt from tax (healthcare, education, etc.) |
0, 7 |
O |
Outside scope of VAT entirely |
Important: An unknown VAT code throws IllegalArgumentException immediately. This is intentional — silently defaulting to O or S on an unknown code would produce a legally non-compliant document.
S(Standard rate): Must be accompanied by the explicit percentage (25.00,15.00, or12.00). The percentage is part of the key for BR-CO-17 grouping.ZvsEvsO: All three have a 0% rate in the XML, but they cannot be merged. They represent legally distinct tax treatments. Merging them will cause a fatal Schematron error.AE(Reverse Charge): The tax amount in the subtotal must be0.00. The buyer calculates and reports the VAT themselves.
| Field | Mandatory | Notes |
|---|---|---|
invoiceNo |
✅ | Unique invoice identifier (BR-02) |
invoiceDate |
✅ | Issue date (BR-03) |
invoiceDueDate |
✅ | Payment due date |
currency |
✅ | ISO 4217 (e.g., NOK, EUR) |
buyerReference |
Required for all Norwegian B2G. Missing = likely rejection | |
invoiceType |
Currently not used — defaults to 380 (Commercial Invoice). See Known Limitations |
| Field | Notes |
|---|---|
companyOrganizationNo |
Norwegian Org.nr (9 digits). Used for PEPPOL EndpointID routing |
isVatRegistered |
If true, formats tax ID as NO{OrgNr}MVA per Norwegian requirements |
companyLegalName |
Official name from Foretaksregisteret |
| Field | Notes |
|---|---|
vatCode |
Required. Internal Norwegian code used by NorwegianVatMapper |
vatRate |
Percentage value (e.g., 25.00, 0.00) |
unitCode |
UN/ECE Rec 20 code. Examples: EA (each), HUR (hour), KGM (kg) |
amount |
Net line amount. Must equal (price × qty) − discAmount per BR-LIN-04 |
disc |
Discount percentage (e.g., 10.00 for 10%). Maps to MultiplierFactorNumeric |
type |
Only lines with type = 1 are included in the generated XML |
The List<InvoiceVat> must contain one entry per unique combination of VAT category and rate. This is required by PEPPOL business rule BR-CO-17.
// Correct: Two separate entries for two different categories InvoiceVat standard = new InvoiceVat(); standard.setVatCode("3"); standard.setVatRate(new BigDecimal("25.00")); standard.setBaseAmount(new BigDecimal("1000.00")); standard.setVatAmount(new BigDecimal("250.00")); InvoiceVat exempt = new InvoiceVat(); exempt.setVatCode("6"); // E category exempt.setVatRate(BigDecimal.ZERO); exempt.setBaseAmount(new BigDecimal("500.00")); exempt.setVatAmount(BigDecimal.ZERO); // Do NOT merge these even though both have a 0% rate
The builder generates up to two PaymentMeans nodes:
- Domestic (always included): Local bank account (
bankAccountNo) + KID (kid) for OCR reconciliation. - International (when both IBAN and BIC are provided): Separate
PaymentMeansnode withbankAccountNoIbanandbankAccountNoBic.
Binary attachments (e.g., a PDF visual of the invoice) can be passed as List<DocumentReferenceType>:
DocumentReferenceType ref = new DocumentReferenceType(); // ... set ID, description, and embedded PDF bytes List<DocumentReferenceType> attachments = List.of(ref); InvoiceType invoice = factory.createInvoice(dto, attachments);
The following features are not yet implemented and are candidates for future development:
- Credit Note (type 381):
dto.invoiceTypefield exists but is currently ignored — the factory always outputs380(Commercial Invoice). PRs welcome. - Customer VAT registration check: The customer tax scheme always formats as
NO{OrgNr}MVAregardless of whether the customer is actually VAT-registered. This may cause issues for foreign customers or non-VAT-registered buyers. - Document-level allowances/charges: The
LegalMonetaryTotalassumesLineExtensionAmount == TaxExclusiveAmount. Header-level discounts or charges are not yet supported. nextInvoiceDatefield: Defined in the DTO but not mapped toInvoicePeriodin the XML output.
src/
└── main/java/com/ehf/builder/
├── EhfInvoiceFactory.java # Core builder — orchestrates the full UBL object graph
├── EhfInvoiceDTO.java # Input data transfer object
├── EhfInvoiceLineItem.java # Single invoice line DTO
├── InvoiceVat.java # VAT subtotal / TaxSubtotal DTO
├── NorwegianVatMapper.java # MVA-kode → UNCL5305 category mapping
└── VatCategoryEnum.java # PEPPOL tax category codes (S, Z, E, AE, G, O, ...)
- PEPPOL BIS Billing 3.0 Specification
- EHF Billing 3.0 (Norwegian extension)
- EN 16931 (European e-invoicing standard)
- PEPPOL Code Lists
- UN/ECE Recommendation 20 — Unit of Measure Codes
- ph-ubl library
MIT