Merge branch 'ZUGFeRD:master' into master

This commit is contained in:
Kemal Taskin
2025-01-13 11:45:40 +01:00
committed by GitHub
11 changed files with 336 additions and 224 deletions

View File

@@ -1,15 +1,25 @@
- 639
- 633
-626,
-622,
-356
allow to add includedNotes with type
- - 645
- 631 multiple invoice referenced documents
- 629
- 630 #296 #565
- 657
- 658
2.16.0
=======
2025-01-10
- #657 allow allowancechargereasoncodes on document level
- allow to add includedNotes with type
- #356 print version of xml report
- #645 Fix visualization of validation logs
- #639 Fix invoice calculation if rounding amount is present
- #633 Bump ch.qos.logback:logback-core from 1.2.13 to 1.5.13
- #626 Fix minor java issues
- #622 Fix FOP config
- #631 multiple invoice referenced documents
- #629 Visualizing xml
- #630 Fix issue #296 (Validation-Error: Ungültiger Content wurde beginnend mit Element 'ram:DueDateDateTime' ) (duplicate of #565)
- #658 prevent nullpointerexception
- #648 Fix log visualization
- #651 ZUGFeRDInvoiceImporter: Item-Allowances not imported
- #652 Discount VAT is not subtracted from duepayable
- #620 Fix logback config
- #653 ZF2EdgeTest: methods 'getPaymentMeansCode()' & 'getPaymentMeansInformation()' does not override super methods
- #654 remove wrong test methods
2.15.2
=======

View File

@@ -37,15 +37,10 @@
<!-- logging -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.13</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.5.13</version>
<version>1.5.16</version>
</dependency>
<dependency>
<!-- This library is needed so that logback stderr output is sent to, well, stderr, otherwise it lands in stdout -->

View File

@@ -1,38 +1,36 @@
<configuration>
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<if condition='property("FILE_APPENDER_ENABLED").contains("true")'>
<then>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender" >
<!--<appender name="FILE" class="org.mustangproject.commandline.LazyRollingFileAppender" >-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- Daily rollover -->
<fileNamePattern>log/ZUV-%d{yyyy-MM}.log</fileNamePattern>
<!-- Keep 5 years worth of history -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
</then>
</if>
<!-- Configure so that it outputs to both console and log file -->
<root level="INFO">
<if condition='property("FILE_APPENDER_ENABLED").contains("true")'>
<then>
<appender-ref ref="FILE" />
</then>
</if>
<appender-ref ref="STDERR" />
</root>
</configuration>
<configuration>
<appender name="STDERR"
class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<if condition='property("FILE_APPENDER_ENABLED").contains("true")'>
<then>
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- Daily rollover -->
<fileNamePattern>log/ZUV-%d{yyyy-MM}.log</fileNamePattern>
<!-- Keep 5 years worth of history -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
</then>
</if>
<!-- Configure so that it outputs to both console and log file -->
<if condition='property("FILE_APPENDER_ENABLED").contains("true")'>
<then>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</then>
</if>
<root level="INFO">
<appender-ref ref="STDERR"/>
</root>
</configuration>

View File

@@ -172,6 +172,32 @@ public class Item implements IZUGFeRDExportableItem {
icnm.getAsNodeMap("ApplicableTradeTax")
.flatMap(cnm -> cnm.getAsBigDecimal("RateApplicablePercent", "ApplicablePercent"))
.ifPresent(product::setVATPercent);
icnm.getAsNodeMap("SpecifiedTradeAllowanceCharge").ifPresent(stac -> {
stac.getAsNodeMap("ChargeIndicator").ifPresent(ci -> {
String isChargeString=ci.getAsString("Indicator").get();
String percentString=stac.getAsStringOrNull("CalculationPercent");
String amountString=stac.getAsStringOrNull("ActualAmount");
String reason=stac.getAsStringOrNull("Reason");
Charge izac= new Charge();
if (isChargeString.equalsIgnoreCase("false")) {
izac = new Allowance();
} else {
izac = new Charge();
}
if (amountString!=null) {
izac.setTotalAmount(new BigDecimal(amountString));
}
izac.setPercent(new BigDecimal(percentString));
izac.setReason(reason);
if (isChargeString.equalsIgnoreCase("false")) {
addAllowance(izac);
} else {
addCharge(izac);
}
});
});
if (recalcPrice && !BigDecimal.ZERO.equals(quantity)) {
icnm.getAsNodeMap("SpecifiedTradeSettlementLineMonetarySummation")

View File

@@ -55,7 +55,7 @@ public class LineCalculator {
BigDecimal basisQuantity = currentItem.getBasisQuantity().compareTo(BigDecimal.ZERO) == 0
? BigDecimal.ONE.setScale(4)
: currentItem.getBasisQuantity();
itemTotalNetAmount = quantity.multiply(getPrice()).divide(basisQuantity, 18, RoundingMode.HALF_UP)
itemTotalNetAmount = quantity.multiply(price).divide(basisQuantity, 18, RoundingMode.HALF_UP)
.subtract(allowanceItemTotal).setScale(2, RoundingMode.HALF_UP);
itemTotalVATAmount = itemTotalNetAmount.multiply(multiplicator);
}

View File

@@ -897,7 +897,7 @@ public class ZUGFeRDInvoiceImporter {
// be read,
// so the invoice remains arithmetically correct
// -> parse document level charges+allowances
xpr = xpath.compile("//*[local-name()=\"SpecifiedTradeAllowanceCharge\"]|//*[local-name()=\"AllowanceCharge\"]");//CII and UBL
xpr = xpath.compile("//*[local-name()=\"ApplicableHeaderTradeSettlement\"]/*[local-name()=\"SpecifiedTradeAllowanceCharge\"]|/*[local-name()=\"AllowanceCharge\"]");//CII and UBL
NodeList chargeNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET);
for (int i = 0; i < chargeNodes.getLength(); i++) {
NodeList chargeNodeChilds = chargeNodes.item(i).getChildNodes();

View File

@@ -1,33 +1,37 @@
<configuration>
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<if condition='property("FILE_APPENDER_ENABLED").contains("true")'>
<then>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- Daily rollover -->
<fileNamePattern>log/ZUV-%d{yyyy-MM}.log</fileNamePattern>
<!-- Keep 5 years worth of history -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
</then>
</if>
<!-- Configure so that it outputs to both console and log file -->
<root level="INFO">
<appender-ref ref="FILE"/>
<appender-ref ref="STDERR"/>
</root>
</configuration>
<configuration>
<appender name="STDERR"
class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<if condition='property("FILE_APPENDER_ENABLED").contains("true")'>
<then>
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- Daily rollover -->
<fileNamePattern>log/ZUV-%d{yyyy-MM}.log</fileNamePattern>
<!-- Keep 5 years worth of history -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
</then>
</if>
<!-- Configure so that it outputs to both console and log file -->
<if condition='property("FILE_APPENDER_ENABLED").contains("true")'>
<then>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</then>
</if>
<root level="INFO">
<appender-ref ref="STDERR"/>
</root>
</configuration>

View File

@@ -9,11 +9,11 @@
<xsl:import href="common-xr.xsl"/>
<xsl:import href="xr-pdf/lib/konstanten.xsl"/>
<!--
FO engine used can be specified.
Specific extensions will be then enabled.
Supported values are:
axf - Antenna House XSL Formatter
fop - Apache FOP
FO engine used can be specified.
Specific extensions will be then enabled.
Supported values are:
axf - Antenna House XSL Formatter
fop - Apache FOP
-->
<xsl:param name="foengine"/>
<xsl:param name="axf.extensions"
@@ -46,7 +46,7 @@
</xsl:variable>
<xsl:variable name="result_text">
<xsl:if test="/validation/xml/summary/@status = 'valid'">
<xsl:text>Es wird empfohlen, das Dokument anzunehmen und weiterzuverarbeiten.</xsl:text>
<xsl:text>Es wird empfohlen, das Dokument anzunehmen und es weiterzuverarbeiten.</xsl:text>
</xsl:if>
<xsl:if test="/validation/xml/summary/@status = 'invalid'">
<xsl:text>Es wird empfohlen, das Dokument zurückzuweisen.</xsl:text>
@@ -71,47 +71,59 @@
</fo:block>
<xsl:call-template name="SubHeader">
<xsl:with-param name="text"
select="&#34;Angaben zum geprüften Dokument&#34;"/>
select="'Angaben zum geprüften Dokument'"/>
<xsl:with-param name="color"
select="&#34;black&#34;"/>
select="'black'"/>
</xsl:call-template>
<fo:block>
<fo:block text-align="justify">
<fo:float float="right">
<fo:block>
<xsl:value-of select="/validation/@filename"/>
</fo:block>
</fo:float>
<xsl:text>Referenz:</xsl:text>
</fo:block>
</fo:block>
<fo:block>
<fo:block text-align="justify">
<fo:float float="right">
<fo:block>
<xsl:value-of select="/validation/@datetime"/>
</fo:block>
</fo:float>
<xsl:text>Zeitpunkt der Prüfung:</xsl:text>
</fo:block>
</fo:block>
<fo:block>
<fo:block text-align="justify">
<fo:float float="right">
<fo:block>
<xsl:value-of select="/validation/xml/info/profile"/>
</fo:block>
</fo:float>
<xsl:text>Erkannter Dokumenttyp:</xsl:text>
</fo:block>
</fo:block>
<xsl:apply-templates select="/validation/pdf"/>
<fo:table>
<fo:table-column border-style="none"/>
<fo:table-column column-width="70%"
border-style="none"/>
<fo:table-body>
<fo:table-row>
<fo:table-cell>
<fo:block text-align="left">Referenz:</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block text-align="right">
<xsl:value-of select="./@filename"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row>
<fo:table-cell>
<fo:block text-align="left">Zeitpunkt der Prüfung:</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block text-align="right">
<xsl:value-of select="./@datetime"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row>
<fo:table-cell>
<fo:block text-align="left">Erkannter Dokumenttyp:</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block text-align="right">
<xsl:call-template name="EnableLineBreaks">
<xsl:with-param name="text"
select="./xml/info/profile"/>
<xsl:with-param name="separator"
select="'#'"/>
</xsl:call-template>
</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
<xsl:apply-templates select="./pdf"/>
<!--
<xsl:call-template name="SubHeader">
<xsl:with-param name="text"
select='"Konformitätsprüfung:"'/>
select=="'Konformitätsprüfung:'"/>
<xsl:with-param name="color"
select='"black"'/>
select=="'black'"/>
</xsl:call-template>
-->
<xsl:call-template name="SubHeader">
@@ -125,35 +137,44 @@
<fo:table-column border-style="solid"/>
<fo:table-column border-style="solid"/>
<fo:table-column border-style="solid"/>
<fo:table-column column-width="70%"
border-style="solid"/>
<fo:table-column border-style="solid"
column-width="68%"/>
<fo:table-header>
<fo:table-row background-color="#E0E0E0"
font-weight="bold">
font-weight="bold"
border-style="solid">
<fo:table-cell>
<fo:block>Type</fo:block>
<fo:block margin="0mm"
padding="1mm">Type</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>Code</fo:block>
<fo:block margin="0mm"
padding="1mm">Code</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>Schwere</fo:block>
<fo:block margin="0mm"
padding="1mm">Schwere</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>Text</fo:block>
<fo:block margin="0mm"
padding="1mm">Text</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-header>
<fo:table-body>
<fo:table-body page-break-after="auto"
page-break-before="auto"
page-break-inside="avoid">
<xsl:choose>
<xsl:when test="/validation/xml/messages">
<xsl:apply-templates select="/validation/xml/messages"/>
<xsl:when test="./xml/messages">
<xsl:apply-templates select="./xml/messages"/>
</xsl:when>
<xsl:otherwise>
<fo:table-row font-size="12px"
border-style="solid">
<fo:table-row border-style="solid"
font-size="12px">
<fo:table-cell number-columns-spanned="4">
<fo:block text-align="center">Es gibt weder Hinweise noch Fehler.</fo:block>
<fo:block margin="0mm"
padding="1mm"
text-align="center">Es gibt keine Hinweise, Warnungen oder Fehler.</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:otherwise>
@@ -164,62 +185,113 @@
</fo:page-sequence>
</fo:root>
</xsl:template>
<xsl:template match="notice|error">
<fo:table-row font-size="12px"
border-style="solid">
<fo:table-cell number-rows-spanned="2">
<fo:block>
<xsl:value-of select="@type"/>
</fo:block>
</fo:table-cell>
<fo:table-cell number-rows-spanned="2">
<fo:block>
<xsl:call-template name="SUBID">
<xsl:with-param name="myparam"
select="substring-after(.,' [ID ')"/>
</xsl:call-template>
</fo:block>
</fo:table-cell>
<fo:table-cell number-rows-spanned="2">
<fo:block>
<xsl:choose>
<xsl:when test="name() = 'error'">
<xsl:attribute name="color">red</xsl:attribute>
<xsl:text>Fehler</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>Hinweis</xsl:text>
</xsl:otherwise>
</xsl:choose>
<xsl:template match="notice|warning|error">
<xsl:variable name="msg_color">
<xsl:choose>
<xsl:when test="name() = 'error'">
<xsl:text>red</xsl:text>
</xsl:when>
<xsl:when test="name() = 'warning'">
<xsl:text>orange</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>black</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="msg_label">
<xsl:choose>
<xsl:when test="name() = 'error'">
<xsl:text>Fehler</xsl:text>
</xsl:when>
<xsl:when test="name() = 'warning'">
<xsl:text>Warnung</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>Hinweis</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="msg_text">
<xsl:choose>
<xsl:when test="contains(., ' [ID ')">
<xsl:value-of select="substring-before(., ' [ID')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="rows-spanned">
<xsl:choose>
<xsl:when test="./@location">
<xsl:value-of select="'2'"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="'1'"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<fo:table-row border-style="solid"
font-size="12px"
keep-with-previous="auto"
keep-with-next="always">
<fo:table-cell number-rows-spanned="{$rows-spanned}">
<fo:block margin="0mm"
padding="1mm">
<xsl:value-of select="./@type"/>
</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>
<xsl:if test="name() = 'error'">
<xsl:attribute name="color">red</xsl:attribute>
</xsl:if>
<xsl:value-of select="substring-before(.,' [ID')"/>
<fo:block margin="0mm"
padding="1mm">
<xsl:value-of select="substring-before(substring-after(., ' [ID '), ']')"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row font-size="10px"
border-style="solid">
<fo:table-cell>
<fo:block>
<xsl:if test="name() = 'error'">
<xsl:attribute name="color">red</xsl:attribute>
</xsl:if>
<xsl:value-of select="concat('Pfad: ', @location)"/>
<fo:block color="{$msg_color}"
margin="0mm"
padding="1mm">
<xsl:value-of select="$msg_label"/>
</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block color="{$msg_color}"
margin="0mm"
padding="1mm">
<xsl:value-of select="$msg_text"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
<xsl:if test="./@location">
<fo:table-row border-style="solid"
font-size="8px"
keep-with-previous="always"
keep-with-next="auto">
<fo:table-cell number-columns-spanned="3">
<fo:block color="{$msg_color}"
margin="0mm"
padding="1mm">
<fo:block font-size="12px">
<xsl:value-of select="'Pfad: '"/>
</fo:block>
<xsl:call-template name="EnableLineBreaks">
<xsl:with-param name="text"
select="./@location"/>
<xsl:with-param name="separator"
select="'/'"/>
</xsl:call-template>
</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:if>
</xsl:template>
<xsl:template match="pdf">
<xsl:call-template name="SubHeader">
<xsl:with-param name="text"
select="concat('ZUGFeRD-PDF: ', $pdf_result_text)"/>
<xsl:with-param name="color"
select="&#34;black&#34;"/>
select="'black'"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="SubHeader">
@@ -234,10 +306,22 @@
<xsl:value-of select="$text"/>
</fo:block>
</xsl:template>
<xsl:template name="SUBID">
<xsl:param name="myparam"/>
<xsl:variable name="myparam.end"
select="substring-before($myparam, ']')"/>
<xsl:value-of select="$myparam.end"/>
<xsl:template name="EnableLineBreaks">
<xsl:param name="text"/>
<xsl:param name="separator"/>
<xsl:for-each select="tokenize($text, $separator)">
<xsl:choose>
<xsl:when test="position() eq 1">
<fo:block>
<xsl:value-of select="."/>
</fo:block>
</xsl:when>
<xsl:otherwise>
<fo:block>
<xsl:value-of select="concat($separator, .)"/>
</fo:block>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

View File

@@ -478,8 +478,8 @@ public class ZF2PushTest extends TestCase {
.addItem(new Item(new Product("Testprodukt", "", "C62", new BigDecimal(19)), amount, new BigDecimal(1.0)))
.addItem(new Item(new Product("Testprodukt", "", "C62", new BigDecimal(19)), amount, new BigDecimal(1.0)))
.addItem(new Item(new Product("Testprodukt", "", "C62", new BigDecimal(19)), amount, new BigDecimal(1.0)))
.addCharge(new Charge(new BigDecimal(0.5)).setTaxPercent(new BigDecimal(19)))
.addAllowance(new Allowance(new BigDecimal(0.2)).setTaxPercent(new BigDecimal(19)))
.addCharge(new Charge(new BigDecimal(0.5)).setTaxPercent(new BigDecimal(19)).setReasonCode("ABK"))
.addAllowance(new Allowance(new BigDecimal(0.2)).setTaxPercent(new BigDecimal(19)).setReasonCode("ABK"))
);
String theXML = new String(ze.getProvider().getXML());
assertTrue(theXML.contains("<rsm:CrossIndustryInvoice"));

View File

@@ -40,7 +40,6 @@ import java.nio.file.Paths;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -103,6 +102,7 @@ public class ZF2ZInvoiceImporterTest extends ResourceCase {
}
public void testInvoiceImportUBL() {

View File

@@ -1,30 +1,25 @@
<configuration>
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- Daily rollover -->
<fileNamePattern>log/ZUV-%d{yyyy-MM}.log</fileNamePattern>
<!-- Keep 5 years worth of history -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Configure so that it outputs to both console and log file -->
<root level="INFO">
<appender-ref ref="FILE" />
<!-- appender-ref ref="STDERR" /-->
</root>
</configuration>
<configuration>
<appender name="STDERR"
class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- Daily rollover -->
<fileNamePattern>log/ZUV-%d{yyyy-MM}.log</fileNamePattern>
<!-- Keep 5 years worth of history -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Configure so that it outputs to both console and log file -->
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</configuration>