Home > database >  XSLT: populating strings from multiple nodes --> specific nodes and number of nodes can vary. How
XSLT: populating strings from multiple nodes --> specific nodes and number of nodes can vary. How

Time:10-12

We are importing Peppol vendor invoices into our ERP-system by converting then into to another xml format using xslt. An example of a such an invoice is here:Github: OpenPEPPOL / peppol-bis-invoice-3

I want to give my accountant full freedom regarding which fields to be populated in each ERP-vendor invoice journal field. I want to give them a choice from a predefined set of nodes to use, for example:

/Invoice/ID /Invoice/IssueDate 
/Invoice/AccountingSupplierParty/Party/PartyName/Name 
/Invoice/InvoiceLine/InvoicedQuantity
/Invoice/InvoiceLine/Item/Name

I import specific vendor information from the ERP-system to use in the transformation process, for example to look up the vendor account:

<vendors>
    <vendor>
        <administration>YIT</administration>
        <FISCALCODE>04705810150</FISCALCODE>
        <accountNumber>20003</accountNumber>
        <Offset_LedgerAccount>67123</Offset_LedgerAccount>
        <name>A.Manzoni&amp;C. Spa</name>
    </vendor>
<vendors>

I lookup this information in the vendor xml table using the xslt below:

<xsl:variable name="LegalEntity">  
<xsl:choose>
<xsl:when test="contains(cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name, 'Italia')">YIT</xsl:when>
<xsl:otherwise>'UNKNOWN LegalEntity'</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="current-vendor-name" select="cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name"/>
<xsl:variable name="vendor-data" select="document('Vendors.xml')/vendors/vendor[name=$current-vendor-name and administration=$LegalEntity]"/>

The output of the transformation (when applied to the example input file in the link) is currently like below:

<?xml version="1.0" encoding="utf-8"?>
<PurchaseInvoices_version_1.0>
   <PurchaseInvoice>
      <LegalEntity>YIT</LegalEntity>
      <SupplierAccountNum>20202</SupplierAccountNum>
      <SupplierBankAccount>IBAN32423940</SupplierBankAccount>
      <SupplierName>SupplierTradingName Ltd.</SupplierName>
      <SupplierCity>London</SupplierCity>
      <CurrencyCode>EUR</CurrencyCode>
      <AmountExclTax>1325</AmountExclTax>
      <AmountInclTax>1656.25</AmountInclTax>
      <TaxAmount>331.25</TaxAmount>
      <InvoiceId>Snippet1</InvoiceId>
      <InvoiceDate>13-11-2017</InvoiceDate>
      <PaymentNote>Payment within 10 days, 2% discount</PaymentNote>
      <TaxCode>S</TaxCode>
      <Lines>
         <Line>
            <Item>|Description of item</Item>
            <Quantity>7</Quantity>
            <UnitPrice>400</UnitPrice>
            <LineAmount>2800</LineAmount>
            <AmountIncl>3500</AmountIncl>
            <AmountExcl>2800</AmountExcl>
            <TaxAmount>700</TaxAmount>
            <TaxCode>S</TaxCode>
            <Description>Description of item|2017-11-13</Description>
            <Unit>pcs</Unit>
         </Line>
         <Line>
            <Item>|Description 2</Item>
            <Quantity>-3</Quantity>
            <UnitPrice>500</UnitPrice>
            <LineAmount>-1500</LineAmount>
            <AmountIncl>-1875</AmountIncl>
            <AmountExcl>-1500</AmountExcl>
            <TaxAmount>-375</TaxAmount>
            <TaxCode>S</TaxCode>
            <Description>Description 2|2017-11-13</Description>
            <Unit>pcs</Unit>
         </Line>
      </Lines>
   </PurchaseInvoice>
</PurchaseInvoices_version_1.0>

I want to create more freedom regarding which text (nodes) from the input xml to use in the Description field as well as the order in which to show them. This should be defined per vendor on the vendor card and imported into the vendors.xml file of which a sample is shown above.

For example for vendor A the description could be a concatenation of all nodes:

/Invoice/ID /Invoice/IssueDate 
/Invoice/InvoiceLine/Item/Name
/Invoice/InvoiceLine/InvoicedQuantity
/Invoice/AccountingSupplierParty/Party/PartyName/Name 

While for vendor B the description could be only:

/Invoice/InvoiceLine/Item/Name

The question is how to proceed from here.

My current idea is to create a description based on fixed number of variables:

<Description><xsl:value-of select="$A"/><xsl:value-of select="$B"/><xsl:value-of select="$C"/><xsl:value-of select="$D"/></Description>

And put the correct nodes in each variable based on some kind of input on Vendor card which is imported into the vendors.xml file and used in the xslt script.

CodePudding user response:

Going with your original idea of using a numeric code for the order of the values to be contained in the description, consider the following, much simplified, example.

XML

<Invoice>
    <ID>001</ID>
    <DueDate>2002-01-01</DueDate>
    <LineItem>
        <Name>Widget</Name>
        <Quantity>5</Quantity>
        <Description>Description of widget</Description>
    </LineItem>
    <LineItem>
        <Name>Gadget</Name>
        <Quantity>17</Quantity>
        <Description>Description of gadget</Description>
    </LineItem>
</Invoice>

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:param name="reorder">030102</xsl:param>

<xsl:template match="/Invoice">
    <PurchaseInvoice Id="{ID}">
        <!-- ... -->
        <xsl:for-each select="LineItem">
            <Line>
                <!-- ... -->
                <Description>
                    <xsl:call-template name="reorder">
                        <xsl:with-param name="nodes" select="../DueDate | Name | Description"/>
                        <xsl:with-param name="order" select="$reorder"/>
                    </xsl:call-template>
                </Description>
            </Line>
        </xsl:for-each>
    </PurchaseInvoice>
</xsl:template>

<xsl:template name="reorder">
    <xsl:param name="nodes" select="/.."/>
    <xsl:param name="order"/>
    <xsl:value-of select="$nodes[number(substring($order, 1 , 2))]"/>
    <xsl:if test="string-length($order) > 2">
        <xsl:text> | </xsl:text>
        <!-- recursive call -->
        <xsl:call-template name="reorder">
            <xsl:with-param name="nodes" select="$nodes"/>
            <xsl:with-param name="order" select="substring($order, 3)"/>
        </xsl:call-template>
    </xsl:if>
</xsl:template>

</xsl:stylesheet>

Result

<?xml version="1.0" encoding="UTF-8"?>
<PurchaseInvoice Id="001">
  <Line>
    <Description>Description of widget | 2002-01-01 | Widget</Description>
  </Line>
  <Line>
    <Description>Description of gadget | 2002-01-01 | Gadget</Description>
  </Line>
</PurchaseInvoice>

To keep this simple, you must assign the numeric codes to the values listed in the nodes parameter in the order in which they appear in the input XML.

In this example the code is a global parameter; in your implementation you will want to retrieve it from the vendors document.


Added:

If you prefer, you can use descriptive strings instead of numeric codes to select the order. But then the stylesheet becomes more complex:

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:param name="reorder">description|due date|name</xsl:param>

<xsl:template match="/Invoice">
    <PurchaseInvoice Id="{ID}">
        <!-- ... -->
        <xsl:for-each select="LineItem">
            <Line>
                <!-- ... -->
                <Description>
                    <xsl:call-template name="reorder">
                        <xsl:with-param name="order" select="$reorder"/>
                    </xsl:call-template>
                </Description>
            </Line>
        </xsl:for-each>
    </PurchaseInvoice>
</xsl:template>

<xsl:template name="reorder">
    <xsl:param name="order"/>
    <xsl:param name="delimiter" select="'|'"/>
    <xsl:variable name="token" select="substring-before(concat($order, $delimiter), $delimiter)" />
    <xsl:choose>
        <xsl:when test="$token='due date'">
            <xsl:value-of select="../DueDate"/>
        </xsl:when>
        <xsl:when test="$token='name'">
            <xsl:value-of select="Name"/>
        </xsl:when>
        <xsl:when test="$token='description'">
            <xsl:value-of select="Description"/>
        </xsl:when>
    </xsl:choose>
    <xsl:if test="contains($order, $delimiter)">
        <xsl:text> | </xsl:text>
        <!-- recursive call -->
        <xsl:call-template name="reorder">
            <xsl:with-param name="order" select="substring-after($order, $delimiter)"/>
        </xsl:call-template>
    </xsl:if>
</xsl:template>

</xsl:stylesheet>
  • Related