diff --git a/src/Apps/BE/PeppolBE/App/src/PEPPOL30BEEscompte.Codeunit.al b/src/Apps/BE/PeppolBE/App/src/PEPPOL30BEEscompte.Codeunit.al
new file mode 100644
index 00000000000..08bae8bf923
--- /dev/null
+++ b/src/Apps/BE/PeppolBE/App/src/PEPPOL30BEEscompte.Codeunit.al
@@ -0,0 +1,78 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.Peppol.BE;
+
+using Microsoft.Finance.GeneralLedger.Setup;
+using Microsoft.Finance.VAT.Calculation;
+using Microsoft.Sales.Document;
+
+///
+/// Shared helpers for the Belgian payment-discount (escompte) compensation. In Belgium VAT is kept on the discounted base, even when the invoice is reported with the full amount.
+/// To avoid reporting a reduced amount payable (the discount is only conditional) a compensating Exempt (category E) breakdown line is added.
+///
+codeunit 37316 "PEPPOL30 BE Escompte"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ CompensationChargeReasonTxt: Label 'Payment discount not deducted from the amount payable';
+ CompensationExemptionReasonTxt: Label 'Conditional early-payment discount, not part of the taxable amount';
+
+ ///
+ /// The VAT Amount Line "VAT Identifier" used to mark this compensation line. Internal to the buffer, not written to the PEPPOL document.
+ ///
+ procedure GetCompensationVATIdentifier(): Code[20]
+ begin
+ exit('ESCOMPTE-COMP');
+ end;
+
+ ///
+ /// The PEPPOL VAT category code used for the compensation line and charge.
+ ///
+ procedure GetExemptTaxCategory(): Code[10]
+ begin
+ exit('E');
+ end;
+
+ ///
+ /// Returns whether the given VAT amount line is the synthetic escompte compensation line.
+ ///
+ procedure IsCompensationLine(VATAmtLine: Record "VAT Amount Line"): Boolean
+ begin
+ exit(VATAmtLine."VAT Identifier" = GetCompensationVATIdentifier());
+ end;
+
+ ///
+ /// The AllowanceChargeReason used on the compensating Exempt charge.
+ ///
+ procedure GetCompensationChargeReason(): Text
+ begin
+ exit(CompensationChargeReasonTxt);
+ end;
+
+ ///
+ /// The VAT exemption reason used on the compensating Exempt VAT breakdown.
+ ///
+ procedure GetCompensationExemptionReason(): Text
+ begin
+ exit(CompensationExemptionReasonTxt);
+ end;
+
+ ///
+ /// Document's currency code (inline with PEPPOL's implementation).
+ ///
+ procedure DocumentCurrencyCode(SalesHeader: Record "Sales Header"): Text
+ var
+ GLSetup: Record "General Ledger Setup";
+ begin
+ if SalesHeader."Currency Code" <> '' then
+ exit(SalesHeader."Currency Code");
+ GLSetup.Get();
+ GLSetup.TestField("LCY Code");
+ exit(GLSetup."LCY Code");
+ end;
+}
diff --git a/src/Apps/BE/PeppolBE/App/src/PEPPOL30BEMonetaryInfo.Codeunit.al b/src/Apps/BE/PeppolBE/App/src/PEPPOL30BEMonetaryInfo.Codeunit.al
new file mode 100644
index 00000000000..79dee0965ce
--- /dev/null
+++ b/src/Apps/BE/PeppolBE/App/src/PEPPOL30BEMonetaryInfo.Codeunit.al
@@ -0,0 +1,104 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.Peppol.BE;
+
+using Microsoft.Finance.VAT.Calculation;
+using Microsoft.Peppol;
+using Microsoft.Sales.Document;
+
+///
+/// Needed to add into the LegalMonetaryTotal the Belgian escompte compensation (if applicable). We are storing the compensation in the VAT Amount Line records.
+///
+codeunit 37318 "PEPPOL30 BE Monetary Info" implements "PEPPOL Monetary Info Provider"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ PEPPOL30: Codeunit "PEPPOL30";
+ Escompte: Codeunit "PEPPOL30 BE Escompte";
+
+ procedure GetLegalMonetaryInfo(SalesHeader: Record "Sales Header"; var TempSalesLine: Record "Sales Line" temporary; var VATAmtLine: Record "VAT Amount Line"; var LineExtensionAmount: Text; var LegalMonetaryTotalCurrencyID: Text; var TaxExclusiveAmount: Text; var TaxExclusiveAmountCurrencyID: Text; var TaxInclusiveAmount: Text; var TaxInclusiveAmountCurrencyID: Text; var AllowanceTotalAmount: Text; var AllowanceTotalAmountCurrencyID: Text; var ChargeTotalAmount: Text; var ChargeTotalAmountCurrencyID: Text; var PrepaidAmount: Text; var PrepaidCurrencyID: Text; var PayableRoundingAmount: Text; var PayableRndingAmountCurrencyID: Text; var PayableAmount: Text; var PayableAmountCurrencyID: Text)
+ var
+ CompensationAmount: Decimal;
+ RealVATBase: Decimal;
+ RealInvDiscount: Decimal;
+ RealPmtDiscount: Decimal;
+ RealAmtInclVAT: Decimal;
+ CurrencyId: Text;
+ begin
+ if not HasCompensationLine(VATAmtLine) then begin
+ PEPPOL30.GetLegalMonetaryInfo(SalesHeader, TempSalesLine, VATAmtLine, LineExtensionAmount, LegalMonetaryTotalCurrencyID, TaxExclusiveAmount, TaxExclusiveAmountCurrencyID, TaxInclusiveAmount, TaxInclusiveAmountCurrencyID, AllowanceTotalAmount, AllowanceTotalAmountCurrencyID, ChargeTotalAmount, ChargeTotalAmountCurrencyID, PrepaidAmount, PrepaidCurrencyID, PayableRoundingAmount, PayableRndingAmountCurrencyID, PayableAmount, PayableAmountCurrencyID);
+ exit;
+ end;
+
+ VATAmtLine.Reset();
+ if VATAmtLine.FindSet() then
+ repeat
+ if Escompte.IsCompensationLine(VATAmtLine) then
+ CompensationAmount += VATAmtLine."VAT Base"
+ else begin
+ RealVATBase += VATAmtLine."VAT Base";
+ RealInvDiscount += VATAmtLine."Invoice Discount Amount";
+ RealPmtDiscount += VATAmtLine."Pmt. Discount Amount";
+ RealAmtInclVAT += VATAmtLine."Amount Including VAT";
+ end;
+ until VATAmtLine.Next() = 0;
+
+ CurrencyId := Escompte.DocumentCurrencyCode(SalesHeader);
+
+ LineExtensionAmount := Format(Round(RealVATBase, 0.01) + Round(RealInvDiscount, 0.01), 0, 9);
+ LegalMonetaryTotalCurrencyID := CurrencyId;
+
+ TaxExclusiveAmount := Format(Round(RealVATBase - RealPmtDiscount + CompensationAmount, 0.01), 0, 9);
+ TaxExclusiveAmountCurrencyID := CurrencyId;
+
+ TaxInclusiveAmount := Format(Round(RealAmtInclVAT - RealPmtDiscount + CompensationAmount, 0.01, '>'), 0, 9);
+ TaxInclusiveAmountCurrencyID := CurrencyId;
+
+ AllowanceTotalAmount := Format(Round(RealInvDiscount + RealPmtDiscount, 0.01), 0, 9);
+ AllowanceTotalAmountCurrencyID := CurrencyId;
+
+ ChargeTotalAmount := Format(Round(CompensationAmount, 0.01), 0, 9);
+ ChargeTotalAmountCurrencyID := CurrencyId;
+
+ PrepaidAmount := '0.00';
+ PrepaidCurrencyID := CurrencyId;
+
+ if TempSalesLine."Line No." = 0 then begin
+ PayableRoundingAmount := Format(RealAmtInclVAT - Round(RealAmtInclVAT, 0.01), 0, 9);
+ PayableRndingAmountCurrencyID := CurrencyId;
+ PayableAmount := Format(Round(RealAmtInclVAT - RealPmtDiscount + CompensationAmount, 0.01), 0, 9);
+ PayableAmountCurrencyID := CurrencyId;
+ end else begin
+ PayableRoundingAmount := Format(TempSalesLine."Amount Including VAT", 0, 9);
+ PayableRndingAmountCurrencyID := CurrencyId;
+ PayableAmount := Format(Round(RealAmtInclVAT + TempSalesLine."Amount Including VAT" - RealPmtDiscount + CompensationAmount, 0.01), 0, 9);
+ PayableAmountCurrencyID := CurrencyId;
+ end;
+ end;
+
+ procedure GetLegalMonetaryDocAmounts(SalesHeader: Record "Sales Header"; var VATAmtLine: Record "VAT Amount Line"; var LineExtensionAmount: Text; var LegalMonetaryTotalCurrencyID: Text; var TaxExclusiveAmount: Text; var TaxExclusiveAmountCurrencyID: Text; var TaxInclusiveAmount: Text; var TaxInclusiveAmountCurrencyID: Text; var AllowanceTotalAmount: Text; var AllowanceTotalAmountCurrencyID: Text; var ChargeTotalAmount: Text; var ChargeTotalAmountCurrencyID: Text)
+ begin
+ PEPPOL30.GetLegalMonetaryDocAmounts(SalesHeader, VATAmtLine, LineExtensionAmount, LegalMonetaryTotalCurrencyID, TaxExclusiveAmount, TaxExclusiveAmountCurrencyID, TaxInclusiveAmount, TaxInclusiveAmountCurrencyID, AllowanceTotalAmount, AllowanceTotalAmountCurrencyID, ChargeTotalAmount, ChargeTotalAmountCurrencyID);
+ end;
+
+ procedure GetInvoiceRoundingLine(var TempSalesLine: Record "Sales Line" temporary; SalesLine: Record "Sales Line")
+ begin
+ PEPPOL30.GetInvoiceRoundingLine(TempSalesLine, SalesLine);
+ end;
+
+ local procedure HasCompensationLine(var VATAmtLine: Record "VAT Amount Line"): Boolean
+ var
+ Found: Boolean;
+ begin
+ VATAmtLine.Reset();
+ VATAmtLine.SetRange("VAT Identifier", Escompte.GetCompensationVATIdentifier());
+ Found := not VATAmtLine.IsEmpty();
+ VATAmtLine.Reset();
+ exit(Found);
+ end;
+}
diff --git a/src/Apps/BE/PeppolBE/App/src/PEPPOL30BEPaymentInfo.Codeunit.al b/src/Apps/BE/PeppolBE/App/src/PEPPOL30BEPaymentInfo.Codeunit.al
new file mode 100644
index 00000000000..58735b34b65
--- /dev/null
+++ b/src/Apps/BE/PeppolBE/App/src/PEPPOL30BEPaymentInfo.Codeunit.al
@@ -0,0 +1,67 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+namespace Microsoft.Peppol.BE;
+
+using Microsoft.Finance.VAT.Calculation;
+using Microsoft.Peppol;
+using Microsoft.Sales.Document;
+
+///
+/// Belgian PEPPOL payment info provider. Delegates every method to the default PEPPOL30 implementation, except that it renders an extra escompte compensation line (see "PEPPOL30 BE Escompte")
+///
+codeunit 37317 "PEPPOL30 BE Payment Info" implements "PEPPOL Payment Info Provider"
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ var
+ PEPPOL30: Codeunit "PEPPOL30";
+ Escompte: Codeunit "PEPPOL30 BE Escompte";
+
+ procedure GetPaymentMeansInfo(SalesHeader: Record "Sales Header"; var PaymentMeansCode: Text; var PaymentMeansListID: Text; var PaymentDueDate: Text; var PaymentChannelCode: Text; var PaymentID: Text; var PrimaryAccountNumberID: Text; var NetworkID: Text)
+ begin
+ PEPPOL30.GetPaymentMeansInfo(SalesHeader, PaymentMeansCode, PaymentMeansListID, PaymentDueDate, PaymentChannelCode, PaymentID, PrimaryAccountNumberID, NetworkID);
+ end;
+
+ procedure GetPaymentMeansPayeeFinancialAcc(var PayeeFinancialAccountID: Text; var PaymentMeansSchemeID: Text; var FinancialInstitutionBranchID: Text; var FinancialInstitutionID: Text; var FinancialInstitutionSchemeID: Text; var FinancialInstitutionName: Text)
+ begin
+ PEPPOL30.GetPaymentMeansPayeeFinancialAcc(PayeeFinancialAccountID, PaymentMeansSchemeID, FinancialInstitutionBranchID, FinancialInstitutionID, FinancialInstitutionSchemeID, FinancialInstitutionName);
+ end;
+
+ procedure GetPaymentMeansPayeeFinancialAccBIS(SalesHeader: Record "Sales Header"; var PayeeFinancialAccountID: Text; var FinancialInstitutionBranchID: Text)
+ begin
+ PEPPOL30.GetPaymentMeansPayeeFinancialAccBIS(SalesHeader, PayeeFinancialAccountID, FinancialInstitutionBranchID);
+ end;
+
+ procedure GetPaymentMeansFinancialInstitutionAddr(var FinancialInstitutionStreetName: Text; var AdditionalStreetName: Text; var FinancialInstitutionCityName: Text; var FinancialInstitutionPostalZone: Text; var FinancialInstCountrySubentity: Text; var FinancialInstCountryIdCode: Text; var FinancialInstCountryListID: Text)
+ begin
+ PEPPOL30.GetPaymentMeansFinancialInstitutionAddr(FinancialInstitutionStreetName, AdditionalStreetName, FinancialInstitutionCityName, FinancialInstitutionPostalZone, FinancialInstCountrySubentity, FinancialInstCountryIdCode, FinancialInstCountryListID);
+ end;
+
+ procedure GetPaymentTermsInfo(SalesHeader: Record "Sales Header"; var PaymentTermsNote: Text)
+ begin
+ PEPPOL30.GetPaymentTermsInfo(SalesHeader, PaymentTermsNote);
+ end;
+
+ procedure GetAllowanceChargeInfoPaymentDiscount(VATAmtLine: Record "VAT Amount Line"; SalesHeader: Record "Sales Header"; var ChargeIndicator: Text; var AllowanceChargeReasonCode: Text; var AllowanceChargeListID: Text; var AllowanceChargeReason: Text; var Amount: Text; var AllowanceChargeCurrencyID: Text; var TaxCategoryID: Text; var TaxCategorySchemeID: Text; var Percent: Text; var AllowanceChargeTaxSchemeID: Text)
+ begin
+ if Escompte.IsCompensationLine(VATAmtLine) then begin
+ ChargeIndicator := 'true';
+ AllowanceChargeReasonCode := '';
+ AllowanceChargeListID := '';
+ AllowanceChargeReason := Escompte.GetCompensationChargeReason();
+ Amount := Format(VATAmtLine."VAT Base", 0, 9);
+ AllowanceChargeCurrencyID := Escompte.DocumentCurrencyCode(SalesHeader);
+ TaxCategoryID := VATAmtLine."Tax Category";
+ TaxCategorySchemeID := '';
+ Percent := Format(VATAmtLine."VAT %", 0, 9);
+ AllowanceChargeTaxSchemeID := 'VAT';
+ exit;
+ end;
+
+ PEPPOL30.GetAllowanceChargeInfoPaymentDiscount(VATAmtLine, SalesHeader, ChargeIndicator, AllowanceChargeReasonCode, AllowanceChargeListID, AllowanceChargeReason, Amount, AllowanceChargeCurrencyID, TaxCategoryID, TaxCategorySchemeID, Percent, AllowanceChargeTaxSchemeID);
+ end;
+}
diff --git a/src/Apps/BE/PeppolBE/App/src/PEPPOL30BETaxInfo.Codeunit.al b/src/Apps/BE/PeppolBE/App/src/PEPPOL30BETaxInfo.Codeunit.al
index 0a0225d8e58..5a1481cc64a 100644
--- a/src/Apps/BE/PeppolBE/App/src/PEPPOL30BETaxInfo.Codeunit.al
+++ b/src/Apps/BE/PeppolBE/App/src/PEPPOL30BETaxInfo.Codeunit.al
@@ -10,9 +10,10 @@ using Microsoft.Peppol;
using Microsoft.Sales.Document;
///
-/// Belgian PEPPOL tax info provider. Delegates every method to the default PEPPOL30 implementation,
-/// except that it excludes the payment discount from the tax totals so that the PEPPOL document totals
-/// (TaxableAmount, TaxExclusiveAmount, TaxInclusiveAmount, PayableAmount) match the invoice printout.
+/// Belgian PEPPOL tax info provider. Delegates every method to the default PEPPOL30 implementation and,
+/// via FinalizeTaxTotals, appends a compensating Exempt (category E) VAT breakdown line for the
+/// conditional payment discount (escompte). This keeps VAT on the discounted base (as required in
+/// Belgium) while the amount payable stays whole.
///
codeunit 37315 "PEPPOL30 BE Tax Info" implements "PEPPOL Tax Info Provider"
{
@@ -22,6 +23,7 @@ codeunit 37315 "PEPPOL30 BE Tax Info" implements "PEPPOL Tax Info Provider"
var
PEPPOL30: Codeunit "PEPPOL30";
+ Escompte: Codeunit "PEPPOL30 BE Escompte";
procedure GetAllowanceChargeInfo(VATAmtLine: Record "VAT Amount Line"; SalesHeader: Record "Sales Header"; var ChargeIndicator: Text; var AllowanceChargeReasonCode: Text; var AllowanceChargeListID: Text; var AllowanceChargeReason: Text; var Amount: Text; var AllowanceChargeCurrencyID: Text; var TaxCategoryID: Text; var TaxCategorySchemeID: Text; var Percent: Text; var AllowanceChargeTaxSchemeID: Text)
begin
@@ -55,14 +57,33 @@ codeunit 37315 "PEPPOL30 BE Tax Info" implements "PEPPOL Tax Info Provider"
procedure GetTaxTotals(SalesLine: Record "Sales Line"; var VATAmtLine: Record "VAT Amount Line")
begin
- // In Belgium the payment discount must not reduce the PEPPOL document totals. Zeroing the payment
- // discount on the by-value sales line before accumulation keeps TaxableAmount, TaxExclusiveAmount,
- // TaxInclusiveAmount and PayableAmount aligned with the invoice printout, and the payment discount
- // AllowanceCharge is skipped by its existing zero-amount guard.
- SalesLine."Pmt. Discount Amount" := 0;
PEPPOL30.GetTaxTotals(SalesLine, VATAmtLine);
end;
+ procedure FinalizeTaxTotals(var VATAmtLine: Record "VAT Amount Line")
+ var
+ TotalPmtDiscount: Decimal;
+ begin
+ VATAmtLine.Reset();
+ VATAmtLine.CalcSums("Pmt. Discount Amount");
+ TotalPmtDiscount := VATAmtLine."Pmt. Discount Amount";
+ if TotalPmtDiscount = 0 then
+ exit;
+
+ VATAmtLine.Init();
+ VATAmtLine."VAT Identifier" := Escompte.GetCompensationVATIdentifier();
+ VATAmtLine."VAT Calculation Type" := VATAmtLine."VAT Calculation Type"::"Normal VAT";
+ VATAmtLine.Positive := true;
+ VATAmtLine."Tax Category" := Escompte.GetExemptTaxCategory();
+ VATAmtLine."VAT %" := 0;
+ VATAmtLine."VAT Base" := TotalPmtDiscount;
+ VATAmtLine."Amount Including VAT" := TotalPmtDiscount;
+ VATAmtLine."VAT Amount" := 0;
+ VATAmtLine."Pmt. Discount Amount" := 0;
+ VATAmtLine."Invoice Discount Amount" := 0;
+ VATAmtLine.Insert();
+ end;
+
procedure GetTaxCategories(SalesLine: Record "Sales Line"; var VATProductPostingGroupCategory: Record "VAT Product Posting Group")
begin
PEPPOL30.GetTaxCategories(SalesLine, VATProductPostingGroupCategory);
@@ -73,6 +94,16 @@ codeunit 37315 "PEPPOL30 BE Tax Info" implements "PEPPOL Tax Info Provider"
PEPPOL30.GetTaxExemptionReason(VATProductPostingGroupCategory, TaxExemptionReasonTxt, TaxCategoryID);
end;
+ procedure GetTaxExemptionReason(VATAmtLine: Record "VAT Amount Line"; var VATProductPostingGroupCategory: Record "VAT Product Posting Group"; var TaxExemptionReasonTxt: Text; TaxCategoryID: Text)
+ begin
+ if Escompte.IsCompensationLine(VATAmtLine) then begin
+ TaxExemptionReasonTxt := Escompte.GetCompensationExemptionReason();
+ exit;
+ end;
+
+ GetTaxExemptionReason(VATProductPostingGroupCategory, TaxExemptionReasonTxt, TaxCategoryID);
+ end;
+
procedure IsZeroVatCategory(TaxCategory: Code[10]): Boolean
begin
exit(PEPPOL30.IsZeroVatCategory(TaxCategory));
diff --git a/src/Apps/BE/PeppolBE/App/src/PEPPOL30FormatBE.EnumExt.al b/src/Apps/BE/PeppolBE/App/src/PEPPOL30FormatBE.EnumExt.al
index c7a210deaa3..a2379581e0c 100644
--- a/src/Apps/BE/PeppolBE/App/src/PEPPOL30FormatBE.EnumExt.al
+++ b/src/Apps/BE/PeppolBE/App/src/PEPPOL30FormatBE.EnumExt.al
@@ -13,13 +13,17 @@ enumextension 37310 "PEPPOL 3.0 Format BE" extends "PEPPOL 3.0 Format"
Caption = 'PEPPOL 3.0 - Belgium Sales Format';
Implementation = "PEPPOL30 Validation" = "PEPPOL30 BE Sales Validation",
"PEPPOL Posted Document Iterator" = "PEPPOL30 Sales Iterator",
- "PEPPOL Tax Info Provider" = "PEPPOL30 BE Tax Info";
+ "PEPPOL Tax Info Provider" = "PEPPOL30 BE Tax Info",
+ "PEPPOL Payment Info Provider" = "PEPPOL30 BE Payment Info",
+ "PEPPOL Monetary Info Provider" = "PEPPOL30 BE Monetary Info";
}
value(37311; "PEPPOL 3.0 - BE Service")
{
Caption = 'PEPPOL 3.0 - Belgium Service Format';
Implementation = "PEPPOL30 Validation" = "PEPPOL30 BE Service Validation",
"PEPPOL Posted Document Iterator" = "PEPPOL30 Services Iterator",
- "PEPPOL Tax Info Provider" = "PEPPOL30 BE Tax Info";
+ "PEPPOL Tax Info Provider" = "PEPPOL30 BE Tax Info",
+ "PEPPOL Payment Info Provider" = "PEPPOL30 BE Payment Info",
+ "PEPPOL Monetary Info Provider" = "PEPPOL30 BE Monetary Info";
}
}
diff --git a/src/Apps/BE/PeppolBE/Test/app.json b/src/Apps/BE/PeppolBE/Test/app.json
new file mode 100644
index 00000000000..dc7ec56b364
--- /dev/null
+++ b/src/Apps/BE/PeppolBE/Test/app.json
@@ -0,0 +1,48 @@
+{
+ "id": "140aecf2-70e7-4ea8-93ee-78dd27659bee",
+ "name": "PEPPOL BE Tests",
+ "publisher": "Microsoft",
+ "version": "29.0.0.0",
+ "brief": "Tests for the PEPPOL BIS 3.0 BE localization.",
+ "description": "Tests for the PEPPOL BIS 3.0 BE localization.",
+ "privacyStatement": "https://go.microsoft.com/fwlink/?LinkId=724009",
+ "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120",
+ "help": "https://go.microsoft.com/fwlink/?linkid=2104024",
+ "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2206603",
+ "url": "https://go.microsoft.com/fwlink/?LinkId=724011",
+ "dependencies": [
+ {
+ "id": "5d86850b-0d76-4eca-bd7b-951ad998e997",
+ "name": "Tests-TestLibraries",
+ "publisher": "Microsoft",
+ "version": "29.0.0.0"
+ },
+ {
+ "id": "e1966889-b5fb-4fda-a84c-ea71b590e1a9",
+ "name": "PEPPOL",
+ "publisher": "Microsoft",
+ "version": "29.0.0.0"
+ },
+ {
+ "id": "e56642d4-5b38-4de1-8cf4-b2ed9513971a",
+ "name": "PEPPOL BE",
+ "publisher": "Microsoft",
+ "version": "29.0.0.0"
+ }
+ ],
+ "screenshots": [],
+ "platform": "29.0.0.0",
+ "application": "29.0.0.0",
+ "idRanges": [
+ {
+ "from": 148720,
+ "to": 148729
+ }
+ ],
+ "resourceExposurePolicy": {
+ "allowDebugging": true,
+ "allowDownloadingSource": true,
+ "includeSourceInSymbolFile": true
+ },
+ "target": "OnPrem"
+}
diff --git a/src/Apps/BE/PeppolBE/Test/src/PEPPOL30BEPmtDiscountTests.Codeunit.al b/src/Apps/BE/PeppolBE/Test/src/PEPPOL30BEPmtDiscountTests.Codeunit.al
index 0bc6e3a79b8..98ebd780468 100644
--- a/src/Apps/BE/PeppolBE/Test/src/PEPPOL30BEPmtDiscountTests.Codeunit.al
+++ b/src/Apps/BE/PeppolBE/Test/src/PEPPOL30BEPmtDiscountTests.Codeunit.al
@@ -38,17 +38,19 @@ codeunit 148720 "PEPPOL30 BE Pmt Disc Tests"
LibraryTestInitialize: Codeunit "Library - Test Initialize";
IsInitialized: Boolean;
InvoiceNamespaceTxt: Label 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', Locked = true;
+ EscompteExemptionReasonTxt: Label 'Conditional early-payment discount, not part of the taxable amount';
+ ExemptionReasonXPathTxt: Label '//cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:TaxExemptionReason', Locked = true;
+ GenuineExemptionReasonTxt: Label 'Exempt under article 44', Locked = true;
[Test]
- procedure PaymentDiscountNotDeductedFromTaxAmountsForBESalesInvoice()
+ procedure BESalesInvoiceEscompteCompensation()
var
SalesInvoiceHeader: Record "Sales Invoice Header";
TempBlob: Codeunit "Temp Blob";
CustomerNo: Code[20];
PaymentTermsCode: Code[10];
begin
- // [SCENARIO 643204] For the Belgian PEPPOL format the taxable amount is calculated on the full amount,
- // i.e. the payment discount is NOT deducted from the VAT-taxable base, so the XML matches the invoice printout.
+ // [SCENARIO 643204] The Belgian escompte keeps VAT on the discounted base, but the conditional payment discount must not reduce the amount payable.
Initialize();
// [GIVEN] Payment Terms with a 3% payment discount
@@ -56,23 +58,102 @@ codeunit 148720 "PEPPOL30 BE Pmt Disc Tests"
// [GIVEN] A customer that uses those payment terms
CustomerNo := CreateCustomerWithAddressAndGLN();
- // [GIVEN] A posted sales invoice for 1 x 111.20 EUR with 21% VAT and the 3% payment discount terms
+ // [GIVEN] A posted sales invoice for 1 x 111.20 with 21% VAT and the 3% payment discount terms; the escompte
+ // is active so VAT is charged on the discounted base 107.86 (VAT 22.65, total 133.85).
PostSalesInvoiceWithPmtDiscount(SalesInvoiceHeader, CustomerNo, PaymentTermsCode, 111.2, 21);
// [WHEN] The posted invoice is exported to PEPPOL BIS 3.0 using the Belgian sales format
SalesInvoiceHeader.SetRecFilter();
ExportInvoiceToBlob(SalesInvoiceHeader, TempBlob);
-
- // [THEN] The taxable/monetary totals are calculated on the full amount (111.20 / 134.55),
- // and NOT reduced by the payment discount (which would give 107.86 / 131.21 and fail BR-S-08).
InitXPathXMLReaderForInvoice(TempBlob);
+
+ // [THEN] Two VAT breakdowns: Standard 107.86 / 22.65 and the compensating Exempt 3.34 / 0.00 (with a reason)
+ LibraryXPathXMLReader.VerifyNodeCountByXPath('//cac:TaxTotal/cac:TaxSubtotal', 2);
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:TaxTotal/cbc:TaxAmount', '22.65');
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:TaxSubtotal[cac:TaxCategory/cbc:ID=''S'']/cbc:TaxableAmount', '107.86');
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:TaxSubtotal[cac:TaxCategory/cbc:ID=''S'']/cbc:TaxAmount', '22.65');
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:TaxSubtotal[cac:TaxCategory/cbc:ID=''E'']/cbc:TaxableAmount', '3.34');
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:TaxSubtotal[cac:TaxCategory/cbc:ID=''E'']/cbc:TaxAmount', '0.00');
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:TaxSubtotal[cac:TaxCategory/cbc:ID=''E'']/cac:TaxCategory/cbc:TaxExemptionReason', EscompteExemptionReasonTxt);
+
+ // [THEN] Two document-level AllowanceCharges: the Standard payment-discount allowance and the Exempt compensating charge
+ LibraryXPathXMLReader.VerifyNodeCountByXPath('//cac:AllowanceCharge', 2);
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:AllowanceCharge[cbc:ChargeIndicator=''false'']/cbc:Amount', '3.34');
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:AllowanceCharge[cbc:ChargeIndicator=''true'']/cbc:Amount', '3.34');
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:AllowanceCharge[cbc:ChargeIndicator=''true'']/cac:TaxCategory/cbc:ID', 'E');
+ // [THEN] The compensating charge carries only a text reason - no (empty) reason code element is emitted
+ LibraryXPathXMLReader.VerifyNodeCountByXPath('//cac:AllowanceCharge[cbc:ChargeIndicator=''true'']/cbc:AllowanceChargeReasonCode', 0);
+
+ // [THEN] The amount payable stays whole: LineExtension/TaxExclusive 111.20, Allowance & Charge 3.34, total 133.85
LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:LegalMonetaryTotal/cbc:LineExtensionAmount', '111.2');
LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount', '111.2');
- LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount', '134.55');
- LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:LegalMonetaryTotal/cbc:PayableAmount', '134.55');
- // [THEN] The tax subtotal taxable amount equals the full amount and no payment-discount allowance is emitted
- LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:TaxTotal/cac:TaxSubtotal/cbc:TaxableAmount', '111.2');
- LibraryXPathXMLReader.VerifyNodeAbsence('//cac:AllowanceCharge');
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount', '3.34');
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:LegalMonetaryTotal/cbc:ChargeTotalAmount', '3.34');
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount', '133.85');
+ LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:LegalMonetaryTotal/cbc:PayableAmount', '133.85');
+ end;
+
+ [Test]
+ procedure BESalesInvoiceExemptLineKeepsOwnExemptionReason()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ TempBlob: Codeunit "Temp Blob";
+ CustomerNo: Code[20];
+ PaymentTermsCode: Code[10];
+ begin
+ // [SCENARIO 643204] A genuinely exempt VAT breakdown keeps its own exemption reason when the escompte compensation is present.
+ Initialize();
+
+ // [GIVEN] Payment Terms with a 3% payment discount and a customer that uses them
+ PaymentTermsCode := CreatePaymentTermsWithDiscount(3);
+ CustomerNo := CreateCustomerWithAddressAndGLN();
+
+ // [GIVEN] A posted sales invoice with a 21% line and an exempt (category E) line whose VAT product posting group is described
+ PostSalesInvoice(
+ SalesInvoiceHeader, CustomerNo, PaymentTermsCode,
+ CreateVATPostingSetupWithPmtDiscount(GetVATBusPostingGroup(CustomerNo), 21), 111.2,
+ CreateExemptVATPostingSetup(GetVATBusPostingGroup(CustomerNo), GenuineExemptionReasonTxt), 100);
+
+ // [WHEN] The posted invoice is exported to PEPPOL BIS 3.0 using the Belgian sales format
+ SalesInvoiceHeader.SetRecFilter();
+ ExportInvoiceToBlob(SalesInvoiceHeader, TempBlob);
+ InitXPathXMLReaderForInvoice(TempBlob);
+
+ // [THEN] Three VAT breakdowns are written: Standard, the genuine Exempt one and the compensating Exempt one
+ LibraryXPathXMLReader.VerifyNodeCountByXPath('//cac:TaxTotal/cac:TaxSubtotal', 3);
+
+ // [THEN] The escompte reason is used once, and the genuine exempt breakdown keeps the reason from its VAT product posting group
+ LibraryXPathXMLReader.VerifyNodeCountWithValueByXPath(ExemptionReasonXPathTxt, EscompteExemptionReasonTxt, 1);
+ LibraryXPathXMLReader.VerifyNodeCountWithValueByXPath(ExemptionReasonXPathTxt, GenuineExemptionReasonTxt, 1);
+ end;
+
+ [Test]
+ procedure BESalesInvoiceWithoutPmtDiscountHasNoEscompteReason()
+ var
+ SalesInvoiceHeader: Record "Sales Invoice Header";
+ TempBlob: Codeunit "Temp Blob";
+ CustomerNo: Code[20];
+ begin
+ // [SCENARIO 643204] An exempt VAT breakdown without an exemption reason is not given the escompte reason when there is no payment discount.
+ Initialize();
+
+ // [GIVEN] A customer without payment discount terms
+ CustomerNo := CreateCustomerWithAddressAndGLN();
+
+ // [GIVEN] A posted sales invoice with a single exempt (category E) line whose VAT product posting group has no description
+ PostSalesInvoice(
+ SalesInvoiceHeader, CustomerNo, '',
+ CreateExemptVATPostingSetup(GetVATBusPostingGroup(CustomerNo), ''), 100,
+ '', 0);
+
+ // [WHEN] The posted invoice is exported to PEPPOL BIS 3.0 using the Belgian sales format
+ SalesInvoiceHeader.SetRecFilter();
+ ExportInvoiceToBlob(SalesInvoiceHeader, TempBlob);
+ InitXPathXMLReaderForInvoice(TempBlob);
+
+ // [THEN] The single Exempt breakdown is not given the escompte exemption reason
+ LibraryXPathXMLReader.VerifyNodeCountByXPath('//cac:TaxTotal/cac:TaxSubtotal', 1);
+ LibraryXPathXMLReader.VerifyNodeCountWithValueByXPath(ExemptionReasonXPathTxt, EscompteExemptionReasonTxt, 0);
end;
local procedure Initialize()
@@ -100,6 +181,9 @@ codeunit 148720 "PEPPOL30 BE Pmt Disc Tests"
CompanyInformation."VAT Registration No." := LibraryERM.GenerateVATRegistrationNo(CompanyInformation."Country/Region Code");
CompanyInformation.Validate(GLN, '1234567891231');
CompanyInformation.Validate("Use GLN in Electronic Document", true);
+ CompanyInformation."Bank Account No." := '1234567890';
+ CompanyInformation."Bank Branch No." := '1234';
+ CompanyInformation."SWIFT Code" := 'GEBABEBB';
CompanyInformation.Modify(true);
LibraryERMCountryData.CreateVATData();
@@ -108,7 +192,7 @@ codeunit 148720 "PEPPOL30 BE Pmt Disc Tests"
LibraryERMCountryData.UpdateSalesReceivablesSetup();
LibraryERMCountryData.UpdateLocalData();
- EnableAdjustForPaymentDiscount();
+ EnableBEPaymentDiscountVAT();
LibrarySetupStorage.Save(Database::"Company Information");
LibrarySetupStorage.Save(Database::"General Ledger Setup");
@@ -128,13 +212,16 @@ codeunit 148720 "PEPPOL30 BE Pmt Disc Tests"
PEPPOLSetup.Modify();
end;
- local procedure EnableAdjustForPaymentDiscount()
+ local procedure EnableBEPaymentDiscountVAT()
var
GeneralLedgerSetup: Record "General Ledger Setup";
begin
+ // Belgian escompte: VAT is charged on the discounted base.
GeneralLedgerSetup.Get();
- GeneralLedgerSetup."Adjust for Payment Disc." := true;
- GeneralLedgerSetup.Modify();
+ GeneralLedgerSetup.Validate("Adjust for Payment Disc.", false);
+ GeneralLedgerSetup.Validate("Pmt. Disc. Excl. VAT", true);
+ GeneralLedgerSetup.Validate("VAT Tolerance %", 3);
+ GeneralLedgerSetup.Modify(true);
end;
local procedure CreatePaymentTermsWithDiscount(DiscountPct: Decimal): Code[10]
@@ -178,9 +265,15 @@ codeunit 148720 "PEPPOL30 BE Pmt Disc Tests"
end;
local procedure PostSalesInvoiceWithPmtDiscount(var SalesInvoiceHeader: Record "Sales Invoice Header"; CustomerNo: Code[20]; PaymentTermsCode: Code[10]; UnitPrice: Decimal; VATPct: Decimal)
+ begin
+ PostSalesInvoice(
+ SalesInvoiceHeader, CustomerNo, PaymentTermsCode,
+ CreateVATPostingSetupWithPmtDiscount(GetVATBusPostingGroup(CustomerNo), VATPct), UnitPrice, '', 0);
+ end;
+
+ local procedure PostSalesInvoice(var SalesInvoiceHeader: Record "Sales Invoice Header"; CustomerNo: Code[20]; PaymentTermsCode: Code[10]; FirstVATProdPostingGroup: Code[20]; FirstUnitPrice: Decimal; SecondVATProdPostingGroup: Code[20]; SecondUnitPrice: Decimal)
var
SalesHeader: Record "Sales Header";
- SalesLine: Record "Sales Line";
begin
LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, CustomerNo);
SalesHeader.Validate("Payment Terms Code", PaymentTermsCode);
@@ -188,13 +281,49 @@ codeunit 148720 "PEPPOL30 BE Pmt Disc Tests"
SalesHeader.Validate("Your Reference", LibraryUtility.GenerateGUID());
SalesHeader.Modify(true);
+ AddSalesLine(SalesHeader, FirstVATProdPostingGroup, FirstUnitPrice);
+ if SecondVATProdPostingGroup <> '' then
+ AddSalesLine(SalesHeader, SecondVATProdPostingGroup, SecondUnitPrice);
+
+ SalesInvoiceHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ end;
+
+ local procedure AddSalesLine(var SalesHeader: Record "Sales Header"; VATProdPostingGroup: Code[20]; UnitPrice: Decimal)
+ var
+ SalesLine: Record "Sales Line";
+ begin
LibrarySales.CreateSalesLine(
SalesLine, SalesHeader, SalesLine.Type::"G/L Account", LibraryERM.CreateGLAccountWithSalesSetup(), 1);
- SalesLine.Validate("VAT Prod. Posting Group", CreateVATPostingSetupWithPmtDiscount(SalesHeader."VAT Bus. Posting Group", VATPct));
+ SalesLine.Validate("VAT Prod. Posting Group", VATProdPostingGroup);
SalesLine.Validate("Unit Price", UnitPrice);
SalesLine.Modify(true);
+ end;
- SalesInvoiceHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, true, true));
+ local procedure GetVATBusPostingGroup(CustomerNo: Code[20]): Code[20]
+ var
+ Customer: Record Customer;
+ begin
+ Customer.Get(CustomerNo);
+ exit(Customer."VAT Bus. Posting Group");
+ end;
+
+ local procedure CreateExemptVATPostingSetup(VATBusPostingGroup: Code[20]; ProductPostingGroupDescription: Text[100]): Code[20]
+ var
+ VATPostingSetup: Record "VAT Posting Setup";
+ VATProductPostingGroup: Record "VAT Product Posting Group";
+ begin
+ LibraryERM.CreateVATProductPostingGroup(VATProductPostingGroup);
+ VATProductPostingGroup.Validate(Description, ProductPostingGroupDescription);
+ VATProductPostingGroup.Modify(true);
+
+ LibraryERM.CreateVATPostingSetup(VATPostingSetup, VATBusPostingGroup, VATProductPostingGroup.Code);
+ VATPostingSetup."VAT Identifier" := LibraryUtility.GenerateGUID();
+ VATPostingSetup.Validate("VAT Calculation Type", VATPostingSetup."VAT Calculation Type"::"Normal VAT");
+ VATPostingSetup.Validate("VAT %", 0);
+ VATPostingSetup.Validate("Tax Category", 'E');
+ VATPostingSetup.Validate("Sales VAT Account", LibraryERM.CreateGLAccountNo());
+ VATPostingSetup.Modify(true);
+ exit(VATProductPostingGroup.Code);
end;
local procedure CreateVATPostingSetupWithPmtDiscount(VATBusPostingGroup: Code[20]; VATPct: Decimal): Code[20]
@@ -208,7 +337,6 @@ codeunit 148720 "PEPPOL30 BE Pmt Disc Tests"
VATPostingSetup.Validate("VAT Calculation Type", VATPostingSetup."VAT Calculation Type"::"Normal VAT");
VATPostingSetup.Validate("VAT %", VATPct);
VATPostingSetup.Validate("Tax Category", 'S');
- VATPostingSetup."Adjust for Payment Discount" := true;
VATPostingSetup.Validate("Sales VAT Account", LibraryERM.CreateGLAccountNo());
VATPostingSetup.Modify(true);
exit(VATProductPostingGroup.Code);
diff --git a/src/Apps/W1/PEPPOL/App/src/Common/PEPPOL30Common.Codeunit.al b/src/Apps/W1/PEPPOL/App/src/Common/PEPPOL30Common.Codeunit.al
index a4298162c09..c7b44c0613e 100644
--- a/src/Apps/W1/PEPPOL/App/src/Common/PEPPOL30Common.Codeunit.al
+++ b/src/Apps/W1/PEPPOL/App/src/Common/PEPPOL30Common.Codeunit.al
@@ -197,6 +197,8 @@ codeunit 37218 "PEPPOL30 Common"
else
Error(UnsupportedDocumentErr);
end;
+
+ PEPPOLTaxInfoProvider.FinalizeTaxTotals(TempVATAmtLine);
end;
///
diff --git a/src/Apps/W1/PEPPOL/App/src/Interfaces/PEPPOLTaxInfoProvider.Interface.al b/src/Apps/W1/PEPPOL/App/src/Interfaces/PEPPOLTaxInfoProvider.Interface.al
index 2512fa9f28a..3f923df2310 100644
--- a/src/Apps/W1/PEPPOL/App/src/Interfaces/PEPPOLTaxInfoProvider.Interface.al
+++ b/src/Apps/W1/PEPPOL/App/src/Interfaces/PEPPOLTaxInfoProvider.Interface.al
@@ -141,4 +141,26 @@ interface "PEPPOL Tax Info Provider"
/// The tax category code to check.
/// True if the tax category is outside VAT scope, false otherwise.
procedure IsOutsideScopeVATCategory(TaxCategory: Code[10]): Boolean;
+
+ ///
+ /// Called once per document after all lines have been aggregated into the VAT amount line buffer,
+ /// letting a format append synthetic VAT breakdown lines if needed. Needed to add, for example, compensation lines.
+ ///
+ /// The accumulated VAT amount line buffer to post-process.
+ procedure FinalizeTaxTotals(var VATAmtLine: Record "VAT Amount Line")
+ begin
+ end;
+
+ ///
+ /// Gets the tax exemption reason text for a given VAT breakdown. Unlike the overload without the VAT amount line,
+ /// this lets a format tell apart breakdowns that share the same tax category.
+ ///
+ /// The VAT amount line the tax subtotal is written from.
+ /// The VAT product posting group category record.
+ /// Returns the tax exemption reason text.
+ /// The tax category ID to get exemption reason for.
+ procedure GetTaxExemptionReason(VATAmtLine: Record "VAT Amount Line"; var VATProductPostingGroupCategory: Record "VAT Product Posting Group"; var TaxExemptionReasonTxt: Text; TaxCategoryID: Text)
+ begin
+ this.GetTaxExemptionReason(VATProductPostingGroupCategory, TaxExemptionReasonTxt, TaxCategoryID);
+ end;
}
diff --git a/src/Apps/W1/PEPPOL/App/src/Sales/XmlPorts/SalesCrMemoPEPPOL30.XmlPort.al b/src/Apps/W1/PEPPOL/App/src/Sales/XmlPorts/SalesCrMemoPEPPOL30.XmlPort.al
index 974f5d06ba1..078185236f5 100644
--- a/src/Apps/W1/PEPPOL/App/src/Sales/XmlPorts/SalesCrMemoPEPPOL30.XmlPort.al
+++ b/src/Apps/W1/PEPPOL/App/src/Sales/XmlPorts/SalesCrMemoPEPPOL30.XmlPort.al
@@ -1357,6 +1357,12 @@ xmlport 37200 "Sales Cr.Memo - PEPPOL30"
{
XmlName = 'AllowanceChargeReasonCode';
NamespacePrefix = 'cbc';
+
+ trigger OnBeforePassVariable()
+ begin
+ if AllowanceChargeReasonCodePaymentDiscount = '' then
+ currXMLport.Skip();
+ end;
}
textelement(AllowanceChargeReasonPaymentDiscount)
{
@@ -1523,7 +1529,7 @@ xmlport 37200 "Sales Cr.Memo - PEPPOL30"
TaxCategoryPercent,
TaxTotalTaxSchemeID);
- PEPPOLTaxInfoProvider.GetTaxExemptionReason(TempVATProductPostingGroup, TaxExemptionReason, TaxTotalTaxCategoryID);
+ PEPPOLTaxInfoProvider.GetTaxExemptionReason(TempVATAmtLine, TempVATProductPostingGroup, TaxExemptionReason, TaxTotalTaxCategoryID);
end;
}
diff --git a/src/Apps/W1/PEPPOL/App/src/Sales/XmlPorts/SalesInvoicePEPPOL30.XmlPort.al b/src/Apps/W1/PEPPOL/App/src/Sales/XmlPorts/SalesInvoicePEPPOL30.XmlPort.al
index cfa66824cdb..aad98473a85 100644
--- a/src/Apps/W1/PEPPOL/App/src/Sales/XmlPorts/SalesInvoicePEPPOL30.XmlPort.al
+++ b/src/Apps/W1/PEPPOL/App/src/Sales/XmlPorts/SalesInvoicePEPPOL30.XmlPort.al
@@ -1308,6 +1308,12 @@ xmlport 37201 "Sales Invoice - PEPPOL30"
{
XmlName = 'AllowanceChargeReasonCode';
NamespacePrefix = 'cbc';
+
+ trigger OnBeforePassVariable()
+ begin
+ if AllowanceChargeReasonCodePaymentDiscount = '' then
+ currXMLport.Skip();
+ end;
}
textelement(AllowanceChargeReasonPaymentDiscount)
{
@@ -1474,7 +1480,7 @@ xmlport 37201 "Sales Invoice - PEPPOL30"
TaxCategoryPercent,
TaxTotalTaxSchemeID);
- PEPPOLTaxInfoProvider.GetTaxExemptionReason(TempVATProductPostingGroup, TaxExemptionReason, TaxTotalTaxCategoryID);
+ PEPPOLTaxInfoProvider.GetTaxExemptionReason(TempVATAmtLine, TempVATProductPostingGroup, TaxExemptionReason, TaxTotalTaxCategoryID);
end;
}
diff --git a/src/Layers/BE/BaseApp/Sales/Peppol/PEPPOLManagement.Codeunit.al b/src/Layers/BE/BaseApp/Sales/Peppol/PEPPOLManagement.Codeunit.al
index c4e3b7a9cf0..8960699c51c 100644
--- a/src/Layers/BE/BaseApp/Sales/Peppol/PEPPOLManagement.Codeunit.al
+++ b/src/Layers/BE/BaseApp/Sales/Peppol/PEPPOLManagement.Codeunit.al
@@ -53,6 +53,9 @@ codeunit 1605 "PEPPOL Management"
BICTxt: Label 'BIC', Locked = true;
AllowanceChargeReasonCodeTxt: Label '104', Locked = true;
AllowanceChargePaymentDiscountReasonCodeTxt: Label '95', Locked = true;
+ PmtDiscCompChargeReasonTxt: Label 'Payment discount not deducted from the amount payable';
+ PmtDiscCompExemptionReasonTxt: Label 'Conditional early-payment discount, not part of the taxable amount';
+ PmtDiscCompVATIdentifierTxt: Label 'ESCOMPTE-COMP', Locked = true;
PaymentMeansFundsTransferCodeTxt: Label '31', Locked = true;
GTINTxt: Label '0160', Locked = true;
UoMforPieceINUNECERec20ListIDTxt: Label 'EA', Locked = true;
@@ -1025,6 +1028,20 @@ codeunit 1605 "PEPPOL Management"
/// Returns the tax scheme identifier.
procedure GetAllowanceChargeInfoPaymentDiscount(VATAmtLine: Record "VAT Amount Line"; SalesHeader: Record "Sales Header"; var ChargeIndicator: Text; var AllowanceChargeReasonCode: Text; var AllowanceChargeListID: Text; var AllowanceChargeReason: Text; var Amount: Text; var AllowanceChargeCurrencyID: Text; var TaxCategoryID: Text; var TaxCategorySchemeID: Text; var Percent: Text; var AllowanceChargeTaxSchemeID: Text)
begin
+ if IsPmtDiscCompensationLine(VATAmtLine) then begin
+ ChargeIndicator := 'true';
+ AllowanceChargeReasonCode := '';
+ AllowanceChargeListID := '';
+ AllowanceChargeReason := PmtDiscCompChargeReasonTxt;
+ Amount := Format(VATAmtLine."VAT Base", 0, 9);
+ AllowanceChargeCurrencyID := GetSalesDocCurrencyCode(SalesHeader);
+ TaxCategoryID := VATAmtLine."Tax Category";
+ TaxCategorySchemeID := '';
+ Percent := Format(VATAmtLine."VAT %", 0, 9);
+ AllowanceChargeTaxSchemeID := VATTxt;
+ exit;
+ end;
+
if VATAmtLine."Pmt. Discount Amount" = 0 then begin
ChargeIndicator := '';
exit;
@@ -1210,6 +1227,19 @@ codeunit 1605 "PEPPOL Management"
/// Returns the currency code for payable amount.
procedure GetLegalMonetaryInfo(SalesHeader: Record "Sales Header"; var TempSalesLine: Record "Sales Line" temporary; var VATAmtLine: Record "VAT Amount Line"; var LineExtensionAmount: Text; var LegalMonetaryTotalCurrencyID: Text; var TaxExclusiveAmount: Text; var TaxExclusiveAmountCurrencyID: Text; var TaxInclusiveAmount: Text; var TaxInclusiveAmountCurrencyID: Text; var AllowanceTotalAmount: Text; var AllowanceTotalAmountCurrencyID: Text; var ChargeTotalAmount: Text; var ChargeTotalAmountCurrencyID: Text; var PrepaidAmount: Text; var PrepaidCurrencyID: Text; var PayableRoundingAmount: Text; var PayableRndingAmountCurrencyID: Text; var PayableAmount: Text; var PayableAmountCurrencyID: Text)
begin
+ if HasPmtDiscCompensationLine(VATAmtLine) then begin
+ CalcLegalMonetaryInfoWithCompensation(
+ SalesHeader, TempSalesLine, VATAmtLine, LineExtensionAmount, LegalMonetaryTotalCurrencyID,
+ TaxExclusiveAmount, TaxExclusiveAmountCurrencyID, TaxInclusiveAmount, TaxInclusiveAmountCurrencyID,
+ AllowanceTotalAmount, AllowanceTotalAmountCurrencyID, ChargeTotalAmount, ChargeTotalAmountCurrencyID,
+ PrepaidAmount, PrepaidCurrencyID, PayableRoundingAmount, PayableRndingAmountCurrencyID,
+ PayableAmount, PayableAmountCurrencyID);
+ OnAfterGetLegalMonetaryInfoWithInvRounding(
+ SalesHeader, TempSalesLine, VATAmtLine, LineExtensionAmount, TaxExclusiveAmount, TaxInclusiveAmount,
+ AllowanceTotalAmount, ChargeTotalAmount, PrepaidAmount, PayableRoundingAmount, PayableAmount);
+ exit;
+ end;
+
VATAmtLine.Reset();
VATAmtLine.CalcSums("Line Amount", "VAT Base", "Amount Including VAT", "Invoice Discount Amount");
@@ -1683,6 +1713,101 @@ codeunit 1605 "PEPPOL Management"
end;
end;
+ procedure AddPaymentDiscountCompensation(var VATAmtLine: Record "VAT Amount Line")
+ var
+ TotalPmtDiscount: Decimal;
+ begin
+ VATAmtLine.Reset();
+ VATAmtLine.CalcSums("Pmt. Discount Amount");
+ TotalPmtDiscount := VATAmtLine."Pmt. Discount Amount";
+ if TotalPmtDiscount = 0 then
+ exit;
+
+ VATAmtLine.Init();
+ VATAmtLine."VAT Identifier" := GetPmtDiscCompensationVATIdentifier();
+ VATAmtLine."VAT Calculation Type" := VATAmtLine."VAT Calculation Type"::"Normal VAT";
+ VATAmtLine.Positive := true;
+ VATAmtLine."Tax Category" := CopyStr(GetTaxCategoryE(), 1, MaxStrLen(VATAmtLine."Tax Category"));
+ VATAmtLine."VAT %" := 0;
+ VATAmtLine."VAT Base" := TotalPmtDiscount;
+ VATAmtLine."Amount Including VAT" := TotalPmtDiscount;
+ VATAmtLine."VAT Amount" := 0;
+ VATAmtLine."Pmt. Discount Amount" := 0;
+ VATAmtLine."Invoice Discount Amount" := 0;
+ VATAmtLine.Insert();
+ end;
+
+ local procedure GetPmtDiscCompensationVATIdentifier(): Code[20]
+ begin
+ exit(CopyStr(PmtDiscCompVATIdentifierTxt, 1, 20));
+ end;
+
+ local procedure IsPmtDiscCompensationLine(VATAmtLine: Record "VAT Amount Line"): Boolean
+ begin
+ exit(VATAmtLine."VAT Identifier" = GetPmtDiscCompensationVATIdentifier());
+ end;
+
+ local procedure HasPmtDiscCompensationLine(var VATAmtLine: Record "VAT Amount Line"): Boolean
+ var
+ Found: Boolean;
+ begin
+ VATAmtLine.Reset();
+ VATAmtLine.SetRange("VAT Identifier", GetPmtDiscCompensationVATIdentifier());
+ Found := not VATAmtLine.IsEmpty();
+ VATAmtLine.Reset();
+ exit(Found);
+ end;
+
+ local procedure CalcLegalMonetaryInfoWithCompensation(SalesHeader: Record "Sales Header"; var TempSalesLine: Record "Sales Line" temporary; var VATAmtLine: Record "VAT Amount Line"; var LineExtensionAmount: Text; var LegalMonetaryTotalCurrencyID: Text; var TaxExclusiveAmount: Text; var TaxExclusiveAmountCurrencyID: Text; var TaxInclusiveAmount: Text; var TaxInclusiveAmountCurrencyID: Text; var AllowanceTotalAmount: Text; var AllowanceTotalAmountCurrencyID: Text; var ChargeTotalAmount: Text; var ChargeTotalAmountCurrencyID: Text; var PrepaidAmount: Text; var PrepaidCurrencyID: Text; var PayableRoundingAmount: Text; var PayableRndingAmountCurrencyID: Text; var PayableAmount: Text; var PayableAmountCurrencyID: Text)
+ var
+ CompensationAmount: Decimal;
+ RealVATBase: Decimal;
+ RealInvDiscount: Decimal;
+ RealPmtDiscount: Decimal;
+ RealAmtInclVAT: Decimal;
+ CurrencyId: Text;
+ begin
+ VATAmtLine.Reset();
+ if VATAmtLine.FindSet() then
+ repeat
+ if IsPmtDiscCompensationLine(VATAmtLine) then
+ CompensationAmount += VATAmtLine."VAT Base"
+ else begin
+ RealVATBase += VATAmtLine."VAT Base";
+ RealInvDiscount += VATAmtLine."Invoice Discount Amount";
+ RealPmtDiscount += VATAmtLine."Pmt. Discount Amount";
+ RealAmtInclVAT += VATAmtLine."Amount Including VAT";
+ end;
+ until VATAmtLine.Next() = 0;
+
+ CurrencyId := GetSalesDocCurrencyCode(SalesHeader);
+
+ LineExtensionAmount := Format(Round(RealVATBase, 0.01) + Round(RealInvDiscount, 0.01), 0, 9);
+ LegalMonetaryTotalCurrencyID := CurrencyId;
+ TaxExclusiveAmount := Format(Round(RealVATBase - RealPmtDiscount + CompensationAmount, 0.01), 0, 9);
+ TaxExclusiveAmountCurrencyID := CurrencyId;
+ TaxInclusiveAmount := Format(Round(RealAmtInclVAT - RealPmtDiscount + CompensationAmount, 0.01, '>'), 0, 9);
+ TaxInclusiveAmountCurrencyID := CurrencyId;
+ AllowanceTotalAmount := Format(Round(RealInvDiscount + RealPmtDiscount, 0.01), 0, 9);
+ AllowanceTotalAmountCurrencyID := CurrencyId;
+ ChargeTotalAmount := Format(Round(CompensationAmount, 0.01), 0, 9);
+ ChargeTotalAmountCurrencyID := CurrencyId;
+ PrepaidAmount := '0.00';
+ PrepaidCurrencyID := CurrencyId;
+
+ if TempSalesLine."Line No." = 0 then begin
+ PayableRoundingAmount := Format(RealAmtInclVAT - Round(RealAmtInclVAT, 0.01), 0, 9);
+ PayableRndingAmountCurrencyID := CurrencyId;
+ PayableAmount := Format(Round(RealAmtInclVAT - RealPmtDiscount + CompensationAmount, 0.01), 0, 9);
+ PayableAmountCurrencyID := CurrencyId;
+ end else begin
+ PayableRoundingAmount := Format(TempSalesLine."Amount Including VAT", 0, 9);
+ PayableRndingAmountCurrencyID := CurrencyId;
+ PayableAmount := Format(Round(RealAmtInclVAT + TempSalesLine."Amount Including VAT" - RealPmtDiscount + CompensationAmount, 0.01), 0, 9);
+ PayableAmountCurrencyID := CurrencyId;
+ end;
+ end;
+
///
/// Retrieves and accumulates tax categories from sales lines into a buffer.
///
@@ -1735,6 +1860,24 @@ codeunit 1605 "PEPPOL Management"
TaxExemptionReasonTxt := VATProductPostingGroupCategory.Description;
end;
+ ///
+ /// Retrieves the tax exemption reason for a specific VAT breakdown. Unlike the overload without the VAT amount line,
+ /// this tells apart breakdowns that share the same tax category, such as the payment discount compensation.
+ ///
+ /// Specifies the VAT amount line the tax subtotal is written from.
+ /// Specifies the VAT product posting group category buffer.
+ /// Returns the tax exemption reason description.
+ /// Specifies the tax category identifier to look up.
+ procedure GetTaxExemptionReason(VATAmtLine: Record "VAT Amount Line"; var VATProductPostingGroupCategory: Record "VAT Product Posting Group"; var TaxExemptionReasonTxt: Text; TaxCategoryID: Text)
+ begin
+ if IsPmtDiscCompensationLine(VATAmtLine) then begin
+ TaxExemptionReasonTxt := PmtDiscCompExemptionReasonTxt;
+ exit;
+ end;
+
+ GetTaxExemptionReason(VATProductPostingGroupCategory, TaxExemptionReasonTxt, TaxCategoryID);
+ end;
+
///
/// Returns the PEPPOL telemetry token for feature usage tracking.
///
diff --git a/src/Layers/BE/BaseApp/Sales/Peppol/SalesCrMemoPEPPOLBIS30.XmlPort.al b/src/Layers/BE/BaseApp/Sales/Peppol/SalesCrMemoPEPPOLBIS30.XmlPort.al
index 9becaa029d8..950c86f09e2 100644
--- a/src/Layers/BE/BaseApp/Sales/Peppol/SalesCrMemoPEPPOLBIS30.XmlPort.al
+++ b/src/Layers/BE/BaseApp/Sales/Peppol/SalesCrMemoPEPPOLBIS30.XmlPort.al
@@ -1478,7 +1478,7 @@ xmlport 1611 "Sales Cr.Memo - PEPPOL BIS 3.0"
TaxCategoryPercent,
TaxTotalTaxSchemeID);
- PEPPOLMgt.GetTaxExemptionReason(TempVATProductPostingGroup, TaxExemptionReason, TaxTotalTaxCategoryID);
+ PEPPOLMgt.GetTaxExemptionReason(TempVATAmtLine, TempVATProductPostingGroup, TaxExemptionReason, TaxTotalTaxCategoryID);
end;
}
diff --git a/src/Layers/BE/BaseApp/Sales/Peppol/SalesInvoicePEPPOLBIS30.XmlPort.al b/src/Layers/BE/BaseApp/Sales/Peppol/SalesInvoicePEPPOLBIS30.XmlPort.al
index cac089e0e46..0e056a95193 100644
--- a/src/Layers/BE/BaseApp/Sales/Peppol/SalesInvoicePEPPOLBIS30.XmlPort.al
+++ b/src/Layers/BE/BaseApp/Sales/Peppol/SalesInvoicePEPPOLBIS30.XmlPort.al
@@ -1267,6 +1267,12 @@ xmlport 1610 "Sales Invoice - PEPPOL BIS 3.0"
{
XmlName = 'AllowanceChargeReasonCode';
NamespacePrefix = 'cbc';
+
+ trigger OnBeforePassVariable()
+ begin
+ if AllowanceChargeReasonCodePaymentDiscount = '' then
+ currXMLport.Skip();
+ end;
}
textelement(AllowanceChargeReasonPaymentDiscount)
{
@@ -1428,7 +1434,7 @@ xmlport 1610 "Sales Invoice - PEPPOL BIS 3.0"
TaxCategoryPercent,
TaxTotalTaxSchemeID);
- PEPPOLMgt.GetTaxExemptionReason(TempVATProductPostingGroup, TaxExemptionReason, TaxTotalTaxCategoryID);
+ PEPPOLMgt.GetTaxExemptionReason(TempVATAmtLine, TempVATProductPostingGroup, TaxExemptionReason, TaxTotalTaxCategoryID);
end;
}
@@ -2185,6 +2191,8 @@ xmlport 1610 "Sales Invoice - PEPPOL BIS 3.0"
else
OnGetTotals(SourceRecRef, SalesLine, TempVATAmtLine, TempVATProductPostingGroup, ProcessedDocType);
end;
+
+ PEPPOLMgt.AddPaymentDiscountCompensation(TempVATAmtLine);
end;
local procedure FindNextInvoiceRec(Position: Integer) Found: Boolean