diff --git a/src/main/resources/i18n/labels.xml b/src/main/resources/i18n/labels.xml index d9cee77..ee4c081 100644 --- a/src/main/resources/i18n/labels.xml +++ b/src/main/resources/i18n/labels.xml @@ -28,6 +28,7 @@ Unikalny numer wiersza Wartość netto Stan przed + Kurs waluty Tak Duplikat z dnia Faktura zaliczkowa @@ -67,6 +68,7 @@ Korekta skutkująca w dacie innej, w tym gdy dla różnych pozycji faktury korygującej daty te są różne Miejsce wystawienia Kurs waluty + Kurs waluty wspólny dla wszystkich wierszy faktury Okres, którego dotyczy rabat Data kursu waluty Numery wcześniejszych faktur zaliczkowych diff --git a/src/main/resources/i18n/labels_en.xml b/src/main/resources/i18n/labels_en.xml index 2222175..7c11d32 100644 --- a/src/main/resources/i18n/labels_en.xml +++ b/src/main/resources/i18n/labels_en.xml @@ -28,6 +28,7 @@ Unique row ID Net value State before + Exchange rate Yes Duplicate from Advance Invoice @@ -67,6 +68,7 @@ Correction effective on another date, including when dates differ for various items Place of issue Exchange rate + Common exchange rate for all invoice rows Discount period Exchange rate date Previous advance invoice numbers diff --git a/src/main/resources/templates/fa3/common-functions.xsl b/src/main/resources/templates/fa3/common-functions.xsl index 003dfce..1bd8aa1 100644 --- a/src/main/resources/templates/fa3/common-functions.xsl +++ b/src/main/resources/templates/fa3/common-functions.xsl @@ -61,4 +61,10 @@ + + + + + + diff --git a/src/main/resources/templates/fa3/invoice-rows.xsl b/src/main/resources/templates/fa3/invoice-rows.xsl index b9bd202..e5fd5d3 100644 --- a/src/main/resources/templates/fa3/invoice-rows.xsl +++ b/src/main/resources/templates/fa3/invoice-rows.xsl @@ -29,13 +29,30 @@ 4pt + + + + + + + + + + + + + + + - + @@ -52,7 +69,8 @@ - + + @@ -105,6 +123,9 @@ + + + @@ -191,6 +212,11 @@ + + + + + @@ -212,6 +238,7 @@ + @@ -233,6 +260,7 @@ + @@ -499,6 +527,13 @@ + + + + + + + diff --git a/src/main/resources/templates/fa3/ksef_invoice.xsl b/src/main/resources/templates/fa3/ksef_invoice.xsl index b6591ea..d819c27 100644 --- a/src/main/resources/templates/fa3/ksef_invoice.xsl +++ b/src/main/resources/templates/fa3/ksef_invoice.xsl @@ -865,11 +865,14 @@ - - + + : + + + diff --git a/src/test/java/io/alapierre/ksef/fop/ExchangeRateTest.java b/src/test/java/io/alapierre/ksef/fop/ExchangeRateTest.java new file mode 100644 index 0000000..59cee8b --- /dev/null +++ b/src/test/java/io/alapierre/ksef/fop/ExchangeRateTest.java @@ -0,0 +1,123 @@ +package io.alapierre.ksef.fop; + +import org.junit.jupiter.api.Test; +import org.w3c.dom.Node; +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Tests how the currency exchange rate ({@code crd:KursWaluty}) is visualized on FA(3) invoices. + */ +class ExchangeRateTest extends AbstractStyleSheetTest { + + private static final String INVOICE_XPATH = "/fa3:Faktura"; + private static final String EXCHANGE_RATE_HEADER_XPATH = "//fo:block[@id='exchangeRate']"; + private static final String EXCHANGE_RATE_NOTE_HEADER_XPATH = "//fo:block[@id='exchangeRateCommonNote']"; + private static final String FIRST_ROW_XPATH = "(//fo:table-cell[starts-with(@id,'lineExchangeRate')])[1]"; + private static final String SECOND_ROW_XPATH = "(//fo:table-cell[starts-with(@id,'lineExchangeRate')])[2]"; + + @Test + void shouldCorrectlyDisplayDifferentLineRatesForVatInvoice() throws Exception { + URL input = resource("ExchangeRateTest/vat_different_rates_in_rows.xml"); + + String firstRowExpectedRate = "4.3200"; + String secondRowExpectedRate = "4.500"; + + Node exchangeRateHeader = transformFa3Invoice(input, INVOICE_XPATH, EXCHANGE_RATE_HEADER_XPATH); + Node exchangeRateNoteHeader = transformFa3Invoice(input, INVOICE_XPATH, EXCHANGE_RATE_NOTE_HEADER_XPATH); + Node firstRow = transformFa3Invoice(input, INVOICE_XPATH, FIRST_ROW_XPATH); + Node secondRow = transformFa3Invoice(input, INVOICE_XPATH, SECOND_ROW_XPATH); + + assertNull(exchangeRateNoteHeader, "Exchange rate header note should not be visible when rows have different rates"); + assertNull(exchangeRateHeader, "Exchange rate header should not be visible when rows have different rates"); + assertTrue(firstRow.getTextContent().contains(firstRowExpectedRate), + () -> "Expected line 1 exchange rate " + firstRowExpectedRate + " but got: " + firstRow.getTextContent()); + assertTrue(secondRow.getTextContent().contains(secondRowExpectedRate), + () -> "Expected line 2 exchange rate " + secondRowExpectedRate + " but got: " + secondRow.getTextContent()); + } + + @Test + void shouldCorrectlyDisplaySameLineRatesForVatInvoice() throws Exception { + URL input = resource("ExchangeRateTest/vat_same_rates_in_rows.xml"); + + String firstRowExpectedRate = "4.3200"; + String commonRateExpectedNote = "Kurs waluty wspólny dla wszystkich wierszy faktury"; + + Node exchangeRateHeader = transformFa3Invoice(input, INVOICE_XPATH, EXCHANGE_RATE_HEADER_XPATH); + Node exchangeRateNoteHeader = transformFa3Invoice(input, INVOICE_XPATH, EXCHANGE_RATE_NOTE_HEADER_XPATH); + Node firstRow = transformFa3Invoice(input, INVOICE_XPATH, FIRST_ROW_XPATH); + + assertNotNull(exchangeRateNoteHeader, "Exchange rate header should be visible when rows have equal rates"); + assertTrue(exchangeRateHeader.getTextContent().contains(firstRowExpectedRate), + () -> "Expected common exchange rate " + firstRowExpectedRate + + " but got: " + exchangeRateHeader.getTextContent()); + + assertTrue(exchangeRateNoteHeader.getTextContent().contains(commonRateExpectedNote), + () -> "Expected common exchange rate note but got: " + exchangeRateNoteHeader.getTextContent()); + assertNull(firstRow, "Per-line exchange rate column must not be shown when all line rates are equal"); + } + + @Test + void shouldCorrectlyDisplayIncompleteLineRatesForVatInvoice() throws Exception { + URL input = resource("ExchangeRateTest/vat_incomplete_rates_in_rows.xml"); + + String firstRowExpectedRate = "4.3200"; + + Node exchangeRateHeader = transformFa3Invoice(input, INVOICE_XPATH, EXCHANGE_RATE_HEADER_XPATH); + Node exchangeRateNoteHeader = transformFa3Invoice(input, INVOICE_XPATH, EXCHANGE_RATE_NOTE_HEADER_XPATH); + Node firstRow = transformFa3Invoice(input, INVOICE_XPATH, FIRST_ROW_XPATH); + + assertNull(exchangeRateHeader, + "Exchange rate header should not be visible when rows have incomplete rates"); + assertNull(exchangeRateNoteHeader, + "Exchange rate header note should not be visible when rows have incomplete rates"); + + assertNotNull(firstRow, + "Exchange rate should be shown per line when rows have incomplete rates"); + assertTrue(firstRow.getTextContent().contains(firstRowExpectedRate), + () -> "Expected line 1 exchange rate " + firstRowExpectedRate + " but got: " + firstRow.getTextContent()); + } + + @Test + void shouldCorrectlyDisplayCorrectedExchangeRates() throws Exception { + URL input = resource("faktury/fa3/korygujaca/FA_3_Przyklad_3.xml"); + + String beforeCorrectionRate = "4.2106"; + String afterCorrectionRate = "4.2142"; + + Node exchangeRateHeader = transformFa3Invoice(input, INVOICE_XPATH, EXCHANGE_RATE_HEADER_XPATH); + Node exchangeRateNoteHeader = transformFa3Invoice(input, INVOICE_XPATH, EXCHANGE_RATE_NOTE_HEADER_XPATH); + Node firstRow = transformFa3Invoice(input, INVOICE_XPATH, FIRST_ROW_XPATH); + Node secondRow = transformFa3Invoice(input, INVOICE_XPATH, SECOND_ROW_XPATH); + + assertNull(exchangeRateHeader, + "Common exchange rate should not be shown when the rate is corrected"); + assertNull(exchangeRateNoteHeader, + "Common exchange rate note should not be shown when the rate is corrected"); + assertNotNull(firstRow, + "Exchange rate should be shown per line for a rate correction"); + assertTrue(firstRow.getTextContent().contains(beforeCorrectionRate), + () -> "Expected before-correction rate " + beforeCorrectionRate + " but got: " + firstRow.getTextContent()); + assertTrue(secondRow.getTextContent().contains(afterCorrectionRate), + () -> "Expected after-correction rate " + afterCorrectionRate + " but got: " + secondRow.getTextContent()); + } + + @Test + void shouldHideExchangeRateForCorrectionWithoutRateChange() throws Exception { + URL input = resource("ExchangeRateTest/correction_unchanged_rate.xml"); + + Node exchangeRateHeader = transformFa3Invoice(input, INVOICE_XPATH, EXCHANGE_RATE_HEADER_XPATH); + Node exchangeRateNoteHeader = transformFa3Invoice(input, INVOICE_XPATH, EXCHANGE_RATE_NOTE_HEADER_XPATH); + Node firstRow = transformFa3Invoice(input, INVOICE_XPATH, FIRST_ROW_XPATH); + + assertNull(exchangeRateHeader, + "Common exchange rate header should not be shown on a correction that does not change the rate"); + assertNull(exchangeRateNoteHeader, + "Common exchange rate note should not be shown on a correction that does not change the rate"); + assertNull(firstRow, + "Per-line exchange rate column should not be shown on a correction that does not change the rate"); + } +} diff --git a/src/test/resources/ExchangeRateTest/correction_unchanged_rate.xml b/src/test/resources/ExchangeRateTest/correction_unchanged_rate.xml new file mode 100644 index 0000000..3ad2fa1 --- /dev/null +++ b/src/test/resources/ExchangeRateTest/correction_unchanged_rate.xml @@ -0,0 +1,87 @@ + + + + FA + 3 + 2026-02-25T10:33:34.1462546Z + Aplikacja Podatnika KSeF + + + + 1234567890 + TEST SP z.o.o. + + + PL + ul. Test-Wilcza 1 Nowy Sącz 33-300 Nowy Sącz + + + + + 1234567890 + TEST 2 SP z.o.o. + + + PL + ul. Test-Wilcza 1 Nowy Sącz 33-300 Nowy Sącz + + 2 + 2 + + + EUR + 2026-02-25 + 001/01/26/K + 2026-01-01 + 0 + 0 + 0.66 + 0 + + 2 + 2 + 2 + 2 + + 1 + + + 1 + + 2 + + 1 + + + KOR + Błędna ilość. + + 2026-02-13 + 001/01/26 + 1 + 210415750-20250310-FF4861C9CC3F-BA + + + 1 + Usługa transportowa + szt. + 1 + 800 + 800 + 23 + 4.2106 + 1 + + + 1 + Usługa transportowa + szt. + 2 + 800 + 1600 + 23 + 4.2106 + + + diff --git a/src/test/resources/ExchangeRateTest/vat_different_rates_in_rows.xml b/src/test/resources/ExchangeRateTest/vat_different_rates_in_rows.xml new file mode 100644 index 0000000..54a19b7 --- /dev/null +++ b/src/test/resources/ExchangeRateTest/vat_different_rates_in_rows.xml @@ -0,0 +1,99 @@ + + + + FA + 3 + 2025-12-02T15:48:23.5668913Z + Aplikacja Podatnika KSeF + + + PL + + 5451824119 + test + + + PL + test + + + + + 1111111111 + eestest + + + PL + test + test + + 2 + 2 + + + EUR + 2025-12-04 + Warszawa + 15/2024 + 2024-10-21 + 100 + 23 + 99.36 + 123 + + 2 + 2 + 2 + 2 + + 1 + + + 1 + + 2 + + 1 + + + VAT + + 1 + Pierwsza pozycja + szt + 1 + 123 + 123 + 23 + 4.3200 + + + 2 + Druga pozycja + szt + 1 + 123 + 123 + 23 + 4.500 + + + 1 + + 100 + 2025-01-01 + + + 200 + 2025-02-01 + + + 300 + 2025-03-01 + + + + \ No newline at end of file diff --git a/src/test/resources/ExchangeRateTest/vat_incomplete_rates_in_rows.xml b/src/test/resources/ExchangeRateTest/vat_incomplete_rates_in_rows.xml new file mode 100644 index 0000000..04e24e5 --- /dev/null +++ b/src/test/resources/ExchangeRateTest/vat_incomplete_rates_in_rows.xml @@ -0,0 +1,98 @@ + + + + FA + 3 + 2025-12-02T15:48:23.5668913Z + Aplikacja Podatnika KSeF + + + PL + + 5451824119 + test + + + PL + test + + + + + 1111111111 + eestest + + + PL + test + test + + 2 + 2 + + + EUR + 2025-12-04 + Warszawa + 15/2024 + 2024-10-21 + 100 + 23 + 99.36 + 123 + + 2 + 2 + 2 + 2 + + 1 + + + 1 + + 2 + + 1 + + + VAT + + 1 + Pierwsza pozycja + szt + 1 + 123 + 123 + 23 + 4.3200 + + + 2 + Druga pozycja + szt + 1 + 123 + 123 + 23 + + + 1 + + 100 + 2025-01-01 + + + 200 + 2025-02-01 + + + 300 + 2025-03-01 + + + + diff --git a/src/test/resources/ExchangeRateTest/vat_same_rates_in_rows.xml b/src/test/resources/ExchangeRateTest/vat_same_rates_in_rows.xml new file mode 100644 index 0000000..d619494 --- /dev/null +++ b/src/test/resources/ExchangeRateTest/vat_same_rates_in_rows.xml @@ -0,0 +1,99 @@ + + + + FA + 3 + 2025-12-02T15:48:23.5668913Z + Aplikacja Podatnika KSeF + + + PL + + 5451824119 + test + + + PL + test + + + + + 1111111111 + eestest + + + PL + test + test + + 2 + 2 + + + EUR + 2025-12-04 + Warszawa + 15/2024 + 2024-10-21 + 100 + 23 + 99.36 + 123 + + 2 + 2 + 2 + 2 + + 1 + + + 1 + + 2 + + 1 + + + VAT + + 1 + Pierwsza pozycja + szt + 1 + 123 + 123 + 23 + 4.3200 + + + 2 + Druga pozycja + szt + 1 + 123 + 123 + 23 + 4.32 + + + 1 + + 100 + 2025-01-01 + + + 200 + 2025-02-01 + + + 300 + 2025-03-01 + + + +