Before start

Dilono’s primary goals are:

  1. Consume and produce EDIFACT messages with Java, in absolutely XML-free way.

  2. Be a lightweight library with few dependencies so that any application can embed it.

  3. Best development experience by enabling comprehensive autocompletion for segments, components, fields, etc.

  4. Take care of a range of non-functional features like segment counting, numbers formatting, etc.

What does Dilono NOT suppose to:

  1. Support other formats than EDIFACT.

  2. Do any kind of communication protocols like AS2, SFTP, etc.

  3. Implement Enterprise Integration Patterns EIP like routing, spilling, streaming, etc.

Getting started

Prerequisites

  1. Java JDK 8+

  2. A valid access token. Can be requested here.

  3. Maven, Gradle or anything else to manage dependencies.

  4. Dilono uses slf4j-api for logging. Any implementation of it has to be defined as a dependency to the application.

Declare maven dependencies

Added repository credentials to settings.xml:
<servers>
    <server>
        <id>dilono-maven-public</id>
        <username>${dilono.maven.user.name}</username>
        <password>${dilono.maven.user.password}</password>
    </server>
</servers>
pom.xml - define repositories
    <repositories>
        <repository>
            <id>dilono-maven-public</id>
            <url>https://maven.dilono.com/releases</url>
        </repository>
    </repositories>
pom.xml - declare dependencies
        <!-- client to communicate with dilono cloud -->
        <dependency>
            <groupId>com.dilono</groupId>
            <artifactId>dilono-edifact-client</artifactId>
            <version>${versions.dilono-edifact}</version>
        </dependency>

        <!-- d96a models -->
        <dependency>
            <groupId>com.dilono</groupId>
            <artifactId>dilono-edifact-d96a</artifactId>
            <version>${versions.dilono-edifact}</version>
        </dependency>

        <!-- dilono test framework -->
        <dependency>
            <groupId>com.dilono</groupId>
            <artifactId>dilono-edifact-test</artifactId>
            <version>${versions.dilono-edifact}</version>
            <scope>test</scope>
        </dependency>

Configure Dilono client

Application.java
    @Bean
    ECSClient ecsClient(@Value("${dilono.server.url}") final URL url,
                        @Value("${dilono.server.token.id}") final String tokenId,
                        @Value("${dilono.server.token.secret}") final String tokenSecret) {

        return new ECSClientBuilder()
            .withBaseUrl(url)
            .withCredentials(ECSClientCredentials.token(tokenId, tokenSecret))
            .build();
    }
Note
Spring annotations are used in this example, but they don’t have to be used. Dilono does not depend on any frameworks.

Consuming EDIFACT

package com.dilono.sample.basic;


import com.dilono.edifact.client.ECSClient;
import com.dilono.edifact.d96a.D96A;
import com.dilono.edifact.toolkit.DTMUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Component;

import java.io.InputStream;
import java.util.List;

@Component
public class EdifactOrdersReader {

    private final ECSClient client;

    EdifactOrdersReader(ECSClient client) {
        this.client = client;
    }

    List<Order> fromEdifact(final InputStream edifact) throws Exception {
        return D96A.reader(client, IOUtils.toByteArray(edifact))
            .orders(() -> new Order(), (orders, myOrder) -> orders
                .bgm(bgm -> bgm
                    .data(bgm_ -> bgm_
                        .e1004DocumentMessageNumber(e1004 -> myOrder.setOrderNr(e1004))))
                .dtm(dtm -> dtm
                    .must(dtm_ -> dtm_
                        .c507DateTimePeriod(c507 -> c507
                            .e2005DateTimePeriodQualifier(e2005 -> e2005.isEqualTo("137"))))
                    .data(dtm_ -> dtm_
                        .c507DateTimePeriod(c507 -> myOrder.setOrderCreatedAt(DTMUtils.parse(
                            c507.e2380DateTimePeriod(),
                            c507.e2379DateTimePeriodFormatQualifier())))))
                .sg2(sg2 -> sg2
                    .must(sg2_ -> sg2_
                        .nad(nad -> nad
                            .e3035PartyQualifier(e3035 -> e3035.isEqualTo("SU"))))
                    .data(sg2_ -> sg2_
                        .nad(nad -> nad.data(nad_ -> nad_
                            .c082PartyIdentificationDetails(c082 -> c082
                                .e3039PartyIdIdentification(e3039 -> myOrder.setSupplierNr(e3039)))))))
                .sg25(sg25 -> sg25.data(() -> newLineItem(myOrder), (sg25_, myLineItem) -> sg25_
                    .lin(lin -> lin.data(lin_ -> lin_
                        .e1082LineItemNumber(e1082 -> myLineItem.setPosition(e1082.intValue()))
                        .c212ItemNumberIdentification(c212 -> c212
                            .e7140ItemNumber(e7140 -> myLineItem.setSku(e7140)))))
                    .qty(qty -> qty
                        .must(qty_ -> qty_
                            .c186QuantityDetails(c186 -> c186
                                .e6063QuantityQualifier(e6063 -> e6063.isEqualTo("21"))))
                        .data(qty_ -> qty_.c186QuantityDetails(c186 -> c186
                            .e6060Quantity(e6060 -> myLineItem.setQty(e6060)))))
                    .sg28(sg28 -> sg28.data(sg28_ -> sg28_
                        .pri(pri -> pri
                            .must(pri_ -> pri_
                                .c509PriceInformation(c509 -> c509
                                    .e5125PriceQualifier(e5126 -> e5126.isEqualTo("AAA"))))
                            .data(pri_ -> pri_
                                .c509PriceInformation(c509 -> c509
                                    .e5118Price(e5118 -> myLineItem.setPiecePriceNet(e5118)))))
                        .pri(pri -> pri
                            .can(pri_ -> pri_
                                .c509PriceInformation(c509 -> c509.e5125PriceQualifier(e5126 -> e5126.isEqualTo("AAB"))))
                            .data(pri_ -> pri_
                                .c509PriceInformation(c509 -> c509
                                    .e5118Price(e5118 -> myLineItem.setPiecePriceGross(e5118))))))))));
    }

    private Order.LineItem newLineItem(Order myOrder) {
        final Order.LineItem LineItem = new Order.LineItem();
        myOrder.getLineItems().add(LineItem);
        return LineItem;
    }

}

Producing EDIFACT

package com.dilono.sample.basic;

import com.dilono.edifact.client.ECSClient;
import com.dilono.edifact.d96a.D96A;
import com.dilono.edifact.toolkit.DTMUtils;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.List;

@Component
public class EdifactInvoicWriter {

    private final ECSClient ecsClient;

    public EdifactInvoicWriter(ECSClient ecsClient) {
        this.ecsClient = ecsClient;
    }

    String toEdifact(final List<Invoice> invoices) throws Exception {
        return D96A.writer(ecsClient)
            .unb(unb -> unb
                .unoc()
                .version3()
                .sender(sender -> sender
                    .id("0000000000001")
                    .codeQualifier("14"))
                .recipient(recipient -> recipient
                    .id("0000000000002")
                    .codeQualifier("14"))
                .interchangeId("ABCD123")
                .interchangeTimestamp(now())
                .eancom())
            .invoic(() -> invoices.stream(), (invoice, invoic) -> invoic
                .unh(unh -> unh.invoic().d96a().un().ean008())
                .bgm(bgm -> bgm
                    .data(bgm_ -> bgm_
                        .c002DocumentMessageName(c002 -> c002
                            .e1001DocumentMessageNameCoded("380")) // Commercial invoice
                        .e1004DocumentMessageNumber(invoice.getInvoiceNr())))
                .dtm(dtm -> dtm.data(dtm_ -> dtm_
                    .c507DateTimePeriod(c507 -> c507
                        .e2005DateTimePeriodQualifier("137") // Document/message date/time
                        .e2379DateTimePeriodFormatQualifier("102")
                        .e2380DateTimePeriod(DTMUtils.format(invoice.getInvoiceCreatedAt(), "102")))))
                .dtm(dtm -> dtm.data(dtm_ -> dtm_
                    .c507DateTimePeriod(c507 -> c507
                        .e2005DateTimePeriodQualifier("35") // Delivery date/time, actual
                        .e2379DateTimePeriodFormatQualifier("102")
                        .e2380DateTimePeriod(DTMUtils.format(invoice.getDeliveryDateActual(), "102")))))
                .sg1(sg1 -> sg1.data(sg1_ -> sg1_
                    .rff(rff -> rff.data(rff_ -> rff_
                        .c506Reference(c506 -> c506
                            .e1153ReferenceQualifier("ON") // Order number (purchase)
                            .e1154ReferenceNumber(invoice.getOrderNr()))))
                    .dtm(dtm -> dtm.data(dtm_ -> dtm_
                        .c507DateTimePeriod(c507 -> c507
                            .e2005DateTimePeriodQualifier("4") // Order date/time
                            .e2379DateTimePeriodFormatQualifier("102")
                            .e2380DateTimePeriod(DTMUtils.format(invoice.getOrderCreatedAt(), "102")))))))
                .sg2(sg2 -> sg2.data(sg2_ -> sg2_
                    .nad(nad -> nad.data(nad_ -> nad_
                        .e3035PartyQualifier("SU") // Supplier
                        .c082PartyIdentificationDetails(c082 -> c082
                            .e3039PartyIdIdentification(invoice.getSupplierNr())
                            .e3055CodeListResponsibleAgencyCoded("9")))) // GLN
                    .sg3(sg3 -> sg3.data(sg3_ -> sg3_
                        .rff(rff -> rff.data(rff_ -> rff_
                            .c506Reference(c506 -> c506
                                .e1153ReferenceQualifier("VA") // VAT ID
                                .e1154ReferenceNumber(invoice.getSupplierVatId()))))))))
                .sg2(sg2 -> sg2.data(sg2_ -> sg2_
                    .nad(nad -> nad.data(nad_ -> nad_
                        .e3035PartyQualifier("DP") // Ship To
                        .c082PartyIdentificationDetails(c082 -> c082
                            .e3039PartyIdIdentification(invoice.getShipToNr())
                            .e3055CodeListResponsibleAgencyCoded("9")))))) // GLN
                .sg2(sg2 -> sg2.data(sg2_ -> sg2_
                    .nad(nad -> nad.data(nad_ -> nad_
                        .e3035PartyQualifier("IV") // Invoicee
                        .c082PartyIdentificationDetails(c082 -> c082
                            .e3039PartyIdIdentification(invoice.getInvoiceeNr())
                            .e3055CodeListResponsibleAgencyCoded("9"))))  // GLN
                    .sg3(sg3 -> sg3.data(sg3_ -> sg3_
                        .rff(rff -> rff.data(rff_ -> rff_
                            .c506Reference(c506 -> c506
                                .e1153ReferenceQualifier("VA") // VAT ID
                                .e1154ReferenceNumber(invoice.getSupplierVatId()))))))))
                .sg7(sg7 -> sg7.data(sg7_ -> sg7_
                    .cux(cux -> cux.data(cux_ -> cux_
                        .c5041CurrencyDetails(c504 -> c504
                            .e6347CurrencyDetailsQualifier("2") // Reference currency
                            .e6345CurrencyCoded("EUR"))))))
                .sg8(sg8 -> sg8.data(sg8_ -> sg8_
                    .pat(pat -> pat.data(pat_ -> pat_
                        .e4279PaymentTermsTypeQualifier("1") // Basic
                        .c112TermsTimeInformation(c112 -> c112
                            .e2475PaymentTimeReferenceCoded("5") // Date of invoice
                            .e2151TypeOfPeriodCoded("D") // Day
                            .e2152NumberOfPeriods(30)))))) // 30 days
                .sg25(() -> invoice.getLineItems().stream(), (lineItem, sg25) -> sg25.data(sg25_ -> sg25_
                    .lin(lin -> lin.data(lin_ -> lin_
                        .e1082LineItemNumber(lineItem.getPosition())
                        .c212ItemNumberIdentification(c212 -> c212
                            .e7140ItemNumber(lineItem.getSku())
                            .e7143ItemNumberTypeCoded("EN"))))  // EAN
                    .qty(qty -> qty.data(qty_ -> qty_
                        .c186QuantityDetails(c186 -> c186
                            .e6063QuantityQualifier("47") // Invoiced quantity
                            .e6060Quantity(lineItem.getInvoicedQty().intValue()))))
                    .sg26(sg26 -> sg26.data(sg26_ -> sg26_
                        .moa(moa -> moa.data(moa_ -> moa_
                            .c516MonetaryAmount(c516 -> c516
                                .e5025MonetaryAmountTypeQualifier("203") // Line item amount
                                .e5004MonetaryAmount(lineItem.sumNetAmount())
                                .e6345CurrencyCoded("EUR")
                                .e6343CurrencyQualifier("4")))))) // Invoicing currency
                    .sg28(sg28 -> sg28.data(sg28_ -> sg28_
                        .pri(pri -> pri.data(pri_ -> pri_
                            .c509PriceInformation(c509 -> c509
                                .e5125PriceQualifier("AAA") // Calculation net
                                .e5118Price(lineItem.getPiecePriceNetAmount())
                                .e6411MeasureUnitQualifier(lineItem.getPiecePriceNetUnit())
                                .e5375PriceTypeCoded("CT") // Contract
                                .e5387PriceTypeQualifier("NTP")))))) // Net unit price
                    .sg33(sg33 -> sg33.data(sg33_ -> sg33_
                        .tax(tax -> tax.data(tax_ -> tax_
                            .e5283DutyTaxFeeFunctionQualifier("7") // Tax
                            .c241DutyTaxFeeType(c241 -> c241
                                .e5153DutyTaxFeeTypeCoded("VAT")) // Value added tax
                            .c243DutyTaxFeeDetail(c243 -> c243
                                .e5278DutyTaxFeeRate(String.valueOf(lineItem.getVatRate().intValue())))
                            .e5305DutyTaxFeeCategoryCoded("S"))))))) // Standard rate
                .uns(uns -> uns.data(uns_ -> uns_
                    .e0081("S"))) // Detail/summary section separation
                .cnt(cnt -> cnt.data(cnt_ -> cnt_
                    .c270Control(c270 -> c270
                        .e6069ControlQualifier("2") // Number of line items in message
                        .e6066ControlValue(invoice.getLineItems().size()))))
                .sg48(sg48 -> sg48
                    .data(sg48_ -> sg48_
                        .moa(moa -> moa.data(moa_ -> moa_.c516MonetaryAmount(c516 -> c516
                            .e5025MonetaryAmountTypeQualifier("77") // Invoice amount
                            .e5004MonetaryAmount(invoice.sumAmount()))))))
                .sg48(sg48 -> sg48
                    .data(sg48_ -> sg48_
                        .moa(moa -> moa.data(moa_ -> moa_.c516MonetaryAmount(c516 -> c516
                            .e5025MonetaryAmountTypeQualifier("125") // taxable amount
                            .e5004MonetaryAmount(invoice.sumTaxableAmount()))))))
                .unt())
            .unz()
            .validate()
            .writeToString();
    }

    private Date now() {
        return Date.from(LocalDateTime.of(2020, 12, 31, 23, 20, 0).toInstant(ZoneOffset.UTC));
    }
}

How does Dilono work?

High-Level Overview

Dilono is client-server model application. The server takes care of actual EDIFACT processing and supplies a model to the client. The client operates on that model like on an object graph. For instance, it reads certain segments and may ignore the rest if it’s not needed. In case of write operation, the client populates the model and sends to the server to convert to EDIFACT.

dilono high level overview

Consuming EDIFACT

Dilono declares operations in functional way through lambdas - consumers, suppliers, functions. Lambdas get evaluated under certain conditions. Let’s consider an example how Dilono converts a D96A ORDERS message to a POJO:

        public List<Order> fromEdifact(byte[] edifact) throws Exception {
            final List<Order> orderPojos = D96A.reader(client, edifact)
                .orders(() -> new Order(), (orders, myOrder) -> orders // (1)
                        .bgm(bgm -> bgm // (2)
                            .data(bgm_ -> bgm_ // (3)
                                .e1004DocumentMessageNumber(e1004 -> myOrder.setOrderNr(e1004)))) // (4)
                    // ...
                );
            return orderPojos;
        }
  1. We start with the factory method .orders() whereas () → new Order() a factory for POJOs being mapped to and (orders, myOrder) → …​ is the actual mapping function with two arguments. An interchange may contain more than one message and the factory will create a new pojo for each message. The first argument orders is a Reader of the actual EDIFACT message and the second one myOrder is a new instance of POJO created by the factory () → new Order().

  2. The .bgm() method accepts a consumer bgm of type BGMBeginningOfMessageQuery to query BGM segments of the message.

  3. The bgm query has a method .data() that accepts a consumer bgm_ of type BGMBeginningOfMessageStdReader.

  4. The reader provides all methods, according to the subset specification, to read the data from the segment, its components and fields. So the e1004 → myOrder.setOrderNr(e1004) consumer sets the order number to the myOrder POJO.

Now, let’s query NAD segments for buyer’s number conditionally:

        public List<Order> fromEdifact(byte[] edifact) throws Exception {
            final List<Order> orderPojos = D96A.reader(client, edifact)
                .orders(() -> new Order(), (orders, myOrder) -> orders //
                    // ...
                    .sg2(sg2 -> sg2 // (1)
                        .must(sg2_ -> sg2_ // (2)
                            .nad(nad -> nad // (3)
                                .e3035PartyQualifier(e3035 -> e3035.isEqualTo("BY")))) // (4)
                        .data(sg2_ -> sg2_
                            .nad(nad -> nad.data(nad_ -> nad_
                                .c082PartyIdentificationDetails(c082 -> c082
                                    .e3039PartyIdIdentification(e3039 -> myOrder.setBuyerNr(e3039))))))) // (5)
                    .sg2(sg2 -> sg2
                        .can(sg2_ -> sg2_ // (6)
                            .nad(nad -> nad
                                .e3035PartyQualifier(e3035 -> e3035.isEqualTo("SU"))))
                        .data(sg2_ -> sg2_
                            .nad(nad -> nad.data(nad_ -> nad_
                                .c082PartyIdentificationDetails(c082 -> c082
                                    .e3039PartyIdIdentification(e3039 -> myOrder.setSupplierNr(e3039))))))));
            //...
            return orderPojos;
        }
  1. We query the Segment Group 2 of the ORDERS message by calling .sg2() on the orders.

  2. The sg2 query has a method .must() that accepts a consumer sg2_ of type SegmentGroup2Condition.

  3. The sg2 query also provides access to all nested conditions, in this case NADNameAndAddressCondition.

  4. So we traverse until the field e3035PartyQualifier and eventually build a predicate for the sg2.data() method.

  5. If .must() is evaluated to true, only then the sg2.data() consumer will be evaluated. Otherwise, an exception is thrown.

  6. Declaration of .can() and .must() predicates is exactly the same. However, the result is different - the .can() doesn’t throw, and it makes .data() optional.

Now, let’s loop over Segment Group 25 and map all line items:

        public List<Order> fromEdifact(byte[] edifact) throws Exception {
            final List<Order> orderPojos = D96A.reader(client, edifact)
                .orders(() -> new Order(), (orders, myOrder) -> orders
                    // ...
                    .sg25(sg25 -> sg25.data(() -> newLineItem(myOrder), (sg25_, myLineItem) -> sg25_ // (1)
                        .lin(lin -> lin.data(lin_ -> lin_ // (2)
                            .e1082LineItemNumber(e1082 -> myLineItem.setPosition(e1082.intValue()))
                            .c212ItemNumberIdentification(c212 -> c212
                                .e7140ItemNumber(e7140 -> myLineItem.setSku(e7140)))))))); // (3)
            // ...
            return orderPojos;
        }
  1. The Segment Group 25 may occur many times and each time it there must be a new POJO that represents a line item. Because of this, the .data() is a consumer of two arguments. The first () → newLineItem(myOrder) is a factory for POJO and the second (sg25_, myLineItem) → …​ is the actual mapping function with two arguments - from and to.

  2. The .lin() query is evaluated for each LIN segment for each segment group.

  3. The value of the e7140 field is set to POJO as sku field.

Note that all names of methods, variables, etc. correspond to the EDIFACT specification. This significantly increases readability of the code.

The source code for this example is available on GitHub.

Producing EDIFACT

Dilono produces EDIFACT in similar to consuming, functional way. Let’s consider an example how Dilono converts a POJO to D96A INVOIC message:

        public String toEdifact(final List<Invoice> invoices) throws Exception {
            return D96A.writer(client)
                .una(una -> una.defaults()) // (1)
                .unb(unb -> unb // (2)
                    .unoc()
                    .version3()
                    .sender(sender -> sender
                        .id("0000000000001")
                        .codeQualifier("14"))
                    .recipient(recipient -> recipient
                        .id("0000000000002")
                        .codeQualifier("14"))
                    .interchangeId("ABCD123")
                    .interchangeTimestamp(now())
                    .eancom())
                .invoic(() -> invoices.stream(), (invoice, invoic) -> invoic // (3)
                    .unh(unh -> unh.invoic().d96a().un().ean008()) // (4)
                    .bgm(bgm -> bgm // (5)
                        .data(bgm_ -> bgm_
                            .c002DocumentMessageName(c002 -> c002
                                .e1001DocumentMessageNameCoded("380")) // Commercial invoice
                            .e1004DocumentMessageNumber(invoice.getInvoiceNr()))) // (6)
                    .dtm(dtm -> dtm.data(dtm_ -> dtm_
                        .c507DateTimePeriod(c507 -> c507
                            .e2005DateTimePeriodQualifier("137") // Document/message date/time
                            .e2379DateTimePeriodFormatQualifier("102")
                            .e2380DateTimePeriod(DTMUtils.format(invoice.getInvoiceCreatedAt(), "102"))))))
                //...
                .dumpToString();
        }
  1. Define default interchange delimiters.

  2. Then the interchange header - sender, recipient, their code qualifiers, etc.

  3. An interchange may contain more than one message. Therefore, the .invoic() method has two arguments. The first one () → invoices.stream() is the source or from producer, and the second (invoice, invoic) → …​ is actual mapping function. Dilono will create exactuly the same number of INVOIC message as the size of the invoices list.

  4. The .unh() method defined the header of the message.

  5. The .bgm() method accepts a consumer bgm of type BGMBeginningOfMessageWrite to write data to the BGM segment has a method .data() that accepts a consumer bgm_ of type BGMBeginningOfMessageStdWriter.

  6. The writer provides all methods, according to the subset specification, to write the data the segment, its components and fields. The returned value of invoice.getInvoiceNr() method will be set to e1004 field.

The example above produces the following:

UNA:+.? '
UNB+UNOB:2+0000000000001:14+0000000000002:14+210101:0020+ABCD123+++++EANCOM'
UNH+1+INVOIC:D:96A:UN:EAN008'
BGM+380+INV123456'
DTM+137:20201227:102'

Now, let’s add information about delivery party and invoicee. For that, .sg2() has to be repeated two times

        public String toEdifact(final List<Invoice> invoices) throws Exception {
            return D96A.writer(client)
                .invoic(() -> invoices.stream(), (invoice, invoic) -> invoic
                    //...
                    .sg2(sg2 -> sg2.data(sg2_ -> sg2_  // (1)
                        .nad(nad -> nad.data(nad_ -> nad_
                            .e3035PartyQualifier("DP") // Ship To
                            .c082PartyIdentificationDetails(c082 -> c082
                                .e3039PartyIdIdentification(invoice.getShipToNr())
                                .e3055CodeListResponsibleAgencyCoded("9")))))) // GLN
                    .sg2(sg2 -> sg2.data(sg2_ -> sg2_ // (2)
                        .nad(nad -> nad.data(nad_ -> nad_
                            .e3035PartyQualifier("IV") // Invoicee
                            .c082PartyIdentificationDetails(c082 -> c082
                                .e3039PartyIdIdentification(invoice.getInvoiceeNr())
                                .e3055CodeListResponsibleAgencyCoded("9"))))  // GLN
                        .sg3(sg3 -> sg3.data(sg3_ -> sg3_
                            .rff(rff -> rff.data(rff_ -> rff_ // (3)
                                .c506Reference(c506 -> c506
                                    .e1153ReferenceQualifier("VA") // VAT ID
                                    .e1154ReferenceNumber(invoice.getSupplierVatId()))))))))
                    //...
                    .unt())
                .dumpToString();
        }
  1. The fist write operation .sg2() declares delivery party and its GLN.

  2. The second write operation .sg2() declares invoicee and its GLN.

  3. Additionally, the VAT ID for invoicee has to be provided. So, a nested .sg3() is declared.

The example above produces the following:

UNH+1+INVOIC:D:96A'
NAD+DP+0000000000004::9'
NAD+IV+0000000000004::9'
RFF+VA:DE000000B'
UNT+4+1'

Let’s move on and add line items.

        public String toEdifact(final List<Invoice> invoices) throws Exception {
            return D96A.writer(client)
                .invoic(() -> invoices.stream(), (invoice, invoic) -> invoic
                    // ...
                    .sg25(() -> invoice.getLineItems().stream(), (lineItem, sg25) -> sg25.data(sg25_ -> sg25_ // (1)
                        .lin(lin -> lin.data(lin_ -> lin_
                            .e1082LineItemNumber(lineItem.getPosition())
                            .c212ItemNumberIdentification(c212 -> c212
                                .e7140ItemNumber(lineItem.getSku())
                                .e7143ItemNumberTypeCoded("EN"))))  // EAN
                        .qty(qty -> qty.data(qty_ -> qty_
                            .c186QuantityDetails(c186 -> c186
                                .e6063QuantityQualifier("47") // Invoiced quantity
                                .e6060Quantity(lineItem.getInvoicedQty().intValue()))))
                        .sg28(sg28 -> sg28.data(sg28_ -> sg28_
                            .pri(pri -> pri.data(pri_ -> pri_
                                .c509PriceInformation(c509 -> c509
                                    .e5125PriceQualifier("AAA") // Calculation net
                                    .e5118Price(lineItem.getPiecePriceNetAmount())
                                    .e6411MeasureUnitQualifier(lineItem.getPiecePriceNetUnit())
                                    .e5375PriceTypeCoded("CT") // Contract
                                    .e5387PriceTypeQualifier("NTP")))))))) // Net unit price
                    // ...
                    .unt())
                .dumpToString();
        }
  1. The line items are declared in the .sg25(). The () → invoice.getLineItems().stream() producer will create exact amount of Segment Group 25 as the invoice.getLineItems() list has. The mapping function (lineItem, sg25) → …​ maps the POJO to the segment.

The example above produces the following:

UNH+1+INVOIC:D:96A'
LIN+1++9783898307529:EN'
QTY+47:5'
PRI+AAA:27.5:CT:NTP::PCE'
LIN+2++9783898307539:EN'
QTY+47:1'
PRI+AAA:10.87:CT:NTP::PCE'
LIN+3++97838983938472:EN'
QTY+47:5'
PRI+AAA:3.85:CT:NTP::PCE'
UNT+10+1'

The source code for this example is available on GitHub.

Supported subsets

  1. Currently supported subsets and their message types

{
    "d01b": [ "DELFOR", "DESADV", "IFTMIN", "INVOIC", "INVRPT", "ORDERS", "ORDRSP", "PRICAT", "SLSRPT" ],
    "d04a": [ "DELFOR", "DESADV", "IFTMIN", "INVOIC", "INVRPT", "ORDERS", "ORDRSP", "PRICAT", "SLSRPT" ],
    "d07a": [ "DELFOR", "DESADV", "IFTMIN", "INVOIC", "INVRPT", "ORDERS", "ORDRSP", "PRICAT", "SLSRPT" ],
    "d96a": [ "DELFOR", "DESADV", "IFTMIN", "INVOIC", "INVRPT", "ORDERS", "ORDRSP", "PRICAT", "SLSRPT" ],
    "d96b": [ "DELFOR", "DESADV", "IFTMIN", "INVOIC", "INVRPT", "ORDERS", "ORDRSP", "PRICAT", "SLSRPT" ],
    "d99a": [ "DELFOR", "DESADV", "IFTMIN", "INVOIC", "INVRPT", "ORDERS", "ORDRSP", "PRICAT", "SLSRPT" ]
}
Note
Support for a subset or a message can be added by request.

Integration scenarios

Checkout our repository on GitHub with various integration scenarios.